// https://root.cern/js/ v7.10.3
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.JSROOT = global.JSROOT || {}));
})(this, (function (exports) { 'use strict';

var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
/* eslint-disable @stylistic/js/indent */

/** @summary version id
  * @desc For the JSROOT release the string in format 'major.minor.patch' like '7.0.0' */
const version_id = '7.10.3',

/** @summary version date
  * @desc Release date in format day/month/year like '14/04/2022' */
version_date = '26/02/2026',

/** @summary version id and date
  * @desc Produced by concatenation of {@link version_id} and {@link version_date}
  * Like '7.0.0 14/04/2022' */
version = version_id + ' ' + version_date,

/** @summary Is node.js flag
  * @private */
nodejs = Boolean((typeof process === 'object') && process.versions?.node && process.versions.v8),

/** @summary internal data
  * @private */
internals = {
   /** @summary unique id counter, starts from 1 */
   id_counter: 1
},

_src = (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('jsroot.js', document.baseURI).href)),

_src_dir = '$jsrootsys';


/** @summary Check if argument is a not-null Object
  * @private */
function isObject(arg) { return arg && typeof arg === 'object'; }

/** @summary Check if argument is a Function
  * @private */
function isFunc(arg) { return typeof arg === 'function'; }

/** @summary Check if argument is a String
  * @private */
function isStr(arg) { return typeof arg === 'string'; }

/** @summary Check if object is a Promise
  * @private */
function isPromise(obj) { return isObject(obj) && isFunc(obj.then); }

/** @summary Postpone func execution and return result in promise
  * @private */
function postponePromise(func, timeout) {
   return new Promise(resolveFunc => {
      setTimeout(() => {
         const res = isFunc(func) ? func() : func;
         resolveFunc(res);
      }, timeout);
   });
}

/** @summary Provide promise in any case
  * @private */
function getPromise(obj) { return isPromise(obj) ? obj : Promise.resolve(obj); }


/** @summary Location of JSROOT modules
  * @desc Automatically detected and used to dynamically load other modules
  * @private */
exports.source_dir = '';

if (_src_dir[0] !== '$')
   exports.source_dir = _src_dir;
else if (_src && isStr(_src)) {
   let pos = _src.indexOf('modules/core.mjs');
   if (pos < 0)
      pos = _src.indexOf('build/jsroot.js');
   if (pos < 0)
      pos = _src.indexOf('build/jsroot.min.js');
   if (pos >= 0)
      exports.source_dir = _src.slice(0, pos);
   else
      internals.ignore_v6 = true;
}

if (!nodejs) {
   if (exports.source_dir)
      console.log(`Set jsroot source_dir to ${exports.source_dir}, ${version}`);
   else
      console.log(`jsroot bundle, ${version}`);
}

/** @summary Is batch mode flag
  * @private */
let batch_mode = nodejs;

/** @summary Indicates if running in batch mode */
function isBatchMode() { return batch_mode; }

/** @summary Set batch mode
  * @private */
function setBatchMode(on) { batch_mode = Boolean(on); }

/** @summary Indicates if running inside Node.js */
function isNodeJs() { return nodejs; }

/** @summary atob function in all environments
  * @private */
const atob_func = isNodeJs() ? str => Buffer.from(str, 'base64').toString('latin1') : globalThis?.atob,

/** @summary btoa function in all environments
  * @private */
btoa_func = isNodeJs() ? str => Buffer.from(str, 'latin1').toString('base64') : globalThis?.btoa,

/** @summary browser detection flags
  * @private */
browser = { isFirefox: true, isSafari: false, isChrome: false, isWin: false, touches: false, screenWidth: 1200 };

if ((typeof document !== 'undefined') && (typeof window !== 'undefined') && (typeof navigator !== 'undefined')) {
   navigator.userAgentData?.brands?.forEach(item => {
      if (item.brand === 'HeadlessChrome') {
         browser.isChromeHeadless = true;
         browser.chromeVersion = parseInt(item.version);
      } else if (item.brand === 'Chromium') {
         browser.isChrome = true;
         browser.chromeVersion = parseInt(item.version);
      }
   });

   if (browser.chromeVersion) {
      browser.isFirefox = false;
      browser.isWin = navigator.userAgentData.platform === 'Windows';
   } else {
      browser.isFirefox = navigator.userAgent.indexOf('Firefox') >= 0;
      browser.isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;
      browser.isChrome = Boolean(window.chrome);
      browser.isChromeHeadless = navigator.userAgent.indexOf('HeadlessChrome') >= 0;
      browser.chromeVersion = (browser.isChrome || browser.isChromeHeadless) ? (navigator.userAgent.indexOf('Chrom') > 0 ? parseInt(navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/)[1]) : 134) : 0;
      browser.isWin = navigator.userAgent.indexOf('Windows') >= 0;
   }
   browser.android = /android/i.test(navigator.userAgent);
   browser.touches = ('ontouchend' in document); // identify if touch events are supported
   browser.screenWidth = window.screen?.width ?? 1200;
}

/** @summary Check if prototype string match to array (typed on untyped)
  * @return {Number} 0 - not array, 1 - regular array, 2 - typed array
  * @private */
function isArrayProto(proto) {
    if ((proto.length < 14) || proto.indexOf('[object '))
      return 0;
    const p = proto.indexOf('Array]');
    if ((p < 0) || (p !== proto.length - 6))
      return 0;
    // plain array has only '[object Array]', typed array type name inside
    return proto.length === 14 ? 1 : 2;
}

/** @desc Specialized JSROOT constants, used in {@link settings}
  * @namespace */
const constants$1 = {
   /** @summary Kind of 3D rendering, used for {@link settings.Render3D}
     * @namespace */
   Render3D: {
      /** @summary Default 3D rendering, normally WebGL, if not supported - SVG */
      Default: 0,
      /** @summary Use normal WebGL rendering and place as interactive Canvas element on HTML page */
      WebGL: 1,
      /** @summary Use WebGL rendering, but convert into svg image, not interactive */
      WebGLImage: 2,
      /** @summary Use SVG rendering, slow, imprecise and not interactive, not recommended */
      SVG: 3,
      /** @summary Disable renderer, used for three.js model creation, only for internal use recommended */
      None: 4,
      fromString(s) {
         if ((s === 'webgl') || (s === 'gl'))
            return this.WebGL;
         if (s === 'img')
            return this.WebGLImage;
         if (s === 'svg')
            return this.SVG;
         if (s === 'none')
            return this.None;
         return this.Default;
      }
   },
   /** @summary Way to embed 3D into SVG, used for {@link settings.Embed3D}
     * @namespace */
   Embed3D: {
      /** @summary Do not embed 3D drawing, use complete space */
      NoEmbed: -1,
      /** @summary Default embedding mode - on Firefox and latest Chrome is real ```Embed```, on all other ```Overlay``` */
      Default: 0,
      /** @summary WebGL canvas not inserted into SVG, but just overlay. The only way how old Chrome browser can be used */
      Overlay: 1,
      /** @summary Really embed WebGL Canvas into SVG */
      Embed: 2,
      /** @summary Embedding, but when SVG rendering or SVG image conversion is used */
      EmbedSVG: 3,
      /** @summary Convert string values into number  */
      fromString(s) {
         if (s === 'embed')
            return this.Embed;
         if (s === 'overlay')
            return this.Overlay;
         return this.Default;
      }
   },
   /** @summary How to use latex in text drawing, used for {@link settings.Latex}
     * @namespace */
   Latex: {
      /** @summary do not use Latex at all for text drawing */
      Off: 0,
      /** @summary convert only known latex symbols */
      Symbols: 1,
      /** @summary normal latex processing with svg */
      Normal: 2,
      /** @summary use MathJax for complex cases, otherwise simple SVG text */
      MathJax: 3,
      /** @summary always use MathJax for text rendering */
      AlwaysMathJax: 4,
      /** @summary Convert string values into number */
      fromString(s) {
         if (!s || !isStr(s))
            return this.Normal;
         switch (s) {
            case 'off': return this.Off;
            case 'symbols': return this.Symbols;
            case 'normal':
            case 'latex':
            case 'exp':
            case 'experimental': return this.Normal;
            case 'MathJax':
            case 'mathjax':
            case 'math': return this.MathJax;
            case 'AlwaysMathJax':
            case 'alwaysmath':
            case 'alwaysmathjax': return this.AlwaysMathJax;
         }
         const code = parseInt(s);
         return (Number.isInteger(code) && (code >= this.Off) && (code <= this.AlwaysMathJax)) ? code : this.Normal;
      }
   }
},

/** @desc Global JSROOT settings
  * @namespace */
settings = {
   /** @summary Render of 3D drawing methods, see {@link constants.Render3D} for possible values */
   Render3D: constants$1.Render3D.Default,
   /** @summary 3D drawing methods in batch mode, see {@link constants.Render3D} for possible values */
   Render3DBatch: constants$1.Render3D.Default,
   /** @summary Way to embed 3D drawing in SVG, see {@link constants.Embed3D} for possible values */
   Embed3D: constants$1.Embed3D.Default,
   /** @summary Default canvas width */
   CanvasWidth: 1200,
   /** @summary Default canvas height */
   CanvasHeight: 800,
   /** @summary Canvas pixel ratio between viewport and display, default 1 */
   CanvasScale: 1,
   /** @summary Enable or disable tooltips, default on */
   Tooltip: !nodejs,
   /** @summary Time in msec for appearance of tooltips, 0 - no animation */
   TooltipAnimation: 500,
   /** @summary Enables context menu usage */
   ContextMenu: !nodejs,
   /** @summary Global zooming flag, enable/disable any kind of interactive zooming */
   Zooming: !nodejs,
   /** @summary Zooming with the mouse events */
   ZoomMouse: !nodejs,
   /** @summary Zooming with mouse wheel */
   ZoomWheel: !nodejs,
   /** @summary Zooming on touch devices */
   ZoomTouch: !nodejs,
   /** @summary Enables move and resize of elements like statistic box, title, pave, colz  */
   MoveResize: !browser.touches && !nodejs,
   /** @summary Configures keyboard key press handling
     * @desc Can be disabled to prevent keys handling in complex HTML layouts
     * @default true */
   HandleKeys: !nodejs,
   /** @summary enables drag and drop functionality */
   DragAndDrop: !nodejs,
   /** @summary Interactive dragging of TGraph points */
   DragGraphs: true,
   /** @summary Value of user-select style in interactive drawings */
   UserSelect: 'none',
   /** @summary Show progress box, can be false, true or 'modal' */
   ProgressBox: !nodejs,
   /** @summary Show additional tool buttons on the canvas, false - disabled, true - enabled, 'popup' - only toggle button */
   ToolBar: nodejs ? false : 'popup',
   /** @summary Position of toolbar 'left' left-bottom corner on canvas, 'right' - right-bottom corner on canvas, opposite on sub-pads */
   ToolBarSide: 'left',
   /** @summary display tool bar vertical (default false) */
   ToolBarVert: false,
   /** @summary if drawing inside particular div can be enlarged on full window */
   CanEnlarge: true,
   /** @summary if frame position can be adjusted to let show axis or colz labels */
   CanAdjustFrame: false,
   /** @summary calculation of text size consumes time and can be skipped to improve performance (but with side effects on text adjustments) */
   ApproxTextSize: false,
   /** @summary Load symbol.ttf font to display greek labels. By default font file not loaded and unicode is used */
   LoadSymbolTtf: false,
   /** @summary Histogram drawing optimization: 0 - disabled, 1 - only for large (>5000 1d bins, >50 2d bins) histograms, 2 - always */
   OptimizeDraw: 1,
   /** @summary Automatically create stats box, default on */
   AutoStat: true,
   /** @summary Default frame position in NFC
     * @deprecated Use gStyle.fPad[Left/Right/Top/Bottom]Margin values instead, to be removed in v8 */
   FrameNDC: {},
   /** @summary size of pad, where many features will be deactivated like text draw or zooming  */
   SmallPad: { width: 150, height: 100 },
   /** @summary Default color palette id  */
   Palette: 57,
   /** @summary Configures Latex usage, see {@link constants.Latex} for possible values */
   Latex: constants$1.Latex.Normal,
   /** @summary Grads per segment in TGeo spherical shapes like tube */
   GeoGradPerSegm: 6,
   /** @summary Enables faces compression after creation of composite shape  */
   GeoCompressComp: true,
   /** @summary if true, ignore all kind of URL options in the browser URL */
   IgnoreUrlOptions: false,
   /** @summary how many items shown on one level of hierarchy */
   HierarchyLimit: 250,
   /** @summary default display kind for the hierarchy painter */
   DislpayKind: 'simple',
   /** @summary default left area width in browser layout */
   BrowserWidth: 250,
   /** @summary custom format for all X values, when not specified {@link gStyle.fStatFormat} is used */
   XValuesFormat: undefined,
   /** @summary custom format for all Y values, when not specified {@link gStyle.fStatFormat} is used */
   YValuesFormat: undefined,
   /** @summary custom format for all Z values, when not specified {@link gStyle.fStatFormat} is used */
   ZValuesFormat: undefined,
   /** @summary Let detect and solve problem when server returns wrong Content-Length header
     * @desc See [jsroot#189]{@link https://github.com/root-project/jsroot/issues/189} for more info
     * Can be enabled by adding 'wrong_http_response' parameter to URL when using JSROOT UI
     * @default false */
   HandleWrongHttpResponse: false,
   /** @summary Tweak browser caching with stamp URL parameter
     * @desc When specified, extra URL parameter like ```?stamp=unique_value``` append to each files loaded
     * In such case browser will be forced to load file content disregards of browser or server cache settings
     * Can be disabled by providing &usestamp=false in URL or via Settings/Files sub-menu
     * Disabled by default on node.js, enabled in the web browsers */
   UseStamp: !nodejs,
   /** @summary Maximal number of bytes ranges in http 'Range' header
     * @desc Some http server has limitations for number of bytes ranges therefore let change maximal number via setting
     * @default 200 */
   MaxRanges: 200,
   /** @summary Number of bytes requested once by TTree::Draw processing
     * @desc TTree can be very large and data from baskets read by portion specified by this variable
     * @default 1e6 */
   TreeReadBunchSize: 1e6,
   /** @summary File read timeout in ms
     * @desc Configures timeout for each http operation for reading ROOT files
     * @default 0 */
   FilesTimeout: 0,
   /** @summary Default remap object for files loading
     * @desc Allows to retry files reading if original URL fails
     * @private */
   FilesRemap: { 'https://root.cern/': 'https://root-eos.web.cern.ch/' },
   /** @summary Configure xhr.withCredentials = true when submitting http requests from JSROOT */
   WithCredentials: false,
   /** @summary Skip streamer infos from the GUI */
   SkipStreamerInfos: false,
   /** @summary Show only last cycle for objects in TFile */
   OnlyLastCycle: false,
   /** @summary Configures dark mode for the GUI */
   DarkMode: false,
   /** @summary Prefer to use saved points in TF1/TF2, avoids eval() and Function() when possible */
   PreferSavedPoints: false,
   /** @summary Angle in degree for axis labels tilt when available space is not enough */
   AxisTiltAngle: 25,
   /** @summary Strip axis labels trailing 0 or replace 10^0 by 1 */
   StripAxisLabels: true,
   /** @summary If true exclude (cut off) axis labels which may exceed graphical range, also axis name can be specified */
   CutAxisLabels: false,
   /** @summary Draw TF1 by default as curve or line */
   FuncAsCurve: false,
   /** @summary Time zone used for date/time display, local by default, can be 'UTC' or 'Europe/Berlin' or any other valid value */
   TimeZone: '',
   /** @summary Page URL which will be used to show item in new tab, jsroot main dir used by default */
   NewTabUrl: '',
   /** @summary Extra parameters which will be append to the url when item shown in new tab */
   NewTabUrlPars: '',
   /** @summary Export different settings in output URL */
   NewTabUrlExportSettings: false,
   /** @summary Enable more debug output, also via 'WebGui.Debug: yes' on ROOT side */
   Debug: false
},

/** @namespace
  * @summary Instance of TStyle object like in ROOT
  * @desc Includes default draw styles, can be changed after loading of core.mjs
  * or can be load from the file providing style=itemname in the URL
  * See [TStyle docu]{@link https://root.cern/doc/master/classTStyle.html} 'Private attributes' section for more detailed info about each value */
gStyle = {
   fName: 'Modern',
   /** @summary Default log x scale */
   fOptLogx: 0,
   /** @summary Default log y scale */
   fOptLogy: 0,
   /** @summary Default log z scale */
   fOptLogz: 0,
   /** @summary Show date on canvas */
   fOptDate: 0,
   /** @summary Show file name on canvas */
   fOptFile: 0,
   /** @summary X position of date on canvas */
   fDateX: 0.01,
   /** @summary Y position of date on canvas */
   fDateY: 0.01,
   /** @summary Draw histogram title */
   fOptTitle: 1,
   /** @summary Canvas fill color */
   fCanvasColor: 0,
   /** @summary Pad fill color */
   fPadColor: 0,
   /** @summary Pad bottom margin */
   fPadBottomMargin: 0.1,
   /** @summary Pad top margin */
   fPadTopMargin: 0.1,
   /** @summary Pad left margin */
   fPadLeftMargin: 0.1,
   /** @summary Pad right margin */
   fPadRightMargin: 0.1,
   /** @summary TPad.fGridx default value */
   fPadGridX: false,
   /** @summary TPad.fGridy default value */
   fPadGridY: false,
   /** @summary TPad.fTickx default value */
   fPadTickX: 0,
   /** @summary TPad.fTicky default value */
   fPadTickY: 0,
   /** @summary Pad border size */
   fPadBorderSize: 2,
   /** @summary Pad border mode */
   fPadBorderMode: 0,
   /** @summary Canvas border size */
   fCanvasBorderSize: 2,
   /** @summary Canvas border mode */
   fCanvasBorderMode: 0,
   /** @summary fill color for stat box */
   fStatColor: 0,
   /** @summary fill style for stat box */
   fStatStyle: 1000,
   /** @summary text color in stat box */
   fStatTextColor: 1,
   /** @summary text size in stat box */
   fStatFontSize: 0,
   /** @summary stat text font */
   fStatFont: 42,
   /** @summary Stat border size */
   fStatBorderSize: 1,
   /** @summary Printing format for stats */
   fStatFormat: '6.4g',
   /** @summary Stat box X position - top right corner */
   fStatX: 0.98,
   /** @summary Stat box Y position - top right corner */
   fStatY: 0.935,
   /** @summary Stat box width */
   fStatW: 0.2,
   /** @summary Stat box height */
   fStatH: 0.16,
   /** @summary Title text align */
   fTitleAlign: 23,
   /** @summary Title fill color */
   fTitleColor: 0,
   /** @summary Title text color */
   fTitleTextColor: 1,
   /** @summary Title border size */
   fTitleBorderSize: 0,
   /** @summary Title text font */
   fTitleFont: 42,
   /** @summary Title font size */
   fTitleFontSize: 0.05,
   /** @summary Title fill style */
   fTitleStyle: 0,
   /** @summary X position of top left corner of title box */
   fTitleX: 0.5,
   /** @summary Y position of top left corner of title box  */
   fTitleY: 0.995,
   /** @summary Width of title box */
   fTitleW: 0,
   /** @summary Height of title box */
   fTitleH: 0,
   /** @summary Printing format for fit parameters */
   fFitFormat: '5.4g',
   /** @summary Default optstat value */
   fOptStat: 1111,
   /** @summary Default optfit value */
   fOptFit: 0,
   /** @summary Default number of colors in contour */
   fNumberContours: 20,
   /** @summary Grids color */
   fGridColor: 0,
   /** @summary Grids line style */
   fGridStyle: 3,
   /** @summary Grids line width */
   fGridWidth: 1,
   /** @summary Frame fill color */
   fFrameFillColor: 0,
   /** @summary Frame fill style */
   fFrameFillStyle: 1001,
   /** @summary Frame line color */
   fFrameLineColor: 1,
   /** @summary Frame line width */
   fFrameLineWidth: 1,
   /** @summary Frame line style */
   fFrameLineStyle: 1,
   /** @summary Frame border size */
   fFrameBorderSize: 1,
   /** @summary Frame border mode */
   fFrameBorderMode: 0,
   /** @summary size in pixels of end error for E1 draw options */
   fEndErrorSize: 2,
   /** @summary X size of the error marks for the histogram drawings */
   fErrorX: 0.5,
   /** @summary when true, BAR and LEGO drawing using base = 0  */
   fHistMinimumZero: false,
   /** @summary Margin between histogram's top and pad's top */
   fHistTopMargin: 0.05,
   /** @summary Histogram fill color */
   fHistFillColor: 0,
   /** @summary Histogram fill style */
   fHistFillStyle: 1001,
   /** @summary Histogram line color */
   fHistLineColor: 602,
   /** @summary Histogram line style */
   fHistLineStyle: 1,
   /** @summary Histogram line width */
   fHistLineWidth: 1,
   /** @summary format for bin content */
   fPaintTextFormat: 'g',
   /** @summary default time offset, UTC time at 01/01/95   */
   fTimeOffset: 788918400,
   /** @summary Legend border size */
   fLegendBorderSize: 1,
   /** @summary Legend text font */
   fLegendFont: 42,
   /** @summary Legend font size */
   fLegendTextSize: 0,
   /** @summary Legend fill color */
   fLegendFillColor: 0,
   /** @summary Legend fill style */
   fLegendFillStyle: 1001,
   /** @summary Hatches line width in fill styles  */
   fHatchesLineWidth: 1,
   fHatchesSpacing: 1,
   fCandleWhiskerRange: 1.0,
   fCandleBoxRange: 0.5,
   fCandleScaled: false,
   fViolinScaled: true,
   fCandleCircleLineWidth: 1,
   fCandleCrossLineWidth: 1,
   fOrthoCamera: false,
   fXAxisExpXOffset: 0,
   fXAxisExpYOffset: 0,
   fYAxisExpXOffset: 0,
   fYAxisExpYOffset: 0,
   fAxisMaxDigits: 5,
   fStripDecimals: true,
   /** @summary Width of bar for graphs */
   fBarWidth: 1,
   /** @summary Offset of bar for graphs */
   fBarOffset: 0
};

/** @summary Method returns current document in use
  * @private */
function getDocument() {
   if (nodejs)
      return internals.nodejs_document;
   if (typeof document !== 'undefined')
      return document;
   if (typeof window === 'object')
      return window.document;
   return undefined;
}

/** @summary Ensure global JSROOT and v6 support methods
  * @private */
exports._ensureJSROOT = null;


/** @summary Generate mask for given bit
  * @param {number} n bit number
  * @return {Number} produced mask
  * @private */
function BIT(n) { return 1 << n; }

/** @summary Make deep clone of the object, including all sub-objects
  * @return {object} cloned object
  * @private */
function clone(src, map, nofunc) {
   if (!src)
      return null;

   if (!map)
      map = { obj: [], clones: [], nofunc };
   else {
      const i = map.obj.indexOf(src);
      if (i >= 0)
         return map.clones[i];
   }

   const arr_kind = isArrayProto(Object.prototype.toString.apply(src));

   // process normal array
   if (arr_kind === 1) {
      const tgt = [];
      map.obj.push(src);
      map.clones.push(tgt);
      for (let i = 0; i < src.length; ++i)
         tgt.push(isObject(src[i]) ? clone(src[i], map) : src[i]);
      return tgt;
   }

   // process typed array
   if (arr_kind === 2) {
      const tgt = [];
      map.obj.push(src);
      map.clones.push(tgt);
      for (let i = 0; i < src.length; ++i)
         tgt.push(src[i]);

      return tgt;
   }

   const tgt = {};
   map.obj.push(src);
   map.clones.push(tgt);

   for (const k in src) {
      if (isObject(src[k]))
         tgt[k] = clone(src[k], map);
      else if (!map.nofunc || !isFunc(src[k]))
         tgt[k] = src[k];
   }

   return tgt;
}

// used very often - keep shortcut
const extend$1 = Object.assign;

/** @summary Adds specific methods to the object.
  * @desc JSROOT implements some basic methods for different ROOT classes.
  * @function
  * @param {object} obj - object where methods are assigned
  * @param {string} [typename] - optional typename, if not specified, obj._typename will be used
  * @private */
exports.addMethods = null;

/** @summary Should be used to parse JSON string produced with TBufferJSON class
  * @desc Replace all references inside object like { "$ref": "1" }
  * @param {object|string} json  object where references will be replaced
  * @return {object} parsed object */
function parse$1(json) {
   if (!json)
      return null;

   const obj = isStr(json) ? JSON.parse(json) : json, map = [];
   let newfmt;

   const unref_value = value => {
      if ((value === null) || (value === undefined))
         return;

      if (isStr(value)) {
         if (newfmt || (value.length < 6) || value.indexOf('$ref:'))
            return;
         const ref = parseInt(value.slice(5));
         if (!Number.isInteger(ref) || (ref < 0) || (ref >= map.length))
            return;
         newfmt = false;
         return map[ref];
      }

      if (typeof value !== 'object')
         return;

      const proto = Object.prototype.toString.apply(value);

      // scan array - it can contain other objects
      if (isArrayProto(proto) > 0) {
          for (let i = 0; i < value.length; ++i) {
             const res = unref_value(value[i]);
             if (res !== undefined)
               value[i] = res;
          }
          return;
      }

      const ks = Object.keys(value), len = ks.length;

      if ((newfmt !== false) && (len === 1) && (ks[0] === '$ref')) {
         const ref = parseInt(value.$ref);
         if (!Number.isInteger(ref) || (ref < 0) || (ref >= map.length))
            return;
         newfmt = true;
         return map[ref];
      }

      if ((newfmt !== false) && (len > 1) && (ks[0] === '$arr') && (ks[1] === 'len')) {
         // this is ROOT-coded array
         let arr;
         switch (value.$arr) {
            case 'Int8': arr = new Int8Array(value.len); break;
            case 'Uint8': arr = new Uint8Array(value.len); break;
            case 'Int16': arr = new Int16Array(value.len); break;
            case 'Uint16': arr = new Uint16Array(value.len); break;
            case 'Int32': arr = new Int32Array(value.len); break;
            case 'Uint32': arr = new Uint32Array(value.len); break;
            case 'Float32': arr = new Float32Array(value.len); break;
            case 'Int64':
            case 'Uint64':
            case 'Float64': arr = new Float64Array(value.len); break;
            default: arr = new Array(value.len);
         }

         arr.fill((value.$arr === 'Bool') ? false : 0);

         if (value.b !== undefined) {
            // base64 coding
            const buf = atob_func(value.b);
            if (arr.buffer) {
               const dv = new DataView(arr.buffer, value.o || 0),
                     blen = Math.min(buf.length, dv.byteLength);
               for (let k = 0; k < blen; ++k)
                  dv.setUint8(k, buf.charCodeAt(k));
            } else
               throw new Error('base64 coding supported only for native arrays with binary data');
         } else {
            // compressed coding
            let nkey = 2, p = 0;
            while (nkey < len) {
               if (ks[nkey][0] === 'p')
                  p = value[ks[nkey++]]; // position
               if (ks[nkey][0] !== 'v')
                  throw new Error(`Unexpected member ${ks[nkey]} in array decoding`);
               const v = value[ks[nkey++]]; // value
               if (typeof v === 'object') {
                  for (let k = 0; k < v.length; ++k)
                     arr[p++] = v[k];
               } else {
                  arr[p++] = v;
                  if ((nkey < len) && (ks[nkey][0] === 'n')) {
                     let cnt = value[ks[nkey++]]; // counter
                     while (--cnt)
                        arr[p++] = v;
                  }
               }
            }
         }

         return arr;
      }

      if ((newfmt !== false) && (len === 3) && (ks[0] === '$pair') && (ks[1] === 'first') && (ks[2] === 'second')) {
         newfmt = true;
         const f1 = unref_value(value.first),
               s1 = unref_value(value.second);
         if (f1 !== undefined)
            value.first = f1;
         if (s1 !== undefined)
            value.second = s1;
         value._typename = value.$pair;
         delete value.$pair;
         return; // pair object is not counted in the objects map
      }

     // prevent endless loop
     if (map.indexOf(value) >= 0)
      return;

      // add object to object map
      map.push(value);

      // add methods to all objects, where _typename is specified
      if (value._typename)
         exports.addMethods(value);

      for (let k = 0; k < len; ++k) {
         const i = ks[k], res = unref_value(value[i]);
         if (res !== undefined)
            value[i] = res;
      }
   };

   unref_value(obj);

   return obj;
}

/** @summary Parse response from multi.json request
  * @desc Method should be used to parse JSON code, produced by multi.json request of THttpServer
  * @param {string} json string to parse
  * @return {Array} array of parsed elements */
function parseMulti(json) {
   if (!json)
      return null;
   const arr = JSON.parse(json);
   if (arr?.length) {
      for (let i = 0; i < arr.length; ++i)
         arr[i] = parse$1(arr[i]);
   }
   return arr;
}

/** @summary Method converts JavaScript object into ROOT-like JSON
  * @desc When performed properly, JSON can be used in [TBufferJSON::fromJSON()]{@link https://root.cern/doc/master/classTBufferJSON.html}
  * method to read data back with C++. Or one can again parse json with {@link parse} function
  * @param {object} obj - JavaScript object to convert
  * @param {number} [spacing] - optional line spacing in JSON
  * @return {string} produced JSON code
  * @example
  * import { openFile, draw, toJSON } from 'https://root.cern/js/latest/modules/main.mjs';
  * let file = await openFile('https://root.cern/js/files/hsimple.root');
  * let obj = await file.readObject('hpxpy;1');
  * obj.fTitle = 'New histogram title';
  * let json = toJSON(obj); */
function toJSON(obj, spacing) {
   if (!isObject(obj))
      return '';

   const map = [], // map of stored objects
   copy_value = value => {
      if (isFunc(value))
         return undefined;

      if ((value === undefined) || (value === null) || !isObject(value))
         return value;

      // typed array need to be converted into normal array, otherwise looks strange
      if (isArrayProto(Object.prototype.toString.apply(value)) > 0) {
         const arr = new Array(value.length);
         for (let i = 0; i < value.length; ++i)
            arr[i] = copy_value(value[i]);
         return arr;
      }

      // this is how reference is code
      const refid = map.indexOf(value);
      if (refid >= 0)
         return { $ref: refid };

      const ks = Object.keys(value), len = ks.length, tgt = {};

      if ((len === 3) && (ks[0] === '$pair') && (ks[1] === 'first') && (ks[2] === 'second')) {
         // special handling of pair objects which does not included into objects map
         tgt.$pair = value.$pair;
         tgt.first = copy_value(value.first);
         tgt.second = copy_value(value.second);
         return tgt;
      }

      map.push(value);

      for (let k = 0; k < len; ++k) {
         const name = ks[k];
         if (name && (name[0] !== '$'))
            tgt[name] = copy_value(value[name]);
      }

      return tgt;
   },
   tgt = copy_value(obj);

   return JSON.stringify(tgt, null, spacing);
}

/** @summary decodes URL options after '?' mark
  * @desc Following options supported ?opt1&opt2=3
  * @param {string} [url] URL string with options, document.URL will be used when not specified
  * @return {Object} with ```.has(opt)``` and ```.get(opt,dflt)``` methods
  * @example
  * import { decodeUrl } from 'https://root.cern/js/latest/modules/core.mjs';
  * let d = decodeUrl('any?opt1&op2=3');
  * console.log(`Has opt1 ${d.has('opt1')}`);     // true
  * console.log(`Get opt1 ${d.get('opt1')}`);     // ''
  * console.log(`Get opt2 ${d.get('opt2')}`);     // '3'
  * console.log(`Get opt3 ${d.get('opt3','-')}`); // '-' */
function decodeUrl(url) {
   const res = {
      opts: {},
      has(opt) { return this.opts[opt] !== undefined; },
      get(opt, dflt) { return this.opts[opt] ?? dflt; }
   };

   if (!url || !isStr(url)) {
      if (settings.IgnoreUrlOptions || (typeof document === 'undefined'))
         return res;
      url = document.URL;
   }
   res.url = url;

   const p1 = url.indexOf('?');
   if (p1 < 0)
      return res;
   url = decodeURI(url.slice(p1 + 1));

   while (url) {
      // try to correctly handle quotes in the URL
      let pos = 0, nq = 0, eq = -1, firstq = -1;
      while ((pos < url.length) && (nq || ((url[pos] !== '&') && (url[pos] !== '#')))) {
         switch (url[pos]) {
            case '\'':
               if (nq >= 0)
                  nq = (nq + 1) % 2;
               if (firstq < 0)
                  firstq = pos;
               break;
            case '"':
               if (nq <= 0)
                  nq = (nq - 1) % 2;
               if (firstq < 0)
                  firstq = pos;
               break;
            case '=':
               if ((firstq < 0) && (eq < 0))
                  eq = pos;
               break;
         }
         pos++;
      }

      if ((eq < 0) && (firstq < 0))
         res.opts[url.slice(0, pos)] = '';
      else if (eq > 0) {
         let val = url.slice(eq + 1, pos);
         if (((val[0] === '\'') || (val[0] === '"')) && (val.at(0) === val.at(-1)))
            val = val.slice(1, val.length - 1);
         res.opts[url.slice(0, eq)] = val;
      }

      if ((pos >= url.length) || (url[pos] === '#'))
         break;

      url = url.slice(pos + 1);
   }

   return res;
}

/** @summary Find function with given name
  * @private */
function findFunction(name) {
   if (isFunc(name))
      return name;
   if (!isStr(name))
      return null;
   const names = name.split('.');
   let elem = globalThis;

   for (let n = 0; elem && (n < names.length); ++n)
      elem = elem[names[n]];

   return isFunc(elem) ? elem : null;
}

/** @summary Method to create http request, without promise can be used only in browser environment
  * @private */
function createHttpRequest(url, kind, user_accept_callback, user_reject_callback, use_promise) {
   function configureXhr(xhr) {
      xhr.http_callback = isFunc(user_accept_callback) ? user_accept_callback.bind(xhr) : () => {};
      xhr.error_callback = isFunc(user_reject_callback) ? user_reject_callback.bind(xhr) : function(err) {
         console.warn(err.message);
         this.http_callback(null);
      }.bind(xhr);

      if (!kind)
         kind = 'buf';

      let method = 'GET', is_async = true;
      const p = kind.indexOf(';sync');
      if (p > 0) {
         kind = kind.slice(0, p);
         is_async = false;
      }
      switch (kind) {
         case 'head':
            method = 'HEAD';
            break;
         case 'posttext':
            method = 'POST';
            kind = 'text';
            break;
         case 'postbuf':
            method = 'POST';
            kind = 'buf';
            break;
         case 'post':
         case 'multi':
            method = 'POST';
            break;
      }

      xhr.kind = kind;

      if (settings.WithCredentials)
         xhr.withCredentials = true;

      if (settings.HandleWrongHttpResponse && (method === 'GET') && isFunc(xhr.addEventListener)) {
         xhr.addEventListener('progress', function(oEvent) {
            if (oEvent.lengthComputable && this.expected_size && (oEvent.loaded > this.expected_size)) {
               this.did_abort = true;
               this.abort();
               this.error_callback(Error(`Server sends more bytes ${oEvent.loaded} than expected ${this.expected_size}. Abort I/O operation`), 598);
            }
         }.bind(xhr));
      }

      xhr.onreadystatechange = function() {
         if (this.did_abort)
            return;

         if ((this.readyState === 2) && this.expected_size) {
            const len = parseInt(this.getResponseHeader('Content-Length'));
            if (Number.isInteger(len) && (len > this.expected_size) && !settings.HandleWrongHttpResponse) {
               this.did_abort = 'large';
               this.abort();
               return this.error_callback(Error(`Server response size ${len} larger than expected ${this.expected_size}. Abort I/O operation`), 599);
            }
         }

         if (this.readyState !== 4)
            return;

         if ((this.status !== 200) && (this.status !== 206) && !browser.qt6 &&
             // in these special cases browsers not always set status
             !((this.status === 0) && ((url.indexOf('file://') === 0) || (url.indexOf('blob:') === 0))))
               return this.error_callback(Error(`Fail to load url ${url}`), this.status);

         if (this.nodejs_checkzip && (this.getResponseHeader('content-encoding') === 'gzip')) {
            // special handling of gzip JSON objects in Node.js
            return Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(handle => {
                const res = handle.unzipSync(Buffer.from(this.response)),
                      obj = JSON.parse(res); // zlib returns Buffer, use JSON to parse it
               return this.http_callback(parse$1(obj));
            });
         }

         switch (this.kind) {
            case 'xml': return this.http_callback(this.responseXML);
            case 'text': return this.http_callback(this.responseText);
            case 'object': return this.http_callback(parse$1(this.responseText));
            case 'multi': return this.http_callback(parseMulti(this.responseText));
            case 'head': return this.http_callback(this);
         }

         // if no response type is supported, return as text (most probably, will fail)
         if (this.responseType === undefined)
            return this.http_callback(this.responseText);

         if ((this.kind === 'bin') && ('byteLength' in this.response)) {
            // if string representation in requested - provide it
            const u8Arr = new Uint8Array(this.response);
            let filecontent = '';
            for (let i = 0; i < u8Arr.length; ++i)
               filecontent += String.fromCharCode(u8Arr[i]);
            return this.http_callback(filecontent);
         }

         this.http_callback(this.response);
      };

      xhr.open(method, url, is_async);

      if ((kind === 'bin') || (kind === 'buf'))
         xhr.responseType = 'arraybuffer';

      if (nodejs && (method === 'GET') && (kind === 'object') && (url.indexOf('.json.gz') > 0)) {
         xhr.nodejs_checkzip = true;
         xhr.responseType = 'arraybuffer';
      }

      return xhr;
   }

   if (isNodeJs()) {
      if (!use_promise)
         throw Error('Not allowed to create http requests in node.js without promise');
      return Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(h => configureXhr(new h.default()));
   }

   const xhr = configureXhr(new XMLHttpRequest());
   return use_promise ? Promise.resolve(xhr) : xhr;
}

/** @summary Submit asynchronous http request
  * @desc Following requests kind can be specified:
  *    - 'bin' - abstract binary data, result as string
  *    - 'buf' - abstract binary data, result as ArrayBuffer (default)
  *    - 'text' - returns req.responseText
  *    - 'object' - returns parse(req.responseText)
  *    - 'multi' - returns correctly parsed multi.json request
  *    - 'xml' - returns req.responseXML
  *    - 'head' - returns request itself, uses 'HEAD' request method
  *    - 'post' - creates post request, submits req.send(post_data)
  *    - 'postbuf' - creates post request, expects binary data as response
  * @param {string} url - URL for the request
  * @param {string} kind - kind of requested data
  * @param {string} [post_data] - data submitted with post kind of request
  * @return {Promise} Promise for requested data, result type depends from the kind
  * @example
  * import { httpRequest } from 'https://root.cern/js/latest/modules/core.mjs';
  * httpRequest('https://root.cern/js/files/thstack.json.gz', 'object')
  *       .then(obj => console.log(`Get object of type ${obj._typename}`))
  *       .catch(err => console.error(err.message)); */
async function httpRequest(url, kind, post_data) {
   return new Promise((resolve, reject) => {
      createHttpRequest(url, kind, resolve, reject, true).then(xhr => xhr.send(post_data || null));
   });
}

/** @summary Inject javascript code
  * @desc Replacement for eval
  * @return {Promise} when code is injected
  * @private */
async function injectCode(code) {
   if (nodejs) {
      let name, fs;
      return Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(tmp => {
         name = tmp.tmpNameSync() + '.js';
         return Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; });
      }).then(_fs => {
         fs = _fs;
         fs.writeFileSync(name, code);
         return import(/* webpackIgnore: true */ 'file://' + name);
      }).finally(() => fs.unlinkSync(name));
   }

   if (typeof document !== 'undefined') {
      // check if code already loaded - to avoid duplication
      const scripts = document.getElementsByTagName('script');
      for (let n = 0; n < scripts.length; ++n) {
         if (scripts[n].innerText === code)
            return true;
      }

      // try to detect if code includes import and must be treated as module
      const is_v6 = code.indexOf('JSROOT.require') >= 0,
            is_mjs = !is_v6 && (code.indexOf('import {') > 0) && (code.indexOf('} from \'') > 0),
            is_batch = !is_v6 && !is_mjs && (code.indexOf('JSROOT.ObjectPainter') >= 0),
            promise = (is_v6 ? exports._ensureJSROOT() : Promise.resolve(true));

      if (is_batch && !globalThis.JSROOT)
         globalThis.JSROOT = internals.jsroot;

      return promise.then(() => {
         const element = document.createElement('script');
         element.setAttribute('type', is_mjs ? 'module' : 'text/javascript');
         element.innerText = code;
         document.head.appendChild(element);
         // while onload event not fired, just postpone resolve
         return isBatchMode() ? true : postponePromise(true, 10);
      });
   }

   return false;
}

/** @summary Load ES6 modules
  * @param {String} arg - single URL or array of URLs
  * @return {Promise} */
async function loadModules(arg) {
   if (isStr(arg))
      arg = arg.split(';');
   if (!arg.length)
      return true;
   return import(/* webpackIgnore: true */ arg.shift()).then(() => loadModules(arg));
}

/** @summary Load script or CSS file into the browser
  * @param {String} url - script or css file URL (or array, in this case they all loaded sequentially)
  * @return {Promise} */
async function loadScript(url) {
   if (!url)
      return true;

   if (isStr(url) && (url.indexOf(';') >= 0))
      url = url.split(';');

   if (!isStr(url)) {
      const scripts = url, loadNext = () => {
         return !scripts.length ? true : loadScript(scripts.shift()).then(loadNext, loadNext);
      };
      return loadNext();
   }

   if (url.indexOf('$$$') === 0) {
      url = url.slice(3);
      if ((url.indexOf('style/') === 0) && (url.indexOf('.css') < 0))
         url += '.css';
      url = exports.source_dir + url;
   }

   const isstyle = url.indexOf('.css') > 0;

   if (nodejs) {
      if (isstyle)
         return null;
      if ((url.indexOf('http:') === 0) || (url.indexOf('https:') === 0))
         return httpRequest(url, 'text').then(code => injectCode(code));

      // local files, read and use it
      if (url.indexOf('./') === 0)
         return Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(fs => injectCode(fs.readFileSync(url)));

      return import(/* webpackIgnore: true */ url);
   }

   const match_url = src => {
      if (src === url)
         return true;
      const indx = src.indexOf(url);
      return (indx > 0) && (indx + url.length === src.length) && (src[indx - 1] === '/');
   };

   if (isstyle) {
      const styles = document.getElementsByTagName('link');
      for (let n = 0; n < styles.length; ++n) {
         if (!styles[n].href || (styles[n].type !== 'text/css') || (styles[n].rel !== 'stylesheet'))
            continue;
         if (match_url(styles[n].href))
            return true;
      }
   } else {
      const scripts = document.getElementsByTagName('script');
      for (let n = 0; n < scripts.length; ++n) {
         if (match_url(scripts[n].src))
            return true;
      }
   }

   let element;
   if (isstyle) {
      element = document.createElement('link');
      element.setAttribute('rel', 'stylesheet');
      element.setAttribute('type', 'text/css');
      element.setAttribute('href', url);
   } else {
      element = document.createElement('script');
      element.setAttribute('type', 'text/javascript');
      element.setAttribute('src', url);
   }

   return new Promise((resolveFunc, rejectFunc) => {
      element.onload = () => resolveFunc(true);
      element.onerror = () => {
         element.remove();
         rejectFunc(Error(`Fail to load ${url}`));
      };
      document.head.appendChild(element);
   });
}

exports._ensureJSROOT = async function() {
   const pr = globalThis.JSROOT ? Promise.resolve(true) : loadScript(exports.source_dir + 'scripts/JSRoot.core.js');

   return pr.then(() => {
      if (globalThis.JSROOT?._complete_loading)
         return globalThis.JSROOT._complete_loading();
   }).then(() => globalThis.JSROOT);
};


const prROOT = 'ROOT.', clTObject = 'TObject', clTNamed = 'TNamed', clTString = 'TString', clTObjString = 'TObjString',
      clTKey = 'TKey', clTFile = 'TFile', clTTree = 'TTree',
      clTList = 'TList', clTHashList = 'THashList', clTMap = 'TMap', clTObjArray = 'TObjArray', clTClonesArray = 'TClonesArray',
      clTAttLine = 'TAttLine', clTAttFill = 'TAttFill', clTAttMarker = 'TAttMarker', clTAttText = 'TAttText',
      clTHStack = 'THStack', clTGraph = 'TGraph', clTMultiGraph = 'TMultiGraph', clTCutG = 'TCutG',
      clTGraph2DErrors = 'TGraph2DErrors', clTGraph2DAsymmErrors = 'TGraph2DAsymmErrors',
      clTGraphPolar = 'TGraphPolar', clTGraphPolargram = 'TGraphPolargram', clTGraphTime = 'TGraphTime',
      clTPave = 'TPave', clTPaveText = 'TPaveText', clTPaveStats = 'TPaveStats', clTPavesText = 'TPavesText',
      clTPaveLabel = 'TPaveLabel', clTPaveClass = 'TPaveClass', clTDiamond = 'TDiamond',
      clTLegend = 'TLegend', clTLegendEntry = 'TLegendEntry',
      clTPaletteAxis = 'TPaletteAxis', clTImagePalette = 'TImagePalette',
      clTText = 'TText', clTLink = 'TLink', clTLatex = 'TLatex', clTMathText = 'TMathText', clTAnnotation = 'TAnnotation',
      clTColor = 'TColor', clTLine = 'TLine', clTMarker = 'TMarker', clTBox = 'TBox', clTPolyLine = 'TPolyLine',
      clTPolyLine3D = 'TPolyLine3D', clTPolyMarker3D = 'TPolyMarker3D',
      clTAttPad = 'TAttPad', clTPad = 'TPad', clTCanvas = 'TCanvas', clTFrame = 'TFrame', clTAttCanvas = 'TAttCanvas',
      clTGaxis = 'TGaxis', clTAttAxis = 'TAttAxis', clTAxis = 'TAxis', clTStyle = 'TStyle',
      clTH1 = 'TH1', clTH1I = 'TH1I', clTH1F = 'TH1F', clTH1D = 'TH1D', clTH2 = 'TH2', clTH2I = 'TH2I', clTH2F = 'TH2F', clTH2D = 'TH2D', clTH3 = 'TH3',
      clTF1 = 'TF1', clTF12 = 'TF12', clTF2 = 'TF2', clTF3 = 'TF3', clTProfile = 'TProfile', clTProfile2D = 'TProfile2D', clTProfile3D = 'TProfile3D',
      clTGeoVolume = 'TGeoVolume', clTGeoNode = 'TGeoNode', clTGeoNodeMatrix = 'TGeoNodeMatrix',
      nsROOT = 'ROOT::', nsREX = nsROOT + 'Experimental::', nsSVG = 'http://www.w3.org/2000/svg',
      kNoZoom = -1111, kNoStats = BIT(9), kInspect = 'inspect', kTitle = 'title',
      urlClassPrefix = 'https://root.cern/doc/master/class';


/** @summary Create some ROOT classes
  * @desc Supported classes: `TObject`, `TNamed`, `TList`, `TAxis`, `TLine`, `TText`, `TLatex`, `TPad`, `TCanvas`
  * @param {string} typename - ROOT class name
  * @example
  * import { create } from 'https://root.cern/js/latest/modules/core.mjs';
  * let obj = create('TNamed');
  * obj.fName = 'name';
  * obj.fTitle = 'title'; */
function create$1(typename, target) {
   const obj = target || {};

   switch (typename) {
      case clTObject:
          extend$1(obj, { fUniqueID: 0, fBits: 0 });
          break;
      case clTNamed:
         extend$1(obj, { fUniqueID: 0, fBits: 0, fName: '', fTitle: '' });
         break;
      case clTList:
      case clTHashList:
         extend$1(obj, { name: typename, arr: [], opt: [] });
         break;
      case clTObjArray:
         extend$1(obj, { name: typename, arr: [] });
         break;
      case clTAttAxis:
         extend$1(obj, { fNdivisions: 510, fAxisColor: 1,
                       fLabelColor: 1, fLabelFont: 42, fLabelOffset: 0.005, fLabelSize: 0.035, fTickLength: 0.03,
                       fTitleOffset: 1, fTitleSize: 0.035, fTitleColor: 1, fTitleFont: 42 });
         break;
      case clTAxis:
         create$1(clTNamed, obj);
         create$1(clTAttAxis, obj);
         extend$1(obj, { fNbins: 1, fXmin: 0, fXmax: 1, fXbins: [], fFirst: 0, fLast: 0,
                       fBits2: 0, fTimeDisplay: false, fTimeFormat: '', fLabels: null, fModLabs: null });
         break;
      case clTAttLine:
         extend$1(obj, { fLineColor: 1, fLineStyle: 1, fLineWidth: 1 });
         break;
      case clTAttFill:
         extend$1(obj, { fFillColor: 0, fFillStyle: 0 });
         break;
      case clTAttMarker:
         extend$1(obj, { fMarkerColor: 1, fMarkerStyle: 1, fMarkerSize: 1 });
         break;
      case clTLine:
         create$1(clTObject, obj);
         create$1(clTAttLine, obj);
         extend$1(obj, { fX1: 0, fX2: 1, fY1: 0, fY2: 1 });
         break;
      case clTMarker:
         create$1(clTObject, obj);
         create$1(clTAttMarker, obj);
         extend$1(obj, { fX: 0, fY: 0 });
         break;
      case clTBox:
         create$1(clTObject, obj);
         create$1(clTAttLine, obj);
         create$1(clTAttFill, obj);
         extend$1(obj, { fX1: 0, fX2: 1, fY1: 0, fY2: 1 });
         break;
      case clTPave:
         create$1(clTBox, obj);
         extend$1(obj, { fX1NDC: 0, fY1NDC: 0, fX2NDC: 0, fY2NDC: 0,
                       fBorderSize: 0, fInit: 1, fShadowColor: 1,
                       fCornerRadius: 0, fOption: 'brNDC', fName: '' });
         break;
      case clTAttText:
         extend$1(obj, { fTextAngle: 0, fTextSize: 0, fTextAlign: 22, fTextColor: 1, fTextFont: 42 });
         break;
      case clTPaveText:
         create$1(clTPave, obj);
         create$1(clTAttText, obj);
         extend$1(obj, { fLabel: '', fLongest: 27, fMargin: 0.05, fLines: create$1(clTList) });
         break;
      case clTPaveStats:
         create$1(clTPaveText, obj);
         extend$1(obj, { fFillColor: gStyle.fStatColor, fFillStyle: gStyle.fStatStyle,
                       fTextFont: gStyle.fStatFont, fTextSize: gStyle.fStatFontSize, fTextColor: gStyle.fStatTextColor,
                       fBorderSize: gStyle.fStatBorderSize,
                       fOptFit: gStyle.fOptFit, fOptStat: gStyle.fOptStat, fFitFormat: gStyle.fFitFormat, fStatFormat: gStyle.fStatFormat, fParent: null });
         break;
      case clTLegend:
         create$1(clTPave, obj);
         create$1(clTAttText, obj);
         extend$1(obj, { fColumnSeparation: 0, fEntrySeparation: 0.1, fMargin: 0.25, fNColumns: 1, fPrimitives: create$1(clTList), fName: clTPave,
                       fBorderSize: gStyle.fLegendBorderSize, fTextFont: gStyle.fLegendFont, fTextSize: gStyle.fLegendTextSize,
                       fFillColor: gStyle.fLegendFillColor, fFillStyle: gStyle.fLegendFillStyle });
         break;
      case clTPaletteAxis:
         create$1(clTPave, obj);
         extend$1(obj, { fAxis: create$1(clTGaxis), fH: null, fName: clTPave });
         break;
      case clTLegendEntry:
         create$1(clTObject, obj);
         create$1(clTAttText, obj);
         create$1(clTAttLine, obj);
         create$1(clTAttFill, obj);
         create$1(clTAttMarker, obj);
         extend$1(obj, { fLabel: '', fObject: null, fOption: '', fTextAlign: 0, fTextColor: 0, fTextFont: 0 });
         break;
      case clTText:
         create$1(clTNamed, obj);
         create$1(clTAttText, obj);
         extend$1(obj, { fX: 0, fY: 0 });
         break;
      case clTLatex:
         create$1(clTText, obj);
         create$1(clTAttLine, obj);
         extend$1(obj, { fLimitFactorSize: 3, fOriginSize: 0.04 });
         break;
      case clTObjString:
         create$1(clTObject, obj);
         extend$1(obj, { fString: '' });
         break;
      case clTH1:
         create$1(clTNamed, obj);
         create$1(clTAttLine, obj);
         create$1(clTAttFill, obj);
         create$1(clTAttMarker, obj);
         extend$1(obj, { fBits: 8, fNcells: 0,
                       fXaxis: create$1(clTAxis), fYaxis: create$1(clTAxis), fZaxis: create$1(clTAxis),
                       fFillColor: gStyle.fHistFillColor, fFillStyle: gStyle.fHistFillStyle,
                       fLineColor: gStyle.fHistLineColor, fLineStyle: gStyle.fHistLineStyle, fLineWidth: gStyle.fHistLineWidth,
                       fBarOffset: 0, fBarWidth: 1000, fEntries: 0,
                       fTsumw: 0, fTsumw2: 0, fTsumwx: 0, fTsumwx2: 0,
                       fMaximum: kNoZoom, fMinimum: kNoZoom, fNormFactor: 0, fContour: [],
                       fSumw2: [], fOption: '', fFunctions: create$1(clTList),
                       fBufferSize: 0, fBuffer: [], fBinStatErrOpt: 0, fStatOverflows: 2 });
         break;
      case clTH1I:
      case clTH1D:
      case 'TH1L64':
      case 'TH1F':
      case 'TH1S':
      case 'TH1C':
         create$1(clTH1, obj);
         obj.fArray = [];
         break;
      case clTH2:
         create$1(clTH1, obj);
         extend$1(obj, { fScalefactor: 1, fTsumwy: 0, fTsumwy2: 0, fTsumwxy: 0 });
         break;
      case clTH2I:
      case 'TH2L64':
      case clTH2F:
      case 'TH2D':
      case 'TH2S':
      case 'TH2C':
         create$1(clTH2, obj);
         obj.fArray = [];
         break;
      case clTH3:
         create$1(clTH1, obj);
         extend$1(obj, { fTsumwy: 0, fTsumwy2: 0, fTsumwz: 0, fTsumwz2: 0, fTsumwxy: 0, fTsumwxz: 0, fTsumwyz: 0 });
         break;
      case 'TH3I':
      case 'TH3L64':
      case 'TH3F':
      case 'TH3D':
      case 'TH3S':
      case 'TH3C':
         create$1(clTH3, obj);
         obj.fArray = [];
         break;
      case clTHStack:
         create$1(clTNamed, obj);
         extend$1(obj, { fHists: create$1(clTList), fHistogram: null, fMaximum: kNoZoom, fMinimum: kNoZoom });
         break;
      case clTGraph:
         create$1(clTNamed, obj);
         create$1(clTAttLine, obj);
         create$1(clTAttFill, obj);
         create$1(clTAttMarker, obj);
         extend$1(obj, { fFunctions: create$1(clTList), fHistogram: null,
                       fMaxSize: 0, fMaximum: kNoZoom, fMinimum: kNoZoom, fNpoints: 0, fX: [], fY: [] });
         break;
      case 'TGraphAsymmErrors':
         create$1(clTGraph, obj);
         extend$1(obj, { fEXlow: [], fEXhigh: [], fEYlow: [], fEYhigh: [] });
         break;
      case clTMultiGraph:
         create$1(clTNamed, obj);
         extend$1(obj, { fFunctions: create$1(clTList), fGraphs: create$1(clTList),
                       fHistogram: null, fMaximum: kNoZoom, fMinimum: kNoZoom });
         break;
      case clTGraphPolargram:
         create$1(clTNamed, obj);
         create$1(clTAttText, obj);
         create$1(clTAttLine, obj);
         extend$1(obj, { fRadian: false, fDegree: false, fGrad: false, fPolarLabelColor: 1, fRadialLabelColor: 1,
                       fAxisAngle: 0, fPolarOffset: 0.04, fPolarTextSize: 0.04, fRadialOffset: 0.025, fRadialTextSize: 0.035,
                       fRwrmin: 0, fRwrmax: 1, fRwtmin: 0, fRwtmax: 2 * Math.PI, fTickpolarSize: 0.02,
                       fPolarLabelFont: 62, fRadialLabelFont: 62, fCutRadial: 0, fNdivRad: 508, fNdivPol: 508 });
         break;
      case clTPolyLine:
         create$1(clTObject, obj);
         create$1(clTAttLine, obj);
         create$1(clTAttFill, obj);
         extend$1(obj, { fLastPoint: -1, fN: 0, fOption: '', fX: null, fY: null });
         break;
      case clTGaxis:
         create$1(clTLine, obj);
         create$1(clTAttText, obj);
         extend$1(obj, { fChopt: '', fFunctionName: '', fGridLength: 0,
                       fLabelColor: 1, fLabelFont: 42, fLabelOffset: 0.005, fLabelSize: 0.035,
                       fName: '', fNdiv: 12, fTickSize: 0.02, fTimeFormat: '',
                       fTitle: '', fTitleOffset: 1, fTitleSize: 0.035,
                       fWmax: 100, fWmin: 0 });
         break;
      case clTAttPad:
         extend$1(obj, { fLeftMargin: gStyle.fPadLeftMargin,
                       fRightMargin: gStyle.fPadRightMargin,
                       fBottomMargin: gStyle.fPadBottomMargin,
                       fTopMargin: gStyle.fPadTopMargin,
                       fXfile: 2, fYfile: 2, fAfile: 1, fXstat: 0.99, fYstat: 0.99, fAstat: 2,
                       fFrameFillColor: gStyle.fFrameFillColor,
                       fFrameFillStyle: gStyle.fFrameFillStyle,
                       fFrameLineColor: gStyle.fFrameLineColor,
                       fFrameLineWidth: gStyle.fFrameLineWidth,
                       fFrameLineStyle: gStyle.fFrameLineStyle,
                       fFrameBorderSize: gStyle.fFrameBorderSize,
                       fFrameBorderMode: gStyle.fFrameBorderMode });
         break;
      case clTPad:
         create$1(clTObject, obj);
         create$1(clTAttLine, obj);
         create$1(clTAttFill, obj);
         create$1(clTAttPad, obj);
         extend$1(obj, { fFillColor: gStyle.fPadColor, fFillStyle: 1001,
                       fX1: 0, fY1: 0, fX2: 1, fY2: 1, fXtoAbsPixelk: 1, fXtoPixelk: 1,
                       fXtoPixel: 1, fYtoAbsPixelk: 1, fYtoPixelk: 1, fYtoPixel: 1,
                       fUtoAbsPixelk: 1, fUtoPixelk: 1, fUtoPixel: 1, fVtoAbsPixelk: 1,
                       fVtoPixelk: 1, fVtoPixel: 1, fAbsPixeltoXk: 1, fPixeltoXk: 1,
                       fPixeltoX: 1, fAbsPixeltoYk: 1, fPixeltoYk: 1, fPixeltoY: 1,
                       fXlowNDC: 0, fYlowNDC: 0, fXUpNDC: 0, fYUpNDC: 0, fWNDC: 1, fHNDC: 1,
                       fAbsXlowNDC: 0, fAbsYlowNDC: 0, fAbsWNDC: 1, fAbsHNDC: 1,
                       fUxmin: 0, fUymin: 0, fUxmax: 0, fUymax: 0, fTheta: 30, fPhi: 30, fAspectRatio: 0,
                       fNumber: 0, fLogx: gStyle.fOptLogx, fLogy: gStyle.fOptLogy, fLogz: gStyle.fOptLogz,
                       fTickx: gStyle.fPadTickX, fTicky: gStyle.fPadTickY,
                       fPadPaint: 0, fCrosshair: 0, fCrosshairPos: 0, fBorderSize: gStyle.fPadBorderSize,
                       fBorderMode: gStyle.fPadBorderMode, fModified: false,
                       fGridx: gStyle.fPadGridX, fGridy: gStyle.fPadGridY,
                       fAbsCoord: false, fEditable: true, fFixedAspectRatio: false,
                       fPrimitives: create$1(clTList), fExecs: null,
                       fName: 'pad', fTitle: 'canvas' });
         break;
      case clTAttCanvas:
         extend$1(obj, { fXBetween: 2, fYBetween: 2, fTitleFromTop: 1.2,
                       fXdate: 0.2, fYdate: 0.3, fAdate: 1 });
         break;
      case clTCanvas:
         create$1(clTPad, obj);
         extend$1(obj, { fFillColor: gStyle.fCanvasColor, fFillStyle: 1001,
                       fNumPaletteColor: 0, fNextPaletteColor: 0, fDISPLAY: '$DISPLAY',
                       fDoubleBuffer: 0, fRetained: true, fXsizeUser: 0,
                       fYsizeUser: 0, fXsizeReal: 20, fYsizeReal: 10,
                       fWindowTopX: 0, fWindowTopY: 0, fWindowWidth: 0, fWindowHeight: 0,
                       fBorderSize: gStyle.fCanvasBorderSize, fBorderMode: gStyle.fCanvasBorderMode,
                       fCw: settings.CanvasWidth, fCh: settings.CanvasHeight, fCatt: create$1(clTAttCanvas),
                       kMoveOpaque: true, kResizeOpaque: true, fHighLightColor: 5,
                       fBatch: true, kShowEventStatus: false, kAutoExec: true, kMenuBar: true });
         break;
      case clTGeoVolume:
         create$1(clTNamed, obj);
         create$1(clTAttLine, obj);
         create$1(clTAttFill, obj);
         extend$1(obj, { fGeoAtt: 0, fFinder: null, fMedium: null, fNodes: null, fNtotal: 0, fNumber: 0, fRefCount: 0, fShape: null, fVoxels: null });
         break;
      case clTGeoNode:
         create$1(clTNamed, obj);
         extend$1(obj, { fGeoAtt: 0, fMother: null, fNovlp: 0, fNumber: 0, fOverlaps: null, fVolume: null });
         break;
      case clTGeoNodeMatrix:
         create$1(clTGeoNode, obj);
         extend$1(obj, { fMatrix: null });
         break;
      case 'TGeoTrack':
         create$1(clTObject, obj);
         create$1(clTAttLine, obj);
         create$1(clTAttMarker, obj);
         extend$1(obj, { fGeoAtt: 0, fNpoints: 0, fPoints: [] });
         break;
      case clTPolyLine3D:
         create$1(clTObject, obj);
         create$1(clTAttLine, obj);
         extend$1(obj, { fLastPoint: -1, fN: 0, fOption: '', fP: [] });
         break;
      case clTPolyMarker3D:
         create$1(clTObject, obj);
         create$1(clTAttMarker, obj);
         extend$1(obj, { fLastPoint: -1, fN: 0, fName: '', fOption: '', fP: [] });
         break;
   }

   obj._typename = typename;
   exports.addMethods(obj, typename);
   return obj;
}

/** @summary Create histogram object of specified type
  * @param {string} typename - histogram typename like 'TH1I' or 'TH2F'
  * @param {number} nbinsx - number of bins on X-axis
  * @param {number} [nbinsy] - number of bins on Y-axis (for 2D/3D histograms)
  * @param {number} [nbinsz] - number of bins on Z-axis (for 3D histograms)
  * @return {Object} created histogram object
  * @example
  * import { createHistogram } from 'https://root.cern/js/latest/modules/core.mjs';
  * let h1 = createHistogram('TH1I', 20);
  * h1.fName = 'Hist1';
  * h1.fTitle = 'Histogram title';
  * h1.fXaxis.fTitle = 'xaxis';
  * h1.fYaxis.fTitle = 'yaxis';
  * h1.fXaxis.fLabelSize = 0.02; */
function createHistogram(typename, nbinsx, nbinsy, nbinsz) {
   const histo = create$1(typename);
   if (!histo.fXaxis || !histo.fYaxis || !histo.fZaxis)
      return null;
   histo.fName = 'hist';
   histo.fTitle = 'title';
   if (nbinsx)
      extend$1(histo.fXaxis, { fNbins: nbinsx, fXmin: 0, fXmax: nbinsx });
   if (nbinsy)
      extend$1(histo.fYaxis, { fNbins: nbinsy, fXmin: 0, fXmax: nbinsy });
   if (nbinsz)
      extend$1(histo.fZaxis, { fNbins: nbinsz, fXmin: 0, fXmax: nbinsz });
   switch (parseInt(typename[2])) {
      case 1:
         if (nbinsx)
            histo.fNcells = nbinsx + 2;
         break;
      case 2:
         if (nbinsx && nbinsy)
            histo.fNcells = (nbinsx + 2) * (nbinsy + 2);
         break;
      case 3:
         if (nbinsx && nbinsy && nbinsz)
            histo.fNcells = (nbinsx + 2) * (nbinsy + 2) * (nbinsz + 2);
         break;
   }
   if (histo.fNcells > 0) {
      switch (typename[3]) {
         case 'C':
            histo.fArray = new Int8Array(histo.fNcells);
            break;
         case 'S':
            histo.fArray = new Int16Array(histo.fNcells);
            break;
         case 'I':
            histo.fArray = new Int32Array(histo.fNcells);
            break;
         case 'F':
            histo.fArray = new Float32Array(histo.fNcells);
            break;
         case 'L':
         case 'D':
            histo.fArray = new Float64Array(histo.fNcells);
            break;
         default:
            histo.fArray = new Array(histo.fNcells);
      }
      histo.fArray.fill(0);
   }
   return histo;
}

/** @summary Set histogram title
 * @desc Title may include axes titles, provided with ';' symbol like "Title;x;y;z" */

function setHistogramTitle(histo, title) {
   if (!histo || !isStr(title))
      return;
   if (title.indexOf(';') < 0)
      histo.fTitle = title;
   else {
      const arr = title.split(';');
      histo.fTitle = arr[0];
      if (arr.length > 1)
         histo.fXaxis.fTitle = arr[1];
      if (arr.length > 2)
         histo.fYaxis.fTitle = arr[2];
      if (arr.length > 3)
         histo.fZaxis.fTitle = arr[3];
   }
}


/** @summary Creates TPolyLine object
  * @param {number} npoints - number of points
  * @param {boolean} [use_int32] - use Int32Array type for points, default is Float32Array */
function createTPolyLine(npoints, use_int32) {
   const poly = create$1(clTPolyLine);
   if (npoints) {
      poly.fN = npoints;
      if (use_int32) {
         poly.fX = new Int32Array(npoints);
         poly.fY = new Int32Array(npoints);
      } else {
         poly.fX = new Float32Array(npoints);
         poly.fY = new Float32Array(npoints);
      }
   }
   return poly;
}

/** @summary Creates TGraph object
  * @param {number} npoints - number of points in TGraph
  * @param {array} [xpts] - array with X coordinates
  * @param {array} [ypts] - array with Y coordinates */
function createTGraph(npoints, xpts, ypts) {
   const graph = extend$1(create$1(clTGraph), { fBits: 0x408, fName: 'graph', fTitle: 'title' });

   if (npoints > 0) {
      graph.fMaxSize = graph.fNpoints = npoints;

      const usex = isObject(xpts) && (xpts.length === npoints),
            usey = isObject(ypts) && (ypts.length === npoints);

      for (let i = 0; i < npoints; ++i) {
         graph.fX.push(usex ? xpts[i] : i / npoints);
         graph.fY.push(usey ? ypts[i] : i / npoints);
      }
   }

   return graph;
}

/** @summary Creates THStack object
  * @desc As arguments one could specify any number of histograms objects
  * @example
  * import { createHistogram, createTHStack } from 'https://root.cern/js/latest/modules/core.mjs';
  * let nbinsx = 20;
  * let h1 = createHistogram('TH1F', nbinsx);
  * let h2 = createHistogram('TH1F', nbinsx);
  * let h3 = createHistogram('TH1F', nbinsx);
  * let stack = createTHStack(h1, h2, h3); */
function createTHStack(...args) {
   const stack = create$1(clTHStack);
   for (let i = 0; i < args.length; ++i)
      stack.fHists.Add(args[i], '');
   return stack;
}

/** @summary Creates TMultiGraph object
  * @desc As arguments one could specify any number of TGraph objects
  * @example
  * import { createTGraph, createTMultiGraph } from 'https://root.cern/js/latest/modules/core.mjs';
  * let gr1 = createTGraph(100);
  * let gr2 = createTGraph(100);
  * let gr3 = createTGraph(100);
  * let mgr = createTMultiGraph(gr1, gr2, gr3); */
function createTMultiGraph(...args) {
   const mgraph = create$1(clTMultiGraph);
   for (let i = 0; i < args.length; ++i)
      mgraph.fGraphs.Add(args[i], '');
   return mgraph;
}

/** @summary variable used to keep methods for known classes
  * @private */
const methodsCache = {};

/** @summary Returns methods for given typename
  * @private */
function getMethods(typename, obj) {
   let m = methodsCache[typename];
   const has_methods = (m !== undefined);
   if (!has_methods)
      m = {};

   // Due to binary I/O such TObject methods may not be set for derived classes
   // Therefore when methods requested for given object, check also that basic methods are there
   if ((typename === clTObject) || (typename === clTNamed) || (obj?.fBits !== undefined)) {
      if (typeof m.TestBit === 'undefined') {
         m.TestBit = function(f) { return Boolean(this.fBits & f); };
         m.InvertBit = function(f) { this.fBits ^= f & 0xffffff; };
         m.SetBit = function(f, on = true) { this.fBits = on ? this.fBits | f : this.fBits & ~f; };
      }
   }

   if (has_methods)
      return m;

   if ((typename === clTList) || (typename === clTHashList)) {
      m.Clear = function() {
         this.arr = [];
         this.opt = [];
      };
      m.Add = function(elem, opt) {
         this.arr.push(elem);
         this.opt.push(isStr(opt) ? opt : '');
      };
      m.AddFirst = function(elem, opt) {
         this.arr.unshift(elem);
         this.opt.unshift(isStr(opt) ? opt : '');
      };
      m.RemoveAt = function(indx) {
         this.arr.splice(indx, 1);
         this.opt.splice(indx, 1);
      };
   }

   if ((typename === clTPaveText) || (typename === clTPaveStats)) {
      m.AddText = function(txt) {
         const line = create$1(clTLatex);
         line.fTitle = txt;
         line.fTextAlign = 0;
         line.fTextColor = 0;
         line.fTextFont = 0;
         line.fTextSize = 0;
         this.fLines.Add(line);
      };
      m.Clear = function() {
         this.fLines.Clear();
      };
   }

   if ((typename.indexOf(clTF1) === 0) || (typename === clTF12) || (typename === clTF2) || (typename === clTF3)) {
      m.addFormula = function(formula) {
         if (formula) {
            if (this.formulas === undefined)
               this.formulas = [];
            this.formulas.push(formula);
         }
      };
      m.GetParName = function(n) {
         if (this.fParams?.fParNames)
            return this.fParams.fParNames[n];
         if (this.fFormula?.fParams) {
            for (let k = 0, arr = this.fFormula.fParams; k < arr.length; ++k) {
               if (arr[k].second === n)
                  return arr[k].first;
            }
         }
         return (this.fNames && this.fNames[n]) ? this.fNames[n] : `p${n}`;
      };
      m.GetParValue = function(n) {
         if (this.fParams?.fParameters)
            return this.fParams.fParameters[n];
         if (this.fFormula?.fClingParameters)
            return this.fFormula.fClingParameters[n];
         return this.fParams ? this.fParams[n] : undefined;
      };
      m.GetParError = function(n) {
         return this.fParErrors ? this.fParErrors[n] : undefined;
      };
      m.GetNumPars = function() {
         return this.fNpar;
      };
   }

   if (((typename.indexOf(clTGraph) === 0) || (typename === clTCutG)) && (typename !== clTGraphPolargram) && (typename !== clTGraphTime)) {
      // check if point inside figure specified by the TGraph
      m.IsInside = function(xp, yp) {
         const x = this.fX, y = this.fY;
         let i = 0, j = this.fNpoints - 1, oddNodes = false;

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

         return oddNodes;
      };
   }

   if (typename.indexOf(clTH1) === 0 || typename.indexOf(clTH2) === 0 || typename.indexOf(clTH3) === 0) {
      m.getBinError = function(bin) {
         //   -*-*-*-*-*Return value of error associated to bin number bin*-*-*-*-*
         //    if the sum of squares of weights has been defined (via Sumw2),
         //    this function returns the sqrt(sum of w2).
         //    otherwise it returns the sqrt(contents) for this bin.
         if (bin >= this.fNcells)
            bin = this.fNcells - 1;
         if (bin < 0)
            bin = 0;
         if (bin < this.fSumw2.length)
            return Math.sqrt(this.fSumw2[bin]);
         return Math.sqrt(Math.abs(this.fArray[bin]));
      };
      m.setBinContent = function(bin, content) {
         // Set bin content - only trivial case, without expansion
         this.fEntries++;
         this.fTsumw = 0;
         if ((bin >= 0) && (bin < this.fArray.length))
            this.fArray[bin] = content;
      };
   }

   if (typename.indexOf(clTH1) === 0) {
      m.getBin = function(x) { return x; };
      m.getBinContent = function(bin) { return this.fArray[bin]; };
      m.Fill = function(x, weight) {
         const a = this.fXaxis,
               bin = Math.max(0, 1 + Math.min(a.fNbins, Math.floor((x - a.fXmin) / (a.fXmax - a.fXmin) * a.fNbins)));
         this.fArray[bin] += weight ?? 1;
         this.fEntries++;
      };
   }

   if (typename.indexOf(clTH2) === 0) {
      m.getBin = function(x, y) { return (x + (this.fXaxis.fNbins + 2) * y); };
      m.getBinContent = function(x, y) { return this.fArray[this.getBin(x, y)]; };
      m.Fill = function(x, y, weight) {
         const a1 = this.fXaxis, a2 = this.fYaxis,
               bin1 = Math.max(0, 1 + Math.min(a1.fNbins, Math.floor((x - a1.fXmin) / (a1.fXmax - a1.fXmin) * a1.fNbins))),
               bin2 = Math.max(0, 1 + Math.min(a2.fNbins, Math.floor((y - a2.fXmin) / (a2.fXmax - a2.fXmin) * a2.fNbins)));
         this.fArray[bin1 + (a1.fNbins + 2) * bin2] += weight ?? 1;
         this.fEntries++;
      };
   }

   if (typename.indexOf(clTH3) === 0) {
      m.getBin = function(x, y, z) { return (x + (this.fXaxis.fNbins + 2) * (y + (this.fYaxis.fNbins + 2) * z)); };
      m.getBinContent = function(x, y, z) { return this.fArray[this.getBin(x, y, z)]; };
      m.Fill = function(x, y, z, weight) {
         const a1 = this.fXaxis, a2 = this.fYaxis, a3 = this.fZaxis,
               bin1 = Math.max(0, 1 + Math.min(a1.fNbins, Math.floor((x - a1.fXmin) / (a1.fXmax - a1.fXmin) * a1.fNbins))),
               bin2 = Math.max(0, 1 + Math.min(a2.fNbins, Math.floor((y - a2.fXmin) / (a2.fXmax - a2.fXmin) * a2.fNbins))),
               bin3 = Math.max(0, 1 + Math.min(a3.fNbins, Math.floor((z - a3.fXmin) / (a3.fXmax - a3.fXmin) * a3.fNbins)));
         this.fArray[bin1 + (a1.fNbins + 2) * (bin2 + (a2.fNbins + 2) * bin3)] += weight ?? 1;
         this.fEntries++;
      };
   }

   if (typename === clTPad || typename === clTCanvas) {
      m.Divide = function(nx, ny, xmargin = 0.01, ymargin = 0.01, color = 0) {
         if (!ny) {
            const ndiv = nx;
            if (ndiv < 2)
               return this;
            nx = ny = Math.round(Math.sqrt(ndiv));
            if (nx * ny < ndiv)
               nx += 1;
         }
         if (nx * ny < 2)
            return 0;
         this.fPrimitives.Clear();
         const dy = 1 / ny, dx = 1 / nx;
         let n = 0;
         for (let iy = 0; iy < ny; iy++) {
            const y2 = 1 - iy * dy - ymargin,
                  y1 = Math.max(0, y2 - dy + 2 * ymargin);
            if (y1 > y2)
               continue;
            for (let ix = 0; ix < nx; ix++) {
               const x1 = ix * dx + xmargin,
                     x2 = x1 + dx - 2 * xmargin;
               if (x1 > x2)
                  continue;
               n++;
               const pad = create$1(clTPad);
               pad.fName = pad.fTitle = `${this.fName}_${n}`;
               pad.fNumber = n;
               if (this._typename !== clTCanvas) {
                  pad.fAbsWNDC = (x2 - x1) * this.fAbsWNDC;
                  pad.fAbsHNDC = (y2 - y1) * this.fAbsHNDC;
                  pad.fAbsXlowNDC = this.fAbsXlowNDC + x1 * this.fAbsWNDC;
                  pad.fAbsYlowNDC = this.fAbsYlowNDC + y1 * this.fAbsHNDC;
               } else {
                  pad.fAbsWNDC = x2 - x1;
                  pad.fAbsHNDC = y2 - y1;
                  pad.fAbsXlowNDC = x1;
                  pad.fAbsYlowNDC = y1;
               }

               pad.fFillColor = color || this.fFillColor;

               this.fPrimitives.Add(pad);
            }
         }
         return nx * ny;
      };
      m.GetPad = function(number) {
         return this.fPrimitives.arr.find(elem => { return elem._typename === clTPad && elem.fNumber === number; });
      };
   }

   if (typename.indexOf(clTProfile) === 0) {
      if (typename === clTProfile3D) {
         m.getBin = function(x, y, z) { return (x + (this.fXaxis.fNbins + 2) * (y + (this.fYaxis.fNbins + 2) * z)); };
         m.getBinContent = function(x, y, z) {
            const bin = this.getBin(x, y, z);
            if (bin < 0 || bin >= this.fNcells || this.fBinEntries[bin] < 1e-300)
               return 0;
            return this.fArray ? this.fArray[bin] / this.fBinEntries[bin] : 0;
         };
         m.getBinEntries = function(x, y, z) {
            const bin = this.getBin(x, y, z);
            return (bin < 0) || (bin >= this.fNcells) ? 0 : this.fBinEntries[bin];
         };
      } else if (typename === clTProfile2D) {
         m.getBin = function(x, y) { return (x + (this.fXaxis.fNbins + 2) * y); };
         m.getBinContent = function(x, y) {
            const bin = this.getBin(x, y);
            if (bin < 0 || bin >= this.fNcells || this.fBinEntries[bin] < 1e-300)
               return 0;
            return this.fArray ? this.fArray[bin] / this.fBinEntries[bin] : 0;
         };
         m.getBinEntries = function(x, y) {
            const bin = this.getBin(x, y);
            return (bin < 0 || bin >= this.fNcells) ? 0 : this.fBinEntries[bin];
         };
      } else {
         m.getBin = function(x) { return x; };
         m.getBinContent = function(bin) {
            if (bin < 0 || bin >= this.fNcells || this.fBinEntries[bin] < 1e-300)
               return 0;
            return this.fArray ? this.fArray[bin] / this.fBinEntries[bin] : 0;
         };
         m.getBinEntries = function(bin) {
            return (bin < 0) || (bin >= this.fNcells) ? 0 : this.fBinEntries[bin];
         };
      }
      m.getBinEffectiveEntries = function(bin) {
         if (bin < 0 || bin >= this.fNcells)
            return 0;
         const sumOfWeights = this.fBinEntries[bin];
         if (!this.fBinSumw2 || this.fBinSumw2.length !== this.fNcells)
            // this can happen  when reading an old file
            return sumOfWeights;
         const sumOfWeightsSquare = this.fBinSumw2[bin];
         return (sumOfWeightsSquare > 0) ? sumOfWeights * sumOfWeights / sumOfWeightsSquare : 0;
      };
      m.getBinError = function(bin) {
         if (bin < 0 || bin >= this.fNcells)
            return 0;
         const cont = this.fArray[bin],               // sum of bin w *y
               sum = this.fBinEntries[bin],          // sum of bin weights
               err2 = this.fSumw2[bin],               // sum of bin w * y^2
               neff = this.getBinEffectiveEntries(bin);  // (sum of w)^2 / (sum of w^2)
         if (sum < 1e-300)
            return 0;                  // for empty bins
         const EErrorType = { kERRORSPREAD: 1, kERRORSPREADI: 2, kERRORSPREADG: 3 };
         // case the values y are gaussian distributed y +/- sigma and w = 1/sigma^2
         if (this.fErrorMode === EErrorType.kERRORSPREADG)
            return 1.0 / Math.sqrt(sum);
         // compute variance in y (eprim2) and standard deviation in y (eprim)
         const contsum = cont / sum, eprim = Math.sqrt(Math.abs(err2 / sum - contsum ** 2));
         if (this.fErrorMode === EErrorType.kERRORSPREADI) {
            if (eprim)
               return eprim / Math.sqrt(neff);
            // in case content y is an integer (so each my has an error +/- 1/sqrt(12)
            // when the std(y) is zero
            return 1.0 / Math.sqrt(12 * neff);
         }
         // if approximate compute the sums (of w, wy and wy2) using all the bins
         //  when the variance in y is zero
         // case option 'S' return standard deviation in y
         if (this.fErrorMode === EErrorType.kERRORSPREAD)
            return eprim;
         // default case : fErrorMode = kERRORMEAN
         // return standard error on the mean of y
         return eprim / Math.sqrt(neff);
      };
   }

   if (typename === clTAxis) {
      m.GetBinLowEdge = function(bin) {
         if (this.fNbins <= 0)
            return 0;
         if (this.fXbins.length) {
            if ((bin > 0) && (bin <= this.fNbins + 1))
               return this.fXbins[bin - 1];
            if (bin === 0) // underflow
               return 2 * this.fXbins[0] - this.fXbins[1];
            if (bin === this.fNbins + 2) // right border of overflow bin
               return 2 * this.fXbins[bin - 2] - this.fXbins[bin - 3];
            return 0;
         }
         return this.fXmin + (bin - 1) * (this.fXmax - this.fXmin) / this.fNbins;
      };
      m.GetBinCenter = function(bin) {
         if (this.fNbins <= 0)
            return 0;
         if (this.fXbins.length) {
            if ((bin > 0) && (bin <= this.fNbins))
               return (this.fXbins[bin - 1] + this.fXbins[bin]) / 2;
            if (bin === 0) // underflow
               return 1.5 * this.fXbins[0] - 0.5 * this.fXbins[1];
            if (bin === this.fNbins + 1) // overflow
               return 1.5 * this.fXbins[bin - 1] - 0.5 * this.fXbins[bin - 2];
            return 0;
         }
         return this.fXmin + (bin - 0.5) * (this.fXmax - this.fXmin) / this.fNbins;
      };
   }

   if (typename.indexOf(nsROOT + 'Math::LorentzVector') === 0) {
      m.Px = m.X = function() { return this.fCoordinates.Px(); };
      m.Py = m.Y = function() { return this.fCoordinates.Py(); };
      m.Pz = m.Z = function() { return this.fCoordinates.Pz(); };
      m.E = m.T = function() { return this.fCoordinates.E(); };
      m.M2 = function() { return this.fCoordinates.M2(); };
      m.M = function() { return this.fCoordinates.M(); };
      m.R = m.P = function() { return this.fCoordinates.R(); };
      m.P2 = function() { return this.P() * this.P(); };
      m.Pt = m.pt = function() { return Math.sqrt(this.P2()); };
      m.Phi = m.phi = function() { return Math.atan2(this.fCoordinates.Py(), this.fCoordinates.Px()); };
      m.Eta = m.eta = function() { return Math.atanh(this.Pz() / this.P()); };
   }

   if (typename.indexOf(nsROOT + 'Math::PxPyPzE4D') === 0) {
      m.Px = m.X = function() { return this.fX; };
      m.Py = m.Y = function() { return this.fY; };
      m.Pz = m.Z = function() { return this.fZ; };
      m.E = m.T = function() { return this.fT; };
      m.P2 = function() { return this.fX ** 2 + this.fY ** 2 + this.fZ ** 2; };
      m.R = m.P = function() { return Math.sqrt(this.P2()); };
      m.Mag2 = m.M2 = function() { return this.fT ** 2 - this.fX ** 2 - this.fY ** 2 - this.fZ ** 2; };
      m.Mag = m.M = function() { return (this.M2() >= 0) ? Math.sqrt(this.M2()) : -Math.sqrt(-this.M2()); };
      m.Perp2 = m.Pt2 = function() { return this.fX ** 2 + this.fY ** 2; };
      m.Pt = m.pt = function() { return Math.sqrt(this.P2()); };
      m.Phi = m.phi = function() { return Math.atan2(this.fY, this.fX); };
      m.Eta = m.eta = function() { return Math.atanh(this.Pz / this.P()); };
   }

   methodsCache[typename] = m;
   return m;
}

exports.addMethods = function(obj, typename) {
   extend$1(obj, getMethods(typename || obj._typename, obj));
};

gStyle.fXaxis = create$1(clTAttAxis);
gStyle.fYaxis = create$1(clTAttAxis);
gStyle.fZaxis = create$1(clTAttAxis);

/** @summary Add methods for specified type.
  * @desc Will be automatically applied when decoding JSON string
  * @private */
function registerMethods(typename, m) {
   methodsCache[typename] = m;
}

/** @summary Returns true if object represents basic ROOT collections
  * @desc Checks if type is TList or TObjArray or TClonesArray or TMap or THashList
  * @param {object} lst - object to check
  * @param {string} [typename] - or just typename to check
  * @private */
function isRootCollection(lst, typename) {
   if (isObject(lst)) {
      if ((lst.$kind === clTList) || (lst.$kind === clTObjArray))
         return true;
      if (!typename)
         typename = lst._typename;
   }
   return (typename === clTList) || (typename === clTHashList) || (typename === clTMap) ||
          (typename === clTObjArray) || (typename === clTClonesArray);
}

/** @summary Return kind string for type
  * @desc Used when internal objects without clear ROOT type are used
  * For such objects abstract 'kind' is defined. Used in hpainter and draw handles
  * @private */
function getKindForType(typ) {
   return (!isStr(typ) || (typ.indexOf(nsROOT) !== 0)) ? prROOT + typ : typ;
}

/** @summary Return type name from kind string
  * @private */
function getTypeForKind(kind) {
   if (!isStr(kind))
      return null;
   if (kind.indexOf(prROOT) === 0)
      return kind.slice(prROOT.length);
   if (kind.indexOf(nsROOT) === 0)
      return kind;
   return null;
}

/** @summary Internal collection of functions potentially used by batch scripts
  * @private */
internals.jsroot = { version, source_dir: exports.source_dir, settings, gStyle, parse: parse$1, isBatchMode };

var core = /*#__PURE__*/Object.freeze({
__proto__: null,
BIT: BIT,
get _ensureJSROOT () { return exports._ensureJSROOT; },
get addMethods () { return exports.addMethods; },
atob_func: atob_func,
browser: browser,
btoa_func: btoa_func,
clTAnnotation: clTAnnotation,
clTAttCanvas: clTAttCanvas,
clTAttFill: clTAttFill,
clTAttLine: clTAttLine,
clTAttMarker: clTAttMarker,
clTAttText: clTAttText,
clTAxis: clTAxis,
clTBox: clTBox,
clTCanvas: clTCanvas,
clTClonesArray: clTClonesArray,
clTColor: clTColor,
clTCutG: clTCutG,
clTDiamond: clTDiamond,
clTF1: clTF1,
clTF12: clTF12,
clTF2: clTF2,
clTF3: clTF3,
clTFile: clTFile,
clTFrame: clTFrame,
clTGaxis: clTGaxis,
clTGeoNode: clTGeoNode,
clTGeoNodeMatrix: clTGeoNodeMatrix,
clTGeoVolume: clTGeoVolume,
clTGraph: clTGraph,
clTGraph2DAsymmErrors: clTGraph2DAsymmErrors,
clTGraph2DErrors: clTGraph2DErrors,
clTGraphPolar: clTGraphPolar,
clTGraphPolargram: clTGraphPolargram,
clTGraphTime: clTGraphTime,
clTH1: clTH1,
clTH1D: clTH1D,
clTH1F: clTH1F,
clTH1I: clTH1I,
clTH2: clTH2,
clTH2D: clTH2D,
clTH2F: clTH2F,
clTH2I: clTH2I,
clTH3: clTH3,
clTHStack: clTHStack,
clTHashList: clTHashList,
clTImagePalette: clTImagePalette,
clTKey: clTKey,
clTLatex: clTLatex,
clTLegend: clTLegend,
clTLegendEntry: clTLegendEntry,
clTLine: clTLine,
clTLink: clTLink,
clTList: clTList,
clTMap: clTMap,
clTMarker: clTMarker,
clTMathText: clTMathText,
clTMultiGraph: clTMultiGraph,
clTNamed: clTNamed,
clTObjArray: clTObjArray,
clTObjString: clTObjString,
clTObject: clTObject,
clTPad: clTPad,
clTPaletteAxis: clTPaletteAxis,
clTPave: clTPave,
clTPaveClass: clTPaveClass,
clTPaveLabel: clTPaveLabel,
clTPaveStats: clTPaveStats,
clTPaveText: clTPaveText,
clTPavesText: clTPavesText,
clTPolyLine: clTPolyLine,
clTPolyLine3D: clTPolyLine3D,
clTPolyMarker3D: clTPolyMarker3D,
clTProfile: clTProfile,
clTProfile2D: clTProfile2D,
clTProfile3D: clTProfile3D,
clTString: clTString,
clTStyle: clTStyle,
clTText: clTText,
clTTree: clTTree,
clone: clone,
constants: constants$1,
create: create$1,
createHistogram: createHistogram,
createHttpRequest: createHttpRequest,
createTGraph: createTGraph,
createTHStack: createTHStack,
createTMultiGraph: createTMultiGraph,
createTPolyLine: createTPolyLine,
decodeUrl: decodeUrl,
findFunction: findFunction,
gStyle: gStyle,
getDocument: getDocument,
getKindForType: getKindForType,
getMethods: getMethods,
getPromise: getPromise,
getTypeForKind: getTypeForKind,
httpRequest: httpRequest,
injectCode: injectCode,
internals: internals,
isArrayProto: isArrayProto,
isBatchMode: isBatchMode,
isFunc: isFunc,
isNodeJs: isNodeJs,
isObject: isObject,
isPromise: isPromise,
isRootCollection: isRootCollection,
isStr: isStr,
kInspect: kInspect,
kNoStats: kNoStats,
kNoZoom: kNoZoom,
kTitle: kTitle,
loadModules: loadModules,
loadScript: loadScript,
nsREX: nsREX,
nsROOT: nsROOT,
nsSVG: nsSVG,
parse: parse$1,
parseMulti: parseMulti,
postponePromise: postponePromise,
prROOT: prROOT,
registerMethods: registerMethods,
setBatchMode: setBatchMode,
setHistogramTitle: setHistogramTitle,
settings: settings,
get source_dir () { return exports.source_dir; },
toJSON: toJSON,
urlClassPrefix: urlClassPrefix,
version: version,
version_date: version_date,
version_id: version_id
});

// https://d3js.org v7.9.0 Copyright 2010-2021 Mike Bostock

function define(constructor, factory, prototype) {
  constructor.prototype = factory.prototype = prototype;
  prototype.constructor = constructor;
}

function extend(parent, definition) {
  var prototype = Object.create(parent.prototype);
  for (var key in definition) prototype[key] = definition[key];
  return prototype;
}

function Color$1() {}

var darker = 0.7;
var brighter = 1 / darker;

var reI = "\\s*([+-]?\\d+)\\s*",
    reN = "\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",
    reP = "\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",
    reHex = /^#([0-9a-f]{3,8})$/,
    reRgbInteger = new RegExp(`^rgb\\(${reI},${reI},${reI}\\)$`),
    reRgbPercent = new RegExp(`^rgb\\(${reP},${reP},${reP}\\)$`),
    reRgbaInteger = new RegExp(`^rgba\\(${reI},${reI},${reI},${reN}\\)$`),
    reRgbaPercent = new RegExp(`^rgba\\(${reP},${reP},${reP},${reN}\\)$`),
    reHslPercent = new RegExp(`^hsl\\(${reN},${reP},${reP}\\)$`),
    reHslaPercent = new RegExp(`^hsla\\(${reN},${reP},${reP},${reN}\\)$`);

var named = {
  aliceblue: 0xf0f8ff,
  antiquewhite: 0xfaebd7,
  aqua: 0x00ffff,
  aquamarine: 0x7fffd4,
  azure: 0xf0ffff,
  beige: 0xf5f5dc,
  bisque: 0xffe4c4,
  black: 0x000000,
  blanchedalmond: 0xffebcd,
  blue: 0x0000ff,
  blueviolet: 0x8a2be2,
  brown: 0xa52a2a,
  burlywood: 0xdeb887,
  cadetblue: 0x5f9ea0,
  chartreuse: 0x7fff00,
  chocolate: 0xd2691e,
  coral: 0xff7f50,
  cornflowerblue: 0x6495ed,
  cornsilk: 0xfff8dc,
  crimson: 0xdc143c,
  cyan: 0x00ffff,
  darkblue: 0x00008b,
  darkcyan: 0x008b8b,
  darkgoldenrod: 0xb8860b,
  darkgray: 0xa9a9a9,
  darkgreen: 0x006400,
  darkgrey: 0xa9a9a9,
  darkkhaki: 0xbdb76b,
  darkmagenta: 0x8b008b,
  darkolivegreen: 0x556b2f,
  darkorange: 0xff8c00,
  darkorchid: 0x9932cc,
  darkred: 0x8b0000,
  darksalmon: 0xe9967a,
  darkseagreen: 0x8fbc8f,
  darkslateblue: 0x483d8b,
  darkslategray: 0x2f4f4f,
  darkslategrey: 0x2f4f4f,
  darkturquoise: 0x00ced1,
  darkviolet: 0x9400d3,
  deeppink: 0xff1493,
  deepskyblue: 0x00bfff,
  dimgray: 0x696969,
  dimgrey: 0x696969,
  dodgerblue: 0x1e90ff,
  firebrick: 0xb22222,
  floralwhite: 0xfffaf0,
  forestgreen: 0x228b22,
  fuchsia: 0xff00ff,
  gainsboro: 0xdcdcdc,
  ghostwhite: 0xf8f8ff,
  gold: 0xffd700,
  goldenrod: 0xdaa520,
  gray: 0x808080,
  green: 0x008000,
  greenyellow: 0xadff2f,
  grey: 0x808080,
  honeydew: 0xf0fff0,
  hotpink: 0xff69b4,
  indianred: 0xcd5c5c,
  indigo: 0x4b0082,
  ivory: 0xfffff0,
  khaki: 0xf0e68c,
  lavender: 0xe6e6fa,
  lavenderblush: 0xfff0f5,
  lawngreen: 0x7cfc00,
  lemonchiffon: 0xfffacd,
  lightblue: 0xadd8e6,
  lightcoral: 0xf08080,
  lightcyan: 0xe0ffff,
  lightgoldenrodyellow: 0xfafad2,
  lightgray: 0xd3d3d3,
  lightgreen: 0x90ee90,
  lightgrey: 0xd3d3d3,
  lightpink: 0xffb6c1,
  lightsalmon: 0xffa07a,
  lightseagreen: 0x20b2aa,
  lightskyblue: 0x87cefa,
  lightslategray: 0x778899,
  lightslategrey: 0x778899,
  lightsteelblue: 0xb0c4de,
  lightyellow: 0xffffe0,
  lime: 0x00ff00,
  limegreen: 0x32cd32,
  linen: 0xfaf0e6,
  magenta: 0xff00ff,
  maroon: 0x800000,
  mediumaquamarine: 0x66cdaa,
  mediumblue: 0x0000cd,
  mediumorchid: 0xba55d3,
  mediumpurple: 0x9370db,
  mediumseagreen: 0x3cb371,
  mediumslateblue: 0x7b68ee,
  mediumspringgreen: 0x00fa9a,
  mediumturquoise: 0x48d1cc,
  mediumvioletred: 0xc71585,
  midnightblue: 0x191970,
  mintcream: 0xf5fffa,
  mistyrose: 0xffe4e1,
  moccasin: 0xffe4b5,
  navajowhite: 0xffdead,
  navy: 0x000080,
  oldlace: 0xfdf5e6,
  olive: 0x808000,
  olivedrab: 0x6b8e23,
  orange: 0xffa500,
  orangered: 0xff4500,
  orchid: 0xda70d6,
  palegoldenrod: 0xeee8aa,
  palegreen: 0x98fb98,
  paleturquoise: 0xafeeee,
  palevioletred: 0xdb7093,
  papayawhip: 0xffefd5,
  peachpuff: 0xffdab9,
  peru: 0xcd853f,
  pink: 0xffc0cb,
  plum: 0xdda0dd,
  powderblue: 0xb0e0e6,
  purple: 0x800080,
  rebeccapurple: 0x663399,
  red: 0xff0000,
  rosybrown: 0xbc8f8f,
  royalblue: 0x4169e1,
  saddlebrown: 0x8b4513,
  salmon: 0xfa8072,
  sandybrown: 0xf4a460,
  seagreen: 0x2e8b57,
  seashell: 0xfff5ee,
  sienna: 0xa0522d,
  silver: 0xc0c0c0,
  skyblue: 0x87ceeb,
  slateblue: 0x6a5acd,
  slategray: 0x708090,
  slategrey: 0x708090,
  snow: 0xfffafa,
  springgreen: 0x00ff7f,
  steelblue: 0x4682b4,
  tan: 0xd2b48c,
  teal: 0x008080,
  thistle: 0xd8bfd8,
  tomato: 0xff6347,
  turquoise: 0x40e0d0,
  violet: 0xee82ee,
  wheat: 0xf5deb3,
  white: 0xffffff,
  whitesmoke: 0xf5f5f5,
  yellow: 0xffff00,
  yellowgreen: 0x9acd32
};

define(Color$1, color, {
  copy(channels) {
    return Object.assign(new this.constructor, this, channels);
  },
  displayable() {
    return this.rgb().displayable();
  },
  hex: color_formatHex, // Deprecated! Use color.formatHex.
  formatHex: color_formatHex,
  formatHex8: color_formatHex8,
  formatHsl: color_formatHsl,
  formatRgb: color_formatRgb,
  toString: color_formatRgb
});

function color_formatHex() {
  return this.rgb().formatHex();
}

function color_formatHex8() {
  return this.rgb().formatHex8();
}

function color_formatHsl() {
  return hslConvert(this).formatHsl();
}

function color_formatRgb() {
  return this.rgb().formatRgb();
}

function color(format) {
  var m, l;
  format = (format + "").trim().toLowerCase();
  return (m = reHex.exec(format)) ? (l = m[1].length, m = parseInt(m[1], 16), l === 6 ? rgbn(m) // #ff0000
      : l === 3 ? new Rgb((m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), ((m & 0xf) << 4) | (m & 0xf), 1) // #f00
      : l === 8 ? rgba(m >> 24 & 0xff, m >> 16 & 0xff, m >> 8 & 0xff, (m & 0xff) / 0xff) // #ff000000
      : l === 4 ? rgba((m >> 12 & 0xf) | (m >> 8 & 0xf0), (m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), (((m & 0xf) << 4) | (m & 0xf)) / 0xff) // #f000
      : null) // invalid hex
      : (m = reRgbInteger.exec(format)) ? new Rgb(m[1], m[2], m[3], 1) // rgb(255, 0, 0)
      : (m = reRgbPercent.exec(format)) ? new Rgb(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, 1) // rgb(100%, 0%, 0%)
      : (m = reRgbaInteger.exec(format)) ? rgba(m[1], m[2], m[3], m[4]) // rgba(255, 0, 0, 1)
      : (m = reRgbaPercent.exec(format)) ? rgba(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, m[4]) // rgb(100%, 0%, 0%, 1)
      : (m = reHslPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, 1) // hsl(120, 50%, 50%)
      : (m = reHslaPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, m[4]) // hsla(120, 50%, 50%, 1)
      : named.hasOwnProperty(format) ? rgbn(named[format]) // eslint-disable-line no-prototype-builtins
      : format === "transparent" ? new Rgb(NaN, NaN, NaN, 0)
      : null;
}

function rgbn(n) {
  return new Rgb(n >> 16 & 0xff, n >> 8 & 0xff, n & 0xff, 1);
}

function rgba(r, g, b, a) {
  if (a <= 0) r = g = b = NaN;
  return new Rgb(r, g, b, a);
}

function rgbConvert(o) {
  if (!(o instanceof Color$1)) o = color(o);
  if (!o) return new Rgb;
  o = o.rgb();
  return new Rgb(o.r, o.g, o.b, o.opacity);
}

function rgb(r, g, b, opacity) {
  return arguments.length === 1 ? rgbConvert(r) : new Rgb(r, g, b, opacity == null ? 1 : opacity);
}

function Rgb(r, g, b, opacity) {
  this.r = +r;
  this.g = +g;
  this.b = +b;
  this.opacity = +opacity;
}

define(Rgb, rgb, extend(Color$1, {
  brighter(k) {
    k = k == null ? brighter : Math.pow(brighter, k);
    return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);
  },
  darker(k) {
    k = k == null ? darker : Math.pow(darker, k);
    return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);
  },
  rgb() {
    return this;
  },
  clamp() {
    return new Rgb(clampi(this.r), clampi(this.g), clampi(this.b), clampa(this.opacity));
  },
  displayable() {
    return (-0.5 <= this.r && this.r < 255.5)
        && (-0.5 <= this.g && this.g < 255.5)
        && (-0.5 <= this.b && this.b < 255.5)
        && (0 <= this.opacity && this.opacity <= 1);
  },
  hex: rgb_formatHex, // Deprecated! Use color.formatHex.
  formatHex: rgb_formatHex,
  formatHex8: rgb_formatHex8,
  formatRgb: rgb_formatRgb,
  toString: rgb_formatRgb
}));

function rgb_formatHex() {
  return `#${hex$1(this.r)}${hex$1(this.g)}${hex$1(this.b)}`;
}

function rgb_formatHex8() {
  return `#${hex$1(this.r)}${hex$1(this.g)}${hex$1(this.b)}${hex$1((isNaN(this.opacity) ? 1 : this.opacity) * 255)}`;
}

function rgb_formatRgb() {
  const a = clampa(this.opacity);
  return `${a === 1 ? "rgb(" : "rgba("}${clampi(this.r)}, ${clampi(this.g)}, ${clampi(this.b)}${a === 1 ? ")" : `, ${a})`}`;
}

function clampa(opacity) {
  return isNaN(opacity) ? 1 : Math.max(0, Math.min(1, opacity));
}

function clampi(value) {
  return Math.max(0, Math.min(255, Math.round(value) || 0));
}

function hex$1(value) {
  value = clampi(value);
  return (value < 16 ? "0" : "") + value.toString(16);
}

function hsla(h, s, l, a) {
  if (a <= 0) h = s = l = NaN;
  else if (l <= 0 || l >= 1) h = s = NaN;
  else if (s <= 0) h = NaN;
  return new Hsl(h, s, l, a);
}

function hslConvert(o) {
  if (o instanceof Hsl) return new Hsl(o.h, o.s, o.l, o.opacity);
  if (!(o instanceof Color$1)) o = color(o);
  if (!o) return new Hsl;
  if (o instanceof Hsl) return o;
  o = o.rgb();
  var r = o.r / 255,
      g = o.g / 255,
      b = o.b / 255,
      min = Math.min(r, g, b),
      max = Math.max(r, g, b),
      h = NaN,
      s = max - min,
      l = (max + min) / 2;
  if (s) {
    if (r === max) h = (g - b) / s + (g < b) * 6;
    else if (g === max) h = (b - r) / s + 2;
    else h = (r - g) / s + 4;
    s /= l < 0.5 ? max + min : 2 - max - min;
    h *= 60;
  } else {
    s = l > 0 && l < 1 ? 0 : h;
  }
  return new Hsl(h, s, l, o.opacity);
}

function hsl(h, s, l, opacity) {
  return arguments.length === 1 ? hslConvert(h) : new Hsl(h, s, l, opacity == null ? 1 : opacity);
}

function Hsl(h, s, l, opacity) {
  this.h = +h;
  this.s = +s;
  this.l = +l;
  this.opacity = +opacity;
}

define(Hsl, hsl, extend(Color$1, {
  brighter(k) {
    k = k == null ? brighter : Math.pow(brighter, k);
    return new Hsl(this.h, this.s, this.l * k, this.opacity);
  },
  darker(k) {
    k = k == null ? darker : Math.pow(darker, k);
    return new Hsl(this.h, this.s, this.l * k, this.opacity);
  },
  rgb() {
    var h = this.h % 360 + (this.h < 0) * 360,
        s = isNaN(h) || isNaN(this.s) ? 0 : this.s,
        l = this.l,
        m2 = l + (l < 0.5 ? l : 1 - l) * s,
        m1 = 2 * l - m2;
    return new Rgb(
      hsl2rgb(h >= 240 ? h - 240 : h + 120, m1, m2),
      hsl2rgb(h, m1, m2),
      hsl2rgb(h < 120 ? h + 240 : h - 120, m1, m2),
      this.opacity
    );
  },
  clamp() {
    return new Hsl(clamph(this.h), clampt(this.s), clampt(this.l), clampa(this.opacity));
  },
  displayable() {
    return (0 <= this.s && this.s <= 1 || isNaN(this.s))
        && (0 <= this.l && this.l <= 1)
        && (0 <= this.opacity && this.opacity <= 1);
  },
  formatHsl() {
    const a = clampa(this.opacity);
    return `${a === 1 ? "hsl(" : "hsla("}${clamph(this.h)}, ${clampt(this.s) * 100}%, ${clampt(this.l) * 100}%${a === 1 ? ")" : `, ${a})`}`;
  }
}));

function clamph(value) {
  value = (value || 0) % 360;
  return value < 0 ? value + 360 : value;
}

function clampt(value) {
  return Math.max(0, Math.min(1, value || 0));
}

/* From FvD 13.37, CSS Color Module Level 3 */
function hsl2rgb(h, m1, m2) {
  return (h < 60 ? m1 + (m2 - m1) * h / 60
      : h < 180 ? m2
      : h < 240 ? m1 + (m2 - m1) * (240 - h) / 60
      : m1) * 255;
}

const radians = Math.PI / 180;
const degrees$1 = 180 / Math.PI;

// https://observablehq.com/@mbostock/lab-and-rgb
const K = 18,
    Xn = 0.96422,
    Yn = 1,
    Zn = 0.82521,
    t0$1 = 4 / 29,
    t1$1 = 6 / 29,
    t2 = 3 * t1$1 * t1$1,
    t3 = t1$1 * t1$1 * t1$1;

function labConvert(o) {
  if (o instanceof Lab) return new Lab(o.l, o.a, o.b, o.opacity);
  if (o instanceof Hcl) return hcl2lab(o);
  if (!(o instanceof Rgb)) o = rgbConvert(o);
  var r = rgb2lrgb(o.r),
      g = rgb2lrgb(o.g),
      b = rgb2lrgb(o.b),
      y = xyz2lab((0.2225045 * r + 0.7168786 * g + 0.0606169 * b) / Yn), x, z;
  if (r === g && g === b) x = z = y; else {
    x = xyz2lab((0.4360747 * r + 0.3850649 * g + 0.1430804 * b) / Xn);
    z = xyz2lab((0.0139322 * r + 0.0971045 * g + 0.7141733 * b) / Zn);
  }
  return new Lab(116 * y - 16, 500 * (x - y), 200 * (y - z), o.opacity);
}

function lab(l, a, b, opacity) {
  return arguments.length === 1 ? labConvert(l) : new Lab(l, a, b, opacity == null ? 1 : opacity);
}

function Lab(l, a, b, opacity) {
  this.l = +l;
  this.a = +a;
  this.b = +b;
  this.opacity = +opacity;
}

define(Lab, lab, extend(Color$1, {
  brighter(k) {
    return new Lab(this.l + K * (k == null ? 1 : k), this.a, this.b, this.opacity);
  },
  darker(k) {
    return new Lab(this.l - K * (k == null ? 1 : k), this.a, this.b, this.opacity);
  },
  rgb() {
    var y = (this.l + 16) / 116,
        x = isNaN(this.a) ? y : y + this.a / 500,
        z = isNaN(this.b) ? y : y - this.b / 200;
    x = Xn * lab2xyz(x);
    y = Yn * lab2xyz(y);
    z = Zn * lab2xyz(z);
    return new Rgb(
      lrgb2rgb( 3.1338561 * x - 1.6168667 * y - 0.4906146 * z),
      lrgb2rgb(-0.9787684 * x + 1.9161415 * y + 0.0334540 * z),
      lrgb2rgb( 0.0719453 * x - 0.2289914 * y + 1.4052427 * z),
      this.opacity
    );
  }
}));

function xyz2lab(t) {
  return t > t3 ? Math.pow(t, 1 / 3) : t / t2 + t0$1;
}

function lab2xyz(t) {
  return t > t1$1 ? t * t * t : t2 * (t - t0$1);
}

function lrgb2rgb(x) {
  return 255 * (x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055);
}

function rgb2lrgb(x) {
  return (x /= 255) <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
}

function hclConvert(o) {
  if (o instanceof Hcl) return new Hcl(o.h, o.c, o.l, o.opacity);
  if (!(o instanceof Lab)) o = labConvert(o);
  if (o.a === 0 && o.b === 0) return new Hcl(NaN, 0 < o.l && o.l < 100 ? 0 : NaN, o.l, o.opacity);
  var h = Math.atan2(o.b, o.a) * degrees$1;
  return new Hcl(h < 0 ? h + 360 : h, Math.sqrt(o.a * o.a + o.b * o.b), o.l, o.opacity);
}

function hcl(h, c, l, opacity) {
  return arguments.length === 1 ? hclConvert(h) : new Hcl(h, c, l, opacity == null ? 1 : opacity);
}

function Hcl(h, c, l, opacity) {
  this.h = +h;
  this.c = +c;
  this.l = +l;
  this.opacity = +opacity;
}

function hcl2lab(o) {
  if (isNaN(o.h)) return new Lab(o.l, 0, 0, o.opacity);
  var h = o.h * radians;
  return new Lab(o.l, Math.cos(h) * o.c, Math.sin(h) * o.c, o.opacity);
}

define(Hcl, hcl, extend(Color$1, {
  brighter(k) {
    return new Hcl(this.h, this.c, this.l + K * (k == null ? 1 : k), this.opacity);
  },
  darker(k) {
    return new Hcl(this.h, this.c, this.l - K * (k == null ? 1 : k), this.opacity);
  },
  rgb() {
    return hcl2lab(this).rgb();
  }
}));

var A = -0.14861,
    B = 1.78277,
    C = -0.29227,
    D = -0.90649,
    E = 1.97294,
    ED = E * D,
    EB = E * B,
    BC_DA = B * C - D * A;

function cubehelixConvert(o) {
  if (o instanceof Cubehelix) return new Cubehelix(o.h, o.s, o.l, o.opacity);
  if (!(o instanceof Rgb)) o = rgbConvert(o);
  var r = o.r / 255,
      g = o.g / 255,
      b = o.b / 255,
      l = (BC_DA * b + ED * r - EB * g) / (BC_DA + ED - EB),
      bl = b - l,
      k = (E * (g - l) - C * bl) / D,
      s = Math.sqrt(k * k + bl * bl) / (E * l * (1 - l)), // NaN if l=0 or l=1
      h = s ? Math.atan2(k, bl) * degrees$1 - 120 : NaN;
  return new Cubehelix(h < 0 ? h + 360 : h, s, l, o.opacity);
}

function cubehelix(h, s, l, opacity) {
  return arguments.length === 1 ? cubehelixConvert(h) : new Cubehelix(h, s, l, opacity == null ? 1 : opacity);
}

function Cubehelix(h, s, l, opacity) {
  this.h = +h;
  this.s = +s;
  this.l = +l;
  this.opacity = +opacity;
}

define(Cubehelix, cubehelix, extend(Color$1, {
  brighter(k) {
    k = k == null ? brighter : Math.pow(brighter, k);
    return new Cubehelix(this.h, this.s, this.l * k, this.opacity);
  },
  darker(k) {
    k = k == null ? darker : Math.pow(darker, k);
    return new Cubehelix(this.h, this.s, this.l * k, this.opacity);
  },
  rgb() {
    var h = isNaN(this.h) ? 0 : (this.h + 120) * radians,
        l = +this.l,
        a = isNaN(this.s) ? 0 : this.s * l * (1 - l),
        cosh = Math.cos(h),
        sinh = Math.sin(h);
    return new Rgb(
      255 * (l + a * (A * cosh + B * sinh)),
      255 * (l + a * (C * cosh + D * sinh)),
      255 * (l + a * (E * cosh)),
      this.opacity
    );
  }
}));

var abs$1 = Math.abs;
var cos$1 = Math.cos;
var sin$1 = Math.sin;
var pi$2 = Math.PI;
var halfPi$1 = pi$2 / 2;
var tau$2 = pi$2 * 2;
var max$2 = Math.max;
var epsilon$2 = 1e-12;

function range$1(i, j) {
  return Array.from({length: j - i}, (_, k) => i + k);
}

function compareValue(compare) {
  return function(a, b) {
    return compare(
      a.source.value + a.target.value,
      b.source.value + b.target.value
    );
  };
}

function chord() {
  return chord$1(false);
}

function chord$1(directed, transpose) {
  var padAngle = 0,
      sortGroups = null,
      sortSubgroups = null,
      sortChords = null;

  function chord(matrix) {
    var n = matrix.length,
        groupSums = new Array(n),
        groupIndex = range$1(0, n),
        chords = new Array(n * n),
        groups = new Array(n),
        k = 0, dx;

    matrix = Float64Array.from({length: n * n}, (_, i) => matrix[i / n | 0][i % n]);

    // Compute the scaling factor from value to angle in [0, 2pi].
    for (let i = 0; i < n; ++i) {
      let x = 0;
      for (let j = 0; j < n; ++j) x += matrix[i * n + j] + directed * matrix[j * n + i];
      k += groupSums[i] = x;
    }
    k = max$2(0, tau$2 - padAngle * n) / k;
    dx = k ? padAngle : tau$2 / n;

    // Compute the angles for each group and constituent chord.
    {
      let x = 0;
      if (sortGroups) groupIndex.sort((a, b) => sortGroups(groupSums[a], groupSums[b]));
      for (const i of groupIndex) {
        const x0 = x;
        {
          const subgroupIndex = range$1(0, n).filter(j => matrix[i * n + j] || matrix[j * n + i]);
          if (sortSubgroups) subgroupIndex.sort((a, b) => sortSubgroups(matrix[i * n + a], matrix[i * n + b]));
          for (const j of subgroupIndex) {
            let chord;
            if (i < j) {
              chord = chords[i * n + j] || (chords[i * n + j] = {source: null, target: null});
              chord.source = {index: i, startAngle: x, endAngle: x += matrix[i * n + j] * k, value: matrix[i * n + j]};
            } else {
              chord = chords[j * n + i] || (chords[j * n + i] = {source: null, target: null});
              chord.target = {index: i, startAngle: x, endAngle: x += matrix[i * n + j] * k, value: matrix[i * n + j]};
              if (i === j) chord.source = chord.target;
            }
            if (chord.source && chord.target && chord.source.value < chord.target.value) {
              const source = chord.source;
              chord.source = chord.target;
              chord.target = source;
            }
          }
          groups[i] = {index: i, startAngle: x0, endAngle: x, value: groupSums[i]};
        }
        x += dx;
      }
    }

    // Remove empty chords.
    chords = Object.values(chords);
    chords.groups = groups;
    return sortChords ? chords.sort(sortChords) : chords;
  }

  chord.padAngle = function(_) {
    return arguments.length ? (padAngle = max$2(0, _), chord) : padAngle;
  };

  chord.sortGroups = function(_) {
    return arguments.length ? (sortGroups = _, chord) : sortGroups;
  };

  chord.sortSubgroups = function(_) {
    return arguments.length ? (sortSubgroups = _, chord) : sortSubgroups;
  };

  chord.sortChords = function(_) {
    return arguments.length ? (_ == null ? sortChords = null : (sortChords = compareValue(_))._ = _, chord) : sortChords && sortChords._;
  };

  return chord;
}

const pi$1 = Math.PI,
    tau$1 = 2 * pi$1,
    epsilon$1 = 1e-6,
    tauEpsilon = tau$1 - epsilon$1;

function append(strings) {
  this._ += strings[0];
  for (let i = 1, n = strings.length; i < n; ++i) {
    this._ += arguments[i] + strings[i];
  }
}

function appendRound(digits) {
  let d = Math.floor(digits);
  if (!(d >= 0)) throw new Error(`invalid digits: ${digits}`);
  if (d > 15) return append;
  const k = 10 ** d;
  return function(strings) {
    this._ += strings[0];
    for (let i = 1, n = strings.length; i < n; ++i) {
      this._ += Math.round(arguments[i] * k) / k + strings[i];
    }
  };
}

let Path$2 = class Path {
  constructor(digits) {
    this._x0 = this._y0 = // start of current subpath
    this._x1 = this._y1 = null; // end of current subpath
    this._ = "";
    this._append = digits == null ? append : appendRound(digits);
  }
  moveTo(x, y) {
    this._append`M${this._x0 = this._x1 = +x},${this._y0 = this._y1 = +y}`;
  }
  closePath() {
    if (this._x1 !== null) {
      this._x1 = this._x0, this._y1 = this._y0;
      this._append`Z`;
    }
  }
  lineTo(x, y) {
    this._append`L${this._x1 = +x},${this._y1 = +y}`;
  }
  quadraticCurveTo(x1, y1, x, y) {
    this._append`Q${+x1},${+y1},${this._x1 = +x},${this._y1 = +y}`;
  }
  bezierCurveTo(x1, y1, x2, y2, x, y) {
    this._append`C${+x1},${+y1},${+x2},${+y2},${this._x1 = +x},${this._y1 = +y}`;
  }
  arcTo(x1, y1, x2, y2, r) {
    x1 = +x1, y1 = +y1, x2 = +x2, y2 = +y2, r = +r;

    // Is the radius negative? Error.
    if (r < 0) throw new Error(`negative radius: ${r}`);

    let x0 = this._x1,
        y0 = this._y1,
        x21 = x2 - x1,
        y21 = y2 - y1,
        x01 = x0 - x1,
        y01 = y0 - y1,
        l01_2 = x01 * x01 + y01 * y01;

    // Is this path empty? Move to (x1,y1).
    if (this._x1 === null) {
      this._append`M${this._x1 = x1},${this._y1 = y1}`;
    }

    // Or, is (x1,y1) coincident with (x0,y0)? Do nothing.
    else if (!(l01_2 > epsilon$1));

    // Or, are (x0,y0), (x1,y1) and (x2,y2) collinear?
    // Equivalently, is (x1,y1) coincident with (x2,y2)?
    // Or, is the radius zero? Line to (x1,y1).
    else if (!(Math.abs(y01 * x21 - y21 * x01) > epsilon$1) || !r) {
      this._append`L${this._x1 = x1},${this._y1 = y1}`;
    }

    // Otherwise, draw an arc!
    else {
      let x20 = x2 - x0,
          y20 = y2 - y0,
          l21_2 = x21 * x21 + y21 * y21,
          l20_2 = x20 * x20 + y20 * y20,
          l21 = Math.sqrt(l21_2),
          l01 = Math.sqrt(l01_2),
          l = r * Math.tan((pi$1 - Math.acos((l21_2 + l01_2 - l20_2) / (2 * l21 * l01))) / 2),
          t01 = l / l01,
          t21 = l / l21;

      // If the start tangent is not coincident with (x0,y0), line to.
      if (Math.abs(t01 - 1) > epsilon$1) {
        this._append`L${x1 + t01 * x01},${y1 + t01 * y01}`;
      }

      this._append`A${r},${r},0,0,${+(y01 * x20 > x01 * y20)},${this._x1 = x1 + t21 * x21},${this._y1 = y1 + t21 * y21}`;
    }
  }
  arc(x, y, r, a0, a1, ccw) {
    x = +x, y = +y, r = +r, ccw = !!ccw;

    // Is the radius negative? Error.
    if (r < 0) throw new Error(`negative radius: ${r}`);

    let dx = r * Math.cos(a0),
        dy = r * Math.sin(a0),
        x0 = x + dx,
        y0 = y + dy,
        cw = 1 ^ ccw,
        da = ccw ? a0 - a1 : a1 - a0;

    // Is this path empty? Move to (x0,y0).
    if (this._x1 === null) {
      this._append`M${x0},${y0}`;
    }

    // Or, is (x0,y0) not coincident with the previous point? Line to (x0,y0).
    else if (Math.abs(this._x1 - x0) > epsilon$1 || Math.abs(this._y1 - y0) > epsilon$1) {
      this._append`L${x0},${y0}`;
    }

    // Is this arc empty? We’re done.
    if (!r) return;

    // Does the angle go the wrong way? Flip the direction.
    if (da < 0) da = da % tau$1 + tau$1;

    // Is this a complete circle? Draw two arcs to complete the circle.
    if (da > tauEpsilon) {
      this._append`A${r},${r},0,1,${cw},${x - dx},${y - dy}A${r},${r},0,1,${cw},${this._x1 = x0},${this._y1 = y0}`;
    }

    // Is this arc non-empty? Draw an arc!
    else if (da > epsilon$1) {
      this._append`A${r},${r},0,${+(da >= pi$1)},${cw},${this._x1 = x + r * Math.cos(a1)},${this._y1 = y + r * Math.sin(a1)}`;
    }
  }
  rect(x, y, w, h) {
    this._append`M${this._x0 = this._x1 = +x},${this._y0 = this._y1 = +y}h${w = +w}v${+h}h${-w}Z`;
  }
  toString() {
    return this._;
  }
};

function path() {
  return new Path$2;
}

// Allow instanceof d3.path
path.prototype = Path$2.prototype;

var slice = Array.prototype.slice;

function constant$4(x) {
  return function() {
    return x;
  };
}

function defaultSource(d) {
  return d.source;
}

function defaultTarget(d) {
  return d.target;
}

function defaultRadius(d) {
  return d.radius;
}

function defaultStartAngle(d) {
  return d.startAngle;
}

function defaultEndAngle(d) {
  return d.endAngle;
}

function defaultPadAngle() {
  return 0;
}

function ribbon(headRadius) {
  var source = defaultSource,
      target = defaultTarget,
      sourceRadius = defaultRadius,
      targetRadius = defaultRadius,
      startAngle = defaultStartAngle,
      endAngle = defaultEndAngle,
      padAngle = defaultPadAngle,
      context = null;

  function ribbon() {
    var buffer,
        s = source.apply(this, arguments),
        t = target.apply(this, arguments),
        ap = padAngle.apply(this, arguments) / 2,
        argv = slice.call(arguments),
        sr = +sourceRadius.apply(this, (argv[0] = s, argv)),
        sa0 = startAngle.apply(this, argv) - halfPi$1,
        sa1 = endAngle.apply(this, argv) - halfPi$1,
        tr = +targetRadius.apply(this, (argv[0] = t, argv)),
        ta0 = startAngle.apply(this, argv) - halfPi$1,
        ta1 = endAngle.apply(this, argv) - halfPi$1;

    if (!context) context = buffer = path();

    if (ap > epsilon$2) {
      if (abs$1(sa1 - sa0) > ap * 2 + epsilon$2) sa1 > sa0 ? (sa0 += ap, sa1 -= ap) : (sa0 -= ap, sa1 += ap);
      else sa0 = sa1 = (sa0 + sa1) / 2;
      if (abs$1(ta1 - ta0) > ap * 2 + epsilon$2) ta1 > ta0 ? (ta0 += ap, ta1 -= ap) : (ta0 -= ap, ta1 += ap);
      else ta0 = ta1 = (ta0 + ta1) / 2;
    }

    context.moveTo(sr * cos$1(sa0), sr * sin$1(sa0));
    context.arc(0, 0, sr, sa0, sa1);
    if (sa0 !== ta0 || sa1 !== ta1) {
      {
        context.quadraticCurveTo(0, 0, tr * cos$1(ta0), tr * sin$1(ta0));
        context.arc(0, 0, tr, ta0, ta1);
      }
    }
    context.quadraticCurveTo(0, 0, sr * cos$1(sa0), sr * sin$1(sa0));
    context.closePath();

    if (buffer) return context = null, buffer + "" || null;
  }

  ribbon.radius = function(_) {
    return arguments.length ? (sourceRadius = targetRadius = typeof _ === "function" ? _ : constant$4(+_), ribbon) : sourceRadius;
  };

  ribbon.sourceRadius = function(_) {
    return arguments.length ? (sourceRadius = typeof _ === "function" ? _ : constant$4(+_), ribbon) : sourceRadius;
  };

  ribbon.targetRadius = function(_) {
    return arguments.length ? (targetRadius = typeof _ === "function" ? _ : constant$4(+_), ribbon) : targetRadius;
  };

  ribbon.startAngle = function(_) {
    return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant$4(+_), ribbon) : startAngle;
  };

  ribbon.endAngle = function(_) {
    return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant$4(+_), ribbon) : endAngle;
  };

  ribbon.padAngle = function(_) {
    return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant$4(+_), ribbon) : padAngle;
  };

  ribbon.source = function(_) {
    return arguments.length ? (source = _, ribbon) : source;
  };

  ribbon.target = function(_) {
    return arguments.length ? (target = _, ribbon) : target;
  };

  ribbon.context = function(_) {
    return arguments.length ? ((context = _ == null ? null : _), ribbon) : context;
  };

  return ribbon;
}

function ribbon$1() {
  return ribbon();
}

var noop = {value: () => {}};

function dispatch() {
  for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) {
    if (!(t = arguments[i] + "") || (t in _) || /[\s.]/.test(t)) throw new Error("illegal type: " + t);
    _[t] = [];
  }
  return new Dispatch(_);
}

function Dispatch(_) {
  this._ = _;
}

function parseTypenames$1(typenames, types) {
  return typenames.trim().split(/^|\s+/).map(function(t) {
    var name = "", i = t.indexOf(".");
    if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
    if (t && !types.hasOwnProperty(t)) throw new Error("unknown type: " + t);
    return {type: t, name: name};
  });
}

Dispatch.prototype = dispatch.prototype = {
  constructor: Dispatch,
  on: function(typename, callback) {
    var _ = this._,
        T = parseTypenames$1(typename + "", _),
        t,
        i = -1,
        n = T.length;

    // If no callback was specified, return the callback of the given type and name.
    if (arguments.length < 2) {
      while (++i < n) if ((t = (typename = T[i]).type) && (t = get$1(_[t], typename.name))) return t;
      return;
    }

    // If a type was specified, set the callback for the given type and name.
    // Otherwise, if a null callback was specified, remove callbacks of the given name.
    if (callback != null && typeof callback !== "function") throw new Error("invalid callback: " + callback);
    while (++i < n) {
      if (t = (typename = T[i]).type) _[t] = set$1(_[t], typename.name, callback);
      else if (callback == null) for (t in _) _[t] = set$1(_[t], typename.name, null);
    }

    return this;
  },
  copy: function() {
    var copy = {}, _ = this._;
    for (var t in _) copy[t] = _[t].slice();
    return new Dispatch(copy);
  },
  call: function(type, that) {
    if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2];
    if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
    for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
  },
  apply: function(type, that, args) {
    if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
    for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
  }
};

function get$1(type, name) {
  for (var i = 0, n = type.length, c; i < n; ++i) {
    if ((c = type[i]).name === name) {
      return c.value;
    }
  }
}

function set$1(type, name, callback) {
  for (var i = 0, n = type.length; i < n; ++i) {
    if (type[i].name === name) {
      type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1));
      break;
    }
  }
  if (callback != null) type.push({name: name, value: callback});
  return type;
}

var xhtml = "http://www.w3.org/1999/xhtml";

var namespaces = {
  svg: "http://www.w3.org/2000/svg",
  xhtml: xhtml,
  xlink: "http://www.w3.org/1999/xlink",
  xml: "http://www.w3.org/XML/1998/namespace",
  xmlns: "http://www.w3.org/2000/xmlns/"
};

function namespace(name) {
  var prefix = name += "", i = prefix.indexOf(":");
  if (i >= 0 && (prefix = name.slice(0, i)) !== "xmlns") name = name.slice(i + 1);
  return namespaces.hasOwnProperty(prefix) ? {space: namespaces[prefix], local: name} : name; // eslint-disable-line no-prototype-builtins
}

function creatorInherit(name) {
  return function() {
    var document = this.ownerDocument,
        uri = this.namespaceURI;
    return uri === xhtml && document.documentElement.namespaceURI === xhtml
        ? document.createElement(name)
        : document.createElementNS(uri, name);
  };
}

function creatorFixed(fullname) {
  return function() {
    return this.ownerDocument.createElementNS(fullname.space, fullname.local);
  };
}

function creator(name) {
  var fullname = namespace(name);
  return (fullname.local
      ? creatorFixed
      : creatorInherit)(fullname);
}

function none() {}

function selector(selector) {
  return selector == null ? none : function() {
    return this.querySelector(selector);
  };
}

function selection_select(select) {
  if (typeof select !== "function") select = selector(select);

  for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
    for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {
      if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) {
        if ("__data__" in node) subnode.__data__ = node.__data__;
        subgroup[i] = subnode;
      }
    }
  }

  return new Selection$1(subgroups, this._parents);
}

// Given something array like (or null), returns something that is strictly an
// array. This is used to ensure that array-like objects passed to d3.selectAll
// or selection.selectAll are converted into proper arrays when creating a
// selection; we don’t ever want to create a selection backed by a live
// HTMLCollection or NodeList. However, note that selection.selectAll will use a
// static NodeList as a group, since it safely derived from querySelectorAll.
function array(x) {
  return x == null ? [] : Array.isArray(x) ? x : Array.from(x);
}

function empty$1() {
  return [];
}

function selectorAll(selector) {
  return selector == null ? empty$1 : function() {
    return this.querySelectorAll(selector);
  };
}

function arrayAll(select) {
  return function() {
    return array(select.apply(this, arguments));
  };
}

function selection_selectAll(select) {
  if (typeof select === "function") select = arrayAll(select);
  else select = selectorAll(select);

  for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {
    for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
      if (node = group[i]) {
        subgroups.push(select.call(node, node.__data__, i, group));
        parents.push(node);
      }
    }
  }

  return new Selection$1(subgroups, parents);
}

function matcher(selector) {
  return function() {
    return this.matches(selector);
  };
}

function childMatcher(selector) {
  return function(node) {
    return node.matches(selector);
  };
}

var find = Array.prototype.find;

function childFind(match) {
  return function() {
    return find.call(this.children, match);
  };
}

function childFirst() {
  return this.firstElementChild;
}

function selection_selectChild(match) {
  return this.select(match == null ? childFirst
      : childFind(typeof match === "function" ? match : childMatcher(match)));
}

var filter = Array.prototype.filter;

function children() {
  return Array.from(this.children);
}

function childrenFilter(match) {
  return function() {
    return filter.call(this.children, match);
  };
}

function selection_selectChildren(match) {
  return this.selectAll(match == null ? children
      : childrenFilter(typeof match === "function" ? match : childMatcher(match)));
}

function selection_filter(match) {
  if (typeof match !== "function") match = matcher(match);

  for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
    for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) {
      if ((node = group[i]) && match.call(node, node.__data__, i, group)) {
        subgroup.push(node);
      }
    }
  }

  return new Selection$1(subgroups, this._parents);
}

function sparse(update) {
  return new Array(update.length);
}

function selection_enter() {
  return new Selection$1(this._enter || this._groups.map(sparse), this._parents);
}

function EnterNode(parent, datum) {
  this.ownerDocument = parent.ownerDocument;
  this.namespaceURI = parent.namespaceURI;
  this._next = null;
  this._parent = parent;
  this.__data__ = datum;
}

EnterNode.prototype = {
  constructor: EnterNode,
  appendChild: function(child) { return this._parent.insertBefore(child, this._next); },
  insertBefore: function(child, next) { return this._parent.insertBefore(child, next); },
  querySelector: function(selector) { return this._parent.querySelector(selector); },
  querySelectorAll: function(selector) { return this._parent.querySelectorAll(selector); }
};

function constant$3(x) {
  return function() {
    return x;
  };
}

function bindIndex(parent, group, enter, update, exit, data) {
  var i = 0,
      node,
      groupLength = group.length,
      dataLength = data.length;

  // Put any non-null nodes that fit into update.
  // Put any null nodes into enter.
  // Put any remaining data into enter.
  for (; i < dataLength; ++i) {
    if (node = group[i]) {
      node.__data__ = data[i];
      update[i] = node;
    } else {
      enter[i] = new EnterNode(parent, data[i]);
    }
  }

  // Put any non-null nodes that don’t fit into exit.
  for (; i < groupLength; ++i) {
    if (node = group[i]) {
      exit[i] = node;
    }
  }
}

function bindKey(parent, group, enter, update, exit, data, key) {
  var i,
      node,
      nodeByKeyValue = new Map,
      groupLength = group.length,
      dataLength = data.length,
      keyValues = new Array(groupLength),
      keyValue;

  // Compute the key for each node.
  // If multiple nodes have the same key, the duplicates are added to exit.
  for (i = 0; i < groupLength; ++i) {
    if (node = group[i]) {
      keyValues[i] = keyValue = key.call(node, node.__data__, i, group) + "";
      if (nodeByKeyValue.has(keyValue)) {
        exit[i] = node;
      } else {
        nodeByKeyValue.set(keyValue, node);
      }
    }
  }

  // Compute the key for each datum.
  // If there a node associated with this key, join and add it to update.
  // If there is not (or the key is a duplicate), add it to enter.
  for (i = 0; i < dataLength; ++i) {
    keyValue = key.call(parent, data[i], i, data) + "";
    if (node = nodeByKeyValue.get(keyValue)) {
      update[i] = node;
      node.__data__ = data[i];
      nodeByKeyValue.delete(keyValue);
    } else {
      enter[i] = new EnterNode(parent, data[i]);
    }
  }

  // Add any remaining nodes that were not bound to data to exit.
  for (i = 0; i < groupLength; ++i) {
    if ((node = group[i]) && (nodeByKeyValue.get(keyValues[i]) === node)) {
      exit[i] = node;
    }
  }
}

function datum(node) {
  return node.__data__;
}

function selection_data(value, key) {
  if (!arguments.length) return Array.from(this, datum);

  var bind = key ? bindKey : bindIndex,
      parents = this._parents,
      groups = this._groups;

  if (typeof value !== "function") value = constant$3(value);

  for (var m = groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) {
    var parent = parents[j],
        group = groups[j],
        groupLength = group.length,
        data = arraylike(value.call(parent, parent && parent.__data__, j, parents)),
        dataLength = data.length,
        enterGroup = enter[j] = new Array(dataLength),
        updateGroup = update[j] = new Array(dataLength),
        exitGroup = exit[j] = new Array(groupLength);

    bind(parent, group, enterGroup, updateGroup, exitGroup, data, key);

    // Now connect the enter nodes to their following update node, such that
    // appendChild can insert the materialized enter node before this node,
    // rather than at the end of the parent node.
    for (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) {
      if (previous = enterGroup[i0]) {
        if (i0 >= i1) i1 = i0 + 1;
        while (!(next = updateGroup[i1]) && ++i1 < dataLength);
        previous._next = next || null;
      }
    }
  }

  update = new Selection$1(update, parents);
  update._enter = enter;
  update._exit = exit;
  return update;
}

// Given some data, this returns an array-like view of it: an object that
// exposes a length property and allows numeric indexing. Note that unlike
// selectAll, this isn’t worried about “live” collections because the resulting
// array will only be used briefly while data is being bound. (It is possible to
// cause the data to change while iterating by using a key function, but please
// don’t; we’d rather avoid a gratuitous copy.)
function arraylike(data) {
  return typeof data === "object" && "length" in data
    ? data // Array, TypedArray, NodeList, array-like
    : Array.from(data); // Map, Set, iterable, string, or anything else
}

function selection_exit() {
  return new Selection$1(this._exit || this._groups.map(sparse), this._parents);
}

function selection_join(onenter, onupdate, onexit) {
  var enter = this.enter(), update = this, exit = this.exit();
  if (typeof onenter === "function") {
    enter = onenter(enter);
    if (enter) enter = enter.selection();
  } else {
    enter = enter.append(onenter + "");
  }
  if (onupdate != null) {
    update = onupdate(update);
    if (update) update = update.selection();
  }
  if (onexit == null) exit.remove(); else onexit(exit);
  return enter && update ? enter.merge(update).order() : update;
}

function selection_merge(context) {
  var selection = context.selection ? context.selection() : context;

  for (var groups0 = this._groups, groups1 = selection._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {
    for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {
      if (node = group0[i] || group1[i]) {
        merge[i] = node;
      }
    }
  }

  for (; j < m0; ++j) {
    merges[j] = groups0[j];
  }

  return new Selection$1(merges, this._parents);
}

function selection_order() {

  for (var groups = this._groups, j = -1, m = groups.length; ++j < m;) {
    for (var group = groups[j], i = group.length - 1, next = group[i], node; --i >= 0;) {
      if (node = group[i]) {
        if (next && node.compareDocumentPosition(next) ^ 4) next.parentNode.insertBefore(node, next);
        next = node;
      }
    }
  }

  return this;
}

function selection_sort(compare) {
  if (!compare) compare = ascending$1;

  function compareNode(a, b) {
    return a && b ? compare(a.__data__, b.__data__) : !a - !b;
  }

  for (var groups = this._groups, m = groups.length, sortgroups = new Array(m), j = 0; j < m; ++j) {
    for (var group = groups[j], n = group.length, sortgroup = sortgroups[j] = new Array(n), node, i = 0; i < n; ++i) {
      if (node = group[i]) {
        sortgroup[i] = node;
      }
    }
    sortgroup.sort(compareNode);
  }

  return new Selection$1(sortgroups, this._parents).order();
}

function ascending$1(a, b) {
  return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
}

function selection_call() {
  var callback = arguments[0];
  arguments[0] = this;
  callback.apply(null, arguments);
  return this;
}

function selection_nodes() {
  return Array.from(this);
}

function selection_node() {

  for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
    for (var group = groups[j], i = 0, n = group.length; i < n; ++i) {
      var node = group[i];
      if (node) return node;
    }
  }

  return null;
}

function selection_size() {
  let size = 0;
  for (const node of this) ++size; // eslint-disable-line no-unused-vars
  return size;
}

function selection_empty() {
  return !this.node();
}

function selection_each(callback) {

  for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
    for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {
      if (node = group[i]) callback.call(node, node.__data__, i, group);
    }
  }

  return this;
}

function attrRemove$1(name) {
  return function() {
    this.removeAttribute(name);
  };
}

function attrRemoveNS$1(fullname) {
  return function() {
    this.removeAttributeNS(fullname.space, fullname.local);
  };
}

function attrConstant$1(name, value) {
  return function() {
    this.setAttribute(name, value);
  };
}

function attrConstantNS$1(fullname, value) {
  return function() {
    this.setAttributeNS(fullname.space, fullname.local, value);
  };
}

function attrFunction$1(name, value) {
  return function() {
    var v = value.apply(this, arguments);
    if (v == null) this.removeAttribute(name);
    else this.setAttribute(name, v);
  };
}

function attrFunctionNS$1(fullname, value) {
  return function() {
    var v = value.apply(this, arguments);
    if (v == null) this.removeAttributeNS(fullname.space, fullname.local);
    else this.setAttributeNS(fullname.space, fullname.local, v);
  };
}

function selection_attr(name, value) {
  var fullname = namespace(name);

  if (arguments.length < 2) {
    var node = this.node();
    return fullname.local
        ? node.getAttributeNS(fullname.space, fullname.local)
        : node.getAttribute(fullname);
  }

  return this.each((value == null
      ? (fullname.local ? attrRemoveNS$1 : attrRemove$1) : (typeof value === "function"
      ? (fullname.local ? attrFunctionNS$1 : attrFunction$1)
      : (fullname.local ? attrConstantNS$1 : attrConstant$1)))(fullname, value));
}

function defaultView(node) {
  return (node.ownerDocument && node.ownerDocument.defaultView) // node is a Node
      || (node.document && node) // node is a Window
      || node.defaultView; // node is a Document
}

function styleRemove$1(name) {
  return function() {
    this.style.removeProperty(name);
  };
}

function styleConstant$1(name, value, priority) {
  return function() {
    this.style.setProperty(name, value, priority);
  };
}

function styleFunction$1(name, value, priority) {
  return function() {
    var v = value.apply(this, arguments);
    if (v == null) this.style.removeProperty(name);
    else this.style.setProperty(name, v, priority);
  };
}

function selection_style(name, value, priority) {
  return arguments.length > 1
      ? this.each((value == null
            ? styleRemove$1 : typeof value === "function"
            ? styleFunction$1
            : styleConstant$1)(name, value, priority == null ? "" : priority))
      : styleValue(this.node(), name);
}

function styleValue(node, name) {
  return node.style.getPropertyValue(name)
      || defaultView(node).getComputedStyle(node, null).getPropertyValue(name);
}

function propertyRemove(name) {
  return function() {
    delete this[name];
  };
}

function propertyConstant(name, value) {
  return function() {
    this[name] = value;
  };
}

function propertyFunction(name, value) {
  return function() {
    var v = value.apply(this, arguments);
    if (v == null) delete this[name];
    else this[name] = v;
  };
}

function selection_property(name, value) {
  return arguments.length > 1
      ? this.each((value == null
          ? propertyRemove : typeof value === "function"
          ? propertyFunction
          : propertyConstant)(name, value))
      : this.node()[name];
}

function classArray(string) {
  return string.trim().split(/^|\s+/);
}

function classList(node) {
  return node.classList || new ClassList(node);
}

function ClassList(node) {
  this._node = node;
  this._names = classArray(node.getAttribute("class") || "");
}

ClassList.prototype = {
  add: function(name) {
    var i = this._names.indexOf(name);
    if (i < 0) {
      this._names.push(name);
      this._node.setAttribute("class", this._names.join(" "));
    }
  },
  remove: function(name) {
    var i = this._names.indexOf(name);
    if (i >= 0) {
      this._names.splice(i, 1);
      this._node.setAttribute("class", this._names.join(" "));
    }
  },
  contains: function(name) {
    return this._names.indexOf(name) >= 0;
  }
};

function classedAdd(node, names) {
  var list = classList(node), i = -1, n = names.length;
  while (++i < n) list.add(names[i]);
}

function classedRemove(node, names) {
  var list = classList(node), i = -1, n = names.length;
  while (++i < n) list.remove(names[i]);
}

function classedTrue(names) {
  return function() {
    classedAdd(this, names);
  };
}

function classedFalse(names) {
  return function() {
    classedRemove(this, names);
  };
}

function classedFunction(names, value) {
  return function() {
    (value.apply(this, arguments) ? classedAdd : classedRemove)(this, names);
  };
}

function selection_classed(name, value) {
  var names = classArray(name + "");

  if (arguments.length < 2) {
    var list = classList(this.node()), i = -1, n = names.length;
    while (++i < n) if (!list.contains(names[i])) return false;
    return true;
  }

  return this.each((typeof value === "function"
      ? classedFunction : value
      ? classedTrue
      : classedFalse)(names, value));
}

function textRemove() {
  this.textContent = "";
}

function textConstant$1(value) {
  return function() {
    this.textContent = value;
  };
}

function textFunction$1(value) {
  return function() {
    var v = value.apply(this, arguments);
    this.textContent = v == null ? "" : v;
  };
}

function selection_text(value) {
  return arguments.length
      ? this.each(value == null
          ? textRemove : (typeof value === "function"
          ? textFunction$1
          : textConstant$1)(value))
      : this.node().textContent;
}

function htmlRemove() {
  this.innerHTML = "";
}

function htmlConstant(value) {
  return function() {
    this.innerHTML = value;
  };
}

function htmlFunction(value) {
  return function() {
    var v = value.apply(this, arguments);
    this.innerHTML = v == null ? "" : v;
  };
}

function selection_html(value) {
  return arguments.length
      ? this.each(value == null
          ? htmlRemove : (typeof value === "function"
          ? htmlFunction
          : htmlConstant)(value))
      : this.node().innerHTML;
}

function raise() {
  if (this.nextSibling) this.parentNode.appendChild(this);
}

function selection_raise() {
  return this.each(raise);
}

function lower() {
  if (this.previousSibling) this.parentNode.insertBefore(this, this.parentNode.firstChild);
}

function selection_lower() {
  return this.each(lower);
}

function selection_append(name) {
  var create = typeof name === "function" ? name : creator(name);
  return this.select(function() {
    return this.appendChild(create.apply(this, arguments));
  });
}

function constantNull() {
  return null;
}

function selection_insert(name, before) {
  var create = typeof name === "function" ? name : creator(name),
      select = before == null ? constantNull : typeof before === "function" ? before : selector(before);
  return this.select(function() {
    return this.insertBefore(create.apply(this, arguments), select.apply(this, arguments) || null);
  });
}

function remove() {
  var parent = this.parentNode;
  if (parent) parent.removeChild(this);
}

function selection_remove() {
  return this.each(remove);
}

function selection_cloneShallow() {
  var clone = this.cloneNode(false), parent = this.parentNode;
  return parent ? parent.insertBefore(clone, this.nextSibling) : clone;
}

function selection_cloneDeep() {
  var clone = this.cloneNode(true), parent = this.parentNode;
  return parent ? parent.insertBefore(clone, this.nextSibling) : clone;
}

function selection_clone(deep) {
  return this.select(deep ? selection_cloneDeep : selection_cloneShallow);
}

function selection_datum(value) {
  return arguments.length
      ? this.property("__data__", value)
      : this.node().__data__;
}

function contextListener(listener) {
  return function(event) {
    listener.call(this, event, this.__data__);
  };
}

function parseTypenames(typenames) {
  return typenames.trim().split(/^|\s+/).map(function(t) {
    var name = "", i = t.indexOf(".");
    if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
    return {type: t, name: name};
  });
}

function onRemove(typename) {
  return function() {
    var on = this.__on;
    if (!on) return;
    for (var j = 0, i = -1, m = on.length, o; j < m; ++j) {
      if (o = on[j], (!typename.type || o.type === typename.type) && o.name === typename.name) {
        this.removeEventListener(o.type, o.listener, o.options);
      } else {
        on[++i] = o;
      }
    }
    if (++i) on.length = i;
    else delete this.__on;
  };
}

function onAdd(typename, value, options) {
  return function() {
    var on = this.__on, o, listener = contextListener(value);
    if (on) for (var j = 0, m = on.length; j < m; ++j) {
      if ((o = on[j]).type === typename.type && o.name === typename.name) {
        this.removeEventListener(o.type, o.listener, o.options);
        this.addEventListener(o.type, o.listener = listener, o.options = options);
        o.value = value;
        return;
      }
    }
    this.addEventListener(typename.type, listener, options);
    o = {type: typename.type, name: typename.name, value: value, listener: listener, options: options};
    if (!on) this.__on = [o];
    else on.push(o);
  };
}

function selection_on(typename, value, options) {
  var typenames = parseTypenames(typename + ""), i, n = typenames.length, t;

  if (arguments.length < 2) {
    var on = this.node().__on;
    if (on) for (var j = 0, m = on.length, o; j < m; ++j) {
      for (i = 0, o = on[j]; i < n; ++i) {
        if ((t = typenames[i]).type === o.type && t.name === o.name) {
          return o.value;
        }
      }
    }
    return;
  }

  on = value ? onAdd : onRemove;
  for (i = 0; i < n; ++i) this.each(on(typenames[i], value, options));
  return this;
}

function dispatchEvent(node, type, params) {
  var window = defaultView(node),
      event = window.CustomEvent;

  if (typeof event === "function") {
    event = new event(type, params);
  } else {
    event = window.document.createEvent("Event");
    if (params) event.initEvent(type, params.bubbles, params.cancelable), event.detail = params.detail;
    else event.initEvent(type, false, false);
  }

  node.dispatchEvent(event);
}

function dispatchConstant(type, params) {
  return function() {
    return dispatchEvent(this, type, params);
  };
}

function dispatchFunction(type, params) {
  return function() {
    return dispatchEvent(this, type, params.apply(this, arguments));
  };
}

function selection_dispatch(type, params) {
  return this.each((typeof params === "function"
      ? dispatchFunction
      : dispatchConstant)(type, params));
}

function* selection_iterator() {
  for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
    for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {
      if (node = group[i]) yield node;
    }
  }
}

var root$1 = [null];

function Selection$1(groups, parents) {
  this._groups = groups;
  this._parents = parents;
}

function selection() {
  return new Selection$1([[document.documentElement]], root$1);
}

function selection_selection() {
  return this;
}

Selection$1.prototype = selection.prototype = {
  constructor: Selection$1,
  select: selection_select,
  selectAll: selection_selectAll,
  selectChild: selection_selectChild,
  selectChildren: selection_selectChildren,
  filter: selection_filter,
  data: selection_data,
  enter: selection_enter,
  exit: selection_exit,
  join: selection_join,
  merge: selection_merge,
  selection: selection_selection,
  order: selection_order,
  sort: selection_sort,
  call: selection_call,
  nodes: selection_nodes,
  node: selection_node,
  size: selection_size,
  empty: selection_empty,
  each: selection_each,
  attr: selection_attr,
  style: selection_style,
  property: selection_property,
  classed: selection_classed,
  text: selection_text,
  html: selection_html,
  raise: selection_raise,
  lower: selection_lower,
  append: selection_append,
  insert: selection_insert,
  remove: selection_remove,
  clone: selection_clone,
  datum: selection_datum,
  on: selection_on,
  dispatch: selection_dispatch,
  [Symbol.iterator]: selection_iterator
};

function select(selector) {
  return typeof selector === "string"
      ? new Selection$1([[document.querySelector(selector)]], [document.documentElement])
      : new Selection$1([[selector]], root$1);
}

var nextId = 0;

function Local() {
  this._ = "@" + (++nextId).toString(36);
}

Local.prototype = {
  constructor: Local,
  get: function(node) {
    var id = this._;
    while (!(id in node)) if (!(node = node.parentNode)) return;
    return node[id];
  },
  set: function(node, value) {
    return node[this._] = value;
  },
  remove: function(node) {
    return this._ in node && delete node[this._];
  },
  toString: function() {
    return this._;
  }
};

function sourceEvent(event) {
  let sourceEvent;
  while (sourceEvent = event.sourceEvent) event = sourceEvent;
  return event;
}

function pointer(event, node) {
  event = sourceEvent(event);
  if (node === undefined) node = event.currentTarget;
  if (node) {
    var svg = node.ownerSVGElement || node;
    if (svg.createSVGPoint) {
      var point = svg.createSVGPoint();
      point.x = event.clientX, point.y = event.clientY;
      point = point.matrixTransform(node.getScreenCTM().inverse());
      return [point.x, point.y];
    }
    if (node.getBoundingClientRect) {
      var rect = node.getBoundingClientRect();
      return [event.clientX - rect.left - node.clientLeft, event.clientY - rect.top - node.clientTop];
    }
  }
  return [event.pageX, event.pageY];
}

function pointers(events, node) {
  if (events.target) { // i.e., instanceof Event, not TouchList or iterable
    events = sourceEvent(events);
    if (node === undefined) node = events.currentTarget;
    events = events.touches || [events];
  }
  return Array.from(events, event => pointer(event, node));
}

// These are typically used in conjunction with noevent to ensure that we can
// preventDefault on the event.
const nonpassive = {passive: false};
const nonpassivecapture = {capture: true, passive: false};

function nopropagation(event) {
  event.stopImmediatePropagation();
}

function noevent(event) {
  event.preventDefault();
  event.stopImmediatePropagation();
}

function nodrag(view) {
  var root = view.document.documentElement,
      selection = select(view).on("dragstart.drag", noevent, nonpassivecapture);
  if ("onselectstart" in root) {
    selection.on("selectstart.drag", noevent, nonpassivecapture);
  } else {
    root.__noselect = root.style.MozUserSelect;
    root.style.MozUserSelect = "none";
  }
}

function yesdrag(view, noclick) {
  var root = view.document.documentElement,
      selection = select(view).on("dragstart.drag", null);
  if (noclick) {
    selection.on("click.drag", noevent, nonpassivecapture);
    setTimeout(function() { selection.on("click.drag", null); }, 0);
  }
  if ("onselectstart" in root) {
    selection.on("selectstart.drag", null);
  } else {
    root.style.MozUserSelect = root.__noselect;
    delete root.__noselect;
  }
}

var constant$2 = x => () => x;

function DragEvent(type, {
  sourceEvent,
  subject,
  target,
  identifier,
  active,
  x, y, dx, dy,
  dispatch
}) {
  Object.defineProperties(this, {
    type: {value: type, enumerable: true, configurable: true},
    sourceEvent: {value: sourceEvent, enumerable: true, configurable: true},
    subject: {value: subject, enumerable: true, configurable: true},
    target: {value: target, enumerable: true, configurable: true},
    identifier: {value: identifier, enumerable: true, configurable: true},
    active: {value: active, enumerable: true, configurable: true},
    x: {value: x, enumerable: true, configurable: true},
    y: {value: y, enumerable: true, configurable: true},
    dx: {value: dx, enumerable: true, configurable: true},
    dy: {value: dy, enumerable: true, configurable: true},
    _: {value: dispatch}
  });
}

DragEvent.prototype.on = function() {
  var value = this._.on.apply(this._, arguments);
  return value === this._ ? this : value;
};

// Ignore right-click, since that should open the context menu.
function defaultFilter(event) {
  return !event.ctrlKey && !event.button;
}

function defaultContainer() {
  return this.parentNode;
}

function defaultSubject(event, d) {
  return d == null ? {x: event.x, y: event.y} : d;
}

function defaultTouchable() {
  return navigator.maxTouchPoints || ("ontouchstart" in this);
}

function drag() {
  var filter = defaultFilter,
      container = defaultContainer,
      subject = defaultSubject,
      touchable = defaultTouchable,
      gestures = {},
      listeners = dispatch("start", "drag", "end"),
      active = 0,
      mousedownx,
      mousedowny,
      mousemoving,
      touchending,
      clickDistance2 = 0;

  function drag(selection) {
    selection
        .on("mousedown.drag", mousedowned)
      .filter(touchable)
        .on("touchstart.drag", touchstarted)
        .on("touchmove.drag", touchmoved, nonpassive)
        .on("touchend.drag touchcancel.drag", touchended)
        .style("touch-action", "none")
        .style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
  }

  function mousedowned(event, d) {
    if (touchending || !filter.call(this, event, d)) return;
    var gesture = beforestart(this, container.call(this, event, d), event, d, "mouse");
    if (!gesture) return;
    select(event.view)
      .on("mousemove.drag", mousemoved, nonpassivecapture)
      .on("mouseup.drag", mouseupped, nonpassivecapture);
    nodrag(event.view);
    nopropagation(event);
    mousemoving = false;
    mousedownx = event.clientX;
    mousedowny = event.clientY;
    gesture("start", event);
  }

  function mousemoved(event) {
    noevent(event);
    if (!mousemoving) {
      var dx = event.clientX - mousedownx, dy = event.clientY - mousedowny;
      mousemoving = dx * dx + dy * dy > clickDistance2;
    }
    gestures.mouse("drag", event);
  }

  function mouseupped(event) {
    select(event.view).on("mousemove.drag mouseup.drag", null);
    yesdrag(event.view, mousemoving);
    noevent(event);
    gestures.mouse("end", event);
  }

  function touchstarted(event, d) {
    if (!filter.call(this, event, d)) return;
    var touches = event.changedTouches,
        c = container.call(this, event, d),
        n = touches.length, i, gesture;

    for (i = 0; i < n; ++i) {
      if (gesture = beforestart(this, c, event, d, touches[i].identifier, touches[i])) {
        nopropagation(event);
        gesture("start", event, touches[i]);
      }
    }
  }

  function touchmoved(event) {
    var touches = event.changedTouches,
        n = touches.length, i, gesture;

    for (i = 0; i < n; ++i) {
      if (gesture = gestures[touches[i].identifier]) {
        noevent(event);
        gesture("drag", event, touches[i]);
      }
    }
  }

  function touchended(event) {
    var touches = event.changedTouches,
        n = touches.length, i, gesture;

    if (touchending) clearTimeout(touchending);
    touchending = setTimeout(function() { touchending = null; }, 500); // Ghost clicks are delayed!
    for (i = 0; i < n; ++i) {
      if (gesture = gestures[touches[i].identifier]) {
        nopropagation(event);
        gesture("end", event, touches[i]);
      }
    }
  }

  function beforestart(that, container, event, d, identifier, touch) {
    var dispatch = listeners.copy(),
        p = pointer(touch || event, container), dx, dy,
        s;

    if ((s = subject.call(that, new DragEvent("beforestart", {
        sourceEvent: event,
        target: drag,
        identifier,
        active,
        x: p[0],
        y: p[1],
        dx: 0,
        dy: 0,
        dispatch
      }), d)) == null) return;

    dx = s.x - p[0] || 0;
    dy = s.y - p[1] || 0;

    return function gesture(type, event, touch) {
      var p0 = p, n;
      switch (type) {
        case "start": gestures[identifier] = gesture, n = active++; break;
        case "end": delete gestures[identifier], --active; // falls through
        case "drag": p = pointer(touch || event, container), n = active; break;
      }
      dispatch.call(
        type,
        that,
        new DragEvent(type, {
          sourceEvent: event,
          subject: s,
          target: drag,
          identifier,
          active: n,
          x: p[0] + dx,
          y: p[1] + dy,
          dx: p[0] - p0[0],
          dy: p[1] - p0[1],
          dispatch
        }),
        d
      );
    };
  }

  drag.filter = function(_) {
    return arguments.length ? (filter = typeof _ === "function" ? _ : constant$2(!!_), drag) : filter;
  };

  drag.container = function(_) {
    return arguments.length ? (container = typeof _ === "function" ? _ : constant$2(_), drag) : container;
  };

  drag.subject = function(_) {
    return arguments.length ? (subject = typeof _ === "function" ? _ : constant$2(_), drag) : subject;
  };

  drag.touchable = function(_) {
    return arguments.length ? (touchable = typeof _ === "function" ? _ : constant$2(!!_), drag) : touchable;
  };

  drag.on = function() {
    var value = listeners.on.apply(listeners, arguments);
    return value === listeners ? drag : value;
  };

  drag.clickDistance = function(_) {
    return arguments.length ? (clickDistance2 = (_ = +_) * _, drag) : Math.sqrt(clickDistance2);
  };

  return drag;
}

function ascending(a, b) {
  return a == null || b == null ? NaN : a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
}

function descending(a, b) {
  return a == null || b == null ? NaN
    : b < a ? -1
    : b > a ? 1
    : b >= a ? 0
    : NaN;
}

function bisector(f) {
  let compare1, compare2, delta;

  // If an accessor is specified, promote it to a comparator. In this case we
  // can test whether the search value is (self-) comparable. We can’t do this
  // for a comparator (except for specific, known comparators) because we can’t
  // tell if the comparator is symmetric, and an asymmetric comparator can’t be
  // used to test whether a single value is comparable.
  if (f.length !== 2) {
    compare1 = ascending;
    compare2 = (d, x) => ascending(f(d), x);
    delta = (d, x) => f(d) - x;
  } else {
    compare1 = f === ascending || f === descending ? f : zero$1$1;
    compare2 = f;
    delta = f;
  }

  function left(a, x, lo = 0, hi = a.length) {
    if (lo < hi) {
      if (compare1(x, x) !== 0) return hi;
      do {
        const mid = (lo + hi) >>> 1;
        if (compare2(a[mid], x) < 0) lo = mid + 1;
        else hi = mid;
      } while (lo < hi);
    }
    return lo;
  }

  function right(a, x, lo = 0, hi = a.length) {
    if (lo < hi) {
      if (compare1(x, x) !== 0) return hi;
      do {
        const mid = (lo + hi) >>> 1;
        if (compare2(a[mid], x) <= 0) lo = mid + 1;
        else hi = mid;
      } while (lo < hi);
    }
    return lo;
  }

  function center(a, x, lo = 0, hi = a.length) {
    const i = left(a, x, lo, hi - 1);
    return i > lo && delta(a[i - 1], x) > -delta(a[i], x) ? i - 1 : i;
  }

  return {left, center, right};
}

function zero$1$1() {
  return 0;
}

function number$2(x) {
  return x === null ? NaN : +x;
}

const ascendingBisect = bisector(ascending);
const bisectRight = ascendingBisect.right;
bisector(number$2).center;

const e10 = Math.sqrt(50),
    e5 = Math.sqrt(10),
    e2 = Math.sqrt(2);

function tickSpec(start, stop, count) {
  const step = (stop - start) / Math.max(0, count),
      power = Math.floor(Math.log10(step)),
      error = step / Math.pow(10, power),
      factor = error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1;
  let i1, i2, inc;
  if (power < 0) {
    inc = Math.pow(10, -power) / factor;
    i1 = Math.round(start * inc);
    i2 = Math.round(stop * inc);
    if (i1 / inc < start) ++i1;
    if (i2 / inc > stop) --i2;
    inc = -inc;
  } else {
    inc = Math.pow(10, power) * factor;
    i1 = Math.round(start / inc);
    i2 = Math.round(stop / inc);
    if (i1 * inc < start) ++i1;
    if (i2 * inc > stop) --i2;
  }
  if (i2 < i1 && 0.5 <= count && count < 2) return tickSpec(start, stop, count * 2);
  return [i1, i2, inc];
}

function ticks(start, stop, count) {
  stop = +stop, start = +start, count = +count;
  if (!(count > 0)) return [];
  if (start === stop) return [start];
  const reverse = stop < start, [i1, i2, inc] = reverse ? tickSpec(stop, start, count) : tickSpec(start, stop, count);
  if (!(i2 >= i1)) return [];
  const n = i2 - i1 + 1, ticks = new Array(n);
  if (reverse) {
    if (inc < 0) for (let i = 0; i < n; ++i) ticks[i] = (i2 - i) / -inc;
    else for (let i = 0; i < n; ++i) ticks[i] = (i2 - i) * inc;
  } else {
    if (inc < 0) for (let i = 0; i < n; ++i) ticks[i] = (i1 + i) / -inc;
    else for (let i = 0; i < n; ++i) ticks[i] = (i1 + i) * inc;
  }
  return ticks;
}

function tickIncrement(start, stop, count) {
  stop = +stop, start = +start, count = +count;
  return tickSpec(start, stop, count)[2];
}

function tickStep(start, stop, count) {
  stop = +stop, start = +start, count = +count;
  const reverse = stop < start, inc = reverse ? tickIncrement(stop, start, count) : tickIncrement(start, stop, count);
  return (reverse ? -1 : 1) * (inc < 0 ? 1 / -inc : inc);
}

function initRange(domain, range) {
  switch (arguments.length) {
    case 0: break;
    case 1: this.range(domain); break;
    default: this.range(range).domain(domain); break;
  }
  return this;
}

var constant$1 = x => () => x;

function linear$1(a, d) {
  return function(t) {
    return a + t * d;
  };
}

function exponential(a, b, y) {
  return a = Math.pow(a, y), b = Math.pow(b, y) - a, y = 1 / y, function(t) {
    return Math.pow(a + t * b, y);
  };
}

function gamma$1(y) {
  return (y = +y) === 1 ? nogamma : function(a, b) {
    return b - a ? exponential(a, b, y) : constant$1(isNaN(a) ? b : a);
  };
}

function nogamma(a, b) {
  var d = b - a;
  return d ? linear$1(a, d) : constant$1(isNaN(a) ? b : a);
}

var interpolateRgb = (function rgbGamma(y) {
  var color = gamma$1(y);

  function rgb$1(start, end) {
    var r = color((start = rgb(start)).r, (end = rgb(end)).r),
        g = color(start.g, end.g),
        b = color(start.b, end.b),
        opacity = nogamma(start.opacity, end.opacity);
    return function(t) {
      start.r = r(t);
      start.g = g(t);
      start.b = b(t);
      start.opacity = opacity(t);
      return start + "";
    };
  }

  rgb$1.gamma = rgbGamma;

  return rgb$1;
})(1);

function numberArray(a, b) {
  if (!b) b = [];
  var n = a ? Math.min(b.length, a.length) : 0,
      c = b.slice(),
      i;
  return function(t) {
    for (i = 0; i < n; ++i) c[i] = a[i] * (1 - t) + b[i] * t;
    return c;
  };
}

function isNumberArray(x) {
  return ArrayBuffer.isView(x) && !(x instanceof DataView);
}

function genericArray(a, b) {
  var nb = b ? b.length : 0,
      na = a ? Math.min(nb, a.length) : 0,
      x = new Array(na),
      c = new Array(nb),
      i;

  for (i = 0; i < na; ++i) x[i] = interpolate$1(a[i], b[i]);
  for (; i < nb; ++i) c[i] = b[i];

  return function(t) {
    for (i = 0; i < na; ++i) c[i] = x[i](t);
    return c;
  };
}

function date$1(a, b) {
  var d = new Date;
  return a = +a, b = +b, function(t) {
    return d.setTime(a * (1 - t) + b * t), d;
  };
}

function interpolateNumber(a, b) {
  return a = +a, b = +b, function(t) {
    return a * (1 - t) + b * t;
  };
}

function object(a, b) {
  var i = {},
      c = {},
      k;

  if (a === null || typeof a !== "object") a = {};
  if (b === null || typeof b !== "object") b = {};

  for (k in b) {
    if (k in a) {
      i[k] = interpolate$1(a[k], b[k]);
    } else {
      c[k] = b[k];
    }
  }

  return function(t) {
    for (k in i) c[k] = i[k](t);
    return c;
  };
}

var reA = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,
    reB = new RegExp(reA.source, "g");

function zero(b) {
  return function() {
    return b;
  };
}

function one(b) {
  return function(t) {
    return b(t) + "";
  };
}

function interpolateString(a, b) {
  var bi = reA.lastIndex = reB.lastIndex = 0, // scan index for next number in b
      am, // current match in a
      bm, // current match in b
      bs, // string preceding current number in b, if any
      i = -1, // index in s
      s = [], // string constants and placeholders
      q = []; // number interpolators

  // Coerce inputs to strings.
  a = a + "", b = b + "";

  // Interpolate pairs of numbers in a & b.
  while ((am = reA.exec(a))
      && (bm = reB.exec(b))) {
    if ((bs = bm.index) > bi) { // a string precedes the next number in b
      bs = b.slice(bi, bs);
      if (s[i]) s[i] += bs; // coalesce with previous string
      else s[++i] = bs;
    }
    if ((am = am[0]) === (bm = bm[0])) { // numbers in a & b match
      if (s[i]) s[i] += bm; // coalesce with previous string
      else s[++i] = bm;
    } else { // interpolate non-matching numbers
      s[++i] = null;
      q.push({i: i, x: interpolateNumber(am, bm)});
    }
    bi = reB.lastIndex;
  }

  // Add remains of b.
  if (bi < b.length) {
    bs = b.slice(bi);
    if (s[i]) s[i] += bs; // coalesce with previous string
    else s[++i] = bs;
  }

  // Special optimization for only a single match.
  // Otherwise, interpolate each of the numbers and rejoin the string.
  return s.length < 2 ? (q[0]
      ? one(q[0].x)
      : zero(b))
      : (b = q.length, function(t) {
          for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t);
          return s.join("");
        });
}

function interpolate$1(a, b) {
  var t = typeof b, c;
  return b == null || t === "boolean" ? constant$1(b)
      : (t === "number" ? interpolateNumber
      : t === "string" ? ((c = color(b)) ? (b = c, interpolateRgb) : interpolateString)
      : b instanceof color ? interpolateRgb
      : b instanceof Date ? date$1
      : isNumberArray(b) ? numberArray
      : Array.isArray(b) ? genericArray
      : typeof b.valueOf !== "function" && typeof b.toString !== "function" || isNaN(b) ? object
      : interpolateNumber)(a, b);
}

function interpolateRound(a, b) {
  return a = +a, b = +b, function(t) {
    return Math.round(a * (1 - t) + b * t);
  };
}

var degrees = 180 / Math.PI;

var identity$3 = {
  translateX: 0,
  translateY: 0,
  rotate: 0,
  skewX: 0,
  scaleX: 1,
  scaleY: 1
};

function decompose(a, b, c, d, e, f) {
  var scaleX, scaleY, skewX;
  if (scaleX = Math.sqrt(a * a + b * b)) a /= scaleX, b /= scaleX;
  if (skewX = a * c + b * d) c -= a * skewX, d -= b * skewX;
  if (scaleY = Math.sqrt(c * c + d * d)) c /= scaleY, d /= scaleY, skewX /= scaleY;
  if (a * d < b * c) a = -a, b = -b, skewX = -skewX, scaleX = -scaleX;
  return {
    translateX: e,
    translateY: f,
    rotate: Math.atan2(b, a) * degrees,
    skewX: Math.atan(skewX) * degrees,
    scaleX: scaleX,
    scaleY: scaleY
  };
}

var svgNode;

/* eslint-disable no-undef */
function parseCss(value) {
  const m = new (typeof DOMMatrix === "function" ? DOMMatrix : WebKitCSSMatrix)(value + "");
  return m.isIdentity ? identity$3 : decompose(m.a, m.b, m.c, m.d, m.e, m.f);
}

function parseSvg(value) {
  if (value == null) return identity$3;
  if (!svgNode) svgNode = document.createElementNS("http://www.w3.org/2000/svg", "g");
  svgNode.setAttribute("transform", value);
  if (!(value = svgNode.transform.baseVal.consolidate())) return identity$3;
  value = value.matrix;
  return decompose(value.a, value.b, value.c, value.d, value.e, value.f);
}

function interpolateTransform(parse, pxComma, pxParen, degParen) {

  function pop(s) {
    return s.length ? s.pop() + " " : "";
  }

  function translate(xa, ya, xb, yb, s, q) {
    if (xa !== xb || ya !== yb) {
      var i = s.push("translate(", null, pxComma, null, pxParen);
      q.push({i: i - 4, x: interpolateNumber(xa, xb)}, {i: i - 2, x: interpolateNumber(ya, yb)});
    } else if (xb || yb) {
      s.push("translate(" + xb + pxComma + yb + pxParen);
    }
  }

  function rotate(a, b, s, q) {
    if (a !== b) {
      if (a - b > 180) b += 360; else if (b - a > 180) a += 360; // shortest path
      q.push({i: s.push(pop(s) + "rotate(", null, degParen) - 2, x: interpolateNumber(a, b)});
    } else if (b) {
      s.push(pop(s) + "rotate(" + b + degParen);
    }
  }

  function skewX(a, b, s, q) {
    if (a !== b) {
      q.push({i: s.push(pop(s) + "skewX(", null, degParen) - 2, x: interpolateNumber(a, b)});
    } else if (b) {
      s.push(pop(s) + "skewX(" + b + degParen);
    }
  }

  function scale(xa, ya, xb, yb, s, q) {
    if (xa !== xb || ya !== yb) {
      var i = s.push(pop(s) + "scale(", null, ",", null, ")");
      q.push({i: i - 4, x: interpolateNumber(xa, xb)}, {i: i - 2, x: interpolateNumber(ya, yb)});
    } else if (xb !== 1 || yb !== 1) {
      s.push(pop(s) + "scale(" + xb + "," + yb + ")");
    }
  }

  return function(a, b) {
    var s = [], // string constants and placeholders
        q = []; // number interpolators
    a = parse(a), b = parse(b);
    translate(a.translateX, a.translateY, b.translateX, b.translateY, s, q);
    rotate(a.rotate, b.rotate, s, q);
    skewX(a.skewX, b.skewX, s, q);
    scale(a.scaleX, a.scaleY, b.scaleX, b.scaleY, s, q);
    a = b = null; // gc
    return function(t) {
      var i = -1, n = q.length, o;
      while (++i < n) s[(o = q[i]).i] = o.x(t);
      return s.join("");
    };
  };
}

var interpolateTransformCss = interpolateTransform(parseCss, "px, ", "px)", "deg)");
var interpolateTransformSvg = interpolateTransform(parseSvg, ", ", ")", ")");

function constants(x) {
  return function() {
    return x;
  };
}

function number$1(x) {
  return +x;
}

var unit = [0, 1];

function identity$2(x) {
  return x;
}

function normalize$2(a, b) {
  return (b -= (a = +a))
      ? function(x) { return (x - a) / b; }
      : constants(isNaN(b) ? NaN : 0.5);
}

function clamper(a, b) {
  var t;
  if (a > b) t = a, a = b, b = t;
  return function(x) { return Math.max(a, Math.min(b, x)); };
}

// normalize(a, b)(x) takes a domain value x in [a,b] and returns the corresponding parameter t in [0,1].
// interpolate(a, b)(t) takes a parameter t in [0,1] and returns the corresponding range value x in [a,b].
function bimap(domain, range, interpolate) {
  var d0 = domain[0], d1 = domain[1], r0 = range[0], r1 = range[1];
  if (d1 < d0) d0 = normalize$2(d1, d0), r0 = interpolate(r1, r0);
  else d0 = normalize$2(d0, d1), r0 = interpolate(r0, r1);
  return function(x) { return r0(d0(x)); };
}

function polymap(domain, range, interpolate) {
  var j = Math.min(domain.length, range.length) - 1,
      d = new Array(j),
      r = new Array(j),
      i = -1;

  // Reverse descending domains.
  if (domain[j] < domain[0]) {
    domain = domain.slice().reverse();
    range = range.slice().reverse();
  }

  while (++i < j) {
    d[i] = normalize$2(domain[i], domain[i + 1]);
    r[i] = interpolate(range[i], range[i + 1]);
  }

  return function(x) {
    var i = bisectRight(domain, x, 1, j) - 1;
    return r[i](d[i](x));
  };
}

function copy$1(source, target) {
  return target
      .domain(source.domain())
      .range(source.range())
      .interpolate(source.interpolate())
      .clamp(source.clamp())
      .unknown(source.unknown());
}

function transformer$2() {
  var domain = unit,
      range = unit,
      interpolate = interpolate$1,
      transform,
      untransform,
      unknown,
      clamp = identity$2,
      piecewise,
      output,
      input;

  function rescale() {
    var n = Math.min(domain.length, range.length);
    if (clamp !== identity$2) clamp = clamper(domain[0], domain[n - 1]);
    piecewise = n > 2 ? polymap : bimap;
    output = input = null;
    return scale;
  }

  function scale(x) {
    return x == null || isNaN(x = +x) ? unknown : (output || (output = piecewise(domain.map(transform), range, interpolate)))(transform(clamp(x)));
  }

  scale.invert = function(y) {
    return clamp(untransform((input || (input = piecewise(range, domain.map(transform), interpolateNumber)))(y)));
  };

  scale.domain = function(_) {
    return arguments.length ? (domain = Array.from(_, number$1), rescale()) : domain.slice();
  };

  scale.range = function(_) {
    return arguments.length ? (range = Array.from(_), rescale()) : range.slice();
  };

  scale.rangeRound = function(_) {
    return range = Array.from(_), interpolate = interpolateRound, rescale();
  };

  scale.clamp = function(_) {
    return arguments.length ? (clamp = _ ? true : identity$2, rescale()) : clamp !== identity$2;
  };

  scale.interpolate = function(_) {
    return arguments.length ? (interpolate = _, rescale()) : interpolate;
  };

  scale.unknown = function(_) {
    return arguments.length ? (unknown = _, scale) : unknown;
  };

  return function(t, u) {
    transform = t, untransform = u;
    return rescale();
  };
}

function continuous() {
  return transformer$2()(identity$2, identity$2);
}

function formatDecimal(x) {
  return Math.abs(x = Math.round(x)) >= 1e21
      ? x.toLocaleString("en").replace(/,/g, "")
      : x.toString(10);
}

// Computes the decimal coefficient and exponent of the specified number x with
// significant digits p, where x is positive and p is in [1, 21] or undefined.
// For example, formatDecimalParts(1.23) returns ["123", 0].
function formatDecimalParts(x, p) {
  if ((i = (x = p ? x.toExponential(p - 1) : x.toExponential()).indexOf("e")) < 0) return null; // NaN, ±Infinity
  var i, coefficient = x.slice(0, i);

  // The string returned by toExponential either has the form \d\.\d+e[-+]\d+
  // (e.g., 1.2e+3) or the form \de[-+]\d+ (e.g., 1e+3).
  return [
    coefficient.length > 1 ? coefficient[0] + coefficient.slice(2) : coefficient,
    +x.slice(i + 1)
  ];
}

function exponent(x) {
  return x = formatDecimalParts(Math.abs(x)), x ? x[1] : NaN;
}

function formatGroup(grouping, thousands) {
  return function(value, width) {
    var i = value.length,
        t = [],
        j = 0,
        g = grouping[0],
        length = 0;

    while (i > 0 && g > 0) {
      if (length + g + 1 > width) g = Math.max(1, width - length);
      t.push(value.substring(i -= g, i + g));
      if ((length += g + 1) > width) break;
      g = grouping[j = (j + 1) % grouping.length];
    }

    return t.reverse().join(thousands);
  };
}

function formatNumerals(numerals) {
  return function(value) {
    return value.replace(/[0-9]/g, function(i) {
      return numerals[+i];
    });
  };
}

// [[fill]align][sign][symbol][0][width][,][.precision][~][type]
var re = /^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;

function formatSpecifier(specifier) {
  if (!(match = re.exec(specifier))) throw new Error("invalid format: " + specifier);
  var match;
  return new FormatSpecifier({
    fill: match[1],
    align: match[2],
    sign: match[3],
    symbol: match[4],
    zero: match[5],
    width: match[6],
    comma: match[7],
    precision: match[8] && match[8].slice(1),
    trim: match[9],
    type: match[10]
  });
}

formatSpecifier.prototype = FormatSpecifier.prototype; // instanceof

function FormatSpecifier(specifier) {
  this.fill = specifier.fill === undefined ? " " : specifier.fill + "";
  this.align = specifier.align === undefined ? ">" : specifier.align + "";
  this.sign = specifier.sign === undefined ? "-" : specifier.sign + "";
  this.symbol = specifier.symbol === undefined ? "" : specifier.symbol + "";
  this.zero = !!specifier.zero;
  this.width = specifier.width === undefined ? undefined : +specifier.width;
  this.comma = !!specifier.comma;
  this.precision = specifier.precision === undefined ? undefined : +specifier.precision;
  this.trim = !!specifier.trim;
  this.type = specifier.type === undefined ? "" : specifier.type + "";
}

FormatSpecifier.prototype.toString = function() {
  return this.fill
      + this.align
      + this.sign
      + this.symbol
      + (this.zero ? "0" : "")
      + (this.width === undefined ? "" : Math.max(1, this.width | 0))
      + (this.comma ? "," : "")
      + (this.precision === undefined ? "" : "." + Math.max(0, this.precision | 0))
      + (this.trim ? "~" : "")
      + this.type;
};

// Trims insignificant zeros, e.g., replaces 1.2000k with 1.2k.
function formatTrim(s) {
  out: for (var n = s.length, i = 1, i0 = -1, i1; i < n; ++i) {
    switch (s[i]) {
      case ".": i0 = i1 = i; break;
      case "0": if (i0 === 0) i0 = i; i1 = i; break;
      default: if (!+s[i]) break out; if (i0 > 0) i0 = 0; break;
    }
  }
  return i0 > 0 ? s.slice(0, i0) + s.slice(i1 + 1) : s;
}

var prefixExponent;

function formatPrefixAuto(x, p) {
  var d = formatDecimalParts(x, p);
  if (!d) return x + "";
  var coefficient = d[0],
      exponent = d[1],
      i = exponent - (prefixExponent = Math.max(-8, Math.min(8, Math.floor(exponent / 3))) * 3) + 1,
      n = coefficient.length;
  return i === n ? coefficient
      : i > n ? coefficient + new Array(i - n + 1).join("0")
      : i > 0 ? coefficient.slice(0, i) + "." + coefficient.slice(i)
      : "0." + new Array(1 - i).join("0") + formatDecimalParts(x, Math.max(0, p + i - 1))[0]; // less than 1y!
}

function formatRounded(x, p) {
  var d = formatDecimalParts(x, p);
  if (!d) return x + "";
  var coefficient = d[0],
      exponent = d[1];
  return exponent < 0 ? "0." + new Array(-exponent).join("0") + coefficient
      : coefficient.length > exponent + 1 ? coefficient.slice(0, exponent + 1) + "." + coefficient.slice(exponent + 1)
      : coefficient + new Array(exponent - coefficient.length + 2).join("0");
}

var formatTypes = {
  "%": (x, p) => (x * 100).toFixed(p),
  "b": (x) => Math.round(x).toString(2),
  "c": (x) => x + "",
  "d": formatDecimal,
  "e": (x, p) => x.toExponential(p),
  "f": (x, p) => x.toFixed(p),
  "g": (x, p) => x.toPrecision(p),
  "o": (x) => Math.round(x).toString(8),
  "p": (x, p) => formatRounded(x * 100, p),
  "r": formatRounded,
  "s": formatPrefixAuto,
  "X": (x) => Math.round(x).toString(16).toUpperCase(),
  "x": (x) => Math.round(x).toString(16)
};

function identity$1(x) {
  return x;
}

var map = Array.prototype.map,
    prefixes = ["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];

function formatLocale$1(locale) {
  var group = locale.grouping === undefined || locale.thousands === undefined ? identity$1 : formatGroup(map.call(locale.grouping, Number), locale.thousands + ""),
      currencyPrefix = locale.currency === undefined ? "" : locale.currency[0] + "",
      currencySuffix = locale.currency === undefined ? "" : locale.currency[1] + "",
      decimal = locale.decimal === undefined ? "." : locale.decimal + "",
      numerals = locale.numerals === undefined ? identity$1 : formatNumerals(map.call(locale.numerals, String)),
      percent = locale.percent === undefined ? "%" : locale.percent + "",
      minus = locale.minus === undefined ? "−" : locale.minus + "",
      nan = locale.nan === undefined ? "NaN" : locale.nan + "";

  function newFormat(specifier) {
    specifier = formatSpecifier(specifier);

    var fill = specifier.fill,
        align = specifier.align,
        sign = specifier.sign,
        symbol = specifier.symbol,
        zero = specifier.zero,
        width = specifier.width,
        comma = specifier.comma,
        precision = specifier.precision,
        trim = specifier.trim,
        type = specifier.type;

    // The "n" type is an alias for ",g".
    if (type === "n") comma = true, type = "g";

    // The "" type, and any invalid type, is an alias for ".12~g".
    else if (!formatTypes[type]) precision === undefined && (precision = 12), trim = true, type = "g";

    // If zero fill is specified, padding goes after sign and before digits.
    if (zero || (fill === "0" && align === "=")) zero = true, fill = "0", align = "=";

    // Compute the prefix and suffix.
    // For SI-prefix, the suffix is lazily computed.
    var prefix = symbol === "$" ? currencyPrefix : symbol === "#" && /[boxX]/.test(type) ? "0" + type.toLowerCase() : "",
        suffix = symbol === "$" ? currencySuffix : /[%p]/.test(type) ? percent : "";

    // What format function should we use?
    // Is this an integer type?
    // Can this type generate exponential notation?
    var formatType = formatTypes[type],
        maybeSuffix = /[defgprs%]/.test(type);

    // Set the default precision if not specified,
    // or clamp the specified precision to the supported range.
    // For significant precision, it must be in [1, 21].
    // For fixed precision, it must be in [0, 20].
    precision = precision === undefined ? 6
        : /[gprs]/.test(type) ? Math.max(1, Math.min(21, precision))
        : Math.max(0, Math.min(20, precision));

    function format(value) {
      var valuePrefix = prefix,
          valueSuffix = suffix,
          i, n, c;

      if (type === "c") {
        valueSuffix = formatType(value) + valueSuffix;
        value = "";
      } else {
        value = +value;

        // Determine the sign. -0 is not less than 0, but 1 / -0 is!
        var valueNegative = value < 0 || 1 / value < 0;

        // Perform the initial formatting.
        value = isNaN(value) ? nan : formatType(Math.abs(value), precision);

        // Trim insignificant zeros.
        if (trim) value = formatTrim(value);

        // If a negative value rounds to zero after formatting, and no explicit positive sign is requested, hide the sign.
        if (valueNegative && +value === 0 && sign !== "+") valueNegative = false;

        // Compute the prefix and suffix.
        valuePrefix = (valueNegative ? (sign === "(" ? sign : minus) : sign === "-" || sign === "(" ? "" : sign) + valuePrefix;
        valueSuffix = (type === "s" ? prefixes[8 + prefixExponent / 3] : "") + valueSuffix + (valueNegative && sign === "(" ? ")" : "");

        // Break the formatted value into the integer “value” part that can be
        // grouped, and fractional or exponential “suffix” part that is not.
        if (maybeSuffix) {
          i = -1, n = value.length;
          while (++i < n) {
            if (c = value.charCodeAt(i), 48 > c || c > 57) {
              valueSuffix = (c === 46 ? decimal + value.slice(i + 1) : value.slice(i)) + valueSuffix;
              value = value.slice(0, i);
              break;
            }
          }
        }
      }

      // If the fill character is not "0", grouping is applied before padding.
      if (comma && !zero) value = group(value, Infinity);

      // Compute the padding.
      var length = valuePrefix.length + value.length + valueSuffix.length,
          padding = length < width ? new Array(width - length + 1).join(fill) : "";

      // If the fill character is "0", grouping is applied after padding.
      if (comma && zero) value = group(padding + value, padding.length ? width - valueSuffix.length : Infinity), padding = "";

      // Reconstruct the final output based on the desired alignment.
      switch (align) {
        case "<": value = valuePrefix + value + valueSuffix + padding; break;
        case "=": value = valuePrefix + padding + value + valueSuffix; break;
        case "^": value = padding.slice(0, length = padding.length >> 1) + valuePrefix + value + valueSuffix + padding.slice(length); break;
        default: value = padding + valuePrefix + value + valueSuffix; break;
      }

      return numerals(value);
    }

    format.toString = function() {
      return specifier + "";
    };

    return format;
  }

  function formatPrefix(specifier, value) {
    var f = newFormat((specifier = formatSpecifier(specifier), specifier.type = "f", specifier)),
        e = Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3,
        k = Math.pow(10, -e),
        prefix = prefixes[8 + e / 3];
    return function(value) {
      return f(k * value) + prefix;
    };
  }

  return {
    format: newFormat,
    formatPrefix: formatPrefix
  };
}

var locale$1;
var format;
var formatPrefix;

defaultLocale$1({
  thousands: ",",
  grouping: [3],
  currency: ["$", ""]
});

function defaultLocale$1(definition) {
  locale$1 = formatLocale$1(definition);
  format = locale$1.format;
  formatPrefix = locale$1.formatPrefix;
  return locale$1;
}

function precisionFixed(step) {
  return Math.max(0, -exponent(Math.abs(step)));
}

function precisionPrefix(step, value) {
  return Math.max(0, Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3 - exponent(Math.abs(step)));
}

function precisionRound(step, max) {
  step = Math.abs(step), max = Math.abs(max) - step;
  return Math.max(0, exponent(max) - exponent(step)) + 1;
}

function tickFormat(start, stop, count, specifier) {
  var step = tickStep(start, stop, count),
      precision;
  specifier = formatSpecifier(specifier == null ? ",f" : specifier);
  switch (specifier.type) {
    case "s": {
      var value = Math.max(Math.abs(start), Math.abs(stop));
      if (specifier.precision == null && !isNaN(precision = precisionPrefix(step, value))) specifier.precision = precision;
      return formatPrefix(specifier, value);
    }
    case "":
    case "e":
    case "g":
    case "p":
    case "r": {
      if (specifier.precision == null && !isNaN(precision = precisionRound(step, Math.max(Math.abs(start), Math.abs(stop))))) specifier.precision = precision - (specifier.type === "e");
      break;
    }
    case "f":
    case "%": {
      if (specifier.precision == null && !isNaN(precision = precisionFixed(step))) specifier.precision = precision - (specifier.type === "%") * 2;
      break;
    }
  }
  return format(specifier);
}

function linearish(scale) {
  var domain = scale.domain;

  scale.ticks = function(count) {
    var d = domain();
    return ticks(d[0], d[d.length - 1], count == null ? 10 : count);
  };

  scale.tickFormat = function(count, specifier) {
    var d = domain();
    return tickFormat(d[0], d[d.length - 1], count == null ? 10 : count, specifier);
  };

  scale.nice = function(count) {
    if (count == null) count = 10;

    var d = domain();
    var i0 = 0;
    var i1 = d.length - 1;
    var start = d[i0];
    var stop = d[i1];
    var prestep;
    var step;
    var maxIter = 10;

    if (stop < start) {
      step = start, start = stop, stop = step;
      step = i0, i0 = i1, i1 = step;
    }
    
    while (maxIter-- > 0) {
      step = tickIncrement(start, stop, count);
      if (step === prestep) {
        d[i0] = start;
        d[i1] = stop;
        return domain(d);
      } else if (step > 0) {
        start = Math.floor(start / step) * step;
        stop = Math.ceil(stop / step) * step;
      } else if (step < 0) {
        start = Math.ceil(start * step) / step;
        stop = Math.floor(stop * step) / step;
      } else {
        break;
      }
      prestep = step;
    }

    return scale;
  };

  return scale;
}

function linear() {
  var scale = continuous();

  scale.copy = function() {
    return copy$1(scale, linear());
  };

  initRange.apply(scale, arguments);

  return linearish(scale);
}

function nice(domain, interval) {
  domain = domain.slice();

  var i0 = 0,
      i1 = domain.length - 1,
      x0 = domain[i0],
      x1 = domain[i1],
      t;

  if (x1 < x0) {
    t = i0, i0 = i1, i1 = t;
    t = x0, x0 = x1, x1 = t;
  }

  domain[i0] = interval.floor(x0);
  domain[i1] = interval.ceil(x1);
  return domain;
}

function transformLog(x) {
  return Math.log(x);
}

function transformExp(x) {
  return Math.exp(x);
}

function transformLogn(x) {
  return -Math.log(-x);
}

function transformExpn(x) {
  return -Math.exp(-x);
}

function pow10(x) {
  return isFinite(x) ? +("1e" + x) : x < 0 ? 0 : x;
}

function powp(base) {
  return base === 10 ? pow10
      : base === Math.E ? Math.exp
      : x => Math.pow(base, x);
}

function logp(base) {
  return base === Math.E ? Math.log
      : base === 10 && Math.log10
      || base === 2 && Math.log2
      || (base = Math.log(base), x => Math.log(x) / base);
}

function reflect(f) {
  return (x, k) => -f(-x, k);
}

function loggish(transform) {
  const scale = transform(transformLog, transformExp);
  const domain = scale.domain;
  let base = 10;
  let logs;
  let pows;

  function rescale() {
    logs = logp(base), pows = powp(base);
    if (domain()[0] < 0) {
      logs = reflect(logs), pows = reflect(pows);
      transform(transformLogn, transformExpn);
    } else {
      transform(transformLog, transformExp);
    }
    return scale;
  }

  scale.base = function(_) {
    return arguments.length ? (base = +_, rescale()) : base;
  };

  scale.domain = function(_) {
    return arguments.length ? (domain(_), rescale()) : domain();
  };

  scale.ticks = count => {
    const d = domain();
    let u = d[0];
    let v = d[d.length - 1];
    const r = v < u;

    if (r) ([u, v] = [v, u]);

    let i = logs(u);
    let j = logs(v);
    let k;
    let t;
    const n = count == null ? 10 : +count;
    let z = [];

    if (!(base % 1) && j - i < n) {
      i = Math.floor(i), j = Math.ceil(j);
      if (u > 0) for (; i <= j; ++i) {
        for (k = 1; k < base; ++k) {
          t = i < 0 ? k / pows(-i) : k * pows(i);
          if (t < u) continue;
          if (t > v) break;
          z.push(t);
        }
      } else for (; i <= j; ++i) {
        for (k = base - 1; k >= 1; --k) {
          t = i > 0 ? k / pows(-i) : k * pows(i);
          if (t < u) continue;
          if (t > v) break;
          z.push(t);
        }
      }
      if (z.length * 2 < n) z = ticks(u, v, n);
    } else {
      z = ticks(i, j, Math.min(j - i, n)).map(pows);
    }
    return r ? z.reverse() : z;
  };

  scale.tickFormat = (count, specifier) => {
    if (count == null) count = 10;
    if (specifier == null) specifier = base === 10 ? "s" : ",";
    if (typeof specifier !== "function") {
      if (!(base % 1) && (specifier = formatSpecifier(specifier)).precision == null) specifier.trim = true;
      specifier = format(specifier);
    }
    if (count === Infinity) return specifier;
    const k = Math.max(1, base * count / scale.ticks().length); // TODO fast estimate?
    return d => {
      let i = d / pows(Math.round(logs(d)));
      if (i * base < base - 0.5) i *= base;
      return i <= k ? specifier(d) : "";
    };
  };

  scale.nice = () => {
    return domain(nice(domain(), {
      floor: x => pows(Math.floor(logs(x))),
      ceil: x => pows(Math.ceil(logs(x)))
    }));
  };

  return scale;
}

function log() {
  const scale = loggish(transformer$2()).domain([1, 10]);
  scale.copy = () => copy$1(scale, log()).base(scale.base());
  initRange.apply(scale, arguments);
  return scale;
}

function transformSymlog(c) {
  return function(x) {
    return Math.sign(x) * Math.log1p(Math.abs(x / c));
  };
}

function transformSymexp(c) {
  return function(x) {
    return Math.sign(x) * Math.expm1(Math.abs(x)) * c;
  };
}

function symlogish(transform) {
  var c = 1, scale = transform(transformSymlog(c), transformSymexp(c));

  scale.constant = function(_) {
    return arguments.length ? transform(transformSymlog(c = +_), transformSymexp(c)) : c;
  };

  return linearish(scale);
}

function symlog() {
  var scale = symlogish(transformer$2());

  scale.copy = function() {
    return copy$1(scale, symlog()).constant(scale.constant());
  };

  return initRange.apply(scale, arguments);
}

const t0 = new Date, t1 = new Date;

function timeInterval(floori, offseti, count, field) {

  function interval(date) {
    return floori(date = arguments.length === 0 ? new Date : new Date(+date)), date;
  }

  interval.floor = (date) => {
    return floori(date = new Date(+date)), date;
  };

  interval.ceil = (date) => {
    return floori(date = new Date(date - 1)), offseti(date, 1), floori(date), date;
  };

  interval.round = (date) => {
    const d0 = interval(date), d1 = interval.ceil(date);
    return date - d0 < d1 - date ? d0 : d1;
  };

  interval.offset = (date, step) => {
    return offseti(date = new Date(+date), step == null ? 1 : Math.floor(step)), date;
  };

  interval.range = (start, stop, step) => {
    const range = [];
    start = interval.ceil(start);
    step = step == null ? 1 : Math.floor(step);
    if (!(start < stop) || !(step > 0)) return range; // also handles Invalid Date
    let previous;
    do range.push(previous = new Date(+start)), offseti(start, step), floori(start);
    while (previous < start && start < stop);
    return range;
  };

  interval.filter = (test) => {
    return timeInterval((date) => {
      if (date >= date) while (floori(date), !test(date)) date.setTime(date - 1);
    }, (date, step) => {
      if (date >= date) {
        if (step < 0) while (++step <= 0) {
          while (offseti(date, -1), !test(date)) {} // eslint-disable-line no-empty
        } else while (--step >= 0) {
          while (offseti(date, 1), !test(date)) {} // eslint-disable-line no-empty
        }
      }
    });
  };

  if (count) {
    interval.count = (start, end) => {
      t0.setTime(+start), t1.setTime(+end);
      floori(t0), floori(t1);
      return Math.floor(count(t0, t1));
    };

    interval.every = (step) => {
      step = Math.floor(step);
      return !isFinite(step) || !(step > 0) ? null
          : !(step > 1) ? interval
          : interval.filter(field
              ? (d) => field(d) % step === 0
              : (d) => interval.count(0, d) % step === 0);
    };
  }

  return interval;
}

const millisecond = timeInterval(() => {
  // noop
}, (date, step) => {
  date.setTime(+date + step);
}, (start, end) => {
  return end - start;
});

// An optimized implementation for this simple case.
millisecond.every = (k) => {
  k = Math.floor(k);
  if (!isFinite(k) || !(k > 0)) return null;
  if (!(k > 1)) return millisecond;
  return timeInterval((date) => {
    date.setTime(Math.floor(date / k) * k);
  }, (date, step) => {
    date.setTime(+date + step * k);
  }, (start, end) => {
    return (end - start) / k;
  });
};

millisecond.range;

const durationSecond = 1000;
const durationMinute = durationSecond * 60;
const durationHour = durationMinute * 60;
const durationDay = durationHour * 24;
const durationWeek = durationDay * 7;
const durationMonth = durationDay * 30;
const durationYear = durationDay * 365;

const second = timeInterval((date) => {
  date.setTime(date - date.getMilliseconds());
}, (date, step) => {
  date.setTime(+date + step * durationSecond);
}, (start, end) => {
  return (end - start) / durationSecond;
}, (date) => {
  return date.getUTCSeconds();
});

second.range;

const timeMinute = timeInterval((date) => {
  date.setTime(date - date.getMilliseconds() - date.getSeconds() * durationSecond);
}, (date, step) => {
  date.setTime(+date + step * durationMinute);
}, (start, end) => {
  return (end - start) / durationMinute;
}, (date) => {
  return date.getMinutes();
});

timeMinute.range;

const utcMinute = timeInterval((date) => {
  date.setUTCSeconds(0, 0);
}, (date, step) => {
  date.setTime(+date + step * durationMinute);
}, (start, end) => {
  return (end - start) / durationMinute;
}, (date) => {
  return date.getUTCMinutes();
});

utcMinute.range;

const timeHour = timeInterval((date) => {
  date.setTime(date - date.getMilliseconds() - date.getSeconds() * durationSecond - date.getMinutes() * durationMinute);
}, (date, step) => {
  date.setTime(+date + step * durationHour);
}, (start, end) => {
  return (end - start) / durationHour;
}, (date) => {
  return date.getHours();
});

timeHour.range;

const utcHour = timeInterval((date) => {
  date.setUTCMinutes(0, 0, 0);
}, (date, step) => {
  date.setTime(+date + step * durationHour);
}, (start, end) => {
  return (end - start) / durationHour;
}, (date) => {
  return date.getUTCHours();
});

utcHour.range;

const timeDay = timeInterval(
  date => date.setHours(0, 0, 0, 0),
  (date, step) => date.setDate(date.getDate() + step),
  (start, end) => (end - start - (end.getTimezoneOffset() - start.getTimezoneOffset()) * durationMinute) / durationDay,
  date => date.getDate() - 1
);

timeDay.range;

const utcDay = timeInterval((date) => {
  date.setUTCHours(0, 0, 0, 0);
}, (date, step) => {
  date.setUTCDate(date.getUTCDate() + step);
}, (start, end) => {
  return (end - start) / durationDay;
}, (date) => {
  return date.getUTCDate() - 1;
});

utcDay.range;

const unixDay = timeInterval((date) => {
  date.setUTCHours(0, 0, 0, 0);
}, (date, step) => {
  date.setUTCDate(date.getUTCDate() + step);
}, (start, end) => {
  return (end - start) / durationDay;
}, (date) => {
  return Math.floor(date / durationDay);
});

unixDay.range;

function timeWeekday(i) {
  return timeInterval((date) => {
    date.setDate(date.getDate() - (date.getDay() + 7 - i) % 7);
    date.setHours(0, 0, 0, 0);
  }, (date, step) => {
    date.setDate(date.getDate() + step * 7);
  }, (start, end) => {
    return (end - start - (end.getTimezoneOffset() - start.getTimezoneOffset()) * durationMinute) / durationWeek;
  });
}

const timeSunday = timeWeekday(0);
const timeMonday = timeWeekday(1);
const timeTuesday = timeWeekday(2);
const timeWednesday = timeWeekday(3);
const timeThursday = timeWeekday(4);
const timeFriday = timeWeekday(5);
const timeSaturday = timeWeekday(6);

timeSunday.range;
timeMonday.range;
timeTuesday.range;
timeWednesday.range;
timeThursday.range;
timeFriday.range;
timeSaturday.range;

function utcWeekday(i) {
  return timeInterval((date) => {
    date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 7 - i) % 7);
    date.setUTCHours(0, 0, 0, 0);
  }, (date, step) => {
    date.setUTCDate(date.getUTCDate() + step * 7);
  }, (start, end) => {
    return (end - start) / durationWeek;
  });
}

const utcSunday = utcWeekday(0);
const utcMonday = utcWeekday(1);
const utcTuesday = utcWeekday(2);
const utcWednesday = utcWeekday(3);
const utcThursday = utcWeekday(4);
const utcFriday = utcWeekday(5);
const utcSaturday = utcWeekday(6);

utcSunday.range;
utcMonday.range;
utcTuesday.range;
utcWednesday.range;
utcThursday.range;
utcFriday.range;
utcSaturday.range;

const timeMonth = timeInterval((date) => {
  date.setDate(1);
  date.setHours(0, 0, 0, 0);
}, (date, step) => {
  date.setMonth(date.getMonth() + step);
}, (start, end) => {
  return end.getMonth() - start.getMonth() + (end.getFullYear() - start.getFullYear()) * 12;
}, (date) => {
  return date.getMonth();
});

timeMonth.range;

const utcMonth = timeInterval((date) => {
  date.setUTCDate(1);
  date.setUTCHours(0, 0, 0, 0);
}, (date, step) => {
  date.setUTCMonth(date.getUTCMonth() + step);
}, (start, end) => {
  return end.getUTCMonth() - start.getUTCMonth() + (end.getUTCFullYear() - start.getUTCFullYear()) * 12;
}, (date) => {
  return date.getUTCMonth();
});

utcMonth.range;

const timeYear = timeInterval((date) => {
  date.setMonth(0, 1);
  date.setHours(0, 0, 0, 0);
}, (date, step) => {
  date.setFullYear(date.getFullYear() + step);
}, (start, end) => {
  return end.getFullYear() - start.getFullYear();
}, (date) => {
  return date.getFullYear();
});

// An optimized implementation for this simple case.
timeYear.every = (k) => {
  return !isFinite(k = Math.floor(k)) || !(k > 0) ? null : timeInterval((date) => {
    date.setFullYear(Math.floor(date.getFullYear() / k) * k);
    date.setMonth(0, 1);
    date.setHours(0, 0, 0, 0);
  }, (date, step) => {
    date.setFullYear(date.getFullYear() + step * k);
  });
};

timeYear.range;

const utcYear = timeInterval((date) => {
  date.setUTCMonth(0, 1);
  date.setUTCHours(0, 0, 0, 0);
}, (date, step) => {
  date.setUTCFullYear(date.getUTCFullYear() + step);
}, (start, end) => {
  return end.getUTCFullYear() - start.getUTCFullYear();
}, (date) => {
  return date.getUTCFullYear();
});

// An optimized implementation for this simple case.
utcYear.every = (k) => {
  return !isFinite(k = Math.floor(k)) || !(k > 0) ? null : timeInterval((date) => {
    date.setUTCFullYear(Math.floor(date.getUTCFullYear() / k) * k);
    date.setUTCMonth(0, 1);
    date.setUTCHours(0, 0, 0, 0);
  }, (date, step) => {
    date.setUTCFullYear(date.getUTCFullYear() + step * k);
  });
};

utcYear.range;

function ticker(year, month, week, day, hour, minute) {

  const tickIntervals = [
    [second,  1,      durationSecond],
    [second,  5,  5 * durationSecond],
    [second, 15, 15 * durationSecond],
    [second, 30, 30 * durationSecond],
    [minute,  1,      durationMinute],
    [minute,  5,  5 * durationMinute],
    [minute, 15, 15 * durationMinute],
    [minute, 30, 30 * durationMinute],
    [  hour,  1,      durationHour  ],
    [  hour,  3,  3 * durationHour  ],
    [  hour,  6,  6 * durationHour  ],
    [  hour, 12, 12 * durationHour  ],
    [   day,  1,      durationDay   ],
    [   day,  2,  2 * durationDay   ],
    [  week,  1,      durationWeek  ],
    [ month,  1,      durationMonth ],
    [ month,  3,  3 * durationMonth ],
    [  year,  1,      durationYear  ]
  ];

  function ticks(start, stop, count) {
    const reverse = stop < start;
    if (reverse) [start, stop] = [stop, start];
    const interval = count && typeof count.range === "function" ? count : tickInterval(start, stop, count);
    const ticks = interval ? interval.range(start, +stop + 1) : []; // inclusive stop
    return reverse ? ticks.reverse() : ticks;
  }

  function tickInterval(start, stop, count) {
    const target = Math.abs(stop - start) / count;
    const i = bisector(([,, step]) => step).right(tickIntervals, target);
    if (i === tickIntervals.length) return year.every(tickStep(start / durationYear, stop / durationYear, count));
    if (i === 0) return millisecond.every(Math.max(tickStep(start, stop, count), 1));
    const [t, step] = tickIntervals[target / tickIntervals[i - 1][2] < tickIntervals[i][2] / target ? i - 1 : i];
    return t.every(step);
  }

  return [ticks, tickInterval];
}
const [timeTicks, timeTickInterval] = ticker(timeYear, timeMonth, timeSunday, timeDay, timeHour, timeMinute);

function localDate(d) {
  if (0 <= d.y && d.y < 100) {
    var date = new Date(-1, d.m, d.d, d.H, d.M, d.S, d.L);
    date.setFullYear(d.y);
    return date;
  }
  return new Date(d.y, d.m, d.d, d.H, d.M, d.S, d.L);
}

function utcDate(d) {
  if (0 <= d.y && d.y < 100) {
    var date = new Date(Date.UTC(-1, d.m, d.d, d.H, d.M, d.S, d.L));
    date.setUTCFullYear(d.y);
    return date;
  }
  return new Date(Date.UTC(d.y, d.m, d.d, d.H, d.M, d.S, d.L));
}

function newDate(y, m, d) {
  return {y: y, m: m, d: d, H: 0, M: 0, S: 0, L: 0};
}

function formatLocale(locale) {
  var locale_dateTime = locale.dateTime,
      locale_date = locale.date,
      locale_time = locale.time,
      locale_periods = locale.periods,
      locale_weekdays = locale.days,
      locale_shortWeekdays = locale.shortDays,
      locale_months = locale.months,
      locale_shortMonths = locale.shortMonths;

  var periodRe = formatRe(locale_periods),
      periodLookup = formatLookup(locale_periods),
      weekdayRe = formatRe(locale_weekdays),
      weekdayLookup = formatLookup(locale_weekdays),
      shortWeekdayRe = formatRe(locale_shortWeekdays),
      shortWeekdayLookup = formatLookup(locale_shortWeekdays),
      monthRe = formatRe(locale_months),
      monthLookup = formatLookup(locale_months),
      shortMonthRe = formatRe(locale_shortMonths),
      shortMonthLookup = formatLookup(locale_shortMonths);

  var formats = {
    "a": formatShortWeekday,
    "A": formatWeekday,
    "b": formatShortMonth,
    "B": formatMonth,
    "c": null,
    "d": formatDayOfMonth,
    "e": formatDayOfMonth,
    "f": formatMicroseconds,
    "g": formatYearISO,
    "G": formatFullYearISO,
    "H": formatHour24,
    "I": formatHour12,
    "j": formatDayOfYear,
    "L": formatMilliseconds,
    "m": formatMonthNumber,
    "M": formatMinutes,
    "p": formatPeriod,
    "q": formatQuarter,
    "Q": formatUnixTimestamp,
    "s": formatUnixTimestampSeconds,
    "S": formatSeconds,
    "u": formatWeekdayNumberMonday,
    "U": formatWeekNumberSunday,
    "V": formatWeekNumberISO,
    "w": formatWeekdayNumberSunday,
    "W": formatWeekNumberMonday,
    "x": null,
    "X": null,
    "y": formatYear,
    "Y": formatFullYear,
    "Z": formatZone,
    "%": formatLiteralPercent
  };

  var utcFormats = {
    "a": formatUTCShortWeekday,
    "A": formatUTCWeekday,
    "b": formatUTCShortMonth,
    "B": formatUTCMonth,
    "c": null,
    "d": formatUTCDayOfMonth,
    "e": formatUTCDayOfMonth,
    "f": formatUTCMicroseconds,
    "g": formatUTCYearISO,
    "G": formatUTCFullYearISO,
    "H": formatUTCHour24,
    "I": formatUTCHour12,
    "j": formatUTCDayOfYear,
    "L": formatUTCMilliseconds,
    "m": formatUTCMonthNumber,
    "M": formatUTCMinutes,
    "p": formatUTCPeriod,
    "q": formatUTCQuarter,
    "Q": formatUnixTimestamp,
    "s": formatUnixTimestampSeconds,
    "S": formatUTCSeconds,
    "u": formatUTCWeekdayNumberMonday,
    "U": formatUTCWeekNumberSunday,
    "V": formatUTCWeekNumberISO,
    "w": formatUTCWeekdayNumberSunday,
    "W": formatUTCWeekNumberMonday,
    "x": null,
    "X": null,
    "y": formatUTCYear,
    "Y": formatUTCFullYear,
    "Z": formatUTCZone,
    "%": formatLiteralPercent
  };

  var parses = {
    "a": parseShortWeekday,
    "A": parseWeekday,
    "b": parseShortMonth,
    "B": parseMonth,
    "c": parseLocaleDateTime,
    "d": parseDayOfMonth,
    "e": parseDayOfMonth,
    "f": parseMicroseconds,
    "g": parseYear,
    "G": parseFullYear,
    "H": parseHour24,
    "I": parseHour24,
    "j": parseDayOfYear,
    "L": parseMilliseconds,
    "m": parseMonthNumber,
    "M": parseMinutes,
    "p": parsePeriod,
    "q": parseQuarter,
    "Q": parseUnixTimestamp,
    "s": parseUnixTimestampSeconds,
    "S": parseSeconds,
    "u": parseWeekdayNumberMonday,
    "U": parseWeekNumberSunday,
    "V": parseWeekNumberISO,
    "w": parseWeekdayNumberSunday,
    "W": parseWeekNumberMonday,
    "x": parseLocaleDate,
    "X": parseLocaleTime,
    "y": parseYear,
    "Y": parseFullYear,
    "Z": parseZone,
    "%": parseLiteralPercent
  };

  // These recursive directive definitions must be deferred.
  formats.x = newFormat(locale_date, formats);
  formats.X = newFormat(locale_time, formats);
  formats.c = newFormat(locale_dateTime, formats);
  utcFormats.x = newFormat(locale_date, utcFormats);
  utcFormats.X = newFormat(locale_time, utcFormats);
  utcFormats.c = newFormat(locale_dateTime, utcFormats);

  function newFormat(specifier, formats) {
    return function(date) {
      var string = [],
          i = -1,
          j = 0,
          n = specifier.length,
          c,
          pad,
          format;

      if (!(date instanceof Date)) date = new Date(+date);

      while (++i < n) {
        if (specifier.charCodeAt(i) === 37) {
          string.push(specifier.slice(j, i));
          if ((pad = pads[c = specifier.charAt(++i)]) != null) c = specifier.charAt(++i);
          else pad = c === "e" ? " " : "0";
          if (format = formats[c]) c = format(date, pad);
          string.push(c);
          j = i + 1;
        }
      }

      string.push(specifier.slice(j, i));
      return string.join("");
    };
  }

  function newParse(specifier, Z) {
    return function(string) {
      var d = newDate(1900, undefined, 1),
          i = parseSpecifier(d, specifier, string += "", 0),
          week, day;
      if (i != string.length) return null;

      // If a UNIX timestamp is specified, return it.
      if ("Q" in d) return new Date(d.Q);
      if ("s" in d) return new Date(d.s * 1000 + ("L" in d ? d.L : 0));

      // If this is utcParse, never use the local timezone.
      if (Z && !("Z" in d)) d.Z = 0;

      // The am-pm flag is 0 for AM, and 1 for PM.
      if ("p" in d) d.H = d.H % 12 + d.p * 12;

      // If the month was not specified, inherit from the quarter.
      if (d.m === undefined) d.m = "q" in d ? d.q : 0;

      // Convert day-of-week and week-of-year to day-of-year.
      if ("V" in d) {
        if (d.V < 1 || d.V > 53) return null;
        if (!("w" in d)) d.w = 1;
        if ("Z" in d) {
          week = utcDate(newDate(d.y, 0, 1)), day = week.getUTCDay();
          week = day > 4 || day === 0 ? utcMonday.ceil(week) : utcMonday(week);
          week = utcDay.offset(week, (d.V - 1) * 7);
          d.y = week.getUTCFullYear();
          d.m = week.getUTCMonth();
          d.d = week.getUTCDate() + (d.w + 6) % 7;
        } else {
          week = localDate(newDate(d.y, 0, 1)), day = week.getDay();
          week = day > 4 || day === 0 ? timeMonday.ceil(week) : timeMonday(week);
          week = timeDay.offset(week, (d.V - 1) * 7);
          d.y = week.getFullYear();
          d.m = week.getMonth();
          d.d = week.getDate() + (d.w + 6) % 7;
        }
      } else if ("W" in d || "U" in d) {
        if (!("w" in d)) d.w = "u" in d ? d.u % 7 : "W" in d ? 1 : 0;
        day = "Z" in d ? utcDate(newDate(d.y, 0, 1)).getUTCDay() : localDate(newDate(d.y, 0, 1)).getDay();
        d.m = 0;
        d.d = "W" in d ? (d.w + 6) % 7 + d.W * 7 - (day + 5) % 7 : d.w + d.U * 7 - (day + 6) % 7;
      }

      // If a time zone is specified, all fields are interpreted as UTC and then
      // offset according to the specified time zone.
      if ("Z" in d) {
        d.H += d.Z / 100 | 0;
        d.M += d.Z % 100;
        return utcDate(d);
      }

      // Otherwise, all fields are in local time.
      return localDate(d);
    };
  }

  function parseSpecifier(d, specifier, string, j) {
    var i = 0,
        n = specifier.length,
        m = string.length,
        c,
        parse;

    while (i < n) {
      if (j >= m) return -1;
      c = specifier.charCodeAt(i++);
      if (c === 37) {
        c = specifier.charAt(i++);
        parse = parses[c in pads ? specifier.charAt(i++) : c];
        if (!parse || ((j = parse(d, string, j)) < 0)) return -1;
      } else if (c != string.charCodeAt(j++)) {
        return -1;
      }
    }

    return j;
  }

  function parsePeriod(d, string, i) {
    var n = periodRe.exec(string.slice(i));
    return n ? (d.p = periodLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
  }

  function parseShortWeekday(d, string, i) {
    var n = shortWeekdayRe.exec(string.slice(i));
    return n ? (d.w = shortWeekdayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
  }

  function parseWeekday(d, string, i) {
    var n = weekdayRe.exec(string.slice(i));
    return n ? (d.w = weekdayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
  }

  function parseShortMonth(d, string, i) {
    var n = shortMonthRe.exec(string.slice(i));
    return n ? (d.m = shortMonthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
  }

  function parseMonth(d, string, i) {
    var n = monthRe.exec(string.slice(i));
    return n ? (d.m = monthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
  }

  function parseLocaleDateTime(d, string, i) {
    return parseSpecifier(d, locale_dateTime, string, i);
  }

  function parseLocaleDate(d, string, i) {
    return parseSpecifier(d, locale_date, string, i);
  }

  function parseLocaleTime(d, string, i) {
    return parseSpecifier(d, locale_time, string, i);
  }

  function formatShortWeekday(d) {
    return locale_shortWeekdays[d.getDay()];
  }

  function formatWeekday(d) {
    return locale_weekdays[d.getDay()];
  }

  function formatShortMonth(d) {
    return locale_shortMonths[d.getMonth()];
  }

  function formatMonth(d) {
    return locale_months[d.getMonth()];
  }

  function formatPeriod(d) {
    return locale_periods[+(d.getHours() >= 12)];
  }

  function formatQuarter(d) {
    return 1 + ~~(d.getMonth() / 3);
  }

  function formatUTCShortWeekday(d) {
    return locale_shortWeekdays[d.getUTCDay()];
  }

  function formatUTCWeekday(d) {
    return locale_weekdays[d.getUTCDay()];
  }

  function formatUTCShortMonth(d) {
    return locale_shortMonths[d.getUTCMonth()];
  }

  function formatUTCMonth(d) {
    return locale_months[d.getUTCMonth()];
  }

  function formatUTCPeriod(d) {
    return locale_periods[+(d.getUTCHours() >= 12)];
  }

  function formatUTCQuarter(d) {
    return 1 + ~~(d.getUTCMonth() / 3);
  }

  return {
    format: function(specifier) {
      var f = newFormat(specifier += "", formats);
      f.toString = function() { return specifier; };
      return f;
    },
    parse: function(specifier) {
      var p = newParse(specifier += "", false);
      p.toString = function() { return specifier; };
      return p;
    },
    utcFormat: function(specifier) {
      var f = newFormat(specifier += "", utcFormats);
      f.toString = function() { return specifier; };
      return f;
    },
    utcParse: function(specifier) {
      var p = newParse(specifier += "", true);
      p.toString = function() { return specifier; };
      return p;
    }
  };
}

var pads = {"-": "", "_": " ", "0": "0"},
    numberRe = /^\s*\d+/, // note: ignores next directive
    percentRe = /^%/,
    requoteRe = /[\\^$*+?|[\]().{}]/g;

function pad(value, fill, width) {
  var sign = value < 0 ? "-" : "",
      string = (sign ? -value : value) + "",
      length = string.length;
  return sign + (length < width ? new Array(width - length + 1).join(fill) + string : string);
}

function requote(s) {
  return s.replace(requoteRe, "\\$&");
}

function formatRe(names) {
  return new RegExp("^(?:" + names.map(requote).join("|") + ")", "i");
}

function formatLookup(names) {
  return new Map(names.map((name, i) => [name.toLowerCase(), i]));
}

function parseWeekdayNumberSunday(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 1));
  return n ? (d.w = +n[0], i + n[0].length) : -1;
}

function parseWeekdayNumberMonday(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 1));
  return n ? (d.u = +n[0], i + n[0].length) : -1;
}

function parseWeekNumberSunday(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 2));
  return n ? (d.U = +n[0], i + n[0].length) : -1;
}

function parseWeekNumberISO(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 2));
  return n ? (d.V = +n[0], i + n[0].length) : -1;
}

function parseWeekNumberMonday(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 2));
  return n ? (d.W = +n[0], i + n[0].length) : -1;
}

function parseFullYear(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 4));
  return n ? (d.y = +n[0], i + n[0].length) : -1;
}

function parseYear(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 2));
  return n ? (d.y = +n[0] + (+n[0] > 68 ? 1900 : 2000), i + n[0].length) : -1;
}

function parseZone(d, string, i) {
  var n = /^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(string.slice(i, i + 6));
  return n ? (d.Z = n[1] ? 0 : -(n[2] + (n[3] || "00")), i + n[0].length) : -1;
}

function parseQuarter(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 1));
  return n ? (d.q = n[0] * 3 - 3, i + n[0].length) : -1;
}

function parseMonthNumber(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 2));
  return n ? (d.m = n[0] - 1, i + n[0].length) : -1;
}

function parseDayOfMonth(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 2));
  return n ? (d.d = +n[0], i + n[0].length) : -1;
}

function parseDayOfYear(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 3));
  return n ? (d.m = 0, d.d = +n[0], i + n[0].length) : -1;
}

function parseHour24(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 2));
  return n ? (d.H = +n[0], i + n[0].length) : -1;
}

function parseMinutes(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 2));
  return n ? (d.M = +n[0], i + n[0].length) : -1;
}

function parseSeconds(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 2));
  return n ? (d.S = +n[0], i + n[0].length) : -1;
}

function parseMilliseconds(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 3));
  return n ? (d.L = +n[0], i + n[0].length) : -1;
}

function parseMicroseconds(d, string, i) {
  var n = numberRe.exec(string.slice(i, i + 6));
  return n ? (d.L = Math.floor(n[0] / 1000), i + n[0].length) : -1;
}

function parseLiteralPercent(d, string, i) {
  var n = percentRe.exec(string.slice(i, i + 1));
  return n ? i + n[0].length : -1;
}

function parseUnixTimestamp(d, string, i) {
  var n = numberRe.exec(string.slice(i));
  return n ? (d.Q = +n[0], i + n[0].length) : -1;
}

function parseUnixTimestampSeconds(d, string, i) {
  var n = numberRe.exec(string.slice(i));
  return n ? (d.s = +n[0], i + n[0].length) : -1;
}

function formatDayOfMonth(d, p) {
  return pad(d.getDate(), p, 2);
}

function formatHour24(d, p) {
  return pad(d.getHours(), p, 2);
}

function formatHour12(d, p) {
  return pad(d.getHours() % 12 || 12, p, 2);
}

function formatDayOfYear(d, p) {
  return pad(1 + timeDay.count(timeYear(d), d), p, 3);
}

function formatMilliseconds(d, p) {
  return pad(d.getMilliseconds(), p, 3);
}

function formatMicroseconds(d, p) {
  return formatMilliseconds(d, p) + "000";
}

function formatMonthNumber(d, p) {
  return pad(d.getMonth() + 1, p, 2);
}

function formatMinutes(d, p) {
  return pad(d.getMinutes(), p, 2);
}

function formatSeconds(d, p) {
  return pad(d.getSeconds(), p, 2);
}

function formatWeekdayNumberMonday(d) {
  var day = d.getDay();
  return day === 0 ? 7 : day;
}

function formatWeekNumberSunday(d, p) {
  return pad(timeSunday.count(timeYear(d) - 1, d), p, 2);
}

function dISO(d) {
  var day = d.getDay();
  return (day >= 4 || day === 0) ? timeThursday(d) : timeThursday.ceil(d);
}

function formatWeekNumberISO(d, p) {
  d = dISO(d);
  return pad(timeThursday.count(timeYear(d), d) + (timeYear(d).getDay() === 4), p, 2);
}

function formatWeekdayNumberSunday(d) {
  return d.getDay();
}

function formatWeekNumberMonday(d, p) {
  return pad(timeMonday.count(timeYear(d) - 1, d), p, 2);
}

function formatYear(d, p) {
  return pad(d.getFullYear() % 100, p, 2);
}

function formatYearISO(d, p) {
  d = dISO(d);
  return pad(d.getFullYear() % 100, p, 2);
}

function formatFullYear(d, p) {
  return pad(d.getFullYear() % 10000, p, 4);
}

function formatFullYearISO(d, p) {
  var day = d.getDay();
  d = (day >= 4 || day === 0) ? timeThursday(d) : timeThursday.ceil(d);
  return pad(d.getFullYear() % 10000, p, 4);
}

function formatZone(d) {
  var z = d.getTimezoneOffset();
  return (z > 0 ? "-" : (z *= -1, "+"))
      + pad(z / 60 | 0, "0", 2)
      + pad(z % 60, "0", 2);
}

function formatUTCDayOfMonth(d, p) {
  return pad(d.getUTCDate(), p, 2);
}

function formatUTCHour24(d, p) {
  return pad(d.getUTCHours(), p, 2);
}

function formatUTCHour12(d, p) {
  return pad(d.getUTCHours() % 12 || 12, p, 2);
}

function formatUTCDayOfYear(d, p) {
  return pad(1 + utcDay.count(utcYear(d), d), p, 3);
}

function formatUTCMilliseconds(d, p) {
  return pad(d.getUTCMilliseconds(), p, 3);
}

function formatUTCMicroseconds(d, p) {
  return formatUTCMilliseconds(d, p) + "000";
}

function formatUTCMonthNumber(d, p) {
  return pad(d.getUTCMonth() + 1, p, 2);
}

function formatUTCMinutes(d, p) {
  return pad(d.getUTCMinutes(), p, 2);
}

function formatUTCSeconds(d, p) {
  return pad(d.getUTCSeconds(), p, 2);
}

function formatUTCWeekdayNumberMonday(d) {
  var dow = d.getUTCDay();
  return dow === 0 ? 7 : dow;
}

function formatUTCWeekNumberSunday(d, p) {
  return pad(utcSunday.count(utcYear(d) - 1, d), p, 2);
}

function UTCdISO(d) {
  var day = d.getUTCDay();
  return (day >= 4 || day === 0) ? utcThursday(d) : utcThursday.ceil(d);
}

function formatUTCWeekNumberISO(d, p) {
  d = UTCdISO(d);
  return pad(utcThursday.count(utcYear(d), d) + (utcYear(d).getUTCDay() === 4), p, 2);
}

function formatUTCWeekdayNumberSunday(d) {
  return d.getUTCDay();
}

function formatUTCWeekNumberMonday(d, p) {
  return pad(utcMonday.count(utcYear(d) - 1, d), p, 2);
}

function formatUTCYear(d, p) {
  return pad(d.getUTCFullYear() % 100, p, 2);
}

function formatUTCYearISO(d, p) {
  d = UTCdISO(d);
  return pad(d.getUTCFullYear() % 100, p, 2);
}

function formatUTCFullYear(d, p) {
  return pad(d.getUTCFullYear() % 10000, p, 4);
}

function formatUTCFullYearISO(d, p) {
  var day = d.getUTCDay();
  d = (day >= 4 || day === 0) ? utcThursday(d) : utcThursday.ceil(d);
  return pad(d.getUTCFullYear() % 10000, p, 4);
}

function formatUTCZone() {
  return "+0000";
}

function formatLiteralPercent() {
  return "%";
}

function formatUnixTimestamp(d) {
  return +d;
}

function formatUnixTimestampSeconds(d) {
  return Math.floor(+d / 1000);
}

var locale;
var timeFormat;
var utcFormat;
var utcParse;

defaultLocale({
  dateTime: "%x, %X",
  date: "%-m/%-d/%Y",
  time: "%-I:%M:%S %p",
  periods: ["AM", "PM"],
  days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
  shortDays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
  months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
  shortMonths: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
});

function defaultLocale(definition) {
  locale = formatLocale(definition);
  timeFormat = locale.format;
  locale.parse;
  utcFormat = locale.utcFormat;
  utcParse = locale.utcParse;
  return locale;
}

var isoSpecifier = "%Y-%m-%dT%H:%M:%S.%LZ";

function formatIsoNative(date) {
  return date.toISOString();
}

Date.prototype.toISOString
    ? formatIsoNative
    : utcFormat(isoSpecifier);

function parseIsoNative(string) {
  var date = new Date(string);
  return isNaN(date) ? null : date;
}

+new Date("2000-01-01T00:00:00.000Z")
    ? parseIsoNative
    : utcParse(isoSpecifier);

function date(t) {
  return new Date(t);
}

function number(t) {
  return t instanceof Date ? +t : +new Date(+t);
}

function calendar(ticks, tickInterval, year, month, week, day, hour, minute, second, format) {
  var scale = continuous(),
      invert = scale.invert,
      domain = scale.domain;

  var formatMillisecond = format(".%L"),
      formatSecond = format(":%S"),
      formatMinute = format("%I:%M"),
      formatHour = format("%I %p"),
      formatDay = format("%a %d"),
      formatWeek = format("%b %d"),
      formatMonth = format("%B"),
      formatYear = format("%Y");

  function tickFormat(date) {
    return (second(date) < date ? formatMillisecond
        : minute(date) < date ? formatSecond
        : hour(date) < date ? formatMinute
        : day(date) < date ? formatHour
        : month(date) < date ? (week(date) < date ? formatDay : formatWeek)
        : year(date) < date ? formatMonth
        : formatYear)(date);
  }

  scale.invert = function(y) {
    return new Date(invert(y));
  };

  scale.domain = function(_) {
    return arguments.length ? domain(Array.from(_, number)) : domain().map(date);
  };

  scale.ticks = function(interval) {
    var d = domain();
    return ticks(d[0], d[d.length - 1], interval == null ? 10 : interval);
  };

  scale.tickFormat = function(count, specifier) {
    return specifier == null ? tickFormat : format(specifier);
  };

  scale.nice = function(interval) {
    var d = domain();
    if (!interval || typeof interval.range !== "function") interval = tickInterval(d[0], d[d.length - 1], interval == null ? 10 : interval);
    return interval ? domain(nice(d, interval)) : scale;
  };

  scale.copy = function() {
    return copy$1(scale, calendar(ticks, tickInterval, year, month, week, day, hour, minute, second, format));
  };

  return scale;
}

function time() {
  return initRange.apply(calendar(timeTicks, timeTickInterval, timeYear, timeMonth, timeSunday, timeDay, timeHour, timeMinute, second, timeFormat).domain([new Date(2000, 0, 1), new Date(2000, 0, 2)]), arguments);
}

function constant(x) {
  return function constant() {
    return x;
  };
}

const abs = Math.abs;
const atan2 = Math.atan2;
const cos = Math.cos;
const max = Math.max;
const min = Math.min;
const sin = Math.sin;
const sqrt = Math.sqrt;

const epsilon = 1e-12;
const pi = Math.PI;
const halfPi = pi / 2;
const tau = 2 * pi;

function acos(x) {
  return x > 1 ? 0 : x < -1 ? pi : Math.acos(x);
}

function asin(x) {
  return x >= 1 ? halfPi : x <= -1 ? -halfPi : Math.asin(x);
}

function withPath(shape) {
  let digits = 3;

  shape.digits = function(_) {
    if (!arguments.length) return digits;
    if (_ == null) {
      digits = null;
    } else {
      const d = Math.floor(_);
      if (!(d >= 0)) throw new RangeError(`invalid digits: ${_}`);
      digits = d;
    }
    return shape;
  };

  return () => new Path$2(digits);
}

function arcInnerRadius(d) {
  return d.innerRadius;
}

function arcOuterRadius(d) {
  return d.outerRadius;
}

function arcStartAngle(d) {
  return d.startAngle;
}

function arcEndAngle(d) {
  return d.endAngle;
}

function arcPadAngle(d) {
  return d && d.padAngle; // Note: optional!
}

function intersect$1(x0, y0, x1, y1, x2, y2, x3, y3) {
  var x10 = x1 - x0, y10 = y1 - y0,
      x32 = x3 - x2, y32 = y3 - y2,
      t = y32 * x10 - x32 * y10;
  if (t * t < epsilon) return;
  t = (x32 * (y0 - y2) - y32 * (x0 - x2)) / t;
  return [x0 + t * x10, y0 + t * y10];
}

// Compute perpendicular offset line of length rc.
// http://mathworld.wolfram.com/Circle-LineIntersection.html
function cornerTangents(x0, y0, x1, y1, r1, rc, cw) {
  var x01 = x0 - x1,
      y01 = y0 - y1,
      lo = (cw ? rc : -rc) / sqrt(x01 * x01 + y01 * y01),
      ox = lo * y01,
      oy = -lo * x01,
      x11 = x0 + ox,
      y11 = y0 + oy,
      x10 = x1 + ox,
      y10 = y1 + oy,
      x00 = (x11 + x10) / 2,
      y00 = (y11 + y10) / 2,
      dx = x10 - x11,
      dy = y10 - y11,
      d2 = dx * dx + dy * dy,
      r = r1 - rc,
      D = x11 * y10 - x10 * y11,
      d = (dy < 0 ? -1 : 1) * sqrt(max(0, r * r * d2 - D * D)),
      cx0 = (D * dy - dx * d) / d2,
      cy0 = (-D * dx - dy * d) / d2,
      cx1 = (D * dy + dx * d) / d2,
      cy1 = (-D * dx + dy * d) / d2,
      dx0 = cx0 - x00,
      dy0 = cy0 - y00,
      dx1 = cx1 - x00,
      dy1 = cy1 - y00;

  // Pick the closer of the two intersection points.
  // TODO Is there a faster way to determine which intersection to use?
  if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) cx0 = cx1, cy0 = cy1;

  return {
    cx: cx0,
    cy: cy0,
    x01: -ox,
    y01: -oy,
    x11: cx0 * (r1 / r - 1),
    y11: cy0 * (r1 / r - 1)
  };
}

function arc() {
  var innerRadius = arcInnerRadius,
      outerRadius = arcOuterRadius,
      cornerRadius = constant(0),
      padRadius = null,
      startAngle = arcStartAngle,
      endAngle = arcEndAngle,
      padAngle = arcPadAngle,
      context = null,
      path = withPath(arc);

  function arc() {
    var buffer,
        r,
        r0 = +innerRadius.apply(this, arguments),
        r1 = +outerRadius.apply(this, arguments),
        a0 = startAngle.apply(this, arguments) - halfPi,
        a1 = endAngle.apply(this, arguments) - halfPi,
        da = abs(a1 - a0),
        cw = a1 > a0;

    if (!context) context = buffer = path();

    // Ensure that the outer radius is always larger than the inner radius.
    if (r1 < r0) r = r1, r1 = r0, r0 = r;

    // Is it a point?
    if (!(r1 > epsilon)) context.moveTo(0, 0);

    // Or is it a circle or annulus?
    else if (da > tau - epsilon) {
      context.moveTo(r1 * cos(a0), r1 * sin(a0));
      context.arc(0, 0, r1, a0, a1, !cw);
      if (r0 > epsilon) {
        context.moveTo(r0 * cos(a1), r0 * sin(a1));
        context.arc(0, 0, r0, a1, a0, cw);
      }
    }

    // Or is it a circular or annular sector?
    else {
      var a01 = a0,
          a11 = a1,
          a00 = a0,
          a10 = a1,
          da0 = da,
          da1 = da,
          ap = padAngle.apply(this, arguments) / 2,
          rp = (ap > epsilon) && (padRadius ? +padRadius.apply(this, arguments) : sqrt(r0 * r0 + r1 * r1)),
          rc = min(abs(r1 - r0) / 2, +cornerRadius.apply(this, arguments)),
          rc0 = rc,
          rc1 = rc,
          t0,
          t1;

      // Apply padding? Note that since r1 ≥ r0, da1 ≥ da0.
      if (rp > epsilon) {
        var p0 = asin(rp / r0 * sin(ap)),
            p1 = asin(rp / r1 * sin(ap));
        if ((da0 -= p0 * 2) > epsilon) p0 *= (cw ? 1 : -1), a00 += p0, a10 -= p0;
        else da0 = 0, a00 = a10 = (a0 + a1) / 2;
        if ((da1 -= p1 * 2) > epsilon) p1 *= (cw ? 1 : -1), a01 += p1, a11 -= p1;
        else da1 = 0, a01 = a11 = (a0 + a1) / 2;
      }

      var x01 = r1 * cos(a01),
          y01 = r1 * sin(a01),
          x10 = r0 * cos(a10),
          y10 = r0 * sin(a10);

      // Apply rounded corners?
      if (rc > epsilon) {
        var x11 = r1 * cos(a11),
            y11 = r1 * sin(a11),
            x00 = r0 * cos(a00),
            y00 = r0 * sin(a00),
            oc;

        // Restrict the corner radius according to the sector angle. If this
        // intersection fails, it’s probably because the arc is too small, so
        // disable the corner radius entirely.
        if (da < pi) {
          if (oc = intersect$1(x01, y01, x00, y00, x11, y11, x10, y10)) {
            var ax = x01 - oc[0],
                ay = y01 - oc[1],
                bx = x11 - oc[0],
                by = y11 - oc[1],
                kc = 1 / sin(acos((ax * bx + ay * by) / (sqrt(ax * ax + ay * ay) * sqrt(bx * bx + by * by))) / 2),
                lc = sqrt(oc[0] * oc[0] + oc[1] * oc[1]);
            rc0 = min(rc, (r0 - lc) / (kc - 1));
            rc1 = min(rc, (r1 - lc) / (kc + 1));
          } else {
            rc0 = rc1 = 0;
          }
        }
      }

      // Is the sector collapsed to a line?
      if (!(da1 > epsilon)) context.moveTo(x01, y01);

      // Does the sector’s outer ring have rounded corners?
      else if (rc1 > epsilon) {
        t0 = cornerTangents(x00, y00, x01, y01, r1, rc1, cw);
        t1 = cornerTangents(x11, y11, x10, y10, r1, rc1, cw);

        context.moveTo(t0.cx + t0.x01, t0.cy + t0.y01);

        // Have the corners merged?
        if (rc1 < rc) context.arc(t0.cx, t0.cy, rc1, atan2(t0.y01, t0.x01), atan2(t1.y01, t1.x01), !cw);

        // Otherwise, draw the two corners and the ring.
        else {
          context.arc(t0.cx, t0.cy, rc1, atan2(t0.y01, t0.x01), atan2(t0.y11, t0.x11), !cw);
          context.arc(0, 0, r1, atan2(t0.cy + t0.y11, t0.cx + t0.x11), atan2(t1.cy + t1.y11, t1.cx + t1.x11), !cw);
          context.arc(t1.cx, t1.cy, rc1, atan2(t1.y11, t1.x11), atan2(t1.y01, t1.x01), !cw);
        }
      }

      // Or is the outer ring just a circular arc?
      else context.moveTo(x01, y01), context.arc(0, 0, r1, a01, a11, !cw);

      // Is there no inner ring, and it’s a circular sector?
      // Or perhaps it’s an annular sector collapsed due to padding?
      if (!(r0 > epsilon) || !(da0 > epsilon)) context.lineTo(x10, y10);

      // Does the sector’s inner ring (or point) have rounded corners?
      else if (rc0 > epsilon) {
        t0 = cornerTangents(x10, y10, x11, y11, r0, -rc0, cw);
        t1 = cornerTangents(x01, y01, x00, y00, r0, -rc0, cw);

        context.lineTo(t0.cx + t0.x01, t0.cy + t0.y01);

        // Have the corners merged?
        if (rc0 < rc) context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t1.y01, t1.x01), !cw);

        // Otherwise, draw the two corners and the ring.
        else {
          context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t0.y11, t0.x11), !cw);
          context.arc(0, 0, r0, atan2(t0.cy + t0.y11, t0.cx + t0.x11), atan2(t1.cy + t1.y11, t1.cx + t1.x11), cw);
          context.arc(t1.cx, t1.cy, rc0, atan2(t1.y11, t1.x11), atan2(t1.y01, t1.x01), !cw);
        }
      }

      // Or is the inner ring just a circular arc?
      else context.arc(0, 0, r0, a10, a00, cw);
    }

    context.closePath();

    if (buffer) return context = null, buffer + "" || null;
  }

  arc.centroid = function() {
    var r = (+innerRadius.apply(this, arguments) + +outerRadius.apply(this, arguments)) / 2,
        a = (+startAngle.apply(this, arguments) + +endAngle.apply(this, arguments)) / 2 - pi / 2;
    return [cos(a) * r, sin(a) * r];
  };

  arc.innerRadius = function(_) {
    return arguments.length ? (innerRadius = typeof _ === "function" ? _ : constant(+_), arc) : innerRadius;
  };

  arc.outerRadius = function(_) {
    return arguments.length ? (outerRadius = typeof _ === "function" ? _ : constant(+_), arc) : outerRadius;
  };

  arc.cornerRadius = function(_) {
    return arguments.length ? (cornerRadius = typeof _ === "function" ? _ : constant(+_), arc) : cornerRadius;
  };

  arc.padRadius = function(_) {
    return arguments.length ? (padRadius = _ == null ? null : typeof _ === "function" ? _ : constant(+_), arc) : padRadius;
  };

  arc.startAngle = function(_) {
    return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant(+_), arc) : startAngle;
  };

  arc.endAngle = function(_) {
    return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant(+_), arc) : endAngle;
  };

  arc.padAngle = function(_) {
    return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant(+_), arc) : padAngle;
  };

  arc.context = function(_) {
    return arguments.length ? ((context = _ == null ? null : _), arc) : context;
  };

  return arc;
}

var frame = 0, // is an animation frame pending?
    timeout$1 = 0, // is a timeout pending?
    interval = 0, // are any timers active?
    pokeDelay = 1000, // how frequently we check for clock skew
    taskHead,
    taskTail,
    clockLast = 0,
    clockNow = 0,
    clockSkew = 0,
    clock = typeof performance === "object" && performance.now ? performance : Date,
    setFrame = typeof window === "object" && window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : function(f) { setTimeout(f, 17); };

function now() {
  return clockNow || (setFrame(clearNow), clockNow = clock.now() + clockSkew);
}

function clearNow() {
  clockNow = 0;
}

function Timer() {
  this._call =
  this._time =
  this._next = null;
}

Timer.prototype = timer.prototype = {
  constructor: Timer,
  restart: function(callback, delay, time) {
    if (typeof callback !== "function") throw new TypeError("callback is not a function");
    time = (time == null ? now() : +time) + (delay == null ? 0 : +delay);
    if (!this._next && taskTail !== this) {
      if (taskTail) taskTail._next = this;
      else taskHead = this;
      taskTail = this;
    }
    this._call = callback;
    this._time = time;
    sleep();
  },
  stop: function() {
    if (this._call) {
      this._call = null;
      this._time = Infinity;
      sleep();
    }
  }
};

function timer(callback, delay, time) {
  var t = new Timer;
  t.restart(callback, delay, time);
  return t;
}

function timerFlush() {
  now(); // Get the current time, if not already set.
  ++frame; // Pretend we’ve set an alarm, if we haven’t already.
  var t = taskHead, e;
  while (t) {
    if ((e = clockNow - t._time) >= 0) t._call.call(undefined, e);
    t = t._next;
  }
  --frame;
}

function wake() {
  clockNow = (clockLast = clock.now()) + clockSkew;
  frame = timeout$1 = 0;
  try {
    timerFlush();
  } finally {
    frame = 0;
    nap();
    clockNow = 0;
  }
}

function poke() {
  var now = clock.now(), delay = now - clockLast;
  if (delay > pokeDelay) clockSkew -= delay, clockLast = now;
}

function nap() {
  var t0, t1 = taskHead, t2, time = Infinity;
  while (t1) {
    if (t1._call) {
      if (time > t1._time) time = t1._time;
      t0 = t1, t1 = t1._next;
    } else {
      t2 = t1._next, t1._next = null;
      t1 = t0 ? t0._next = t2 : taskHead = t2;
    }
  }
  taskTail = t0;
  sleep(time);
}

function sleep(time) {
  if (frame) return; // Soonest alarm already set, or will be.
  if (timeout$1) timeout$1 = clearTimeout(timeout$1);
  var delay = time - clockNow; // Strictly less than if we recomputed clockNow.
  if (delay > 24) {
    if (time < Infinity) timeout$1 = setTimeout(wake, time - clock.now() - clockSkew);
    if (interval) interval = clearInterval(interval);
  } else {
    if (!interval) clockLast = clock.now(), interval = setInterval(poke, pokeDelay);
    frame = 1, setFrame(wake);
  }
}

function timeout(callback, delay, time) {
  var t = new Timer;
  delay = delay == null ? 0 : +delay;
  t.restart(elapsed => {
    t.stop();
    callback(elapsed + delay);
  }, delay, time);
  return t;
}

var emptyOn = dispatch("start", "end", "cancel", "interrupt");
var emptyTween = [];

var CREATED = 0;
var SCHEDULED = 1;
var STARTING = 2;
var STARTED = 3;
var RUNNING = 4;
var ENDING = 5;
var ENDED = 6;

function schedule(node, name, id, index, group, timing) {
  var schedules = node.__transition;
  if (!schedules) node.__transition = {};
  else if (id in schedules) return;
  create(node, id, {
    name: name,
    index: index, // For context during callback.
    group: group, // For context during callback.
    on: emptyOn,
    tween: emptyTween,
    time: timing.time,
    delay: timing.delay,
    duration: timing.duration,
    ease: timing.ease,
    timer: null,
    state: CREATED
  });
}

function init(node, id) {
  var schedule = get(node, id);
  if (schedule.state > CREATED) throw new Error("too late; already scheduled");
  return schedule;
}

function set(node, id) {
  var schedule = get(node, id);
  if (schedule.state > STARTED) throw new Error("too late; already running");
  return schedule;
}

function get(node, id) {
  var schedule = node.__transition;
  if (!schedule || !(schedule = schedule[id])) throw new Error("transition not found");
  return schedule;
}

function create(node, id, self) {
  var schedules = node.__transition,
      tween;

  // Initialize the self timer when the transition is created.
  // Note the actual delay is not known until the first callback!
  schedules[id] = self;
  self.timer = timer(schedule, 0, self.time);

  function schedule(elapsed) {
    self.state = SCHEDULED;
    self.timer.restart(start, self.delay, self.time);

    // If the elapsed delay is less than our first sleep, start immediately.
    if (self.delay <= elapsed) start(elapsed - self.delay);
  }

  function start(elapsed) {
    var i, j, n, o;

    // If the state is not SCHEDULED, then we previously errored on start.
    if (self.state !== SCHEDULED) return stop();

    for (i in schedules) {
      o = schedules[i];
      if (o.name !== self.name) continue;

      // While this element already has a starting transition during this frame,
      // defer starting an interrupting transition until that transition has a
      // chance to tick (and possibly end); see d3/d3-transition#54!
      if (o.state === STARTED) return timeout(start);

      // Interrupt the active transition, if any.
      if (o.state === RUNNING) {
        o.state = ENDED;
        o.timer.stop();
        o.on.call("interrupt", node, node.__data__, o.index, o.group);
        delete schedules[i];
      }

      // Cancel any pre-empted transitions.
      else if (+i < id) {
        o.state = ENDED;
        o.timer.stop();
        o.on.call("cancel", node, node.__data__, o.index, o.group);
        delete schedules[i];
      }
    }

    // Defer the first tick to end of the current frame; see d3/d3#1576.
    // Note the transition may be canceled after start and before the first tick!
    // Note this must be scheduled before the start event; see d3/d3-transition#16!
    // Assuming this is successful, subsequent callbacks go straight to tick.
    timeout(function() {
      if (self.state === STARTED) {
        self.state = RUNNING;
        self.timer.restart(tick, self.delay, self.time);
        tick(elapsed);
      }
    });

    // Dispatch the start event.
    // Note this must be done before the tween are initialized.
    self.state = STARTING;
    self.on.call("start", node, node.__data__, self.index, self.group);
    if (self.state !== STARTING) return; // interrupted
    self.state = STARTED;

    // Initialize the tween, deleting null tween.
    tween = new Array(n = self.tween.length);
    for (i = 0, j = -1; i < n; ++i) {
      if (o = self.tween[i].value.call(node, node.__data__, self.index, self.group)) {
        tween[++j] = o;
      }
    }
    tween.length = j + 1;
  }

  function tick(elapsed) {
    var t = elapsed < self.duration ? self.ease.call(null, elapsed / self.duration) : (self.timer.restart(stop), self.state = ENDING, 1),
        i = -1,
        n = tween.length;

    while (++i < n) {
      tween[i].call(node, t);
    }

    // Dispatch the end event.
    if (self.state === ENDING) {
      self.on.call("end", node, node.__data__, self.index, self.group);
      stop();
    }
  }

  function stop() {
    self.state = ENDED;
    self.timer.stop();
    delete schedules[id];
    for (var i in schedules) return; // eslint-disable-line no-unused-vars
    delete node.__transition;
  }
}

function interrupt(node, name) {
  var schedules = node.__transition,
      schedule,
      active,
      empty = true,
      i;

  if (!schedules) return;

  name = name == null ? null : name + "";

  for (i in schedules) {
    if ((schedule = schedules[i]).name !== name) { empty = false; continue; }
    active = schedule.state > STARTING && schedule.state < ENDING;
    schedule.state = ENDED;
    schedule.timer.stop();
    schedule.on.call(active ? "interrupt" : "cancel", node, node.__data__, schedule.index, schedule.group);
    delete schedules[i];
  }

  if (empty) delete node.__transition;
}

function selection_interrupt(name) {
  return this.each(function() {
    interrupt(this, name);
  });
}

function tweenRemove(id, name) {
  var tween0, tween1;
  return function() {
    var schedule = set(this, id),
        tween = schedule.tween;

    // If this node shared tween with the previous node,
    // just assign the updated shared tween and we’re done!
    // Otherwise, copy-on-write.
    if (tween !== tween0) {
      tween1 = tween0 = tween;
      for (var i = 0, n = tween1.length; i < n; ++i) {
        if (tween1[i].name === name) {
          tween1 = tween1.slice();
          tween1.splice(i, 1);
          break;
        }
      }
    }

    schedule.tween = tween1;
  };
}

function tweenFunction(id, name, value) {
  var tween0, tween1;
  if (typeof value !== "function") throw new Error;
  return function() {
    var schedule = set(this, id),
        tween = schedule.tween;

    // If this node shared tween with the previous node,
    // just assign the updated shared tween and we’re done!
    // Otherwise, copy-on-write.
    if (tween !== tween0) {
      tween1 = (tween0 = tween).slice();
      for (var t = {name: name, value: value}, i = 0, n = tween1.length; i < n; ++i) {
        if (tween1[i].name === name) {
          tween1[i] = t;
          break;
        }
      }
      if (i === n) tween1.push(t);
    }

    schedule.tween = tween1;
  };
}

function transition_tween(name, value) {
  var id = this._id;

  name += "";

  if (arguments.length < 2) {
    var tween = get(this.node(), id).tween;
    for (var i = 0, n = tween.length, t; i < n; ++i) {
      if ((t = tween[i]).name === name) {
        return t.value;
      }
    }
    return null;
  }

  return this.each((value == null ? tweenRemove : tweenFunction)(id, name, value));
}

function tweenValue(transition, name, value) {
  var id = transition._id;

  transition.each(function() {
    var schedule = set(this, id);
    (schedule.value || (schedule.value = {}))[name] = value.apply(this, arguments);
  });

  return function(node) {
    return get(node, id).value[name];
  };
}

function interpolate(a, b) {
  var c;
  return (typeof b === "number" ? interpolateNumber
      : b instanceof color ? interpolateRgb
      : (c = color(b)) ? (b = c, interpolateRgb)
      : interpolateString)(a, b);
}

function attrRemove(name) {
  return function() {
    this.removeAttribute(name);
  };
}

function attrRemoveNS(fullname) {
  return function() {
    this.removeAttributeNS(fullname.space, fullname.local);
  };
}

function attrConstant(name, interpolate, value1) {
  var string00,
      string1 = value1 + "",
      interpolate0;
  return function() {
    var string0 = this.getAttribute(name);
    return string0 === string1 ? null
        : string0 === string00 ? interpolate0
        : interpolate0 = interpolate(string00 = string0, value1);
  };
}

function attrConstantNS(fullname, interpolate, value1) {
  var string00,
      string1 = value1 + "",
      interpolate0;
  return function() {
    var string0 = this.getAttributeNS(fullname.space, fullname.local);
    return string0 === string1 ? null
        : string0 === string00 ? interpolate0
        : interpolate0 = interpolate(string00 = string0, value1);
  };
}

function attrFunction(name, interpolate, value) {
  var string00,
      string10,
      interpolate0;
  return function() {
    var string0, value1 = value(this), string1;
    if (value1 == null) return void this.removeAttribute(name);
    string0 = this.getAttribute(name);
    string1 = value1 + "";
    return string0 === string1 ? null
        : string0 === string00 && string1 === string10 ? interpolate0
        : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
  };
}

function attrFunctionNS(fullname, interpolate, value) {
  var string00,
      string10,
      interpolate0;
  return function() {
    var string0, value1 = value(this), string1;
    if (value1 == null) return void this.removeAttributeNS(fullname.space, fullname.local);
    string0 = this.getAttributeNS(fullname.space, fullname.local);
    string1 = value1 + "";
    return string0 === string1 ? null
        : string0 === string00 && string1 === string10 ? interpolate0
        : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
  };
}

function transition_attr(name, value) {
  var fullname = namespace(name), i = fullname === "transform" ? interpolateTransformSvg : interpolate;
  return this.attrTween(name, typeof value === "function"
      ? (fullname.local ? attrFunctionNS : attrFunction)(fullname, i, tweenValue(this, "attr." + name, value))
      : value == null ? (fullname.local ? attrRemoveNS : attrRemove)(fullname)
      : (fullname.local ? attrConstantNS : attrConstant)(fullname, i, value));
}

function attrInterpolate(name, i) {
  return function(t) {
    this.setAttribute(name, i.call(this, t));
  };
}

function attrInterpolateNS(fullname, i) {
  return function(t) {
    this.setAttributeNS(fullname.space, fullname.local, i.call(this, t));
  };
}

function attrTweenNS(fullname, value) {
  var t0, i0;
  function tween() {
    var i = value.apply(this, arguments);
    if (i !== i0) t0 = (i0 = i) && attrInterpolateNS(fullname, i);
    return t0;
  }
  tween._value = value;
  return tween;
}

function attrTween(name, value) {
  var t0, i0;
  function tween() {
    var i = value.apply(this, arguments);
    if (i !== i0) t0 = (i0 = i) && attrInterpolate(name, i);
    return t0;
  }
  tween._value = value;
  return tween;
}

function transition_attrTween(name, value) {
  var key = "attr." + name;
  if (arguments.length < 2) return (key = this.tween(key)) && key._value;
  if (value == null) return this.tween(key, null);
  if (typeof value !== "function") throw new Error;
  var fullname = namespace(name);
  return this.tween(key, (fullname.local ? attrTweenNS : attrTween)(fullname, value));
}

function delayFunction(id, value) {
  return function() {
    init(this, id).delay = +value.apply(this, arguments);
  };
}

function delayConstant(id, value) {
  return value = +value, function() {
    init(this, id).delay = value;
  };
}

function transition_delay(value) {
  var id = this._id;

  return arguments.length
      ? this.each((typeof value === "function"
          ? delayFunction
          : delayConstant)(id, value))
      : get(this.node(), id).delay;
}

function durationFunction(id, value) {
  return function() {
    set(this, id).duration = +value.apply(this, arguments);
  };
}

function durationConstant(id, value) {
  return value = +value, function() {
    set(this, id).duration = value;
  };
}

function transition_duration(value) {
  var id = this._id;

  return arguments.length
      ? this.each((typeof value === "function"
          ? durationFunction
          : durationConstant)(id, value))
      : get(this.node(), id).duration;
}

function easeConstant(id, value) {
  if (typeof value !== "function") throw new Error;
  return function() {
    set(this, id).ease = value;
  };
}

function transition_ease(value) {
  var id = this._id;

  return arguments.length
      ? this.each(easeConstant(id, value))
      : get(this.node(), id).ease;
}

function easeVarying(id, value) {
  return function() {
    var v = value.apply(this, arguments);
    if (typeof v !== "function") throw new Error;
    set(this, id).ease = v;
  };
}

function transition_easeVarying(value) {
  if (typeof value !== "function") throw new Error;
  return this.each(easeVarying(this._id, value));
}

function transition_filter(match) {
  if (typeof match !== "function") match = matcher(match);

  for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
    for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) {
      if ((node = group[i]) && match.call(node, node.__data__, i, group)) {
        subgroup.push(node);
      }
    }
  }

  return new Transition(subgroups, this._parents, this._name, this._id);
}

function transition_merge(transition) {
  if (transition._id !== this._id) throw new Error;

  for (var groups0 = this._groups, groups1 = transition._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {
    for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {
      if (node = group0[i] || group1[i]) {
        merge[i] = node;
      }
    }
  }

  for (; j < m0; ++j) {
    merges[j] = groups0[j];
  }

  return new Transition(merges, this._parents, this._name, this._id);
}

function start(name) {
  return (name + "").trim().split(/^|\s+/).every(function(t) {
    var i = t.indexOf(".");
    if (i >= 0) t = t.slice(0, i);
    return !t || t === "start";
  });
}

function onFunction(id, name, listener) {
  var on0, on1, sit = start(name) ? init : set;
  return function() {
    var schedule = sit(this, id),
        on = schedule.on;

    // If this node shared a dispatch with the previous node,
    // just assign the updated shared dispatch and we’re done!
    // Otherwise, copy-on-write.
    if (on !== on0) (on1 = (on0 = on).copy()).on(name, listener);

    schedule.on = on1;
  };
}

function transition_on(name, listener) {
  var id = this._id;

  return arguments.length < 2
      ? get(this.node(), id).on.on(name)
      : this.each(onFunction(id, name, listener));
}

function removeFunction(id) {
  return function() {
    var parent = this.parentNode;
    for (var i in this.__transition) if (+i !== id) return;
    if (parent) parent.removeChild(this);
  };
}

function transition_remove() {
  return this.on("end.remove", removeFunction(this._id));
}

function transition_select(select) {
  var name = this._name,
      id = this._id;

  if (typeof select !== "function") select = selector(select);

  for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
    for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {
      if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) {
        if ("__data__" in node) subnode.__data__ = node.__data__;
        subgroup[i] = subnode;
        schedule(subgroup[i], name, id, i, subgroup, get(node, id));
      }
    }
  }

  return new Transition(subgroups, this._parents, name, id);
}

function transition_selectAll(select) {
  var name = this._name,
      id = this._id;

  if (typeof select !== "function") select = selectorAll(select);

  for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {
    for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
      if (node = group[i]) {
        for (var children = select.call(node, node.__data__, i, group), child, inherit = get(node, id), k = 0, l = children.length; k < l; ++k) {
          if (child = children[k]) {
            schedule(child, name, id, k, children, inherit);
          }
        }
        subgroups.push(children);
        parents.push(node);
      }
    }
  }

  return new Transition(subgroups, parents, name, id);
}

var Selection = selection.prototype.constructor;

function transition_selection() {
  return new Selection(this._groups, this._parents);
}

function styleNull(name, interpolate) {
  var string00,
      string10,
      interpolate0;
  return function() {
    var string0 = styleValue(this, name),
        string1 = (this.style.removeProperty(name), styleValue(this, name));
    return string0 === string1 ? null
        : string0 === string00 && string1 === string10 ? interpolate0
        : interpolate0 = interpolate(string00 = string0, string10 = string1);
  };
}

function styleRemove(name) {
  return function() {
    this.style.removeProperty(name);
  };
}

function styleConstant(name, interpolate, value1) {
  var string00,
      string1 = value1 + "",
      interpolate0;
  return function() {
    var string0 = styleValue(this, name);
    return string0 === string1 ? null
        : string0 === string00 ? interpolate0
        : interpolate0 = interpolate(string00 = string0, value1);
  };
}

function styleFunction(name, interpolate, value) {
  var string00,
      string10,
      interpolate0;
  return function() {
    var string0 = styleValue(this, name),
        value1 = value(this),
        string1 = value1 + "";
    if (value1 == null) string1 = value1 = (this.style.removeProperty(name), styleValue(this, name));
    return string0 === string1 ? null
        : string0 === string00 && string1 === string10 ? interpolate0
        : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
  };
}

function styleMaybeRemove(id, name) {
  var on0, on1, listener0, key = "style." + name, event = "end." + key, remove;
  return function() {
    var schedule = set(this, id),
        on = schedule.on,
        listener = schedule.value[key] == null ? remove || (remove = styleRemove(name)) : undefined;

    // If this node shared a dispatch with the previous node,
    // just assign the updated shared dispatch and we’re done!
    // Otherwise, copy-on-write.
    if (on !== on0 || listener0 !== listener) (on1 = (on0 = on).copy()).on(event, listener0 = listener);

    schedule.on = on1;
  };
}

function transition_style(name, value, priority) {
  var i = (name += "") === "transform" ? interpolateTransformCss : interpolate;
  return value == null ? this
      .styleTween(name, styleNull(name, i))
      .on("end.style." + name, styleRemove(name))
    : typeof value === "function" ? this
      .styleTween(name, styleFunction(name, i, tweenValue(this, "style." + name, value)))
      .each(styleMaybeRemove(this._id, name))
    : this
      .styleTween(name, styleConstant(name, i, value), priority)
      .on("end.style." + name, null);
}

function styleInterpolate(name, i, priority) {
  return function(t) {
    this.style.setProperty(name, i.call(this, t), priority);
  };
}

function styleTween(name, value, priority) {
  var t, i0;
  function tween() {
    var i = value.apply(this, arguments);
    if (i !== i0) t = (i0 = i) && styleInterpolate(name, i, priority);
    return t;
  }
  tween._value = value;
  return tween;
}

function transition_styleTween(name, value, priority) {
  var key = "style." + (name += "");
  if (arguments.length < 2) return (key = this.tween(key)) && key._value;
  if (value == null) return this.tween(key, null);
  if (typeof value !== "function") throw new Error;
  return this.tween(key, styleTween(name, value, priority == null ? "" : priority));
}

function textConstant(value) {
  return function() {
    this.textContent = value;
  };
}

function textFunction(value) {
  return function() {
    var value1 = value(this);
    this.textContent = value1 == null ? "" : value1;
  };
}

function transition_text(value) {
  return this.tween("text", typeof value === "function"
      ? textFunction(tweenValue(this, "text", value))
      : textConstant(value == null ? "" : value + ""));
}

function textInterpolate(i) {
  return function(t) {
    this.textContent = i.call(this, t);
  };
}

function textTween(value) {
  var t0, i0;
  function tween() {
    var i = value.apply(this, arguments);
    if (i !== i0) t0 = (i0 = i) && textInterpolate(i);
    return t0;
  }
  tween._value = value;
  return tween;
}

function transition_textTween(value) {
  var key = "text";
  if (arguments.length < 1) return (key = this.tween(key)) && key._value;
  if (value == null) return this.tween(key, null);
  if (typeof value !== "function") throw new Error;
  return this.tween(key, textTween(value));
}

function transition_transition() {
  var name = this._name,
      id0 = this._id,
      id1 = newId();

  for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) {
    for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
      if (node = group[i]) {
        var inherit = get(node, id0);
        schedule(node, name, id1, i, group, {
          time: inherit.time + inherit.delay + inherit.duration,
          delay: 0,
          duration: inherit.duration,
          ease: inherit.ease
        });
      }
    }
  }

  return new Transition(groups, this._parents, name, id1);
}

function transition_end() {
  var on0, on1, that = this, id = that._id, size = that.size();
  return new Promise(function(resolve, reject) {
    var cancel = {value: reject},
        end = {value: function() { if (--size === 0) resolve(); }};

    that.each(function() {
      var schedule = set(this, id),
          on = schedule.on;

      // If this node shared a dispatch with the previous node,
      // just assign the updated shared dispatch and we’re done!
      // Otherwise, copy-on-write.
      if (on !== on0) {
        on1 = (on0 = on).copy();
        on1._.cancel.push(cancel);
        on1._.interrupt.push(cancel);
        on1._.end.push(end);
      }

      schedule.on = on1;
    });

    // The selection was empty, resolve end immediately
    if (size === 0) resolve();
  });
}

var id = 0;

function Transition(groups, parents, name, id) {
  this._groups = groups;
  this._parents = parents;
  this._name = name;
  this._id = id;
}

function newId() {
  return ++id;
}

var selection_prototype = selection.prototype;

Transition.prototype = {
  constructor: Transition,
  select: transition_select,
  selectAll: transition_selectAll,
  selectChild: selection_prototype.selectChild,
  selectChildren: selection_prototype.selectChildren,
  filter: transition_filter,
  merge: transition_merge,
  selection: transition_selection,
  transition: transition_transition,
  call: selection_prototype.call,
  nodes: selection_prototype.nodes,
  node: selection_prototype.node,
  size: selection_prototype.size,
  empty: selection_prototype.empty,
  each: selection_prototype.each,
  on: transition_on,
  attr: transition_attr,
  attrTween: transition_attrTween,
  style: transition_style,
  styleTween: transition_styleTween,
  text: transition_text,
  textTween: transition_textTween,
  remove: transition_remove,
  tween: transition_tween,
  delay: transition_delay,
  duration: transition_duration,
  ease: transition_ease,
  easeVarying: transition_easeVarying,
  end: transition_end,
  [Symbol.iterator]: selection_prototype[Symbol.iterator]
};

function cubicInOut(t) {
  return ((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2;
}

var defaultTiming = {
  time: null, // Set on use.
  delay: 0,
  duration: 250,
  ease: cubicInOut
};

function inherit$1(node, id) {
  var timing;
  while (!(timing = node.__transition) || !(timing = timing[id])) {
    if (!(node = node.parentNode)) {
      throw new Error(`transition ${id} not found`);
    }
  }
  return timing;
}

function selection_transition(name) {
  var id,
      timing;

  if (name instanceof Transition) {
    id = name._id, name = name._name;
  } else {
    id = newId(), (timing = defaultTiming).time = now(), name = name == null ? null : name + "";
  }

  for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) {
    for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
      if (node = group[i]) {
        schedule(node, name, id, i, group, timing || inherit$1(node, id));
      }
    }
  }

  return new Transition(groups, this._parents, name, id);
}

selection.prototype.interrupt = selection_interrupt;
selection.prototype.transition = selection_transition;

const clTLinearGradient = 'TLinearGradient', clTRadialGradient = 'TRadialGradient',
      kWhite = 0, kBlack = 1;

/** @summary Covert value between 0 and 1 into decimal string using scale factor, used for colors coding
  * @private */
function toDec(num, scale = 255) {
   return Math.round(num * scale).toString(10);
}

/** @summary Convert alfa value from rgba to string
  * @private */
function toAlfa(a) {
   const res = a.toFixed(2);
   if ((res.length === 4) && (res[3] === '0'))
      return res.slice(0, 3);
   return res;
}

/** @summary Convert r,g,b,a values to string
  * @private */
function toColor(r, g, b, a = 1) {
   return (a !== undefined) && (a !== 1)
      ? `rgba(${toDec(r)}, ${toDec(g)}, ${toDec(b)}, ${toAlfa(a)})`
      : `rgb(${toDec(r)}, ${toDec(g)}, ${toDec(b)})`;
}

/** @summary Convert color string to unify node.js and browser
  * @private */
function convertColor(col) {
   return (isNodeJs() || (isBatchMode() && settings.ApproxTextSize)) && (col[0] === '#' || col[0] === 'r') ? color(col).formatRgb() : col;
}

/** @summary list of global root colors
  * @private */
let gbl_colors_list = [];

/** @summary Generates all root colors, used also in jstests to reset colors
  * @private */
function createRootColors() {
   function conv(arg) {
      const r = Number.parseInt(arg.slice(0, 2), 16),
            g = Number.parseInt(arg.slice(2, 4), 16),
            b = Number.parseInt(arg.slice(4, 6), 16);
      return `rgb(${r}, ${g}, ${b})`;
   }

   const colorMap = ['white', 'black', 'red', 'green', 'blue', 'yellow', 'magenta', 'cyan', conv('59d454'), conv('5954d9'), 'white'];
   colorMap[110] = 'white';

   const moreCol = [
      { n: 11, s: 'c1b7ad4d4d4d6666668080809a9a9ab3b3b3cdcdcde6e6e6f3f3f3cdc8accdc8acc3c0a9bbb6a4b3a697b8a49cae9a8d9c8f83886657b1cfc885c3a48aa9a1839f8daebdc87b8f9a768a926983976e7b857d9ad280809caca6c0d4cf88dfbb88bd9f83c89a7dc08378cf5f61ac8f94a6787b946971d45a549300ff7b00ff6300ff4b00ff3300ff1b00ff0300ff0014ff002cff0044ff005cff0074ff008cff00a4ff00bcff00d4ff00ecff00fffd00ffe500ffcd00ffb500ff9d00ff8500ff6d00ff5500ff3d00ff2600ff0e0aff0022ff003aff0052ff006aff0082ff009aff00b1ff00c9ff00e1ff00f9ff00ffef00ffd700ffbf00ffa700ff8f00ff7700ff6000ff4800ff3000ff18006f2da8a52a2ab2beb55790fcf89c20e42536964a8b9c9ca17a21dd1845fbff5e02c91f16c849a9adad7d86c8dd578dff6563643f90daffa90ebd1f0194a4a2832db6a96b59e76300b9ac7071758192daddb2b2b2' },
      { n: 201, s: '5c5c5c7b7b7bb8b8b8d7d7d78a0f0fb81414ec4848f176760f8a0f14b81448ec4876f1760f0f8a1414b84848ec7676f18a8a0fb8b814ecec48f1f1768a0f8ab814b8ec48ecf176f10f8a8a14b8b848ecec76f1f1' },
      { n: 390, s: 'ffffcdffff9acdcd9affff66cdcd669a9a66ffff33cdcd339a9a33666633ffff00cdcd009a9a00666600333300' },
      { n: 406, s: 'cdffcd9aff9a9acd9a66ff6666cd66669a6633ff3333cd33339a3333663300ff0000cd00009a00006600003300' },
      { n: 422, s: 'cdffff9affff9acdcd66ffff66cdcd669a9a33ffff33cdcd339a9a33666600ffff00cdcd009a9a006666003333' },
      { n: 590, s: 'cdcdff9a9aff9a9acd6666ff6666cd66669a3333ff3333cd33339a3333660000ff0000cd00009a000066000033' },
      { n: 606, s: 'ffcdffff9affcd9acdff66ffcd66cd9a669aff33ffcd33cd9a339a663366ff00ffcd00cd9a009a660066330033' },
      { n: 622, s: 'ffcdcdff9a9acd9a9aff6666cd66669a6666ff3333cd33339a3333663333ff0000cd00009a0000660000330000' },
      { n: 791, s: 'ffcd9acd9a669a66339a6600cd9a33ffcd66ff9a00ffcd33cd9a00ffcd00ff9a33cd66006633009a3300cd6633ff9a66ff6600ff6633cd3300ff33009aff3366cd00336600339a0066cd339aff6666ff0066ff3333cd0033ff00cdff9a9acd66669a33669a009acd33cdff669aff00cdff339acd00cdff009affcd66cd9a339a66009a6633cd9a66ffcd00ff6633ffcd00cd9a00ffcd33ff9a00cd66006633009a3333cd6666ff9a00ff9a33ff6600cd3300ff339acdff669acd33669a00339a3366cd669aff0066ff3366ff0033cd0033ff339aff0066cd00336600669a339acd66cdff009aff33cdff009acd00cdffcd9aff9a66cd66339a66009a9a33cdcd66ff9a00ffcd33ff9a00cdcd00ff9a33ff6600cd33006633009a6633cd9a66ff6600ff6633ff3300cd3300ffff339acd00666600339a0033cd3366ff669aff0066ff3366cd0033ff0033ff9acdcd669a9a33669a0066cd339aff66cdff009acd009aff33cdff009a' },
      { n: 920, s: 'cdcdcd9a9a9a666666333333' }];

   moreCol.forEach(entry => {
      const s = entry.s;
      for (let n = 0; n < s.length; n += 6) {
         const num = entry.n + n / 6;
         colorMap[num] = conv(s.slice(n, n + 6));
      }
   });

   gbl_colors_list = colorMap;
}

/** @summary Get list of colors
  * @private */
function getRootColors() {
   return gbl_colors_list;
}

/** @summary Produces rgb code for TColor object
  * @private */
function getRGBfromTColor(col) {
   if (col?._typename !== clTColor)
      return null;

   const rgb = toColor(col.fRed, col.fGreen, col.fBlue, col.fAlpha);

   switch (rgb) {
      case 'rgb(255, 255, 255)': return 'white';
      case 'rgb(0, 0, 0)': return 'black';
      case 'rgb(255, 0, 0)': return 'red';
      case 'rgb(0, 255, 0)': return 'green';
      case 'rgb(0, 0, 255)': return 'blue';
      case 'rgb(255, 255, 0)': return 'yellow';
      case 'rgb(255, 0, 255)': return 'magenta';
      case 'rgb(0, 255, 255)': return 'cyan';
   }
   return rgb;
}

/** @summary Return list of grey colors for the original array
  * @private */
function getGrayColors(rgb_array) {
   const gray_colors = [];

   if (!rgb_array)
      rgb_array = getRootColors();

   for (let n = 0; n < rgb_array.length; ++n) {
      if (!rgb_array[n])
         continue;
      const rgb = color(rgb_array[n]),
            gray = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b;
      rgb.r = rgb.g = rgb.b = gray;
      gray_colors[n] = rgb.formatRgb();
   }

   return gray_colors;
}

/** @summary Add new colors from object array
  * @private */
function extendRootColors(jsarr, objarr, grayscale) {
   if (!jsarr) {
      jsarr = [];
      for (let n = 0; n < gbl_colors_list.length; ++n)
         jsarr[n] = gbl_colors_list[n];
   }

   if (!objarr)
      return jsarr;

   let rgb_array = objarr;
   if (objarr._typename && objarr.arr) {
      rgb_array = [];
      for (let n = 0; n < objarr.arr.length; ++n) {
         const col = objarr.arr[n];
         if ((col?._typename === clTLinearGradient) || (col?._typename === clTRadialGradient)) {
            rgb_array[col.fNumber] = col;
            col.toString = () => 'white';
            continue;
         }

         if (col?._typename !== clTColor)
            continue;

         if ((col.fNumber >= 0) && (col.fNumber <= 10000))
            rgb_array[col.fNumber] = getRGBfromTColor(col);
      }
   }

   for (let n = 0; n < rgb_array.length; ++n) {
      if (rgb_array[n] && (jsarr[n] !== rgb_array[n]))
         jsarr[n] = rgb_array[n];
   }

   return grayscale ? getGrayColors(jsarr) : jsarr;
}

/** @summary Set global list of colors.
  * @desc Either TObjArray of TColor instances or just plain array with rgb() code.
  * List of colors typically stored together with TCanvas primitives
  * @private */
function adoptRootColors(objarr) {
   extendRootColors(gbl_colors_list, objarr);
}

/** @summary Return ROOT color by index
  * @desc Color numbering corresponds typical ROOT colors
  * @return {String} with RGB color code or existing color name like 'cyan'
  * @private */
function getColor(indx) {
   return gbl_colors_list[indx];
}

/** @summary Search for specified color in the list of colors
  * @return Color index or -1 if fails
  * @private */
function findColor(name) {
   if (!name)
      return -1;
   for (let indx = 0; indx < gbl_colors_list.length; ++indx) {
      if (gbl_colors_list[indx] === name)
         return indx;
   }
   return -1;
}

/** @summary Add new color
  * @param {string} rgb - color name or just string with rgb value
  * @param {array} [lst] - optional colors list, to which add colors
  * @return {number} index of new color
  * @private */
function addColor(rgb, lst) {
   if (!lst)
      lst = gbl_colors_list;

   if ((rgb[0] === '#') && (isNodeJs() || (isBatchMode() && settings.ApproxTextSize)))
      rgb = color(rgb).formatRgb();

   const indx = lst.indexOf(rgb);
   if (indx >= 0)
      return indx;
   lst.push(rgb);
   return lst.length - 1;
}

/**
 * @summary Color palette handle
 *
 * @private
 */

class ColorPalette {

   /** @summary constructor */
   constructor(arr, grayscale) {
      this.palette = grayscale ? getGrayColors(arr) : arr;
   }

   /** @summary Returns color index which correspond to contour index of provided length */
   calcColorIndex(i, len) {
      const plen = this.palette.length, theColor = Math.floor((i + 0.99) * plen / (len - 1));
      return (theColor > plen - 1) ? plen - 1 : theColor;
   }

   /** @summary Returns color with provided index */
   getColor(indx) { return this.palette[indx]; }

   /** @summary Returns number of colors in the palette */
   getLength() { return this.palette.length; }

   /** @summary Calculate color for given i and len */
   calcColor(i, len) { return this.getColor(this.calcColorIndex(i, len)); }

} // class ColorPalette

function createDefaultPalette(grayscale) {
   const hue2rgb = (p, q, t) => {
      if (t < 0)
         t += 1;
      if (t > 1)
         t -= 1;
      if (t < 1 / 6)
         return p + (q - p) * 6 * t;
      if (t < 1 / 2)
         return q;
      if (t < 2 / 3)
         return p + (q - p) * (2 / 3 - t) * 6;
      return p;
   }, HLStoRGB = (h, l, s) => {
      const q = l + s - l * s,
            p = 2 * l - q,
            r = hue2rgb(p, q, h + 1 / 3),
            g = hue2rgb(p, q, h),
            b = hue2rgb(p, q, h - 1 / 3);
      return toColor(r, g, b);
   }, minHue = 0, maxHue = 280, maxPretty = 50, palette = [];
   for (let i = 0; i < maxPretty; ++i) {
      const hue = (maxHue - (i + 1) * ((maxHue - minHue) / maxPretty)) / 360;
      palette.push(HLStoRGB(hue, 0.5, 1));
   }
   return new ColorPalette(palette, grayscale);
}

function createGrayPalette() {
   const palette = [];
   for (let i = 0; i < 50; ++i) {
      const code = toDec((i + 2) / 60);
      palette.push(`rgb(${code}, ${code}, ${code})`);
   }
   return new ColorPalette(palette);
}

/** @summary Create color palette
  * @private */
function getColorPalette(id, grayscale) {
   id = id || settings.Palette;
   if ((id > 0) && (id < 10))
      return createGrayPalette();
   if (id < 51)
      return createDefaultPalette(grayscale);
   if (id > 113)
      id = 57;
   const stops = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1];
   let rgb;
   /* eslint-disable @stylistic/js/comma-spacing */
   switch (id) {
      // Deep Sea
      case 51: rgb = [[0,9,13,17,24,32,27,25,29],[0,0,0,2,37,74,113,160,221],[28,42,59,78,98,129,154,184,221]]; break;
      // Grey Scale
      case 52: rgb = [[0,32,64,96,128,160,192,224,255],[0,32,64,96,128,160,192,224,255],[0,32,64,96,128,160,192,224,255]]; break;
      // Dark Body Radiator
      case 53: rgb = [[0,45,99,156,212,230,237,234,242],[0,0,0,45,101,168,238,238,243],[0,1,1,3,9,8,11,95,230]]; break;
      // Two-color hue (dark blue through neutral gray to bright yellow)
      case 54: rgb = [[0,22,44,68,93,124,160,192,237],[0,16,41,67,93,125,162,194,241],[97,100,99,99,93,68,44,26,74]]; break;
      // Rain Bow
      case 55: rgb = [[0,5,15,35,102,196,208,199,110],[0,48,124,192,206,226,97,16,0],[99,142,198,201,90,22,13,8,2]]; break;
      // Inverted Dark Body Radiator
      case 56: rgb = [[242,234,237,230,212,156,99,45,0],[243,238,238,168,101,45,0,0,0],[230,95,11,8,9,3,1,1,0]]; break;
      // Bird (default, keep float for backward compatibility)
      case 57:
         rgb = [[53.091,15.096,19.89,5.916,45.951,135.1755,208.743,253.878,248.982],
                [42.432,91.7745,128.5455,163.6845,183.039,191.046,186.864,200.481,250.716],
                [134.9715,221.442,213.8175,201.807,163.8375,118.881,89.2245,50.184,13.7445]];
         break;
      // Cubehelix
      case 58: rgb = [[0,24,2,54,176,236,202,194,255],[0,29,92,129,117,120,176,236,255],[0,68,80,34,57,172,252,245,255]]; break;
      // Green Red Violet
      case 59: rgb = [[13,23,25,63,76,104,137,161,206],[95,67,37,21,0,12,35,52,79],[4,3,2,6,11,22,49,98,208]]; break;
      // Blue Red Yellow
      case 60: rgb = [[0,61,89,122,143,160,185,204,231],[0,0,0,0,14,37,72,132,235],[0,140,224,144,4,5,6,9,13]]; break;
      // Ocean
      case 61: rgb = [[14,7,2,0,5,11,55,131,229],[105,56,26,1,42,74,131,171,229],[2,21,35,60,92,113,160,185,229]]; break;
      // Color Printable On Grey
      case 62: rgb = [[0,0,0,70,148,231,235,237,244],[0,0,0,0,0,69,67,216,244],[0,102,228,231,177,124,137,20,244]]; break;
      // Alpine
      case 63: rgb = [[50,56,63,68,93,121,165,192,241],[66,81,91,96,111,128,155,189,241],[97,91,75,65,77,103,143,167,217]]; break;
      // Aquamarine
      case 64: rgb = [[145,166,167,156,131,114,101,112,132],[158,178,179,181,163,154,144,152,159],[190,199,201,192,176,169,160,166,190]]; break;
      // Army
      case 65: rgb = [[93,91,99,108,130,125,132,155,174],[126,124,128,129,131,121,119,153,173],[103,94,87,85,80,85,107,120,146]]; break;
      // Atlantic
      case 66: rgb = [[24,40,69,90,104,114,120,132,103],[29,52,94,127,150,162,159,151,101],[29,52,96,132,162,181,184,186,131]]; break;
      // Aurora
      case 67: rgb = [[46,38,61,92,113,121,132,150,191],[46,36,40,69,110,135,131,92,34],[46,80,74,70,81,105,165,211,225]]; break;
      // Avocado
      case 68: rgb = [[0,4,12,30,52,101,142,190,237],[0,40,86,121,140,172,187,213,240],[0,9,14,18,21,23,27,35,101]]; break;
      // Beach
      case 69: rgb = [[198,206,206,211,198,181,161,171,244],[103,133,150,172,178,174,163,175,244],[49,54,55,66,91,130,184,224,244]]; break;
      // Black Body
      case 70: rgb = [[243,243,240,240,241,239,186,151,129],[0,46,99,149,194,220,183,166,147],[6,8,36,91,169,235,246,240,233]]; break;
      // Blue Green Yellow
      case 71: rgb = [[22,19,19,25,35,53,88,139,210],[0,32,69,108,135,159,183,198,215],[77,96,110,116,110,100,90,78,70]]; break;
      // Brown Cyan
      case 72: rgb = [[68,116,165,182,189,180,145,111,71],[37,82,135,178,204,225,221,202,147],[16,55,105,147,196,226,232,224,178]]; break;
      // CMYK
      case 73: rgb = [[61,99,136,181,213,225,198,136,24],[149,140,96,83,132,178,190,135,22],[214,203,168,135,110,100,111,113,22]]; break;
      // Candy
      case 74: rgb = [[76,120,156,183,197,180,162,154,140],[34,35,42,69,102,137,164,188,197],[64,69,78,105,142,177,205,217,198]]; break;
      // Cherry
      case 75: rgb = [[37,102,157,188,196,214,223,235,251],[37,29,25,37,67,91,132,185,251],[37,32,33,45,66,98,137,187,251]]; break;
      // Coffee
      case 76: rgb = [[79,100,119,137,153,172,192,205,250],[63,79,93,103,115,135,167,196,250],[51,59,66,61,62,70,110,160,250]]; break;
      // Dark Rain Bow
      case 77: rgb = [[43,44,50,66,125,172,178,155,157],[63,63,85,101,138,163,122,51,39],[121,101,58,44,47,55,57,44,43]]; break;
      // Dark Terrain
      case 78: rgb = [[0,41,62,79,90,87,99,140,228],[0,57,81,93,85,70,71,125,228],[95,91,91,82,60,43,44,112,228]]; break;
      // Fall
      case 79: rgb = [[49,59,72,88,114,141,176,205,222],[78,72,66,57,59,75,106,142,173],[78,55,46,40,39,39,40,41,47]]; break;
      // Fruit Punch
      case 80: rgb = [[243,222,201,185,165,158,166,187,219],[94,108,132,135,125,96,68,51,61],[7,9,12,19,45,89,118,146,118]]; break;
      // Fuchsia
      case 81: rgb = [[19,44,74,105,137,166,194,206,220],[19,28,40,55,82,110,159,181,220],[19,42,68,96,129,157,188,203,220]]; break;
      // Grey Yellow
      case 82: rgb = [[33,44,70,99,140,165,199,211,216],[38,50,76,105,140,165,191,189,167],[55,67,97,124,140,166,163,129,52]]; break;
      // Green Brown Terrain
      case 83: rgb = [[0,33,73,124,136,152,159,171,223],[0,43,92,124,134,126,121,144,223],[0,43,68,76,73,64,72,114,223]]; break;
      // Green Pink
      case 84: rgb = [[5,18,45,124,193,223,205,128,49],[48,134,207,230,193,113,28,0,7],[6,15,41,121,193,226,208,130,49]]; break;
      // Island
      case 85: rgb = [[180,106,104,135,164,188,189,165,144],[72,126,154,184,198,207,205,190,179],[41,120,158,188,194,181,145,100,62]]; break;
      // Lake
      case 86: rgb = [[57,72,94,117,136,154,174,192,215],[0,33,68,109,140,171,192,196,209],[116,137,173,201,200,201,203,190,187]]; break;
      // Light Temperature
      case 87: rgb = [[31,71,123,160,210,222,214,199,183],[40,117,171,211,231,220,190,132,65],[234,214,228,222,210,160,105,60,34]]; break;
      // Light Terrain
      case 88: rgb = [[123,108,109,126,154,172,188,196,218],[184,138,130,133,154,175,188,196,218],[208,130,109,99,110,122,150,171,218]]; break;
      // Mint
      case 89: rgb = [[105,106,122,143,159,172,176,181,207],[252,197,194,187,174,162,153,136,125],[146,133,144,155,163,167,166,162,174]]; break;
      // Neon
      case 90: rgb = [[171,141,145,152,154,159,163,158,177],[236,143,100,63,53,55,44,31,6],[59,48,46,44,42,54,82,112,179]]; break;
      // Pastel
      case 91: rgb = [[180,190,209,223,204,228,205,152,91],[93,125,147,172,181,224,233,198,158],[236,218,160,133,114,132,162,220,218]]; break;
      // Pearl
      case 92: rgb = [[225,183,162,135,115,111,119,145,211],[205,177,166,135,124,117,117,132,172],[186,165,155,135,126,130,150,178,226]]; break;
      // Pigeon
      case 93: rgb = [[39,43,59,63,80,116,153,177,223],[39,43,59,74,91,114,139,165,223],[39,50,59,70,85,115,151,176,223]]; break;
      // Plum
      case 94: rgb = [[0,38,60,76,84,89,101,128,204],[0,10,15,23,35,57,83,123,199],[0,11,22,40,63,86,97,94,85]]; break;
      // Red Blue
      case 95: rgb = [[94,112,141,165,167,140,91,49,27],[27,46,88,135,166,161,135,97,58],[42,52,81,106,139,158,155,137,116]]; break;
      // Rose
      case 96: rgb = [[30,49,79,117,135,151,146,138,147],[63,60,72,90,94,94,68,46,16],[18,28,41,56,62,63,50,36,21]]; break;
      // Rust
      case 97: rgb = [[0,30,63,101,143,152,169,187,230],[0,14,28,42,58,61,67,74,91],[39,26,21,18,15,14,14,13,13]]; break;
      // Sandy Terrain
      case 98: rgb = [[149,140,164,179,182,181,131,87,61],[62,70,107,136,144,138,117,87,74],[40,38,45,49,49,49,38,32,34]]; break;
      // Sienna
      case 99: rgb = [[99,112,148,165,179,182,183,183,208],[39,40,57,79,104,127,148,161,198],[15,16,18,33,51,79,103,129,177]]; break;
      // Solar
      case 100: rgb = [[99,116,154,174,200,196,201,201,230],[0,0,8,32,58,83,119,136,173],[5,6,7,9,9,14,17,19,24]]; break;
      // South West
      case 101: rgb = [[82,106,126,141,155,163,142,107,66],[62,44,69,107,135,152,149,132,119],[39,25,31,60,73,68,49,72,188]]; break;
      // Starry Night
      case 102: rgb = [[18,29,44,72,116,158,184,208,221],[27,46,71,105,146,177,189,190,183],[39,55,80,108,130,133,124,100,76]]; break;
      // Sunset
      case 103: rgb = [[0,48,119,173,212,224,228,228,245],[0,13,30,47,79,127,167,205,245],[0,68,75,43,16,22,55,128,245]]; break;
      // Temperature Map
      case 104: rgb = [[34,70,129,187,225,226,216,193,179],[48,91,147,194,226,229,196,110,12],[234,212,216,224,206,110,53,40,29]]; break;
      // Thermometer
      case 105: rgb = [[30,55,103,147,174,203,188,151,105],[0,65,138,182,187,175,121,53,9],[191,202,212,208,171,140,97,57,30]]; break;
      // Valentine
      case 106: rgb = [[112,97,113,125,138,159,178,188,225],[16,17,24,37,56,81,110,136,189],[38,35,46,59,78,103,130,152,201]]; break;
      // Visible Spectrum
      case 107: rgb = [[18,72,5,23,29,201,200,98,29],[0,0,43,167,211,117,0,0,0],[51,203,177,26,10,9,8,3,0]]; break;
      // Water Melon
      case 108: rgb = [[19,42,64,88,118,147,175,187,205],[19,55,89,125,154,169,161,129,70],[19,32,47,70,100,128,145,130,75]]; break;
      // Cool
      case 109: rgb = [[33,31,42,68,86,111,141,172,227],[255,175,145,106,88,55,15,0,0],[255,205,202,203,208,205,203,206,231]]; break;
      // Copper
      case 110: rgb = [[0,25,50,79,110,145,181,201,254],[0,16,30,46,63,82,101,124,179],[0,12,21,29,39,49,61,74,103]]; break;
      // Gist Earth
      case 111: rgb = [[0,13,30,44,72,120,156,200,247],[0,36,84,117,141,153,151,158,247],[0,94,100,82,56,66,76,131,247]]; break;
      // Viridis
      case 112: rgb = [[26,51,43,33,28,35,74,144,246],[9,24,55,87,118,150,180,200,222],[30,96,112,114,112,101,72,35,0]]; break;
      // Cividis
      case 113: rgb = [[0,5,65,97,124,156,189,224,255],[32,54,77,100,123,148,175,203,234],[77,110,107,111,120,119,111,94,70]]; break;
      default: return createDefaultPalette();
   }
   /* eslint-enable @stylistic/js/comma-spacing */

   const NColors = 255, Red = rgb[0], Green = rgb[1], Blue = rgb[2], palette = [];

   for (let g = 1; g < stops.length; g++) {
      // create the colors...
      const nColorsGradient = Math.round(Math.floor(NColors * stops[g]) - Math.floor(NColors * stops[g - 1]));
      for (let c = 0; c < nColorsGradient; c++) {
         const col = 'rgb(' + toDec(Red[g - 1] + c * (Red[g] - Red[g - 1]) / nColorsGradient, 1) + ', ' +
                              toDec(Green[g - 1] + c * (Green[g] - Green[g - 1]) / nColorsGradient, 1) + ', ' +
                              toDec(Blue[g - 1] + c * (Blue[g] - Blue[g - 1]) / nColorsGradient, 1) + ')';
         palette.push(col);
      }
   }

   return new ColorPalette(palette, grayscale);
}


/** @summary Decode list of ROOT colors coded by TWebCanvas
  * @private */
function decodeWebCanvasColors(oper) {
   const colors = [], arr = oper.split(';'),
         convert_rgb = isNodeJs() || (isBatchMode() && settings.ApproxTextSize);
   for (let n = 0; n < arr.length; ++n) {
      const name = arr[n];
      let p = name.indexOf(':');
      if (p > 0) {
         const col = `rgb(${name.slice(p + 1)})`;
         colors[parseInt(name.slice(0, p))] = convert_rgb ? color(col).formatRgb() : col;
         continue;
      }
      p = name.indexOf('=');
      if (p > 0) {
         let col = `rgba(${name.slice(p + 1)})`;
         if (convert_rgb) {
            col = color(col);
            col.opacity = (Math.round(col.opacity * 255) / 255).toFixed(2);
            col = col.formatRgb();
         }
         colors[parseInt(name.slice(0, p))] = col;
         continue;
      }
      p = name.indexOf('#');
      if (p < 0)
         continue;

      const colindx = parseInt(name.slice(0, p)),
            data = JSON.parse(name.slice(p + 1)),
            grad = { _typename: data[0] === 10 ? clTLinearGradient : clTRadialGradient, fNumber: colindx, fType: data[0] };

      let cnt = 1;

      grad.fCoordinateMode = Math.round(data[cnt++]);
      const nsteps = Math.round(data[cnt++]);
      grad.fColorPositions = data.slice(cnt, cnt + nsteps);
      cnt += nsteps;
      grad.fColors = data.slice(cnt, cnt + 4 * nsteps);
      cnt += 4 * nsteps;
      grad.fStart = { fX: data[cnt++], fY: data[cnt++] };
      grad.fEnd = { fX: data[cnt++], fY: data[cnt++] };
      if (grad._typename === clTRadialGradient && cnt < data.length) {
         grad.fR1 = data[cnt++];
         grad.fR2 = data[cnt];
      }

      colors[colindx] = grad;
   }

   return colors;
}


createRootColors();

/** @summary Standard prefix for SVG file context as data url
 * @private */
const prSVG = 'data:image/svg+xml;charset=utf-8,',
/** @summary Standard prefix for JSON file context as data url
 * @private */
      prJSON = 'data:application/json;charset=utf-8,';


/** @summary Returns visible rect of element
  * @param {object} elem - d3.select object with element
  * @param {string} [kind] - which size method is used
  * @desc kind = 'bbox' use getBBox, works only with SVG
  * kind = 'full' - full size of element, using getBoundingClientRect function
  * kind = 'nopadding' - excludes padding area
  * With node.js can use 'width' and 'height' attributes when provided in element
  * @private */
function getElementRect(elem, sizearg) {
   if (!elem || elem.empty())
      return { x: 0, y: 0, width: 0, height: 0 };

   if ((isNodeJs() && (sizearg !== 'bbox')) || elem.property('_batch_mode'))
      return { x: 0, y: 0, width: parseInt(elem.attr('width')), height: parseInt(elem.attr('height')) };

   const styleValue = name => {
      let value = elem.style(name);
      if (!value || !isStr(value))
         return 0;
      value = parseFloat(value.replace('px', ''));
      return !Number.isFinite(value) ? 0 : Math.round(value);
   };

   let rect = elem.node().getBoundingClientRect();
   if ((sizearg === 'bbox') && (parseFloat(rect.width) > 0))
      rect = elem.node().getBBox();

   const res = { x: 0, y: 0, width: parseInt(rect.width), height: parseInt(rect.height) };
   if (rect.left !== undefined) {
      res.x = parseInt(rect.left);
      res.y = parseInt(rect.top);
   } else if (rect.x !== undefined) {
      res.x = parseInt(rect.x);
      res.y = parseInt(rect.y);
   }

   if ((sizearg === undefined) || (sizearg === 'nopadding')) {
      // this is size exclude padding area
      res.width -= styleValue('padding-left') + styleValue('padding-right');
      res.height -= styleValue('padding-top') + styleValue('padding-bottom');
   }

   return res;
}


/** @summary Calculate absolute position of provided element in canvas
  * @private */
function getAbsPosInCanvas(sel, pos) {
   if (!pos)
      return pos;

   while (!sel.empty() && !sel.classed('root_canvas')) {
      const cl = sel.attr('class');
      if (cl && ((cl.indexOf('root_frame') >= 0) || (cl.indexOf('__root_pad_') >= 0))) {
         pos.x += sel.property('draw_x') || 0;
         pos.y += sel.property('draw_y') || 0;
      }
      sel = select(sel.node().parentNode);
   }
   return pos;
}


/** @summary Converts numeric value to string according to specified format.
  * @param {number} value - value to convert
  * @param {string} [fmt='6.4g'] - format can be like 5.4g or 4.2e or 6.4f
  * @param {boolean} [ret_fmt] - when true returns array with value and actual format like ['0.1','6.4f']
  * @return {string|Array} - converted value or array with value and actual format
  * @private */
function floatToString(value, fmt, ret_fmt) {
   if (!fmt)
      fmt = '6.4g';
   else if (fmt === 'g')
      fmt = '7.5g';

   fmt = fmt.trim();
   const len = fmt.length;
   if (len < 2)
      return ret_fmt ? [value.toFixed(4), '6.4f'] : value.toFixed(4);

   const kind = fmt[len - 1].toLowerCase(),
         compact = (len > 1) && (fmt[len - 2] === 'c') ? 'c' : '';
   fmt = fmt.slice(0, len - (compact ? 2 : 1));

   if (kind === 'g') {
      const se = floatToString(value, fmt + 'ce', true),
            sg = floatToString(value, fmt + 'cf', true),
            res = se[0].length < sg[0].length || ((sg[0] === '0') && value) ? se : sg;
      return ret_fmt ? res : res[0];
   }

   let isexp, prec = fmt.indexOf('.');
   prec = (prec < 0) ? 4 : parseInt(fmt.slice(prec + 1));
   if (!Number.isInteger(prec) || (prec <= 0))
      prec = 4;

   switch (kind) {
      case 'e':
         isexp = true;
         break;
      case 'f':
         isexp = false;
         break;
      default:
         isexp = false;
         prec = 4;
   }

   if (isexp) {
      let se = value.toExponential(prec);

      if (compact) {
         const pnt = se.indexOf('.'),
               pe = se.toLowerCase().indexOf('e');
         if ((pnt > 0) && (pe > pnt)) {
            let p = pe;
            while ((p > pnt) && (se[p - 1] === '0'))
               p--;
            if (p === pnt + 1)
               p--;
            if (p !== pe)
               se = se.slice(0, p) + se.slice(pe);
         }
      }

      return ret_fmt ? [se, `${prec + 2}.${prec}${compact}e`] : se;
   }

   let sg = value.toFixed(prec);

   if (compact) {
      let l = 0;
      while ((l < sg.length) && (sg[l] === '0' || sg[l] === '-' || sg[l] === '.'))
         l++;

      let diff = sg.length - l - prec;
      if (sg.indexOf('.') > l)
         diff--;

      if (diff) {
         prec -= diff;
         if (prec < 0)
            prec = 0;
         else if (prec > 20)
            prec = 20;
         sg = value.toFixed(prec);
      }

      const pnt = sg.indexOf('.');
      if (pnt > 0) {
         let p = sg.length;
         while ((p > pnt) && (sg[p - 1] === '0'))
            p--;
         if (p === pnt + 1)
            p--;
         sg = sg.slice(0, p);
      }

      if (sg === '-0')
         sg = '0';
   }

   return ret_fmt ? [sg, `${prec + 2}.${prec}${compact}f`] : sg;
}


/** @summary Draw options interpreter
  * @private */
class DrawOptions {

   constructor(opt) {
      if (isStr(opt)) {
         this.origin = opt.trim();
         this.opt = this.origin.toUpperCase();
      } else
         this.opt = this.origin = '';
      this.part = this.partO = '';
   }

   /** @summary Returns true if remaining options are empty or contain only separators symbols. */
   empty() { return !this.opt ? true : !this.opt.replace(/[ ;_,]/g, ''); }

   /** @summary Returns remaining part of the draw options. */
   remain() { return this.opt; }

   /** @summary Remove [pos, pos2) part from the string */
   #cut(pos, pos2) {
      this.opt = this.opt.slice(0, pos) + this.opt.slice(pos2);
      this.origin = this.origin.slice(0, pos) + this.origin.slice(pos2);
   }

   /** @summary Checks if given option exists */
   check(name, postpart) {
      const pos = this.opt.indexOf(name);
      if (pos < 0)
         return false;
      this.#cut(pos, pos + name.length);
      this.part = '';
      if (!postpart)
         return true;

      let pos2 = pos;
      const is_array = postpart === 'array';
      if (is_array) {
         if (this.opt[pos2] !== '[')
            return false;
         while ((pos2 < this.opt.length) && (this.opt[pos2] !== ']'))
            pos2++;
         if (++pos2 > this.opt.length)
            return false;
      } else {
         while ((pos2 < this.opt.length) && (this.opt[pos2] !== ' ') && (this.opt[pos2] !== ',') && (this.opt[pos2] !== ';'))
            pos2++;
      }
      if (pos2 > pos) {
         this.part = this.opt.slice(pos, pos2);
         this.partO = this.origin.slice(pos, pos2);
         this.#cut(pos, pos2);
      }

      if (is_array) {
         try {
            this.array = JSON.parse(this.part);
         } catch {
            this.array = undefined;
         }
         return this.array?.length !== undefined;
      }

      if (postpart !== 'color')
         return true;

      if (((this.part.length === 6) || (this.part.length === 8)) && this.part.match(/^[a-fA-F0-9]+/)) {
         this.color = addColor('#' + this.part);
         return true;
      }

      this.color = this.partAsInt(1) - 1;
      if (this.color >= 0)
         return true;
      for (let col = 0; col < 8; ++col) {
         if (getColor(col).toUpperCase() === this.part) {
            this.color = col;
            return true;
         }
      }
      return false;
   }

   /** @summary Returns (original) part after found options. */
   getPart(origin) { return origin ? this.partO : this.part; }

   /** @summary Returns remaining part of found option as integer. */
   partAsInt(offset, dflt) {
      let mult = 1;
      const last = this.part ? this.part.at(-1) : '';
      if (last === 'K')
         mult = 1e3;
      else if (last === 'M')
         mult = 1e6;
      else if (last === 'G')
         mult = 1e9;
      let val = this.part.replace(/^\D+/g, '');
      val = val ? parseInt(val, 10) : Number.NaN;
      return !Number.isInteger(val) ? (dflt || 0) : mult * val + (offset || 0);
   }

   /** @summary Returns remaining part of found option as float. */
   partAsFloat(offset, dflt) {
      let val = this.part.replace(/^\D+/g, '');
      val = val ? parseFloat(val) : Number.NaN;
      return !Number.isFinite(val) ? (dflt || 0) : val + (offset || 0);
   }

} // class DrawOptions


/** @summary Simple random generator with controlled seed
  * @private */
class TRandom {

   constructor(i) {
      if (i !== undefined)
         this.seed(i);
   }

   /** @summary Seed simple random generator */
   seed(i) {
      i = Math.abs(i);
      if (i > 1e8)
         i = Math.abs(1e8 * Math.sin(i));
      else if (i < 1)
         i *= 1e8;
      this.m_w = Math.round(i);
      this.m_z = 987654321;
   }

   /** @summary Produce random value between 0 and 1 */
   random() {
      if (this.m_z === undefined)
         return Math.random();
      this.m_z = (36969 * (this.m_z & 65535) + (this.m_z >> 16)) & 0xffffffff;
      this.m_w = (18000 * (this.m_w & 65535) + (this.m_w >> 16)) & 0xffffffff;
      let result = ((this.m_z << 16) + this.m_w) & 0xffffffff;
      result /= 4294967296;
      return result + 0.5;
   }

} // class TRandom


/** @summary Build smooth SVG curve using Bezier
  * @desc Reuse code from https://stackoverflow.com/questions/62855310
  * @private */
function buildSvgCurve(p, args) {
   if (!args)
      args = {};
   if (!args.line)
      args.calc = true;
   else if (args.ndig === undefined)
      args.ndig = 0;

   let npnts = p.length;
   if (npnts < 3)
      args.line = true;

   args.t = args.t ?? 0.2;

   if ((args.ndig === undefined) || args.height) {
      args.maxy = p[0].gry;
      args.mindiff = 100;
      for (let i = 1; i < npnts; i++) {
         args.maxy = Math.max(args.maxy, p[i].gry);
         args.mindiff = Math.min(args.mindiff, Math.abs(p[i].grx - p[i - 1].grx), Math.abs(p[i].gry - p[i - 1].gry));
      }
      if (args.ndig === undefined)
         args.ndig = args.mindiff > 20 ? 0 : (args.mindiff > 5 ? 1 : 2);
   }

   const end_point = (pnt1, pnt2, sign) => {
      const len = Math.sqrt((pnt2.gry - pnt1.gry) ** 2 + (pnt2.grx - pnt1.grx) ** 2) * args.t,
            a2 = Math.atan2(pnt2.dgry, pnt2.dgrx),
            a1 = Math.atan2(sign * (pnt2.gry - pnt1.gry), sign * (pnt2.grx - pnt1.grx));

      pnt1.dgrx = len * Math.cos(2 * a1 - a2);
      pnt1.dgry = len * Math.sin(2 * a1 - a2);
   }, conv = val => {
      if (!args.ndig || (Math.round(val) === val))
         return val.toFixed(0);
      let s = val.toFixed(args.ndig), p1 = s.length - 1;
      while (s[p1] === '0')
         p1--;
      if (s[p1] === '.')
         p1--;
      s = s.slice(0, p1 + 1);
      return (s === '-0') ? '0' : s;
   };

   if (args.calc) {
      for (let i = 1; i < npnts - 1; i++) {
         p[i].dgrx = (p[i + 1].grx - p[i - 1].grx) * args.t;
         p[i].dgry = (p[i + 1].gry - p[i - 1].gry) * args.t;
      }

      if (npnts > 2) {
         end_point(p[0], p[1], 1);
         end_point(p[npnts - 1], p[npnts - 2], -1);
      } else if (p.length === 2) {
         p[0].dgrx = (p[1].grx - p[0].grx) * args.t;
         p[0].dgry = (p[1].gry - p[0].gry) * args.t;
         p[1].dgrx = -p[0].dgrx;
         p[1].dgry = -p[0].dgry;
      }
   }

   let path = `${args.cmd ?? 'M'}${conv(p[0].grx)},${conv(p[0].gry)}`;

   if (!args.line) {
      let i0 = 1;
      if (args.qubic) {
         npnts--; i0++;
         path += `Q${conv(p[1].grx - p[1].dgrx)},${conv(p[1].gry - p[1].dgry)},${conv(p[1].grx)},${conv(p[1].gry)}`;
      }
      path += `C${conv(p[i0 - 1].grx + p[i0 - 1].dgrx)},${conv(p[i0 - 1].gry + p[i0 - 1].dgry)},${conv(p[i0].grx - p[i0].dgrx)},${conv(p[i0].gry - p[i0].dgry)},${conv(p[i0].grx)},${conv(p[i0].gry)}`;

      // continue with simpler points
      for (let i = i0 + 1; i < npnts; i++)
         path += `S${conv(p[i].grx - p[i].dgrx)},${conv(p[i].gry - p[i].dgry)},${conv(p[i].grx)},${conv(p[i].gry)}`;

      if (args.qubic)
         path += `Q${conv(p[npnts].grx - p[npnts].dgrx)},${conv(p[npnts].gry - p[npnts].dgry)},${conv(p[npnts].grx)},${conv(p[npnts].gry)}`;
   } else if (npnts < 10000) {
      // build simple curve

      let acc_x = 0, acc_y = 0, currx = Math.round(p[0].grx), curry = Math.round(p[0].gry);

      const flush = () => {
         if (acc_x) {
            path += 'h' + acc_x;
            acc_x = 0;
         }
         if (acc_y) {
            path += 'v' + acc_y;
            acc_y = 0;
         }
      };

      for (let n = 1; n < npnts; ++n) {
         const bin = p[n],
               dx = Math.round(bin.grx) - currx,
               dy = Math.round(bin.gry) - curry;
         if (dx && dy) {
            flush();
            path += `l${dx},${dy}`;
         } else if (!dx && dy) {
            if ((acc_y === 0) || ((dy < 0) !== (acc_y < 0)))
               flush();
            acc_y += dy;
         } else if (dx && !dy) {
            if ((acc_x === 0) || ((dx < 0) !== (acc_x < 0)))
               flush();
            acc_x += dx;
         }
         currx += dx;
         curry += dy;
      }

      flush();
   } else {
      // build line with trying optimize many vertical moves
      let currx = Math.round(p[0].grx), curry = Math.round(p[0].gry),
          cminy = curry, cmaxy = curry, prevy = curry;

      for (let n = 1; n < npnts; ++n) {
         const bin = p[n],
               lastx = Math.round(bin.grx),
               lasty = Math.round(bin.gry),
               dx = lastx - currx;
         if (dx === 0) {
            // if X not change, just remember amplitude and
            cminy = Math.min(cminy, lasty);
            cmaxy = Math.max(cmaxy, lasty);
            prevy = lasty;
            continue;
         }

         if (cminy !== cmaxy) {
            if (cminy !== curry)
               path += `v${cminy - curry}`;
            path += `v${cmaxy - cminy}`;
            if (cmaxy !== prevy)
               path += `v${prevy - cmaxy}`;
            curry = prevy;
         }
         const dy = lasty - curry;
         if (dy)
            path += `l${dx},${dy}`;
         else
            path += `h${dx}`;
         currx = lastx;
         curry = lasty;
         prevy = cminy = cmaxy = lasty;
      }

      if (cminy !== cmaxy) {
         if (cminy !== curry)
            path += `v${cminy - curry}`;
         path += `v${cmaxy - cminy}`;
         if (cmaxy !== prevy)
            path += `v${prevy - cmaxy}`;
      }
   }

   if (args.height)
      args.close = `L${conv(p.at(-1).grx)},${conv(Math.max(args.maxy, args.height))}H${conv(p[0].grx)}Z`;

   return path;
}

/** @summary Compress SVG code, produced from drawing
  * @desc removes extra info or empty elements
  * @private */
function compressSVG(svg) {
   svg = svg.replace(/url\(&quot;#(\w+)&quot;\)/g, 'url(#$1)')         // decode all URL
            .replace(/ class="\w*"/g, '')                              // remove all classes
            .replace(/ pad="\w*"/g, '')                                // remove all pad ids
            .replace(/ title=""/g, '')                                 // remove all empty titles
            .replace(/ style=""/g, '')                                 // remove all empty styles
            .replace(/<g objname="\w*" objtype="\w*"/g, '<g')          // remove object ids
            .replace(/<g transform="translate\([0-9,]+\)"><\/g>/g, '')  // remove all empty groups with transform
            .replace(/<g transform="translate\([0-9,]+\)" style="display: none;"><\/g>/g, '') // remove hidden title
            .replace(/<g><\/g>/g, '');                                 // remove all empty groups

   // remove all empty frame svg, typically appears in 3D drawings, maybe should be improved in frame painter itself
   svg = svg.replace(/<svg x="0" y="0" overflow="hidden" width="\d+" height="\d+" viewBox="0 0 \d+ \d+"><\/svg>/g, '');

   return svg;
}


/**
 * @summary Base painter class
 *
 */

class BasePainter {

   #divid;  // either id of DOM element or element itself
   #selected_main; // d3.select for dom elements
   #hitemname; // item name in the hpainter
   #hdrawopt; // draw option in the hpainter
   #hpainter; // assigned hpainter

   /** @summary constructor
     * @param {object|string} [dom] - dom element or id of dom element */
   constructor(dom) {
      this.#divid = null; // either id of DOM element or element itself
      if (dom)
         this.setDom(dom);
   }

   /** @summary Assign painter to specified DOM element
     * @param {string|object} elem - element ID or DOM Element
     * @desc Normally DOM element should be already assigned in constructor
     * @protected */
   setDom(elem) {
      if (elem !== undefined) {
         this.#divid = elem;
         this.#selected_main = null;
      }
   }

   /** @summary Returns assigned dom element */
   getDom() { return this.#divid; }

   /** @summary Returns argument for draw function */
   getDrawDom() { return this.#divid; }

   /** @summary Selects main HTML element assigned for drawing
     * @desc if main element was layout, returns main element inside layout
     * @param {string} [is_direct] - if 'origin' specified, returns original element even if actual drawing moved to some other place
     * @return {object} d3.select object for main element for drawing */
   selectDom(is_direct) {
      if (!this.#divid)
         return select(null);

      let res = this.#selected_main;
      if (!res) {
         if (isStr(this.#divid)) {
            let id = this.#divid;
            if (id[0] !== '#')
               id = '#' + id;
            res = select(id);
            if (!res.empty())
               this.#divid = res.node();
         } else
            res = select(this.#divid);
         this.#selected_main = res;
      }

      if (!res || res.empty() || (is_direct === 'origin'))
         return res;

      const use_enlarge = res.property('use_enlarge'),
            layout = res.property('layout') || 'simple',
            layout_selector = (layout === 'simple') ? '' : res.property('layout_selector');

      if (layout_selector)
         res = res.select(layout_selector);

      // one could redirect here
      if (!is_direct && !res.empty() && use_enlarge)
         res = select(getDocument().getElementById('jsroot_enlarge_div'));

      return res;
   }

   /** @summary Access/change top painter
     * @private */
   #accessTopPainter(on) {
      const chld = this.selectDom().node()?.firstChild;
      if (!chld)
         return null;
      if (on === true)
         chld.painter = this;
      else if ((on === false) && (chld.painter === this))
         delete chld.painter;
      return chld.painter;
   }

   /** @summary Set painter, stored in first child element
     * @desc Only make sense after first drawing is completed and any child element add to configured DOM
     * @protected */
   setTopPainter() { this.#accessTopPainter(true); }

   /** @summary Return top painter set for the selected dom element
     * @protected */
   getTopPainter() { return this.#accessTopPainter(); }

   /** @summary Clear reference on top painter
     * @protected */
   clearTopPainter() { this.#accessTopPainter(false); }

   /** @summary Generic method to cleanup painter
     * @desc Removes all visible elements and all internal data */
   cleanup(keep_origin) {
      this.clearTopPainter();
      const origin = this.selectDom('origin');
      if (!origin.empty() && !keep_origin)
         origin.html('');
      this.#divid = null;
      this.#selected_main = undefined;

      if (isFunc(this.#hpainter?.removePainter))
         this.#hpainter.removePainter(this);

      this.#hitemname = undefined;
      this.#hdrawopt = undefined;
      this.#hpainter = undefined;
   }

   /** @summary Checks if draw elements were resized and drawing should be updated
     * @return {boolean} true if resize was detected
     * @protected
     * @abstract */
   checkResize(/* arg */) {}

   /** @summary Function checks if geometry of main div was changed.
     * @desc take into account enlarge state, used only in PadPainter class
     * @return size of area when main div is drawn
     * @private */
   testMainResize(check_level, new_size, height_factor) {
      const enlarge = this.enlargeMain('state'),
            origin = this.selectDom('origin'),
            main = this.selectDom(),
            lmt = 5; // minimal size

      if ((enlarge !== 'on') && new_size?.width && new_size?.height) {
         origin.style('width', new_size.width + 'px')
               .style('height', new_size.height + 'px');
      }

      const rect_origin = getElementRect(origin, true),
            can_resize = origin.attr('can_resize');
      let do_resize = false;

      if ((can_resize === 'height') && height_factor && Math.abs(rect_origin.width * height_factor - rect_origin.height) > 0.1 * rect_origin.width)
         do_resize = true;

      if (((rect_origin.height <= lmt) || (rect_origin.width <= lmt)) && can_resize && (can_resize !== 'false'))
         do_resize = true;

      if (do_resize && (enlarge !== 'on')) {
         // if zero size and can_resize attribute set, change container size

         if (rect_origin.width > lmt) {
            height_factor = height_factor || 0.66;
            origin.style('height', Math.round(rect_origin.width * height_factor) + 'px');
         } else if (can_resize !== 'height')
            origin.style('width', '200px').style('height', '100px');
      }

      const rect = getElementRect(main),
            old_h = main.property('_jsroot_height'),
            old_w = main.property('_jsroot_width');

      rect.changed = false;

      if (!rect.width && !rect.height && !main.empty() && main.attr('style')) {
         const ws = main.style('width'), hs = main.style('height');
         if (isStr(ws) && isStr(hs) && ws.match(/^\d+px$/) && hs.match(/^\d+px$/)) {
            rect.width = parseInt(ws.slice(0, ws.length - 2));
            rect.height = parseInt(hs.slice(0, hs.length - 2));
         }
      }

      if (old_h && old_w && (old_h > 0) && (old_w > 0)) {
         if ((old_h !== rect.height) || (old_w !== rect.width))
            rect.changed = (check_level > 1) || (rect.width / old_w < 0.99) || (rect.width / old_w > 1.01) || (rect.height / old_h < 0.99) || (rect.height / old_h > 1.01);
      } else
         rect.changed = true;

      if (rect.changed)
         main.property('_jsroot_height', rect.height).property('_jsroot_width', rect.width);

      // after change enlarge state always mark main element as resized
      if (origin.property('did_enlarge')) {
         rect.changed = true;
         origin.property('did_enlarge', false);
      }

      return rect;
   }

   /** @summary Try enlarge main drawing element to full HTML page.
     * @param {string|boolean} action  - defines that should be done
     * @desc Possible values for action parameter:
     *    - true - try to enlarge
     *    - false - revert enlarge state
     *    - 'toggle' - toggle enlarge state
     *    - 'state' - only returns current enlarge state
     *    - 'verify' - check if element can be enlarged
     * if action not specified, just return possibility to enlarge main div
     * @protected */
   enlargeMain(action, skip_warning) {
      const main = this.selectDom(true),
            origin = this.selectDom('origin'),
            doc = getDocument();

      if (main.empty() || !settings.CanEnlarge || (origin.property('can_enlarge') === false))
         return false;

      if ((action === undefined) || (action === 'verify'))
         return true;

      const state = origin.property('use_enlarge') ? 'on' : 'off';

      if (action === 'state')
         return state;

      if (action === 'toggle')
         action = (state === 'off');

      let enlarge = select(doc.getElementById('jsroot_enlarge_div'));

      if ((action === true) && (state !== 'on')) {
         if (!enlarge.empty())
            return false;

         enlarge = select(doc.body)
            .append('div')
            .attr('id', 'jsroot_enlarge_div')
            .attr('style', 'position: fixed; margin: 0px; border: 0px; padding: 0px; left: 1px; top: 1px; bottom: 1px; right: 1px; background: white; opacity: 0.95; z-index: 100; overflow: hidden;');

         const rect1 = getElementRect(main),
               rect2 = getElementRect(enlarge);

         // if new enlarge area not big enough, do not do it
         if ((rect2.width <= rect1.width) || (rect2.height <= rect1.height)) {
            if (rect2.width * rect2.height < rect1.width * rect1.height) {
               if (!skip_warning)
                  console.log(`Enlarged area ${rect2.width} x ${rect2.height} smaller then original drawing ${rect1.width} x ${rect1.height}`);
               enlarge.remove();
               return false;
            }
         }

         while (main.node().childNodes.length)
            enlarge.node().appendChild(main.node().firstChild);

         origin.property('use_enlarge', true);
         origin.property('did_enlarge', true);
         return true;
      }
      if ((action === false) && (state !== 'off')) {
         while (enlarge.node()?.childNodes.length)
            main.node().appendChild(enlarge.node().firstChild);

         enlarge.remove();
         origin.property('use_enlarge', false);
         origin.property('did_enlarge', true);
         return true;
      }

      return false;
   }

   /** @summary Set item name, associated with the painter
     * @desc Used by {@link HierarchyPainter}
     * @private */
   setItemName(name, opt, hpainter) {
      this.#hitemname = isStr(name) ? name : undefined;
      // only update draw option, never delete.
      if (isStr(opt))
         this.#hdrawopt = opt;

      this.#hpainter = hpainter;
   }

   /** @summary Returns assigned histogram painter */
   getHPainter() { return this.#hpainter; }

   /** @summary Returns assigned item name
     * @desc Used with {@link HierarchyPainter} to identify drawn item name */
   getItemName() { return this.#hitemname ?? null; }

   /** @summary Returns assigned item draw option
     * @desc Used with {@link HierarchyPainter} to identify drawn item option */
   getItemDrawOpt() { return this.#hdrawopt ?? ''; }

} // class BasePainter

/** @summary Load and initialize JSDOM from nodes
  * @return {Promise} with d3 selection for d3_body
  * @private */
async function _loadJSDOM() {
   return Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(handle => {
      if (!internals.nodejs_window) {
         internals.nodejs_window = (new handle.JSDOM('<!DOCTYPE html>hello')).window;
         internals.nodejs_document = internals.nodejs_window.document; // used with three.js
         internals.nodejs_body = select(internals.nodejs_document).select('body'); // get d3 handle for body
      }

      return { JSDOM: handle.JSDOM, doc: internals.nodejs_document, body: internals.nodejs_body };
   });
}

/** @summary Return translate string for transform attribute of some svg element
  * @return string or null if x and y are zeros
  * @private */
function makeTranslate(g, x, y, scale = 1) {
   if (!isObject(g)) {
      scale = y;
      y = x;
      x = g;
      g = null;
   }
   let res = y ? `translate(${x},${y})` : (x ? `translate(${x})` : null);
   if (scale && scale !== 1) {
      if (res)
         res += ' ';
      else
         res = '';
      res += `scale(${scale.toFixed(3)})`;
   }

   return g ? g.attr('transform', res) : res;
}


/** @summary Configure special style used for highlight or dragging elements
  * @private */
function addHighlightStyle(elem, drag) {
   if (drag) {
      elem.style('stroke', 'steelblue')
          .style('fill-opacity', '0.1');
   } else {
      elem.style('stroke', '#4572A7')
          .style('fill', '#4572A7')
          .style('opacity', '0');
   }
}

/** @summary Create image based on SVG
  * @param {string} svg - svg code of the image
  * @param {string} [image_format] - image format like 'png', 'jpeg' or 'webp'
  * @param {Objects} [args] - optional arguments
  * @param {boolean} [args.as_buffer] - return image as buffer
  * @return {Promise} with produced image in base64 form or as Buffer (or canvas when no image_format specified)
  * @private */
async function svgToImage(svg, image_format, args) {
   if ((args === true) || (args === false))
      args = { as_buffer: args };

   if (image_format === 'svg')
      return svg;

   if (image_format === 'pdf')
      return internals.makePDF ? internals.makePDF(svg, args) : null;

   // required with df104.py/df105.py example with RCanvas or any special symbols in TLatex
   const doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';

   if (isNodeJs()) {
      svg = encodeURIComponent(doctype + svg);
      svg = svg.replace(/%([0-9A-F]{2})/g, (match, p1) => {
         const c = String.fromCharCode('0x' + p1);
         return c === '%' ? '%25' : c;
      });

      const img_src = 'data:image/svg+xml;base64,' + btoa_func(decodeURIComponent(svg));

      return Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(async handle => {
         return handle.default.loadImage(img_src).then(img => {
            const canvas = handle.default.createCanvas(img.width, img.height);

            canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);

            if (args?.as_buffer)
               return canvas.toBuffer('image/' + image_format);

            return image_format ? canvas.toDataURL('image/' + image_format) : canvas;
         });
      });
   }

   const img_src = URL.createObjectURL(new Blob([doctype + svg], { type: 'image/svg+xml;charset=utf-8' }));

   return new Promise(resolveFunc => {
      const image = document.createElement('img');

      image.onload = function() {
         URL.revokeObjectURL(img_src);

         const canvas = document.createElement('canvas');
         canvas.width = image.width;
         canvas.height = image.height;

         canvas.getContext('2d').drawImage(image, 0, 0);

         if (args?.as_buffer && image_format)
            canvas.toBlob(blob => blob.arrayBuffer().then(resolveFunc), 'image/' + image_format);
         else
            resolveFunc(image_format ? canvas.toDataURL('image/' + image_format) : canvas);
      };
      image.onerror = function(arg) {
         URL.revokeObjectURL(img_src);
         console.log(`IMAGE ERROR ${arg}`);
         resolveFunc(null);
      };

      image.setAttribute('src', img_src);
   });
}

/** @summary Convert ROOT TDatime object into Date
 * @desc Always use UTC to avoid any variation between timezones */
function getTDatime(dt) {
   const y = (dt.fDatime >>> 26) + 1995,
         m = ((dt.fDatime << 6) >>> 28) - 1,
         d = (dt.fDatime << 10) >>> 27,
         h = (dt.fDatime << 15) >>> 27,
         min = (dt.fDatime << 20) >>> 26,
         s = (dt.fDatime << 26) >>> 26;
   return new Date(Date.UTC(y, m, d, h, min, s));
}

/** @summary Convert Date object into string used configured time zone
 * @desc Time zone stored in settings.TimeZone */
function convertDate(dt) {
   let res = '';

   if (settings.TimeZone && isStr(settings.TimeZone)) {
      try {
         res = dt.toLocaleString('en-GB', { timeZone: settings.TimeZone });
      } catch {
         res = '';
      }
   }
   return res || dt.toLocaleString('en-GB');
}

/** @summary Box decorations
  * @private */
function getBoxDecorations(xx, yy, ww, hh, bmode, pww, phh) {
   const side1 = `M${xx},${yy}h${ww}l${-pww},${phh}h${2 * pww - ww}v${hh - 2 * phh}l${-pww},${phh}z`,
         side2 = `M${xx + ww},${yy + hh}v${-hh}l${-pww},${phh}v${hh - 2 * phh}h${2 * pww - ww}l${-pww},${phh}z`;
   return bmode > 0 ? [side1, side2] : [side2, side1];
}

const kArial = 'Arial', kTimes = 'Times New Roman', kCourier = 'Courier New', kVerdana = 'Verdana', kSymbol = 'RootSymbol', kWingdings = 'Wingdings',
   // average width taken from symbols.html, counted only for letters and digits
      root_fonts = [null,  // index 0 not exists
                    { n: kTimes, s: 'italic', aw: 0.5314 },
                    { n: kTimes, w: 'bold', aw: 0.5809 },
                    { n: kTimes, s: 'italic', w: 'bold', aw: 0.5540 },
                    { n: kArial, aw: 0.5778 },
                    { n: kArial, s: 'oblique', aw: 0.5783 },
                    { n: kArial, w: 'bold', aw: 0.6034 },
                    { n: kArial, s: 'oblique', w: 'bold', aw: 0.6030 },
                    { n: kCourier, aw: 0.6003 },
                    { n: kCourier, s: 'oblique', aw: 0.6004 },
                    { n: kCourier, w: 'bold', aw: 0.6003 },
                    { n: kCourier, s: 'oblique', w: 'bold', aw: 0.6005 },
                    { n: kSymbol, aw: 0.5521, file: 'symbol.ttf' },
                    { n: kTimes, aw: 0.5521 },
                    { n: kWingdings, aw: 0.5664, file: 'wingding.ttf' },
                    { n: kSymbol, s: 'oblique', aw: 0.5314, file: 'symbol.ttf' },
                    { n: kVerdana, aw: 0.5664 },
                    { n: kVerdana, s: 'italic', aw: 0.5495 },
                    { n: kVerdana, w: 'bold', aw: 0.5748 },
                    { n: kVerdana, s: 'italic', w: 'bold', aw: 0.5578 }],
   // list of loaded fonts including handling of multiple simultaneous requests
      gFontFiles = {};

/** @summary Read font file from some pre-configured locations
  * @return {Promise} with base64 code of the font
  * @private */
async function loadFontFile(fname) {
   let entry = gFontFiles[fname];
   if (entry?.base64)
      return entry?.base64;

   if (entry?.promises !== undefined) {
      return new Promise(resolveFunc => {
         entry.promises.push(resolveFunc);
      });
   }

   entry = gFontFiles[fname] = { promises: [] };

   const locations = [];
   if (fname.indexOf('/') >= 0)
      locations.push(''); // just use file name as is
   else {
      locations.push(exports.source_dir + 'fonts/');
      if (isNodeJs())
         locations.push('../../fonts/');
      else if (exports.source_dir.indexOf('jsrootsys/') >= 0) {
         locations.unshift(exports.source_dir.replace(/jsrootsys/g, 'rootsys_fonts'));
         locations.unshift(exports.source_dir.replace(/jsrootsys/g, 'rootsys/fonts'));
      }
   }

   function completeReading(base64) {
      entry.base64 = base64;
      const arr = entry.promises;
      delete entry.promises;
      arr.forEach(func => func(base64));
      return base64;
   }

   async function tryNext() {
      if (!locations.length) {
         completeReading(null);
         throw new Error(`Fail to load ${fname} font`);
      }
      let path = locations.shift() + fname;
      console.log('loading font', path);
      const pr = isNodeJs() ? Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(fs => {
         const prefix = 'file://' + (process?.platform === 'win32' ? '/' : '');
         if (path.indexOf(prefix) === 0)
            path = path.slice(prefix.length);
         return fs.readFileSync(path).toString('base64');
      }) : httpRequest(path, 'bin').then(buf => btoa_func(buf));

      return pr.then(res => { return res ? completeReading(res) : tryNext();}).catch(() => tryNext());
   }

   return tryNext();
}


/**
 * @summary Helper class for font handling
 * @private
 */

class FontHandler {

   /** @summary constructor */
   constructor(fontIndex, size, scale) {
      if (scale && (size < 1)) {
         size *= scale;
         this.scaled = true;
      }

      this.size = Math.round(size);
      this.scale = scale;
      this.index = 0;

      this.func = this.setFont.bind(this);

      let cfg;

      if (fontIndex && isObject(fontIndex))
         cfg = fontIndex;
      else {
         if (fontIndex && Number.isInteger(fontIndex))
            this.index = Math.floor(fontIndex / 10);
         cfg = root_fonts[this.index];
      }

      if (cfg) {
         this.cfg = cfg;
         this.setNameStyleWeight(cfg.n, cfg.s, cfg.w, cfg.aw, cfg.format, cfg.base64);
      } else
         this.setNameStyleWeight(kArial);
   }

   /** @summary Should returns true if font has to be loaded before
    * @private */
   needLoad() { return this.cfg?.file && !this.isSymbol && !this.base64; }

   /** @summary Async function to load font
    * @private */
   async load() {
      if (!this.needLoad())
         return true;
      return loadFontFile(this.cfg.file).then(base64 => {
         this.cfg.base64 = this.base64 = base64;
         this.format = 'ttf';
         return Boolean(base64);
      });
   }

   /** @summary Directly set name, style and weight for the font
    * @private */
   setNameStyleWeight(name, style, weight, aver_width, format, base64) {
      this.name = name;
      this.style = style || null;
      this.weight = weight || null;
      this.aver_width = aver_width || (weight ? 0.58 : 0.55);
      this.format = format; // format of custom font, ttf by default
      this.base64 = base64; // indication of custom font
      if (!settings.LoadSymbolTtf && ((this.name === kSymbol) || (this.name === kWingdings))) {
         this.isSymbol = this.name;
         this.name = kTimes;
      } else
         this.isSymbol = '';
   }

   /** @summary Set painter for which font will be applied */
   setPainter(painter) {
      this.painter = painter;
   }

   /** @summary Force setting of style and weight, used in latex */
   setUseFullStyle(flag) {
      this.full_style = flag;
   }

   /** @summary Assigns font-related attributes */
   addCustomFontToSvg(svg) {
      if (!this.base64 || !this.name)
         return;
      const clname = 'custom_font_' + this.name, fmt = 'ttf';
      let defs = svg.selectChild('.canvas_defs');
      if (defs.empty())
         defs = svg.insert('svg:defs', ':first-child').attr('class', 'canvas_defs');
      const entry = defs.selectChild('.' + clname);
      if (entry.empty()) {
         defs.append('style')
               .attr('class', clname)
               .property('$fontcfg', this.cfg || null)
               .text(`@font-face { font-family: "${this.name}"; font-weight: normal; font-style: normal; src: url(data:application/font-${fmt};charset=utf-8;base64,${this.base64}); }`);
      }
   }

   /** @summary Assigns font-related attributes */
   setFont(selection) {
      if (this.base64 && this.painter)
         this.addCustomFontToSvg(this.painter.getCanvSvg());

      selection.attr('font-family', this.name)
               .attr('font-size', this.size)
               .attr(':xml:space', 'preserve');
      this.setFontStyle(selection);
   }

   /** @summary Assigns only font style attributes */
   setFontStyle(selection) {
      selection.attr('font-weight', this.weight || (this.full_style ? 'normal' : null))
               .attr('font-style', this.style || (this.full_style ? 'normal' : null));
   }

   /** @summary Set font size (optional) */
   setSize(size) { this.size = Math.round(size); }

   /** @summary Set text color (optional) */
   setColor(color) { this.color = color; }

   /** @summary Set text align (optional) */
   setAlign(align) { this.align = align; }

   /** @summary Set text angle (optional) */
   setAngle(angle) { this.angle = angle; }

   /** @summary Align angle to step raster, add optional offset */
   roundAngle(step, offset) {
      this.angle = parseInt(this.angle || 0);
      if (!Number.isInteger(this.angle))
         this.angle = 0;
      this.angle = Math.round(this.angle / step) * step + (offset || 0);
      if (this.angle < 0)
         this.angle += 360;
      else if (this.angle >= 360)
         this.angle -= 360;
   }

   /** @summary Clears all font-related attributes */
   clearFont(selection) {
      selection.attr('font-family', null)
               .attr('font-size', null)
               .attr(':xml:space', null)
               .attr('font-weight', null)
               .attr('font-style', null);
   }

   /** @summary Returns true in case of monospace font
     * @private */
   isMonospace() {
      const n = this.name.toLowerCase();
      return (n.indexOf('courier') === 0) || (n === 'monospace') || (n === 'monaco');
   }

   /** @summary Return full font declaration which can be set as font property like '12pt Arial bold'
     * @private */
   getFontHtml() {
      let res = Math.round(this.size) + 'pt ' + this.name;
      if (this.weight)
         res += ' ' + this.weight;
      if (this.style)
         res += ' ' + this.style;
      return res;
   }

   /** @summary Returns font name */
   getFontName() {
      return this.isSymbol || this.name || 'none';
   }

} // class FontHandler

/** @summary Register custom font
  * @private */
function addCustomFont(index, name, format, base64) {
   if (!Number.isInteger(index))
      console.error(`Wrong index ${index} for custom font`);
   else
      root_fonts[index] = { n: name, format, base64 };
}

/** @summary Try to detect and create font handler for SVG text node
  * @private */
function detectPdfFont(node) {
   const sz = node.getAttribute('font-size'),
         p = sz.indexOf('px'),
         sz_pixels = p > 0 ? Number.parseInt(sz.slice(0, p)) : 12;

   let family = node.getAttribute('font-family'),
       style = node.getAttribute('font-style'),
       weight = node.getAttribute('font-weight');

   if (family === 'times')
      family = kTimes;
   else if (family === 'symbol')
      family = kSymbol;
   else if (family === 'arial')
      family = kArial;
   else if (family === 'verdana')
      family = kVerdana;
   if (weight === 'normal')
      weight = '';
   if (style === 'normal')
      style = '';

   const fcfg = root_fonts.find(elem => {
      return (elem?.n === family) &&
             ((!weight && !elem.w) || (elem.w === weight)) &&
             ((!style && !elem.s) || (elem.s === style));
   });

   return new FontHandler(fcfg || root_fonts[13], sz_pixels);
}

/* eslint-disable one-var */

const symbols_map = {
   // greek letters from symbols.ttf
   '#alpha': '\u03B1',
   '#beta': '\u03B2',
   '#chi': '\u03C7',
   '#delta': '\u03B4',
   '#varepsilon': '\u03B5',
   '#phi': '\u03C6',
   '#gamma': '\u03B3',
   '#eta': '\u03B7',
   '#iota': '\u03B9',
   '#varphi': '\u03C6',
   '#kappa': '\u03BA',
   '#lambda': '\u03BB',
   '#mu': '\u03BC',
   '#nu': '\u03BD',
   '#omicron': '\u03BF',
   '#pi': '\u03C0',
   '#theta': '\u03B8',
   '#rho': '\u03C1',
   '#sigma': '\u03C3',
   '#tau': '\u03C4',
   '#upsilon': '\u03C5',
   '#varomega': '\u03D6',
   '#omega': '\u03C9',
   '#xi': '\u03BE',
   '#psi': '\u03C8',
   '#zeta': '\u03B6',
   '#Alpha': '\u0391',
   '#Beta': '\u0392',
   '#Chi': '\u03A7',
   '#Delta': '\u0394',
   '#Epsilon': '\u0395',
   '#Phi': '\u03A6',
   '#Gamma': '\u0393',
   '#Eta': '\u0397',
   '#Iota': '\u0399',
   '#vartheta': '\u03D1',
   '#Kappa': '\u039A',
   '#Lambda': '\u039B',
   '#Mu': '\u039C',
   '#Nu': '\u039D',
   '#Omicron': '\u039F',
   '#Pi': '\u03A0',
   '#Theta': '\u0398',
   '#Rho': '\u03A1',
   '#Sigma': '\u03A3',
   '#Tau': '\u03A4',
   '#Upsilon': '\u03A5',
   '#varsigma': '\u03C2',
   '#Omega': '\u03A9',
   '#Xi': '\u039E',
   '#Psi': '\u03A8',
   '#Zeta': '\u0396',
   '#varUpsilon': '\u03D2',
   '#epsilon': '\u03B5',

   // second set from symbols.ttf
   '#leq': '\u2264',
   '#/': '\u2044',
   '#infty': '\u221E',
   '#voidb': '\u0192',
   '#club': '\u2663',
   '#diamond': '\u2666',
   '#heart': '\u2665',
   '#spade': '\u2660',
   '#leftrightarrow': '\u2194',
   '#leftarrow': '\u2190',
   '#uparrow': '\u2191',
   '#rightarrow': '\u2192',
   '#downarrow': '\u2193',
   '#circ': '\u2E30',
   '#pm': '\xB1',
   '#doublequote': '\u2033',
   '#geq': '\u2265',
   '#times': '\xD7',
   '#propto': '\u221D',
   '#partial': '\u2202',
   '#bullet': '\u2022',
   '#divide': '\xF7',
   '#neq': '\u2260',
   '#equiv': '\u2261',
   '#approx': '\u2248', // should be \u2245 ?
   '#3dots': '\u2026',
   '#cbar': '\x7C',
   '#topbar': '\xAF',
   '#downleftarrow': '\u21B5',
   '#aleph': '\u2135',
   '#Jgothic': '\u2111',
   '#Rgothic': '\u211C',
   '#voidn': '\u2118',
   '#otimes': '\u2297',
   '#oplus': '\u2295',
   '#oslash': '\u2205',
   '#cap': '\u2229',
   '#cup': '\u222A',
   '#supset': '\u2283',
   '#supseteq': '\u2287',
   '#notsubset': '\u2284',
   '#subset': '\u2282',
   '#subseteq': '\u2286',
   '#in': '\u2208',
   '#notin': '\u2209',
   '#angle': '\u2220',
   '#nabla': '\u2207',
   '#oright': '\xAE',
   '#ocopyright': '\xA9',
   '#trademark': '\u2122',
   '#prod': '\u220F',
   '#surd': '\u221A',
   '#upoint': '\u2027',
   '#corner': '\xAC',
   '#wedge': '\u2227',
   '#vee': '\u2228',
   '#Leftrightarrow': '\u21D4',
   '#Leftarrow': '\u21D0',
   '#Uparrow': '\u21D1',
   '#Rightarrow': '\u21D2',
   '#Downarrow': '\u21D3',
   '#void2': '', // dummy, placeholder
   '#LT': '\x3C',
   '#void1': '\xAE',
   '#copyright': '\xA9',
   '#void3': '\u2122',  // it is dummy placeholder, TM
   '#sum': '\u2211',
   '#arctop': '\u239B',
   '#lbar': '\u23A2',
   '#arcbottom': '\u239D',
   '#void4': '', // dummy, placeholder
   '#void8': '\u23A2', // same as lbar
   '#bottombar': '\u230A',
   '#arcbar': '\u23A7',
   '#ltbar': '\u23A8',
   '#AA': '\u212B',
   '#aa': '\xE5',
   '#void06': '',
   '#GT': '\x3E',
   '#int': '\u222B',
   '#forall': '\u2200',
   '#exists': '\u2203',
   // here ends second set from symbols.ttf

   // more greek symbols
   '#koppa': '\u03DF',
   '#sampi': '\u03E1',
   '#stigma': '\u03DB',
   '#san': '\u03FB',
   '#sho': '\u03F8',
   '#varcoppa': '\u03D9',
   '#digamma': '\u03DD',
   '#Digamma': '\u03DC',
   '#Koppa': '\u03DE',
   '#varKoppa': '\u03D8',
   '#Sampi': '\u03E0',
   '#Stigma': '\u03DA',
   '#San': '\u03FA',
   '#Sho': '\u03F7',

   '#vec': '',
   '#dot': '\u22C5',
   '#hat': '\xB7',
   '#ddot': '',
   '#acute': '',
   '#grave': '',
   '#check': '\u2713',
   '#tilde': '\u02DC',
   '#slash': '\u2044',
   '#hbar': '\u0127',
   '#box': '\u25FD',
   '#Box': '\u2610',
   '#parallel': '\u2225',
   '#perp': '\u22A5',
   '#odot': '\u2299',
   '#left': '',
   '#right': '',
   '{}': '',

   '#mp': '\u2213',

   '#P': '\u00B6', // paragraph

   // only required for MathJax to provide correct replacement
   '#sqrt': '\u221A',
   '#bar': '',
   '#overline': '',
   '#underline': '',
   '#strike': ''
};


/** @summary Create a single regex to detect any symbol to replace, apply longer symbols first
  * @private */
const symbolsRegexCache = new RegExp(Object.keys(symbols_map).sort((a, b) => (a.length < b.length ? 1 : (a.length > b.length ? -1 : 0))).join('|'), 'g');

/** @summary Simple replacement of latex letters
  * @private */
function translateLaTeX(str) {
   while ((str.length > 2) && (str.at(0) === '{') && (str.at(-1) === '}'))
      str = str.slice(1, str.length - 1);

   return str.replace(symbolsRegexCache, ch => symbols_map[ch]).replace(/\{\}/g, '');
}

// array with relative width of base symbols from range 32..126
// eslint-disable-next-line
const base_symbols_width = [453,535,661,973,955,1448,1242,324,593,596,778,1011,200,570,200,492,947,885,947,947,947,947,947,947,947,947,511,495,980,1010,987,893,1624,1185,1147,1193,1216,1080,1028,1270,1274,531,910,1177,1004,1521,1252,1276,1111,1276,1164,1056,1073,1215,1159,1596,1150,1124,1065,540,591,540,837,874,572,929,972,879,973,901,569,967,973,453,458,903,453,1477,973,970,972,976,638,846,548,973,870,1285,884,864,835,656,430,656,1069];

// eslint-disable-next-line
const extra_symbols_width = {945:1002,946:996,967:917,948:953,949:834,966:1149,947:847,951:989,953:516,954:951,955:913,956:1003,957:862,959:967,960:1070,952:954,961:973,963:1017,964:797,965:944,982:1354,969:1359,958:803,968:1232,950:825,913:1194,914:1153,935:1162,916:1178,917:1086,934:1358,915:1016,919:1275,921:539,977:995,922:1189,923:1170,924:1523,925:1253,927:1281,928:1281,920:1285,929:1102,931:1041,932:1069,933:1135,962:848,937:1279,926:1092,936:1334,918:1067,978:1154,8730:986,8804:940,8260:476,8734:1453,402:811,9827:1170,9830:931,9829:1067,9824:965,8596:1768,8592:1761,8593:895,8594:1761,8595:895,710:695,177:955,8243:680,8805:947,215:995,8733:1124,8706:916,8226:626,247:977,8800:969,8801:1031,8776:976,8230:1552,175:883,8629:1454,8501:1095,8465:1002,8476:1490,8472:1493,8855:1417,8853:1417,8709:1205,8745:1276,8746:1404,8839:1426,8835:1426,8836:1426,8838:1426,8834:1426,8747:480,8712:1426,8713:1426,8736:1608,8711:1551,174:1339,169:1339,8482:1469,8719:1364,729:522,172:1033,8743:1383,8744:1383,8660:1768,8656:1496,8657:1447,8658:1496,8659:1447,8721:1182,9115:882,9144:1000,9117:882,8970:749,9127:1322,9128:1322,8491:1150,229:929,8704:1397,8707:1170,8901:524,183:519,10003:1477,732:692,295:984,9725:1780,9744:1581,8741:737,8869:1390,8857:1421};

/** @summary Calculate approximate labels width
  * @private */
function approximateLabelWidth(label, font, fsize) {
   if (Number.isInteger(font))
      font = new FontHandler(font, fsize);

   const len = label.length,
         symbol_width = (fsize || font.size) * font.aver_width;
   if (font.isMonospace())
      return len * symbol_width;

   let sum = 0;
   for (let i = 0; i < len; ++i) {
      const code = label.charCodeAt(i);
      if ((code >= 32) && (code < 127))
         sum += base_symbols_width[code - 32];
      else
         sum += extra_symbols_width[code] || 1000;
   }

   return sum / 1000 * symbol_width;
}

/** @summary array defines features supported by latex parser, used by both old and new parsers
  * @private */
const latex_features = [
   { name: '#it{', bi: 'italic' }, // italic
   { name: '#bf{', bi: 'bold' }, // bold
   { name: '#underline{', deco: 'underline' }, // underline
   { name: '#overline{', deco: 'overline' }, // overline
   { name: '#strike{', deco: 'line-through' }, // line through
   { name: '#kern[', arg: 'float', shift: 'x' }, // horizontal shift
   { name: '#lower[', arg: 'float', shift: 'y' },  // vertical shift
   { name: '#scale[', arg: 'float' },  // font scale
   { name: '#color[', arg: 'int' },   // font color
   { name: '#font[', arg: 'int' },    // font face
   { name: '#url[', arg: 'string' },   // url link
   { name: '_{', low_up: 'low' },  // subscript
   { name: '^{', low_up: 'up' },   // superscript
   { name: '#bar{', deco: 'overline' /* accent: '\u02C9' */ }, // '\u0305'
   { name: '#hat{', accent: '\u02C6', hasw: true }, // '\u0302'
   { name: '#check{', accent: '\u02C7', hasw: true }, // '\u030C'
   { name: '#acute{', accent: '\u02CA' }, // '\u0301'
   { name: '#grave{', accent: '\u02CB' }, // '\u0300'
   { name: '#dot{', accent: '\u02D9' }, // '\u0307'
   { name: '#ddot{', accent: '\u02BA', hasw: true }, // '\u0308'
   { name: '#tilde{', accent: '\u02DC', hasw: true }, // '\u0303'
   { name: '#slash{', accent: '\u2215' }, // '\u0337'
   { name: '#vec{', accent: '\u02ED', hasw: true }, // '\u0350' arrowhead
   { name: '#frac{', twolines: 'line', middle: true },
   { name: '#splitmline{', twolines: true, middle: true },
   { name: '#splitline{', twolines: true },
   { name: '#sqrt[', arg: 'int', sqrt: true }, // root with arbitrary power
   { name: '#sqrt{', sqrt: true },             // square root
   { name: '#sum', special: '\u2211', w: 0.8, h: 0.9 },
   { name: '#int', special: '\u222B', w: 0.3, h: 1.0 },
   { name: '#left[', right: '#right]', braces: '[]' },
   { name: '#left(', right: '#right)', braces: '()' },
   { name: '#left{', right: '#right}', braces: '{}' },
   { name: '#left|', right: '#right|', braces: '||' },
   { name: '#[]{', braces: '[]' },
   { name: '#(){', braces: '()' },
   { name: '#{}{', braces: '{}' },
   { name: '#||{', braces: '||' }
];

// taken from: https://sites.math.washington.edu/~marshall/cxseminar/symbol.htm, starts from 33
// eslint-disable-next-line
const symbolsMap = [0,8704,0,8707,0,0,8717,0,0,8727,0,0,8722,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8773,913,914,935,916,917,934,915,919,921,977,922,923,924,925,927,928,920,929,931,932,933,962,937,926,936,918,0,8756,0,8869,0,0,945,946,967,948,949,966,947,951,953,981,954,955,956,957,959,960,952,961,963,964,965,982,969,958,968,950,0,402,0,8764,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,978,8242,8804,8260,8734,0,9827,9830,9829,9824,8596,8592,8593,8594,8595,0,0,8243,8805,0,8733,8706,8729,0,8800,8801,8776,8230,0,0,8629,8501,8465,8476,8472,8855,8853,8709,8745,8746,8835,8839,8836,8834,8838,8712,8713,8736,8711,0,0,8482,8719,8730,8901,0,8743,8744,8660,8656,8657,8658,8659,9674,9001,0,0,8482,8721,0,0,0,0,0,0,0,0,0,0,8364,9002,8747,8992,0,8993];

// taken from http://www.alanwood.net/demos/wingdings.html, starts from 33
// eslint-disable-next-line
const wingdingsMap = [128393,9986,9985,128083,128365,128366,128367,128383,9990,128386,128387,128234,128235,128236,128237,128193,128194,128196,128463,128464,128452,8987,128430,128432,128434,128435,128436,128427,128428,9991,9997,128398,9996,128076,128077,128078,9756,9758,9757,9759,128400,9786,128528,9785,128163,9760,127987,127985,9992,9788,128167,10052,128326,10014,128328,10016,10017,9770,9775,2384,9784,9800,9801,9802,9803,9804,9805,9806,9807,9808,9809,9810,9811,128624,128629,9679,128318,9632,9633,128912,10065,10066,11047,10731,9670,10070,11045,8999,11193,8984,127989,127990,128630,128631,0,9450,9312,9313,9314,9315,9316,9317,9318,9319,9320,9321,9471,10102,10103,10104,10105,10106,10107,10108,10109,10110,10111,128610,128608,128609,128611,128606,128604,128605,128607,183,8226,9642,9898,128902,128904,9673,9678,128319,9642,9723,128962,10022,9733,10038,10036,10041,10037,11216,8982,10209,8977,11217,10026,10032,128336,128337,128338,128339,128340,128341,128342,128343,128344,128345,128346,128347,11184,11185,11186,11187,11188,11189,11190,11191,128618,128619,128597,128596,128599,128598,128592,128593,128594,128595,9003,8998,11160,11162,11161,11163,11144,11146,11145,11147,129128,129130,129129,129131,129132,129133,129135,129134,129144,129146,129145,129147,129148,129149,129151,129150,8678,8680,8679,8681,11012,8691,11008,11009,11011,11010,129196,129197,128502,10004,128503,128505];

const symbolsPdfMap = {};

/** @summary Return code for symbols from symbols.ttf
 * @desc Used in PDF generation
 * @private */
function remapSymbolTtfCode(code) {
   if (!symbolsPdfMap[0x3B1]) {
      let cnt = 0;
      for (const key in symbols_map) {
         const symbol = symbols_map[key];
         if (symbol.length === 1) {
            let letter;
            if (cnt < 54) {
               const opGreek = cnt;
               // see code in TLatex.cxx, line 1302
               letter = 97 + opGreek;
               if (opGreek > 25)
                  letter -= 58;
               if (opGreek === 52)
                  letter = 0o241; // varUpsilon
               if (opGreek === 53)
                  letter = 0o316; // epsilon
            } else {
               // see code in TLatex.cxx, line 1323
               const opSpec = cnt - 54;
               letter = 0o243 + opSpec;
               switch (opSpec) {
                  case 75: letter = 0o305; break; // AA Angstroem
                  case 76: letter = 0o345; break; // aa Angstroem
                  case 80: letter = 0o42; break; // #forall
                  case 81: letter = 0o44; break; // #exists
               }
            }
            const scode = symbol.charCodeAt(0);
            if (scode > 0x80)
               symbolsPdfMap[scode] = letter;
         }
         if (++cnt > 54 + 82)
            break;
      }
      for (let k = 0; k < symbolsMap.length; ++k) {
         const scode2 = symbolsMap[k];
         if (scode2)
            symbolsPdfMap[scode2] = k + 33;
      }
   }
   return symbolsPdfMap[code] ?? code;
}


/** @summary Reformat text node if it includes greek or special symbols
 * @desc Used in PDF generation where greek symbols are not available
 * @private */
function replaceSymbolsInTextNode(node) {
   if (node.$text && node.$font) {
      node.$originalHTML = node.innerHTML;
      node.$originalFont = node.getAttribute('font-family');

      node.innerHTML = node.$text;
      if (settings.LoadSymbolTtf)
         node.setAttribute('font-family', node.$font.isSymbol);
      else
         node.setAttribute('font-family', (node.$font.isSymbol === kWingdings) ? 'zapfdingbats' : 'symbol');
      return node.$font.isSymbol;
   }

   if (node.childNodes.length !== 1)
      return false;
   const txt = node.textContent;
   if (!txt)
      return false;
   let new_html = '', lasti = -1;
   for (let i = 0; i < txt.length; i++) {
      const code = txt.charCodeAt(i),
            newcode = remapSymbolTtfCode(code);
      if (code !== newcode) {
         new_html += txt.slice(lasti + 1, i) + `<tspan font-family="${settings.LoadSymbolTtf ? kSymbol : 'symbol'}" font-style="normal" font-weight="normal">${String.fromCharCode(newcode)} </tspan>`;
         lasti = i;
      }
   }

   if (lasti < 0)
      return false;

   if (lasti < txt.length - 1)
      new_html += txt.slice(lasti + 1, txt.length);

   node.$originalHTML = node.innerHTML;
   node.$originalFont = node.getAttribute('font-family');
   node.innerHTML = new_html;
   return kSymbol;
}


/** @summary Replace codes from symbols.ttf into normal font - when symbols.ttf cannot be used
  * @private */
function replaceSymbols(s, name) {
   const m = name === kWingdings ? wingdingsMap : symbolsMap;
   let res = '';
   for (let k = 0; k < s.length; ++k) {
      const code = s.charCodeAt(k),
            new_code = (code > 32) ? m[code - 33] : 0;
      res += String.fromCodePoint(new_code || code);
   }
   return res;
}

/** @summary Just add plain text to the SVG text elements
  * @private */
function producePlainText(painter, txt_node, arg) {
   arg.plain = true;
   if (arg.simple_latex)
      arg.text = translateLaTeX(arg.text); // replace latex symbols
   if (arg.font?.isSymbol) {
      txt_node.text(replaceSymbols(arg.text, arg.font.isSymbol));
      txt_node.property('$text', arg.text);
      txt_node.property('$font', arg.font);
   } else
      txt_node.text(arg.text);
}

/** @summary Check if plain text
  * @private */
function isPlainText(txt) {
   return !txt || ((txt.indexOf('#') < 0) && (txt.indexOf('{') < 0));
}

/** @summary translate TLatex and draw inside provided g element
  * @desc use <text> together with normal <path> elements
  * @private */
function parseLatex(node, arg, label, curr) {
   let nelements = 0;

   const currG = () => {
      if (!curr.g)
         curr.g = node.append('svg:g');
      return curr.g;
   };

   const shiftX = dx => { curr.x += Math.round(dx); };

   const extendPosition = (x1, y1, x2, y2) => {
      if (!curr.rect)
         curr.rect = { x1, y1, x2, y2 };
      else {
         curr.rect.x1 = Math.min(curr.rect.x1, x1);
         curr.rect.y1 = Math.min(curr.rect.y1, y1);
         curr.rect.x2 = Math.max(curr.rect.x2, x2);
         curr.rect.y2 = Math.max(curr.rect.y2, y2);
      }

      curr.rect.last_y1 = y1; // upper position of last symbols

      curr.rect.width = curr.rect.x2 - curr.rect.x1;
      curr.rect.height = curr.rect.y2 - curr.rect.y1;

      if (!curr.parent)
         arg.text_rect = curr.rect;
   };

   const addSpaces = nspaces => {
      extendPosition(curr.x, curr.y, curr.x + nspaces * curr.fsize * 0.4, curr.y);
      shiftX(nspaces * curr.fsize * 0.4);
   };

   /** Position pos.g node which directly attached to curr.g and uses curr.g coordinates */
   const positionGNode = (pos, x, y, inside_gg) => {
      x = Math.round(x);
      y = Math.round(y);

      makeTranslate(pos.g, x, y);
      pos.rect.x1 += x;
      pos.rect.x2 += x;
      pos.rect.y1 += y;
      pos.rect.y2 += y;

      if (inside_gg)
         extendPosition(curr.x + pos.rect.x1, curr.y + pos.rect.y1, curr.x + pos.rect.x2, curr.y + pos.rect.y2);
      else
         extendPosition(pos.rect.x1, pos.rect.y1, pos.rect.x2, pos.rect.y2);
   };

   /** Create special sub-container for elements like sqrt or braces  */
   const createGG = is_a => {
      const gg = currG();

      // this is indicator that gg element will be the only one, one can use directly main container
      if ((nelements === 1) && !label && !curr.x && !curr.y && !is_a)
         return gg;

      return makeTranslate(gg.append(is_a ? 'svg:a' : 'svg:g'), curr.x, curr.y);
   };

   const extractSubLabel = (check_first, lbrace, rbrace) => {
      let pos = 0, n = 1, extra_braces = false;
      if (!lbrace)
         lbrace = '{';
      if (!rbrace)
         rbrace = '}';

      const match = br => (pos + br.length <= label.length) && (label.slice(pos, pos + br.length) === br);

      if (check_first) {
         if (!match(lbrace)) {
            console.log(`not starting with ${lbrace} in ${label}`);
            return -1;
         }
         label = label.slice(lbrace.length);
      }

      while (n && (pos < label.length)) {
         if (match(lbrace)) {
            n++;
            pos += lbrace.length;
         } else if (match(rbrace)) {
            n--;
            pos += rbrace.length;
            if ((n === 0) && (typeof check_first === 'string') && match(check_first + lbrace)) {
               // handle special case like a^{b}^{2} should mean a^{b^{2}}
               n++;
               pos += lbrace.length + check_first.length;
               check_first = true;
               extra_braces = true;
            }
         } else pos++;
      }
      if (n) {
         console.log(`mismatch with open ${lbrace} and closing ${rbrace} in ${label}`);
         return -1;
      }

      let sublabel = label.slice(0, pos - rbrace.length);

      if (extra_braces)
         sublabel = lbrace + sublabel + rbrace;

      label = label.slice(pos);

      return sublabel;
   };

   const createPath = (gg, d, dofill) => {
      return gg.append('svg:path')
               .attr('d', d || 'M0,0') // provide dummy d value as placeholder, preserve order of attributes
               .style('stroke', dofill ? 'none' : (curr.color || arg.color))
               .style('stroke-width', dofill ? null : Math.max(1, Math.round(curr.fsize * (curr.font.weight ? 0.1 : 0.07))))
               .style('fill', dofill ? (curr.color || arg.color) : 'none');
   };

   const createSubPos = fscale => {
      return {
         lvl: curr.lvl + 1, x: 0, y: 0, fsize: curr.fsize * (fscale || 1),
         color: curr.color, font: curr.font, parent: curr,
         painter: curr.painter, italic: curr.italic, bold: curr.bold
      };
   };

   while (label) {
      let best = label.length, found = null;

      for (let n = 0; n < latex_features.length; ++n) {
         const pos = label.indexOf(latex_features[n].name);
         if ((pos >= 0) && (pos < best)) {
            best = pos;
            found = latex_features[n];
         }
      }

      if (best > 0) {
         const alone = (best === label.length) && (nelements === 0) && !found;

         nelements++;

         let s = translateLaTeX(label.slice(0, best)),
             nbeginspaces = 0, nendspaces = 0;

         while ((nbeginspaces < s.length) && (s[nbeginspaces] === ' '))
            nbeginspaces++;

         if (nbeginspaces > 0) {
            addSpaces(nbeginspaces);
            s = s.slice(nbeginspaces);
         }

         while ((nendspaces < s.length) && (s[s.length - 1 - nendspaces] === ' '))
            nendspaces++;

         if (nendspaces > 0)
            s = s.slice(0, s.length - nendspaces);

         if (s || alone) {
            // if single text element created, place it directly in the node
            const g = curr.g || (alone ? node : currG()),
                  elem = g.append('svg:text');

            if (alone && !curr.g)
               curr.g = elem;

            // apply font attributes only once, inherited by all other elements
            if (curr.ufont) {
               curr.font.setPainter(arg.painter);
               curr.font.setFont(curr.g);
            }

            if (curr.bold !== undefined)
               curr.g.attr('font-weight', curr.bold ? 'bold' : 'normal');

            if (curr.italic !== undefined)
               curr.g.attr('font-style', curr.italic ? 'italic' : 'normal');

            // set fill color directly to element
            elem.attr('fill', curr.color || arg.color || null);

            // set font size directly to element to avoid complex control
            elem.attr('font-size', Math.max(1, Math.round(curr.fsize)));

            if (curr.font?.isSymbol) {
               elem.text(replaceSymbols(s, curr.font.isSymbol));
               elem.property('$text', s);
               elem.property('$font', curr.font);
            } else
               elem.text(s);

            const rect = !isNodeJs() && !settings.ApproxTextSize && !arg.fast
                          ? getElementRect(elem, 'nopadding')
                          : { height: curr.fsize * 1.2, width: approximateLabelWidth(s, curr.font, curr.fsize) };

            if (curr.x)
               elem.attr('x', curr.x);
            if (curr.y)
               elem.attr('y', curr.y);

            // for single symbols like f,l,i,j one gets wrong estimation of total width, use it in sup/sub-scripts
            const xgap = (s.length === 1) && !curr.font.isMonospace() && ('lfij'.indexOf(s) >= 0) ? 0.1 * curr.fsize : 0;

            extendPosition(curr.x, curr.y - rect.height * 0.8, curr.x + rect.width, curr.y + rect.height * 0.2);

            if (!alone) {
               shiftX(rect.width + xgap);
               addSpaces(nendspaces);
               curr.xgap = 0;
            } else if (curr.deco) {
               elem.attr('text-decoration', curr.deco);
               curr.deco = ''; // inform that decoration was applied
            } else
               curr.xgap = xgap; // may be used in accent or somewhere else
         } else
            addSpaces(nendspaces);
      }

      if (!found)
         return true;

      // remove preceding block and tag itself
      label = label.slice(best + found.name.length);

      nelements++;

      if (found.accent) {
         const sublabel = extractSubLabel();
         if (sublabel === -1)
            return false;

         const gg = createGG(),
               subpos = createSubPos(),
               reduce = (sublabel.length !== 1) ? 1 : (((sublabel >= 'a') && (sublabel <= 'z') && ('tdbfhkli'.indexOf(sublabel) < 0)) ? 0.75 : 0.9);

         parseLatex(gg, arg, sublabel, subpos);

         const minw = curr.fsize * 0.6,
               y1 = Math.round(subpos.rect.y1 * reduce),
               dy2 = Math.round(curr.fsize * 0.1), dy = dy2 * 2,
               dot = `a${dy2},${dy2},0,0,1,${dy},0a${dy2},${dy2},0,0,1,${-dy},0z`;
         let xpos = 0, w = subpos.rect.width;

         // shift symbol when it is too small
         if (found.hasw && (w < minw)) {
            w = minw;
            xpos = (minw - subpos.rect.width) / 2;
         }

         const w5 = Math.round(w * 0.5), w3 = Math.round(w * 0.3), w2 = w5 - w3, w8 = w5 + w3;
         w = w5 * 2;

         positionGNode(subpos, xpos, 0, true);

         switch (found.name) {
            case '#check{': createPath(gg, `M${w2},${y1 - dy}L${w5},${y1}L${w8},${y1 - dy}`); break;
            case '#acute{': createPath(gg, `M${w5},${y1}l${dy},${-dy}`); break;
            case '#grave{': createPath(gg, `M${w5},${y1}l${-dy},${-dy}`); break;
            case '#dot{': createPath(gg, `M${w5 - dy2},${y1}${dot}`, true); break;
            case '#ddot{': createPath(gg, `M${w5 - 3 * dy2},${y1}${dot} M${w5 + dy2},${y1}${dot}`, true); break;
            case '#tilde{': createPath(gg, `M${w2},${y1} a${w3},${dy},0,0,1,${w3},0 a${w3},${dy},0,0,0,${w3},0`); break;
            case '#slash{': createPath(gg, `M${w},${y1}L0,${Math.round(subpos.rect.y2)}`); break;
            case '#vec{': createPath(gg, `M${w2},${y1}H${w8}M${w8 - dy},${y1 - dy}l${dy},${dy}l${-dy},${dy}`); break;
            default: createPath(gg, `M${w2},${y1}L${w5},${y1 - dy}L${w8},${y1}`); // #hat{
         }

         shiftX(subpos.rect.width + (subpos.xgap ?? 0));

         continue;
      }

      if (found.twolines) {
         curr.twolines = true;

         const line1 = extractSubLabel(),
               line2 = extractSubLabel(true);
         if ((line1 === -1) || (line2 === -1))
            return false;

         const gg = createGG(),
               fscale = curr.parent?.twolines ? 0.7 : 1,
               subpos1 = createSubPos(fscale);

         parseLatex(gg, arg, line1, subpos1);

         const path = found.twolines === 'line' ? createPath(gg) : null,
               subpos2 = createSubPos(fscale);

         parseLatex(gg, arg, line2, subpos2);

         const w = Math.max(subpos1.rect.width, subpos2.rect.width),
               dw = subpos1.rect.width - subpos2.rect.width,
               dy = -curr.fsize * 0.35; // approximate position of middle line

         positionGNode(subpos1, found.middle && (dw < 0) ? -dw / 2 : 0, dy - subpos1.rect.y2, true);

         positionGNode(subpos2, found.middle && (dw > 0) ? dw / 2 : 0, dy - subpos2.rect.y1, true);

         path?.attr('d', `M0,${Math.round(dy)}h${Math.round(w - curr.fsize * 0.1)}`);

         shiftX(w);

         delete curr.twolines;

         continue;
      }

      const extractLowUp = name => {
         const res = {};

         if (name) {
            label = '{' + label;
            res[name] = extractSubLabel(name === 'low' ? '_' : '^');
            if (res[name] === -1)
               return false;
         }

         while (label) {
            if ((label[0] === '_') && !res.low) {
               label = label.slice(1);
               res.low = extractSubLabel('_');
               if (res.low === -1) {
                  console.log(`error with ${found.name} low limit ${label}`);
                  return false;
               }
            } else if ((label[0] === '^') && !res.up) {
               label = label.slice(1);
               res.up = extractSubLabel('^');
               if (res.up === -1) {
                  console.log(`error with ${found.name} upper limit ${label}`);
                  return false;
               }
            } else break;
         }
         return res;
      };

      if (found.low_up) {
         const subs = extractLowUp(found.low_up);
         if (!subs)
            return false;

         const x = curr.x, dx = 0.03 * curr.fsize, ylow = 0.25 * curr.fsize;
         let pos_up, pos_low, w1 = 0, w2 = 0, yup = -curr.fsize;

         if (subs.up) {
            pos_up = createSubPos(0.6);
            parseLatex(currG(), arg, subs.up, pos_up);
         }

         if (subs.low) {
            pos_low = createSubPos(0.6);
            parseLatex(currG(), arg, subs.low, pos_low);
         }

         if (pos_up) {
            if (!pos_low && curr.rect)
               yup = Math.min(yup, curr.rect.last_y1);
            positionGNode(pos_up, x + dx, yup - pos_up.rect.y1 - curr.fsize * 0.1);
            w1 = pos_up.rect.width;
         }

         if (pos_low) {
            positionGNode(pos_low, x + dx, ylow - pos_low.rect.y2 + curr.fsize * 0.1);
            w2 = pos_low.rect.width;
         }

         shiftX(dx + Math.max(w1, w2));

         continue;
      }

      if (found.special) {
         // this is sum and integral, now make fix height, later can adjust to right-content size

         const subs = extractLowUp() || {},
               gg = createGG(), path = createPath(gg),
               h = Math.round(curr.fsize * 1.7), w = Math.round(curr.fsize), r = Math.round(h * 0.1);
         let x_up, x_low;

         if (found.name === '#sum') {
            x_up = x_low = w / 2;
            path.attr('d', `M${w},${Math.round(-0.75 * h)}h${-w}l${Math.round(0.4 * w)},${Math.round(0.3 * h)}l${Math.round(-0.4 * w)},${Math.round(0.7 * h)}h${w}`);
         } else {
            x_up = 3 * r;
            x_low = r;
            path.attr('d', `M0,${Math.round(0.25 * h - r)}a${r},${r},0,0,0,${2 * r},0v${2 * r - h}a${r},${r},0,1,1,${2 * r},0`);
            // path.attr('transform','skewX(-3)'); could use skewX for italic-like style
         }

         extendPosition(curr.x, curr.y - 0.6 * h, curr.x + w, curr.y + 0.4 * h);

         if (subs.low) {
            const subpos1 = createSubPos(0.6);
            parseLatex(gg, arg, subs.low, subpos1);
            positionGNode(subpos1, (x_low - subpos1.rect.width / 2), 0.25 * h - subpos1.rect.y1, true);
         }

         if (subs.up) {
            const subpos2 = createSubPos(0.6);
            parseLatex(gg, arg, subs.up, subpos2);
            positionGNode(subpos2, (x_up - subpos2.rect.width / 2), -0.75 * h - subpos2.rect.y2, true);
         }

         shiftX(w);

         continue;
      }

      if (found.braces) {
         const rbrace = found.right,
               lbrace = rbrace ? found.name : '{',
               sublabel = extractSubLabel(false, lbrace, rbrace),
               gg = createGG(),
               subpos = createSubPos(),
               path1 = createPath(gg);

         parseLatex(gg, arg, sublabel, subpos);

         const path2 = createPath(gg),
               w = Math.max(2, Math.round(curr.fsize * 0.2)),
               r = subpos.rect, dy = Math.round(r.y2 - r.y1),
               r_y1 = Math.round(r.y1), r_width = Math.round(r.width);

         switch (found.braces) {
            case '||':
               path1.attr('d', `M${w},${r_y1}v${dy}`);
               path2.attr('d', `M${3 * w + r_width},${r_y1}v${dy}`);
               break;
            case '[]':
               path1.attr('d', `M${2 * w},${r_y1}h${-w}v${dy}h${w}`);
               path2.attr('d', `M${2 * w + r_width},${r_y1}h${w}v${dy}h${-w}`);
               break;
            case '{}':
               path1.attr('d', `M${2 * w},${r_y1}a${w},${w},0,0,0,${-w},${w}v${dy / 2 - 2 * w}a${w},${w},0,0,1,${-w},${w}a${w},${w},0,0,1,${w},${w}v${dy / 2 - 2 * w}a${w},${w},0,0,0,${w},${w}`);
               path2.attr('d', `M${2 * w + r_width},${r_y1}a${w},${w},0,0,1,${w},${w}v${dy / 2 - 2 * w}a${w},${w},0,0,0,${w},${w}a${w},${w},0,0,0,${-w},${w}v${dy / 2 - 2 * w}a${w},${w},0,0,1,${-w},${w}`);
               break;
            default: // ()
               path1.attr('d', `M${w},${r_y1}a${4 * dy},${4 * dy},0,0,0,0,${dy}`);
               path2.attr('d', `M${3 * w + r_width},${r_y1}a${4 * dy},${4 * dy},0,0,1,0,${dy}`);
         }

         positionGNode(subpos, 2 * w, 0, true);

         extendPosition(curr.x, curr.y + r.y1, curr.x + 4 * w + r.width, curr.y + r.y2);

         shiftX(4 * w + r.width);

         continue;
      }

      if (found.deco) {
         const sublabel = extractSubLabel(),
               gg = createGG(),
               subpos = createSubPos();

         subpos.deco = found.deco;

         parseLatex(gg, arg, sublabel, subpos);

         const r = subpos.rect;
         switch (subpos.deco) {
            case 'underline':
               createPath(gg, `M0,${Math.round(r.y2)}h${Math.round(r.width)}`);
               break;
            case 'overline':
               createPath(gg, `M0,${Math.round(r.y1)}h${Math.round(r.width)}`);
               break;
            case 'line-through':
               createPath(gg, `M0,${Math.round(0.45 * r.y1 + 0.55 * r.y2)}h${Math.round(r.width)}`);
               break;
         }

         positionGNode(subpos, 0, 0, true);
         shiftX(r.width);
         continue;
      }

      if (found.bi) { // bold or italic
         const sublabel = extractSubLabel();
         if (sublabel === -1)
            return false;

         const subpos = createSubPos();

         subpos[found.bi] = !subpos[found.bi];

         parseLatex(currG(), arg, sublabel, subpos);

         positionGNode(subpos, curr.x, curr.y);

         shiftX(subpos.rect.width);

         continue;
      }

      let foundarg = 0;

      if (found.arg) {
         const pos = label.indexOf(']{');
         if (pos < 0) {
            console.log('missing argument for ', found.name);
            return false;
         }
         foundarg = label.slice(0, pos);
         if (found.arg === 'int') {
            foundarg = parseInt(foundarg);
            if (!Number.isInteger(foundarg)) {
               console.log('wrong int argument', label.slice(0, pos));
               return false;
            }
         } else if (found.arg === 'float') {
            foundarg = parseFloat(foundarg);
            if (!Number.isFinite(foundarg)) {
               console.log('wrong float argument', label.slice(0, pos));
               return false;
            }
         }
         label = label.slice(pos + 2);
      }

      if (found.shift) {
         const sublabel = extractSubLabel();
         if (sublabel === -1)
            return false;

         const subpos = createSubPos();

         parseLatex(currG(), arg, sublabel, subpos);

         let shiftx = 0, shifty = 0;
         if (found.shift === 'x')
            shiftx = foundarg * subpos.rect.width;
         else
            shifty = foundarg * subpos.rect.height;

         positionGNode(subpos, curr.x + shiftx, curr.y + shifty);

         shiftX(subpos.rect.width * (shiftx > 0 ? 1 + foundarg : 1));

         continue;
      }

      if (found.name === '#url[') {
         const sublabel = extractSubLabel();
         if (sublabel === -1)
            return false;

         const gg = createGG(true),
               subpos = createSubPos();

         gg.attr('href', foundarg);
         if (!isBatchMode() && !curr.painter?.isBatchMode()) {
            gg.on('mouseenter', () => gg.style('text-decoration', 'underline'))
              .on('mouseleave', () => gg.style('text-decoration', null))
              .append('svg:title').text(`link on ${foundarg}`);
         }

         parseLatex(gg, arg, sublabel, subpos);

         positionGNode(subpos, 0, 0, true);
         shiftX(subpos.rect.width);
         continue;
      }

      if ((found.name === '#color[') || (found.name === '#scale[') || (found.name === '#font[')) {
         const sublabel = extractSubLabel();
         if (sublabel === -1)
            return false;

         const subpos = createSubPos();

         if (found.name === '#color[')
            subpos.color = curr.painter.getColor(foundarg);
         else if (found.name === '#font[') {
            subpos.font = new FontHandler(foundarg, subpos.fsize);
            // here symbols embedding not works, use replacement
            if ((subpos.font.name === kSymbol) && !subpos.font.isSymbol) {
               subpos.font.isSymbol = kSymbol;
               subpos.font.name = kTimes;
            }
            subpos.font.setUseFullStyle(true); // while embedding - need to enforce full style
            subpos.ufont = true; // mark that custom font is applied
         } else
            subpos.fsize *= foundarg;

         parseLatex(currG(), arg, sublabel, subpos);

         positionGNode(subpos, curr.x, curr.y);

         shiftX(subpos.rect.width);

         continue;
      }

      if (found.sqrt) {
         const sublabel = extractSubLabel();
         if (sublabel === -1)
            return false;

         const gg = createGG(), subpos = createSubPos();
         let subpos0;

         if (found.arg) {
            subpos0 = createSubPos(0.7);
            parseLatex(gg, arg, foundarg.toString(), subpos0);
         }

         // placeholder for the sqrt sign
         const path = createPath(gg);

         parseLatex(gg, arg, sublabel, subpos);

         const r = subpos.rect,
               h = Math.round(r.height),
               h1 = Math.round(r.height * 0.1),
               w = Math.round(r.width), midy = Math.round((r.y1 + r.y2) / 2),
               f2 = Math.round(curr.fsize * 0.2), r_y2 = Math.round(r.y2);

         if (subpos0)
            positionGNode(subpos0, 0, midy - subpos0.fsize * 0.3, true);

         path.attr('d', `M0,${midy}h${h1}l${h1},${r_y2 - midy - f2}l${h1},${f2 - h}h${Math.round(h * 0.2 + w)}v${h1}`);

         positionGNode(subpos, h * 0.4, 0, true);

         extendPosition(curr.x, curr.y + r.y1 - curr.fsize * 0.1, curr.x + w + h * 0.6, curr.y + r.y2);

         shiftX(w + h * 0.6);

         continue;
      }
   }

   return true;
}

/** @summary translate TLatex and draw inside provided g element
  * @desc use <text> together with normal <path> elements
  * @private */
function produceLatex(painter, node, arg) {
   const pos = { lvl: 0, g: node, x: 0, y: 0, dx: 0, dy: -0.1, fsize: arg.font_size, font: arg.font, parent: null, painter };
   return parseLatex(node, arg, arg.text, pos);
}

let _mj_loading;

/** @summary Load MathJax functionality,
  * @desc one need not only to load script but wait for initialization
  * @private */
async function loadMathjax() {
   const loading = _mj_loading !== undefined;

   if (!loading && (typeof globalThis.MathJax !== 'undefined'))
      return globalThis.MathJax;

   if (!loading)
      _mj_loading = [];

   const promise = new Promise(resolve => {
      if (_mj_loading)
         _mj_loading.push(resolve);
      else
         resolve(globalThis.MathJax);
   });

   if (loading)
      return promise;

   const svg = {
      scale: 1,                      // global scaling factor for all expressions
      minScale: 0.5,                 // smallest scaling factor to use
      mtextInheritFont: false,       // true to make mtext elements use surrounding font
      merrorInheritFont: true,       // true to make merror text use surrounding font
      mathmlSpacing: false,          // true for MathML spacing rules, false for TeX rules
      skipAttributes: {},            // RFDa and other attributes NOT to copy to the output
      exFactor: 0.5,                 // default size of ex in em units
      displayAlign: 'center',        // default for indentalign when set to 'auto'
      displayIndent: '0',            // default for indentshift when set to 'auto'
      fontCache: 'local',            // or 'global' or 'none'
      localID: null,                 // ID to use for local font cache (for single equation processing)
      internalSpeechTitles: true,    // insert <title> tags with speech content
      titleID: 0                     // initial id number to use for aria-labeledby titles
   };

   if (!isNodeJs()) {
      window.MathJax = {
         options: {
            enableMenu: false
         },
         loader: {
            load: ['[tex]/color', '[tex]/upgreek', '[tex]/mathtools', '[tex]/physics']
         },
         tex: {
            packages: { '[+]': ['color', 'upgreek', 'mathtools', 'physics'] }
         },
         svg,
         startup: {
            ready() {
               MathJax.startup.defaultReady();
               const arr = _mj_loading;
               _mj_loading = undefined;
               arr.forEach(func => func(globalThis.MathJax));
            }
         }
      };

      let mj_dir = '../mathjax/3.2.0';
      if (browser.webwindow && exports.source_dir.indexOf('https://root.cern/js') < 0 && exports.source_dir.indexOf('https://jsroot.gsi.de') < 0)
         mj_dir = 'mathjax';

      return loadScript(exports.source_dir + mj_dir + '/es5/tex-svg.js')
               .catch(() => loadScript('https://cdn.jsdelivr.net/npm/mathjax@3.2.0/es5/tex-svg.js'))
               .then(() => promise);
   }

   let JSDOM;

   return _loadJSDOM().then(handle => {
      JSDOM = handle.JSDOM;
      return Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; });
   }).then(mj0 => {
      // return Promise with mathjax loading
      mj0.init({
         loader: {
            load: ['input/tex', 'output/svg', '[tex]/color', '[tex]/upgreek', '[tex]/mathtools', '[tex]/physics']
         },
         tex: {
            packages: { '[+]': ['color', 'upgreek', 'mathtools', 'physics'] }
         },
         svg,
         config: {
            JSDOM
         },
         startup: {
            typeset: false,
            ready() {
               const mj = MathJax;

               mj.startup.registerConstructor('jsdomAdaptor', () => {
                  return new mj._.adaptors.HTMLAdaptor.HTMLAdaptor(new mj.config.config.JSDOM().window);
               });
               mj.startup.useAdaptor('jsdomAdaptor', true);
               mj.startup.defaultReady();
               const arr = _mj_loading;
               _mj_loading = undefined;
               arr.forEach(func => func(mj));
            }
         }
      });

      return promise;
   });
}

const math_symbols_map = {
   '#LT': '\\langle',
   '#GT': '\\rangle',
   '#club': '\\clubsuit',
   '#spade': '\\spadesuit',
   '#heart': '\\heartsuit',
   '#diamond': '\\diamondsuit',
   '#voidn': '\\wp',
   '#voidb': 'f',
   '#copyright': '(c)',
   '#ocopyright': '(c)',
   '#trademark': 'TM',
   '#void3': 'TM',
   '#oright': 'R',
   '#void1': 'R',
   '#3dots': '\\ldots',
   '#lbar': '\\mid',
   '#void8': '\\mid',
   '#divide': '\\div',
   '#Jgothic': '\\Im',
   '#Rgothic': '\\Re',
   '#doublequote': '"',
   '#plus': '+',
   '#minus': '-',
   '#/': '/',
   '#upoint': '.',
   '#aa': '\\mathring{a}',
   '#AA': '\\mathring{A}',
   '#omicron': 'o',
   '#Alpha': 'A',
   '#Beta': 'B',
   '#Epsilon': 'E',
   '#Zeta': 'Z',
   '#Eta': 'H',
   '#Iota': 'I',
   '#Kappa': 'K',
   '#Mu': 'M',
   '#Nu': 'N',
   '#Omicron': 'O',
   '#Rho': 'P',
   '#Tau': 'T',
   '#Chi': 'X',
   '#varomega': '\\varpi',
   '#corner': '?',
   '#ltbar': '?',
   '#bottombar': '?',
   '#notsubset': '?',
   '#arcbottom': '?',
   '#cbar': '?',
   '#arctop': '?',
   '#topbar': '?',
   '#arcbar': '?',
   '#downleftarrow': '?',
   '#splitline': '\\genfrac{}{}{0pt}{}',
   '#it': '\\textit',
   '#bf': '\\textbf',
   '#frac': '\\frac',
   '#left{': '\\lbrace',
   '#right}': '\\rbrace',
   '#left\\[': '\\lbrack',
   '#right\\]': '\\rbrack',
   '#\\[\\]{': '\\lbrack',
   ' } ': '\\rbrack',
   '#\\[': '\\lbrack',
   '#\\]': '\\rbrack',
   '#{': '\\lbrace',
   '#}': '\\rbrace',
   ' ': '\\;'
};

const mathjax_remap = {
   upDelta: 'Updelta',
   upGamma: 'Upgamma',
   upLambda: 'Uplambda',
   upOmega: 'Upomega',
   upPhi: 'Upphi',
   upPi: 'Uppi',
   upPsi: 'Uppsi',
   upSigma: 'Upsigma',
   upTheta: 'Uptheta',
   upUpsilon: 'Upupsilon',
   upXi: 'Upxi',
   notcong: 'ncong',
   notgeq: 'ngeq',
   notgr: 'ngtr',
   notless: 'nless',
   notleq: 'nleq',
   notsucc: 'nsucc',
   notprec: 'nprec',
   notsubseteq: 'nsubseteq',
   notsupseteq: 'nsupseteq',
   openclubsuit: 'clubsuit',
   openspadesuit: 'spadesuit',
   dasharrow: 'dashrightarrow',
   comp: 'circ',
   iiintop: 'iiint',
   iintop: 'iint',
   ointop: 'oint'
};

const mathjax_unicode = {
   Digamma: 0x3DC,
   upDigamma: 0x3DC,
   digamma: 0x3DD,
   updigamma: 0x3DD,
   Koppa: 0x3DE,
   koppa: 0x3DF,
   upkoppa: 0x3DF,
   upKoppa: 0x3DE,
   VarKoppa: 0x3D8,
   upVarKoppa: 0x3D8,
   varkoppa: 0x3D9,
   upvarkoppa: 0x3D9,
   varkappa: 0x3BA, // not found archaic kappa - use normal
   upvarkappa: 0x3BA,
   varbeta: 0x3D0, // not found archaic beta - use normal
   upvarbeta: 0x3D0,
   Sampi: 0x3E0,
   upSampi: 0x3E0,
   sampi: 0x3E1,
   upsampi: 0x3E1,
   Stigma: 0x3DA,
   upStigma: 0x3DA,
   stigma: 0x3DB,
   upstigma: 0x3DB,
   San: 0x3FA,
   upSan: 0x3FA,
   san: 0x3FB,
   upsan: 0x3FB,
   Sho: 0x3F7,
   upSho: 0x3F7,
   sho: 0x3F8,
   upsho: 0x3F8,
   P: 0xB6,
   aa: 0xB0,
   bulletdashcirc: 0x22B7,
   circdashbullet: 0x22B6,
   downuparrows: 0x21F5,
   updownarrows: 0x21C5,
   dashdownarrow: 0x21E3,
   dashuparrow: 0x21E1,
   complement: 0x2201,
   dbar: 0x18C,
   ddddot: 0x22EF,
   dddot: 0x22EF,
   ddots: 0x22F1,
   defineequal: 0x225D,
   defineeq: 0x225D,
   downdownharpoons: 0x2965,
   downupharpoons: 0x296F,
   updownharpoons: 0x296E,
   upupharpoons: 0x2963,
   hateq: 0x2259,
   ldbrack: 0x27E6,
   rdbrack: 0x27E7,
   leadsfrom: 0x219C,
   leftsquigarrow: 0x21DC,
   lightning: 0x2607,
   napprox: 0x2249,
   nasymp: 0x226D,
   nequiv: 0x2262,
   nsimeq: 0x2244,
   nsubseteq: 0x2288,
   nsubset: 0x2284,
   notapprox: 0x2249,
   notasymp: 0x226D,
   notequiv: 0x2262,
   notni: 0x220C,
   notsimeq: 0x2244,
   notsubseteq: 0x2288,
   notsubset: 0x2284,
   notsupseteq: 0x2289,
   notsupset: 0x2285,
   nsupset: 0x2285,
   setdif: 0x2216,
   simarrow: 0x2972,
   t: 0x2040,
   u: 0x2C7,
   v: 0x2C7,
   undercurvearrowright: 0x293B,
   updbar: 0x18C,
   wwbar: 0x2015,
   awointop: 0x2232,
   awoint: 0x2233,
   barintop: 0x2A1C,
   barint: 0x2A1B,
   cwintop: 0x2231, // no opposite direction, use same
   cwint: 0x2231,
   cwointop: 0x2233,
   cwoint: 0x2232,
   oiiintop: 0x2230,
   oiiint: 0x2230,
   oiintop: 0x222F,
   oiint: 0x222F,
   slashintop: 0x2A0F,
   slashint: 0x2A0F
};

const mathjax_asis = ['"', '\'', '`', '=', '~'];

/** @summary Function translates ROOT TLatex into MathJax format
  * @private */
function translateMath(str, kind, color, painter) {
   if (kind !== 2) {
      for (const x in math_symbols_map)
         str = str.replace(new RegExp(x, 'g'), math_symbols_map[x]);

      for (const x in symbols_map) {
         if (x.length > 2)
            str = str.replace(new RegExp(x, 'g'), '\\' + x.slice(1));
      }

      // replace all #color[]{} occurrences
      let clean = '', first = true;
      while (str) {
         let p = str.indexOf('#color[');
         if ((p < 0) && first) {
            clean = str;
            break;
         }
         first = false;
         if (p) {
            const norm = (p < 0) ? str : str.slice(0, p);
            clean += norm;
            if (p < 0)
               break;
         }

         str = str.slice(p + 7);
         p = str.indexOf(']{');
         if (p <= 0)
            break;
         const colindx = parseInt(str.slice(0, p));
         if (!Number.isInteger(colindx))
            break;
         const col = painter.getColor(colindx);
         let cnt = 1;
         str = str.slice(p + 2);
         p = -1;
         while (cnt && (++p < str.length)) {
            if (str[p] === '{')
               cnt++;
            else if (str[p] === '}')
               cnt--;
         }
         if (cnt)
            break;

         const part = str.slice(0, p);
         str = str.slice(p + 1);
         if (part)
            clean += `\\color{${col}}{${part}}`;
      }

      str = clean;
   } else {
      if (str === '\\^')
         str = '\\unicode{0x5E}';
      if (str === '\\vec')
         str = '\\unicode{0x2192}';
      str = str.replace(/\\\./g, '\\unicode{0x2E}').replace(/\\\^/g, '\\hat');
      for (const x in mathjax_unicode)
         str = str.replace(new RegExp(`\\\\\\b${x}\\b`, 'g'), `\\unicode{0x${mathjax_unicode[x].toString(16)}}`);
      mathjax_asis.forEach(symbol => {
         str = str.replace(new RegExp(`(\\\\${symbol})`, 'g'), `\\unicode{0x${symbol.charCodeAt(0).toString(16)}}`);
      });
      for (const x in mathjax_remap)
         str = str.replace(new RegExp(`\\\\\\b${x}\\b`, 'g'), `\\${mathjax_remap[x]}`);
   }

   if (!isStr(color))
      return str;

   // MathJax SVG converter use colors in normal form
   // if (color.indexOf('rgb(') >= 0)
   //    color = color.replace(/rgb/g, '[RGB]')
   //                 .replace(/\(/g, '{')
   //                 .replace(/\)/g, '}');
   return `\\color{${color}}{${str}}`;
}

/** @summary Workaround to fix size attributes in MathJax SVG
  * @private */
function repairMathJaxSvgSize(painter, mj_node, svg, arg) {
   const transform = value => {
      if (!value || !isStr(value) || (value.length < 3))
         return null;
      const p = value.indexOf('ex');
      if ((p < 0) || (p !== value.length - 2))
         return null;
      value = parseFloat(value.slice(0, p));
      return Number.isFinite(value) ? value * arg.font.size * 0.5 : null;
   };

   let width = transform(svg.getAttribute('width')),
       height = transform(svg.getAttribute('height')),
       valign = svg.getAttribute('style');

   if (valign && (valign.length > 18) && valign.indexOf('vertical-align:') === 0) {
      const p = valign.indexOf('ex;');
      valign = ((p > 0) && (p === valign.length - 3)) ? transform(valign.slice(16, valign.length - 1)) : null;
   } else
      valign = null;

   width = (!width || (width <= 0.5)) ? 1 : Math.round(width);
   height = (!height || (height <= 0.5)) ? 1 : Math.round(height);

   svg.setAttribute('width', width);
   svg.setAttribute('height', height);
   svg.removeAttribute('style');

   if (!isNodeJs()) {
      const box = getElementRect(mj_node, 'bbox');
      width = 1.05 * box.width; height = 1.05 * box.height;
   }

   arg.valign = valign;

   if (arg.scale)
      painter.scaleTextDrawing(Math.max(width / arg.width, height / arg.height), arg.draw_g);
}

/** @summary Apply attributes to mathjax drawing
  * @private */
function applyAttributesToMathJax(painter, mj_node, svg, arg, font_size, svg_factor) {
   let mw = parseInt(svg.attr('width')),
       mh = parseInt(svg.attr('height'));

   if (isNodeJs()) {
      // workaround for NaN in viewBox produced by MathJax
      const vb = svg.attr('viewBox');
      if (isStr(vb) && vb.indexOf('NaN') > 0)
         svg.attr('viewBox', vb.replaceAll('NaN', '600'));
      // console.log('Problematic viewBox', vb, svg.select('text').node()?.innerHTML);
   }

   if (Number.isInteger(mh) && Number.isInteger(mw)) {
      if (svg_factor > 0) {
         mw /= svg_factor;
         mh /= svg_factor;
         svg.attr('width', Math.round(mw)).attr('height', Math.round(mh));
      }
   } else {
      const box = getElementRect(mj_node, 'bbox'); // sizes before rotation
      mw = box.width || mw || 100;
      mh = box.height || mh || 10;
   }

   if ((svg_factor > 0) && arg.valign)
      arg.valign /= svg_factor;

   if (arg.valign === null)
      arg.valign = (font_size - mh) / 2;

   const sign = { x: 1, y: 1 };
   let nx = 'x', ny = 'y';
   if (arg.rotate === 180)
      sign.x = sign.y = -1;
   else if ((arg.rotate === 270) || (arg.rotate === 90)) {
      sign.x = (arg.rotate === 270) ? -1 : 1;
      sign.y = -sign.x;
      nx = 'y';
      ny = 'x'; // replace names to which align applied
   }

   if (arg.align[0] === 'middle')
      arg[nx] += sign.x * (arg.width - mw) / 2;
   else if (arg.align[0] === 'end')
      arg[nx] += sign.x * (arg.width - mw);

   if (arg.align[1] === 'middle')
      arg[ny] += sign.y * (arg.height - mh) / 2;
   else if (arg.align[1] === 'bottom')
      arg[ny] += sign.y * (arg.height - mh);
   else if (arg.align[1] === 'bottom-base')
      arg[ny] += sign.y * (arg.height - mh - arg.valign);

   let trans = makeTranslate(arg.x, arg.y) || '';
   if (arg.rotate)
      trans += `${trans ? ' ' : ''}rotate(${arg.rotate})`;

   mj_node.attr('transform', trans || null).attr('visibility', null);
}

/** @summary Produce text with MathJax
  * @private */
async function produceMathjax(painter, mj_node, arg) {
   const mtext = translateMath(arg.text, arg.latex, arg.color, painter),
         options = { em: arg.font.size, ex: arg.font.size / 2, family: arg.font.name, scale: 1, containerWidth: -1, lineWidth: 100000 };

   return loadMathjax()
          .then(mj => mj.tex2svgPromise(mtext, options))
          .then(elem => {
             // when adding element to new node, it will be removed from original parent
             const svg = elem.querySelector('svg');

             mj_node.append(() => svg);

             repairMathJaxSvgSize(painter, mj_node, svg, arg);

             arg.mj_func = applyAttributesToMathJax;
             return true;
          });
}

/** @summary Just typeset HTML node with MathJax
  * @private */
async function typesetMathjax(node) {
   return loadMathjax().then(mj => mj.typesetPromise(node ? [node] : undefined));
}

// list of marker types which can have line widths
const root_50_67 = [2, 3, 5, 4, 25, 26, 27, 28, 30, 32, 35, 36, 37, 38, 40, 42, 44, 46],
   // internal recoding of root markers
      root_markers = [
         0, 1, 2, 3, 4,           //  0..4
         5, 106, 107, 104, 1,     //  5..9
         1, 1, 1, 1, 1,           // 10..14
         1, 1, 1, 1, 1,           // 15..19
         104, 125, 126, 132, 4,   // 20..24
         25, 26, 27, 28, 130,     // 25..29
         30, 3, 32, 127, 128,     // 30..34
         35, 36, 37, 38, 137,     // 35..39
         40, 140, 42, 142, 44,    // 40..44
         144, 46, 146, 148, 149]; // 45..49


/**
  * @summary Handle for marker attributes
  * @private
  */

class TAttMarkerHandler {

   /** @summary constructor
     * @param {object} args - attributes, see {@link TAttMarkerHandler#setArgs} for details */
   constructor(args) {
      this.x0 = this.y0 = 0;
      this.color = 'black';
      this.style = 1;
      this.size = 8;
      this.scale = 1;
      this.stroke = true;
      this.fill = true;
      this.marker = '';
      this.ndig = 0;
      this.used = true;
      this.changed = false;
      this.func = this.apply.bind(this);
      this.setArgs(args);
      this.changed = false;
   }

   /** @summary Set marker attributes.
     * @param {object} args - arguments can be
     * @param {object} args.attr - instance of TAttrMarker (or derived class) or
     * @param {string} args.color - color in HTML form like grb(1,4,5) or 'green'
     * @param {number} args.style - marker style
     * @param {number} args.size - marker size
     * @param {number} [args.refsize] - when specified and marker size < 1, marker size will be calculated relative to that size */
   setArgs(args) {
      if (isObject(args) && (typeof args.fMarkerStyle === 'number'))
         args = { attr: args };

      if (args.attr) {
         args.color ??= args.painter ? args.painter.getColor(args.attr.fMarkerColor) : getColor(args.attr.fMarkerColor);
         if (!args.style || (args.style < 0))
            args.style = args.attr.fMarkerStyle;
         args.size ??= args.attr.fMarkerSize;
      }

      this.color = args.color;
      this.style = args.style;
      this.size = args.size;
      this.refsize = args.refsize;

      this.#configure();
   }

   /** @summary Set usage flag of attribute */
   setUsed(flag) {
      this.used = flag;
   }

   /** @summary Reset position, used for optimization of drawing of multiple markers
    * @private */
   resetPos() { this.lastx = this.lasty = null; }

   /** @summary Create marker path for given position.
     * @desc When drawing many elementary points, created path may depend from previously produced markers.
     * @param {number} x - first coordinate
     * @param {number} y - second coordinate
     * @return {string} path string */
   create(x, y) {
      if (!this.optimized)
         return `M${(x + this.x0).toFixed(this.ndig)},${(y + this.y0).toFixed(this.ndig)}${this.marker}`;

      // use optimized handling with relative position
      const xx = Math.round(x), yy = Math.round(y);
      let mv = `M${xx},${yy}`;
      if (this.lastx !== null) {
         if ((xx === this.lastx) && (yy === this.lasty))
            mv = ''; // pathological case, but let exclude it
         else {
            const m2 = `m${xx - this.lastx},${yy - this.lasty}`;
            if (m2.length < mv.length)
               mv = m2;
         }
      }
      this.lastx = xx + 1;
      this.lasty = yy;
      return mv + 'h1';
   }

   /** @summary Returns full size of marker */
   getFullSize() { return this.scale * this.size; }

   /** @summary Returns approximate length of produced marker string */
   getMarkerLength() { return this.marker ? this.marker.length : 10; }

   /** @summary Change marker attributes.
    *  @param {string} color - marker color
    *  @param {number} style - marker style
    *  @param {number} size - marker size */
   change(color, style, size) {
      this.changed = true;

      if (color !== undefined)
         this.color = color;
      if ((style !== undefined) && (style >= 0))
         this.style = style;
      if (size !== undefined)
         this.size = size;

      this.#configure();
   }

   /** @summary Prepare object to create marker
     * @private */
   #configure() {
      this.x0 = this.y0 = 0;

      if ((this.style === 1) || (this.style === 777)) {
         this.fill = false;
         this.marker = 'h1';
         this.size = 1;
         this.optimized = true;
         this.resetPos();
         return true;
      }

      this.optimized = false;
      this.lwidth = 1;

      let style = this.style;
      if (style >= 50) {
         this.lwidth = 2 + Math.floor((style - 50) / root_50_67.length);
         style = root_50_67[(style - 50) % root_50_67.length];
      }

      const marker_kind = root_markers[style] ?? 104,
            shape = marker_kind % 100;

      this.fill = (marker_kind >= 100);

      this.scale = this.refsize || 8; // v7 defines refsize as 1 or pad height

      const size = this.getFullSize();

      this.ndig = (size > 7) ? 0 : ((size > 2) ? 1 : 2);
      if (shape === 30)
         this.ndig++; // increase precision for star
      let s1 = size.toFixed(this.ndig);
      const s2 = (size / 2).toFixed(this.ndig),
            s3 = (size / 3).toFixed(this.ndig),
            s4 = (size / 4).toFixed(this.ndig),
            s8 = (size / 8).toFixed(this.ndig),
            s38 = (size * 3 / 8).toFixed(this.ndig),
            s34 = (size * 3 / 4).toFixed(this.ndig);

      switch (shape) {
         case 1: // dot
            this.marker = 'h1';
            break;
         case 2: // plus
            this.y0 = -size / 2;
            this.marker = `v${s1}m-${s2},-${s2}h${s1}`;
            break;
         case 3: // asterisk
            this.y0 = -size / 2;
            this.marker = `v${s1}m-${s2},-${s2}h${s1}m-${s8},-${s38}l-${s34},${s34}m${s34},0l-${s34},-${s34}`;
            break;
         case 4: // circle
            this.x0 = -parseFloat(s2);
            s1 = (parseFloat(s2) * 2).toFixed(this.ndig);
            this.marker = `a${s2},${s2},0,1,0,${s1},0a${s2},${s2},0,1,0,-${s1},0z`;
            break;
         case 5: // multiply
            this.x0 = this.y0 = -3 / 8 * size;
            this.marker = `l${s34},${s34}m0,-${s34}l-${s34},${s34}`;
            break;
         case 6: // small dot
            this.x0 = -1;
            this.marker = 'a1,1,0,1,0,2,0a1,1,0,1,0,-2,0z';
            break;
         case 7: // medium dot
            this.x0 = -1.5;
            this.marker = 'a1.5,1.5,0,1,0,3,0a1.5,1.5,0,1,0,-3,0z';
            break;
         case 25: // square
            this.x0 = this.y0 = -size / 2;
            this.marker = `v${s1}h${s1}v-${s1}z`;
            break;
         case 26: // triangle-up
            this.y0 = -size / 2;
            this.marker = `l-${s2},${s1}h${s1}z`;
            break;
         case 27: // diamond
            this.y0 = -size / 2;
            this.marker = `l${s3},${s2}l-${s3},${s2}l-${s3},-${s2}z`;
            break;
         case 28: // cross
            this.x0 = this.y0 = size / 6;
            this.marker = `h${s3}v-${s3}h-${s3}v-${s3}h-${s3}v${s3}h-${s3}v${s3}h${s3}v${s3}h${s3}z`;
            break;
         case 30: { // star
            this.y0 = -size / 2;
            const s56 = (size * 5 / 6).toFixed(this.ndig), s58 = (size * 5 / 8).toFixed(this.ndig);
            this.marker = `l${s3},${s1}l-${s56},-${s58}h${s1}l-${s56},${s58}z`;
            break;
         }
         case 32: // triangle-down
            this.y0 = size / 2;
            this.marker = `l-${s2},-${s1}h${s1}z`;
            break;
         case 35:
            this.x0 = -size / 2;
            this.marker = `l${s2},${s2}l${s2},-${s2}l-${s2},-${s2}zh${s1}m-${s2},-${s2}v${s1}`;
            break;
         case 36:
            this.x0 = this.y0 = -size / 2;
            this.marker = `h${s1}v${s1}h-${s1}zl${s1},${s1}m0,-${s1}l-${s1},${s1}`;
            break;
         case 37:
            this.x0 = -size / 2;
            this.marker = `h${s1}l-${s4},-${s2}l-${s2},${s1}h${s2}l-${s2},-${s1}z`;
            break;
         case 38:
            this.x0 = -size / 4;
            this.y0 = -size / 2;
            this.marker = `h${s2}l${s4},${s4}v${s2}l-${s4},${s4}h-${s2}l-${s4},-${s4}v-${s2}zm${s4},0v${s1}m-${s2},-${s2}h${s1}`;
            break;
         case 40:
            this.x0 = -size / 4;
            this.y0 = -size / 2;
            this.marker = `l${s2},${s1}l${s4},-${s4}l-${s1},-${s2}zm${s2},0l-${s2},${s1}l-${s4},-${s4}l${s1},-${s2}z`;
            break;
         case 42:
            this.y0 = -size / 2;
            this.marker = `l${s8},${s38}l${s38},${s8}l-${s38},${s8}l-${s8},${s38}l-${s8},-${s38}l-${s38},-${s8}l${s38},-${s8}z`;
            break;
         case 44:
            this.x0 = -size / 4;
            this.y0 = -size / 2;
            this.marker = `h${s2}l-${s8},${s38}l${s38},-${s8}v${s2}l-${s38},-${s8}l${s8},${s38}h-${s2}l${s8},-${s38}l-${s38},${s8}v-${s2}l${s38},${s8}z`;
            break;
         case 46:
            this.x0 = -size / 4;
            this.y0 = -size / 2;
            this.marker = `l${s4},${s4}l${s4},-${s4}l${s4},${s4}l-${s4},${s4}l${s4},${s4}l-${s4},${s4}l-${s4},-${s4}l-${s4},${s4}l-${s4},-${s4}l${s4},-${s4}l-${s4},-${s4}z`;
            break;
         case 48:
            this.x0 = -size / 4;
            this.y0 = -size / 2;
            this.marker = `l${s4},${s4}l-${s4},${s4}l-${s4},-${s4}zm${s2},0l${s4},${s4}l-${s4},${s4}l-${s4},-${s4}zm0,${s2}l${s4},${s4}l-${s4},${s4}l-${s4},-${s4}zm-${s2},0l${s4},${s4}l-${s4},${s4}l-${s4},-${s4}z`;
            break;
         case 49:
            this.x0 = -size / 6;
            this.y0 = -size / 2;
            this.marker = `h${s3}v${s3}h-${s3}zm${s3},${s3}h${s3}v${s3}h-${s3}zm-${s3},${s3}h${s3}v${s3}h-${s3}zm-${s3},-${s3}h${s3}v${s3}h-${s3}z`;
            break;
         default: // diamond
            this.y0 = -size / 2;
            this.marker = `l${s3},${s2}l-${s3},${s2}l-${s3},-${s2}z`;
            break;
      }

      return true;
   }

   /** @summary get stroke color */
   getStrokeColor() { return this.stroke ? this.color : 'none'; }

   /** @summary get fill color */
   getFillColor() { return this.fill ? this.color : 'none'; }

   /** @summary returns true if marker attributes will produce empty (invisible) output */
   empty() { return (this.color === 'none') || (!this.fill && !this.stroke); }

   /** @summary Apply marker styles to created element */
   apply(selection) {
      this.used = true;
      selection.style('stroke', this.stroke ? this.color : 'none')
               .style('stroke-width', this.stroke && (this.lwidth > 1) ? this.lwidth : null)
               .style('fill', this.fill ? this.color : 'none');
   }

   /** @summary Method used when color or pattern were changed with OpenUi5 widgets.
     * @private */
   verifyDirectChange(/* painter */) {
      this.change(this.color, parseInt(this.style), parseFloat(this.size));
   }

   /** @summary Create sample with marker in given SVG element
     * @param {selection} svg - SVG element
     * @param {number} width - width of sample SVG
     * @param {number} height - height of sample SVG
     * @private */
   createSample(svg, width, height, plain) {
      if (plain)
         svg = select(svg);
      this.resetPos();
      svg.append('path')
         .attr('d', this.create(width / 2, height / 2))
         .call(this.func);
   }

} // class TAttMarkerHandler

/**
  * @summary Handle for fill attributes
  * @private
  */

class TAttFillHandler {

   #disabled; // if fill disabled

   /** @summary constructor
     * @param {object} args - arguments see {@link TAttFillHandler#setArgs} for more info
     * @param {number} [args.kind = 2] - 1 means object drawing where combination fillcolor == 0 and fillstyle == 1001 means no filling,
     *                                   2 means all other objects where such combination is white-color filling */
   constructor(args) {
      this.color = 'none';
      this.colorindx = 0;
      this.pattern = 0;
      this.used = true;
      this.kind = args.kind || 2;
      this.changed = false;
      this.func = this.apply.bind(this);
      this.setArgs(args);
      this.changed = false; // unset change property
   }

   /** @summary Set fill style as arguments
     * @param {object} args - different arguments to set fill attributes
     * @param {object} [args.attr] - TAttFill object
     * @param {number} [args.color] - color id
     * @param {number} [args.pattern] - fill pattern id
     * @param {object} [args.svg] - SVG element to store newly created patterns
     * @param {string} [args.color_as_svg] - color in SVG format */
   setArgs(args) {
      if (isObject(args.attr)) {
         args.pattern ??= args.attr.fFillStyle;
         args.color ??= args.attr.fFillColor;
      }

      if (args.enable !== undefined)
         this.enable(args.enable);

      const was_changed = this.changed; // preserve changed state
      this.change(args.color, args.pattern, args.svg, args.color_as_svg, args.painter);
      this.changed = was_changed;
   }

   /** @summary Apply fill style to selection */
   apply(selection) {
      if (this.#disabled) {
         selection.style('fill', 'none');
         return;
      }

      this.used = true;

      selection.style('fill', this.getFillColor());

      if ('opacity' in this)
         selection.style('opacity', this.opacity);

      if ('antialias' in this)
         selection.style('antialias', this.antialias);
   }

   /** @summary Returns fill color (or pattern url) */
   getFillColor() { return this.pattern_url || this.color; }

   /** @summary Returns fill color without pattern url.
     * @desc If empty, alternative color will be provided
     * @param {string} [alt] - alternative color which returned when fill color not exists
     * @private */
   getFillColorAlt(alt) { return this.color && (this.color !== 'none') ? this.color : alt; }

   /** @summary Returns true if color not specified or fill style not specified */
   empty() {
      const fill = this.getFillColor();
      return !fill || (fill === 'none');
   }

   /** @summary Enable or disable fill usage - if disabled only 'fill: none' will be applied */
   enable(on) {
      this.#disabled = ((on === undefined) || on) ? undefined : true;
   }

   /** @summary Set usage flag of attribute */
   setUsed(flag) {
      this.used = flag;
   }

   /** @summary Returns true if fill attributes has real color */
   hasColor() {
      return this.color && (this.color !== 'none');
   }

   /** @summary Set solid fill color as fill pattern
     * @param {string} col - solid color */
   setSolidColor(col) {
      delete this.pattern_url;
      this.color = col;
      this.pattern = 1001;
   }

   /** @summary Set fill color opacity */
   setOpacity(o) {
      this.opacity = o;
   }

   /** @summary Check if solid fill is used, also color can be checked
     * @param {string} [solid_color] - when specified, checks if fill color matches */
   isSolid(solid_color) {
      if ((this.pattern !== 1001) || this.gradient)
         return false;
      return !solid_color || (solid_color === this.color);
   }

   /** @summary Method used when color or pattern were changed with OpenUi5 widgets
     * @private */
   verifyDirectChange(painter) {
      if (isStr(this.pattern))
         this.pattern = parseInt(this.pattern);
      if (!Number.isInteger(this.pattern))
         this.pattern = 0;

      this.change(this.color, this.pattern, painter?.getCanvSvg(), true, painter);
   }

   /** @summary Method to change fill attributes.
     * @param {number} color - color index
     * @param {number} pattern - pattern index
     * @param {selection} svg - top canvas element for pattern storages
     * @param {string} [color_as_svg] - when color is string, interpret as normal SVG color
     * @param {object} [painter] - when specified, used to extract color by index */
   change(color$1, pattern, svg, color_as_svg, painter) {
      delete this.pattern_url;
      delete this.gradient;
      this.changed = true;

      if ((color$1 !== undefined) && Number.isInteger(parseInt(color$1)) && !color_as_svg)
         this.colorindx = parseInt(color$1);

      if ((pattern !== undefined) && Number.isInteger(parseInt(pattern))) {
         this.pattern = parseInt(pattern);
         delete this.opacity;
         delete this.antialias;
      }

      if ((this.pattern === 1000) && (this.colorindx === 0)) {
         this.pattern_url = 'white';
         return true;
      }

      if (this.pattern === 1000)
         this.pattern = 1001;

      if (this.pattern < 1001) {
         this.pattern_url = 'none';
         return true;
      }

      if (this.isSolid() && (this.colorindx === 0) && (this.kind === 1) && !color_as_svg) {
         this.pattern_url = 'none';
         return true;
      }

      let indx = this.colorindx;

      if (color_as_svg) {
         this.color = color$1;
         if (color$1 !== 'none')
            indx = color(color$1).hex().slice(1); // fictional index produced from color code
      } else
         this.color = painter ? painter.getColor(indx) : getColor(indx);

      if (!isStr(this.color)) {
         if (isObject(this.color) && (this.color?._typename === clTLinearGradient || this.color?._typename === clTRadialGradient))
            this.gradient = this.color;
         this.color = 'none';
      }

      if (this.isSolid())
         return true;

      if (!this.gradient) {
         if ((this.pattern >= 4000) && (this.pattern <= 4100)) {
            // special transparent colors (use for sub-pads)
            this.opacity = (this.pattern - 4000) / 100;
            return true;
         }
         if ((this.pattern < 3000) || (this.color === 'none'))
            return false;
      }

      if (!svg || svg.empty())
         return false;

      let id, lines = '', lfill = null, fills = '', fills2 = '', w = 2, h = 2;

      if (this.gradient)
         id = `grad_${this.gradient.fNumber}`;
      else {
         id = `pat_${this.pattern}_${indx}`;

         switch (this.pattern) {
            case 3001:
               w = h = 2;
               fills = 'M0,0h1v1h-1zM1,1h1v1h-1z';
               break;
            case 3002:
               w = 4;
               h = 2;
               fills = 'M1,0h1v1h-1zM3,1h1v1h-1z';
               break;
            case 3003:
               w = h = 4;
               fills = 'M2,1h1v1h-1zM0,3h1v1h-1z';
               break;
            case 3004:
               w = h = 8;
               lines = 'M8,0L0,8';
               break;
            case 3005:
               w = h = 8;
               lines = 'M0,0L8,8';
               break;
            case 3006:
               w = h = 4;
               lines = 'M1,0v4';
               break;
            case 3007:
               w = h = 4;
               lines = 'M0,1h4';
               break;
            case 3008:
               w = h = 10;
               fills = 'M0,3v-3h3ZM7,0h3v3ZM0,7v3h3ZM7,10h3v-3ZM5,2l3,3l-3,3l-3,-3Z';
               lines = 'M0,3l5,5M3,10l5,-5M10,7l-5,-5M7,0l-5,5';
               break;
            case 3009:
               w = h = 12;
               lines = 'M0,0A6,6,0,0,0,12,0M6,6A6,6,0,0,0,12,12M6,6A6,6,0,0,1,0,12';
               lfill = 'none';
               break;
            case 3010: // bricks
               w = h = 10;
               lines = 'M0,2h10M0,7h10M2,0v2M7,2v5M2,7v3';
               break;
            case 3011:
               w = 9;
               h = 18;
               lines = 'M5,0v8M2,1l6,6M8,1l-6,6M9,9v8M6,10l3,3l-3,3M0,9v8M3,10l-3,3l3,3';
               lfill = 'none';
               break;
            case 3012:
               w = 10;
               h = 20;
               lines = 'M5,1A4,4,0,0,0,5,9A4,4,0,0,0,5,1M0,11A4,4,0,0,1,0,19M10,11A4,4,0,0,0,10,19';
               lfill = 'none';
               break;
            case 3013:
               w = h = 7;
               lines = 'M0,0L7,7M7,0L0,7';
               lfill = 'none';
               break;
            case 3014:
               w = h = 16;
               lines = 'M0,0h16v16h-16v-16M0,12h16M12,0v16M4,0v8M4,4h8M0,8h8M8,4v8';
               lfill = 'none';
               break;
            case 3015:
               w = 6;
               h = 12;
               lines = 'M2,1A2,2,0,0,0,2,5A2,2,0,0,0,2,1M0,7A2,2,0,0,1,0,11M6,7A2,2,0,0,0,6,11';
               lfill = 'none';
               break;
            case 3016:
               w = 12;
               h = 7;
               lines = 'M0,1A3,2,0,0,1,3,3A3,2,0,0,0,9,3A3,2,0,0,1,12,1';
               lfill = 'none';
               break;
            case 3017:
               w = h = 4;
               lines = 'M3,1l-2,2';
               break;
            case 3018:
               w = h = 4;
               lines = 'M1,1l2,2';
               break;
            case 3019:
               w = h = 12;
               lines = 'M1,6A5,5,0,0,0,11,6A5,5,0,0,0,1,6h-1h1A5,5,0,0,1,6,11v1v-1A5,5,0,0,1,11,6h1h-1A5,5,0,0,1,6,1v-1v1A5,5,0,0,1,1,6';
               lfill = 'none';
               break;
            case 3020:
               w = 7;
               h = 12;
               lines = 'M1,0A2,3,0,0,0,3,3A2,3,0,0,1,3,9A2,3,0,0,0,1,12';
               lfill = 'none';
               break;
            case 3021: // left stairs
               w = h = 8;
               lines = 'M8,2h-2v4h-4v2M2,0v2h-2';
               lfill = 'none';
               break;
            case 3022: // right stairs
               w = h = 8;
               lines = 'M0,2h2v4h4v2M6,0v2h2';
               lfill = 'none';
               break;
            case 3023:
               w = h = 8;
               fills = 'M4,0h4v4zM8,4v4h-4z';
               fills2 = 'M4,0L0,4L4,8L8,4Z';
               break;
            case 3024:
               w = h = 16;
               fills = 'M0,8v8h2v-8zM8,0v8h2v-8M4,14v2h12v-2z';
               fills2 = 'M0,2h8v6h4v-6h4v12h-12v-6h-4z';
               break;
            case 3025:
               w = h = 18;
               fills = 'M5,13v-8h8ZM18,0v18h-18l5,-5h8v-8Z';
               break;
            default: {
               if ((this.pattern > 3025) && (this.pattern < 3100)) {
                  // same as 3002, see TGX11.cxx, line 2234
                  w = 4;
                  h = 2;
                  fills = 'M1,0h1v1h-1zM3,1h1v1h-1z';
                  break;
               }

               const code = this.pattern % 1000,
                     k = code % 10,
                     j = ((code - k) % 100) / 10,
                     i = (code - j * 10 - k) / 100;
               if (!i)
                  break;

               // use flexible hatches only possible when single pattern is used,
               // otherwise it is not possible to adjust pattern dimension that both hatches match with each other
               const use_new = (j === k) || (j === 0) || (j === 5) || (j === 9) || (k === 0) || (k === 5) || (k === 9),
                     pp = painter?.getPadPainter(),
                     scale_size = pp ? Math.max(pp.getPadWidth(), pp.getPadHeight()) : 600,
                     spacing_original = Math.max(0.1, gStyle.fHatchesSpacing * scale_size * 0.001),
                     hatches_spacing = Math.max(1, Math.round(spacing_original)) * 6,
                     sz = i * hatches_spacing; // axis distance between lines

               id += use_new ? `_hn${Math.round(spacing_original * 100)}` : `_ho${hatches_spacing}`;

               w = h = 6 * sz; // we use at least 6 steps

               const produce_old = (dy, swap) => {
                  const pos = [];
                  let step = sz, y1 = 0, max = h, y2, x1, x2;

                  // reduce step for smaller angles to keep normal distance approx same
                  if (Math.abs(dy) < 3)
                     step = Math.round(sz / 12 * 9);
                  if (dy === 0) {
                     step = Math.round(sz / 12 * 8);
                     y1 = step / 2;
                  } else if (dy > 0)
                     max -= step;
                  else
                     y1 = step;

                  while (y1 <= max) {
                     y2 = y1 + dy * step;
                     if (y2 < 0) {
                        x2 = Math.round(y1 / (y1 - y2) * w);
                        pos.push(0, y1, x2, 0);
                        pos.push(w, h - y1, w - x2, h);
                     } else if (y2 > h) {
                        x2 = Math.round((h - y1) / (y2 - y1) * w);
                        pos.push(0, y1, x2, h);
                        pos.push(w, h - y1, w - x2, 0);
                     } else
                        pos.push(0, y1, w, y2);
                     y1 += step;
                  }
                  for (let b = 0; b < pos.length; b += 4) {
                     if (swap) {
                        x1 = pos[b + 1];
                        y1 = pos[b];
                        x2 = pos[b + 3];
                        y2 = pos[b + 2];
                     } else {
                        x1 = pos[b];
                        y1 = pos[b + 1];
                        x2 = pos[b + 2];
                        y2 = pos[b + 3];
                     }
                     lines += `M${x1},${y1}`;
                     if (y2 === y1)
                        lines += `h${x2 - x1}`;
                     else if (x2 === x1)
                        lines += `v${y2 - y1}`;
                     else
                        lines += `L${x2},${y2}`;
                  }
               };

               /* eslint-disable-next-line one-var */
               const produce_new = (_aa, _bb, angle, swapx) => {
                  if ((angle === 0) || (angle === 90)) {
                     const dy = i * spacing_original * 3,
                           nsteps = Math.round(h / dy),
                           dyreal = h / nsteps;
                     let yy = dyreal / 2;

                     while (yy < h) {
                        if (angle === 0)
                           lines += `M0,${Math.round(yy)}h${w}`;
                        else
                           lines += `M${Math.round(yy)},0v${h}`;
                        yy += dyreal;
                     }

                     return;
                  }

                  const a = angle / 180 * Math.PI,
                        dy = i * spacing_original * 3 / Math.cos(a),
                        hside = Math.tan(a) * w,
                        hside_steps = Math.round(hside / dy),
                        dyreal = hside / hside_steps,
                        nsteps = Math.floor(h / dyreal);

                  h = Math.round(nsteps * dyreal);

                  let yy = nsteps * dyreal;

                  while (Math.abs(yy - h) < 0.1)
                     yy -= dyreal;

                  while (yy + hside > 0) {
                     let x1 = 0, y1 = yy, x2 = w, y2 = yy + hside;

                     if (y1 < -1e-5) {
                        // cut at the begin
                        x1 = -y1 / hside * w;
                        y1 = 0;
                     } else if (y2 > h) {
                        // cut at the end
                        x2 = (h - y1) / hside * w;
                        y2 = h;
                     }

                     if (swapx) {
                        x1 = w - x1;
                        x2 = w - x2;
                     }

                     lines += `M${Math.round(x1)},${Math.round(y1)}L${Math.round(x2)},${Math.round(y2)}`;
                     yy -= dyreal;
                  }
               };

               /* eslint-disable-next-line one-var */
               const func = use_new ? produce_new : produce_old;

               let horiz = false, vertical = false;

               switch (j) {
                  case 0: horiz = true; break;
                  case 1: func(1, false, 10); break;
                  case 2: func(2, false, 20); break;
                  case 3: func(3, false, 30); break;
                  case 4: func(6, false, 45); break;
                  case 6: func(3, true, 60); break;
                  case 7: func(2, true, 70); break;
                  case 8: func(1, true, 80); break;
                  case 9: vertical = true; break;
               }

               switch (k) {
                  case 0: horiz = true; break;
                  case 1: func(-1, false, 10, true); break;
                  case 2: func(-2, false, 20, true); break;
                  case 3: func(-3, false, 30, true); break;
                  case 4: func(-6, false, 45, true); break;
                  case 6: func(-3, true, 60, true); break;
                  case 7: func(-2, true, 70, true); break;
                  case 8: func(-1, true, 80, true); break;
                  case 9: vertical = true; break;
               }

               if (horiz)
                  func(0, false, 0);
               if (vertical)
                  func(0, true, 90);

               break;
            }
         }

         if (!fills && !lines)
            return false;
      }

      this.pattern_url = `url(#${id})`;
      this.antialias = false;

      let defs = svg.selectChild('.canvas_defs');
      if (defs.empty())
         defs = svg.insert('svg:defs', ':first-child').attr('class', 'canvas_defs');

      if (defs.selectChild('.' + id).empty()) {
         if (this.gradient) {
            const is_linear = this.gradient._typename === clTLinearGradient,
                  grad = defs.append(is_linear ? 'svg:linearGradient' : 'svg:radialGradient')
                             .attr('id', id).attr('class', id),
                  conv = v => { return v === Math.round(v) ? v.toFixed(0) : v.toFixed(2); };
            if (is_linear) {
               grad.attr('x1', conv(this.gradient.fStart.fX))
                   .attr('y1', conv(1 - this.gradient.fStart.fY))
                   .attr('x2', conv(this.gradient.fEnd.fX))
                   .attr('y2', conv(1 - this.gradient.fEnd.fY));
            } else {
               grad.attr('cx', conv(this.gradient.fStart.fX))
                   .attr('cy', conv(1 - this.gradient.fStart.fY))
                   .attr('cr', conv(this.gradient.fR1));
            }
            for (let n = 0; n < this.gradient.fColorPositions.length; ++n) {
               const pos = this.gradient.fColorPositions[n],
                     col = toColor(this.gradient.fColors[n * 4], this.gradient.fColors[n * 4 + 1], this.gradient.fColors[n * 4 + 2]);
               grad.append('svg:stop').attr('offset', `${Math.round(pos * 100)}%`)
                                      .attr('stop-color', col)
                                      .attr('stop-opacity', `${Math.round(this.gradient.fColors[n * 4 + 3] * 100)}%`);
            }
         } else {
            const patt = defs.append('svg:pattern')
                             .attr('id', id).attr('class', id).attr('patternUnits', 'userSpaceOnUse')
                             .attr('width', w).attr('height', h);

            if (fills2) {
               const col = rgb(this.color);
               col.r = Math.round((col.r + 255) / 2);
               col.g = Math.round((col.g + 255) / 2);
               col.b = Math.round((col.b + 255) / 2);
               patt.append('svg:path').attr('d', fills2).style('fill', col);
            }
            if (fills)
               patt.append('svg:path').attr('d', fills).style('fill', this.color);
            if (lines)
               patt.append('svg:path').attr('d', lines).style('stroke', this.color).style('stroke-width', gStyle.fHatchesLineWidth || 1).style('fill', lfill);
         }
      }

      return true;
   }

   /** @summary Create sample of fill pattern inside SVG
     * @private */
   createSample(svg, width, height, plain) {
      // we need to create extra handle to change
      if (plain)
         svg = select(svg);

      const sample = new TAttFillHandler({ svg, pattern: this.pattern, color: this.color, color_as_svg: true });

      svg.append('path')
         .attr('d', `M0,0h${width}v${height}h${-width}z`)
         .call(sample.func);
   }

   /** @summary Save fill attributes to style
     * @private */
   saveToStyle(name_color, name_pattern) {
      if (name_color) {
         const indx = this.colorindx ?? findColor(this.color);
         if (indx >= 0)
            gStyle[name_color] = indx;
      }
      if (name_pattern)
         gStyle[name_pattern] = this.pattern;
   }

} // class TAttFillHandler

const root_line_styles = [
   '', '', '3, 3', '1, 2',
   '3, 4, 1, 4', '5, 3, 1, 3', '5, 3, 1, 3, 1, 3, 1, 3', '5, 5',
   '5, 3, 1, 3, 1, 3', '20, 5', '20, 10, 1, 10', '1, 3'];

/**
  * @summary Handle for line attributes
  * @private
  */

class TAttLineHandler {

   /** @summary constructor
     * @param {object} attr - attributes, see {@link TAttLineHandler#setArgs} */
   constructor(args) {
      this.func = this.apply.bind(this);
      this.used = true;
      if (args._typename && (args.fLineStyle !== undefined))
         args = { attr: args };
      this.setArgs(args);
   }

   /** @summary Set line attributes.
     * @param {object} args - specify attributes by different ways
     * @param {object} args.attr - TAttLine object with appropriate data members or
     * @param {string} args.color - color in html like rgb(255,0,0) or 'red' or '#ff0000'
     * @param {number} args.style - line style number
     * @param {number} args.width - line width */
   setArgs(args) {
      if (args.attr) {
         this.color_index = args.attr.fLineColor;
         args.color = args.color0 || (args.painter?.getColor(this.color_index) ?? getColor(this.color_index));
         args.width ??= args.attr.fLineWidth;
         args.style ??= args.attr.fLineStyle;
      } else if (isStr(args.color)) {
         if ((args.color !== 'none') && !args.width)
            args.width = 1;
      } else if (typeof args.color === 'number') {
         this.color_index = args.color;
         args.color = args.painter?.getColor(args.color) ?? getColor(args.color);
      }

      if (args.width === undefined)
         args.width = (args.color && args.color !== 'none') ? 1 : 0;

      this.nocolor = args.nocolor;
      this.color = (args.width === 0) || this.nocolor ? 'none' : args.color;
      this.width = args.width;
      this.style = args.style;
      this.pattern = args.pattern || root_line_styles[this.style] || null;

      if (args.can_excl) {
         this.excl_side = this.excl_width = 0;
         if (Math.abs(this.width) > 99) {
            // exclusion graph
            this.excl_side = (this.width < 0) ? -1 : 1;
            this.excl_width = Math.floor(this.width / 100) * 5;
            this.width = Math.abs(this.width % 100); // line width
         }
      }

      // if custom color number used, use lightgrey color to show lines
      if (!this.color && (this.width > 0))
         this.color = 'lightgrey';
   }

   /** @summary Change exclusion attributes */
   changeExcl(side, width) {
      if (width !== undefined)
         this.excl_width = width;
      if (side !== undefined) {
         this.excl_side = side;
         if ((this.excl_width === 0) && this.excl_side)
            this.excl_width = 20;
      }
      this.changed = true;
   }

   /** @summary returns true if line attribute is empty and will not be applied. */
   empty() { return this.color === 'none'; }

   /** @summary Set usage flag of attribute */
   setUsed(flag) {
      this.used = flag;
   }

   /** @summary set border parameters, used for rect drawing */
   setBorder(rx, ry) {
      this.rx = rx;
      this.ry = ry;
      this.func = this.applyBorder.bind(this);
   }

   /** @summary Applies line attribute to selection.
     * @param {object} selection - d3.js selection */
   apply(selection) {
      this.used = true;
      if (this.empty()) {
         selection.style('stroke', null)
                  .style('stroke-width', null)
                  .style('stroke-dasharray', null);
      } else {
         selection.style('stroke', this.color)
                  .style('stroke-width', this.width)
                  .style('stroke-dasharray', this.pattern);
      }
   }

   /** @summary Applies line and border attribute to selection.
     * @param {object} selection - d3.js selection */
   applyBorder(selection) {
      this.used = true;
      if (this.empty()) {
         selection.attr('rx', null)
                  .attr('ry', null)
                  .style('stroke', null)
                  .style('stroke-width', null)
                  .style('stroke-dasharray', null);
      } else {
         selection.attr('rx', this.rx || null)
                  .attr('ry', this.ry || null)
                  .style('stroke', this.color)
                  .style('stroke-width', this.width)
                  .style('stroke-dasharray', this.pattern);
      }
   }

   /** @summary Change line attributes */
   change(color, width, style) {
      if (color !== undefined) {
         if (this.color !== color)
            delete this.color_index;
         this.color = color;
      }
      if (width !== undefined)
         this.width = width;
      if (style !== undefined) {
         this.style = style;
         this.pattern = root_line_styles[this.style] || null;
      }
      this.changed = true;
   }

   /** @summary Method used when color or pattern were changed with OpenUi5 widgets.
     * @private */
   verifyDirectChange(/* painter */) {
      this.change(this.color, parseInt(this.width), parseInt(this.style));
   }

   /** @summary Create sample element inside primitive SVG - used in context menu */
   createSample(svg, width, height, plain) {
      if (plain)
         svg = select(svg);
      svg.append('path')
         .attr('d', `M0,${height / 2}h${width}`)
         .call(this.func);
   }

   /** @summary Save attributes values to gStyle */
   saveToStyle(name_color, name_width, name_style) {
      if (name_color) {
         const indx = (this.color_index !== undefined) ? this.color_index : findColor(this.color);
         if (indx >= 0)
            gStyle[name_color] = indx;
      }
      if (name_width)
         gStyle[name_width] = this.width;
      if (name_style)
         gStyle[name_style] = this.style;
   }

} // class TAttLineHandler

/** @summary Get svg string for specified line style
  * @private */
function getSvgLineStyle(indx) {
   if ((indx < 0) || (indx >= root_line_styles.length))
      indx = 11;
   return root_line_styles[indx];
}

function calcTextSize(sz, sz0, fact, pp) {
   if (!sz)
      sz = sz0 || 0;

   if (sz >= 1)
      return Math.round(sz * (pp?.getPadScale() || 1));

   return Math.round(sz * Math.min(pp?.getPadWidth() ?? 1000, pp?.getPadHeight() ?? 1000) * (fact || 1));
}

/**
  * @summary Handle for text attributes
  * @private
  */

class TAttTextHandler {

   /** @summary constructor
     * @param {object} attr - attributes, see {@link TAttTextHandler#setArgs} */
   constructor(args) {
      this.used = true;
      if (args._typename && (args.fTextFont !== undefined))
         args = { attr: args };
      this.setArgs(args);
   }

   /** @summary Set text attributes.
     * @param {object} args - specify attributes by different ways
     * @param {object} args.attr - TAttText object with appropriate data members or
     * @param {object} args.attr_alt - alternative TAttText object with appropriate data members if values are 0
     * @param {string} args.color - color in html like rgb(255,0,0) or 'red' or '#ff0000'
     * @param {number} args.align - text align
     * @param {number} args.angle - text angle
     * @param {number} args.font  - font index
     * @param {number} args.size  - text size */
   setArgs(args) {
      if (args.attr) {
         args.font = args.attr.fTextFont || args.attr_alt?.fTextFont || 0;
         args.size = args.attr.fTextSize || args.attr_alt?.fTextSize || 0;
         this.color_index = args.attr.fTextColor || args.attr_alt?.fTextColor || 0;
         args.color = args.painter?.getColor(this.color_index) ?? getColor(this.color_index);
         args.align = args.attr.fTextAlign || args.attr_alt?.fTextAlign || 0;
         args.angle = args.attr.fTextAngle || args.attr_alt?.fTextAngle || 0;
      } else if (typeof args.color === 'number') {
         this.color_index = args.color;
         args.color = args.painter?.getColor(args.color) ?? getColor(args.color);
      }

      this.font = args.font;
      this.size = args.size;
      this.color = args.color;
      this.align = args.align;
      this.angle = args.angle;

      this.can_rotate = args.can_rotate ?? true;
      this.angle_used = false;
      this.align_used = false;
   }

   /** @summary returns true if line attribute is empty and will not be applied. */
   empty() { return this.color === 'none'; }

   /** @summary Change text attributes */
   change(font, size, color, align, angle) {
      if (font !== undefined)
         this.font = font;
      if (size !== undefined)
         this.size = size;
      if (color !== undefined) {
         if (this.color !== color)
            delete this.color_index;
         this.color = color;
      }
      if (align !== undefined)
         this.align = align;
      if (angle !== undefined)
         this.angle = angle;
      this.changed = true;
   }

   /** @summary Method used when color or pattern were changed with OpenUi5 widgets.
     * @private */
   verifyDirectChange(/* painter */) {
      this.change(parseInt(this.font), parseFloat(this.size), this.color, parseInt(this.align), parseInt(this.angle));
   }

   /** @summary Create argument for drawText method */
   createArg(arg) {
      if (!arg)
         arg = {};
      this.align_used = !arg.noalign && !arg.align;
      if (this.align_used)
         arg.align = this.align;
      this.angle_used = !arg.norotate && this.can_rotate;
      if (this.angle_used && this.angle)
         arg.rotate = -this.angle; // SVG rotation angle has different sign
      arg.color = this.color || 'black';
      return arg;
   }

   /** @summary Provides pixel size */
   getSize(pp, fact, zero_size) { return calcTextSize(this.size, zero_size, fact, pp); }

   /** @summary Returns alternating size - which defined by sz1 variable */
   getAltSize(sz1, pp) { return calcTextSize(sz1, this.size, 1, pp); }

   /** @summary Get font index - without precision */
   getGedFont() { return Math.floor(this.font / 10); }

   /** @summary Change text font from GED */
   setGedFont(value) {
      const v = parseInt(value);
      if ((v > 0) && (v < 17))
         this.change(v * 10 + (this.font % 10));
      return this.font;
   }

} // class TAttTextHandler

/** @summary returns true if pad painter @private */
function isPadPainter(p) {
   return isFunc(p?.getRootPad) && isFunc(p?.forEachPainterInPad);
}

/** @summary returns canvas painter from DOM element @private */
function getDomCanvasPainter(dom) {
   const elem = dom?.select('.root_canvas');
   return !elem || elem.empty() ? null : elem.property('pad_painter');
}

/**
 * @summary Painter class for ROOT objects
 *
 */

class ObjectPainter extends BasePainter {

   #draw_object;     // drawn object
   #draw_g;          // <g> element for object drawing
   #pad_painter_ref; // reference of pad painter
   #main_painter;    // WeakRef to main painter in the pad
   #primary_ref;     // reference of primary painter - if any
   #snapid;          // assigned online identifier
   #is_primary;      // if primary painter
   #secondary_id;    // id of this painter in relation to primary painter
   #options;         // current options object
   #options_store;   // stored draw options used to check changes
   #user_tooltip_handler; // configured user tooltip handler
   #user_tooltip_timeout; // timeout configured with tooltip handler
   #user_toottip_handle; // timeout handle processing user tooltip
   #user_context_menu; // function for user context menu
   #special_draw_area; // current special draw area like projection
   #root_colors;     // custom colors list
   #fillatt;         // fill attribute
   #lineatt;         // line attribute
   #markeratt;       // marker attribute
   #textatt;         // text attribute

   /** @summary constructor
     * @param {object|string} dom - dom element or identifier or pad painter
     * @param {object} obj - object to draw
     * @param {string} [opt] - object draw options */
   constructor(dom, obj, opt) {
      const pp = isPadPainter(dom) ? dom : null;

      super(pp?.getDom() ?? dom);

      this.setPadPainter(pp);

      this.#draw_g = undefined; // container for all drawn objects
      this.assignObject(obj);
      if (isStr(opt))
         this.#options = { original: opt };
   }

   /** @summary Assign object to the painter
     * @protected */
   assignObject(obj) { this.#draw_object = isObject(obj) ? obj : null; }

   /** @summary Returns drawn object */
   getObject() { return this.#draw_object; }

   /** @summary Assign new pad painter
     * @protected */
   setPadPainter(pp) { this.#pad_painter_ref = pp ? new WeakRef(pp) : undefined; }

   /** @summary returns pad painter where object is drawn
     * @protected */
   getPadPainter() { return this.#pad_painter_ref?.deref(); }

   /** @summary returns canvas painter
     * @protected */
   getCanvPainter() {
      let pp = this.getPadPainter();
      while (pp && !pp.isCanvas())
         pp = pp.getPadPainter();
      return pp;
   }

   /** @summary Indicates that drawing runs in batch mode
     * @private */
   isBatchMode() { return isBatchMode() ? true : (this.getCanvPainter()?.isBatchMode() ?? false); }

   /** @summary Assign snapid to the painter
    * @desc Identifier used to communicate with server side and identifies object on the server
    * @private */
   assignSnapId(id) { this.#snapid = id; }

   /** @summary Provides identifier on server for requested sub-element */
   getSnapId(subelem) { return !this.#snapid ? '' : (this.#snapid + (subelem ? '#' + subelem : '')); }

   /** @summary Returns true if snapid was assigned */
   hasSnapId() { return this.#snapid !== undefined; }

   /** @summary Generic method to cleanup painter.
     * @desc Remove object drawing and (in case of main painter) also main HTML components
     * @protected */
   cleanup() {
      this.removeG();

      let keep_origin = true;

      if (this.isMainPainter()) {
         const pp = this.getPadPainter();
         if (!pp || pp.isCanvas('auto'))
            keep_origin = false;
      }

      // cleanup all existing references
      this.#pad_painter_ref = undefined;
      this.#main_painter = null;
      this.#draw_object = null;
      this.#snapid = undefined;
      this.#is_primary = undefined;
      this.#primary_ref = undefined;
      this.#secondary_id = undefined;

      // remove attributes objects (if any)
      this.deleteAttr();
      this.#root_colors = undefined;
      this.#options = undefined;
      this.#options_store = undefined;

      // remove extra fields from v7 painters
      delete this.rstyle;
      delete this.csstype;

      super.cleanup(keep_origin);
   }

   /** @summary Returns drawn object name */
   getObjectName() { return this.getObject()?.fName ?? ''; }

   /** @summary Returns drawn object class name */
   getClassName() { return this.getObject()?._typename ?? ''; }

   /** @summary Checks if drawn object matches with provided typename
     * @param {string|object} arg - typename (or object with _typename member)
     * @protected */
   matchObjectType(arg) {
      const clname = this.getClassName();
      if (!arg || !clname)
         return false;
      if (isStr(arg))
         return arg === clname;
      if (isStr(arg._typename))
         return arg._typename === clname;
      return Boolean(clname.match(arg));
   }

   /** @summary Change item name
     * @desc When available, used for svg:title property
     * @private */
   setItemName(name, opt, hpainter) {
      super.setItemName(name, opt, hpainter);
      if (this._no_default_title || !name)
         return;
      const can = this.getCanvSvg();
      if (!can.empty())
         can.select('title').text(name);
      else
         this.selectDom().attr('title', name);
      const cp = this.getCanvPainter();
      if (cp && ((cp === this) || (this.isMainPainter() && (cp === this.getPadPainter()))))
         cp.drawItemNameOnCanvas(name);
   }

   /** @summary Create options and copy new args
     * @return options
     * @private */
   setOptions(new_options, as_is) {
      if (as_is)
         this.#options = new_options;
      else {
         if (!this.#options)
            this.#options = {};
         Object.assign(this.#options, new_options);
      }
      return this.#options;
   }

   /** @summary Return actual options */
   getOptions(as_is) {
      if (!as_is && !this.#options)
         this.#options = {};
      return this.#options;
   }

   /** @summary Emulate old options property */
   get options() { return this.getOptions(); }

   /** @summary Store actual options together with original string
     * @private */
   storeDrawOpt(original) {
      if (!this.#options)
         return;
      if (!original)
         original = '';
      const pp = original.indexOf(';;');
      if (pp >= 0)
         original = original.slice(0, pp);
      this.#options.original = original;
      this.#options_store = Object.assign({}, this.#options);
   }

   /** @summary Return dom argument for object drawing
    * @desc Can be used to draw other objects on same pad / same dom element
    * @protected */
   getDrawDom() { return this.getPadPainter() || this.getDom(); }

   /** @summary Return actual draw options as string
     * @param ignore_pad - do not include pad settings into histogram draw options
     * @desc if options are not modified - returns original string which was specified for object draw */
   getDrawOpt(ignore_pad) {
      if (!this.#options)
         return '';

      if (isFunc(this.#options.asString)) {
         let changed = false;
         const pp = this.getPadPainter();
         if (!this.#options_store || pp?.options._interactively_changed)
            changed = true;
         else {
            for (const k in this.#options_store) {
               if (this.#options[k] !== this.#options_store[k]) {
                  if ((k[0] !== '_') && (k[0] !== '$') && (k[0].toLowerCase() !== k[0]))
                     changed = true;
               }
            }
         }

         if (changed && isFunc(this.#options.asString))
            return this.#options.asString(this.isMainPainter(), ignore_pad ? null : pp?.getRootPad());
      }

      return this.#options.original || ''; // nothing better, return original draw option
   }

   /** @summary Returns array with supported draw options as configured in draw.mjs
     * @desc works via pad painter and only when module was loaded */
   getSupportedDrawOptions() {
      const pp = this.getPadPainter(),
            cl = this.getClassName();

      if (!cl || !isFunc(pp?.getObjectDrawSettings))
         return [];

      return pp.getObjectDrawSettings(getKindForType(cl), 'nosame')?.opts;
   }

   /** @summary Central place to update objects drawing
     * @param {object} obj - new version of object, values will be updated in original object
     * @param {string} [opt] - when specified, new draw options
     * @return {boolean|Promise} for object redraw
     * @desc Two actions typically done by redraw - update object content via {@link ObjectPainter#updateObject} and
      * then redraw correspondent pad via {@link ObjectPainter#redrawPad}. If possible one should redefine
      * only updateObject function and keep this function unchanged. But for some special painters this function is the
      * only way to control how object can be update while requested from the server
      * @protected */
   redrawObject(obj, opt) {
      if (!this.updateObject(obj, opt))
         return false;
      const doc = this.isBatchMode() ? null : getDocument(),
            current = doc?.body.style.cursor;
      if (doc)
         doc.body.style.cursor = 'wait';
      return this.redrawPad().then(res => {
         if (doc)
            doc.body.style.cursor = current;
         return res;
      });
   }

   /** @summary Generic method to update object content.
     * @desc Default implementation just copies first-level members to current object
     * @param {object} obj - object with new data
     * @param {string} [opt] - option which will be used for redrawing
     * @protected */
   updateObject(obj /* , opt */) {
      if (!this.matchObjectType(obj))
         return false;
      Object.assign(this.getObject(), obj);
      return true;
   }

   /** @summary Returns string with object hint
     * @desc It is either item name or object name or class name.
     * Such string typically used as object tooltip.
     * If result string larger than 20 symbols, it will be shorten. */
   getObjectHint() {
      const iname = this.getItemName();
      if (iname)
         return (iname.length > 20) ? '...' + iname.slice(iname.length - 17) : iname;
      return this.getObjectName() || this.getClassName() || '';
   }

   /** @summary Set colors list
    * @protected */
   setColors(lst) { this.#root_colors = lst; }

   /** @summary Return colors list
    * @protected */
   getColors(force) {
      if (!this.#root_colors && force)
         this.setColors(this.getCanvPainter()?.getColors() || getRootColors());
      return this.#root_colors;
   }

   /** @summary returns color from current list of colors
     * @desc First checks canvas painter and then just access global list of colors
     * @param {number} indx - color index
     * @return {string} with SVG color name or rgb()
     * @protected */
   getColor(indx) { return this.getColors(true)[indx]; }

   /** @summary Add color to list of colors
     * @desc Returned color index can be used as color number in all other draw functions
     * @return {number} new color index
     * @protected */
   addColor(color) {
      const lst = this.getColors(true),
            indx = lst.indexOf(color);
      if (indx >= 0)
         return indx;
      lst.push(color);
      return lst.length - 1;
   }

   /** @summary returns tooltip allowed flag
     * @desc If available, checks in canvas painter
     * @private */
   isTooltipAllowed() {
      const src = this.getCanvPainter() || this;
      return src.tooltip_allowed;
   }

   /** @summary change tooltip allowed flag
     * @param {boolean|string} [on = true] set tooltip allowed state or 'toggle'
     * @private */
   setTooltipAllowed(on = true) {
      const src = this.getCanvPainter() || this;
      src.tooltip_allowed = (on === 'toggle') ? !src.tooltip_allowed : on;
   }

   /** @summary Checks if draw elements were resized and drawing should be updated.
     * @desc Redirects to {@link TPadPainter#checkCanvasResize}
     * @private */
   checkResize(arg) {
      return this.getCanvPainter()?.checkCanvasResize(arg);
   }

   /** @summary removes <g> element with object drawing
     * @desc generic method to delete all graphical elements, associated with the painter
     * @protected */
   removeG() {
      this.#draw_g?.remove();
      this.#draw_g = undefined;
   }

   /** @summary Returns created <g> element used for object drawing
     * @desc Element should be created by {@link ObjectPainter#createG}
     * @protected */
   getG() { return this.#draw_g; }

   /** @summary introduce property for backward compatibility */
   get draw_g() { return this.#draw_g; }

   /** @summary Assign G element used for object drawing
     * @protected */
   setG(g) {
      this.#draw_g = g;
      return g;
   }

   /** @summary Append svg::path to G
     * @protected */
   appendPath(d) { return this.#draw_g.append('svg:path').attr('d', d); }

   /** @summary (re)creates svg:g element for object drawings
     * @desc either one attach svg:g to pad primitives (default)
     * or svg:g element created in specified frame layer ('main_layer' will be used when true specified)
     * @param {boolean|string} [frame_layer] - when specified, <g> element will be created inside frame layer, otherwise in the pad
     * @protected */
   createG(frame_layer, use_a = false) {
      let layer;

      const pp = this.getPadPainter();

      if (frame_layer === 'frame2d') {
         const fp = this.getFramePainter();
         frame_layer = fp && !fp.mode3d;
      }

      if (frame_layer) {
         const frame = pp.getFrameSvg();
         if (frame.empty()) {
            console.error('Not found frame to create g element inside');
            return frame;
         }
         if (!isStr(frame_layer))
            frame_layer = 'main_layer';
         layer = frame.selectChild('.' + frame_layer);
      } else
         layer = pp.getLayerSvg('primitives_layer');

      if (this.#draw_g && this.#draw_g.node().parentNode !== layer.node()) {
         console.log('g element changes its layer!');
         this.removeG();
      }

      if (this.#draw_g) {
         // clear all elements, keep g element on its place
         this.#draw_g.selectAll('*').remove();
      } else {
         this.#draw_g = layer.append(use_a ? 'svg:a' : 'svg:g');

         if (!frame_layer)
            layer.selectChildren('.most_upper_primitives').raise();
      }

      // set attributes for debugging, both should be there for opt out them later
      const clname = this.getClassName(), objname = this.getObjectName();
      if (objname || clname) {
         this.#draw_g.attr('objname', (objname || 'name').replace(/[^\w]/g, '_'))
                     .attr('objtype', (clname || 'type').replace(/[^\w]/g, '_'));
      }

      this.#draw_g.property('in_frame', Boolean(frame_layer)); // indicates coordinate system

      return this.#draw_g;
   }

   /** @summary Bring draw element to the front */
   bringToFront(check_online) {
      if (!this.#draw_g)
         return;
      const prnt = this.#draw_g.node().parentNode;
      prnt?.appendChild(this.#draw_g.node());

      if (check_online && this.getSnapId()) {
         const pp = this.getPadPainter();
         if (pp?.getSnapId())
            this.getCanvPainter()?.sendWebsocket('POPOBJ:' + JSON.stringify([pp.getSnapId(), this.getSnapId()]));
      }
   }

   /** @summary Assign is_primary flag
     * @private */
   setPrimary(flag = true) { this.#is_primary = flag; }

   /** @summary Return is_primary flag
     * @private */
   isPrimary() { return this.#is_primary; }

   /** @summary Assign secondary id
     * @private */
   setSecondaryId(primary, name) {
      primary.setPrimary(true); // mark as primary, used later
      this.#primary_ref = new WeakRef(primary);
      this.#secondary_id = name;
   }

   /** @summary Returns secondary id
     * @private */
   getSecondaryId() { return this.#secondary_id; }

   /** @summary Check if this is secondary painter
     * @desc if primary painter provided - check if this really main for this
     * @private */
   isSecondary(primary) {
      if (!this.#primary_ref)
         return false;
      return !isObject(primary) ? true : this.#primary_ref.deref() === primary;
   }

   /** @summary Return primary object
     * @private */
   getPrimary() { return this.#primary_ref?.deref(); }

   /** @summary Canvas main svg element
     * @return {object} d3 selection with canvas svg
     * @protected */
   getCanvSvg() { return this.selectDom().select('.root_canvas'); }

   /** @summary Method selects current pad name
     * @param {string} [new_name] - when specified, new current pad name will be configured
     * @return {string} previous selected pad or actual pad when new_name not specified
     * @private
     * @deprecated to be removed in v8 */
   selectCurrentPad() {
      console.warn('selectCurrentPad is deprecated, will be removed in v8');
      return '';
   }

   /** @summary Return functor, which can convert x and y coordinates into pixels, used for drawing in the pad
     * @desc X and Y coordinates can be converted by calling func.x(x) and func.y(y)
     * Only can be used for painting in the pad, means CreateG() should be called without arguments
     * @param {boolean} isndc - if NDC coordinates will be used
     * @param {boolean} [noround] - if set, return coordinates will not be rounded
     * @param {boolean} [use_frame_coordinates] - use frame coordinates even when drawing on the pad
     * @protected */
   getAxisToSvgFunc(isndc, nornd, use_frame_coordinates) {
      const func = { isndc, nornd },
            use_frame = this.getG()?.property('in_frame');
      if (use_frame || (use_frame_coordinates && !isndc))
         func.fp = this.getFramePainter();
      if (func.fp?.grx && func.fp?.gry) {
         func.x0 = (use_frame_coordinates && !isndc) ? func.fp.getFrameX() : 0;
         func.y0 = (use_frame_coordinates && !isndc) ? func.fp.getFrameY() : 0;
         if (nornd) {
            func.x = function(x) { return this.x0 + this.fp.grx(x); };
            func.y = function(y) { return this.y0 + this.fp.gry(y); };
         } else {
            func.x = function(x) { return this.x0 + Math.round(this.fp.grx(x)); };
            func.y = function(y) { return this.y0 + Math.round(this.fp.gry(y)); };
         }
      } else if (!use_frame) {
         const pp = this.getPadPainter();
         func.pad = isndc ? null : pp?.getRootPad(true); // need for NDC conversion
         func.padw = pp?.getPadWidth() ?? 10;
         func.x = function(value) {
            if (this.pad) {
               if (this.pad.fLogx)
                  value = (value > 0) ? Math.log10(value) : this.pad.fUxmin;
               value = (value - this.pad.fX1) / (this.pad.fX2 - this.pad.fX1);
            }
            value *= this.padw;
            return this.nornd ? value : Math.round(value);
         };
         func.padh = pp?.getPadHeight() ?? 10;
         func.y = function(value) {
            if (this.pad) {
               if (this.pad.fLogy)
                  value = (value > 0) ? Math.log10(value) : this.pad.fUymin;
               value = (value - this.pad.fY1) / (this.pad.fY2 - this.pad.fY1);
            }
            value = (1 - value) * this.padh;
            return this.nornd ? value : Math.round(value);
         };
      } else {
         console.error(`Problem to create functor for ${this.getClassName()}`);
         func.x = () => 0;
         func.y = () => 0;
      }
      return func;
   }

   /** @summary Converts x or y coordinate into pad SVG coordinates.
     * @desc Only can be used for painting in the pad, means CreateG() should be called without arguments
     * @param {string} axis - name like 'x' or 'y'
     * @param {number} value - axis value to convert.
     * @param {boolean} ndc - is value in NDC coordinates
     * @param {boolean} [noround] - skip rounding
     * @return {number} value of requested coordinates
     * @protected */
   axisToSvg(axis, value, ndc, noround) {
      const func = this.getAxisToSvgFunc(ndc, noround);
      return func[axis](value);
   }

   /** @summary Converts pad SVG x or y coordinates into axis values.
     * @desc Reverse transformation for {@link ObjectPainter#axisToSvg}
     * @param {string} axis - name like 'x' or 'y'
     * @param {number} coord - graphics coordinate.
     * @param {boolean} ndc - kind of return value
     * @return {number} value of requested coordinates
     * @protected */
   svgToAxis(axis, coord, ndc) {
      const use_frame = this.getG()?.property('in_frame');

      if (use_frame)
         return this.getFramePainter()?.revertAxis(axis, coord) ?? 0;

      const pp = this.getPadPainter(),
            pad = (ndc || !pp) ? null : pp.getRootPad(true);
      let value = !pp ? 0 : ((axis === 'y') ? (1 - coord / pp.getPadHeight()) : coord / pp.getPadWidth());

      if (pad) {
         if (axis === 'y') {
            value = pad.fY1 + value * (pad.fY2 - pad.fY1);
            if (pad.fLogy)
               value = Math.pow(10, value);
         } else {
            value = pad.fX1 + value * (pad.fX2 - pad.fX1);
            if (pad.fLogx)
               value = Math.pow(10, value);
         }
      }

      return value;
   }

   /** @summary Returns frame painter for current pad
     * @desc Pad has direct reference on frame if any
     * @protected */
   getFramePainter() {
      return this.getPadPainter()?.getFramePainter();
   }

   /** @summary Returns painter for main object on the pad.
     * @desc Typically it is first histogram drawn on the pad and which draws frame axes
     * But it also can be special use-case as TASImage or TGraphPolargram
     * @param {boolean} [not_store] - if true, prevent temporary storage of main painter reference
     * @protected */
   getMainPainter(not_store) {
      let res = this.#main_painter?.deref();
      if (!res) {
         const pp = this.getPadPainter();
         res = pp ? pp.getMainPainter() : this.getTopPainter();
         this.#main_painter = not_store || !res ? null : new WeakRef(res);
      }
      return res || null;
   }

   /** @summary Returns true if this is main painter
     * @protected */
   isMainPainter() { return this === this.getMainPainter(); }

   /** @summary Assign this as main painter on the pad
     * @desc Main painter typically responsible for axes drawing
     * Should not be used by pad/canvas painters, but rather by objects which are drawing axis
     * @protected */
   setAsMainPainter(force) {
      const pp = this.getPadPainter();
      if (!pp)
         this.setTopPainter(); // fallback on BasePainter method
      else
         pp.setMainPainter(this, force);
   }

   /** @summary Add painter to pad list of painters
     * @desc Normally called from {@link ensureTCanvas} function when new painter is created
     * @protected */
   addToPadPrimitives(pad_painter) {
      if (this.#pad_painter_ref)
         pad_painter = this.#pad_painter_ref.deref();
      else {
         if (!pad_painter)
            pad_painter = getDomCanvasPainter(this.selectDom()); // try to detect in DOM
         if (pad_painter)
            this.#pad_painter_ref = new WeakRef(pad_painter);
      }

      if (!pad_painter || (pad_painter === this))
         return null;

      return pad_painter.addToPrimitives(this);
   }

   /** @summary Remove painter from pad list of painters
     * @desc Can be used from external frameworks to add/remove painters
     * @protected */
   removeFromPadPrimitives() { this.getPadPainter()?.removePrimitive(this); }

   /** @summary Creates marker attributes object
     * @desc Can be used to produce markers in painter.
     * See {@link TAttMarkerHandler} for more info.
     * Instance assigned as this.markeratt data member, recognized by GED editor
     * @param {object} args - either TAttMarker or see arguments of {@link TAttMarkerHandler}
     * @return {object} created handler
     * @protected */
   createAttMarker(args) {
      if (args === undefined)
         args = { attr: this.getObject() };
      else if (!isObject(args))
         args = { std: true };
      else if (args.fMarkerColor !== undefined && args.fMarkerStyle !== undefined && args.fMarkerSize !== undefined)
         args = { attr: args, std: false };

      if (args.std === undefined)
         args.std = true;
      if (args.painter === undefined)
         args.painter = this;

      let handler = args.std ? this.#markeratt : null;

      if (!handler)
         handler = new TAttMarkerHandler(args);
      else if (!handler.changed || args.force)
         handler.setArgs(args);

      if (args.std)
         this.#markeratt = handler;
      return handler;
   }

   /** @summary Creates line attributes object.
     * @desc Can be used to produce lines in painter.
     * See {@link TAttLineHandler} for more info.
     * Instance assigned as this.lineatt data member, recognized by GED editor
     * @param {object} args - either TAttLine or see constructor arguments of {@link TAttLineHandler}
     * @protected */
   createAttLine(args) {
      if (args === undefined)
         args = { attr: this.getObject() };
      else if (!isObject(args))
         args = { std: true };
      else if (args.fLineColor !== undefined && args.fLineStyle !== undefined && args.fLineWidth !== undefined)
         args = { attr: args, std: false };

      if (args.std === undefined)
         args.std = true;
      if (args.painter === undefined)
         args.painter = this;

      let handler = args.std ? this.#lineatt : null;

      if (!handler)
         handler = new TAttLineHandler(args);
      else if (!handler.changed || args.force)
         handler.setArgs(args);

      if (args.std)
         this.#lineatt = handler;
      return handler;
   }

   /** @summary Creates text attributes object.
     * @param {object} args - either TAttText or see constructor arguments of {@link TAttTextHandler}
     * @protected */
   createAttText(args) {
      if (args === undefined)
         args = { attr: this.getObject() };
      else if (!isObject(args))
         args = { std: true };
      else if (args.fTextFont !== undefined && args.fTextSize !== undefined && args.fTextColor !== undefined)
         args = { attr: args, std: false };

      if (args.std === undefined)
         args.std = true;
      if (args.painter === undefined)
         args.painter = this;

      let handler = args.std ? this.#textatt : null;

      if (!handler)
         handler = new TAttTextHandler(args);
      else if (!handler.changed || args.force)
         handler.setArgs(args);

      if (args.std)
         this.#textatt = handler;
      return handler;
   }

   /** @summary Creates fill attributes object.
     * @desc Method dedicated to create fill attributes, bound to canvas SVG
     * otherwise newly created patters will not be usable in the canvas
     * See {@link TAttFillHandler} for more info.
     * Instance assigned as this.fillatt data member, recognized by GED editors
     * @param {object} [args] - for special cases one can specify TAttFill as args or number of parameters
     * @param {boolean} [args.std = true] - this is standard fill attribute for object and should be used as this.fillatt
     * @param {object} [args.attr = null] - object, derived from TAttFill
     * @param {number} [args.pattern = undefined] - integer index of fill pattern
     * @param {number} [args.color = undefined] - integer index of fill color
     * @param {string} [args.color_as_svg = undefined] - color will be specified as SVG string, not as index from color palette
     * @param {number} [args.kind = undefined] - some special kind which is handled differently from normal patterns
     * @return created handle
     * @protected */
   createAttFill(args) {
      if (args === undefined)
         args = { attr: this.getObject() };
      else if (!isObject(args))
         args = { std: true };
      else if (args._typename && args.fFillColor !== undefined && args.fFillStyle !== undefined)
         args = { attr: args, std: false };

      if (args.std === undefined)
         args.std = true;
      if (args.painter === undefined)
         args.painter = this;

      let handler = args.std ? this.#fillatt : null;

      if (!args.svg)
         args.svg = this.getCanvSvg();

      if (!handler)
         handler = new TAttFillHandler(args);
      else if (!handler.changed || args.force)
         handler.setArgs(args);

      if (args.std)
         this.#fillatt = handler;
      return handler;
   }

   get fillatt() { return this.#fillatt; }
   get lineatt() { return this.#lineatt; }
   get markeratt() { return this.#markeratt; }
   get textatt() { return this.#textatt; }

   /** @summary call function for each painter in the pad
     * @desc Iterate over all known painters
     * @private */
   forEachPainter(userfunc, kind) {
      // iterate over all painters from pad list
      let pp = this.getPadPainter(), top = null;
      if (!pp) {
         top = this.getTopPainter();
         if (isPadPainter(top))
            pp = top;
      }
      if (pp)
         pp.forEachPainterInPad(userfunc, kind);
      else if (top && (kind !== 'pads'))
         userfunc(top);
   }

   /** @summary indicate that redraw was invoked via interactive action (like context menu or zooming)
     * @desc Use to catch such action by GED and by server-side
     * @return {Promise} when completed
     * @private */
   async interactiveRedraw(arg, info, subelem) {
      let reason, res;
      if (isStr(info) && info.indexOf('exec:'))
         reason = info;

      if (arg === 'pad')
         res = this.redrawPad(reason);
      else if (arg !== false)
         res = this.redraw(reason);

      return getPromise(res).then(() => {
         if (arg === 'attribute')
            return this.getPadPainter()?.redrawLegend();
      }).then(() => {
         // inform GED that something changes
         const canp = this.getCanvPainter();

         if (isFunc(canp?.producePadEvent))
            canp.producePadEvent('redraw', this.getPadPainter(), this, null, subelem);

         // inform server that draw options changes
         if (isFunc(canp?.processChanges))
            canp.processChanges(info, this, subelem);

         return this;
      });
   }

   /** @summary Redraw all objects in the current pad
     * @param {string} [reason] - like 'resize' or 'zoom'
     * @return {Promise} when pad redraw completed
     * @protected */
   async redrawPad(reason) {
      return this.getPadPainter()?.redrawPad(reason) ?? false;
   }

   /** @summary execute selected menu command, either locally or remotely
     * @private */
   executeMenuCommand(method) {
      if (method.fName === 'Inspect')
         // primitive inspector, keep it here
         return this.showInspector();

      return false;
   }

   /** @summary Invoke method for object via WebCanvas functionality
     * @desc Requires that painter marked with object identifier (this.#snapid) or identifier provided as second argument
     * Canvas painter should exists and in non-readonly mode
     * Execution string can look like 'Print()'.
     * Many methods call can be chained with 'Print();;Update();;Clear()'
     * @private */
   submitCanvExec(exec, snapid) {
      if (!exec || !isStr(exec))
         return;

      const canp = this.getCanvPainter();
      if (isFunc(canp?.submitExec))
         canp.submitExec(this, exec, snapid);
   }

   /** @summary remove all created draw attributes
     * @protected */
   deleteAttr(name) {
      if (!name || name === 'line')
         this.#lineatt = undefined;
      if (!name || name === 'fill')
         this.#fillatt = undefined;
      if (!name || name === 'marker')
         this.#markeratt = undefined;
      if (!name || name === 'text')
         this.#textatt = undefined;
   }

   /** @summary Show object in inspector for provided object
     * @protected */
   showInspector(/* opt */) {
      return false;
   }

   /** @summary Fill context menu for the object
     * @private */
   fillContextMenu(menu) {
      const cl = this.getClassName(),
            name = this.getObjectName(),
            p = cl.lastIndexOf('::'),
            cl0 = (p > 0) ? cl.slice(p + 2) : cl,
            hdr = (cl0 && name) ? `${cl0}:${name}` : (cl0 || name || 'object'),
            url = cl ? `${urlClassPrefix}${cl.replaceAll('::', '_1_1')}.html` : '';

      menu.header(hdr, url);

      const size0 = menu.size();

      if (isFunc(this.fillContextMenuItems))
         this.fillContextMenuItems(menu);

      if ((menu.size() > size0) && this.showInspector('check'))
         menu.add('Inspect', this.showInspector);

      menu.addAttributesMenu(this);

      return menu.size() > size0;
   }

   /** @summary shows objects status
     * @desc Either used canvas painter method or globally assigned
     * When no parameters are specified, just basic object properties are shown
     * @private */
   showObjectStatus(name, title, info, info2) {
      let cp = this.getCanvPainter();

      if (!isFunc(cp?.showCanvasStatus))
         cp = null;

      if (!cp && !isFunc(internals.showStatus))
         return false;

      if (this.enlargeMain('state') === 'on')
         return false;

      if ((name === undefined) && (title === undefined)) {
         const obj = this.getObject();
         if (!obj)
            return;
         name = this.getItemName() || obj.fName;
         title = obj.fTitle || obj._typename;
         info = obj._typename;
      }

      if (cp)
         cp.showCanvasStatus(name, title, info, info2);
      else
         internals.showStatus(name, title, info, info2);
   }

   /** @summary Redraw object
     * @desc Basic method, should be reimplemented in all derived objects
     * for the case when drawing should be repeated
     * @abstract
     * @protected */
   redraw(/* reason */) {}

   /** @summary Start text drawing
     * @desc required before any text can be drawn
     * @param {number} font_face - font id as used in ROOT font attributes
     * @param {number} font_size - font size as used in ROOT font attributes
     * @param {object} [draw_g] - element where text drawn, by default using main object <g> element
     * @param {number} [max_font_size] - maximal font size, used when text can be scaled
     * @protected */
   startTextDrawing(font_face, font_size, draw_g, max_font_size, can_async) {
      if (!draw_g)
         draw_g = this.getG();
      if (!draw_g || draw_g.empty())
         return false;

      const font = (font_size === 'font') ? font_face : new FontHandler(font_face, font_size);
      if (can_async && font.needLoad())
         return font;

      font.setPainter(this); // may be required when custom font is used

      draw_g.call(font.func);

      draw_g.property('draw_text_completed', false) // indicate that draw operations submitted
            .property('all_args', []) // array of all submitted args, makes easier to analyze them
            .property('text_font', font)
            .property('text_factor', 0)
            .property('max_text_width', 0) // keep maximal text width, use it later
            .property('max_font_size', max_font_size)
            .property('_fast_drawing', this.getPadPainter()?.isFastDrawing() ?? false);

      if (draw_g.property('_fast_drawing'))
         draw_g.property('_font_too_small', (max_font_size && (max_font_size < 5)) || (font.size < 4));

      return true;
   }

   /** @summary Start async text drawing
    * @return {Promise} for loading of font if necessary
    * @private */
   async startTextDrawingAsync(font_face, font_size, draw_g, max_font_size) {
      const font = this.startTextDrawing(font_face, font_size, draw_g, max_font_size, true);
      if ((font === true) || (font === false))
         return font;
      return font.load().then(res => {
         if (!res)
            return false;

         return this.startTextDrawing(font, 'font', draw_g, max_font_size);
      });
   }

   /** @summary Apply scaling factor to all drawn text in the <g> element
     * @desc Can be applied at any time before finishTextDrawing is called - even in the postprocess callbacks of text draw
     * @param {number} factor - scaling factor
     * @param {object} [draw_g] - drawing element for the text
     * @protected */
   scaleTextDrawing(factor, draw_g) {
      if (!draw_g)
         draw_g = this.getG();
      if (!draw_g || draw_g.empty())
         return;
      if (factor && (factor > draw_g.property('text_factor')))
         draw_g.property('text_factor', factor);
   }

   /** @summary Analyze if all text draw operations are completed
     * @private */
   #checkAllTextDrawing(draw_g, resolveFunc, try_optimize) {
      const all_args = draw_g.property('all_args') || [];
      let missing = 0;

      all_args.forEach(arg => {
         if (!arg.ready)
            missing++;
      });

      if (missing > 0) {
         if (isFunc(resolveFunc)) {
            draw_g.node().textResolveFunc = resolveFunc;
            draw_g.node().try_optimize = try_optimize;
         }
         return;
      }

      draw_g.property('all_args', null); // clear all_args property

      // adjust font size (if there are normal text)
      const f = draw_g.property('text_factor'),
            font = draw_g.property('text_font'),
            max_sz = draw_g.property('max_font_size');
      let font_size = font.size, any_text = false, only_text = true;

      if ((f > 0) && ((f < 0.95) || (f > 1.05)))
         font.size = Math.max(1, Math.floor(font.size / f));

      if (max_sz && (font.size > max_sz))
         font.size = max_sz;

      if (font.size !== font_size) {
         draw_g.call(font.func);
         font_size = font.size;
      }

      all_args.forEach(arg => {
         if (arg.mj_node && arg.mj_func) {
            const svg = arg.mj_node.select('svg'); // MathJax svg
            arg.mj_func(this, arg.mj_node, svg, arg, font_size, f);
            delete arg.mj_node; // remove reference
            only_text = false;
         } else if (arg.txt_g)
            only_text = false;
      });

      if (!resolveFunc) {
         resolveFunc = draw_g.node().textResolveFunc;
         try_optimize = draw_g.node().try_optimize;
         delete draw_g.node().textResolveFunc;
         delete draw_g.node().try_optimize;
      }

      const optimize_arr = (try_optimize && only_text) ? [] : null;

      // now process text and latex drawings
      all_args.forEach(arg => {
         let txt, is_txt, scale = 1;
         if (arg.txt_node) {
            txt = arg.txt_node;
            delete arg.txt_node;
            is_txt = true;
            if (optimize_arr !== null)
               optimize_arr.push(txt);
         } else if (arg.txt_g) {
            txt = arg.txt_g;
            delete arg.txt_g;
            is_txt = false;
         } else
            return;

         txt.attr('visibility', null);

         any_text = true;

         if (arg.width) {
            // adjust x position when scale into specified rectangle
            if (arg.align[0] === 'middle')
               arg.x += arg.width / 2;
            else if (arg.align[0] === 'end')
               arg.x += arg.width;
         }

         if (arg.height) {
            if (arg.align[1].indexOf('bottom') === 0)
               arg.y += arg.height;
            else if (arg.align[1] === 'middle')
               arg.y += arg.height / 2;
         }

         let dx = 0, dy = 0;

         if (is_txt) {
            // handle simple text drawing

            if (isNodeJs()) {
               if (arg.scale && (f > 0)) {
                  arg.box.width *= 1 / f;
                  arg.box.height *= 1 / f;
               }
            } else if (!arg.plain && !arg.fast) {
               // exact box dimension only required when complex text was build
               arg.box = getElementRect(txt, 'bbox');
            }

            if (arg.plain) {
               txt.attr('text-anchor', arg.align[0]);
               if (arg.align[1] === 'top')
                  txt.attr('dy', '.8em');
               else if (arg.align[1] === 'middle') {
                  // if (isNodeJs()) txt.attr('dy', '.4em'); else // old workaround for node.js
                  txt.attr('dominant-baseline', 'middle');
               }
            } else {
               txt.attr('text-anchor', 'start');
               dx = ((arg.align[0] === 'middle') ? -0.5 : ((arg.align[0] === 'end') ? -1 : 0)) * arg.box.width;
               dy = ((arg.align[1] === 'top') ? (arg.top_shift || 1) : (arg.align[1] === 'middle') ? (arg.mid_shift || 0.5) : 0) * arg.box.height;
            }
         } else if (arg.text_rect) {
            // handle latex drawing
            const box = arg.text_rect;

            scale = (f > 0) && (Math.abs(1 - f) > 0.01) ? 1 / f : 1;

            dx = ((arg.align[0] === 'middle') ? -0.5 : ((arg.align[0] === 'end') ? -1 : 0)) * box.width * scale;

            if (arg.align[1] === 'top')
               dy = -box.y1 * scale;
            else if (arg.align[1] === 'bottom')
               dy = -box.y2 * scale;
            else if (arg.align[1] === 'middle')
               dy = -0.5 * (box.y1 + box.y2) * scale;
         } else
            console.error('text rect not calcualted - please check code');

         if (!arg.rotate) {
            arg.x += dx;
            arg.y += dy;
            dx = dy = 0;
         }

         // use translate and then rotate to avoid complex sign calculations
         let trans = makeTranslate(Math.round(arg.x), Math.round(arg.y)) || '';
         const dtrans = makeTranslate(Math.round(dx), Math.round(dy)),
               append = aaa => {
                  if (trans)
                     trans += ' ';
                  trans += aaa;
               };

         if (arg.rotate)
            append(`rotate(${Math.round(arg.rotate)})`);
         if (scale !== 1)
            append(`scale(${scale.toFixed(3)})`);
         if (dtrans)
            append(dtrans);
         if (trans)
            txt.attr('transform', trans);
      });


      // when no any normal text drawn - remove font attributes
      if (!any_text)
         font.clearFont(draw_g);

      if ((optimize_arr !== null) && (optimize_arr.length > 1)) {
         ['fill', 'text-anchor'].forEach(name => {
            let first = optimize_arr[0].attr(name);
            optimize_arr.forEach(txt_node => {
               const value = txt_node.attr(name);
               if (!value || (value !== first))
                  first = undefined;
            });
            if (first) {
               draw_g.attr(name, first);
               optimize_arr.forEach(txt_node => { txt_node.attr(name, null); });
            }
         });
      }

      // if specified, call resolve function
      if (resolveFunc)
         resolveFunc(this); // IMPORTANT - return painter, may use in draw methods
   }

   /** @summary Post-process plain text drawing
     * @private */
   #postprocessDrawText(arg, txt_node) {
      // complete rectangle with very rough size estimations
      arg.box = !isNodeJs() && !settings.ApproxTextSize && !arg.fast
                 ? getElementRect(txt_node, 'bbox')
                 : (arg.text_rect || { height: Math.round(1.15 * arg.font_size), width: approximateLabelWidth(arg.text, arg.font, arg.font_size) });

      txt_node.attr('visibility', 'hidden'); // hide elements until text drawing is finished

      if (arg.box.width > arg.draw_g.property('max_text_width'))
         arg.draw_g.property('max_text_width', arg.box.width);
      if (arg.scale)
         this.scaleTextDrawing(Math.max(1.05 * arg.box.width / arg.width, arg.box.height / arg.height), arg.draw_g);

      arg.result_width = arg.box.width;
      arg.result_height = arg.box.height;

      if (isFunc(arg.post_process))
         arg.post_process(this);

      return arg.box.width;
   }

   /** @summary Draw text
     * @desc The only legal way to draw text, support plain, latex and math text output
     * @param {object} arg - different text draw options
     * @param {string} arg.text - text to draw
     * @param {number} [arg.align = 12] - int value like 12 or 31
     * @param {string} [arg.align = undefined] - end;bottom
     * @param {number} [arg.x = 0] - x position
     * @param {number} [arg.y = 0] - y position
     * @param {number} [arg.width] - when specified, adjust font size in the specified box
     * @param {number} [arg.height] - when specified, adjust font size in the specified box
     * @param {boolean} [arg.scale = true] - scale into draw box when width and height parameters are specified
     * @param {number} [arg.latex] - 0 - plain text, 1 - normal TLatex, 2 - math
     * @param {string} [arg.color=black] - text color
     * @param {number} [arg.rotate] - rotation angle
     * @param {number} [arg.font_size] - fixed font size
     * @param {object} [arg.draw_g] - element where to place text, if not specified central draw_g container is used
     * @param {function} [arg.post_process] - optional function called when specified text is drawn
     * @protected */
   drawText(arg) {
      if (!arg.text)
         arg.text = '';

      arg.draw_g = arg.draw_g || this.getG();
      if (!arg.draw_g || arg.draw_g.empty())
         return;

      const font = arg.draw_g.property('text_font');
      arg.font = font; // use in latex conversion

      if (font) {
         arg.color = arg.color || font.color;
         arg.align = arg.align || font.align;
         arg.rotate = arg.rotate || font.angle;
      }

      let align = ['start', 'middle'];

      if (isStr(arg.align)) {
         align = arg.align.split(';');
         if (align.length === 1)
            align.push('middle');
      } else if (typeof arg.align === 'number') {
         if ((arg.align / 10) >= 3)
            align[0] = 'end';
         else if ((arg.align / 10) >= 2)
            align[0] = 'middle';
         if ((arg.align % 10) === 0)
            align[1] = 'bottom';
         else if ((arg.align % 10) === 1)
            align[1] = 'bottom-base';
         else if ((arg.align % 10) === 3)
            align[1] = 'top';
      } else if (isObject(arg.align) && (arg.align.length === 2))
         align = arg.align;

      if (arg.latex === undefined)
         arg.latex = 1; //  0: text, 1: latex, 2: math
      arg.align = align;
      arg.x = arg.x || 0;
      arg.y = arg.y || 0;
      if (arg.scale !== false)
         arg.scale = arg.width && arg.height && !arg.font_size;
      arg.width = arg.width || 0;
      arg.height = arg.height || 0;

      if (arg.draw_g.property('_fast_drawing')) {
         if (arg.scale) {
            // area too small - ignore such drawing
            if (arg.height < 4)
               return 0;
         } else if (arg.font_size) {
            // font size too small
            if (arg.font_size < 4)
               return 0;
         } else if (arg.draw_g.property('_font_too_small')) {
            // configure font is too small - ignore drawing
            return 0;
         }
      }

      // include drawing into list of all args
      arg.draw_g.property('all_args').push(arg);
      arg.ready = false; // indicates if drawing is ready for post-processing

      let use_mathjax = (arg.latex === 2);
      const cl = constants$1.Latex;

      if (arg.latex === 1) {
         use_mathjax = (settings.Latex === cl.AlwaysMathJax) ||
                       ((settings.Latex === cl.MathJax) && arg.text.match(/[#{\\]/g)) ||
                       arg.text.match(/[\\]/g);
      }

      if (!use_mathjax || arg.nomathjax) {
         arg.txt_node = arg.draw_g.append('svg:text');

         if (arg.color)
            arg.txt_node.attr('fill', arg.color);

         if (arg.font_size)
            arg.txt_node.attr('font-size', arg.font_size);
         else
            arg.font_size = font.size;

         arg.plain = !arg.latex || (settings.Latex === cl.Off) || (settings.Latex === cl.Symbols);

         arg.simple_latex = arg.latex && (settings.Latex === cl.Symbols);

         if (!arg.plain || arg.simple_latex || arg.font?.isSymbol) {
            if (arg.simple_latex || isPlainText(arg.text) || arg.plain) {
               arg.simple_latex = true;
               producePlainText(this, arg.txt_node, arg);
            } else {
               arg.txt_node.remove(); // just remove text node
               delete arg.txt_node;
               arg.txt_g = arg.draw_g.append('svg:g');
               produceLatex(this, arg.txt_g, arg);
            }
            arg.ready = true;
            this.#postprocessDrawText(arg, arg.txt_g || arg.txt_node);

            if (arg.draw_g.property('draw_text_completed'))
               this.#checkAllTextDrawing(arg.draw_g); // check if all other elements are completed
            return 0;
         }

         arg.plain = true;
         arg.txt_node.text(arg.text);
         arg.ready = true;

         return this.#postprocessDrawText(arg, arg.txt_node);
      }

      arg.mj_node = arg.draw_g.append('svg:g').attr('visibility', 'hidden'); // hide text until drawing is finished

      produceMathjax(this, arg.mj_node, arg).then(() => {
         arg.ready = true;
         if (arg.draw_g.property('draw_text_completed'))
            this.#checkAllTextDrawing(arg.draw_g);
      });

      return 0;
   }

   /** @summary Finish text drawing
     * @desc Should be called to complete all text drawing operations
     * @param {function} [draw_g] - <g> element for text drawing, default is getG()
     * @return {Promise} when text drawing completed
     * @protected */
   async finishTextDrawing(draw_g, try_optimize) {
      if (!draw_g)
         draw_g = this.getG();
      if (!draw_g || draw_g.empty())
         return false;

      draw_g.property('draw_text_completed', true); // mark that text drawing is completed

      return new Promise(resolveFunc => {
         this.#checkAllTextDrawing(draw_g, resolveFunc, try_optimize);
      });
   }

   /** @summary Configure user-defined context menu for the object
     * @desc fillmenu_func will be called when context menu is activated
     * Arguments fillmenu_func are (menu,kind)
     * First is menu object, second is object sub-element like axis 'x' or 'y'
     * Function should return promise with menu when items are filled
     * @param {function} fillmenu_func - function to fill custom context menu for object */
   configureUserContextMenu(fillmenu_func) {
      this.#user_context_menu = isFunc(fillmenu_func) ? fillmenu_func : undefined;
   }

   /** @summary Fill object menu in web canvas
     * @private */
   async fillObjectExecMenu(menu, kind) {
      if (isFunc(this.#user_context_menu))
         return this.#user_context_menu(menu, kind);

      const canvp = this.getCanvPainter();

      if (!this.getSnapId() || !canvp || canvp?.isReadonly() || !canvp?.getWebsocket())
         return menu;

      function doExecMenu(arg) {
         const execp = menu.exec_painter || this,
               cp = execp.getCanvPainter(),
               item = menu.exec_items[parseInt(arg)];

         if (!item?.fName)
            return;

         // this is special entry, produced by TWebMenuItem, which recognizes editor entries itself
         if (item.fExec === 'Show:Editor') {
            if (isFunc(cp?.activateGed))
               cp.activateGed(execp);
            return;
         }

         if (isFunc(cp?.executeObjectMethod) && cp.executeObjectMethod(execp, item, item.$execid))
            return;

         item.fClassName = execp.getClassName();
         if ((item.$execid.indexOf('#x') > 0) || (item.$execid.indexOf('#y') > 0) || (item.$execid.indexOf('#z') > 0))
            item.fClassName = clTAxis;

         if (execp.executeMenuCommand(item))
            return;

         if (!item.$execid)
            return;

         if (!item.fArgs) {
            return cp?.v7canvas ? cp.submitExec(execp, item.fExec, kind)
                                : execp.submitCanvExec(item.fExec, item.$execid);
         }

         menu.showMethodArgsDialog(item).then(args => {
            if (!args || execp.executeMenuCommand(item, args))
               return;

            const exec = item.fExec.slice(0, item.fExec.length - 1) + args + ')';
            if (cp?.v7canvas)
               cp.submitExec(execp, exec, kind);
            else
               cp?.sendWebsocket(`OBJEXEC:${item.$execid}:${exec}`);
         });
      }

      const doFillMenu = (_menu, _reqid, _resolveFunc, reply) => {
         // avoid multiple call of the callback after timeout
         if (menu._got_menu)
            return;
         menu._got_menu = true;

         if (reply && (_reqid !== reply.fId))
            console.error(`missmatch between request ${_reqid} and reply ${reply.fId} identifiers`);

         menu.exec_items = reply?.fItems;

         if (menu.exec_items?.length) {
            if (_menu.size() > 0)
               _menu.separator();

            let lastclname;

            for (let n = 0; n < menu.exec_items.length; ++n) {
               const item = menu.exec_items[n];
               item.$execid = reply.fId;
               item.$menu = menu;

               if (item.fClassName && lastclname && (lastclname !== item.fClassName)) {
                  _menu.endsub();
                  lastclname = '';
               }
               if (lastclname !== item.fClassName) {
                  lastclname = item.fClassName;
                  const p = lastclname.lastIndexOf('::'),
                        shortname = (p > 0) ? lastclname.slice(p + 2) : lastclname;

                  _menu.sub(shortname.replace(/[<>]/g, '_'));
               }

               if ((item.fChecked === undefined) || (item.fChecked < 0))
                  _menu.add(item.fName, n, doExecMenu);
               else
                  _menu.addchk(item.fChecked, item.fName, n, doExecMenu);
            }

            if (lastclname)
               _menu.endsub();
         }

         _resolveFunc(_menu);
      },
            reqid = this.getSnapId(kind);

      menu._got_menu = false;

      // if menu painter differs from this, remember it for further usage
      if (menu.painter)
         menu.exec_painter = (menu.painter !== this) ? this : undefined;

      return new Promise(resolveFunc => {
         let did_resolve = false;

         function handleResolve(res) {
            if (did_resolve)
               return;
            did_resolve = true;
            resolveFunc(res);
         }

         // set timeout to avoid menu hanging
         setTimeout(() => doFillMenu(menu, reqid, handleResolve), 2000);

         canvp.submitMenuRequest(this, kind, reqid).then(lst => doFillMenu(menu, reqid, handleResolve, lst));
      });
   }

   /** @summary Configure user-defined tooltip handler
     * @desc Hook for the users to get tooltip information when mouse cursor moves over frame area
     * Handler function will be called every time when new data is selected
     * when mouse leave frame area, handler(null) will be called
     * @param {function} handler - function called when tooltip is produced
     * @param {number} [tmout = 100] - delay in ms before tooltip delivered */
   configureUserTooltipHandler(handler, tmout = 100) {
      if (!handler || !isFunc(handler)) {
         this.#user_tooltip_handler = undefined;
         this.#user_tooltip_timeout = undefined;
      } else {
         this.#user_tooltip_handler = handler;
         this.#user_tooltip_timeout = tmout;
      }
   }

   /** @summary Configure user-defined click handler
      * @desc Function will be called every time when frame click was performed
      * As argument, tooltip object with selected bins will be provided
      * If handler function returns true, default handling of click will be disabled
      * @param {function} handler - function called when mouse click is done */
   configureUserClickHandler(handler) {
      const fp = this.getFramePainter();
      if (isFunc(fp?.configureUserClickHandler))
         fp.configureUserClickHandler(handler);
   }

   /** @summary Configure user-defined dblclick handler
     * @desc Function will be called every time when double click was called
     * As argument, tooltip object with selected bins will be provided
     * If handler function returns true, default handling of dblclick (unzoom) will be disabled
     * @param {function} handler - function called when mouse double click is done */
   configureUserDblclickHandler(handler) {
      const fp = this.getFramePainter();
      if (isFunc(fp?.configureUserDblclickHandler))
         fp.configureUserDblclickHandler(handler);
   }

   /** @summary Check if user-defined tooltip function was configured
     * @return {boolean} flag is user tooltip handler was configured */
   hasUserTooltip() {
      return isFunc(this.#user_tooltip_handler);
   }

   /** @summary Provide tooltips data to user-defined function
     * @param {object} data - tooltip data
     * @private */
   provideUserTooltip(data) {
      if (!this.hasUserTooltip())
         return;

      if (this.#user_tooltip_timeout <= 0)
         return this.#user_tooltip_handler(data);

      if (this.#user_toottip_handle) {
         clearTimeout(this.#user_toottip_handle);
         this.#user_toottip_handle = undefined;
      }

      if (!data)
         return this.#user_tooltip_handler(data);

      // only after timeout user function will be called
      this.#user_toottip_handle = setTimeout(() => {
         this.#user_toottip_handle = undefined;
         if (this.#user_tooltip_handler)
            this.#user_tooltip_handler(data);
      }, this.#user_tooltip_timeout);
   }

   /** @summary Provide projection areas
     * @param kind - 'X', 'Y', 'XY' or ''
     * @private */
   async provideSpecialDrawArea(kind) {
      if (kind === this.#special_draw_area)
         return true;

      return this.getCanvPainter().toggleProjection(kind).then(() => {
         this.#special_draw_area = kind;
         return true;
      });
   }

   /** @summary Draw in special projection areas
     * @param obj - object to draw
     * @param opt - draw option
     * @param kind - '', 'X', 'Y'
     * @private */
   async drawInSpecialArea(obj, opt, kind) {
      const canp = this.getCanvPainter();
      if (this.#special_draw_area && isFunc(canp?.drawProjection))
         return canp.drawProjection(kind || this.#special_draw_area, obj, opt);

      return false;
   }

   /** @summary Get tooltip for painter and specified event position
     * @param {Object} evnt - object with clientX and clientY positions
     * @private */
   getToolTip(evnt) {
      if ((evnt?.clientX === undefined) || (evnt?.clientY === undefined))
         return null;

      const frame = this.getPadPainter()?.getFrameSvg();
      if (!frame || frame.empty())
         return null;
      const layer = frame.selectChild('.main_layer');
      if (layer.empty())
         return null;

      const pos = pointer(evnt, layer.node()),
            pnt = { touch: false, x: pos[0], y: pos[1] };

      if (isFunc(this.extractToolTip))
         return this.extractToolTip(pnt);

      pnt.disabled = true;

      const res = isFunc(this.processTooltipEvent) ? this.processTooltipEvent(pnt) : null;

      return res?.user_info || res;
   }

} // class ObjectPainter


/** @summary Generic text drawing
  * @private */
function drawRawText(dom, txt /* , opt */) {
   const painter = new BasePainter(dom);
   painter.txt = txt;

   painter.redrawObject = async function(obj) {
      this.txt = obj;
      return this.drawText();
   };

   painter.drawText = async function() {
      let stxt = (this.txt._typename === clTObjString) ? this.txt.fString : this.txt.value;
      if (!isStr(stxt))
         stxt = '<undefined>';

      const mathjax = this.txt.mathjax || (settings.Latex === constants$1.Latex.AlwaysMathJax),
            frame = this.selectDom();
      let main = frame.select('div');
      if (main.empty())
         main = frame.append('div').attr('style', 'max-width:100%;max-height:100%;overflow:auto');
      else
         main.html('');

      // (re) set painter to first child element, base painter not requires canvas
      this.setTopPainter();

      if (!mathjax && !('as_is' in this.txt)) {
         const arr = stxt.split('\n');
         for (let i = 0; i < arr.length; ++i)
            main.append('pre').style('margin', '0').text(arr[i]);
      } else
         main.text(stxt);

      if (mathjax)
         typesetMathjax(frame.node());

      return this;
   };

   return painter.drawText();
}

/** @summary Returns canvas painter (if any) for specified DOM element
  * @param {string|object} dom - id or DOM element or pad painter
  * @private */
function getElementCanvPainter(dom) {
   return isPadPainter(dom) ? dom.getCanvPainter() : getDomCanvasPainter(new ObjectPainter(dom).selectDom());
}

/** @summary Returns pad painter (if any) for specified DOM element
  * @param {string|object} dom - id or DOM element or pad painter
  * @private */
function getElementPadPainter(dom) {
   return isPadPainter(dom) ? dom : new ObjectPainter(dom).getPadPainter();
}

/** @summary Returns main painter (if any) for specified HTML element - typically histogram painter
  * @param {string|object} dom - id or DOM element or pad painter
  * @private */
function getElementMainPainter(dom) {
   return isPadPainter(dom) ? dom.getMainPainter() : new ObjectPainter(dom).getMainPainter(true);
}

/** @summary Save object, drawn in specified element, as JSON.
  * @desc Normally it is TCanvas object with list of primitives
  * @param {string|object} dom - id of top div element or directly DOMElement
  * @return {string} produced JSON string */
function drawingJSON(dom) {
   return getElementCanvPainter(dom)?.produceJSON() || '';
}

let $active_pp = null;

/** @summary Set active pad painter
  * @desc Normally be used to handle key press events, which are global in the web browser
  * @param {object} args - functions arguments
  * @param {object} args.pp - pad painter
  * @param {boolean} [args.active] - is pad activated or not
  * @private */
function selectActivePad(args) {
   if (args.active) {
      $active_pp?.getFramePainter()?.setFrameActive(false);
      $active_pp = args.pp;
      $active_pp?.getFramePainter()?.setFrameActive(true);
   } else if ($active_pp === args.pp)
      $active_pp = null;
}

/** @summary Returns current active pad
  * @desc Should be used only for keyboard handling
  * @private */
function getActivePad() {
   return $active_pp;
}

/** @summary Check resize of drawn element
  * @param {string|object} dom - id or DOM element
  * @param {boolean|object} arg - options on how to resize
  * @desc As first argument dom one should use same argument as for the drawing
  * As second argument, one could specify 'true' value to force redrawing of
  * the element even after minimal resize
  * Or one just supply object with exact sizes like { width:300, height:200, force:true };
  * @example
  * import { resize } from 'https://root.cern/js/latest/modules/base/ObjectPainter.mjs';
  * resize('drawing', { width: 500, height: 200 });
  * resize(document.querySelector('#drawing'), true); */
function resize(dom, arg) {
   if (arg === true)
      arg = { force: true };
   else if (!isObject(arg))
      arg = null;
   let done = false;
   new ObjectPainter(dom).forEachPainter(painter => {
      if (!done && isFunc(painter.checkResize))
         done = painter.checkResize(arg);
   });
   return done;
}


/** @summary Safely remove all drawings from specified element
  * @param {string|object} dom - id or DOM element
  * @public
  * @example
  * import { cleanup } from 'https://root.cern/js/latest/modules/base/ObjectPainter.mjs';
  * cleanup('drawing');
  * cleanup(document.querySelector('#drawing')); */
function cleanup(dom) {
   const dummy = new ObjectPainter(dom), lst = [];
   dummy.forEachPainter(p => {
      if (lst.indexOf(p) < 0)
         lst.push(p);
   });
   lst.forEach(p => p.cleanup());
   dummy.selectDom().html('');
   return lst;
}

const EAxisBits = {
   kDecimals: BIT(7),
   kTickPlus: BIT(9),
   kTickMinus: BIT(10),
   kAxisRange: BIT(11),
   kCenterTitle: BIT(12),
   kCenterLabels: BIT(14),
   kRotateTitle: BIT(15),
   kPalette: BIT(16),
   kNoExponent: BIT(17),
   kLabelsHori: BIT(18),
   kLabelsVert: BIT(19),
   kLabelsDown: BIT(20),
   kLabelsUp: BIT(21),
   kIsInteger: BIT(22),
   kMoreLogLabels: BIT(23),
   kOppositeTitle: BIT(32) // artificial bit, not possible to set in ROOT
}, kAxisLabels = 'labels', kAxisNormal = 'normal', kAxisFunc = 'func', kAxisTime = 'time';

Object.assign(internals.jsroot, { ObjectPainter, cleanup, resize });

/**
 * @license
 * Copyright 2010-2025 Three.js Authors
 * SPDX-License-Identifier: MIT
 */
const REVISION = '180';

/**
 * Represents mouse buttons and interaction types in context of controls.
 *
 * @type {ConstantsMouse}
 * @constant
 */
const MOUSE = { ROTATE: 0, DOLLY: 1, PAN: 2 };

/**
 * Represents touch interaction types in context of controls.
 *
 * @type {ConstantsTouch}
 * @constant
 */
const TOUCH = { ROTATE: 0, PAN: 1, DOLLY_PAN: 2, DOLLY_ROTATE: 3 };

/**
 * Disables face culling.
 *
 * @type {number}
 * @constant
 */
const CullFaceNone = 0;

/**
 * Culls back faces.
 *
 * @type {number}
 * @constant
 */
const CullFaceBack = 1;

/**
 * Culls front faces.
 *
 * @type {number}
 * @constant
 */
const CullFaceFront = 2;

/**
 * Filters shadow maps using the Percentage-Closer Filtering (PCF) algorithm.
 *
 * @type {number}
 * @constant
 */
const PCFShadowMap = 1;

/**
 * Filters shadow maps using the Percentage-Closer Filtering (PCF) algorithm with
 * better soft shadows especially when using low-resolution shadow maps.
 *
 * @type {number}
 * @constant
 */
const PCFSoftShadowMap = 2;

/**
 * Filters shadow maps using the Variance Shadow Map (VSM) algorithm.
 * When using VSMShadowMap all shadow receivers will also cast shadows.
 *
 * @type {number}
 * @constant
 */
const VSMShadowMap = 3;

/**
 * Only front faces are rendered.
 *
 * @type {number}
 * @constant
 */
const FrontSide = 0;

/**
 * Only back faces are rendered.
 *
 * @type {number}
 * @constant
 */
const BackSide = 1;

/**
 * Both front and back faces are rendered.
 *
 * @type {number}
 * @constant
 */
const DoubleSide = 2;

/**
 * No blending is performed which effectively disables
 * alpha transparency.
 *
 * @type {number}
 * @constant
 */
const NoBlending = 0;

/**
 * The default blending.
 *
 * @type {number}
 * @constant
 */
const NormalBlending = 1;

/**
 * Represents additive blending.
 *
 * @type {number}
 * @constant
 */
const AdditiveBlending = 2;

/**
 * Represents subtractive blending.
 *
 * @type {number}
 * @constant
 */
const SubtractiveBlending = 3;

/**
 * Represents multiply blending.
 *
 * @type {number}
 * @constant
 */
const MultiplyBlending = 4;

/**
 * Represents custom blending.
 *
 * @type {number}
 * @constant
 */
const CustomBlending = 5;

/**
 * A `source + destination` blending equation.
 *
 * @type {number}
 * @constant
 */
const AddEquation = 100;

/**
 * A `source - destination` blending equation.
 *
 * @type {number}
 * @constant
 */
const SubtractEquation = 101;

/**
 * A `destination - source` blending equation.
 *
 * @type {number}
 * @constant
 */
const ReverseSubtractEquation = 102;

/**
 * A blend equation that uses the minimum of source and destination.
 *
 * @type {number}
 * @constant
 */
const MinEquation = 103;

/**
 * A blend equation that uses the maximum of source and destination.
 *
 * @type {number}
 * @constant
 */
const MaxEquation = 104;

/**
 * Multiplies all colors by `0`.
 *
 * @type {number}
 * @constant
 */
const ZeroFactor = 200;

/**
 * Multiplies all colors by `1`.
 *
 * @type {number}
 * @constant
 */
const OneFactor = 201;

/**
 * Multiplies all colors by the source colors.
 *
 * @type {number}
 * @constant
 */
const SrcColorFactor = 202;

/**
 * Multiplies all colors by `1` minus each source color.
 *
 * @type {number}
 * @constant
 */
const OneMinusSrcColorFactor = 203;

/**
 * Multiplies all colors by the source alpha value.
 *
 * @type {number}
 * @constant
 */
const SrcAlphaFactor = 204;

/**
 * Multiplies all colors by 1 minus the source alpha value.
 *
 * @type {number}
 * @constant
 */
const OneMinusSrcAlphaFactor = 205;

/**
 * Multiplies all colors by the destination alpha value.
 *
 * @type {number}
 * @constant
 */
const DstAlphaFactor = 206;

/**
 * Multiplies all colors by `1` minus the destination alpha value.
 *
 * @type {number}
 * @constant
 */
const OneMinusDstAlphaFactor = 207;

/**
 * Multiplies all colors by the destination color.
 *
 * @type {number}
 * @constant
 */
const DstColorFactor = 208;

/**
 * Multiplies all colors by `1` minus each destination color.
 *
 * @type {number}
 * @constant
 */
const OneMinusDstColorFactor = 209;

/**
 * Multiplies the RGB colors by the smaller of either the source alpha
 * value or the value of `1` minus the destination alpha value. The alpha
 * value is multiplied by `1`.
 *
 * @type {number}
 * @constant
 */
const SrcAlphaSaturateFactor = 210;

/**
 * Multiplies all colors by a constant color.
 *
 * @type {number}
 * @constant
 */
const ConstantColorFactor = 211;

/**
 * Multiplies all colors by `1` minus a constant color.
 *
 * @type {number}
 * @constant
 */
const OneMinusConstantColorFactor = 212;

/**
 * Multiplies all colors by a constant alpha value.
 *
 * @type {number}
 * @constant
 */
const ConstantAlphaFactor = 213;

/**
 * Multiplies all colors by 1 minus a constant alpha value.
 *
 * @type {number}
 * @constant
 */
const OneMinusConstantAlphaFactor = 214;

/**
 * Never pass.
 *
 * @type {number}
 * @constant
 */
const NeverDepth = 0;

/**
 * Always pass.
 *
 * @type {number}
 * @constant
 */
const AlwaysDepth = 1;

/**
 * Pass if the incoming value is less than the depth buffer value.
 *
 * @type {number}
 * @constant
 */
const LessDepth = 2;

/**
 * Pass if the incoming value is less than or equal to the depth buffer value.
 *
 * @type {number}
 * @constant
 */
const LessEqualDepth = 3;

/**
 * Pass if the incoming value equals the depth buffer value.
 *
 * @type {number}
 * @constant
 */
const EqualDepth = 4;

/**
 * Pass if the incoming value is greater than or equal to the depth buffer value.
 *
 * @type {number}
 * @constant
 */
const GreaterEqualDepth = 5;

/**
 * Pass if the incoming value is greater than the depth buffer value.
 *
 * @type {number}
 * @constant
 */
const GreaterDepth = 6;

/**
 * Pass if the incoming value is not equal to the depth buffer value.
 *
 * @type {number}
 * @constant
 */
const NotEqualDepth = 7;

/**
 * Multiplies the environment map color with the surface color.
 *
 * @type {number}
 * @constant
 */
const MultiplyOperation = 0;

/**
 * Uses reflectivity to blend between the two colors.
 *
 * @type {number}
 * @constant
 */
const MixOperation = 1;

/**
 * Adds the two colors.
 *
 * @type {number}
 * @constant
 */
const AddOperation = 2;

/**
 * No tone mapping is applied.
 *
 * @type {number}
 * @constant
 */
const NoToneMapping = 0;

/**
 * Linear tone mapping.
 *
 * @type {number}
 * @constant
 */
const LinearToneMapping = 1;

/**
 * Reinhard tone mapping.
 *
 * @type {number}
 * @constant
 */
const ReinhardToneMapping = 2;

/**
 * Cineon tone mapping.
 *
 * @type {number}
 * @constant
 */
const CineonToneMapping = 3;

/**
 * ACES Filmic tone mapping.
 *
 * @type {number}
 * @constant
 */
const ACESFilmicToneMapping = 4;

/**
 * Custom tone mapping.
 *
 * Expects a custom implementation by modifying shader code of the material's fragment shader.
 *
 * @type {number}
 * @constant
 */
const CustomToneMapping = 5;

/**
 * AgX tone mapping.
 *
 * @type {number}
 * @constant
 */
const AgXToneMapping = 6;

/**
 * Neutral tone mapping.
 *
 * Implementation based on the Khronos 3D Commerce Group standard tone mapping.
 *
 * @type {number}
 * @constant
 */
const NeutralToneMapping = 7;

/**
 * Maps textures using the geometry's UV coordinates.
 *
 * @type {number}
 * @constant
 */
const UVMapping = 300;

/**
 * Reflection mapping for cube textures.
 *
 * @type {number}
 * @constant
 */
const CubeReflectionMapping = 301;

/**
 * Refraction mapping for cube textures.
 *
 * @type {number}
 * @constant
 */
const CubeRefractionMapping = 302;

/**
 * Reflection mapping for equirectangular textures.
 *
 * @type {number}
 * @constant
 */
const EquirectangularReflectionMapping = 303;

/**
 * Refraction mapping for equirectangular textures.
 *
 * @type {number}
 * @constant
 */
const EquirectangularRefractionMapping = 304;

/**
 * Reflection mapping for PMREM textures.
 *
 * @type {number}
 * @constant
 */
const CubeUVReflectionMapping = 306;

/**
 * The texture will simply repeat to infinity.
 *
 * @type {number}
 * @constant
 */
const RepeatWrapping = 1000;

/**
 * The last pixel of the texture stretches to the edge of the mesh.
 *
 * @type {number}
 * @constant
 */
const ClampToEdgeWrapping = 1001;

/**
 * The texture will repeats to infinity, mirroring on each repeat.
 *
 * @type {number}
 * @constant
 */
const MirroredRepeatWrapping = 1002;

/**
 * Returns the value of the texture element that is nearest (in Manhattan distance)
 * to the specified texture coordinates.
 *
 * @type {number}
 * @constant
 */
const NearestFilter = 1003;

/**
 * Chooses the mipmap that most closely matches the size of the pixel being textured
 * and uses the `NearestFilter` criterion (the texel nearest to the center of the pixel)
 * to produce a texture value.
 *
 * @type {number}
 * @constant
 */
const NearestMipmapNearestFilter = 1004;

/**
 * Chooses the two mipmaps that most closely match the size of the pixel being textured and
 * uses the `NearestFilter` criterion to produce a texture value from each mipmap.
 * The final texture value is a weighted average of those two values.
 *
 * @type {number}
 * @constant
 */
const NearestMipmapLinearFilter = 1005;

/**
 * Returns the weighted average of the four texture elements that are closest to the specified
 * texture coordinates, and can include items wrapped or repeated from other parts of a texture,
 * depending on the values of `wrapS` and `wrapT`, and on the exact mapping.
 *
 * @type {number}
 * @constant
 */
const LinearFilter = 1006;

/**
 * Chooses the mipmap that most closely matches the size of the pixel being textured and uses
 * the `LinearFilter` criterion (a weighted average of the four texels that are closest to the
 * center of the pixel) to produce a texture value.
 *
 * @type {number}
 * @constant
 */
const LinearMipmapNearestFilter = 1007;

/**
 * Chooses the two mipmaps that most closely match the size of the pixel being textured and uses
 * the `LinearFilter` criterion to produce a texture value from each mipmap. The final texture value
 * is a weighted average of those two values.
 *
 * @type {number}
 * @constant
 */
const LinearMipmapLinearFilter = 1008;

/**
 * An unsigned byte data type for textures.
 *
 * @type {number}
 * @constant
 */
const UnsignedByteType = 1009;

/**
 * A byte data type for textures.
 *
 * @type {number}
 * @constant
 */
const ByteType = 1010;

/**
 * A short data type for textures.
 *
 * @type {number}
 * @constant
 */
const ShortType = 1011;

/**
 * An unsigned short data type for textures.
 *
 * @type {number}
 * @constant
 */
const UnsignedShortType = 1012;

/**
 * An int data type for textures.
 *
 * @type {number}
 * @constant
 */
const IntType = 1013;

/**
 * An unsigned int data type for textures.
 *
 * @type {number}
 * @constant
 */
const UnsignedIntType = 1014;

/**
 * A float data type for textures.
 *
 * @type {number}
 * @constant
 */
const FloatType = 1015;

/**
 * A half float data type for textures.
 *
 * @type {number}
 * @constant
 */
const HalfFloatType = 1016;

/**
 * An unsigned short 4_4_4_4 (packed) data type for textures.
 *
 * @type {number}
 * @constant
 */
const UnsignedShort4444Type = 1017;

/**
 * An unsigned short 5_5_5_1 (packed) data type for textures.
 *
 * @type {number}
 * @constant
 */
const UnsignedShort5551Type = 1018;

/**
 * An unsigned int 24_8 data type for textures.
 *
 * @type {number}
 * @constant
 */
const UnsignedInt248Type = 1020;

/**
 * An unsigned int 5_9_9_9 (packed) data type for textures.
 *
 * @type {number}
 * @constant
 */
const UnsignedInt5999Type = 35902;

/**
 * An unsigned int 10_11_11 (packed) data type for textures.
 *
 * @type {number}
 * @constant
 */
const UnsignedInt101111Type = 35899;

/**
 * Discards the red, green and blue components and reads just the alpha component.
 *
 * @type {number}
 * @constant
 */
const AlphaFormat = 1021;

/**
 * Discards the alpha component and reads the red, green and blue component.
 *
 * @type {number}
 * @constant
 */
const RGBFormat = 1022;

/**
 * Reads the red, green, blue and alpha components.
 *
 * @type {number}
 * @constant
 */
const RGBAFormat = 1023;

/**
 * Reads each element as a single depth value, converts it to floating point, and clamps to the range `[0,1]`.
 *
 * @type {number}
 * @constant
 */
const DepthFormat = 1026;

/**
 * Reads each element is a pair of depth and stencil values. The depth component of the pair is interpreted as
 * in `DepthFormat`. The stencil component is interpreted based on the depth + stencil internal format.
 *
 * @type {number}
 * @constant
 */
const DepthStencilFormat = 1027;

/**
 * Discards the green, blue and alpha components and reads just the red component.
 *
 * @type {number}
 * @constant
 */
const RedFormat = 1028;

/**
 * Discards the green, blue and alpha components and reads just the red component. The texels are read as integers instead of floating point.
 *
 * @type {number}
 * @constant
 */
const RedIntegerFormat = 1029;

/**
 * Discards the alpha, and blue components and reads the red, and green components.
 *
 * @type {number}
 * @constant
 */
const RGFormat = 1030;

/**
 * Discards the alpha, and blue components and reads the red, and green components. The texels are read as integers instead of floating point.
 *
 * @type {number}
 * @constant
 */
const RGIntegerFormat = 1031;

/**
 * Reads the red, green, blue and alpha components. The texels are read as integers instead of floating point.
 *
 * @type {number}
 * @constant
 */
const RGBAIntegerFormat = 1033;

/**
 * A DXT1-compressed image in an RGB image format.
 *
 * @type {number}
 * @constant
 */
const RGB_S3TC_DXT1_Format = 33776;

/**
 * A DXT1-compressed image in an RGB image format with a simple on/off alpha value.
 *
 * @type {number}
 * @constant
 */
const RGBA_S3TC_DXT1_Format = 33777;

/**
 * A DXT3-compressed image in an RGBA image format. Compared to a 32-bit RGBA texture, it offers 4:1 compression.
 *
 * @type {number}
 * @constant
 */
const RGBA_S3TC_DXT3_Format = 33778;

/**
 * A DXT5-compressed image in an RGBA image format. It also provides a 4:1 compression, but differs to the DXT3
 * compression in how the alpha compression is done.
 *
 * @type {number}
 * @constant
 */
const RGBA_S3TC_DXT5_Format = 33779;

/**
 * PVRTC RGB compression in 4-bit mode. One block for each 4×4 pixels.
 *
 * @type {number}
 * @constant
 */
const RGB_PVRTC_4BPPV1_Format = 35840;

/**
 * PVRTC RGB compression in 2-bit mode. One block for each 8×4 pixels.
 *
 * @type {number}
 * @constant
 */
const RGB_PVRTC_2BPPV1_Format = 35841;

/**
 * PVRTC RGBA compression in 4-bit mode. One block for each 4×4 pixels.
 *
 * @type {number}
 * @constant
 */
const RGBA_PVRTC_4BPPV1_Format = 35842;

/**
 * PVRTC RGBA compression in 2-bit mode. One block for each 8×4 pixels.
 *
 * @type {number}
 * @constant
 */
const RGBA_PVRTC_2BPPV1_Format = 35843;

/**
 * ETC1 RGB format.
 *
 * @type {number}
 * @constant
 */
const RGB_ETC1_Format = 36196;

/**
 * ETC2 RGB format.
 *
 * @type {number}
 * @constant
 */
const RGB_ETC2_Format = 37492;

/**
 * ETC2 RGBA format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ETC2_EAC_Format = 37496;

/**
 * ASTC RGBA 4x4 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_4x4_Format = 37808;

/**
 * ASTC RGBA 5x4 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_5x4_Format = 37809;

/**
 * ASTC RGBA 5x5 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_5x5_Format = 37810;

/**
 * ASTC RGBA 6x5 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_6x5_Format = 37811;

/**
 * ASTC RGBA 6x6 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_6x6_Format = 37812;

/**
 * ASTC RGBA 8x5 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_8x5_Format = 37813;

/**
 * ASTC RGBA 8x6 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_8x6_Format = 37814;

/**
 * ASTC RGBA 8x8 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_8x8_Format = 37815;

/**
 * ASTC RGBA 10x5 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_10x5_Format = 37816;

/**
 * ASTC RGBA 10x6 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_10x6_Format = 37817;

/**
 * ASTC RGBA 10x8 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_10x8_Format = 37818;

/**
 * ASTC RGBA 10x10 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_10x10_Format = 37819;

/**
 * ASTC RGBA 12x10 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_12x10_Format = 37820;

/**
 * ASTC RGBA 12x12 format.
 *
 * @type {number}
 * @constant
 */
const RGBA_ASTC_12x12_Format = 37821;

/**
 * BPTC RGBA format.
 *
 * @type {number}
 * @constant
 */
const RGBA_BPTC_Format = 36492;

/**
 * BPTC Signed RGB format.
 *
 * @type {number}
 * @constant
 */
const RGB_BPTC_SIGNED_Format = 36494;

/**
 * BPTC Unsigned RGB format.
 *
 * @type {number}
 * @constant
 */
const RGB_BPTC_UNSIGNED_Format = 36495;

/**
 * RGTC1 Red format.
 *
 * @type {number}
 * @constant
 */
const RED_RGTC1_Format = 36283;

/**
 * RGTC1 Signed Red format.
 *
 * @type {number}
 * @constant
 */
const SIGNED_RED_RGTC1_Format = 36284;

/**
 * RGTC2 Red Green format.
 *
 * @type {number}
 * @constant
 */
const RED_GREEN_RGTC2_Format = 36285;

/**
 * RGTC2 Signed Red Green format.
 *
 * @type {number}
 * @constant
 */
const SIGNED_RED_GREEN_RGTC2_Format = 36286;

/**
 * Basic depth packing.
 *
 * @type {number}
 * @constant
 */
const BasicDepthPacking = 3200;

/**
 * A depth value is packed into 32 bit RGBA.
 *
 * @type {number}
 * @constant
 */
const RGBADepthPacking = 3201;

/**
 * Normal information is relative to the underlying surface.
 *
 * @type {number}
 * @constant
 */
const TangentSpaceNormalMap = 0;

/**
 * Normal information is relative to the object orientation.
 *
 * @type {number}
 * @constant
 */
const ObjectSpaceNormalMap = 1;

// Color space string identifiers, matching CSS Color Module Level 4 and WebGPU names where available.

/**
 * No color space.
 *
 * @type {string}
 * @constant
 */
const NoColorSpace = '';

/**
 * sRGB color space.
 *
 * @type {string}
 * @constant
 */
const SRGBColorSpace = 'srgb';

/**
 * sRGB-linear color space.
 *
 * @type {string}
 * @constant
 */
const LinearSRGBColorSpace = 'srgb-linear';

/**
 * Linear transfer function.
 *
 * @type {string}
 * @constant
 */
const LinearTransfer = 'linear';

/**
 * sRGB transfer function.
 *
 * @type {string}
 * @constant
 */
const SRGBTransfer = 'srgb';

/**
 * Keeps the current value.
 *
 * @type {number}
 * @constant
 */
const KeepStencilOp = 7680;

/**
 * Will always return true.
 *
 * @type {number}
 * @constant
 */
const AlwaysStencilFunc = 519;

/**
 * Never pass.
 *
 * @type {number}
 * @constant
 */
const NeverCompare = 512;

/**
 * Pass if the incoming value is less than the texture value.
 *
 * @type {number}
 * @constant
 */
const LessCompare = 513;

/**
 * Pass if the incoming value equals the texture value.
 *
 * @type {number}
 * @constant
 */
const EqualCompare = 514;

/**
 * Pass if the incoming value is less than or equal to the texture value.
 *
 * @type {number}
 * @constant
 */
const LessEqualCompare = 515;

/**
 * Pass if the incoming value is greater than the texture value.
 *
 * @type {number}
 * @constant
 */
const GreaterCompare = 516;

/**
 * Pass if the incoming value is not equal to the texture value.
 *
 * @type {number}
 * @constant
 */
const NotEqualCompare = 517;

/**
 * Pass if the incoming value is greater than or equal to the texture value.
 *
 * @type {number}
 * @constant
 */
const GreaterEqualCompare = 518;

/**
 * Always pass.
 *
 * @type {number}
 * @constant
 */
const AlwaysCompare = 519;

/**
 * The contents are intended to be specified once by the application, and used many
 * times as the source for drawing and image specification commands.
 *
 * @type {number}
 * @constant
 */
const StaticDrawUsage = 35044;

/**
 * GLSL 3 shader code.
 *
 * @type {string}
 * @constant
 */
const GLSL3 = '300 es';

/**
 * WebGL coordinate system.
 *
 * @type {number}
 * @constant
 */
const WebGLCoordinateSystem = 2000;

/**
 * WebGPU coordinate system.
 *
 * @type {number}
 * @constant
 */
const WebGPUCoordinateSystem = 2001;

/**
 * This type represents mouse buttons and interaction types in context of controls.
 *
 * @typedef {Object} ConstantsMouse
 * @property {number} MIDDLE - The left mouse button.
 * @property {number} LEFT - The middle mouse button.
 * @property {number} RIGHT - The right mouse button.
 * @property {number} ROTATE - A rotate interaction.
 * @property {number} DOLLY - A dolly interaction.
 * @property {number} PAN - A pan interaction.
 **/

/**
 * This type represents touch interaction types in context of controls.
 *
 * @typedef {Object} ConstantsTouch
 * @property {number} ROTATE - A rotate interaction.
 * @property {number} PAN - A pan interaction.
 * @property {number} DOLLY_PAN - The dolly-pan interaction.
 * @property {number} DOLLY_ROTATE - A dolly-rotate interaction.
 **/

/**
 * This type represents the different timestamp query types.
 *
 * @typedef {Object} ConstantsTimestampQuery
 * @property {string} COMPUTE - A `compute` timestamp query.
 * @property {string} RENDER - A `render` timestamp query.
 **/

/**
 * Represents the different interpolation sampling types.
 *
 * @typedef {Object} ConstantsInterpolationSamplingType
 * @property {string} PERSPECTIVE - Perspective-correct interpolation.
 * @property {string} LINEAR - Linear interpolation.
 * @property {string} FLAT - Flat interpolation.
 */

/**
 * Represents the different interpolation sampling modes.
 *
 * @typedef {Object} ConstantsInterpolationSamplingMode
 * @property {string} NORMAL - Normal sampling mode.
 * @property {string} CENTROID - Centroid sampling mode.
 * @property {string} SAMPLE - Sample-specific sampling mode.
 * @property {string} FIRST - Flat interpolation using the first vertex.
 * @property {string} EITHER - Flat interpolation using either vertex.
 */

/**
 * This modules allows to dispatch event objects on custom JavaScript objects.
 *
 * Main repository: [eventdispatcher.js]{@link https://github.com/mrdoob/eventdispatcher.js/}
 *
 * Code Example:
 * ```js
 * class Car extends EventDispatcher {
 * 	start() {
 *		this.dispatchEvent( { type: 'start', message: 'vroom vroom!' } );
 *	}
 *};
 *
 * // Using events with the custom object
 * const car = new Car();
 * car.addEventListener( 'start', function ( event ) {
 * 	alert( event.message );
 * } );
 *
 * car.start();
 * ```
 */
class EventDispatcher {

	/**
	 * Adds the given event listener to the given event type.
	 *
	 * @param {string} type - The type of event to listen to.
	 * @param {Function} listener - The function that gets called when the event is fired.
	 */
	addEventListener( type, listener ) {

		if ( this._listeners === undefined ) this._listeners = {};

		const listeners = this._listeners;

		if ( listeners[ type ] === undefined ) {

			listeners[ type ] = [];

		}

		if ( listeners[ type ].indexOf( listener ) === -1 ) {

			listeners[ type ].push( listener );

		}

	}

	/**
	 * Returns `true` if the given event listener has been added to the given event type.
	 *
	 * @param {string} type - The type of event.
	 * @param {Function} listener - The listener to check.
	 * @return {boolean} Whether the given event listener has been added to the given event type.
	 */
	hasEventListener( type, listener ) {

		const listeners = this._listeners;

		if ( listeners === undefined ) return false;

		return listeners[ type ] !== undefined && listeners[ type ].indexOf( listener ) !== -1;

	}

	/**
	 * Removes the given event listener from the given event type.
	 *
	 * @param {string} type - The type of event.
	 * @param {Function} listener - The listener to remove.
	 */
	removeEventListener( type, listener ) {

		const listeners = this._listeners;

		if ( listeners === undefined ) return;

		const listenerArray = listeners[ type ];

		if ( listenerArray !== undefined ) {

			const index = listenerArray.indexOf( listener );

			if ( index !== -1 ) {

				listenerArray.splice( index, 1 );

			}

		}

	}

	/**
	 * Dispatches an event object.
	 *
	 * @param {Object} event - The event that gets fired.
	 */
	dispatchEvent( event ) {

		const listeners = this._listeners;

		if ( listeners === undefined ) return;

		const listenerArray = listeners[ event.type ];

		if ( listenerArray !== undefined ) {

			event.target = this;

			// Make a copy, in case listeners are removed while iterating.
			const array = listenerArray.slice( 0 );

			for ( let i = 0, l = array.length; i < l; i ++ ) {

				array[ i ].call( this, event );

			}

			event.target = null;

		}

	}

}

const _lut = [ '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '0a', '0b', '0c', '0d', '0e', '0f', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '1a', '1b', '1c', '1d', '1e', '1f', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '2a', '2b', '2c', '2d', '2e', '2f', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '3a', '3b', '3c', '3d', '3e', '3f', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '4a', '4b', '4c', '4d', '4e', '4f', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '5a', '5b', '5c', '5d', '5e', '5f', '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '6a', '6b', '6c', '6d', '6e', '6f', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '7a', '7b', '7c', '7d', '7e', '7f', '80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '8a', '8b', '8c', '8d', '8e', '8f', '90', '91', '92', '93', '94', '95', '96', '97', '98', '99', '9a', '9b', '9c', '9d', '9e', '9f', 'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'aa', 'ab', 'ac', 'ad', 'ae', 'af', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'b8', 'b9', 'ba', 'bb', 'bc', 'bd', 'be', 'bf', 'c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'ca', 'cb', 'cc', 'cd', 'ce', 'cf', 'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', 'da', 'db', 'dc', 'dd', 'de', 'df', 'e0', 'e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8', 'e9', 'ea', 'eb', 'ec', 'ed', 'ee', 'ef', 'f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'fa', 'fb', 'fc', 'fd', 'fe', 'ff' ];

let _seed = 1234567;


const DEG2RAD = Math.PI / 180;
const RAD2DEG = 180 / Math.PI;

/**
 * Generate a [UUID]{@link https://en.wikipedia.org/wiki/Universally_unique_identifier}
 * (universally unique identifier).
 *
 * @return {string} The UUID.
 */
function generateUUID() {

	// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136

	const d0 = Math.random() * 0xffffffff | 0;
	const d1 = Math.random() * 0xffffffff | 0;
	const d2 = Math.random() * 0xffffffff | 0;
	const d3 = Math.random() * 0xffffffff | 0;
	const uuid = _lut[ d0 & 0xff ] + _lut[ d0 >> 8 & 0xff ] + _lut[ d0 >> 16 & 0xff ] + _lut[ d0 >> 24 & 0xff ] + '-' +
			_lut[ d1 & 0xff ] + _lut[ d1 >> 8 & 0xff ] + '-' + _lut[ d1 >> 16 & 0x0f | 0x40 ] + _lut[ d1 >> 24 & 0xff ] + '-' +
			_lut[ d2 & 0x3f | 0x80 ] + _lut[ d2 >> 8 & 0xff ] + '-' + _lut[ d2 >> 16 & 0xff ] + _lut[ d2 >> 24 & 0xff ] +
			_lut[ d3 & 0xff ] + _lut[ d3 >> 8 & 0xff ] + _lut[ d3 >> 16 & 0xff ] + _lut[ d3 >> 24 & 0xff ];

	// .toLowerCase() here flattens concatenated strings to save heap memory space.
	return uuid.toLowerCase();

}

/**
 * Clamps the given value between min and max.
 *
 * @param {number} value - The value to clamp.
 * @param {number} min - The min value.
 * @param {number} max - The max value.
 * @return {number} The clamped value.
 */
function clamp( value, min, max ) {

	return Math.max( min, Math.min( max, value ) );

}

/**
 * Computes the Euclidean modulo of the given parameters that
 * is `( ( n % m ) + m ) % m`.
 *
 * @param {number} n - The first parameter.
 * @param {number} m - The second parameter.
 * @return {number} The Euclidean modulo.
 */
function euclideanModulo( n, m ) {

	// https://en.wikipedia.org/wiki/Modulo_operation

	return ( ( n % m ) + m ) % m;

}

/**
 * Performs a linear mapping from range `<a1, a2>` to range `<b1, b2>`
 * for the given value.
 *
 * @param {number} x - The value to be mapped.
 * @param {number} a1 - Minimum value for range A.
 * @param {number} a2 - Maximum value for range A.
 * @param {number} b1 - Minimum value for range B.
 * @param {number} b2 - Maximum value for range B.
 * @return {number} The mapped value.
 */
function mapLinear( x, a1, a2, b1, b2 ) {

	return b1 + ( x - a1 ) * ( b2 - b1 ) / ( a2 - a1 );

}

/**
 * Returns the percentage in the closed interval `[0, 1]` of the given value
 * between the start and end point.
 *
 * @param {number} x - The start point
 * @param {number} y - The end point.
 * @param {number} value - A value between start and end.
 * @return {number} The interpolation factor.
 */
function inverseLerp( x, y, value ) {

	// https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/inverse-lerp-a-super-useful-yet-often-overlooked-function-r5230/

	if ( x !== y ) {

		return ( value - x ) / ( y - x );

	} else {

		return 0;

	}

}

/**
 * Returns a value linearly interpolated from two known points based on the given interval -
 * `t = 0` will return `x` and `t = 1` will return `y`.
 *
 * @param {number} x - The start point
 * @param {number} y - The end point.
 * @param {number} t - The interpolation factor in the closed interval `[0, 1]`.
 * @return {number} The interpolated value.
 */
function lerp( x, y, t ) {

	return ( 1 - t ) * x + t * y;

}

/**
 * Smoothly interpolate a number from `x` to `y` in  a spring-like manner using a delta
 * time to maintain frame rate independent movement. For details, see
 * [Frame rate independent damping using lerp]{@link http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/}.
 *
 * @param {number} x - The current point.
 * @param {number} y - The target point.
 * @param {number} lambda - A higher lambda value will make the movement more sudden,
 * and a lower value will make the movement more gradual.
 * @param {number} dt - Delta time in seconds.
 * @return {number} The interpolated value.
 */
function damp( x, y, lambda, dt ) {

	return lerp( x, y, 1 - Math.exp( - lambda * dt ) );

}

/**
 * Returns a value that alternates between `0` and the given `length` parameter.
 *
 * @param {number} x - The value to pingpong.
 * @param {number} [length=1] - The positive value the function will pingpong to.
 * @return {number} The alternated value.
 */
function pingpong( x, length = 1 ) {

	// https://www.desmos.com/calculator/vcsjnyz7x4

	return length - Math.abs( euclideanModulo( x, length * 2 ) - length );

}

/**
 * Returns a value in the range `[0,1]` that represents the percentage that `x` has
 * moved between `min` and `max`, but smoothed or slowed down the closer `x` is to
 * the `min` and `max`.
 *
 * See [Smoothstep]{@link http://en.wikipedia.org/wiki/Smoothstep} for more details.
 *
 * @param {number} x - The value to evaluate based on its position between min and max.
 * @param {number} min - The min value. Any x value below min will be `0`.
 * @param {number} max - The max value. Any x value above max will be `1`.
 * @return {number} The alternated value.
 */
function smoothstep( x, min, max ) {

	if ( x <= min ) return 0;
	if ( x >= max ) return 1;

	x = ( x - min ) / ( max - min );

	return x * x * ( 3 - 2 * x );

}

/**
 * A [variation on smoothstep]{@link https://en.wikipedia.org/wiki/Smoothstep#Variations}
 * that has zero 1st and 2nd order derivatives at x=0 and x=1.
 *
 * @param {number} x - The value to evaluate based on its position between min and max.
 * @param {number} min - The min value. Any x value below min will be `0`.
 * @param {number} max - The max value. Any x value above max will be `1`.
 * @return {number} The alternated value.
 */
function smootherstep( x, min, max ) {

	if ( x <= min ) return 0;
	if ( x >= max ) return 1;

	x = ( x - min ) / ( max - min );

	return x * x * x * ( x * ( x * 6 - 15 ) + 10 );

}

/**
 * Returns a random integer from `<low, high>` interval.
 *
 * @param {number} low - The lower value boundary.
 * @param {number} high - The upper value boundary
 * @return {number} A random integer.
 */
function randInt( low, high ) {

	return low + Math.floor( Math.random() * ( high - low + 1 ) );

}

/**
 * Returns a random float from `<low, high>` interval.
 *
 * @param {number} low - The lower value boundary.
 * @param {number} high - The upper value boundary
 * @return {number} A random float.
 */
function randFloat( low, high ) {

	return low + Math.random() * ( high - low );

}

/**
 * Returns a random integer from `<-range/2, range/2>` interval.
 *
 * @param {number} range - Defines the value range.
 * @return {number} A random float.
 */
function randFloatSpread( range ) {

	return range * ( 0.5 - Math.random() );

}

/**
 * Returns a deterministic pseudo-random float in the interval `[0, 1]`.
 *
 * @param {number} [s] - The integer seed.
 * @return {number} A random float.
 */
function seededRandom( s ) {

	if ( s !== undefined ) _seed = s;

	// Mulberry32 generator

	let t = _seed += 0x6D2B79F5;

	t = Math.imul( t ^ t >>> 15, t | 1 );

	t ^= t + Math.imul( t ^ t >>> 7, t | 61 );

	return ( ( t ^ t >>> 14 ) >>> 0 ) / 4294967296;

}

/**
 * Converts degrees to radians.
 *
 * @param {number} degrees - A value in degrees.
 * @return {number} The converted value in radians.
 */
function degToRad( degrees ) {

	return degrees * DEG2RAD;

}

/**
 * Converts radians to degrees.
 *
 * @param {number} radians - A value in radians.
 * @return {number} The converted value in degrees.
 */
function radToDeg( radians ) {

	return radians * RAD2DEG;

}

/**
 * Returns `true` if the given number is a power of two.
 *
 * @param {number} value - The value to check.
 * @return {boolean} Whether the given number is a power of two or not.
 */
function isPowerOfTwo( value ) {

	return ( value & ( value - 1 ) ) === 0 && value !== 0;

}

/**
 * Returns the smallest power of two that is greater than or equal to the given number.
 *
 * @param {number} value - The value to find a POT for.
 * @return {number} The smallest power of two that is greater than or equal to the given number.
 */
function ceilPowerOfTwo( value ) {

	return Math.pow( 2, Math.ceil( Math.log( value ) / Math.LN2 ) );

}

/**
 * Returns the largest power of two that is less than or equal to the given number.
 *
 * @param {number} value - The value to find a POT for.
 * @return {number} The largest power of two that is less than or equal to the given number.
 */
function floorPowerOfTwo( value ) {

	return Math.pow( 2, Math.floor( Math.log( value ) / Math.LN2 ) );

}

/**
 * Sets the given quaternion from the [Intrinsic Proper Euler Angles]{@link https://en.wikipedia.org/wiki/Euler_angles}
 * defined by the given angles and order.
 *
 * Rotations are applied to the axes in the order specified by order:
 * rotation by angle `a` is applied first, then by angle `b`, then by angle `c`.
 *
 * @param {Quaternion} q - The quaternion to set.
 * @param {number} a - The rotation applied to the first axis, in radians.
 * @param {number} b - The rotation applied to the second axis, in radians.
 * @param {number} c - The rotation applied to the third axis, in radians.
 * @param {('XYX'|'XZX'|'YXY'|'YZY'|'ZXZ'|'ZYZ')} order - A string specifying the axes order.
 */
function setQuaternionFromProperEuler( q, a, b, c, order ) {

	const cos = Math.cos;
	const sin = Math.sin;

	const c2 = cos( b / 2 );
	const s2 = sin( b / 2 );

	const c13 = cos( ( a + c ) / 2 );
	const s13 = sin( ( a + c ) / 2 );

	const c1_3 = cos( ( a - c ) / 2 );
	const s1_3 = sin( ( a - c ) / 2 );

	const c3_1 = cos( ( c - a ) / 2 );
	const s3_1 = sin( ( c - a ) / 2 );

	switch ( order ) {

		case 'XYX':
			q.set( c2 * s13, s2 * c1_3, s2 * s1_3, c2 * c13 );
			break;

		case 'YZY':
			q.set( s2 * s1_3, c2 * s13, s2 * c1_3, c2 * c13 );
			break;

		case 'ZXZ':
			q.set( s2 * c1_3, s2 * s1_3, c2 * s13, c2 * c13 );
			break;

		case 'XZX':
			q.set( c2 * s13, s2 * s3_1, s2 * c3_1, c2 * c13 );
			break;

		case 'YXY':
			q.set( s2 * c3_1, c2 * s13, s2 * s3_1, c2 * c13 );
			break;

		case 'ZYZ':
			q.set( s2 * s3_1, s2 * c3_1, c2 * s13, c2 * c13 );
			break;

		default:
			console.warn( 'THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: ' + order );

	}

}

/**
 * Denormalizes the given value according to the given typed array.
 *
 * @param {number} value - The value to denormalize.
 * @param {TypedArray} array - The typed array that defines the data type of the value.
 * @return {number} The denormalize (float) value in the range `[0,1]`.
 */
function denormalize( value, array ) {

	switch ( array.constructor ) {

		case Float32Array:

			return value;

		case Uint32Array:

			return value / 4294967295.0;

		case Uint16Array:

			return value / 65535.0;

		case Uint8Array:

			return value / 255.0;

		case Int32Array:

			return Math.max( value / 2147483647.0, -1 );

		case Int16Array:

			return Math.max( value / 32767.0, -1 );

		case Int8Array:

			return Math.max( value / 127.0, -1 );

		default:

			throw new Error( 'Invalid component type.' );

	}

}

/**
 * Normalizes the given value according to the given typed array.
 *
 * @param {number} value - The float value in the range `[0,1]` to normalize.
 * @param {TypedArray} array - The typed array that defines the data type of the value.
 * @return {number} The normalize value.
 */
function normalize$1( value, array ) {

	switch ( array.constructor ) {

		case Float32Array:

			return value;

		case Uint32Array:

			return Math.round( value * 4294967295.0 );

		case Uint16Array:

			return Math.round( value * 65535.0 );

		case Uint8Array:

			return Math.round( value * 255.0 );

		case Int32Array:

			return Math.round( value * 2147483647.0 );

		case Int16Array:

			return Math.round( value * 32767.0 );

		case Int8Array:

			return Math.round( value * 127.0 );

		default:

			throw new Error( 'Invalid component type.' );

	}

}

/**
 * @class
 * @classdesc A collection of math utility functions.
 * @hideconstructor
 */
const MathUtils = {
	DEG2RAD: DEG2RAD,
	RAD2DEG: RAD2DEG,
	/**
	 * Generate a [UUID]{@link https://en.wikipedia.org/wiki/Universally_unique_identifier}
	 * (universally unique identifier).
	 *
	 * @static
	 * @method
	 * @return {string} The UUID.
	 */
	generateUUID: generateUUID,
	/**
	 * Clamps the given value between min and max.
	 *
	 * @static
	 * @method
	 * @param {number} value - The value to clamp.
	 * @param {number} min - The min value.
	 * @param {number} max - The max value.
	 * @return {number} The clamped value.
	 */
	clamp: clamp,
	/**
	 * Computes the Euclidean modulo of the given parameters that
	 * is `( ( n % m ) + m ) % m`.
	 *
	 * @static
	 * @method
	 * @param {number} n - The first parameter.
	 * @param {number} m - The second parameter.
	 * @return {number} The Euclidean modulo.
	 */
	euclideanModulo: euclideanModulo,
	/**
	 * Performs a linear mapping from range `<a1, a2>` to range `<b1, b2>`
	 * for the given value.
	 *
	 * @static
	 * @method
	 * @param {number} x - The value to be mapped.
	 * @param {number} a1 - Minimum value for range A.
	 * @param {number} a2 - Maximum value for range A.
	 * @param {number} b1 - Minimum value for range B.
	 * @param {number} b2 - Maximum value for range B.
	 * @return {number} The mapped value.
	 */
	mapLinear: mapLinear,
	/**
	 * Returns the percentage in the closed interval `[0, 1]` of the given value
	 * between the start and end point.
	 *
	 * @static
	 * @method
	 * @param {number} x - The start point
	 * @param {number} y - The end point.
	 * @param {number} value - A value between start and end.
	 * @return {number} The interpolation factor.
	 */
	inverseLerp: inverseLerp,
	/**
	 * Returns a value linearly interpolated from two known points based on the given interval -
	 * `t = 0` will return `x` and `t = 1` will return `y`.
	 *
	 * @static
	 * @method
	 * @param {number} x - The start point
	 * @param {number} y - The end point.
	 * @param {number} t - The interpolation factor in the closed interval `[0, 1]`.
	 * @return {number} The interpolated value.
	 */
	lerp: lerp,
	/**
	 * Smoothly interpolate a number from `x` to `y` in  a spring-like manner using a delta
	 * time to maintain frame rate independent movement. For details, see
	 * [Frame rate independent damping using lerp]{@link http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/}.
	 *
	 * @static
	 * @method
	 * @param {number} x - The current point.
	 * @param {number} y - The target point.
	 * @param {number} lambda - A higher lambda value will make the movement more sudden,
	 * and a lower value will make the movement more gradual.
	 * @param {number} dt - Delta time in seconds.
	 * @return {number} The interpolated value.
	 */
	damp: damp,
	/**
	 * Returns a value that alternates between `0` and the given `length` parameter.
	 *
	 * @static
	 * @method
	 * @param {number} x - The value to pingpong.
	 * @param {number} [length=1] - The positive value the function will pingpong to.
	 * @return {number} The alternated value.
	 */
	pingpong: pingpong,
	/**
	 * Returns a value in the range `[0,1]` that represents the percentage that `x` has
	 * moved between `min` and `max`, but smoothed or slowed down the closer `x` is to
	 * the `min` and `max`.
	 *
	 * See [Smoothstep]{@link http://en.wikipedia.org/wiki/Smoothstep} for more details.
	 *
	 * @static
	 * @method
	 * @param {number} x - The value to evaluate based on its position between min and max.
	 * @param {number} min - The min value. Any x value below min will be `0`.
	 * @param {number} max - The max value. Any x value above max will be `1`.
	 * @return {number} The alternated value.
	 */
	smoothstep: smoothstep,
	/**
	 * A [variation on smoothstep]{@link https://en.wikipedia.org/wiki/Smoothstep#Variations}
	 * that has zero 1st and 2nd order derivatives at x=0 and x=1.
	 *
	 * @static
	 * @method
	 * @param {number} x - The value to evaluate based on its position between min and max.
	 * @param {number} min - The min value. Any x value below min will be `0`.
	 * @param {number} max - The max value. Any x value above max will be `1`.
	 * @return {number} The alternated value.
	 */
	smootherstep: smootherstep,
	/**
	 * Returns a random integer from `<low, high>` interval.
	 *
	 * @static
	 * @method
	 * @param {number} low - The lower value boundary.
	 * @param {number} high - The upper value boundary
	 * @return {number} A random integer.
	 */
	randInt: randInt,
	/**
	 * Returns a random float from `<low, high>` interval.
	 *
	 * @static
	 * @method
	 * @param {number} low - The lower value boundary.
	 * @param {number} high - The upper value boundary
	 * @return {number} A random float.
	 */
	randFloat: randFloat,
	/**
	 * Returns a random integer from `<-range/2, range/2>` interval.
	 *
	 * @static
	 * @method
	 * @param {number} range - Defines the value range.
	 * @return {number} A random float.
	 */
	randFloatSpread: randFloatSpread,
	/**
	 * Returns a deterministic pseudo-random float in the interval `[0, 1]`.
	 *
	 * @static
	 * @method
	 * @param {number} [s] - The integer seed.
	 * @return {number} A random float.
	 */
	seededRandom: seededRandom,
	/**
	 * Converts degrees to radians.
	 *
	 * @static
	 * @method
	 * @param {number} degrees - A value in degrees.
	 * @return {number} The converted value in radians.
	 */
	degToRad: degToRad,
	/**
	 * Converts radians to degrees.
	 *
	 * @static
	 * @method
	 * @param {number} radians - A value in radians.
	 * @return {number} The converted value in degrees.
	 */
	radToDeg: radToDeg,
	/**
	 * Returns `true` if the given number is a power of two.
	 *
	 * @static
	 * @method
	 * @param {number} value - The value to check.
	 * @return {boolean} Whether the given number is a power of two or not.
	 */
	isPowerOfTwo: isPowerOfTwo,
	/**
	 * Returns the smallest power of two that is greater than or equal to the given number.
	 *
	 * @static
	 * @method
	 * @param {number} value - The value to find a POT for.
	 * @return {number} The smallest power of two that is greater than or equal to the given number.
	 */
	ceilPowerOfTwo: ceilPowerOfTwo,
	/**
	 * Returns the largest power of two that is less than or equal to the given number.
	 *
	 * @static
	 * @method
	 * @param {number} value - The value to find a POT for.
	 * @return {number} The largest power of two that is less than or equal to the given number.
	 */
	floorPowerOfTwo: floorPowerOfTwo,
	/**
	 * Sets the given quaternion from the [Intrinsic Proper Euler Angles]{@link https://en.wikipedia.org/wiki/Euler_angles}
	 * defined by the given angles and order.
	 *
	 * Rotations are applied to the axes in the order specified by order:
	 * rotation by angle `a` is applied first, then by angle `b`, then by angle `c`.
	 *
	 * @static
	 * @method
	 * @param {Quaternion} q - The quaternion to set.
	 * @param {number} a - The rotation applied to the first axis, in radians.
	 * @param {number} b - The rotation applied to the second axis, in radians.
	 * @param {number} c - The rotation applied to the third axis, in radians.
	 * @param {('XYX'|'XZX'|'YXY'|'YZY'|'ZXZ'|'ZYZ')} order - A string specifying the axes order.
	 */
	setQuaternionFromProperEuler: setQuaternionFromProperEuler,
	/**
	 * Normalizes the given value according to the given typed array.
	 *
	 * @static
	 * @method
	 * @param {number} value - The float value in the range `[0,1]` to normalize.
	 * @param {TypedArray} array - The typed array that defines the data type of the value.
	 * @return {number} The normalize value.
	 */
	normalize: normalize$1,
	/**
	 * Denormalizes the given value according to the given typed array.
	 *
	 * @static
	 * @method
	 * @param {number} value - The value to denormalize.
	 * @param {TypedArray} array - The typed array that defines the data type of the value.
	 * @return {number} The denormalize (float) value in the range `[0,1]`.
	 */
	denormalize: denormalize
};

var MathUtils$1 = /*#__PURE__*/Object.freeze({
	__proto__: null,
	DEG2RAD: DEG2RAD,
	MathUtils: MathUtils,
	RAD2DEG: RAD2DEG,
	ceilPowerOfTwo: ceilPowerOfTwo,
	clamp: clamp,
	damp: damp,
	degToRad: degToRad,
	denormalize: denormalize,
	euclideanModulo: euclideanModulo,
	floorPowerOfTwo: floorPowerOfTwo,
	generateUUID: generateUUID,
	inverseLerp: inverseLerp,
	isPowerOfTwo: isPowerOfTwo,
	lerp: lerp,
	mapLinear: mapLinear,
	normalize: normalize$1,
	pingpong: pingpong,
	radToDeg: radToDeg,
	randFloat: randFloat,
	randFloatSpread: randFloatSpread,
	randInt: randInt,
	seededRandom: seededRandom,
	setQuaternionFromProperEuler: setQuaternionFromProperEuler,
	smootherstep: smootherstep,
	smoothstep: smoothstep
});

/**
 * Class representing a 2D vector. A 2D vector is an ordered pair of numbers
 * (labeled x and y), which can be used to represent a number of things, such as:
 *
 * - A point in 2D space (i.e. a position on a plane).
 * - A direction and length across a plane. In three.js the length will
 * always be the Euclidean distance(straight-line distance) from `(0, 0)` to `(x, y)`
 * and the direction is also measured from `(0, 0)` towards `(x, y)`.
 * - Any arbitrary ordered pair of numbers.
 *
 * There are other things a 2D vector can be used to represent, such as
 * momentum vectors, complex numbers and so on, however these are the most
 * common uses in three.js.
 *
 * Iterating through a vector instance will yield its components `(x, y)` in
 * the corresponding order.
 * ```js
 * const a = new THREE.Vector2( 0, 1 );
 *
 * //no arguments; will be initialised to (0, 0)
 * const b = new THREE.Vector2( );
 *
 * const d = a.distanceTo( b );
 * ```
 */
class Vector2 {

	/**
	 * Constructs a new 2D vector.
	 *
	 * @param {number} [x=0] - The x value of this vector.
	 * @param {number} [y=0] - The y value of this vector.
	 */
	constructor( x = 0, y = 0 ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		Vector2.prototype.isVector2 = true;

		/**
		 * The x value of this vector.
		 *
		 * @type {number}
		 */
		this.x = x;

		/**
		 * The y value of this vector.
		 *
		 * @type {number}
		 */
		this.y = y;

	}

	/**
	 * Alias for {@link Vector2#x}.
	 *
	 * @type {number}
	 */
	get width() {

		return this.x;

	}

	set width( value ) {

		this.x = value;

	}

	/**
	 * Alias for {@link Vector2#y}.
	 *
	 * @type {number}
	 */
	get height() {

		return this.y;

	}

	set height( value ) {

		this.y = value;

	}

	/**
	 * Sets the vector components.
	 *
	 * @param {number} x - The value of the x component.
	 * @param {number} y - The value of the y component.
	 * @return {Vector2} A reference to this vector.
	 */
	set( x, y ) {

		this.x = x;
		this.y = y;

		return this;

	}

	/**
	 * Sets the vector components to the same value.
	 *
	 * @param {number} scalar - The value to set for all vector components.
	 * @return {Vector2} A reference to this vector.
	 */
	setScalar( scalar ) {

		this.x = scalar;
		this.y = scalar;

		return this;

	}

	/**
	 * Sets the vector's x component to the given value
	 *
	 * @param {number} x - The value to set.
	 * @return {Vector2} A reference to this vector.
	 */
	setX( x ) {

		this.x = x;

		return this;

	}

	/**
	 * Sets the vector's y component to the given value
	 *
	 * @param {number} y - The value to set.
	 * @return {Vector2} A reference to this vector.
	 */
	setY( y ) {

		this.y = y;

		return this;

	}

	/**
	 * Allows to set a vector component with an index.
	 *
	 * @param {number} index - The component index. `0` equals to x, `1` equals to y.
	 * @param {number} value - The value to set.
	 * @return {Vector2} A reference to this vector.
	 */
	setComponent( index, value ) {

		switch ( index ) {

			case 0: this.x = value; break;
			case 1: this.y = value; break;
			default: throw new Error( 'index is out of range: ' + index );

		}

		return this;

	}

	/**
	 * Returns the value of the vector component which matches the given index.
	 *
	 * @param {number} index - The component index. `0` equals to x, `1` equals to y.
	 * @return {number} A vector component value.
	 */
	getComponent( index ) {

		switch ( index ) {

			case 0: return this.x;
			case 1: return this.y;
			default: throw new Error( 'index is out of range: ' + index );

		}

	}

	/**
	 * Returns a new vector with copied values from this instance.
	 *
	 * @return {Vector2} A clone of this instance.
	 */
	clone() {

		return new this.constructor( this.x, this.y );

	}

	/**
	 * Copies the values of the given vector to this instance.
	 *
	 * @param {Vector2} v - The vector to copy.
	 * @return {Vector2} A reference to this vector.
	 */
	copy( v ) {

		this.x = v.x;
		this.y = v.y;

		return this;

	}

	/**
	 * Adds the given vector to this instance.
	 *
	 * @param {Vector2} v - The vector to add.
	 * @return {Vector2} A reference to this vector.
	 */
	add( v ) {

		this.x += v.x;
		this.y += v.y;

		return this;

	}

	/**
	 * Adds the given scalar value to all components of this instance.
	 *
	 * @param {number} s - The scalar to add.
	 * @return {Vector2} A reference to this vector.
	 */
	addScalar( s ) {

		this.x += s;
		this.y += s;

		return this;

	}

	/**
	 * Adds the given vectors and stores the result in this instance.
	 *
	 * @param {Vector2} a - The first vector.
	 * @param {Vector2} b - The second vector.
	 * @return {Vector2} A reference to this vector.
	 */
	addVectors( a, b ) {

		this.x = a.x + b.x;
		this.y = a.y + b.y;

		return this;

	}

	/**
	 * Adds the given vector scaled by the given factor to this instance.
	 *
	 * @param {Vector2} v - The vector.
	 * @param {number} s - The factor that scales `v`.
	 * @return {Vector2} A reference to this vector.
	 */
	addScaledVector( v, s ) {

		this.x += v.x * s;
		this.y += v.y * s;

		return this;

	}

	/**
	 * Subtracts the given vector from this instance.
	 *
	 * @param {Vector2} v - The vector to subtract.
	 * @return {Vector2} A reference to this vector.
	 */
	sub( v ) {

		this.x -= v.x;
		this.y -= v.y;

		return this;

	}

	/**
	 * Subtracts the given scalar value from all components of this instance.
	 *
	 * @param {number} s - The scalar to subtract.
	 * @return {Vector2} A reference to this vector.
	 */
	subScalar( s ) {

		this.x -= s;
		this.y -= s;

		return this;

	}

	/**
	 * Subtracts the given vectors and stores the result in this instance.
	 *
	 * @param {Vector2} a - The first vector.
	 * @param {Vector2} b - The second vector.
	 * @return {Vector2} A reference to this vector.
	 */
	subVectors( a, b ) {

		this.x = a.x - b.x;
		this.y = a.y - b.y;

		return this;

	}

	/**
	 * Multiplies the given vector with this instance.
	 *
	 * @param {Vector2} v - The vector to multiply.
	 * @return {Vector2} A reference to this vector.
	 */
	multiply( v ) {

		this.x *= v.x;
		this.y *= v.y;

		return this;

	}

	/**
	 * Multiplies the given scalar value with all components of this instance.
	 *
	 * @param {number} scalar - The scalar to multiply.
	 * @return {Vector2} A reference to this vector.
	 */
	multiplyScalar( scalar ) {

		this.x *= scalar;
		this.y *= scalar;

		return this;

	}

	/**
	 * Divides this instance by the given vector.
	 *
	 * @param {Vector2} v - The vector to divide.
	 * @return {Vector2} A reference to this vector.
	 */
	divide( v ) {

		this.x /= v.x;
		this.y /= v.y;

		return this;

	}

	/**
	 * Divides this vector by the given scalar.
	 *
	 * @param {number} scalar - The scalar to divide.
	 * @return {Vector2} A reference to this vector.
	 */
	divideScalar( scalar ) {

		return this.multiplyScalar( 1 / scalar );

	}

	/**
	 * Multiplies this vector (with an implicit 1 as the 3rd component) by
	 * the given 3x3 matrix.
	 *
	 * @param {Matrix3} m - The matrix to apply.
	 * @return {Vector2} A reference to this vector.
	 */
	applyMatrix3( m ) {

		const x = this.x, y = this.y;
		const e = m.elements;

		this.x = e[ 0 ] * x + e[ 3 ] * y + e[ 6 ];
		this.y = e[ 1 ] * x + e[ 4 ] * y + e[ 7 ];

		return this;

	}

	/**
	 * If this vector's x or y value is greater than the given vector's x or y
	 * value, replace that value with the corresponding min value.
	 *
	 * @param {Vector2} v - The vector.
	 * @return {Vector2} A reference to this vector.
	 */
	min( v ) {

		this.x = Math.min( this.x, v.x );
		this.y = Math.min( this.y, v.y );

		return this;

	}

	/**
	 * If this vector's x or y value is less than the given vector's x or y
	 * value, replace that value with the corresponding max value.
	 *
	 * @param {Vector2} v - The vector.
	 * @return {Vector2} A reference to this vector.
	 */
	max( v ) {

		this.x = Math.max( this.x, v.x );
		this.y = Math.max( this.y, v.y );

		return this;

	}

	/**
	 * If this vector's x or y value is greater than the max vector's x or y
	 * value, it is replaced by the corresponding value.
	 * If this vector's x or y value is less than the min vector's x or y value,
	 * it is replaced by the corresponding value.
	 *
	 * @param {Vector2} min - The minimum x and y values.
	 * @param {Vector2} max - The maximum x and y values in the desired range.
	 * @return {Vector2} A reference to this vector.
	 */
	clamp( min, max ) {

		// assumes min < max, componentwise

		this.x = clamp( this.x, min.x, max.x );
		this.y = clamp( this.y, min.y, max.y );

		return this;

	}

	/**
	 * If this vector's x or y values are greater than the max value, they are
	 * replaced by the max value.
	 * If this vector's x or y values are less than the min value, they are
	 * replaced by the min value.
	 *
	 * @param {number} minVal - The minimum value the components will be clamped to.
	 * @param {number} maxVal - The maximum value the components will be clamped to.
	 * @return {Vector2} A reference to this vector.
	 */
	clampScalar( minVal, maxVal ) {

		this.x = clamp( this.x, minVal, maxVal );
		this.y = clamp( this.y, minVal, maxVal );

		return this;

	}

	/**
	 * If this vector's length is greater than the max value, it is replaced by
	 * the max value.
	 * If this vector's length is less than the min value, it is replaced by the
	 * min value.
	 *
	 * @param {number} min - The minimum value the vector length will be clamped to.
	 * @param {number} max - The maximum value the vector length will be clamped to.
	 * @return {Vector2} A reference to this vector.
	 */
	clampLength( min, max ) {

		const length = this.length();

		return this.divideScalar( length || 1 ).multiplyScalar( clamp( length, min, max ) );

	}

	/**
	 * The components of this vector are rounded down to the nearest integer value.
	 *
	 * @return {Vector2} A reference to this vector.
	 */
	floor() {

		this.x = Math.floor( this.x );
		this.y = Math.floor( this.y );

		return this;

	}

	/**
	 * The components of this vector are rounded up to the nearest integer value.
	 *
	 * @return {Vector2} A reference to this vector.
	 */
	ceil() {

		this.x = Math.ceil( this.x );
		this.y = Math.ceil( this.y );

		return this;

	}

	/**
	 * The components of this vector are rounded to the nearest integer value
	 *
	 * @return {Vector2} A reference to this vector.
	 */
	round() {

		this.x = Math.round( this.x );
		this.y = Math.round( this.y );

		return this;

	}

	/**
	 * The components of this vector are rounded towards zero (up if negative,
	 * down if positive) to an integer value.
	 *
	 * @return {Vector2} A reference to this vector.
	 */
	roundToZero() {

		this.x = Math.trunc( this.x );
		this.y = Math.trunc( this.y );

		return this;

	}

	/**
	 * Inverts this vector - i.e. sets x = -x and y = -y.
	 *
	 * @return {Vector2} A reference to this vector.
	 */
	negate() {

		this.x = - this.x;
		this.y = - this.y;

		return this;

	}

	/**
	 * Calculates the dot product of the given vector with this instance.
	 *
	 * @param {Vector2} v - The vector to compute the dot product with.
	 * @return {number} The result of the dot product.
	 */
	dot( v ) {

		return this.x * v.x + this.y * v.y;

	}

	/**
	 * Calculates the cross product of the given vector with this instance.
	 *
	 * @param {Vector2} v - The vector to compute the cross product with.
	 * @return {number} The result of the cross product.
	 */
	cross( v ) {

		return this.x * v.y - this.y * v.x;

	}

	/**
	 * Computes the square of the Euclidean length (straight-line length) from
	 * (0, 0) to (x, y). If you are comparing the lengths of vectors, you should
	 * compare the length squared instead as it is slightly more efficient to calculate.
	 *
	 * @return {number} The square length of this vector.
	 */
	lengthSq() {

		return this.x * this.x + this.y * this.y;

	}

	/**
	 * Computes the  Euclidean length (straight-line length) from (0, 0) to (x, y).
	 *
	 * @return {number} The length of this vector.
	 */
	length() {

		return Math.sqrt( this.x * this.x + this.y * this.y );

	}

	/**
	 * Computes the Manhattan length of this vector.
	 *
	 * @return {number} The length of this vector.
	 */
	manhattanLength() {

		return Math.abs( this.x ) + Math.abs( this.y );

	}

	/**
	 * Converts this vector to a unit vector - that is, sets it equal to a vector
	 * with the same direction as this one, but with a vector length of `1`.
	 *
	 * @return {Vector2} A reference to this vector.
	 */
	normalize() {

		return this.divideScalar( this.length() || 1 );

	}

	/**
	 * Computes the angle in radians of this vector with respect to the positive x-axis.
	 *
	 * @return {number} The angle in radians.
	 */
	angle() {

		const angle = Math.atan2( - this.y, - this.x ) + Math.PI;

		return angle;

	}

	/**
	 * Returns the angle between the given vector and this instance in radians.
	 *
	 * @param {Vector2} v - The vector to compute the angle with.
	 * @return {number} The angle in radians.
	 */
	angleTo( v ) {

		const denominator = Math.sqrt( this.lengthSq() * v.lengthSq() );

		if ( denominator === 0 ) return Math.PI / 2;

		const theta = this.dot( v ) / denominator;

		// clamp, to handle numerical problems

		return Math.acos( clamp( theta, -1, 1 ) );

	}

	/**
	 * Computes the distance from the given vector to this instance.
	 *
	 * @param {Vector2} v - The vector to compute the distance to.
	 * @return {number} The distance.
	 */
	distanceTo( v ) {

		return Math.sqrt( this.distanceToSquared( v ) );

	}

	/**
	 * Computes the squared distance from the given vector to this instance.
	 * If you are just comparing the distance with another distance, you should compare
	 * the distance squared instead as it is slightly more efficient to calculate.
	 *
	 * @param {Vector2} v - The vector to compute the squared distance to.
	 * @return {number} The squared distance.
	 */
	distanceToSquared( v ) {

		const dx = this.x - v.x, dy = this.y - v.y;
		return dx * dx + dy * dy;

	}

	/**
	 * Computes the Manhattan distance from the given vector to this instance.
	 *
	 * @param {Vector2} v - The vector to compute the Manhattan distance to.
	 * @return {number} The Manhattan distance.
	 */
	manhattanDistanceTo( v ) {

		return Math.abs( this.x - v.x ) + Math.abs( this.y - v.y );

	}

	/**
	 * Sets this vector to a vector with the same direction as this one, but
	 * with the specified length.
	 *
	 * @param {number} length - The new length of this vector.
	 * @return {Vector2} A reference to this vector.
	 */
	setLength( length ) {

		return this.normalize().multiplyScalar( length );

	}

	/**
	 * Linearly interpolates between the given vector and this instance, where
	 * alpha is the percent distance along the line - alpha = 0 will be this
	 * vector, and alpha = 1 will be the given one.
	 *
	 * @param {Vector2} v - The vector to interpolate towards.
	 * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.
	 * @return {Vector2} A reference to this vector.
	 */
	lerp( v, alpha ) {

		this.x += ( v.x - this.x ) * alpha;
		this.y += ( v.y - this.y ) * alpha;

		return this;

	}

	/**
	 * Linearly interpolates between the given vectors, where alpha is the percent
	 * distance along the line - alpha = 0 will be first vector, and alpha = 1 will
	 * be the second one. The result is stored in this instance.
	 *
	 * @param {Vector2} v1 - The first vector.
	 * @param {Vector2} v2 - The second vector.
	 * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.
	 * @return {Vector2} A reference to this vector.
	 */
	lerpVectors( v1, v2, alpha ) {

		this.x = v1.x + ( v2.x - v1.x ) * alpha;
		this.y = v1.y + ( v2.y - v1.y ) * alpha;

		return this;

	}

	/**
	 * Returns `true` if this vector is equal with the given one.
	 *
	 * @param {Vector2} v - The vector to test for equality.
	 * @return {boolean} Whether this vector is equal with the given one.
	 */
	equals( v ) {

		return ( ( v.x === this.x ) && ( v.y === this.y ) );

	}

	/**
	 * Sets this vector's x value to be `array[ offset ]` and y
	 * value to be `array[ offset + 1 ]`.
	 *
	 * @param {Array<number>} array - An array holding the vector component values.
	 * @param {number} [offset=0] - The offset into the array.
	 * @return {Vector2} A reference to this vector.
	 */
	fromArray( array, offset = 0 ) {

		this.x = array[ offset ];
		this.y = array[ offset + 1 ];

		return this;

	}

	/**
	 * Writes the components of this vector to the given array. If no array is provided,
	 * the method returns a new instance.
	 *
	 * @param {Array<number>} [array=[]] - The target array holding the vector components.
	 * @param {number} [offset=0] - Index of the first element in the array.
	 * @return {Array<number>} The vector components.
	 */
	toArray( array = [], offset = 0 ) {

		array[ offset ] = this.x;
		array[ offset + 1 ] = this.y;

		return array;

	}

	/**
	 * Sets the components of this vector from the given buffer attribute.
	 *
	 * @param {BufferAttribute} attribute - The buffer attribute holding vector data.
	 * @param {number} index - The index into the attribute.
	 * @return {Vector2} A reference to this vector.
	 */
	fromBufferAttribute( attribute, index ) {

		this.x = attribute.getX( index );
		this.y = attribute.getY( index );

		return this;

	}

	/**
	 * Rotates this vector around the given center by the given angle.
	 *
	 * @param {Vector2} center - The point around which to rotate.
	 * @param {number} angle - The angle to rotate, in radians.
	 * @return {Vector2} A reference to this vector.
	 */
	rotateAround( center, angle ) {

		const c = Math.cos( angle ), s = Math.sin( angle );

		const x = this.x - center.x;
		const y = this.y - center.y;

		this.x = x * c - y * s + center.x;
		this.y = x * s + y * c + center.y;

		return this;

	}

	/**
	 * Sets each component of this vector to a pseudo-random value between `0` and
	 * `1`, excluding `1`.
	 *
	 * @return {Vector2} A reference to this vector.
	 */
	random() {

		this.x = Math.random();
		this.y = Math.random();

		return this;

	}

	*[ Symbol.iterator ]() {

		yield this.x;
		yield this.y;

	}

}

/**
 * Class for representing a Quaternion. Quaternions are used in three.js to represent rotations.
 *
 * Iterating through a vector instance will yield its components `(x, y, z, w)` in
 * the corresponding order.
 *
 * Note that three.js expects Quaternions to be normalized.
 * ```js
 * const quaternion = new THREE.Quaternion();
 * quaternion.setFromAxisAngle( new THREE.Vector3( 0, 1, 0 ), Math.PI / 2 );
 *
 * const vector = new THREE.Vector3( 1, 0, 0 );
 * vector.applyQuaternion( quaternion );
 * ```
 */
class Quaternion {

	/**
	 * Constructs a new quaternion.
	 *
	 * @param {number} [x=0] - The x value of this quaternion.
	 * @param {number} [y=0] - The y value of this quaternion.
	 * @param {number} [z=0] - The z value of this quaternion.
	 * @param {number} [w=1] - The w value of this quaternion.
	 */
	constructor( x = 0, y = 0, z = 0, w = 1 ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isQuaternion = true;

		this._x = x;
		this._y = y;
		this._z = z;
		this._w = w;

	}

	/**
	 * Interpolates between two quaternions via SLERP. This implementation assumes the
	 * quaternion data are managed  in flat arrays.
	 *
	 * @param {Array<number>} dst - The destination array.
	 * @param {number} dstOffset - An offset into the destination array.
	 * @param {Array<number>} src0 - The source array of the first quaternion.
	 * @param {number} srcOffset0 - An offset into the first source array.
	 * @param {Array<number>} src1 -  The source array of the second quaternion.
	 * @param {number} srcOffset1 - An offset into the second source array.
	 * @param {number} t - The interpolation factor in the range `[0,1]`.
	 * @see {@link Quaternion#slerp}
	 */
	static slerpFlat( dst, dstOffset, src0, srcOffset0, src1, srcOffset1, t ) {

		// fuzz-free, array-based Quaternion SLERP operation

		let x0 = src0[ srcOffset0 + 0 ],
			y0 = src0[ srcOffset0 + 1 ],
			z0 = src0[ srcOffset0 + 2 ],
			w0 = src0[ srcOffset0 + 3 ];

		const x1 = src1[ srcOffset1 + 0 ],
			y1 = src1[ srcOffset1 + 1 ],
			z1 = src1[ srcOffset1 + 2 ],
			w1 = src1[ srcOffset1 + 3 ];

		if ( t === 0 ) {

			dst[ dstOffset + 0 ] = x0;
			dst[ dstOffset + 1 ] = y0;
			dst[ dstOffset + 2 ] = z0;
			dst[ dstOffset + 3 ] = w0;
			return;

		}

		if ( t === 1 ) {

			dst[ dstOffset + 0 ] = x1;
			dst[ dstOffset + 1 ] = y1;
			dst[ dstOffset + 2 ] = z1;
			dst[ dstOffset + 3 ] = w1;
			return;

		}

		if ( w0 !== w1 || x0 !== x1 || y0 !== y1 || z0 !== z1 ) {

			let s = 1 - t;
			const cos = x0 * x1 + y0 * y1 + z0 * z1 + w0 * w1,
				dir = ( cos >= 0 ? 1 : -1 ),
				sqrSin = 1 - cos * cos;

			// Skip the Slerp for tiny steps to avoid numeric problems:
			if ( sqrSin > Number.EPSILON ) {

				const sin = Math.sqrt( sqrSin ),
					len = Math.atan2( sin, cos * dir );

				s = Math.sin( s * len ) / sin;
				t = Math.sin( t * len ) / sin;

			}

			const tDir = t * dir;

			x0 = x0 * s + x1 * tDir;
			y0 = y0 * s + y1 * tDir;
			z0 = z0 * s + z1 * tDir;
			w0 = w0 * s + w1 * tDir;

			// Normalize in case we just did a lerp:
			if ( s === 1 - t ) {

				const f = 1 / Math.sqrt( x0 * x0 + y0 * y0 + z0 * z0 + w0 * w0 );

				x0 *= f;
				y0 *= f;
				z0 *= f;
				w0 *= f;

			}

		}

		dst[ dstOffset ] = x0;
		dst[ dstOffset + 1 ] = y0;
		dst[ dstOffset + 2 ] = z0;
		dst[ dstOffset + 3 ] = w0;

	}

	/**
	 * Multiplies two quaternions. This implementation assumes the quaternion data are managed
	 * in flat arrays.
	 *
	 * @param {Array<number>} dst - The destination array.
	 * @param {number} dstOffset - An offset into the destination array.
	 * @param {Array<number>} src0 - The source array of the first quaternion.
	 * @param {number} srcOffset0 - An offset into the first source array.
	 * @param {Array<number>} src1 -  The source array of the second quaternion.
	 * @param {number} srcOffset1 - An offset into the second source array.
	 * @return {Array<number>} The destination array.
	 * @see {@link Quaternion#multiplyQuaternions}.
	 */
	static multiplyQuaternionsFlat( dst, dstOffset, src0, srcOffset0, src1, srcOffset1 ) {

		const x0 = src0[ srcOffset0 ];
		const y0 = src0[ srcOffset0 + 1 ];
		const z0 = src0[ srcOffset0 + 2 ];
		const w0 = src0[ srcOffset0 + 3 ];

		const x1 = src1[ srcOffset1 ];
		const y1 = src1[ srcOffset1 + 1 ];
		const z1 = src1[ srcOffset1 + 2 ];
		const w1 = src1[ srcOffset1 + 3 ];

		dst[ dstOffset ] = x0 * w1 + w0 * x1 + y0 * z1 - z0 * y1;
		dst[ dstOffset + 1 ] = y0 * w1 + w0 * y1 + z0 * x1 - x0 * z1;
		dst[ dstOffset + 2 ] = z0 * w1 + w0 * z1 + x0 * y1 - y0 * x1;
		dst[ dstOffset + 3 ] = w0 * w1 - x0 * x1 - y0 * y1 - z0 * z1;

		return dst;

	}

	/**
	 * The x value of this quaternion.
	 *
	 * @type {number}
	 * @default 0
	 */
	get x() {

		return this._x;

	}

	set x( value ) {

		this._x = value;
		this._onChangeCallback();

	}

	/**
	 * The y value of this quaternion.
	 *
	 * @type {number}
	 * @default 0
	 */
	get y() {

		return this._y;

	}

	set y( value ) {

		this._y = value;
		this._onChangeCallback();

	}

	/**
	 * The z value of this quaternion.
	 *
	 * @type {number}
	 * @default 0
	 */
	get z() {

		return this._z;

	}

	set z( value ) {

		this._z = value;
		this._onChangeCallback();

	}

	/**
	 * The w value of this quaternion.
	 *
	 * @type {number}
	 * @default 1
	 */
	get w() {

		return this._w;

	}

	set w( value ) {

		this._w = value;
		this._onChangeCallback();

	}

	/**
	 * Sets the quaternion components.
	 *
	 * @param {number} x - The x value of this quaternion.
	 * @param {number} y - The y value of this quaternion.
	 * @param {number} z - The z value of this quaternion.
	 * @param {number} w - The w value of this quaternion.
	 * @return {Quaternion} A reference to this quaternion.
	 */
	set( x, y, z, w ) {

		this._x = x;
		this._y = y;
		this._z = z;
		this._w = w;

		this._onChangeCallback();

		return this;

	}

	/**
	 * Returns a new quaternion with copied values from this instance.
	 *
	 * @return {Quaternion} A clone of this instance.
	 */
	clone() {

		return new this.constructor( this._x, this._y, this._z, this._w );

	}

	/**
	 * Copies the values of the given quaternion to this instance.
	 *
	 * @param {Quaternion} quaternion - The quaternion to copy.
	 * @return {Quaternion} A reference to this quaternion.
	 */
	copy( quaternion ) {

		this._x = quaternion.x;
		this._y = quaternion.y;
		this._z = quaternion.z;
		this._w = quaternion.w;

		this._onChangeCallback();

		return this;

	}

	/**
	 * Sets this quaternion from the rotation specified by the given
	 * Euler angles.
	 *
	 * @param {Euler} euler - The Euler angles.
	 * @param {boolean} [update=true] - Whether the internal `onChange` callback should be executed or not.
	 * @return {Quaternion} A reference to this quaternion.
	 */
	setFromEuler( euler, update = true ) {

		const x = euler._x, y = euler._y, z = euler._z, order = euler._order;

		// http://www.mathworks.com/matlabcentral/fileexchange/
		// 	20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/
		//	content/SpinCalc.m

		const cos = Math.cos;
		const sin = Math.sin;

		const c1 = cos( x / 2 );
		const c2 = cos( y / 2 );
		const c3 = cos( z / 2 );

		const s1 = sin( x / 2 );
		const s2 = sin( y / 2 );
		const s3 = sin( z / 2 );

		switch ( order ) {

			case 'XYZ':
				this._x = s1 * c2 * c3 + c1 * s2 * s3;
				this._y = c1 * s2 * c3 - s1 * c2 * s3;
				this._z = c1 * c2 * s3 + s1 * s2 * c3;
				this._w = c1 * c2 * c3 - s1 * s2 * s3;
				break;

			case 'YXZ':
				this._x = s1 * c2 * c3 + c1 * s2 * s3;
				this._y = c1 * s2 * c3 - s1 * c2 * s3;
				this._z = c1 * c2 * s3 - s1 * s2 * c3;
				this._w = c1 * c2 * c3 + s1 * s2 * s3;
				break;

			case 'ZXY':
				this._x = s1 * c2 * c3 - c1 * s2 * s3;
				this._y = c1 * s2 * c3 + s1 * c2 * s3;
				this._z = c1 * c2 * s3 + s1 * s2 * c3;
				this._w = c1 * c2 * c3 - s1 * s2 * s3;
				break;

			case 'ZYX':
				this._x = s1 * c2 * c3 - c1 * s2 * s3;
				this._y = c1 * s2 * c3 + s1 * c2 * s3;
				this._z = c1 * c2 * s3 - s1 * s2 * c3;
				this._w = c1 * c2 * c3 + s1 * s2 * s3;
				break;

			case 'YZX':
				this._x = s1 * c2 * c3 + c1 * s2 * s3;
				this._y = c1 * s2 * c3 + s1 * c2 * s3;
				this._z = c1 * c2 * s3 - s1 * s2 * c3;
				this._w = c1 * c2 * c3 - s1 * s2 * s3;
				break;

			case 'XZY':
				this._x = s1 * c2 * c3 - c1 * s2 * s3;
				this._y = c1 * s2 * c3 - s1 * c2 * s3;
				this._z = c1 * c2 * s3 + s1 * s2 * c3;
				this._w = c1 * c2 * c3 + s1 * s2 * s3;
				break;

			default:
				console.warn( 'THREE.Quaternion: .setFromEuler() encountered an unknown order: ' + order );

		}

		if ( update === true ) this._onChangeCallback();

		return this;

	}

	/**
	 * Sets this quaternion from the given axis and angle.
	 *
	 * @param {Vector3} axis - The normalized axis.
	 * @param {number} angle - The angle in radians.
	 * @return {Quaternion} A reference to this quaternion.
	 */
	setFromAxisAngle( axis, angle ) {

		// http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm

		const halfAngle = angle / 2, s = Math.sin( halfAngle );

		this._x = axis.x * s;
		this._y = axis.y * s;
		this._z = axis.z * s;
		this._w = Math.cos( halfAngle );

		this._onChangeCallback();

		return this;

	}

	/**
	 * Sets this quaternion from the given rotation matrix.
	 *
	 * @param {Matrix4} m - A 4x4 matrix of which the upper 3x3 of matrix is a pure rotation matrix (i.e. unscaled).
	 * @return {Quaternion} A reference to this quaternion.
	 */
	setFromRotationMatrix( m ) {

		// http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm

		// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)

		const te = m.elements,

			m11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ],
			m21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ],
			m31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ],

			trace = m11 + m22 + m33;

		if ( trace > 0 ) {

			const s = 0.5 / Math.sqrt( trace + 1.0 );

			this._w = 0.25 / s;
			this._x = ( m32 - m23 ) * s;
			this._y = ( m13 - m31 ) * s;
			this._z = ( m21 - m12 ) * s;

		} else if ( m11 > m22 && m11 > m33 ) {

			const s = 2.0 * Math.sqrt( 1.0 + m11 - m22 - m33 );

			this._w = ( m32 - m23 ) / s;
			this._x = 0.25 * s;
			this._y = ( m12 + m21 ) / s;
			this._z = ( m13 + m31 ) / s;

		} else if ( m22 > m33 ) {

			const s = 2.0 * Math.sqrt( 1.0 + m22 - m11 - m33 );

			this._w = ( m13 - m31 ) / s;
			this._x = ( m12 + m21 ) / s;
			this._y = 0.25 * s;
			this._z = ( m23 + m32 ) / s;

		} else {

			const s = 2.0 * Math.sqrt( 1.0 + m33 - m11 - m22 );

			this._w = ( m21 - m12 ) / s;
			this._x = ( m13 + m31 ) / s;
			this._y = ( m23 + m32 ) / s;
			this._z = 0.25 * s;

		}

		this._onChangeCallback();

		return this;

	}

	/**
	 * Sets this quaternion to the rotation required to rotate the direction vector
	 * `vFrom` to the direction vector `vTo`.
	 *
	 * @param {Vector3} vFrom - The first (normalized) direction vector.
	 * @param {Vector3} vTo - The second (normalized) direction vector.
	 * @return {Quaternion} A reference to this quaternion.
	 */
	setFromUnitVectors( vFrom, vTo ) {

		// assumes direction vectors vFrom and vTo are normalized

		let r = vFrom.dot( vTo ) + 1;

		if ( r < 1e-8 ) { // the epsilon value has been discussed in #31286

			// vFrom and vTo point in opposite directions

			r = 0;

			if ( Math.abs( vFrom.x ) > Math.abs( vFrom.z ) ) {

				this._x = - vFrom.y;
				this._y = vFrom.x;
				this._z = 0;
				this._w = r;

			} else {

				this._x = 0;
				this._y = - vFrom.z;
				this._z = vFrom.y;
				this._w = r;

			}

		} else {

			// crossVectors( vFrom, vTo ); // inlined to avoid cyclic dependency on Vector3

			this._x = vFrom.y * vTo.z - vFrom.z * vTo.y;
			this._y = vFrom.z * vTo.x - vFrom.x * vTo.z;
			this._z = vFrom.x * vTo.y - vFrom.y * vTo.x;
			this._w = r;

		}

		return this.normalize();

	}

	/**
	 * Returns the angle between this quaternion and the given one in radians.
	 *
	 * @param {Quaternion} q - The quaternion to compute the angle with.
	 * @return {number} The angle in radians.
	 */
	angleTo( q ) {

		return 2 * Math.acos( Math.abs( clamp( this.dot( q ), -1, 1 ) ) );

	}

	/**
	 * Rotates this quaternion by a given angular step to the given quaternion.
	 * The method ensures that the final quaternion will not overshoot `q`.
	 *
	 * @param {Quaternion} q - The target quaternion.
	 * @param {number} step - The angular step in radians.
	 * @return {Quaternion} A reference to this quaternion.
	 */
	rotateTowards( q, step ) {

		const angle = this.angleTo( q );

		if ( angle === 0 ) return this;

		const t = Math.min( 1, step / angle );

		this.slerp( q, t );

		return this;

	}

	/**
	 * Sets this quaternion to the identity quaternion; that is, to the
	 * quaternion that represents "no rotation".
	 *
	 * @return {Quaternion} A reference to this quaternion.
	 */
	identity() {

		return this.set( 0, 0, 0, 1 );

	}

	/**
	 * Inverts this quaternion via {@link Quaternion#conjugate}. The
	 * quaternion is assumed to have unit length.
	 *
	 * @return {Quaternion} A reference to this quaternion.
	 */
	invert() {

		return this.conjugate();

	}

	/**
	 * Returns the rotational conjugate of this quaternion. The conjugate of a
	 * quaternion represents the same rotation in the opposite direction about
	 * the rotational axis.
	 *
	 * @return {Quaternion} A reference to this quaternion.
	 */
	conjugate() {

		this._x *= -1;
		this._y *= -1;
		this._z *= -1;

		this._onChangeCallback();

		return this;

	}

	/**
	 * Calculates the dot product of this quaternion and the given one.
	 *
	 * @param {Quaternion} v - The quaternion to compute the dot product with.
	 * @return {number} The result of the dot product.
	 */
	dot( v ) {

		return this._x * v._x + this._y * v._y + this._z * v._z + this._w * v._w;

	}

	/**
	 * Computes the squared Euclidean length (straight-line length) of this quaternion,
	 * considered as a 4 dimensional vector. This can be useful if you are comparing the
	 * lengths of two quaternions, as this is a slightly more efficient calculation than
	 * {@link Quaternion#length}.
	 *
	 * @return {number} The squared Euclidean length.
	 */
	lengthSq() {

		return this._x * this._x + this._y * this._y + this._z * this._z + this._w * this._w;

	}

	/**
	 * Computes the Euclidean length (straight-line length) of this quaternion,
	 * considered as a 4 dimensional vector.
	 *
	 * @return {number} The Euclidean length.
	 */
	length() {

		return Math.sqrt( this._x * this._x + this._y * this._y + this._z * this._z + this._w * this._w );

	}

	/**
	 * Normalizes this quaternion - that is, calculated the quaternion that performs
	 * the same rotation as this one, but has a length equal to `1`.
	 *
	 * @return {Quaternion} A reference to this quaternion.
	 */
	normalize() {

		let l = this.length();

		if ( l === 0 ) {

			this._x = 0;
			this._y = 0;
			this._z = 0;
			this._w = 1;

		} else {

			l = 1 / l;

			this._x = this._x * l;
			this._y = this._y * l;
			this._z = this._z * l;
			this._w = this._w * l;

		}

		this._onChangeCallback();

		return this;

	}

	/**
	 * Multiplies this quaternion by the given one.
	 *
	 * @param {Quaternion} q - The quaternion.
	 * @return {Quaternion} A reference to this quaternion.
	 */
	multiply( q ) {

		return this.multiplyQuaternions( this, q );

	}

	/**
	 * Pre-multiplies this quaternion by the given one.
	 *
	 * @param {Quaternion} q - The quaternion.
	 * @return {Quaternion} A reference to this quaternion.
	 */
	premultiply( q ) {

		return this.multiplyQuaternions( q, this );

	}

	/**
	 * Multiplies the given quaternions and stores the result in this instance.
	 *
	 * @param {Quaternion} a - The first quaternion.
	 * @param {Quaternion} b - The second quaternion.
	 * @return {Quaternion} A reference to this quaternion.
	 */
	multiplyQuaternions( a, b ) {

		// from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm

		const qax = a._x, qay = a._y, qaz = a._z, qaw = a._w;
		const qbx = b._x, qby = b._y, qbz = b._z, qbw = b._w;

		this._x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby;
		this._y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz;
		this._z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx;
		this._w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz;

		this._onChangeCallback();

		return this;

	}

	/**
	 * Performs a spherical linear interpolation between quaternions.
	 *
	 * @param {Quaternion} qb - The target quaternion.
	 * @param {number} t - The interpolation factor in the closed interval `[0, 1]`.
	 * @return {Quaternion} A reference to this quaternion.
	 */
	slerp( qb, t ) {

		if ( t === 0 ) return this;
		if ( t === 1 ) return this.copy( qb );

		const x = this._x, y = this._y, z = this._z, w = this._w;

		// http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/

		let cosHalfTheta = w * qb._w + x * qb._x + y * qb._y + z * qb._z;

		if ( cosHalfTheta < 0 ) {

			this._w = - qb._w;
			this._x = - qb._x;
			this._y = - qb._y;
			this._z = - qb._z;

			cosHalfTheta = - cosHalfTheta;

		} else {

			this.copy( qb );

		}

		if ( cosHalfTheta >= 1.0 ) {

			this._w = w;
			this._x = x;
			this._y = y;
			this._z = z;

			return this;

		}

		const sqrSinHalfTheta = 1.0 - cosHalfTheta * cosHalfTheta;

		if ( sqrSinHalfTheta <= Number.EPSILON ) {

			const s = 1 - t;
			this._w = s * w + t * this._w;
			this._x = s * x + t * this._x;
			this._y = s * y + t * this._y;
			this._z = s * z + t * this._z;

			this.normalize(); // normalize calls _onChangeCallback()

			return this;

		}

		const sinHalfTheta = Math.sqrt( sqrSinHalfTheta );
		const halfTheta = Math.atan2( sinHalfTheta, cosHalfTheta );
		const ratioA = Math.sin( ( 1 - t ) * halfTheta ) / sinHalfTheta,
			ratioB = Math.sin( t * halfTheta ) / sinHalfTheta;

		this._w = ( w * ratioA + this._w * ratioB );
		this._x = ( x * ratioA + this._x * ratioB );
		this._y = ( y * ratioA + this._y * ratioB );
		this._z = ( z * ratioA + this._z * ratioB );

		this._onChangeCallback();

		return this;

	}

	/**
	 * Performs a spherical linear interpolation between the given quaternions
	 * and stores the result in this quaternion.
	 *
	 * @param {Quaternion} qa - The source quaternion.
	 * @param {Quaternion} qb - The target quaternion.
	 * @param {number} t - The interpolation factor in the closed interval `[0, 1]`.
	 * @return {Quaternion} A reference to this quaternion.
	 */
	slerpQuaternions( qa, qb, t ) {

		return this.copy( qa ).slerp( qb, t );

	}

	/**
	 * Sets this quaternion to a uniformly random, normalized quaternion.
	 *
	 * @return {Quaternion} A reference to this quaternion.
	 */
	random() {

		// Ken Shoemake
		// Uniform random rotations
		// D. Kirk, editor, Graphics Gems III, pages 124-132. Academic Press, New York, 1992.

		const theta1 = 2 * Math.PI * Math.random();
		const theta2 = 2 * Math.PI * Math.random();

		const x0 = Math.random();
		const r1 = Math.sqrt( 1 - x0 );
		const r2 = Math.sqrt( x0 );

		return this.set(
			r1 * Math.sin( theta1 ),
			r1 * Math.cos( theta1 ),
			r2 * Math.sin( theta2 ),
			r2 * Math.cos( theta2 ),
		);

	}

	/**
	 * Returns `true` if this quaternion is equal with the given one.
	 *
	 * @param {Quaternion} quaternion - The quaternion to test for equality.
	 * @return {boolean} Whether this quaternion is equal with the given one.
	 */
	equals( quaternion ) {

		return ( quaternion._x === this._x ) && ( quaternion._y === this._y ) && ( quaternion._z === this._z ) && ( quaternion._w === this._w );

	}

	/**
	 * Sets this quaternion's components from the given array.
	 *
	 * @param {Array<number>} array - An array holding the quaternion component values.
	 * @param {number} [offset=0] - The offset into the array.
	 * @return {Quaternion} A reference to this quaternion.
	 */
	fromArray( array, offset = 0 ) {

		this._x = array[ offset ];
		this._y = array[ offset + 1 ];
		this._z = array[ offset + 2 ];
		this._w = array[ offset + 3 ];

		this._onChangeCallback();

		return this;

	}

	/**
	 * Writes the components of this quaternion to the given array. If no array is provided,
	 * the method returns a new instance.
	 *
	 * @param {Array<number>} [array=[]] - The target array holding the quaternion components.
	 * @param {number} [offset=0] - Index of the first element in the array.
	 * @return {Array<number>} The quaternion components.
	 */
	toArray( array = [], offset = 0 ) {

		array[ offset ] = this._x;
		array[ offset + 1 ] = this._y;
		array[ offset + 2 ] = this._z;
		array[ offset + 3 ] = this._w;

		return array;

	}

	/**
	 * Sets the components of this quaternion from the given buffer attribute.
	 *
	 * @param {BufferAttribute} attribute - The buffer attribute holding quaternion data.
	 * @param {number} index - The index into the attribute.
	 * @return {Quaternion} A reference to this quaternion.
	 */
	fromBufferAttribute( attribute, index ) {

		this._x = attribute.getX( index );
		this._y = attribute.getY( index );
		this._z = attribute.getZ( index );
		this._w = attribute.getW( index );

		this._onChangeCallback();

		return this;

	}

	/**
	 * This methods defines the serialization result of this class. Returns the
	 * numerical elements of this quaternion in an array of format `[x, y, z, w]`.
	 *
	 * @return {Array<number>} The serialized quaternion.
	 */
	toJSON() {

		return this.toArray();

	}

	_onChange( callback ) {

		this._onChangeCallback = callback;

		return this;

	}

	_onChangeCallback() {}

	*[ Symbol.iterator ]() {

		yield this._x;
		yield this._y;
		yield this._z;
		yield this._w;

	}

}

/**
 * Class representing a 3D vector. A 3D vector is an ordered triplet of numbers
 * (labeled x, y and z), which can be used to represent a number of things, such as:
 *
 * - A point in 3D space.
 * - A direction and length in 3D space. In three.js the length will
 * always be the Euclidean distance(straight-line distance) from `(0, 0, 0)` to `(x, y, z)`
 * and the direction is also measured from `(0, 0, 0)` towards `(x, y, z)`.
 * - Any arbitrary ordered triplet of numbers.
 *
 * There are other things a 3D vector can be used to represent, such as
 * momentum vectors and so on, however these are the most
 * common uses in three.js.
 *
 * Iterating through a vector instance will yield its components `(x, y, z)` in
 * the corresponding order.
 * ```js
 * const a = new THREE.Vector3( 0, 1, 0 );
 *
 * //no arguments; will be initialised to (0, 0, 0)
 * const b = new THREE.Vector3( );
 *
 * const d = a.distanceTo( b );
 * ```
 */
class Vector3 {

	/**
	 * Constructs a new 3D vector.
	 *
	 * @param {number} [x=0] - The x value of this vector.
	 * @param {number} [y=0] - The y value of this vector.
	 * @param {number} [z=0] - The z value of this vector.
	 */
	constructor( x = 0, y = 0, z = 0 ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		Vector3.prototype.isVector3 = true;

		/**
		 * The x value of this vector.
		 *
		 * @type {number}
		 */
		this.x = x;

		/**
		 * The y value of this vector.
		 *
		 * @type {number}
		 */
		this.y = y;

		/**
		 * The z value of this vector.
		 *
		 * @type {number}
		 */
		this.z = z;

	}

	/**
	 * Sets the vector components.
	 *
	 * @param {number} x - The value of the x component.
	 * @param {number} y - The value of the y component.
	 * @param {number} z - The value of the z component.
	 * @return {Vector3} A reference to this vector.
	 */
	set( x, y, z ) {

		if ( z === undefined ) z = this.z; // sprite.scale.set(x,y)

		this.x = x;
		this.y = y;
		this.z = z;

		return this;

	}

	/**
	 * Sets the vector components to the same value.
	 *
	 * @param {number} scalar - The value to set for all vector components.
	 * @return {Vector3} A reference to this vector.
	 */
	setScalar( scalar ) {

		this.x = scalar;
		this.y = scalar;
		this.z = scalar;

		return this;

	}

	/**
	 * Sets the vector's x component to the given value
	 *
	 * @param {number} x - The value to set.
	 * @return {Vector3} A reference to this vector.
	 */
	setX( x ) {

		this.x = x;

		return this;

	}

	/**
	 * Sets the vector's y component to the given value
	 *
	 * @param {number} y - The value to set.
	 * @return {Vector3} A reference to this vector.
	 */
	setY( y ) {

		this.y = y;

		return this;

	}

	/**
	 * Sets the vector's z component to the given value
	 *
	 * @param {number} z - The value to set.
	 * @return {Vector3} A reference to this vector.
	 */
	setZ( z ) {

		this.z = z;

		return this;

	}

	/**
	 * Allows to set a vector component with an index.
	 *
	 * @param {number} index - The component index. `0` equals to x, `1` equals to y, `2` equals to z.
	 * @param {number} value - The value to set.
	 * @return {Vector3} A reference to this vector.
	 */
	setComponent( index, value ) {

		switch ( index ) {

			case 0: this.x = value; break;
			case 1: this.y = value; break;
			case 2: this.z = value; break;
			default: throw new Error( 'index is out of range: ' + index );

		}

		return this;

	}

	/**
	 * Returns the value of the vector component which matches the given index.
	 *
	 * @param {number} index - The component index. `0` equals to x, `1` equals to y, `2` equals to z.
	 * @return {number} A vector component value.
	 */
	getComponent( index ) {

		switch ( index ) {

			case 0: return this.x;
			case 1: return this.y;
			case 2: return this.z;
			default: throw new Error( 'index is out of range: ' + index );

		}

	}

	/**
	 * Returns a new vector with copied values from this instance.
	 *
	 * @return {Vector3} A clone of this instance.
	 */
	clone() {

		return new this.constructor( this.x, this.y, this.z );

	}

	/**
	 * Copies the values of the given vector to this instance.
	 *
	 * @param {Vector3} v - The vector to copy.
	 * @return {Vector3} A reference to this vector.
	 */
	copy( v ) {

		this.x = v.x;
		this.y = v.y;
		this.z = v.z;

		return this;

	}

	/**
	 * Adds the given vector to this instance.
	 *
	 * @param {Vector3} v - The vector to add.
	 * @return {Vector3} A reference to this vector.
	 */
	add( v ) {

		this.x += v.x;
		this.y += v.y;
		this.z += v.z;

		return this;

	}

	/**
	 * Adds the given scalar value to all components of this instance.
	 *
	 * @param {number} s - The scalar to add.
	 * @return {Vector3} A reference to this vector.
	 */
	addScalar( s ) {

		this.x += s;
		this.y += s;
		this.z += s;

		return this;

	}

	/**
	 * Adds the given vectors and stores the result in this instance.
	 *
	 * @param {Vector3} a - The first vector.
	 * @param {Vector3} b - The second vector.
	 * @return {Vector3} A reference to this vector.
	 */
	addVectors( a, b ) {

		this.x = a.x + b.x;
		this.y = a.y + b.y;
		this.z = a.z + b.z;

		return this;

	}

	/**
	 * Adds the given vector scaled by the given factor to this instance.
	 *
	 * @param {Vector3|Vector4} v - The vector.
	 * @param {number} s - The factor that scales `v`.
	 * @return {Vector3} A reference to this vector.
	 */
	addScaledVector( v, s ) {

		this.x += v.x * s;
		this.y += v.y * s;
		this.z += v.z * s;

		return this;

	}

	/**
	 * Subtracts the given vector from this instance.
	 *
	 * @param {Vector3} v - The vector to subtract.
	 * @return {Vector3} A reference to this vector.
	 */
	sub( v ) {

		this.x -= v.x;
		this.y -= v.y;
		this.z -= v.z;

		return this;

	}

	/**
	 * Subtracts the given scalar value from all components of this instance.
	 *
	 * @param {number} s - The scalar to subtract.
	 * @return {Vector3} A reference to this vector.
	 */
	subScalar( s ) {

		this.x -= s;
		this.y -= s;
		this.z -= s;

		return this;

	}

	/**
	 * Subtracts the given vectors and stores the result in this instance.
	 *
	 * @param {Vector3} a - The first vector.
	 * @param {Vector3} b - The second vector.
	 * @return {Vector3} A reference to this vector.
	 */
	subVectors( a, b ) {

		this.x = a.x - b.x;
		this.y = a.y - b.y;
		this.z = a.z - b.z;

		return this;

	}

	/**
	 * Multiplies the given vector with this instance.
	 *
	 * @param {Vector3} v - The vector to multiply.
	 * @return {Vector3} A reference to this vector.
	 */
	multiply( v ) {

		this.x *= v.x;
		this.y *= v.y;
		this.z *= v.z;

		return this;

	}

	/**
	 * Multiplies the given scalar value with all components of this instance.
	 *
	 * @param {number} scalar - The scalar to multiply.
	 * @return {Vector3} A reference to this vector.
	 */
	multiplyScalar( scalar ) {

		this.x *= scalar;
		this.y *= scalar;
		this.z *= scalar;

		return this;

	}

	/**
	 * Multiplies the given vectors and stores the result in this instance.
	 *
	 * @param {Vector3} a - The first vector.
	 * @param {Vector3} b - The second vector.
	 * @return {Vector3} A reference to this vector.
	 */
	multiplyVectors( a, b ) {

		this.x = a.x * b.x;
		this.y = a.y * b.y;
		this.z = a.z * b.z;

		return this;

	}

	/**
	 * Applies the given Euler rotation to this vector.
	 *
	 * @param {Euler} euler - The Euler angles.
	 * @return {Vector3} A reference to this vector.
	 */
	applyEuler( euler ) {

		return this.applyQuaternion( _quaternion$2.setFromEuler( euler ) );

	}

	/**
	 * Applies a rotation specified by an axis and an angle to this vector.
	 *
	 * @param {Vector3} axis - A normalized vector representing the rotation axis.
	 * @param {number} angle - The angle in radians.
	 * @return {Vector3} A reference to this vector.
	 */
	applyAxisAngle( axis, angle ) {

		return this.applyQuaternion( _quaternion$2.setFromAxisAngle( axis, angle ) );

	}

	/**
	 * Multiplies this vector with the given 3x3 matrix.
	 *
	 * @param {Matrix3} m - The 3x3 matrix.
	 * @return {Vector3} A reference to this vector.
	 */
	applyMatrix3( m ) {

		const x = this.x, y = this.y, z = this.z;
		const e = m.elements;

		this.x = e[ 0 ] * x + e[ 3 ] * y + e[ 6 ] * z;
		this.y = e[ 1 ] * x + e[ 4 ] * y + e[ 7 ] * z;
		this.z = e[ 2 ] * x + e[ 5 ] * y + e[ 8 ] * z;

		return this;

	}

	/**
	 * Multiplies this vector by the given normal matrix and normalizes
	 * the result.
	 *
	 * @param {Matrix3} m - The normal matrix.
	 * @return {Vector3} A reference to this vector.
	 */
	applyNormalMatrix( m ) {

		return this.applyMatrix3( m ).normalize();

	}

	/**
	 * Multiplies this vector (with an implicit 1 in the 4th dimension) by m, and
	 * divides by perspective.
	 *
	 * @param {Matrix4} m - The matrix to apply.
	 * @return {Vector3} A reference to this vector.
	 */
	applyMatrix4( m ) {

		const x = this.x, y = this.y, z = this.z;
		const e = m.elements;

		const w = 1 / ( e[ 3 ] * x + e[ 7 ] * y + e[ 11 ] * z + e[ 15 ] );

		this.x = ( e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ] ) * w;
		this.y = ( e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ] ) * w;
		this.z = ( e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z + e[ 14 ] ) * w;

		return this;

	}

	/**
	 * Applies the given Quaternion to this vector.
	 *
	 * @param {Quaternion} q - The Quaternion.
	 * @return {Vector3} A reference to this vector.
	 */
	applyQuaternion( q ) {

		// quaternion q is assumed to have unit length

		const vx = this.x, vy = this.y, vz = this.z;
		const qx = q.x, qy = q.y, qz = q.z, qw = q.w;

		// t = 2 * cross( q.xyz, v );
		const tx = 2 * ( qy * vz - qz * vy );
		const ty = 2 * ( qz * vx - qx * vz );
		const tz = 2 * ( qx * vy - qy * vx );

		// v + q.w * t + cross( q.xyz, t );
		this.x = vx + qw * tx + qy * tz - qz * ty;
		this.y = vy + qw * ty + qz * tx - qx * tz;
		this.z = vz + qw * tz + qx * ty - qy * tx;

		return this;

	}

	/**
	 * Projects this vector from world space into the camera's normalized
	 * device coordinate (NDC) space.
	 *
	 * @param {Camera} camera - The camera.
	 * @return {Vector3} A reference to this vector.
	 */
	project( camera ) {

		return this.applyMatrix4( camera.matrixWorldInverse ).applyMatrix4( camera.projectionMatrix );

	}

	/**
	 * Unprojects this vector from the camera's normalized device coordinate (NDC)
	 * space into world space.
	 *
	 * @param {Camera} camera - The camera.
	 * @return {Vector3} A reference to this vector.
	 */
	unproject( camera ) {

		return this.applyMatrix4( camera.projectionMatrixInverse ).applyMatrix4( camera.matrixWorld );

	}

	/**
	 * Transforms the direction of this vector by a matrix (the upper left 3 x 3
	 * subset of the given 4x4 matrix and then normalizes the result.
	 *
	 * @param {Matrix4} m - The matrix.
	 * @return {Vector3} A reference to this vector.
	 */
	transformDirection( m ) {

		// input: THREE.Matrix4 affine matrix
		// vector interpreted as a direction

		const x = this.x, y = this.y, z = this.z;
		const e = m.elements;

		this.x = e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z;
		this.y = e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z;
		this.z = e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z;

		return this.normalize();

	}

	/**
	 * Divides this instance by the given vector.
	 *
	 * @param {Vector3} v - The vector to divide.
	 * @return {Vector3} A reference to this vector.
	 */
	divide( v ) {

		this.x /= v.x;
		this.y /= v.y;
		this.z /= v.z;

		return this;

	}

	/**
	 * Divides this vector by the given scalar.
	 *
	 * @param {number} scalar - The scalar to divide.
	 * @return {Vector3} A reference to this vector.
	 */
	divideScalar( scalar ) {

		return this.multiplyScalar( 1 / scalar );

	}

	/**
	 * If this vector's x, y or z value is greater than the given vector's x, y or z
	 * value, replace that value with the corresponding min value.
	 *
	 * @param {Vector3} v - The vector.
	 * @return {Vector3} A reference to this vector.
	 */
	min( v ) {

		this.x = Math.min( this.x, v.x );
		this.y = Math.min( this.y, v.y );
		this.z = Math.min( this.z, v.z );

		return this;

	}

	/**
	 * If this vector's x, y or z value is less than the given vector's x, y or z
	 * value, replace that value with the corresponding max value.
	 *
	 * @param {Vector3} v - The vector.
	 * @return {Vector3} A reference to this vector.
	 */
	max( v ) {

		this.x = Math.max( this.x, v.x );
		this.y = Math.max( this.y, v.y );
		this.z = Math.max( this.z, v.z );

		return this;

	}

	/**
	 * If this vector's x, y or z value is greater than the max vector's x, y or z
	 * value, it is replaced by the corresponding value.
	 * If this vector's x, y or z value is less than the min vector's x, y or z value,
	 * it is replaced by the corresponding value.
	 *
	 * @param {Vector3} min - The minimum x, y and z values.
	 * @param {Vector3} max - The maximum x, y and z values in the desired range.
	 * @return {Vector3} A reference to this vector.
	 */
	clamp( min, max ) {

		// assumes min < max, componentwise

		this.x = clamp( this.x, min.x, max.x );
		this.y = clamp( this.y, min.y, max.y );
		this.z = clamp( this.z, min.z, max.z );

		return this;

	}

	/**
	 * If this vector's x, y or z values are greater than the max value, they are
	 * replaced by the max value.
	 * If this vector's x, y or z values are less than the min value, they are
	 * replaced by the min value.
	 *
	 * @param {number} minVal - The minimum value the components will be clamped to.
	 * @param {number} maxVal - The maximum value the components will be clamped to.
	 * @return {Vector3} A reference to this vector.
	 */
	clampScalar( minVal, maxVal ) {

		this.x = clamp( this.x, minVal, maxVal );
		this.y = clamp( this.y, minVal, maxVal );
		this.z = clamp( this.z, minVal, maxVal );

		return this;

	}

	/**
	 * If this vector's length is greater than the max value, it is replaced by
	 * the max value.
	 * If this vector's length is less than the min value, it is replaced by the
	 * min value.
	 *
	 * @param {number} min - The minimum value the vector length will be clamped to.
	 * @param {number} max - The maximum value the vector length will be clamped to.
	 * @return {Vector3} A reference to this vector.
	 */
	clampLength( min, max ) {

		const length = this.length();

		return this.divideScalar( length || 1 ).multiplyScalar( clamp( length, min, max ) );

	}

	/**
	 * The components of this vector are rounded down to the nearest integer value.
	 *
	 * @return {Vector3} A reference to this vector.
	 */
	floor() {

		this.x = Math.floor( this.x );
		this.y = Math.floor( this.y );
		this.z = Math.floor( this.z );

		return this;

	}

	/**
	 * The components of this vector are rounded up to the nearest integer value.
	 *
	 * @return {Vector3} A reference to this vector.
	 */
	ceil() {

		this.x = Math.ceil( this.x );
		this.y = Math.ceil( this.y );
		this.z = Math.ceil( this.z );

		return this;

	}

	/**
	 * The components of this vector are rounded to the nearest integer value
	 *
	 * @return {Vector3} A reference to this vector.
	 */
	round() {

		this.x = Math.round( this.x );
		this.y = Math.round( this.y );
		this.z = Math.round( this.z );

		return this;

	}

	/**
	 * The components of this vector are rounded towards zero (up if negative,
	 * down if positive) to an integer value.
	 *
	 * @return {Vector3} A reference to this vector.
	 */
	roundToZero() {

		this.x = Math.trunc( this.x );
		this.y = Math.trunc( this.y );
		this.z = Math.trunc( this.z );

		return this;

	}

	/**
	 * Inverts this vector - i.e. sets x = -x, y = -y and z = -z.
	 *
	 * @return {Vector3} A reference to this vector.
	 */
	negate() {

		this.x = - this.x;
		this.y = - this.y;
		this.z = - this.z;

		return this;

	}

	/**
	 * Calculates the dot product of the given vector with this instance.
	 *
	 * @param {Vector3} v - The vector to compute the dot product with.
	 * @return {number} The result of the dot product.
	 */
	dot( v ) {

		return this.x * v.x + this.y * v.y + this.z * v.z;

	}

	// TODO lengthSquared?

	/**
	 * Computes the square of the Euclidean length (straight-line length) from
	 * (0, 0, 0) to (x, y, z). If you are comparing the lengths of vectors, you should
	 * compare the length squared instead as it is slightly more efficient to calculate.
	 *
	 * @return {number} The square length of this vector.
	 */
	lengthSq() {

		return this.x * this.x + this.y * this.y + this.z * this.z;

	}

	/**
	 * Computes the  Euclidean length (straight-line length) from (0, 0, 0) to (x, y, z).
	 *
	 * @return {number} The length of this vector.
	 */
	length() {

		return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z );

	}

	/**
	 * Computes the Manhattan length of this vector.
	 *
	 * @return {number} The length of this vector.
	 */
	manhattanLength() {

		return Math.abs( this.x ) + Math.abs( this.y ) + Math.abs( this.z );

	}

	/**
	 * Converts this vector to a unit vector - that is, sets it equal to a vector
	 * with the same direction as this one, but with a vector length of `1`.
	 *
	 * @return {Vector3} A reference to this vector.
	 */
	normalize() {

		return this.divideScalar( this.length() || 1 );

	}

	/**
	 * Sets this vector to a vector with the same direction as this one, but
	 * with the specified length.
	 *
	 * @param {number} length - The new length of this vector.
	 * @return {Vector3} A reference to this vector.
	 */
	setLength( length ) {

		return this.normalize().multiplyScalar( length );

	}

	/**
	 * Linearly interpolates between the given vector and this instance, where
	 * alpha is the percent distance along the line - alpha = 0 will be this
	 * vector, and alpha = 1 will be the given one.
	 *
	 * @param {Vector3} v - The vector to interpolate towards.
	 * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.
	 * @return {Vector3} A reference to this vector.
	 */
	lerp( v, alpha ) {

		this.x += ( v.x - this.x ) * alpha;
		this.y += ( v.y - this.y ) * alpha;
		this.z += ( v.z - this.z ) * alpha;

		return this;

	}

	/**
	 * Linearly interpolates between the given vectors, where alpha is the percent
	 * distance along the line - alpha = 0 will be first vector, and alpha = 1 will
	 * be the second one. The result is stored in this instance.
	 *
	 * @param {Vector3} v1 - The first vector.
	 * @param {Vector3} v2 - The second vector.
	 * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.
	 * @return {Vector3} A reference to this vector.
	 */
	lerpVectors( v1, v2, alpha ) {

		this.x = v1.x + ( v2.x - v1.x ) * alpha;
		this.y = v1.y + ( v2.y - v1.y ) * alpha;
		this.z = v1.z + ( v2.z - v1.z ) * alpha;

		return this;

	}

	/**
	 * Calculates the cross product of the given vector with this instance.
	 *
	 * @param {Vector3} v - The vector to compute the cross product with.
	 * @return {Vector3} The result of the cross product.
	 */
	cross( v ) {

		return this.crossVectors( this, v );

	}

	/**
	 * Calculates the cross product of the given vectors and stores the result
	 * in this instance.
	 *
	 * @param {Vector3} a - The first vector.
	 * @param {Vector3} b - The second vector.
	 * @return {Vector3} A reference to this vector.
	 */
	crossVectors( a, b ) {

		const ax = a.x, ay = a.y, az = a.z;
		const bx = b.x, by = b.y, bz = b.z;

		this.x = ay * bz - az * by;
		this.y = az * bx - ax * bz;
		this.z = ax * by - ay * bx;

		return this;

	}

	/**
	 * Projects this vector onto the given one.
	 *
	 * @param {Vector3} v - The vector to project to.
	 * @return {Vector3} A reference to this vector.
	 */
	projectOnVector( v ) {

		const denominator = v.lengthSq();

		if ( denominator === 0 ) return this.set( 0, 0, 0 );

		const scalar = v.dot( this ) / denominator;

		return this.copy( v ).multiplyScalar( scalar );

	}

	/**
	 * Projects this vector onto a plane by subtracting this
	 * vector projected onto the plane's normal from this vector.
	 *
	 * @param {Vector3} planeNormal - The plane normal.
	 * @return {Vector3} A reference to this vector.
	 */
	projectOnPlane( planeNormal ) {

		_vector$8.copy( this ).projectOnVector( planeNormal );

		return this.sub( _vector$8 );

	}

	/**
	 * Reflects this vector off a plane orthogonal to the given normal vector.
	 *
	 * @param {Vector3} normal - The (normalized) normal vector.
	 * @return {Vector3} A reference to this vector.
	 */
	reflect( normal ) {

		return this.sub( _vector$8.copy( normal ).multiplyScalar( 2 * this.dot( normal ) ) );

	}
	/**
	 * Returns the angle between the given vector and this instance in radians.
	 *
	 * @param {Vector3} v - The vector to compute the angle with.
	 * @return {number} The angle in radians.
	 */
	angleTo( v ) {

		const denominator = Math.sqrt( this.lengthSq() * v.lengthSq() );

		if ( denominator === 0 ) return Math.PI / 2;

		const theta = this.dot( v ) / denominator;

		// clamp, to handle numerical problems

		return Math.acos( clamp( theta, -1, 1 ) );

	}

	/**
	 * Computes the distance from the given vector to this instance.
	 *
	 * @param {Vector3} v - The vector to compute the distance to.
	 * @return {number} The distance.
	 */
	distanceTo( v ) {

		return Math.sqrt( this.distanceToSquared( v ) );

	}

	/**
	 * Computes the squared distance from the given vector to this instance.
	 * If you are just comparing the distance with another distance, you should compare
	 * the distance squared instead as it is slightly more efficient to calculate.
	 *
	 * @param {Vector3} v - The vector to compute the squared distance to.
	 * @return {number} The squared distance.
	 */
	distanceToSquared( v ) {

		const dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z;

		return dx * dx + dy * dy + dz * dz;

	}

	/**
	 * Computes the Manhattan distance from the given vector to this instance.
	 *
	 * @param {Vector3} v - The vector to compute the Manhattan distance to.
	 * @return {number} The Manhattan distance.
	 */
	manhattanDistanceTo( v ) {

		return Math.abs( this.x - v.x ) + Math.abs( this.y - v.y ) + Math.abs( this.z - v.z );

	}

	/**
	 * Sets the vector components from the given spherical coordinates.
	 *
	 * @param {Spherical} s - The spherical coordinates.
	 * @return {Vector3} A reference to this vector.
	 */
	setFromSpherical( s ) {

		return this.setFromSphericalCoords( s.radius, s.phi, s.theta );

	}

	/**
	 * Sets the vector components from the given spherical coordinates.
	 *
	 * @param {number} radius - The radius.
	 * @param {number} phi - The phi angle in radians.
	 * @param {number} theta - The theta angle in radians.
	 * @return {Vector3} A reference to this vector.
	 */
	setFromSphericalCoords( radius, phi, theta ) {

		const sinPhiRadius = Math.sin( phi ) * radius;

		this.x = sinPhiRadius * Math.sin( theta );
		this.y = Math.cos( phi ) * radius;
		this.z = sinPhiRadius * Math.cos( theta );

		return this;

	}

	/**
	 * Sets the vector components from the given cylindrical coordinates.
	 *
	 * @param {Cylindrical} c - The cylindrical coordinates.
	 * @return {Vector3} A reference to this vector.
	 */
	setFromCylindrical( c ) {

		return this.setFromCylindricalCoords( c.radius, c.theta, c.y );

	}

	/**
	 * Sets the vector components from the given cylindrical coordinates.
	 *
	 * @param {number} radius - The radius.
	 * @param {number} theta - The theta angle in radians.
	 * @param {number} y - The y value.
	 * @return {Vector3} A reference to this vector.
	 */
	setFromCylindricalCoords( radius, theta, y ) {

		this.x = radius * Math.sin( theta );
		this.y = y;
		this.z = radius * Math.cos( theta );

		return this;

	}

	/**
	 * Sets the vector components to the position elements of the
	 * given transformation matrix.
	 *
	 * @param {Matrix4} m - The 4x4 matrix.
	 * @return {Vector3} A reference to this vector.
	 */
	setFromMatrixPosition( m ) {

		const e = m.elements;

		this.x = e[ 12 ];
		this.y = e[ 13 ];
		this.z = e[ 14 ];

		return this;

	}

	/**
	 * Sets the vector components to the scale elements of the
	 * given transformation matrix.
	 *
	 * @param {Matrix4} m - The 4x4 matrix.
	 * @return {Vector3} A reference to this vector.
	 */
	setFromMatrixScale( m ) {

		const sx = this.setFromMatrixColumn( m, 0 ).length();
		const sy = this.setFromMatrixColumn( m, 1 ).length();
		const sz = this.setFromMatrixColumn( m, 2 ).length();

		this.x = sx;
		this.y = sy;
		this.z = sz;

		return this;

	}

	/**
	 * Sets the vector components from the specified matrix column.
	 *
	 * @param {Matrix4} m - The 4x4 matrix.
	 * @param {number} index - The column index.
	 * @return {Vector3} A reference to this vector.
	 */
	setFromMatrixColumn( m, index ) {

		return this.fromArray( m.elements, index * 4 );

	}

	/**
	 * Sets the vector components from the specified matrix column.
	 *
	 * @param {Matrix3} m - The 3x3 matrix.
	 * @param {number} index - The column index.
	 * @return {Vector3} A reference to this vector.
	 */
	setFromMatrix3Column( m, index ) {

		return this.fromArray( m.elements, index * 3 );

	}

	/**
	 * Sets the vector components from the given Euler angles.
	 *
	 * @param {Euler} e - The Euler angles to set.
	 * @return {Vector3} A reference to this vector.
	 */
	setFromEuler( e ) {

		this.x = e._x;
		this.y = e._y;
		this.z = e._z;

		return this;

	}

	/**
	 * Sets the vector components from the RGB components of the
	 * given color.
	 *
	 * @param {Color} c - The color to set.
	 * @return {Vector3} A reference to this vector.
	 */
	setFromColor( c ) {

		this.x = c.r;
		this.y = c.g;
		this.z = c.b;

		return this;

	}

	/**
	 * Returns `true` if this vector is equal with the given one.
	 *
	 * @param {Vector3} v - The vector to test for equality.
	 * @return {boolean} Whether this vector is equal with the given one.
	 */
	equals( v ) {

		return ( ( v.x === this.x ) && ( v.y === this.y ) && ( v.z === this.z ) );

	}

	/**
	 * Sets this vector's x value to be `array[ offset ]`, y value to be `array[ offset + 1 ]`
	 * and z value to be `array[ offset + 2 ]`.
	 *
	 * @param {Array<number>} array - An array holding the vector component values.
	 * @param {number} [offset=0] - The offset into the array.
	 * @return {Vector3} A reference to this vector.
	 */
	fromArray( array, offset = 0 ) {

		this.x = array[ offset ];
		this.y = array[ offset + 1 ];
		this.z = array[ offset + 2 ];

		return this;

	}

	/**
	 * Writes the components of this vector to the given array. If no array is provided,
	 * the method returns a new instance.
	 *
	 * @param {Array<number>} [array=[]] - The target array holding the vector components.
	 * @param {number} [offset=0] - Index of the first element in the array.
	 * @return {Array<number>} The vector components.
	 */
	toArray( array = [], offset = 0 ) {

		array[ offset ] = this.x;
		array[ offset + 1 ] = this.y;
		array[ offset + 2 ] = this.z;

		return array;

	}

	/**
	 * Sets the components of this vector from the given buffer attribute.
	 *
	 * @param {BufferAttribute} attribute - The buffer attribute holding vector data.
	 * @param {number} index - The index into the attribute.
	 * @return {Vector3} A reference to this vector.
	 */
	fromBufferAttribute( attribute, index ) {

		this.x = attribute.getX( index );
		this.y = attribute.getY( index );
		this.z = attribute.getZ( index );

		return this;

	}

	/**
	 * Sets each component of this vector to a pseudo-random value between `0` and
	 * `1`, excluding `1`.
	 *
	 * @return {Vector3} A reference to this vector.
	 */
	random() {

		this.x = Math.random();
		this.y = Math.random();
		this.z = Math.random();

		return this;

	}

	/**
	 * Sets this vector to a uniformly random point on a unit sphere.
	 *
	 * @return {Vector3} A reference to this vector.
	 */
	randomDirection() {

		// https://mathworld.wolfram.com/SpherePointPicking.html

		const theta = Math.random() * Math.PI * 2;
		const u = Math.random() * 2 - 1;
		const c = Math.sqrt( 1 - u * u );

		this.x = c * Math.cos( theta );
		this.y = u;
		this.z = c * Math.sin( theta );

		return this;

	}

	*[ Symbol.iterator ]() {

		yield this.x;
		yield this.y;
		yield this.z;

	}

}

const _vector$8 = /*@__PURE__*/ new Vector3();
const _quaternion$2 = /*@__PURE__*/ new Quaternion();

/**
 * Represents a 3x3 matrix.
 *
 * A Note on Row-Major and Column-Major Ordering:
 *
 * The constructor and {@link Matrix3#set} method take arguments in
 * [row-major]{@link https://en.wikipedia.org/wiki/Row-_and_column-major_order#Column-major_order}
 * order, while internally they are stored in the {@link Matrix3#elements} array in column-major order.
 * This means that calling:
 * ```js
 * const m = new THREE.Matrix();
 * m.set( 11, 12, 13,
 *        21, 22, 23,
 *        31, 32, 33 );
 * ```
 * will result in the elements array containing:
 * ```js
 * m.elements = [ 11, 21, 31,
 *                12, 22, 32,
 *                13, 23, 33 ];
 * ```
 * and internally all calculations are performed using column-major ordering.
 * However, as the actual ordering makes no difference mathematically and
 * most people are used to thinking about matrices in row-major order, the
 * three.js documentation shows matrices in row-major order. Just bear in
 * mind that if you are reading the source code, you'll have to take the
 * transpose of any matrices outlined here to make sense of the calculations.
 */
class Matrix3 {

	/**
	 * Constructs a new 3x3 matrix. The arguments are supposed to be
	 * in row-major order. If no arguments are provided, the constructor
	 * initializes the matrix as an identity matrix.
	 *
	 * @param {number} [n11] - 1-1 matrix element.
	 * @param {number} [n12] - 1-2 matrix element.
	 * @param {number} [n13] - 1-3 matrix element.
	 * @param {number} [n21] - 2-1 matrix element.
	 * @param {number} [n22] - 2-2 matrix element.
	 * @param {number} [n23] - 2-3 matrix element.
	 * @param {number} [n31] - 3-1 matrix element.
	 * @param {number} [n32] - 3-2 matrix element.
	 * @param {number} [n33] - 3-3 matrix element.
	 */
	constructor( n11, n12, n13, n21, n22, n23, n31, n32, n33 ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		Matrix3.prototype.isMatrix3 = true;

		/**
		 * A column-major list of matrix values.
		 *
		 * @type {Array<number>}
		 */
		this.elements = [

			1, 0, 0,
			0, 1, 0,
			0, 0, 1

		];

		if ( n11 !== undefined ) {

			this.set( n11, n12, n13, n21, n22, n23, n31, n32, n33 );

		}

	}

	/**
	 * Sets the elements of the matrix.The arguments are supposed to be
	 * in row-major order.
	 *
	 * @param {number} [n11] - 1-1 matrix element.
	 * @param {number} [n12] - 1-2 matrix element.
	 * @param {number} [n13] - 1-3 matrix element.
	 * @param {number} [n21] - 2-1 matrix element.
	 * @param {number} [n22] - 2-2 matrix element.
	 * @param {number} [n23] - 2-3 matrix element.
	 * @param {number} [n31] - 3-1 matrix element.
	 * @param {number} [n32] - 3-2 matrix element.
	 * @param {number} [n33] - 3-3 matrix element.
	 * @return {Matrix3} A reference to this matrix.
	 */
	set( n11, n12, n13, n21, n22, n23, n31, n32, n33 ) {

		const te = this.elements;

		te[ 0 ] = n11; te[ 1 ] = n21; te[ 2 ] = n31;
		te[ 3 ] = n12; te[ 4 ] = n22; te[ 5 ] = n32;
		te[ 6 ] = n13; te[ 7 ] = n23; te[ 8 ] = n33;

		return this;

	}

	/**
	 * Sets this matrix to the 3x3 identity matrix.
	 *
	 * @return {Matrix3} A reference to this matrix.
	 */
	identity() {

		this.set(

			1, 0, 0,
			0, 1, 0,
			0, 0, 1

		);

		return this;

	}

	/**
	 * Copies the values of the given matrix to this instance.
	 *
	 * @param {Matrix3} m - The matrix to copy.
	 * @return {Matrix3} A reference to this matrix.
	 */
	copy( m ) {

		const te = this.elements;
		const me = m.elements;

		te[ 0 ] = me[ 0 ]; te[ 1 ] = me[ 1 ]; te[ 2 ] = me[ 2 ];
		te[ 3 ] = me[ 3 ]; te[ 4 ] = me[ 4 ]; te[ 5 ] = me[ 5 ];
		te[ 6 ] = me[ 6 ]; te[ 7 ] = me[ 7 ]; te[ 8 ] = me[ 8 ];

		return this;

	}

	/**
	 * Extracts the basis of this matrix into the three axis vectors provided.
	 *
	 * @param {Vector3} xAxis - The basis's x axis.
	 * @param {Vector3} yAxis - The basis's y axis.
	 * @param {Vector3} zAxis - The basis's z axis.
	 * @return {Matrix3} A reference to this matrix.
	 */
	extractBasis( xAxis, yAxis, zAxis ) {

		xAxis.setFromMatrix3Column( this, 0 );
		yAxis.setFromMatrix3Column( this, 1 );
		zAxis.setFromMatrix3Column( this, 2 );

		return this;

	}

	/**
	 * Set this matrix to the upper 3x3 matrix of the given 4x4 matrix.
	 *
	 * @param {Matrix4} m - The 4x4 matrix.
	 * @return {Matrix3} A reference to this matrix.
	 */
	setFromMatrix4( m ) {

		const me = m.elements;

		this.set(

			me[ 0 ], me[ 4 ], me[ 8 ],
			me[ 1 ], me[ 5 ], me[ 9 ],
			me[ 2 ], me[ 6 ], me[ 10 ]

		);

		return this;

	}

	/**
	 * Post-multiplies this matrix by the given 3x3 matrix.
	 *
	 * @param {Matrix3} m - The matrix to multiply with.
	 * @return {Matrix3} A reference to this matrix.
	 */
	multiply( m ) {

		return this.multiplyMatrices( this, m );

	}

	/**
	 * Pre-multiplies this matrix by the given 3x3 matrix.
	 *
	 * @param {Matrix3} m - The matrix to multiply with.
	 * @return {Matrix3} A reference to this matrix.
	 */
	premultiply( m ) {

		return this.multiplyMatrices( m, this );

	}

	/**
	 * Multiples the given 3x3 matrices and stores the result
	 * in this matrix.
	 *
	 * @param {Matrix3} a - The first matrix.
	 * @param {Matrix3} b - The second matrix.
	 * @return {Matrix3} A reference to this matrix.
	 */
	multiplyMatrices( a, b ) {

		const ae = a.elements;
		const be = b.elements;
		const te = this.elements;

		const a11 = ae[ 0 ], a12 = ae[ 3 ], a13 = ae[ 6 ];
		const a21 = ae[ 1 ], a22 = ae[ 4 ], a23 = ae[ 7 ];
		const a31 = ae[ 2 ], a32 = ae[ 5 ], a33 = ae[ 8 ];

		const b11 = be[ 0 ], b12 = be[ 3 ], b13 = be[ 6 ];
		const b21 = be[ 1 ], b22 = be[ 4 ], b23 = be[ 7 ];
		const b31 = be[ 2 ], b32 = be[ 5 ], b33 = be[ 8 ];

		te[ 0 ] = a11 * b11 + a12 * b21 + a13 * b31;
		te[ 3 ] = a11 * b12 + a12 * b22 + a13 * b32;
		te[ 6 ] = a11 * b13 + a12 * b23 + a13 * b33;

		te[ 1 ] = a21 * b11 + a22 * b21 + a23 * b31;
		te[ 4 ] = a21 * b12 + a22 * b22 + a23 * b32;
		te[ 7 ] = a21 * b13 + a22 * b23 + a23 * b33;

		te[ 2 ] = a31 * b11 + a32 * b21 + a33 * b31;
		te[ 5 ] = a31 * b12 + a32 * b22 + a33 * b32;
		te[ 8 ] = a31 * b13 + a32 * b23 + a33 * b33;

		return this;

	}

	/**
	 * Multiplies every component of the matrix by the given scalar.
	 *
	 * @param {number} s - The scalar.
	 * @return {Matrix3} A reference to this matrix.
	 */
	multiplyScalar( s ) {

		const te = this.elements;

		te[ 0 ] *= s; te[ 3 ] *= s; te[ 6 ] *= s;
		te[ 1 ] *= s; te[ 4 ] *= s; te[ 7 ] *= s;
		te[ 2 ] *= s; te[ 5 ] *= s; te[ 8 ] *= s;

		return this;

	}

	/**
	 * Computes and returns the determinant of this matrix.
	 *
	 * @return {number} The determinant.
	 */
	determinant() {

		const te = this.elements;

		const a = te[ 0 ], b = te[ 1 ], c = te[ 2 ],
			d = te[ 3 ], e = te[ 4 ], f = te[ 5 ],
			g = te[ 6 ], h = te[ 7 ], i = te[ 8 ];

		return a * e * i - a * f * h - b * d * i + b * f * g + c * d * h - c * e * g;

	}

	/**
	 * Inverts this matrix, using the [analytic method]{@link https://en.wikipedia.org/wiki/Invertible_matrix#Analytic_solution}.
	 * You can not invert with a determinant of zero. If you attempt this, the method produces
	 * a zero matrix instead.
	 *
	 * @return {Matrix3} A reference to this matrix.
	 */
	invert() {

		const te = this.elements,

			n11 = te[ 0 ], n21 = te[ 1 ], n31 = te[ 2 ],
			n12 = te[ 3 ], n22 = te[ 4 ], n32 = te[ 5 ],
			n13 = te[ 6 ], n23 = te[ 7 ], n33 = te[ 8 ],

			t11 = n33 * n22 - n32 * n23,
			t12 = n32 * n13 - n33 * n12,
			t13 = n23 * n12 - n22 * n13,

			det = n11 * t11 + n21 * t12 + n31 * t13;

		if ( det === 0 ) return this.set( 0, 0, 0, 0, 0, 0, 0, 0, 0 );

		const detInv = 1 / det;

		te[ 0 ] = t11 * detInv;
		te[ 1 ] = ( n31 * n23 - n33 * n21 ) * detInv;
		te[ 2 ] = ( n32 * n21 - n31 * n22 ) * detInv;

		te[ 3 ] = t12 * detInv;
		te[ 4 ] = ( n33 * n11 - n31 * n13 ) * detInv;
		te[ 5 ] = ( n31 * n12 - n32 * n11 ) * detInv;

		te[ 6 ] = t13 * detInv;
		te[ 7 ] = ( n21 * n13 - n23 * n11 ) * detInv;
		te[ 8 ] = ( n22 * n11 - n21 * n12 ) * detInv;

		return this;

	}

	/**
	 * Transposes this matrix in place.
	 *
	 * @return {Matrix3} A reference to this matrix.
	 */
	transpose() {

		let tmp;
		const m = this.elements;

		tmp = m[ 1 ]; m[ 1 ] = m[ 3 ]; m[ 3 ] = tmp;
		tmp = m[ 2 ]; m[ 2 ] = m[ 6 ]; m[ 6 ] = tmp;
		tmp = m[ 5 ]; m[ 5 ] = m[ 7 ]; m[ 7 ] = tmp;

		return this;

	}

	/**
	 * Computes the normal matrix which is the inverse transpose of the upper
	 * left 3x3 portion of the given 4x4 matrix.
	 *
	 * @param {Matrix4} matrix4 - The 4x4 matrix.
	 * @return {Matrix3} A reference to this matrix.
	 */
	getNormalMatrix( matrix4 ) {

		return this.setFromMatrix4( matrix4 ).invert().transpose();

	}

	/**
	 * Transposes this matrix into the supplied array, and returns itself unchanged.
	 *
	 * @param {Array<number>} r - An array to store the transposed matrix elements.
	 * @return {Matrix3} A reference to this matrix.
	 */
	transposeIntoArray( r ) {

		const m = this.elements;

		r[ 0 ] = m[ 0 ];
		r[ 1 ] = m[ 3 ];
		r[ 2 ] = m[ 6 ];
		r[ 3 ] = m[ 1 ];
		r[ 4 ] = m[ 4 ];
		r[ 5 ] = m[ 7 ];
		r[ 6 ] = m[ 2 ];
		r[ 7 ] = m[ 5 ];
		r[ 8 ] = m[ 8 ];

		return this;

	}

	/**
	 * Sets the UV transform matrix from offset, repeat, rotation, and center.
	 *
	 * @param {number} tx - Offset x.
	 * @param {number} ty - Offset y.
	 * @param {number} sx - Repeat x.
	 * @param {number} sy - Repeat y.
	 * @param {number} rotation - Rotation, in radians. Positive values rotate counterclockwise.
	 * @param {number} cx - Center x of rotation.
	 * @param {number} cy - Center y of rotation
	 * @return {Matrix3} A reference to this matrix.
	 */
	setUvTransform( tx, ty, sx, sy, rotation, cx, cy ) {

		const c = Math.cos( rotation );
		const s = Math.sin( rotation );

		this.set(
			sx * c, sx * s, - sx * ( c * cx + s * cy ) + cx + tx,
			- sy * s, sy * c, - sy * ( - s * cx + c * cy ) + cy + ty,
			0, 0, 1
		);

		return this;

	}

	/**
	 * Scales this matrix with the given scalar values.
	 *
	 * @param {number} sx - The amount to scale in the X axis.
	 * @param {number} sy - The amount to scale in the Y axis.
	 * @return {Matrix3} A reference to this matrix.
	 */
	scale( sx, sy ) {

		this.premultiply( _m3.makeScale( sx, sy ) );

		return this;

	}

	/**
	 * Rotates this matrix by the given angle.
	 *
	 * @param {number} theta - The rotation in radians.
	 * @return {Matrix3} A reference to this matrix.
	 */
	rotate( theta ) {

		this.premultiply( _m3.makeRotation( - theta ) );

		return this;

	}

	/**
	 * Translates this matrix by the given scalar values.
	 *
	 * @param {number} tx - The amount to translate in the X axis.
	 * @param {number} ty - The amount to translate in the Y axis.
	 * @return {Matrix3} A reference to this matrix.
	 */
	translate( tx, ty ) {

		this.premultiply( _m3.makeTranslation( tx, ty ) );

		return this;

	}

	// for 2D Transforms

	/**
	 * Sets this matrix as a 2D translation transform.
	 *
	 * @param {number|Vector2} x - The amount to translate in the X axis or alternatively a translation vector.
	 * @param {number} y - The amount to translate in the Y axis.
	 * @return {Matrix3} A reference to this matrix.
	 */
	makeTranslation( x, y ) {

		if ( x.isVector2 ) {

			this.set(

				1, 0, x.x,
				0, 1, x.y,
				0, 0, 1

			);

		} else {

			this.set(

				1, 0, x,
				0, 1, y,
				0, 0, 1

			);

		}

		return this;

	}

	/**
	 * Sets this matrix as a 2D rotational transformation.
	 *
	 * @param {number} theta - The rotation in radians.
	 * @return {Matrix3} A reference to this matrix.
	 */
	makeRotation( theta ) {

		// counterclockwise

		const c = Math.cos( theta );
		const s = Math.sin( theta );

		this.set(

			c, - s, 0,
			s, c, 0,
			0, 0, 1

		);

		return this;

	}

	/**
	 * Sets this matrix as a 2D scale transform.
	 *
	 * @param {number} x - The amount to scale in the X axis.
	 * @param {number} y - The amount to scale in the Y axis.
	 * @return {Matrix3} A reference to this matrix.
	 */
	makeScale( x, y ) {

		this.set(

			x, 0, 0,
			0, y, 0,
			0, 0, 1

		);

		return this;

	}

	/**
	 * Returns `true` if this matrix is equal with the given one.
	 *
	 * @param {Matrix3} matrix - The matrix to test for equality.
	 * @return {boolean} Whether this matrix is equal with the given one.
	 */
	equals( matrix ) {

		const te = this.elements;
		const me = matrix.elements;

		for ( let i = 0; i < 9; i ++ ) {

			if ( te[ i ] !== me[ i ] ) return false;

		}

		return true;

	}

	/**
	 * Sets the elements of the matrix from the given array.
	 *
	 * @param {Array<number>} array - The matrix elements in column-major order.
	 * @param {number} [offset=0] - Index of the first element in the array.
	 * @return {Matrix3} A reference to this matrix.
	 */
	fromArray( array, offset = 0 ) {

		for ( let i = 0; i < 9; i ++ ) {

			this.elements[ i ] = array[ i + offset ];

		}

		return this;

	}

	/**
	 * Writes the elements of this matrix to the given array. If no array is provided,
	 * the method returns a new instance.
	 *
	 * @param {Array<number>} [array=[]] - The target array holding the matrix elements in column-major order.
	 * @param {number} [offset=0] - Index of the first element in the array.
	 * @return {Array<number>} The matrix elements in column-major order.
	 */
	toArray( array = [], offset = 0 ) {

		const te = this.elements;

		array[ offset ] = te[ 0 ];
		array[ offset + 1 ] = te[ 1 ];
		array[ offset + 2 ] = te[ 2 ];

		array[ offset + 3 ] = te[ 3 ];
		array[ offset + 4 ] = te[ 4 ];
		array[ offset + 5 ] = te[ 5 ];

		array[ offset + 6 ] = te[ 6 ];
		array[ offset + 7 ] = te[ 7 ];
		array[ offset + 8 ] = te[ 8 ];

		return array;

	}

	/**
	 * Returns a matrix with copied values from this instance.
	 *
	 * @return {Matrix3} A clone of this instance.
	 */
	clone() {

		return new this.constructor().fromArray( this.elements );

	}

}

const _m3 = /*@__PURE__*/ new Matrix3();

function arrayNeedsUint32( array ) {

	// assumes larger values usually on last

	for ( let i = array.length - 1; i >= 0; -- i ) {

		if ( array[ i ] >= 65535 ) return true; // account for PRIMITIVE_RESTART_FIXED_INDEX, #24565

	}

	return false;

}

function createElementNS( name ) {

	return document.createElementNS( 'http://www.w3.org/1999/xhtml', name );

}

function createCanvasElement() {

	const canvas = createElementNS( 'canvas' );
	canvas.style.display = 'block';
	return canvas;

}

const _cache = {};

function warnOnce( message ) {

	if ( message in _cache ) return;

	_cache[ message ] = true;

	console.warn( message );

}

function probeAsync( gl, sync, interval ) {

	return new Promise( function ( resolve, reject ) {

		function probe() {

			switch ( gl.clientWaitSync( sync, gl.SYNC_FLUSH_COMMANDS_BIT, 0 ) ) {

				case gl.WAIT_FAILED:
					reject();
					break;

				case gl.TIMEOUT_EXPIRED:
					setTimeout( probe, interval );
					break;

				default:
					resolve();

			}

		}

		setTimeout( probe, interval );

	} );

}

const LINEAR_REC709_TO_XYZ = /*@__PURE__*/ new Matrix3().set(
	0.4123908, 0.3575843, 0.1804808,
	0.2126390, 0.7151687, 0.0721923,
	0.0193308, 0.1191948, 0.9505322
);

const XYZ_TO_LINEAR_REC709 = /*@__PURE__*/ new Matrix3().set(
	3.2409699, -1.5373832, -0.4986108,
	-0.9692436, 1.8759675, 0.0415551,
	0.0556301, -0.203977, 1.0569715
);

function createColorManagement() {

	const ColorManagement = {

		enabled: true,

		workingColorSpace: LinearSRGBColorSpace,

		/**
		 * Implementations of supported color spaces.
		 *
		 * Required:
		 *	- primaries: chromaticity coordinates [ rx ry gx gy bx by ]
		 *	- whitePoint: reference white [ x y ]
		 *	- transfer: transfer function (pre-defined)
		 *	- toXYZ: Matrix3 RGB to XYZ transform
		 *	- fromXYZ: Matrix3 XYZ to RGB transform
		 *	- luminanceCoefficients: RGB luminance coefficients
		 *
		 * Optional:
		 *  - outputColorSpaceConfig: { drawingBufferColorSpace: ColorSpace, toneMappingMode: 'extended' | 'standard' }
		 *  - workingColorSpaceConfig: { unpackColorSpace: ColorSpace }
		 *
		 * Reference:
		 * - https://www.russellcottrell.com/photo/matrixCalculator.htm
		 */
		spaces: {},

		convert: function ( color, sourceColorSpace, targetColorSpace ) {

			if ( this.enabled === false || sourceColorSpace === targetColorSpace || ! sourceColorSpace || ! targetColorSpace ) {

				return color;

			}

			if ( this.spaces[ sourceColorSpace ].transfer === SRGBTransfer ) {

				color.r = SRGBToLinear( color.r );
				color.g = SRGBToLinear( color.g );
				color.b = SRGBToLinear( color.b );

			}

			if ( this.spaces[ sourceColorSpace ].primaries !== this.spaces[ targetColorSpace ].primaries ) {

				color.applyMatrix3( this.spaces[ sourceColorSpace ].toXYZ );
				color.applyMatrix3( this.spaces[ targetColorSpace ].fromXYZ );

			}

			if ( this.spaces[ targetColorSpace ].transfer === SRGBTransfer ) {

				color.r = LinearToSRGB( color.r );
				color.g = LinearToSRGB( color.g );
				color.b = LinearToSRGB( color.b );

			}

			return color;

		},

		workingToColorSpace: function ( color, targetColorSpace ) {

			return this.convert( color, this.workingColorSpace, targetColorSpace );

		},

		colorSpaceToWorking: function ( color, sourceColorSpace ) {

			return this.convert( color, sourceColorSpace, this.workingColorSpace );

		},

		getPrimaries: function ( colorSpace ) {

			return this.spaces[ colorSpace ].primaries;

		},

		getTransfer: function ( colorSpace ) {

			if ( colorSpace === NoColorSpace ) return LinearTransfer;

			return this.spaces[ colorSpace ].transfer;

		},

		getToneMappingMode: function ( colorSpace ) {

			return this.spaces[ colorSpace ].outputColorSpaceConfig.toneMappingMode || 'standard';

		},

		getLuminanceCoefficients: function ( target, colorSpace = this.workingColorSpace ) {

			return target.fromArray( this.spaces[ colorSpace ].luminanceCoefficients );

		},

		define: function ( colorSpaces ) {

			Object.assign( this.spaces, colorSpaces );

		},

		// Internal APIs

		_getMatrix: function ( targetMatrix, sourceColorSpace, targetColorSpace ) {

			return targetMatrix
				.copy( this.spaces[ sourceColorSpace ].toXYZ )
				.multiply( this.spaces[ targetColorSpace ].fromXYZ );

		},

		_getDrawingBufferColorSpace: function ( colorSpace ) {

			return this.spaces[ colorSpace ].outputColorSpaceConfig.drawingBufferColorSpace;

		},

		_getUnpackColorSpace: function ( colorSpace = this.workingColorSpace ) {

			return this.spaces[ colorSpace ].workingColorSpaceConfig.unpackColorSpace;

		},

		// Deprecated

		fromWorkingColorSpace: function ( color, targetColorSpace ) {

			warnOnce( 'THREE.ColorManagement: .fromWorkingColorSpace() has been renamed to .workingToColorSpace().' ); // @deprecated, r177

			return ColorManagement.workingToColorSpace( color, targetColorSpace );

		},

		toWorkingColorSpace: function ( color, sourceColorSpace ) {

			warnOnce( 'THREE.ColorManagement: .toWorkingColorSpace() has been renamed to .colorSpaceToWorking().' ); // @deprecated, r177

			return ColorManagement.colorSpaceToWorking( color, sourceColorSpace );

		},

	};

	/******************************************************************************
	 * sRGB definitions
	 */

	const REC709_PRIMARIES = [ 0.640, 0.330, 0.300, 0.600, 0.150, 0.060 ];
	const REC709_LUMINANCE_COEFFICIENTS = [ 0.2126, 0.7152, 0.0722 ];
	const D65 = [ 0.3127, 0.3290 ];

	ColorManagement.define( {

		[ LinearSRGBColorSpace ]: {
			primaries: REC709_PRIMARIES,
			whitePoint: D65,
			transfer: LinearTransfer,
			toXYZ: LINEAR_REC709_TO_XYZ,
			fromXYZ: XYZ_TO_LINEAR_REC709,
			luminanceCoefficients: REC709_LUMINANCE_COEFFICIENTS,
			workingColorSpaceConfig: { unpackColorSpace: SRGBColorSpace },
			outputColorSpaceConfig: { drawingBufferColorSpace: SRGBColorSpace }
		},

		[ SRGBColorSpace ]: {
			primaries: REC709_PRIMARIES,
			whitePoint: D65,
			transfer: SRGBTransfer,
			toXYZ: LINEAR_REC709_TO_XYZ,
			fromXYZ: XYZ_TO_LINEAR_REC709,
			luminanceCoefficients: REC709_LUMINANCE_COEFFICIENTS,
			outputColorSpaceConfig: { drawingBufferColorSpace: SRGBColorSpace }
		},

	} );

	return ColorManagement;

}

const ColorManagement = /*@__PURE__*/ createColorManagement();

function SRGBToLinear( c ) {

	return ( c < 0.04045 ) ? c * 0.0773993808 : Math.pow( c * 0.9478672986 + 0.0521327014, 2.4 );

}

function LinearToSRGB( c ) {

	return ( c < 0.0031308 ) ? c * 12.92 : 1.055 * ( Math.pow( c, 0.41666 ) ) - 0.055;

}

let _canvas;

/**
 * A class containing utility functions for images.
 *
 * @hideconstructor
 */
class ImageUtils {

	/**
	 * Returns a data URI containing a representation of the given image.
	 *
	 * @param {(HTMLImageElement|HTMLCanvasElement)} image - The image object.
	 * @param {string} [type='image/png'] - Indicates the image format.
	 * @return {string} The data URI.
	 */
	static getDataURL( image, type = 'image/png' ) {

		if ( /^data:/i.test( image.src ) ) {

			return image.src;

		}

		if ( typeof HTMLCanvasElement === 'undefined' ) {

			return image.src;

		}

		let canvas;

		if ( image instanceof HTMLCanvasElement ) {

			canvas = image;

		} else {

			if ( _canvas === undefined ) _canvas = createElementNS( 'canvas' );

			_canvas.width = image.width;
			_canvas.height = image.height;

			const context = _canvas.getContext( '2d' );

			if ( image instanceof ImageData ) {

				context.putImageData( image, 0, 0 );

			} else {

				context.drawImage( image, 0, 0, image.width, image.height );

			}

			canvas = _canvas;

		}

		return canvas.toDataURL( type );

	}

	/**
	 * Converts the given sRGB image data to linear color space.
	 *
	 * @param {(HTMLImageElement|HTMLCanvasElement|ImageBitmap|Object)} image - The image object.
	 * @return {HTMLCanvasElement|Object} The converted image.
	 */
	static sRGBToLinear( image ) {

		if ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||
			( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||
			( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ) {

			const canvas = createElementNS( 'canvas' );

			canvas.width = image.width;
			canvas.height = image.height;

			const context = canvas.getContext( '2d' );
			context.drawImage( image, 0, 0, image.width, image.height );

			const imageData = context.getImageData( 0, 0, image.width, image.height );
			const data = imageData.data;

			for ( let i = 0; i < data.length; i ++ ) {

				data[ i ] = SRGBToLinear( data[ i ] / 255 ) * 255;

			}

			context.putImageData( imageData, 0, 0 );

			return canvas;

		} else if ( image.data ) {

			const data = image.data.slice( 0 );

			for ( let i = 0; i < data.length; i ++ ) {

				if ( data instanceof Uint8Array || data instanceof Uint8ClampedArray ) {

					data[ i ] = Math.floor( SRGBToLinear( data[ i ] / 255 ) * 255 );

				} else {

					// assuming float

					data[ i ] = SRGBToLinear( data[ i ] );

				}

			}

			return {
				data: data,
				width: image.width,
				height: image.height
			};

		} else {

			console.warn( 'THREE.ImageUtils.sRGBToLinear(): Unsupported image type. No color space conversion applied.' );
			return image;

		}

	}

}

let _sourceId = 0;

/**
 * Represents the data source of a texture.
 *
 * The main purpose of this class is to decouple the data definition from the texture
 * definition so the same data can be used with multiple texture instances.
 */
class Source {

	/**
	 * Constructs a new video texture.
	 *
	 * @param {any} [data=null] - The data definition of a texture.
	 */
	constructor( data = null ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isSource = true;

		/**
		 * The ID of the source.
		 *
		 * @name Source#id
		 * @type {number}
		 * @readonly
		 */
		Object.defineProperty( this, 'id', { value: _sourceId ++ } );

		/**
		 * The UUID of the source.
		 *
		 * @type {string}
		 * @readonly
		 */
		this.uuid = generateUUID();

		/**
		 * The data definition of a texture.
		 *
		 * @type {any}
		 */
		this.data = data;

		/**
		 * This property is only relevant when {@link Source#needsUpdate} is set to `true` and
		 * provides more control on how texture data should be processed. When `dataReady` is set
		 * to `false`, the engine performs the memory allocation (if necessary) but does not transfer
		 * the data into the GPU memory.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.dataReady = true;

		/**
		 * This starts at `0` and counts how many times {@link Source#needsUpdate} is set to `true`.
		 *
		 * @type {number}
		 * @readonly
		 * @default 0
		 */
		this.version = 0;

	}

	/**
	 * Returns the dimensions of the source into the given target vector.
	 *
	 * @param {(Vector2|Vector3)} target - The target object the result is written into.
	 * @return {(Vector2|Vector3)} The dimensions of the source.
	 */
	getSize( target ) {

		const data = this.data;

		if ( ( typeof HTMLVideoElement !== 'undefined' ) && ( data instanceof HTMLVideoElement ) ) {

			target.set( data.videoWidth, data.videoHeight, 0 );

		} else if ( data instanceof VideoFrame ) {

			target.set( data.displayHeight, data.displayWidth, 0 );

		} else if ( data !== null ) {

			target.set( data.width, data.height, data.depth || 0 );

		} else {

			target.set( 0, 0, 0 );

		}

		return target;

	}

	/**
	 * When the property is set to `true`, the engine allocates the memory
	 * for the texture (if necessary) and triggers the actual texture upload
	 * to the GPU next time the source is used.
	 *
	 * @type {boolean}
	 * @default false
	 * @param {boolean} value
	 */
	set needsUpdate( value ) {

		if ( value === true ) this.version ++;

	}

	/**
	 * Serializes the source into JSON.
	 *
	 * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.
	 * @return {Object} A JSON object representing the serialized source.
	 * @see {@link ObjectLoader#parse}
	 */
	toJSON( meta ) {

		const isRootObject = ( meta === undefined || typeof meta === 'string' );

		if ( ! isRootObject && meta.images[ this.uuid ] !== undefined ) {

			return meta.images[ this.uuid ];

		}

		const output = {
			uuid: this.uuid,
			url: ''
		};

		const data = this.data;

		if ( data !== null ) {

			let url;

			if ( Array.isArray( data ) ) {

				// cube texture

				url = [];

				for ( let i = 0, l = data.length; i < l; i ++ ) {

					if ( data[ i ].isDataTexture ) {

						url.push( serializeImage( data[ i ].image ) );

					} else {

						url.push( serializeImage( data[ i ] ) );

					}

				}

			} else {

				// texture

				url = serializeImage( data );

			}

			output.url = url;

		}

		if ( ! isRootObject ) {

			meta.images[ this.uuid ] = output;

		}

		return output;

	}

}

function serializeImage( image ) {

	if ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||
		( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||
		( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ) {

		// default images

		return ImageUtils.getDataURL( image );

	} else {

		if ( image.data ) {

			// images of DataTexture

			return {
				data: Array.from( image.data ),
				width: image.width,
				height: image.height,
				type: image.data.constructor.name
			};

		} else {

			console.warn( 'THREE.Texture: Unable to serialize Texture.' );
			return {};

		}

	}

}

let _textureId = 0;

const _tempVec3 = /*@__PURE__*/ new Vector3();

/**
 * Base class for all textures.
 *
 * Note: After the initial use of a texture, its dimensions, format, and type
 * cannot be changed. Instead, call {@link Texture#dispose} on the texture and instantiate a new one.
 *
 * @augments EventDispatcher
 */
class Texture extends EventDispatcher {

	/**
	 * Constructs a new texture.
	 *
	 * @param {?Object} [image=Texture.DEFAULT_IMAGE] - The image holding the texture data.
	 * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.
	 * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
	 * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
	 * @param {number} [magFilter=LinearFilter] - The mag filter value.
	 * @param {number} [minFilter=LinearMipmapLinearFilter] - The min filter value.
	 * @param {number} [format=RGBAFormat] - The texture format.
	 * @param {number} [type=UnsignedByteType] - The texture type.
	 * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
	 * @param {string} [colorSpace=NoColorSpace] - The color space.
	 */
	constructor( image = Texture.DEFAULT_IMAGE, mapping = Texture.DEFAULT_MAPPING, wrapS = ClampToEdgeWrapping, wrapT = ClampToEdgeWrapping, magFilter = LinearFilter, minFilter = LinearMipmapLinearFilter, format = RGBAFormat, type = UnsignedByteType, anisotropy = Texture.DEFAULT_ANISOTROPY, colorSpace = NoColorSpace ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isTexture = true;

		/**
		 * The ID of the texture.
		 *
		 * @name Texture#id
		 * @type {number}
		 * @readonly
		 */
		Object.defineProperty( this, 'id', { value: _textureId ++ } );

		/**
		 * The UUID of the material.
		 *
		 * @type {string}
		 * @readonly
		 */
		this.uuid = generateUUID();

		/**
		 * The name of the material.
		 *
		 * @type {string}
		 */
		this.name = '';

		/**
		 * The data definition of a texture. A reference to the data source can be
		 * shared across textures. This is often useful in context of spritesheets
		 * where multiple textures render the same data but with different texture
		 * transformations.
		 *
		 * @type {Source}
		 */
		this.source = new Source( image );

		/**
		 * An array holding user-defined mipmaps.
		 *
		 * @type {Array<Object>}
		 */
		this.mipmaps = [];

		/**
		 * How the texture is applied to the object. The value `UVMapping`
		 * is the default, where texture or uv coordinates are used to apply the map.
		 *
		 * @type {(UVMapping|CubeReflectionMapping|CubeRefractionMapping|EquirectangularReflectionMapping|EquirectangularRefractionMapping|CubeUVReflectionMapping)}
		 * @default UVMapping
		*/
		this.mapping = mapping;

		/**
		 * Lets you select the uv attribute to map the texture to. `0` for `uv`,
		 * `1` for `uv1`, `2` for `uv2` and `3` for `uv3`.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.channel = 0;

		/**
		 * This defines how the texture is wrapped horizontally and corresponds to
		 * *U* in UV mapping.
		 *
		 * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}
		 * @default ClampToEdgeWrapping
		 */
		this.wrapS = wrapS;

		/**
		 * This defines how the texture is wrapped horizontally and corresponds to
		 * *V* in UV mapping.
		 *
		 * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}
		 * @default ClampToEdgeWrapping
		 */
		this.wrapT = wrapT;

		/**
		 * How the texture is sampled when a texel covers more than one pixel.
		 *
		 * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
		 * @default LinearFilter
		 */
		this.magFilter = magFilter;

		/**
		 * How the texture is sampled when a texel covers less than one pixel.
		 *
		 * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
		 * @default LinearMipmapLinearFilter
		 */
		this.minFilter = minFilter;

		/**
		 * The number of samples taken along the axis through the pixel that has the
		 * highest density of texels. By default, this value is `1`. A higher value
		 * gives a less blurry result than a basic mipmap, at the cost of more
		 * texture samples being used.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.anisotropy = anisotropy;

		/**
		 * The format of the texture.
		 *
		 * @type {number}
		 * @default RGBAFormat
		 */
		this.format = format;

		/**
		 * The default internal format is derived from {@link Texture#format} and {@link Texture#type} and
		 * defines how the texture data is going to be stored on the GPU.
		 *
		 * This property allows to overwrite the default format.
		 *
		 * @type {?string}
		 * @default null
		 */
		this.internalFormat = null;

		/**
		 * The data type of the texture.
		 *
		 * @type {number}
		 * @default UnsignedByteType
		 */
		this.type = type;

		/**
		 * How much a single repetition of the texture is offset from the beginning,
		 * in each direction U and V. Typical range is `0.0` to `1.0`.
		 *
		 * @type {Vector2}
		 * @default (0,0)
		 */
		this.offset = new Vector2( 0, 0 );

		/**
		 * How many times the texture is repeated across the surface, in each
		 * direction U and V. If repeat is set greater than `1` in either direction,
		 * the corresponding wrap parameter should also be set to `RepeatWrapping`
		 * or `MirroredRepeatWrapping` to achieve the desired tiling effect.
		 *
		 * @type {Vector2}
		 * @default (1,1)
		 */
		this.repeat = new Vector2( 1, 1 );

		/**
		 * The point around which rotation occurs. A value of `(0.5, 0.5)` corresponds
		 * to the center of the texture. Default is `(0, 0)`, the lower left.
		 *
		 * @type {Vector2}
		 * @default (0,0)
		 */
		this.center = new Vector2( 0, 0 );

		/**
		 * How much the texture is rotated around the center point, in radians.
		 * Positive values are counter-clockwise.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.rotation = 0;

		/**
		 * Whether to update the texture's uv-transformation {@link Texture#matrix}
		 * from the properties {@link Texture#offset}, {@link Texture#repeat},
		 * {@link Texture#rotation}, and {@link Texture#center}.
		 *
		 * Set this to `false` if you are specifying the uv-transform matrix directly.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.matrixAutoUpdate = true;

		/**
		 * The uv-transformation matrix of the texture.
		 *
		 * @type {Matrix3}
		 */
		this.matrix = new Matrix3();

		/**
		 * Whether to generate mipmaps (if possible) for a texture.
		 *
		 * Set this to `false` if you are creating mipmaps manually.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.generateMipmaps = true;

		/**
		 * If set to `true`, the alpha channel, if present, is multiplied into the
		 * color channels when the texture is uploaded to the GPU.
		 *
		 * Note that this property has no effect when using `ImageBitmap`. You need to
		 * configure premultiply alpha on bitmap creation instead.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.premultiplyAlpha = false;

		/**
		 * If set to `true`, the texture is flipped along the vertical axis when
		 * uploaded to the GPU.
		 *
		 * Note that this property has no effect when using `ImageBitmap`. You need to
		 * configure the flip on bitmap creation instead.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.flipY = true;

		/**
		 * Specifies the alignment requirements for the start of each pixel row in memory.
		 * The allowable values are `1` (byte-alignment), `2` (rows aligned to even-numbered bytes),
		 * `4` (word-alignment), and `8` (rows start on double-word boundaries).
		 *
		 * @type {number}
		 * @default 4
		 */
		this.unpackAlignment = 4;	// valid values: 1, 2, 4, 8 (see http://www.khronos.org/opengles/sdk/docs/man/xhtml/glPixelStorei.xml)

		/**
		 * Textures containing color data should be annotated with `SRGBColorSpace` or `LinearSRGBColorSpace`.
		 *
		 * @type {string}
		 * @default NoColorSpace
		 */
		this.colorSpace = colorSpace;

		/**
		 * An object that can be used to store custom data about the texture. It
		 * should not hold references to functions as these will not be cloned.
		 *
		 * @type {Object}
		 */
		this.userData = {};

		/**
		 * This can be used to only update a subregion or specific rows of the texture (for example, just the
		 * first 3 rows). Use the `addUpdateRange()` function to add ranges to this array.
		 *
		 * @type {Array<Object>}
		 */
		this.updateRanges = [];

		/**
		 * This starts at `0` and counts how many times {@link Texture#needsUpdate} is set to `true`.
		 *
		 * @type {number}
		 * @readonly
		 * @default 0
		 */
		this.version = 0;

		/**
		 * A callback function, called when the texture is updated (e.g., when
		 * {@link Texture#needsUpdate} has been set to true and then the texture is used).
		 *
		 * @type {?Function}
		 * @default null
		 */
		this.onUpdate = null;

		/**
		 * An optional back reference to the textures render target.
		 *
		 * @type {?(RenderTarget|WebGLRenderTarget)}
		 * @default null
		 */
		this.renderTarget = null;

		/**
		 * Indicates whether a texture belongs to a render target or not.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default false
		 */
		this.isRenderTargetTexture = false;

		/**
		 * Indicates if a texture should be handled like a texture array.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default false
		 */
		this.isArrayTexture = image && image.depth && image.depth > 1 ? true : false;

		/**
		 * Indicates whether this texture should be processed by `PMREMGenerator` or not
		 * (only relevant for render target textures).
		 *
		 * @type {number}
		 * @readonly
		 * @default 0
		 */
		this.pmremVersion = 0;

	}

	/**
	 * The width of the texture in pixels.
	 */
	get width() {

		return this.source.getSize( _tempVec3 ).x;

	}

	/**
	 * The height of the texture in pixels.
	 */
	get height() {

		return this.source.getSize( _tempVec3 ).y;

	}

	/**
	 * The depth of the texture in pixels.
	 */
	get depth() {

		return this.source.getSize( _tempVec3 ).z;

	}

	/**
	 * The image object holding the texture data.
	 *
	 * @type {?Object}
	 */
	get image() {

		return this.source.data;

	}

	set image( value = null ) {

		this.source.data = value;

	}

	/**
	 * Updates the texture transformation matrix from the from the properties {@link Texture#offset},
	 * {@link Texture#repeat}, {@link Texture#rotation}, and {@link Texture#center}.
	 */
	updateMatrix() {

		this.matrix.setUvTransform( this.offset.x, this.offset.y, this.repeat.x, this.repeat.y, this.rotation, this.center.x, this.center.y );

	}

	/**
	 * Adds a range of data in the data texture to be updated on the GPU.
	 *
	 * @param {number} start - Position at which to start update.
	 * @param {number} count - The number of components to update.
	 */
	addUpdateRange( start, count ) {

		this.updateRanges.push( { start, count } );

	}

	/**
	 * Clears the update ranges.
	 */
	clearUpdateRanges() {

		this.updateRanges.length = 0;

	}

	/**
	 * Returns a new texture with copied values from this instance.
	 *
	 * @return {Texture} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

	/**
	 * Copies the values of the given texture to this instance.
	 *
	 * @param {Texture} source - The texture to copy.
	 * @return {Texture} A reference to this instance.
	 */
	copy( source ) {

		this.name = source.name;

		this.source = source.source;
		this.mipmaps = source.mipmaps.slice( 0 );

		this.mapping = source.mapping;
		this.channel = source.channel;

		this.wrapS = source.wrapS;
		this.wrapT = source.wrapT;

		this.magFilter = source.magFilter;
		this.minFilter = source.minFilter;

		this.anisotropy = source.anisotropy;

		this.format = source.format;
		this.internalFormat = source.internalFormat;
		this.type = source.type;

		this.offset.copy( source.offset );
		this.repeat.copy( source.repeat );
		this.center.copy( source.center );
		this.rotation = source.rotation;

		this.matrixAutoUpdate = source.matrixAutoUpdate;
		this.matrix.copy( source.matrix );

		this.generateMipmaps = source.generateMipmaps;
		this.premultiplyAlpha = source.premultiplyAlpha;
		this.flipY = source.flipY;
		this.unpackAlignment = source.unpackAlignment;
		this.colorSpace = source.colorSpace;

		this.renderTarget = source.renderTarget;
		this.isRenderTargetTexture = source.isRenderTargetTexture;
		this.isArrayTexture = source.isArrayTexture;

		this.userData = JSON.parse( JSON.stringify( source.userData ) );

		this.needsUpdate = true;

		return this;

	}

	/**
	 * Sets this texture's properties based on `values`.
	 * @param {Object} values - A container with texture parameters.
	 */
	setValues( values ) {

		for ( const key in values ) {

			const newValue = values[ key ];

			if ( newValue === undefined ) {

				console.warn( `THREE.Texture.setValues(): parameter '${ key }' has value of undefined.` );
				continue;

			}

			const currentValue = this[ key ];

			if ( currentValue === undefined ) {

				console.warn( `THREE.Texture.setValues(): property '${ key }' does not exist.` );
				continue;

			}

			if ( ( currentValue && newValue ) && ( currentValue.isVector2 && newValue.isVector2 ) ) {

				currentValue.copy( newValue );

			} else if ( ( currentValue && newValue ) && ( currentValue.isVector3 && newValue.isVector3 ) ) {

				currentValue.copy( newValue );

			} else if ( ( currentValue && newValue ) && ( currentValue.isMatrix3 && newValue.isMatrix3 ) ) {

				currentValue.copy( newValue );

			} else {

				this[ key ] = newValue;

			}

		}

	}

	/**
	 * Serializes the texture into JSON.
	 *
	 * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.
	 * @return {Object} A JSON object representing the serialized texture.
	 * @see {@link ObjectLoader#parse}
	 */
	toJSON( meta ) {

		const isRootObject = ( meta === undefined || typeof meta === 'string' );

		if ( ! isRootObject && meta.textures[ this.uuid ] !== undefined ) {

			return meta.textures[ this.uuid ];

		}

		const output = {

			metadata: {
				version: 4.7,
				type: 'Texture',
				generator: 'Texture.toJSON'
			},

			uuid: this.uuid,
			name: this.name,

			image: this.source.toJSON( meta ).uuid,

			mapping: this.mapping,
			channel: this.channel,

			repeat: [ this.repeat.x, this.repeat.y ],
			offset: [ this.offset.x, this.offset.y ],
			center: [ this.center.x, this.center.y ],
			rotation: this.rotation,

			wrap: [ this.wrapS, this.wrapT ],

			format: this.format,
			internalFormat: this.internalFormat,
			type: this.type,
			colorSpace: this.colorSpace,

			minFilter: this.minFilter,
			magFilter: this.magFilter,
			anisotropy: this.anisotropy,

			flipY: this.flipY,

			generateMipmaps: this.generateMipmaps,
			premultiplyAlpha: this.premultiplyAlpha,
			unpackAlignment: this.unpackAlignment

		};

		if ( Object.keys( this.userData ).length > 0 ) output.userData = this.userData;

		if ( ! isRootObject ) {

			meta.textures[ this.uuid ] = output;

		}

		return output;

	}

	/**
	 * Frees the GPU-related resources allocated by this instance. Call this
	 * method whenever this instance is no longer used in your app.
	 *
	 * @fires Texture#dispose
	 */
	dispose() {

		/**
		 * Fires when the texture has been disposed of.
		 *
		 * @event Texture#dispose
		 * @type {Object}
		 */
		this.dispatchEvent( { type: 'dispose' } );

	}

	/**
	 * Transforms the given uv vector with the textures uv transformation matrix.
	 *
	 * @param {Vector2} uv - The uv vector.
	 * @return {Vector2} The transformed uv vector.
	 */
	transformUv( uv ) {

		if ( this.mapping !== UVMapping ) return uv;

		uv.applyMatrix3( this.matrix );

		if ( uv.x < 0 || uv.x > 1 ) {

			switch ( this.wrapS ) {

				case RepeatWrapping:

					uv.x = uv.x - Math.floor( uv.x );
					break;

				case ClampToEdgeWrapping:

					uv.x = uv.x < 0 ? 0 : 1;
					break;

				case MirroredRepeatWrapping:

					if ( Math.abs( Math.floor( uv.x ) % 2 ) === 1 ) {

						uv.x = Math.ceil( uv.x ) - uv.x;

					} else {

						uv.x = uv.x - Math.floor( uv.x );

					}

					break;

			}

		}

		if ( uv.y < 0 || uv.y > 1 ) {

			switch ( this.wrapT ) {

				case RepeatWrapping:

					uv.y = uv.y - Math.floor( uv.y );
					break;

				case ClampToEdgeWrapping:

					uv.y = uv.y < 0 ? 0 : 1;
					break;

				case MirroredRepeatWrapping:

					if ( Math.abs( Math.floor( uv.y ) % 2 ) === 1 ) {

						uv.y = Math.ceil( uv.y ) - uv.y;

					} else {

						uv.y = uv.y - Math.floor( uv.y );

					}

					break;

			}

		}

		if ( this.flipY ) {

			uv.y = 1 - uv.y;

		}

		return uv;

	}

	/**
	 * Setting this property to `true` indicates the engine the texture
	 * must be updated in the next render. This triggers a texture upload
	 * to the GPU and ensures correct texture parameter configuration.
	 *
	 * @type {boolean}
	 * @default false
	 * @param {boolean} value
	 */
	set needsUpdate( value ) {

		if ( value === true ) {

			this.version ++;
			this.source.needsUpdate = true;

		}

	}

	/**
	 * Setting this property to `true` indicates the engine the PMREM
	 * must be regenerated.
	 *
	 * @type {boolean}
	 * @default false
	 * @param {boolean} value
	 */
	set needsPMREMUpdate( value ) {

		if ( value === true ) {

			this.pmremVersion ++;

		}

	}

}

/**
 * The default image for all textures.
 *
 * @static
 * @type {?Image}
 * @default null
 */
Texture.DEFAULT_IMAGE = null;

/**
 * The default mapping for all textures.
 *
 * @static
 * @type {number}
 * @default UVMapping
 */
Texture.DEFAULT_MAPPING = UVMapping;

/**
 * The default anisotropy value for all textures.
 *
 * @static
 * @type {number}
 * @default 1
 */
Texture.DEFAULT_ANISOTROPY = 1;

/**
 * Class representing a 4D vector. A 4D vector is an ordered quadruplet of numbers
 * (labeled x, y, z and w), which can be used to represent a number of things, such as:
 *
 * - A point in 4D space.
 * - A direction and length in 4D space. In three.js the length will
 * always be the Euclidean distance(straight-line distance) from `(0, 0, 0, 0)` to `(x, y, z, w)`
 * and the direction is also measured from `(0, 0, 0, 0)` towards `(x, y, z, w)`.
 * - Any arbitrary ordered quadruplet of numbers.
 *
 * There are other things a 4D vector can be used to represent, however these
 * are the most common uses in *three.js*.
 *
 * Iterating through a vector instance will yield its components `(x, y, z, w)` in
 * the corresponding order.
 * ```js
 * const a = new THREE.Vector4( 0, 1, 0, 0 );
 *
 * //no arguments; will be initialised to (0, 0, 0, 1)
 * const b = new THREE.Vector4( );
 *
 * const d = a.dot( b );
 * ```
 */
class Vector4 {

	/**
	 * Constructs a new 4D vector.
	 *
	 * @param {number} [x=0] - The x value of this vector.
	 * @param {number} [y=0] - The y value of this vector.
	 * @param {number} [z=0] - The z value of this vector.
	 * @param {number} [w=1] - The w value of this vector.
	 */
	constructor( x = 0, y = 0, z = 0, w = 1 ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		Vector4.prototype.isVector4 = true;

		/**
		 * The x value of this vector.
		 *
		 * @type {number}
		 */
		this.x = x;

		/**
		 * The y value of this vector.
		 *
		 * @type {number}
		 */
		this.y = y;

		/**
		 * The z value of this vector.
		 *
		 * @type {number}
		 */
		this.z = z;

		/**
		 * The w value of this vector.
		 *
		 * @type {number}
		 */
		this.w = w;

	}

	/**
	 * Alias for {@link Vector4#z}.
	 *
	 * @type {number}
	 */
	get width() {

		return this.z;

	}

	set width( value ) {

		this.z = value;

	}

	/**
	 * Alias for {@link Vector4#w}.
	 *
	 * @type {number}
	 */
	get height() {

		return this.w;

	}

	set height( value ) {

		this.w = value;

	}

	/**
	 * Sets the vector components.
	 *
	 * @param {number} x - The value of the x component.
	 * @param {number} y - The value of the y component.
	 * @param {number} z - The value of the z component.
	 * @param {number} w - The value of the w component.
	 * @return {Vector4} A reference to this vector.
	 */
	set( x, y, z, w ) {

		this.x = x;
		this.y = y;
		this.z = z;
		this.w = w;

		return this;

	}

	/**
	 * Sets the vector components to the same value.
	 *
	 * @param {number} scalar - The value to set for all vector components.
	 * @return {Vector4} A reference to this vector.
	 */
	setScalar( scalar ) {

		this.x = scalar;
		this.y = scalar;
		this.z = scalar;
		this.w = scalar;

		return this;

	}

	/**
	 * Sets the vector's x component to the given value
	 *
	 * @param {number} x - The value to set.
	 * @return {Vector4} A reference to this vector.
	 */
	setX( x ) {

		this.x = x;

		return this;

	}

	/**
	 * Sets the vector's y component to the given value
	 *
	 * @param {number} y - The value to set.
	 * @return {Vector4} A reference to this vector.
	 */
	setY( y ) {

		this.y = y;

		return this;

	}

	/**
	 * Sets the vector's z component to the given value
	 *
	 * @param {number} z - The value to set.
	 * @return {Vector4} A reference to this vector.
	 */
	setZ( z ) {

		this.z = z;

		return this;

	}

	/**
	 * Sets the vector's w component to the given value
	 *
	 * @param {number} w - The value to set.
	 * @return {Vector4} A reference to this vector.
	 */
	setW( w ) {

		this.w = w;

		return this;

	}

	/**
	 * Allows to set a vector component with an index.
	 *
	 * @param {number} index - The component index. `0` equals to x, `1` equals to y,
	 * `2` equals to z, `3` equals to w.
	 * @param {number} value - The value to set.
	 * @return {Vector4} A reference to this vector.
	 */
	setComponent( index, value ) {

		switch ( index ) {

			case 0: this.x = value; break;
			case 1: this.y = value; break;
			case 2: this.z = value; break;
			case 3: this.w = value; break;
			default: throw new Error( 'index is out of range: ' + index );

		}

		return this;

	}

	/**
	 * Returns the value of the vector component which matches the given index.
	 *
	 * @param {number} index - The component index. `0` equals to x, `1` equals to y,
	 * `2` equals to z, `3` equals to w.
	 * @return {number} A vector component value.
	 */
	getComponent( index ) {

		switch ( index ) {

			case 0: return this.x;
			case 1: return this.y;
			case 2: return this.z;
			case 3: return this.w;
			default: throw new Error( 'index is out of range: ' + index );

		}

	}

	/**
	 * Returns a new vector with copied values from this instance.
	 *
	 * @return {Vector4} A clone of this instance.
	 */
	clone() {

		return new this.constructor( this.x, this.y, this.z, this.w );

	}

	/**
	 * Copies the values of the given vector to this instance.
	 *
	 * @param {Vector3|Vector4} v - The vector to copy.
	 * @return {Vector4} A reference to this vector.
	 */
	copy( v ) {

		this.x = v.x;
		this.y = v.y;
		this.z = v.z;
		this.w = ( v.w !== undefined ) ? v.w : 1;

		return this;

	}

	/**
	 * Adds the given vector to this instance.
	 *
	 * @param {Vector4} v - The vector to add.
	 * @return {Vector4} A reference to this vector.
	 */
	add( v ) {

		this.x += v.x;
		this.y += v.y;
		this.z += v.z;
		this.w += v.w;

		return this;

	}

	/**
	 * Adds the given scalar value to all components of this instance.
	 *
	 * @param {number} s - The scalar to add.
	 * @return {Vector4} A reference to this vector.
	 */
	addScalar( s ) {

		this.x += s;
		this.y += s;
		this.z += s;
		this.w += s;

		return this;

	}

	/**
	 * Adds the given vectors and stores the result in this instance.
	 *
	 * @param {Vector4} a - The first vector.
	 * @param {Vector4} b - The second vector.
	 * @return {Vector4} A reference to this vector.
	 */
	addVectors( a, b ) {

		this.x = a.x + b.x;
		this.y = a.y + b.y;
		this.z = a.z + b.z;
		this.w = a.w + b.w;

		return this;

	}

	/**
	 * Adds the given vector scaled by the given factor to this instance.
	 *
	 * @param {Vector4} v - The vector.
	 * @param {number} s - The factor that scales `v`.
	 * @return {Vector4} A reference to this vector.
	 */
	addScaledVector( v, s ) {

		this.x += v.x * s;
		this.y += v.y * s;
		this.z += v.z * s;
		this.w += v.w * s;

		return this;

	}

	/**
	 * Subtracts the given vector from this instance.
	 *
	 * @param {Vector4} v - The vector to subtract.
	 * @return {Vector4} A reference to this vector.
	 */
	sub( v ) {

		this.x -= v.x;
		this.y -= v.y;
		this.z -= v.z;
		this.w -= v.w;

		return this;

	}

	/**
	 * Subtracts the given scalar value from all components of this instance.
	 *
	 * @param {number} s - The scalar to subtract.
	 * @return {Vector4} A reference to this vector.
	 */
	subScalar( s ) {

		this.x -= s;
		this.y -= s;
		this.z -= s;
		this.w -= s;

		return this;

	}

	/**
	 * Subtracts the given vectors and stores the result in this instance.
	 *
	 * @param {Vector4} a - The first vector.
	 * @param {Vector4} b - The second vector.
	 * @return {Vector4} A reference to this vector.
	 */
	subVectors( a, b ) {

		this.x = a.x - b.x;
		this.y = a.y - b.y;
		this.z = a.z - b.z;
		this.w = a.w - b.w;

		return this;

	}

	/**
	 * Multiplies the given vector with this instance.
	 *
	 * @param {Vector4} v - The vector to multiply.
	 * @return {Vector4} A reference to this vector.
	 */
	multiply( v ) {

		this.x *= v.x;
		this.y *= v.y;
		this.z *= v.z;
		this.w *= v.w;

		return this;

	}

	/**
	 * Multiplies the given scalar value with all components of this instance.
	 *
	 * @param {number} scalar - The scalar to multiply.
	 * @return {Vector4} A reference to this vector.
	 */
	multiplyScalar( scalar ) {

		this.x *= scalar;
		this.y *= scalar;
		this.z *= scalar;
		this.w *= scalar;

		return this;

	}

	/**
	 * Multiplies this vector with the given 4x4 matrix.
	 *
	 * @param {Matrix4} m - The 4x4 matrix.
	 * @return {Vector4} A reference to this vector.
	 */
	applyMatrix4( m ) {

		const x = this.x, y = this.y, z = this.z, w = this.w;
		const e = m.elements;

		this.x = e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ] * w;
		this.y = e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ] * w;
		this.z = e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z + e[ 14 ] * w;
		this.w = e[ 3 ] * x + e[ 7 ] * y + e[ 11 ] * z + e[ 15 ] * w;

		return this;

	}

	/**
	 * Divides this instance by the given vector.
	 *
	 * @param {Vector4} v - The vector to divide.
	 * @return {Vector4} A reference to this vector.
	 */
	divide( v ) {

		this.x /= v.x;
		this.y /= v.y;
		this.z /= v.z;
		this.w /= v.w;

		return this;

	}

	/**
	 * Divides this vector by the given scalar.
	 *
	 * @param {number} scalar - The scalar to divide.
	 * @return {Vector4} A reference to this vector.
	 */
	divideScalar( scalar ) {

		return this.multiplyScalar( 1 / scalar );

	}

	/**
	 * Sets the x, y and z components of this
	 * vector to the quaternion's axis and w to the angle.
	 *
	 * @param {Quaternion} q - The Quaternion to set.
	 * @return {Vector4} A reference to this vector.
	 */
	setAxisAngleFromQuaternion( q ) {

		// http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToAngle/index.htm

		// q is assumed to be normalized

		this.w = 2 * Math.acos( q.w );

		const s = Math.sqrt( 1 - q.w * q.w );

		if ( s < 0.0001 ) {

			this.x = 1;
			this.y = 0;
			this.z = 0;

		} else {

			this.x = q.x / s;
			this.y = q.y / s;
			this.z = q.z / s;

		}

		return this;

	}

	/**
	 * Sets the x, y and z components of this
	 * vector to the axis of rotation and w to the angle.
	 *
	 * @param {Matrix4} m - A 4x4 matrix of which the upper left 3x3 matrix is a pure rotation matrix.
	 * @return {Vector4} A reference to this vector.
	 */
	setAxisAngleFromRotationMatrix( m ) {

		// http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToAngle/index.htm

		// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)

		let angle, x, y, z; // variables for result
		const epsilon = 0.01,		// margin to allow for rounding errors
			epsilon2 = 0.1,		// margin to distinguish between 0 and 180 degrees

			te = m.elements,

			m11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ],
			m21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ],
			m31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ];

		if ( ( Math.abs( m12 - m21 ) < epsilon ) &&
		     ( Math.abs( m13 - m31 ) < epsilon ) &&
		     ( Math.abs( m23 - m32 ) < epsilon ) ) {

			// singularity found
			// first check for identity matrix which must have +1 for all terms
			// in leading diagonal and zero in other terms

			if ( ( Math.abs( m12 + m21 ) < epsilon2 ) &&
			     ( Math.abs( m13 + m31 ) < epsilon2 ) &&
			     ( Math.abs( m23 + m32 ) < epsilon2 ) &&
			     ( Math.abs( m11 + m22 + m33 - 3 ) < epsilon2 ) ) {

				// this singularity is identity matrix so angle = 0

				this.set( 1, 0, 0, 0 );

				return this; // zero angle, arbitrary axis

			}

			// otherwise this singularity is angle = 180

			angle = Math.PI;

			const xx = ( m11 + 1 ) / 2;
			const yy = ( m22 + 1 ) / 2;
			const zz = ( m33 + 1 ) / 2;
			const xy = ( m12 + m21 ) / 4;
			const xz = ( m13 + m31 ) / 4;
			const yz = ( m23 + m32 ) / 4;

			if ( ( xx > yy ) && ( xx > zz ) ) {

				// m11 is the largest diagonal term

				if ( xx < epsilon ) {

					x = 0;
					y = 0.707106781;
					z = 0.707106781;

				} else {

					x = Math.sqrt( xx );
					y = xy / x;
					z = xz / x;

				}

			} else if ( yy > zz ) {

				// m22 is the largest diagonal term

				if ( yy < epsilon ) {

					x = 0.707106781;
					y = 0;
					z = 0.707106781;

				} else {

					y = Math.sqrt( yy );
					x = xy / y;
					z = yz / y;

				}

			} else {

				// m33 is the largest diagonal term so base result on this

				if ( zz < epsilon ) {

					x = 0.707106781;
					y = 0.707106781;
					z = 0;

				} else {

					z = Math.sqrt( zz );
					x = xz / z;
					y = yz / z;

				}

			}

			this.set( x, y, z, angle );

			return this; // return 180 deg rotation

		}

		// as we have reached here there are no singularities so we can handle normally

		let s = Math.sqrt( ( m32 - m23 ) * ( m32 - m23 ) +
			( m13 - m31 ) * ( m13 - m31 ) +
			( m21 - m12 ) * ( m21 - m12 ) ); // used to normalize

		if ( Math.abs( s ) < 0.001 ) s = 1;

		// prevent divide by zero, should not happen if matrix is orthogonal and should be
		// caught by singularity test above, but I've left it in just in case

		this.x = ( m32 - m23 ) / s;
		this.y = ( m13 - m31 ) / s;
		this.z = ( m21 - m12 ) / s;
		this.w = Math.acos( ( m11 + m22 + m33 - 1 ) / 2 );

		return this;

	}

	/**
	 * Sets the vector components to the position elements of the
	 * given transformation matrix.
	 *
	 * @param {Matrix4} m - The 4x4 matrix.
	 * @return {Vector4} A reference to this vector.
	 */
	setFromMatrixPosition( m ) {

		const e = m.elements;

		this.x = e[ 12 ];
		this.y = e[ 13 ];
		this.z = e[ 14 ];
		this.w = e[ 15 ];

		return this;

	}

	/**
	 * If this vector's x, y, z or w value is greater than the given vector's x, y, z or w
	 * value, replace that value with the corresponding min value.
	 *
	 * @param {Vector4} v - The vector.
	 * @return {Vector4} A reference to this vector.
	 */
	min( v ) {

		this.x = Math.min( this.x, v.x );
		this.y = Math.min( this.y, v.y );
		this.z = Math.min( this.z, v.z );
		this.w = Math.min( this.w, v.w );

		return this;

	}

	/**
	 * If this vector's x, y, z or w value is less than the given vector's x, y, z or w
	 * value, replace that value with the corresponding max value.
	 *
	 * @param {Vector4} v - The vector.
	 * @return {Vector4} A reference to this vector.
	 */
	max( v ) {

		this.x = Math.max( this.x, v.x );
		this.y = Math.max( this.y, v.y );
		this.z = Math.max( this.z, v.z );
		this.w = Math.max( this.w, v.w );

		return this;

	}

	/**
	 * If this vector's x, y, z or w value is greater than the max vector's x, y, z or w
	 * value, it is replaced by the corresponding value.
	 * If this vector's x, y, z or w value is less than the min vector's x, y, z or w value,
	 * it is replaced by the corresponding value.
	 *
	 * @param {Vector4} min - The minimum x, y and z values.
	 * @param {Vector4} max - The maximum x, y and z values in the desired range.
	 * @return {Vector4} A reference to this vector.
	 */
	clamp( min, max ) {

		// assumes min < max, componentwise

		this.x = clamp( this.x, min.x, max.x );
		this.y = clamp( this.y, min.y, max.y );
		this.z = clamp( this.z, min.z, max.z );
		this.w = clamp( this.w, min.w, max.w );

		return this;

	}

	/**
	 * If this vector's x, y, z or w values are greater than the max value, they are
	 * replaced by the max value.
	 * If this vector's x, y, z or w values are less than the min value, they are
	 * replaced by the min value.
	 *
	 * @param {number} minVal - The minimum value the components will be clamped to.
	 * @param {number} maxVal - The maximum value the components will be clamped to.
	 * @return {Vector4} A reference to this vector.
	 */
	clampScalar( minVal, maxVal ) {

		this.x = clamp( this.x, minVal, maxVal );
		this.y = clamp( this.y, minVal, maxVal );
		this.z = clamp( this.z, minVal, maxVal );
		this.w = clamp( this.w, minVal, maxVal );

		return this;

	}

	/**
	 * If this vector's length is greater than the max value, it is replaced by
	 * the max value.
	 * If this vector's length is less than the min value, it is replaced by the
	 * min value.
	 *
	 * @param {number} min - The minimum value the vector length will be clamped to.
	 * @param {number} max - The maximum value the vector length will be clamped to.
	 * @return {Vector4} A reference to this vector.
	 */
	clampLength( min, max ) {

		const length = this.length();

		return this.divideScalar( length || 1 ).multiplyScalar( clamp( length, min, max ) );

	}

	/**
	 * The components of this vector are rounded down to the nearest integer value.
	 *
	 * @return {Vector4} A reference to this vector.
	 */
	floor() {

		this.x = Math.floor( this.x );
		this.y = Math.floor( this.y );
		this.z = Math.floor( this.z );
		this.w = Math.floor( this.w );

		return this;

	}

	/**
	 * The components of this vector are rounded up to the nearest integer value.
	 *
	 * @return {Vector4} A reference to this vector.
	 */
	ceil() {

		this.x = Math.ceil( this.x );
		this.y = Math.ceil( this.y );
		this.z = Math.ceil( this.z );
		this.w = Math.ceil( this.w );

		return this;

	}

	/**
	 * The components of this vector are rounded to the nearest integer value
	 *
	 * @return {Vector4} A reference to this vector.
	 */
	round() {

		this.x = Math.round( this.x );
		this.y = Math.round( this.y );
		this.z = Math.round( this.z );
		this.w = Math.round( this.w );

		return this;

	}

	/**
	 * The components of this vector are rounded towards zero (up if negative,
	 * down if positive) to an integer value.
	 *
	 * @return {Vector4} A reference to this vector.
	 */
	roundToZero() {

		this.x = Math.trunc( this.x );
		this.y = Math.trunc( this.y );
		this.z = Math.trunc( this.z );
		this.w = Math.trunc( this.w );

		return this;

	}

	/**
	 * Inverts this vector - i.e. sets x = -x, y = -y, z = -z, w = -w.
	 *
	 * @return {Vector4} A reference to this vector.
	 */
	negate() {

		this.x = - this.x;
		this.y = - this.y;
		this.z = - this.z;
		this.w = - this.w;

		return this;

	}

	/**
	 * Calculates the dot product of the given vector with this instance.
	 *
	 * @param {Vector4} v - The vector to compute the dot product with.
	 * @return {number} The result of the dot product.
	 */
	dot( v ) {

		return this.x * v.x + this.y * v.y + this.z * v.z + this.w * v.w;

	}

	/**
	 * Computes the square of the Euclidean length (straight-line length) from
	 * (0, 0, 0, 0) to (x, y, z, w). If you are comparing the lengths of vectors, you should
	 * compare the length squared instead as it is slightly more efficient to calculate.
	 *
	 * @return {number} The square length of this vector.
	 */
	lengthSq() {

		return this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w;

	}

	/**
	 * Computes the  Euclidean length (straight-line length) from (0, 0, 0, 0) to (x, y, z, w).
	 *
	 * @return {number} The length of this vector.
	 */
	length() {

		return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w );

	}

	/**
	 * Computes the Manhattan length of this vector.
	 *
	 * @return {number} The length of this vector.
	 */
	manhattanLength() {

		return Math.abs( this.x ) + Math.abs( this.y ) + Math.abs( this.z ) + Math.abs( this.w );

	}

	/**
	 * Converts this vector to a unit vector - that is, sets it equal to a vector
	 * with the same direction as this one, but with a vector length of `1`.
	 *
	 * @return {Vector4} A reference to this vector.
	 */
	normalize() {

		return this.divideScalar( this.length() || 1 );

	}

	/**
	 * Sets this vector to a vector with the same direction as this one, but
	 * with the specified length.
	 *
	 * @param {number} length - The new length of this vector.
	 * @return {Vector4} A reference to this vector.
	 */
	setLength( length ) {

		return this.normalize().multiplyScalar( length );

	}

	/**
	 * Linearly interpolates between the given vector and this instance, where
	 * alpha is the percent distance along the line - alpha = 0 will be this
	 * vector, and alpha = 1 will be the given one.
	 *
	 * @param {Vector4} v - The vector to interpolate towards.
	 * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.
	 * @return {Vector4} A reference to this vector.
	 */
	lerp( v, alpha ) {

		this.x += ( v.x - this.x ) * alpha;
		this.y += ( v.y - this.y ) * alpha;
		this.z += ( v.z - this.z ) * alpha;
		this.w += ( v.w - this.w ) * alpha;

		return this;

	}

	/**
	 * Linearly interpolates between the given vectors, where alpha is the percent
	 * distance along the line - alpha = 0 will be first vector, and alpha = 1 will
	 * be the second one. The result is stored in this instance.
	 *
	 * @param {Vector4} v1 - The first vector.
	 * @param {Vector4} v2 - The second vector.
	 * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.
	 * @return {Vector4} A reference to this vector.
	 */
	lerpVectors( v1, v2, alpha ) {

		this.x = v1.x + ( v2.x - v1.x ) * alpha;
		this.y = v1.y + ( v2.y - v1.y ) * alpha;
		this.z = v1.z + ( v2.z - v1.z ) * alpha;
		this.w = v1.w + ( v2.w - v1.w ) * alpha;

		return this;

	}

	/**
	 * Returns `true` if this vector is equal with the given one.
	 *
	 * @param {Vector4} v - The vector to test for equality.
	 * @return {boolean} Whether this vector is equal with the given one.
	 */
	equals( v ) {

		return ( ( v.x === this.x ) && ( v.y === this.y ) && ( v.z === this.z ) && ( v.w === this.w ) );

	}

	/**
	 * Sets this vector's x value to be `array[ offset ]`, y value to be `array[ offset + 1 ]`,
	 * z value to be `array[ offset + 2 ]`, w value to be `array[ offset + 3 ]`.
	 *
	 * @param {Array<number>} array - An array holding the vector component values.
	 * @param {number} [offset=0] - The offset into the array.
	 * @return {Vector4} A reference to this vector.
	 */
	fromArray( array, offset = 0 ) {

		this.x = array[ offset ];
		this.y = array[ offset + 1 ];
		this.z = array[ offset + 2 ];
		this.w = array[ offset + 3 ];

		return this;

	}

	/**
	 * Writes the components of this vector to the given array. If no array is provided,
	 * the method returns a new instance.
	 *
	 * @param {Array<number>} [array=[]] - The target array holding the vector components.
	 * @param {number} [offset=0] - Index of the first element in the array.
	 * @return {Array<number>} The vector components.
	 */
	toArray( array = [], offset = 0 ) {

		array[ offset ] = this.x;
		array[ offset + 1 ] = this.y;
		array[ offset + 2 ] = this.z;
		array[ offset + 3 ] = this.w;

		return array;

	}

	/**
	 * Sets the components of this vector from the given buffer attribute.
	 *
	 * @param {BufferAttribute} attribute - The buffer attribute holding vector data.
	 * @param {number} index - The index into the attribute.
	 * @return {Vector4} A reference to this vector.
	 */
	fromBufferAttribute( attribute, index ) {

		this.x = attribute.getX( index );
		this.y = attribute.getY( index );
		this.z = attribute.getZ( index );
		this.w = attribute.getW( index );

		return this;

	}

	/**
	 * Sets each component of this vector to a pseudo-random value between `0` and
	 * `1`, excluding `1`.
	 *
	 * @return {Vector4} A reference to this vector.
	 */
	random() {

		this.x = Math.random();
		this.y = Math.random();
		this.z = Math.random();
		this.w = Math.random();

		return this;

	}

	*[ Symbol.iterator ]() {

		yield this.x;
		yield this.y;
		yield this.z;
		yield this.w;

	}

}

/**
 * A render target is a buffer where the video card draws pixels for a scene
 * that is being rendered in the background. It is used in different effects,
 * such as applying postprocessing to a rendered image before displaying it
 * on the screen.
 *
 * @augments EventDispatcher
 */
class RenderTarget extends EventDispatcher {

	/**
	 * Render target options.
	 *
	 * @typedef {Object} RenderTarget~Options
	 * @property {boolean} [generateMipmaps=false] - Whether to generate mipmaps or not.
	 * @property {number} [magFilter=LinearFilter] - The mag filter.
	 * @property {number} [minFilter=LinearFilter] - The min filter.
	 * @property {number} [format=RGBAFormat] - The texture format.
	 * @property {number} [type=UnsignedByteType] - The texture type.
	 * @property {?string} [internalFormat=null] - The texture's internal format.
	 * @property {number} [wrapS=ClampToEdgeWrapping] - The texture's uv wrapping mode.
	 * @property {number} [wrapT=ClampToEdgeWrapping] - The texture's uv wrapping mode.
	 * @property {number} [anisotropy=1] - The texture's anisotropy value.
	 * @property {string} [colorSpace=NoColorSpace] - The texture's color space.
	 * @property {boolean} [depthBuffer=true] - Whether to allocate a depth buffer or not.
	 * @property {boolean} [stencilBuffer=false] - Whether to allocate a stencil buffer or not.
	 * @property {boolean} [resolveDepthBuffer=true] - Whether to resolve the depth buffer or not.
	 * @property {boolean} [resolveStencilBuffer=true] - Whether  to resolve the stencil buffer or not.
	 * @property {?Texture} [depthTexture=null] - Reference to a depth texture.
	 * @property {number} [samples=0] - The MSAA samples count.
	 * @property {number} [count=1] - Defines the number of color attachments . Must be at least `1`.
	 * @property {number} [depth=1] - The texture depth.
	 * @property {boolean} [multiview=false] - Whether this target is used for multiview rendering.
	 */

	/**
	 * Constructs a new render target.
	 *
	 * @param {number} [width=1] - The width of the render target.
	 * @param {number} [height=1] - The height of the render target.
	 * @param {RenderTarget~Options} [options] - The configuration object.
	 */
	constructor( width = 1, height = 1, options = {} ) {

		super();

		options = Object.assign( {
			generateMipmaps: false,
			internalFormat: null,
			minFilter: LinearFilter,
			depthBuffer: true,
			stencilBuffer: false,
			resolveDepthBuffer: true,
			resolveStencilBuffer: true,
			depthTexture: null,
			samples: 0,
			count: 1,
			depth: 1,
			multiview: false
		}, options );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isRenderTarget = true;

		/**
		 * The width of the render target.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.width = width;

		/**
		 * The height of the render target.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.height = height;

		/**
		 * The depth of the render target.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.depth = options.depth;

		/**
		 * A rectangular area inside the render target's viewport. Fragments that are
		 * outside the area will be discarded.
		 *
		 * @type {Vector4}
		 * @default (0,0,width,height)
		 */
		this.scissor = new Vector4( 0, 0, width, height );

		/**
		 * Indicates whether the scissor test should be enabled when rendering into
		 * this render target or not.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.scissorTest = false;

		/**
		 * A rectangular area representing the render target's viewport.
		 *
		 * @type {Vector4}
		 * @default (0,0,width,height)
		 */
		this.viewport = new Vector4( 0, 0, width, height );

		const image = { width: width, height: height, depth: options.depth };

		const texture = new Texture( image );

		/**
		 * An array of textures. Each color attachment is represented as a separate texture.
		 * Has at least a single entry for the default color attachment.
		 *
		 * @type {Array<Texture>}
		 */
		this.textures = [];

		const count = options.count;
		for ( let i = 0; i < count; i ++ ) {

			this.textures[ i ] = texture.clone();
			this.textures[ i ].isRenderTargetTexture = true;
			this.textures[ i ].renderTarget = this;

		}

		this._setTextureOptions( options );

		/**
		 * Whether to allocate a depth buffer or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.depthBuffer = options.depthBuffer;

		/**
		 * Whether to allocate a stencil buffer or not.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.stencilBuffer = options.stencilBuffer;

		/**
		 * Whether to resolve the depth buffer or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.resolveDepthBuffer = options.resolveDepthBuffer;

		/**
		 * Whether to resolve the stencil buffer or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.resolveStencilBuffer = options.resolveStencilBuffer;

		this._depthTexture = null;
		this.depthTexture = options.depthTexture;

		/**
		 * The number of MSAA samples.
		 *
		 * A value of `0` disables MSAA.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.samples = options.samples;

		/**
		 * Whether to this target is used in multiview rendering.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.multiview = options.multiview;

	}

	_setTextureOptions( options = {} ) {

		const values = {
			minFilter: LinearFilter,
			generateMipmaps: false,
			flipY: false,
			internalFormat: null
		};

		if ( options.mapping !== undefined ) values.mapping = options.mapping;
		if ( options.wrapS !== undefined ) values.wrapS = options.wrapS;
		if ( options.wrapT !== undefined ) values.wrapT = options.wrapT;
		if ( options.wrapR !== undefined ) values.wrapR = options.wrapR;
		if ( options.magFilter !== undefined ) values.magFilter = options.magFilter;
		if ( options.minFilter !== undefined ) values.minFilter = options.minFilter;
		if ( options.format !== undefined ) values.format = options.format;
		if ( options.type !== undefined ) values.type = options.type;
		if ( options.anisotropy !== undefined ) values.anisotropy = options.anisotropy;
		if ( options.colorSpace !== undefined ) values.colorSpace = options.colorSpace;
		if ( options.flipY !== undefined ) values.flipY = options.flipY;
		if ( options.generateMipmaps !== undefined ) values.generateMipmaps = options.generateMipmaps;
		if ( options.internalFormat !== undefined ) values.internalFormat = options.internalFormat;

		for ( let i = 0; i < this.textures.length; i ++ ) {

			const texture = this.textures[ i ];
			texture.setValues( values );

		}

	}

	/**
	 * The texture representing the default color attachment.
	 *
	 * @type {Texture}
	 */
	get texture() {

		return this.textures[ 0 ];

	}

	set texture( value ) {

		this.textures[ 0 ] = value;

	}

	set depthTexture( current ) {

		if ( this._depthTexture !== null ) this._depthTexture.renderTarget = null;
		if ( current !== null ) current.renderTarget = this;

		this._depthTexture = current;

	}

	/**
	 * Instead of saving the depth in a renderbuffer, a texture
	 * can be used instead which is useful for further processing
	 * e.g. in context of post-processing.
	 *
	 * @type {?DepthTexture}
	 * @default null
	 */
	get depthTexture() {

		return this._depthTexture;

	}

	/**
	 * Sets the size of this render target.
	 *
	 * @param {number} width - The width.
	 * @param {number} height - The height.
	 * @param {number} [depth=1] - The depth.
	 */
	setSize( width, height, depth = 1 ) {

		if ( this.width !== width || this.height !== height || this.depth !== depth ) {

			this.width = width;
			this.height = height;
			this.depth = depth;

			for ( let i = 0, il = this.textures.length; i < il; i ++ ) {

				this.textures[ i ].image.width = width;
				this.textures[ i ].image.height = height;
				this.textures[ i ].image.depth = depth;
				this.textures[ i ].isArrayTexture = this.textures[ i ].image.depth > 1;

			}

			this.dispose();

		}

		this.viewport.set( 0, 0, width, height );
		this.scissor.set( 0, 0, width, height );

	}

	/**
	 * Returns a new render target with copied values from this instance.
	 *
	 * @return {RenderTarget} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

	/**
	 * Copies the settings of the given render target. This is a structural copy so
	 * no resources are shared between render targets after the copy. That includes
	 * all MRT textures and the depth texture.
	 *
	 * @param {RenderTarget} source - The render target to copy.
	 * @return {RenderTarget} A reference to this instance.
	 */
	copy( source ) {

		this.width = source.width;
		this.height = source.height;
		this.depth = source.depth;

		this.scissor.copy( source.scissor );
		this.scissorTest = source.scissorTest;

		this.viewport.copy( source.viewport );

		this.textures.length = 0;

		for ( let i = 0, il = source.textures.length; i < il; i ++ ) {

			this.textures[ i ] = source.textures[ i ].clone();
			this.textures[ i ].isRenderTargetTexture = true;
			this.textures[ i ].renderTarget = this;

			// ensure image object is not shared, see #20328

			const image = Object.assign( {}, source.textures[ i ].image );
			this.textures[ i ].source = new Source( image );

		}

		this.depthBuffer = source.depthBuffer;
		this.stencilBuffer = source.stencilBuffer;

		this.resolveDepthBuffer = source.resolveDepthBuffer;
		this.resolveStencilBuffer = source.resolveStencilBuffer;

		if ( source.depthTexture !== null ) this.depthTexture = source.depthTexture.clone();

		this.samples = source.samples;

		return this;

	}

	/**
	 * Frees the GPU-related resources allocated by this instance. Call this
	 * method whenever this instance is no longer used in your app.
	 *
	 * @fires RenderTarget#dispose
	 */
	dispose() {

		this.dispatchEvent( { type: 'dispose' } );

	}

}

/**
 * A render target used in context of {@link WebGLRenderer}.
 *
 * @augments RenderTarget
 */
class WebGLRenderTarget extends RenderTarget {

	/**
	 * Constructs a new 3D render target.
	 *
	 * @param {number} [width=1] - The width of the render target.
	 * @param {number} [height=1] - The height of the render target.
	 * @param {RenderTarget~Options} [options] - The configuration object.
	 */
	constructor( width = 1, height = 1, options = {} ) {

		super( width, height, options );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isWebGLRenderTarget = true;

	}

}

const _colorKeywords = { 'aliceblue': 0xF0F8FF, 'antiquewhite': 0xFAEBD7, 'aqua': 0x00FFFF, 'aquamarine': 0x7FFFD4, 'azure': 0xF0FFFF,
	'beige': 0xF5F5DC, 'bisque': 0xFFE4C4, 'black': 0x000000, 'blanchedalmond': 0xFFEBCD, 'blue': 0x0000FF, 'blueviolet': 0x8A2BE2,
	'brown': 0xA52A2A, 'burlywood': 0xDEB887, 'cadetblue': 0x5F9EA0, 'chartreuse': 0x7FFF00, 'chocolate': 0xD2691E, 'coral': 0xFF7F50,
	'cornflowerblue': 0x6495ED, 'cornsilk': 0xFFF8DC, 'crimson': 0xDC143C, 'cyan': 0x00FFFF, 'darkblue': 0x00008B, 'darkcyan': 0x008B8B,
	'darkgoldenrod': 0xB8860B, 'darkgray': 0xA9A9A9, 'darkgreen': 0x006400, 'darkgrey': 0xA9A9A9, 'darkkhaki': 0xBDB76B, 'darkmagenta': 0x8B008B,
	'darkolivegreen': 0x556B2F, 'darkorange': 0xFF8C00, 'darkorchid': 0x9932CC, 'darkred': 0x8B0000, 'darksalmon': 0xE9967A, 'darkseagreen': 0x8FBC8F,
	'darkslateblue': 0x483D8B, 'darkslategray': 0x2F4F4F, 'darkslategrey': 0x2F4F4F, 'darkturquoise': 0x00CED1, 'darkviolet': 0x9400D3,
	'deeppink': 0xFF1493, 'deepskyblue': 0x00BFFF, 'dimgray': 0x696969, 'dimgrey': 0x696969, 'dodgerblue': 0x1E90FF, 'firebrick': 0xB22222,
	'floralwhite': 0xFFFAF0, 'forestgreen': 0x228B22, 'fuchsia': 0xFF00FF, 'gainsboro': 0xDCDCDC, 'ghostwhite': 0xF8F8FF, 'gold': 0xFFD700,
	'goldenrod': 0xDAA520, 'gray': 0x808080, 'green': 0x008000, 'greenyellow': 0xADFF2F, 'grey': 0x808080, 'honeydew': 0xF0FFF0, 'hotpink': 0xFF69B4,
	'indianred': 0xCD5C5C, 'indigo': 0x4B0082, 'ivory': 0xFFFFF0, 'khaki': 0xF0E68C, 'lavender': 0xE6E6FA, 'lavenderblush': 0xFFF0F5, 'lawngreen': 0x7CFC00,
	'lemonchiffon': 0xFFFACD, 'lightblue': 0xADD8E6, 'lightcoral': 0xF08080, 'lightcyan': 0xE0FFFF, 'lightgoldenrodyellow': 0xFAFAD2, 'lightgray': 0xD3D3D3,
	'lightgreen': 0x90EE90, 'lightgrey': 0xD3D3D3, 'lightpink': 0xFFB6C1, 'lightsalmon': 0xFFA07A, 'lightseagreen': 0x20B2AA, 'lightskyblue': 0x87CEFA,
	'lightslategray': 0x778899, 'lightslategrey': 0x778899, 'lightsteelblue': 0xB0C4DE, 'lightyellow': 0xFFFFE0, 'lime': 0x00FF00, 'limegreen': 0x32CD32,
	'linen': 0xFAF0E6, 'magenta': 0xFF00FF, 'maroon': 0x800000, 'mediumaquamarine': 0x66CDAA, 'mediumblue': 0x0000CD, 'mediumorchid': 0xBA55D3,
	'mediumpurple': 0x9370DB, 'mediumseagreen': 0x3CB371, 'mediumslateblue': 0x7B68EE, 'mediumspringgreen': 0x00FA9A, 'mediumturquoise': 0x48D1CC,
	'mediumvioletred': 0xC71585, 'midnightblue': 0x191970, 'mintcream': 0xF5FFFA, 'mistyrose': 0xFFE4E1, 'moccasin': 0xFFE4B5, 'navajowhite': 0xFFDEAD,
	'navy': 0x000080, 'oldlace': 0xFDF5E6, 'olive': 0x808000, 'olivedrab': 0x6B8E23, 'orange': 0xFFA500, 'orangered': 0xFF4500, 'orchid': 0xDA70D6,
	'palegoldenrod': 0xEEE8AA, 'palegreen': 0x98FB98, 'paleturquoise': 0xAFEEEE, 'palevioletred': 0xDB7093, 'papayawhip': 0xFFEFD5, 'peachpuff': 0xFFDAB9,
	'peru': 0xCD853F, 'pink': 0xFFC0CB, 'plum': 0xDDA0DD, 'powderblue': 0xB0E0E6, 'purple': 0x800080, 'rebeccapurple': 0x663399, 'red': 0xFF0000, 'rosybrown': 0xBC8F8F,
	'royalblue': 0x4169E1, 'saddlebrown': 0x8B4513, 'salmon': 0xFA8072, 'sandybrown': 0xF4A460, 'seagreen': 0x2E8B57, 'seashell': 0xFFF5EE,
	'sienna': 0xA0522D, 'silver': 0xC0C0C0, 'skyblue': 0x87CEEB, 'slateblue': 0x6A5ACD, 'slategray': 0x708090, 'slategrey': 0x708090, 'snow': 0xFFFAFA,
	'springgreen': 0x00FF7F, 'steelblue': 0x4682B4, 'tan': 0xD2B48C, 'teal': 0x008080, 'thistle': 0xD8BFD8, 'tomato': 0xFF6347, 'turquoise': 0x40E0D0,
	'violet': 0xEE82EE, 'wheat': 0xF5DEB3, 'white': 0xFFFFFF, 'whitesmoke': 0xF5F5F5, 'yellow': 0xFFFF00, 'yellowgreen': 0x9ACD32 };

const _hslA = { h: 0, s: 0, l: 0 };
const _hslB = { h: 0, s: 0, l: 0 };

function hue2rgb( p, q, t ) {

	if ( t < 0 ) t += 1;
	if ( t > 1 ) t -= 1;
	if ( t < 1 / 6 ) return p + ( q - p ) * 6 * t;
	if ( t < 1 / 2 ) return q;
	if ( t < 2 / 3 ) return p + ( q - p ) * 6 * ( 2 / 3 - t );
	return p;

}

/**
 * A Color instance is represented by RGB components in the linear <i>working
 * color space</i>, which defaults to `LinearSRGBColorSpace`. Inputs
 * conventionally using `SRGBColorSpace` (such as hexadecimals and CSS
 * strings) are converted to the working color space automatically.
 *
 * ```js
 * // converted automatically from SRGBColorSpace to LinearSRGBColorSpace
 * const color = new THREE.Color().setHex( 0x112233 );
 * ```
 * Source color spaces may be specified explicitly, to ensure correct conversions.
 * ```js
 * // assumed already LinearSRGBColorSpace; no conversion
 * const color = new THREE.Color().setRGB( 0.5, 0.5, 0.5 );
 *
 * // converted explicitly from SRGBColorSpace to LinearSRGBColorSpace
 * const color = new THREE.Color().setRGB( 0.5, 0.5, 0.5, SRGBColorSpace );
 * ```
 * If THREE.ColorManagement is disabled, no conversions occur. For details,
 * see <i>Color management</i>. Iterating through a Color instance will yield
 * its components (r, g, b) in the corresponding order. A Color can be initialised
 * in any of the following ways:
 * ```js
 * //empty constructor - will default white
 * const color1 = new THREE.Color();
 *
 * //Hexadecimal color (recommended)
 * const color2 = new THREE.Color( 0xff0000 );
 *
 * //RGB string
 * const color3 = new THREE.Color("rgb(255, 0, 0)");
 * const color4 = new THREE.Color("rgb(100%, 0%, 0%)");
 *
 * //X11 color name - all 140 color names are supported.
 * //Note the lack of CamelCase in the name
 * const color5 = new THREE.Color( 'skyblue' );
 * //HSL string
 * const color6 = new THREE.Color("hsl(0, 100%, 50%)");
 *
 * //Separate RGB values between 0 and 1
 * const color7 = new THREE.Color( 1, 0, 0 );
 * ```
 */
class Color {

	/**
	 * Constructs a new color.
	 *
	 * Note that standard method of specifying color in three.js is with a hexadecimal triplet,
	 * and that method is used throughout the rest of the documentation.
	 *
	 * @param {(number|string|Color)} [r] - The red component of the color. If `g` and `b` are
	 * not provided, it can be hexadecimal triplet, a CSS-style string or another `Color` instance.
	 * @param {number} [g] - The green component.
	 * @param {number} [b] - The blue component.
	 */
	constructor( r, g, b ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isColor = true;

		/**
		 * The red component.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.r = 1;

		/**
		 * The green component.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.g = 1;

		/**
		 * The blue component.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.b = 1;

		return this.set( r, g, b );

	}

	/**
	 * Sets the colors's components from the given values.
	 *
	 * @param {(number|string|Color)} [r] - The red component of the color. If `g` and `b` are
	 * not provided, it can be hexadecimal triplet, a CSS-style string or another `Color` instance.
	 * @param {number} [g] - The green component.
	 * @param {number} [b] - The blue component.
	 * @return {Color} A reference to this color.
	 */
	set( r, g, b ) {

		if ( g === undefined && b === undefined ) {

			// r is THREE.Color, hex or string

			const value = r;

			if ( value && value.isColor ) {

				this.copy( value );

			} else if ( typeof value === 'number' ) {

				this.setHex( value );

			} else if ( typeof value === 'string' ) {

				this.setStyle( value );

			}

		} else {

			this.setRGB( r, g, b );

		}

		return this;

	}

	/**
	 * Sets the colors's components to the given scalar value.
	 *
	 * @param {number} scalar - The scalar value.
	 * @return {Color} A reference to this color.
	 */
	setScalar( scalar ) {

		this.r = scalar;
		this.g = scalar;
		this.b = scalar;

		return this;

	}

	/**
	 * Sets this color from a hexadecimal value.
	 *
	 * @param {number} hex - The hexadecimal value.
	 * @param {string} [colorSpace=SRGBColorSpace] - The color space.
	 * @return {Color} A reference to this color.
	 */
	setHex( hex, colorSpace = SRGBColorSpace ) {

		hex = Math.floor( hex );

		this.r = ( hex >> 16 & 255 ) / 255;
		this.g = ( hex >> 8 & 255 ) / 255;
		this.b = ( hex & 255 ) / 255;

		ColorManagement.colorSpaceToWorking( this, colorSpace );

		return this;

	}

	/**
	 * Sets this color from RGB values.
	 *
	 * @param {number} r - Red channel value between `0.0` and `1.0`.
	 * @param {number} g - Green channel value between `0.0` and `1.0`.
	 * @param {number} b - Blue channel value between `0.0` and `1.0`.
	 * @param {string} [colorSpace=ColorManagement.workingColorSpace] - The color space.
	 * @return {Color} A reference to this color.
	 */
	setRGB( r, g, b, colorSpace = ColorManagement.workingColorSpace ) {

		this.r = r;
		this.g = g;
		this.b = b;

		ColorManagement.colorSpaceToWorking( this, colorSpace );

		return this;

	}

	/**
	 * Sets this color from RGB values.
	 *
	 * @param {number} h - Hue value between `0.0` and `1.0`.
	 * @param {number} s - Saturation value between `0.0` and `1.0`.
	 * @param {number} l - Lightness value between `0.0` and `1.0`.
	 * @param {string} [colorSpace=ColorManagement.workingColorSpace] - The color space.
	 * @return {Color} A reference to this color.
	 */
	setHSL( h, s, l, colorSpace = ColorManagement.workingColorSpace ) {

		// h,s,l ranges are in 0.0 - 1.0
		h = euclideanModulo( h, 1 );
		s = clamp( s, 0, 1 );
		l = clamp( l, 0, 1 );

		if ( s === 0 ) {

			this.r = this.g = this.b = l;

		} else {

			const p = l <= 0.5 ? l * ( 1 + s ) : l + s - ( l * s );
			const q = ( 2 * l ) - p;

			this.r = hue2rgb( q, p, h + 1 / 3 );
			this.g = hue2rgb( q, p, h );
			this.b = hue2rgb( q, p, h - 1 / 3 );

		}

		ColorManagement.colorSpaceToWorking( this, colorSpace );

		return this;

	}

	/**
	 * Sets this color from a CSS-style string. For example, `rgb(250, 0,0)`,
	 * `rgb(100%, 0%, 0%)`, `hsl(0, 100%, 50%)`, `#ff0000`, `#f00`, or `red` ( or
	 * any [X11 color name]{@link https://en.wikipedia.org/wiki/X11_color_names#Color_name_chart} -
	 * all 140 color names are supported).
	 *
	 * @param {string} style - Color as a CSS-style string.
	 * @param {string} [colorSpace=SRGBColorSpace] - The color space.
	 * @return {Color} A reference to this color.
	 */
	setStyle( style, colorSpace = SRGBColorSpace ) {

		function handleAlpha( string ) {

			if ( string === undefined ) return;

			if ( parseFloat( string ) < 1 ) {

				console.warn( 'THREE.Color: Alpha component of ' + style + ' will be ignored.' );

			}

		}


		let m;

		if ( m = /^(\w+)\(([^\)]*)\)/.exec( style ) ) {

			// rgb / hsl

			let color;
			const name = m[ 1 ];
			const components = m[ 2 ];

			switch ( name ) {

				case 'rgb':
				case 'rgba':

					if ( color = /^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec( components ) ) {

						// rgb(255,0,0) rgba(255,0,0,0.5)

						handleAlpha( color[ 4 ] );

						return this.setRGB(
							Math.min( 255, parseInt( color[ 1 ], 10 ) ) / 255,
							Math.min( 255, parseInt( color[ 2 ], 10 ) ) / 255,
							Math.min( 255, parseInt( color[ 3 ], 10 ) ) / 255,
							colorSpace
						);

					}

					if ( color = /^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec( components ) ) {

						// rgb(100%,0%,0%) rgba(100%,0%,0%,0.5)

						handleAlpha( color[ 4 ] );

						return this.setRGB(
							Math.min( 100, parseInt( color[ 1 ], 10 ) ) / 100,
							Math.min( 100, parseInt( color[ 2 ], 10 ) ) / 100,
							Math.min( 100, parseInt( color[ 3 ], 10 ) ) / 100,
							colorSpace
						);

					}

					break;

				case 'hsl':
				case 'hsla':

					if ( color = /^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec( components ) ) {

						// hsl(120,50%,50%) hsla(120,50%,50%,0.5)

						handleAlpha( color[ 4 ] );

						return this.setHSL(
							parseFloat( color[ 1 ] ) / 360,
							parseFloat( color[ 2 ] ) / 100,
							parseFloat( color[ 3 ] ) / 100,
							colorSpace
						);

					}

					break;

				default:

					console.warn( 'THREE.Color: Unknown color model ' + style );

			}

		} else if ( m = /^\#([A-Fa-f\d]+)$/.exec( style ) ) {

			// hex color

			const hex = m[ 1 ];
			const size = hex.length;

			if ( size === 3 ) {

				// #ff0
				return this.setRGB(
					parseInt( hex.charAt( 0 ), 16 ) / 15,
					parseInt( hex.charAt( 1 ), 16 ) / 15,
					parseInt( hex.charAt( 2 ), 16 ) / 15,
					colorSpace
				);

			} else if ( size === 6 ) {

				// #ff0000
				return this.setHex( parseInt( hex, 16 ), colorSpace );

			} else {

				console.warn( 'THREE.Color: Invalid hex color ' + style );

			}

		} else if ( style && style.length > 0 ) {

			return this.setColorName( style, colorSpace );

		}

		return this;

	}

	/**
	 * Sets this color from a color name. Faster than {@link Color#setStyle} if
	 * you don't need the other CSS-style formats.
	 *
	 * For convenience, the list of names is exposed in `Color.NAMES` as a hash.
	 * ```js
	 * Color.NAMES.aliceblue // returns 0xF0F8FF
	 * ```
	 *
	 * @param {string} style - The color name.
	 * @param {string} [colorSpace=SRGBColorSpace] - The color space.
	 * @return {Color} A reference to this color.
	 */
	setColorName( style, colorSpace = SRGBColorSpace ) {

		// color keywords
		const hex = _colorKeywords[ style.toLowerCase() ];

		if ( hex !== undefined ) {

			// red
			this.setHex( hex, colorSpace );

		} else {

			// unknown color
			console.warn( 'THREE.Color: Unknown color ' + style );

		}

		return this;

	}

	/**
	 * Returns a new color with copied values from this instance.
	 *
	 * @return {Color} A clone of this instance.
	 */
	clone() {

		return new this.constructor( this.r, this.g, this.b );

	}

	/**
	 * Copies the values of the given color to this instance.
	 *
	 * @param {Color} color - The color to copy.
	 * @return {Color} A reference to this color.
	 */
	copy( color ) {

		this.r = color.r;
		this.g = color.g;
		this.b = color.b;

		return this;

	}

	/**
	 * Copies the given color into this color, and then converts this color from
	 * `SRGBColorSpace` to `LinearSRGBColorSpace`.
	 *
	 * @param {Color} color - The color to copy/convert.
	 * @return {Color} A reference to this color.
	 */
	copySRGBToLinear( color ) {

		this.r = SRGBToLinear( color.r );
		this.g = SRGBToLinear( color.g );
		this.b = SRGBToLinear( color.b );

		return this;

	}

	/**
	 * Copies the given color into this color, and then converts this color from
	 * `LinearSRGBColorSpace` to `SRGBColorSpace`.
	 *
	 * @param {Color} color - The color to copy/convert.
	 * @return {Color} A reference to this color.
	 */
	copyLinearToSRGB( color ) {

		this.r = LinearToSRGB( color.r );
		this.g = LinearToSRGB( color.g );
		this.b = LinearToSRGB( color.b );

		return this;

	}

	/**
	 * Converts this color from `SRGBColorSpace` to `LinearSRGBColorSpace`.
	 *
	 * @return {Color} A reference to this color.
	 */
	convertSRGBToLinear() {

		this.copySRGBToLinear( this );

		return this;

	}

	/**
	 * Converts this color from `LinearSRGBColorSpace` to `SRGBColorSpace`.
	 *
	 * @return {Color} A reference to this color.
	 */
	convertLinearToSRGB() {

		this.copyLinearToSRGB( this );

		return this;

	}

	/**
	 * Returns the hexadecimal value of this color.
	 *
	 * @param {string} [colorSpace=SRGBColorSpace] - The color space.
	 * @return {number} The hexadecimal value.
	 */
	getHex( colorSpace = SRGBColorSpace ) {

		ColorManagement.workingToColorSpace( _color.copy( this ), colorSpace );

		return Math.round( clamp( _color.r * 255, 0, 255 ) ) * 65536 + Math.round( clamp( _color.g * 255, 0, 255 ) ) * 256 + Math.round( clamp( _color.b * 255, 0, 255 ) );

	}

	/**
	 * Returns the hexadecimal value of this color as a string (for example, 'FFFFFF').
	 *
	 * @param {string} [colorSpace=SRGBColorSpace] - The color space.
	 * @return {string} The hexadecimal value as a string.
	 */
	getHexString( colorSpace = SRGBColorSpace ) {

		return ( '000000' + this.getHex( colorSpace ).toString( 16 ) ).slice( -6 );

	}

	/**
	 * Converts the colors RGB values into the HSL format and stores them into the
	 * given target object.
	 *
	 * @param {{h:number,s:number,l:number}} target - The target object that is used to store the method's result.
	 * @param {string} [colorSpace=ColorManagement.workingColorSpace] - The color space.
	 * @return {{h:number,s:number,l:number}} The HSL representation of this color.
	 */
	getHSL( target, colorSpace = ColorManagement.workingColorSpace ) {

		// h,s,l ranges are in 0.0 - 1.0

		ColorManagement.workingToColorSpace( _color.copy( this ), colorSpace );

		const r = _color.r, g = _color.g, b = _color.b;

		const max = Math.max( r, g, b );
		const min = Math.min( r, g, b );

		let hue, saturation;
		const lightness = ( min + max ) / 2.0;

		if ( min === max ) {

			hue = 0;
			saturation = 0;

		} else {

			const delta = max - min;

			saturation = lightness <= 0.5 ? delta / ( max + min ) : delta / ( 2 - max - min );

			switch ( max ) {

				case r: hue = ( g - b ) / delta + ( g < b ? 6 : 0 ); break;
				case g: hue = ( b - r ) / delta + 2; break;
				case b: hue = ( r - g ) / delta + 4; break;

			}

			hue /= 6;

		}

		target.h = hue;
		target.s = saturation;
		target.l = lightness;

		return target;

	}

	/**
	 * Returns the RGB values of this color and stores them into the given target object.
	 *
	 * @param {Color} target - The target color that is used to store the method's result.
	 * @param {string} [colorSpace=ColorManagement.workingColorSpace] - The color space.
	 * @return {Color} The RGB representation of this color.
	 */
	getRGB( target, colorSpace = ColorManagement.workingColorSpace ) {

		ColorManagement.workingToColorSpace( _color.copy( this ), colorSpace );

		target.r = _color.r;
		target.g = _color.g;
		target.b = _color.b;

		return target;

	}

	/**
	 * Returns the value of this color as a CSS style string. Example: `rgb(255,0,0)`.
	 *
	 * @param {string} [colorSpace=SRGBColorSpace] - The color space.
	 * @return {string} The CSS representation of this color.
	 */
	getStyle( colorSpace = SRGBColorSpace ) {

		ColorManagement.workingToColorSpace( _color.copy( this ), colorSpace );

		const r = _color.r, g = _color.g, b = _color.b;

		if ( colorSpace !== SRGBColorSpace ) {

			// Requires CSS Color Module Level 4 (https://www.w3.org/TR/css-color-4/).
			return `color(${ colorSpace } ${ r.toFixed( 3 ) } ${ g.toFixed( 3 ) } ${ b.toFixed( 3 ) })`;

		}

		return `rgb(${ Math.round( r * 255 ) },${ Math.round( g * 255 ) },${ Math.round( b * 255 ) })`;

	}

	/**
	 * Adds the given HSL values to this color's values.
	 * Internally, this converts the color's RGB values to HSL, adds HSL
	 * and then converts the color back to RGB.
	 *
	 * @param {number} h - Hue value between `0.0` and `1.0`.
	 * @param {number} s - Saturation value between `0.0` and `1.0`.
	 * @param {number} l - Lightness value between `0.0` and `1.0`.
	 * @return {Color} A reference to this color.
	 */
	offsetHSL( h, s, l ) {

		this.getHSL( _hslA );

		return this.setHSL( _hslA.h + h, _hslA.s + s, _hslA.l + l );

	}

	/**
	 * Adds the RGB values of the given color to the RGB values of this color.
	 *
	 * @param {Color} color - The color to add.
	 * @return {Color} A reference to this color.
	 */
	add( color ) {

		this.r += color.r;
		this.g += color.g;
		this.b += color.b;

		return this;

	}

	/**
	 * Adds the RGB values of the given colors and stores the result in this instance.
	 *
	 * @param {Color} color1 - The first color.
	 * @param {Color} color2 - The second color.
	 * @return {Color} A reference to this color.
	 */
	addColors( color1, color2 ) {

		this.r = color1.r + color2.r;
		this.g = color1.g + color2.g;
		this.b = color1.b + color2.b;

		return this;

	}

	/**
	 * Adds the given scalar value to the RGB values of this color.
	 *
	 * @param {number} s - The scalar to add.
	 * @return {Color} A reference to this color.
	 */
	addScalar( s ) {

		this.r += s;
		this.g += s;
		this.b += s;

		return this;

	}

	/**
	 * Subtracts the RGB values of the given color from the RGB values of this color.
	 *
	 * @param {Color} color - The color to subtract.
	 * @return {Color} A reference to this color.
	 */
	sub( color ) {

		this.r = Math.max( 0, this.r - color.r );
		this.g = Math.max( 0, this.g - color.g );
		this.b = Math.max( 0, this.b - color.b );

		return this;

	}

	/**
	 * Multiplies the RGB values of the given color with the RGB values of this color.
	 *
	 * @param {Color} color - The color to multiply.
	 * @return {Color} A reference to this color.
	 */
	multiply( color ) {

		this.r *= color.r;
		this.g *= color.g;
		this.b *= color.b;

		return this;

	}

	/**
	 * Multiplies the given scalar value with the RGB values of this color.
	 *
	 * @param {number} s - The scalar to multiply.
	 * @return {Color} A reference to this color.
	 */
	multiplyScalar( s ) {

		this.r *= s;
		this.g *= s;
		this.b *= s;

		return this;

	}

	/**
	 * Linearly interpolates this color's RGB values toward the RGB values of the
	 * given color. The alpha argument can be thought of as the ratio between
	 * the two colors, where `0.0` is this color and `1.0` is the first argument.
	 *
	 * @param {Color} color - The color to converge on.
	 * @param {number} alpha - The interpolation factor in the closed interval `[0,1]`.
	 * @return {Color} A reference to this color.
	 */
	lerp( color, alpha ) {

		this.r += ( color.r - this.r ) * alpha;
		this.g += ( color.g - this.g ) * alpha;
		this.b += ( color.b - this.b ) * alpha;

		return this;

	}

	/**
	 * Linearly interpolates between the given colors and stores the result in this instance.
	 * The alpha argument can be thought of as the ratio between the two colors, where `0.0`
	 * is the first and `1.0` is the second color.
	 *
	 * @param {Color} color1 - The first color.
	 * @param {Color} color2 - The second color.
	 * @param {number} alpha - The interpolation factor in the closed interval `[0,1]`.
	 * @return {Color} A reference to this color.
	 */
	lerpColors( color1, color2, alpha ) {

		this.r = color1.r + ( color2.r - color1.r ) * alpha;
		this.g = color1.g + ( color2.g - color1.g ) * alpha;
		this.b = color1.b + ( color2.b - color1.b ) * alpha;

		return this;

	}

	/**
	 * Linearly interpolates this color's HSL values toward the HSL values of the
	 * given color. It differs from {@link Color#lerp} by not interpolating straight
	 * from one color to the other, but instead going through all the hues in between
	 * those two colors. The alpha argument can be thought of as the ratio between
	 * the two colors, where 0.0 is this color and 1.0 is the first argument.
	 *
	 * @param {Color} color - The color to converge on.
	 * @param {number} alpha - The interpolation factor in the closed interval `[0,1]`.
	 * @return {Color} A reference to this color.
	 */
	lerpHSL( color, alpha ) {

		this.getHSL( _hslA );
		color.getHSL( _hslB );

		const h = lerp( _hslA.h, _hslB.h, alpha );
		const s = lerp( _hslA.s, _hslB.s, alpha );
		const l = lerp( _hslA.l, _hslB.l, alpha );

		this.setHSL( h, s, l );

		return this;

	}

	/**
	 * Sets the color's RGB components from the given 3D vector.
	 *
	 * @param {Vector3} v - The vector to set.
	 * @return {Color} A reference to this color.
	 */
	setFromVector3( v ) {

		this.r = v.x;
		this.g = v.y;
		this.b = v.z;

		return this;

	}

	/**
	 * Transforms this color with the given 3x3 matrix.
	 *
	 * @param {Matrix3} m - The matrix.
	 * @return {Color} A reference to this color.
	 */
	applyMatrix3( m ) {

		const r = this.r, g = this.g, b = this.b;
		const e = m.elements;

		this.r = e[ 0 ] * r + e[ 3 ] * g + e[ 6 ] * b;
		this.g = e[ 1 ] * r + e[ 4 ] * g + e[ 7 ] * b;
		this.b = e[ 2 ] * r + e[ 5 ] * g + e[ 8 ] * b;

		return this;

	}

	/**
	 * Returns `true` if this color is equal with the given one.
	 *
	 * @param {Color} c - The color to test for equality.
	 * @return {boolean} Whether this bounding color is equal with the given one.
	 */
	equals( c ) {

		return ( c.r === this.r ) && ( c.g === this.g ) && ( c.b === this.b );

	}

	/**
	 * Sets this color's RGB components from the given array.
	 *
	 * @param {Array<number>} array - An array holding the RGB values.
	 * @param {number} [offset=0] - The offset into the array.
	 * @return {Color} A reference to this color.
	 */
	fromArray( array, offset = 0 ) {

		this.r = array[ offset ];
		this.g = array[ offset + 1 ];
		this.b = array[ offset + 2 ];

		return this;

	}

	/**
	 * Writes the RGB components of this color to the given array. If no array is provided,
	 * the method returns a new instance.
	 *
	 * @param {Array<number>} [array=[]] - The target array holding the color components.
	 * @param {number} [offset=0] - Index of the first element in the array.
	 * @return {Array<number>} The color components.
	 */
	toArray( array = [], offset = 0 ) {

		array[ offset ] = this.r;
		array[ offset + 1 ] = this.g;
		array[ offset + 2 ] = this.b;

		return array;

	}

	/**
	 * Sets the components of this color from the given buffer attribute.
	 *
	 * @param {BufferAttribute} attribute - The buffer attribute holding color data.
	 * @param {number} index - The index into the attribute.
	 * @return {Color} A reference to this color.
	 */
	fromBufferAttribute( attribute, index ) {

		this.r = attribute.getX( index );
		this.g = attribute.getY( index );
		this.b = attribute.getZ( index );

		return this;

	}

	/**
	 * This methods defines the serialization result of this class. Returns the color
	 * as a hexadecimal value.
	 *
	 * @return {number} The hexadecimal value.
	 */
	toJSON() {

		return this.getHex();

	}

	*[ Symbol.iterator ]() {

		yield this.r;
		yield this.g;
		yield this.b;

	}

}

const _color = /*@__PURE__*/ new Color();

/**
 * A dictionary with X11 color names.
 *
 * Note that multiple words such as Dark Orange become the string 'darkorange'.
 *
 * @static
 * @type {Object}
 */
Color.NAMES = _colorKeywords;

/**
 * Represents an axis-aligned bounding box (AABB) in 3D space.
 */
class Box3 {

	/**
	 * Constructs a new bounding box.
	 *
	 * @param {Vector3} [min=(Infinity,Infinity,Infinity)] - A vector representing the lower boundary of the box.
	 * @param {Vector3} [max=(-Infinity,-Infinity,-Infinity)] - A vector representing the upper boundary of the box.
	 */
	constructor( min = new Vector3( + Infinity, + Infinity, + Infinity ), max = new Vector3( - Infinity, - Infinity, - Infinity ) ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isBox3 = true;

		/**
		 * The lower boundary of the box.
		 *
		 * @type {Vector3}
		 */
		this.min = min;

		/**
		 * The upper boundary of the box.
		 *
		 * @type {Vector3}
		 */
		this.max = max;

	}

	/**
	 * Sets the lower and upper boundaries of this box.
	 * Please note that this method only copies the values from the given objects.
	 *
	 * @param {Vector3} min - The lower boundary of the box.
	 * @param {Vector3} max - The upper boundary of the box.
	 * @return {Box3} A reference to this bounding box.
	 */
	set( min, max ) {

		this.min.copy( min );
		this.max.copy( max );

		return this;

	}

	/**
	 * Sets the upper and lower bounds of this box so it encloses the position data
	 * in the given array.
	 *
	 * @param {Array<number>} array - An array holding 3D position data.
	 * @return {Box3} A reference to this bounding box.
	 */
	setFromArray( array ) {

		this.makeEmpty();

		for ( let i = 0, il = array.length; i < il; i += 3 ) {

			this.expandByPoint( _vector$7.fromArray( array, i ) );

		}

		return this;

	}

	/**
	 * Sets the upper and lower bounds of this box so it encloses the position data
	 * in the given buffer attribute.
	 *
	 * @param {BufferAttribute} attribute - A buffer attribute holding 3D position data.
	 * @return {Box3} A reference to this bounding box.
	 */
	setFromBufferAttribute( attribute ) {

		this.makeEmpty();

		for ( let i = 0, il = attribute.count; i < il; i ++ ) {

			this.expandByPoint( _vector$7.fromBufferAttribute( attribute, i ) );

		}

		return this;

	}

	/**
	 * Sets the upper and lower bounds of this box so it encloses the position data
	 * in the given array.
	 *
	 * @param {Array<Vector3>} points - An array holding 3D position data as instances of {@link Vector3}.
	 * @return {Box3} A reference to this bounding box.
	 */
	setFromPoints( points ) {

		this.makeEmpty();

		for ( let i = 0, il = points.length; i < il; i ++ ) {

			this.expandByPoint( points[ i ] );

		}

		return this;

	}

	/**
	 * Centers this box on the given center vector and sets this box's width, height and
	 * depth to the given size values.
	 *
	 * @param {Vector3} center - The center of the box.
	 * @param {Vector3} size - The x, y and z dimensions of the box.
	 * @return {Box3} A reference to this bounding box.
	 */
	setFromCenterAndSize( center, size ) {

		const halfSize = _vector$7.copy( size ).multiplyScalar( 0.5 );

		this.min.copy( center ).sub( halfSize );
		this.max.copy( center ).add( halfSize );

		return this;

	}

	/**
	 * Computes the world-axis-aligned bounding box for the given 3D object
	 * (including its children), accounting for the object's, and children's,
	 * world transforms. The function may result in a larger box than strictly necessary.
	 *
	 * @param {Object3D} object - The 3D object to compute the bounding box for.
	 * @param {boolean} [precise=false] - If set to `true`, the method computes the smallest
	 * world-axis-aligned bounding box at the expense of more computation.
	 * @return {Box3} A reference to this bounding box.
	 */
	setFromObject( object, precise = false ) {

		this.makeEmpty();

		return this.expandByObject( object, precise );

	}

	/**
	 * Returns a new box with copied values from this instance.
	 *
	 * @return {Box3} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

	/**
	 * Copies the values of the given box to this instance.
	 *
	 * @param {Box3} box - The box to copy.
	 * @return {Box3} A reference to this bounding box.
	 */
	copy( box ) {

		this.min.copy( box.min );
		this.max.copy( box.max );

		return this;

	}

	/**
	 * Makes this box empty which means in encloses a zero space in 3D.
	 *
	 * @return {Box3} A reference to this bounding box.
	 */
	makeEmpty() {

		this.min.x = this.min.y = this.min.z = + Infinity;
		this.max.x = this.max.y = this.max.z = - Infinity;

		return this;

	}

	/**
	 * Returns true if this box includes zero points within its bounds.
	 * Note that a box with equal lower and upper bounds still includes one
	 * point, the one both bounds share.
	 *
	 * @return {boolean} Whether this box is empty or not.
	 */
	isEmpty() {

		// this is a more robust check for empty than ( volume <= 0 ) because volume can get positive with two negative axes

		return ( this.max.x < this.min.x ) || ( this.max.y < this.min.y ) || ( this.max.z < this.min.z );

	}

	/**
	 * Returns the center point of this box.
	 *
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The center point.
	 */
	getCenter( target ) {

		return this.isEmpty() ? target.set( 0, 0, 0 ) : target.addVectors( this.min, this.max ).multiplyScalar( 0.5 );

	}

	/**
	 * Returns the dimensions of this box.
	 *
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The size.
	 */
	getSize( target ) {

		return this.isEmpty() ? target.set( 0, 0, 0 ) : target.subVectors( this.max, this.min );

	}

	/**
	 * Expands the boundaries of this box to include the given point.
	 *
	 * @param {Vector3} point - The point that should be included by the bounding box.
	 * @return {Box3} A reference to this bounding box.
	 */
	expandByPoint( point ) {

		this.min.min( point );
		this.max.max( point );

		return this;

	}

	/**
	 * Expands this box equilaterally by the given vector. The width of this
	 * box will be expanded by the x component of the vector in both
	 * directions. The height of this box will be expanded by the y component of
	 * the vector in both directions. The depth of this box will be
	 * expanded by the z component of the vector in both directions.
	 *
	 * @param {Vector3} vector - The vector that should expand the bounding box.
	 * @return {Box3} A reference to this bounding box.
	 */
	expandByVector( vector ) {

		this.min.sub( vector );
		this.max.add( vector );

		return this;

	}

	/**
	 * Expands each dimension of the box by the given scalar. If negative, the
	 * dimensions of the box will be contracted.
	 *
	 * @param {number} scalar - The scalar value that should expand the bounding box.
	 * @return {Box3} A reference to this bounding box.
	 */
	expandByScalar( scalar ) {

		this.min.addScalar( - scalar );
		this.max.addScalar( scalar );

		return this;

	}

	/**
	 * Expands the boundaries of this box to include the given 3D object and
	 * its children, accounting for the object's, and children's, world
	 * transforms. The function may result in a larger box than strictly
	 * necessary (unless the precise parameter is set to true).
	 *
	 * @param {Object3D} object - The 3D object that should expand the bounding box.
	 * @param {boolean} precise - If set to `true`, the method expands the bounding box
	 * as little as necessary at the expense of more computation.
	 * @return {Box3} A reference to this bounding box.
	 */
	expandByObject( object, precise = false ) {

		// Computes the world-axis-aligned bounding box of an object (including its children),
		// accounting for both the object's, and children's, world transforms

		object.updateWorldMatrix( false, false );

		const geometry = object.geometry;

		if ( geometry !== undefined ) {

			const positionAttribute = geometry.getAttribute( 'position' );

			// precise AABB computation based on vertex data requires at least a position attribute.
			// instancing isn't supported so far and uses the normal (conservative) code path.

			if ( precise === true && positionAttribute !== undefined && object.isInstancedMesh !== true ) {

				for ( let i = 0, l = positionAttribute.count; i < l; i ++ ) {

					if ( object.isMesh === true ) {

						object.getVertexPosition( i, _vector$7 );

					} else {

						_vector$7.fromBufferAttribute( positionAttribute, i );

					}

					_vector$7.applyMatrix4( object.matrixWorld );
					this.expandByPoint( _vector$7 );

				}

			} else {

				if ( object.boundingBox !== undefined ) {

					// object-level bounding box

					if ( object.boundingBox === null ) {

						object.computeBoundingBox();

					}

					_box$3.copy( object.boundingBox );


				} else {

					// geometry-level bounding box

					if ( geometry.boundingBox === null ) {

						geometry.computeBoundingBox();

					}

					_box$3.copy( geometry.boundingBox );

				}

				_box$3.applyMatrix4( object.matrixWorld );

				this.union( _box$3 );

			}

		}

		const children = object.children;

		for ( let i = 0, l = children.length; i < l; i ++ ) {

			this.expandByObject( children[ i ], precise );

		}

		return this;

	}

	/**
	 * Returns `true` if the given point lies within or on the boundaries of this box.
	 *
	 * @param {Vector3} point - The point to test.
	 * @return {boolean} Whether the bounding box contains the given point or not.
	 */
	containsPoint( point ) {

		return point.x >= this.min.x && point.x <= this.max.x &&
			point.y >= this.min.y && point.y <= this.max.y &&
			point.z >= this.min.z && point.z <= this.max.z;

	}

	/**
	 * Returns `true` if this bounding box includes the entirety of the given bounding box.
	 * If this box and the given one are identical, this function also returns `true`.
	 *
	 * @param {Box3} box - The bounding box to test.
	 * @return {boolean} Whether the bounding box contains the given bounding box or not.
	 */
	containsBox( box ) {

		return this.min.x <= box.min.x && box.max.x <= this.max.x &&
			this.min.y <= box.min.y && box.max.y <= this.max.y &&
			this.min.z <= box.min.z && box.max.z <= this.max.z;

	}

	/**
	 * Returns a point as a proportion of this box's width, height and depth.
	 *
	 * @param {Vector3} point - A point in 3D space.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} A point as a proportion of this box's width, height and depth.
	 */
	getParameter( point, target ) {

		// This can potentially have a divide by zero if the box
		// has a size dimension of 0.

		return target.set(
			( point.x - this.min.x ) / ( this.max.x - this.min.x ),
			( point.y - this.min.y ) / ( this.max.y - this.min.y ),
			( point.z - this.min.z ) / ( this.max.z - this.min.z )
		);

	}

	/**
	 * Returns `true` if the given bounding box intersects with this bounding box.
	 *
	 * @param {Box3} box - The bounding box to test.
	 * @return {boolean} Whether the given bounding box intersects with this bounding box.
	 */
	intersectsBox( box ) {

		// using 6 splitting planes to rule out intersections.
		return box.max.x >= this.min.x && box.min.x <= this.max.x &&
			box.max.y >= this.min.y && box.min.y <= this.max.y &&
			box.max.z >= this.min.z && box.min.z <= this.max.z;

	}

	/**
	 * Returns `true` if the given bounding sphere intersects with this bounding box.
	 *
	 * @param {Sphere} sphere - The bounding sphere to test.
	 * @return {boolean} Whether the given bounding sphere intersects with this bounding box.
	 */
	intersectsSphere( sphere ) {

		// Find the point on the AABB closest to the sphere center.
		this.clampPoint( sphere.center, _vector$7 );

		// If that point is inside the sphere, the AABB and sphere intersect.
		return _vector$7.distanceToSquared( sphere.center ) <= ( sphere.radius * sphere.radius );

	}

	/**
	 * Returns `true` if the given plane intersects with this bounding box.
	 *
	 * @param {Plane} plane - The plane to test.
	 * @return {boolean} Whether the given plane intersects with this bounding box.
	 */
	intersectsPlane( plane ) {

		// We compute the minimum and maximum dot product values. If those values
		// are on the same side (back or front) of the plane, then there is no intersection.

		let min, max;

		if ( plane.normal.x > 0 ) {

			min = plane.normal.x * this.min.x;
			max = plane.normal.x * this.max.x;

		} else {

			min = plane.normal.x * this.max.x;
			max = plane.normal.x * this.min.x;

		}

		if ( plane.normal.y > 0 ) {

			min += plane.normal.y * this.min.y;
			max += plane.normal.y * this.max.y;

		} else {

			min += plane.normal.y * this.max.y;
			max += plane.normal.y * this.min.y;

		}

		if ( plane.normal.z > 0 ) {

			min += plane.normal.z * this.min.z;
			max += plane.normal.z * this.max.z;

		} else {

			min += plane.normal.z * this.max.z;
			max += plane.normal.z * this.min.z;

		}

		return ( min <= - plane.constant && max >= - plane.constant );

	}

	/**
	 * Returns `true` if the given triangle intersects with this bounding box.
	 *
	 * @param {Triangle} triangle - The triangle to test.
	 * @return {boolean} Whether the given triangle intersects with this bounding box.
	 */
	intersectsTriangle( triangle ) {

		if ( this.isEmpty() ) {

			return false;

		}

		// compute box center and extents
		this.getCenter( _center );
		_extents.subVectors( this.max, _center );

		// translate triangle to aabb origin
		_v0$3.subVectors( triangle.a, _center );
		_v1$6.subVectors( triangle.b, _center );
		_v2$3.subVectors( triangle.c, _center );

		// compute edge vectors for triangle
		_f0.subVectors( _v1$6, _v0$3 );
		_f1.subVectors( _v2$3, _v1$6 );
		_f2.subVectors( _v0$3, _v2$3 );

		// test against axes that are given by cross product combinations of the edges of the triangle and the edges of the aabb
		// make an axis testing of each of the 3 sides of the aabb against each of the 3 sides of the triangle = 9 axis of separation
		// axis_ij = u_i x f_j (u0, u1, u2 = face normals of aabb = x,y,z axes vectors since aabb is axis aligned)
		let axes = [
			0, - _f0.z, _f0.y, 0, - _f1.z, _f1.y, 0, - _f2.z, _f2.y,
			_f0.z, 0, - _f0.x, _f1.z, 0, - _f1.x, _f2.z, 0, - _f2.x,
			- _f0.y, _f0.x, 0, - _f1.y, _f1.x, 0, - _f2.y, _f2.x, 0
		];
		if ( ! satForAxes( axes, _v0$3, _v1$6, _v2$3, _extents ) ) {

			return false;

		}

		// test 3 face normals from the aabb
		axes = [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ];
		if ( ! satForAxes( axes, _v0$3, _v1$6, _v2$3, _extents ) ) {

			return false;

		}

		// finally testing the face normal of the triangle
		// use already existing triangle edge vectors here
		_triangleNormal.crossVectors( _f0, _f1 );
		axes = [ _triangleNormal.x, _triangleNormal.y, _triangleNormal.z ];

		return satForAxes( axes, _v0$3, _v1$6, _v2$3, _extents );

	}

	/**
	 * Clamps the given point within the bounds of this box.
	 *
	 * @param {Vector3} point - The point to clamp.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The clamped point.
	 */
	clampPoint( point, target ) {

		return target.copy( point ).clamp( this.min, this.max );

	}

	/**
	 * Returns the euclidean distance from any edge of this box to the specified point. If
	 * the given point lies inside of this box, the distance will be `0`.
	 *
	 * @param {Vector3} point - The point to compute the distance to.
	 * @return {number} The euclidean distance.
	 */
	distanceToPoint( point ) {

		return this.clampPoint( point, _vector$7 ).distanceTo( point );

	}

	/**
	 * Returns a bounding sphere that encloses this bounding box.
	 *
	 * @param {Sphere} target - The target sphere that is used to store the method's result.
	 * @return {Sphere} The bounding sphere that encloses this bounding box.
	 */
	getBoundingSphere( target ) {

		if ( this.isEmpty() ) {

			target.makeEmpty();

		} else {

			this.getCenter( target.center );

			target.radius = this.getSize( _vector$7 ).length() * 0.5;

		}

		return target;

	}

	/**
	 * Computes the intersection of this bounding box and the given one, setting the upper
	 * bound of this box to the lesser of the two boxes' upper bounds and the
	 * lower bound of this box to the greater of the two boxes' lower bounds. If
	 * there's no overlap, makes this box empty.
	 *
	 * @param {Box3} box - The bounding box to intersect with.
	 * @return {Box3} A reference to this bounding box.
	 */
	intersect( box ) {

		this.min.max( box.min );
		this.max.min( box.max );

		// ensure that if there is no overlap, the result is fully empty, not slightly empty with non-inf/+inf values that will cause subsequence intersects to erroneously return valid values.
		if ( this.isEmpty() ) this.makeEmpty();

		return this;

	}

	/**
	 * Computes the union of this box and another and the given one, setting the upper
	 * bound of this box to the greater of the two boxes' upper bounds and the
	 * lower bound of this box to the lesser of the two boxes' lower bounds.
	 *
	 * @param {Box3} box - The bounding box that will be unioned with this instance.
	 * @return {Box3} A reference to this bounding box.
	 */
	union( box ) {

		this.min.min( box.min );
		this.max.max( box.max );

		return this;

	}

	/**
	 * Transforms this bounding box by the given 4x4 transformation matrix.
	 *
	 * @param {Matrix4} matrix - The transformation matrix.
	 * @return {Box3} A reference to this bounding box.
	 */
	applyMatrix4( matrix ) {

		// transform of empty box is an empty box.
		if ( this.isEmpty() ) return this;

		// NOTE: I am using a binary pattern to specify all 2^3 combinations below
		_points[ 0 ].set( this.min.x, this.min.y, this.min.z ).applyMatrix4( matrix ); // 000
		_points[ 1 ].set( this.min.x, this.min.y, this.max.z ).applyMatrix4( matrix ); // 001
		_points[ 2 ].set( this.min.x, this.max.y, this.min.z ).applyMatrix4( matrix ); // 010
		_points[ 3 ].set( this.min.x, this.max.y, this.max.z ).applyMatrix4( matrix ); // 011
		_points[ 4 ].set( this.max.x, this.min.y, this.min.z ).applyMatrix4( matrix ); // 100
		_points[ 5 ].set( this.max.x, this.min.y, this.max.z ).applyMatrix4( matrix ); // 101
		_points[ 6 ].set( this.max.x, this.max.y, this.min.z ).applyMatrix4( matrix ); // 110
		_points[ 7 ].set( this.max.x, this.max.y, this.max.z ).applyMatrix4( matrix ); // 111

		this.setFromPoints( _points );

		return this;

	}

	/**
	 * Adds the given offset to both the upper and lower bounds of this bounding box,
	 * effectively moving it in 3D space.
	 *
	 * @param {Vector3} offset - The offset that should be used to translate the bounding box.
	 * @return {Box3} A reference to this bounding box.
	 */
	translate( offset ) {

		this.min.add( offset );
		this.max.add( offset );

		return this;

	}

	/**
	 * Returns `true` if this bounding box is equal with the given one.
	 *
	 * @param {Box3} box - The box to test for equality.
	 * @return {boolean} Whether this bounding box is equal with the given one.
	 */
	equals( box ) {

		return box.min.equals( this.min ) && box.max.equals( this.max );

	}

	/**
	 * Returns a serialized structure of the bounding box.
	 *
	 * @return {Object} Serialized structure with fields representing the object state.
	 */
	toJSON() {

		return {
			min: this.min.toArray(),
			max: this.max.toArray()
		};

	}

	/**
	 * Returns a serialized structure of the bounding box.
	 *
	 * @param {Object} json - The serialized json to set the box from.
	 * @return {Box3} A reference to this bounding box.
	 */
	fromJSON( json ) {

		this.min.fromArray( json.min );
		this.max.fromArray( json.max );
		return this;

	}

}

const _points = [
	/*@__PURE__*/ new Vector3(),
	/*@__PURE__*/ new Vector3(),
	/*@__PURE__*/ new Vector3(),
	/*@__PURE__*/ new Vector3(),
	/*@__PURE__*/ new Vector3(),
	/*@__PURE__*/ new Vector3(),
	/*@__PURE__*/ new Vector3(),
	/*@__PURE__*/ new Vector3()
];

const _vector$7 = /*@__PURE__*/ new Vector3();

const _box$3 = /*@__PURE__*/ new Box3();

// triangle centered vertices

const _v0$3 = /*@__PURE__*/ new Vector3();
const _v1$6 = /*@__PURE__*/ new Vector3();
const _v2$3 = /*@__PURE__*/ new Vector3();

// triangle edge vectors

const _f0 = /*@__PURE__*/ new Vector3();
const _f1 = /*@__PURE__*/ new Vector3();
const _f2 = /*@__PURE__*/ new Vector3();

const _center = /*@__PURE__*/ new Vector3();
const _extents = /*@__PURE__*/ new Vector3();
const _triangleNormal = /*@__PURE__*/ new Vector3();
const _testAxis = /*@__PURE__*/ new Vector3();

function satForAxes( axes, v0, v1, v2, extents ) {

	for ( let i = 0, j = axes.length - 3; i <= j; i += 3 ) {

		_testAxis.fromArray( axes, i );
		// project the aabb onto the separating axis
		const r = extents.x * Math.abs( _testAxis.x ) + extents.y * Math.abs( _testAxis.y ) + extents.z * Math.abs( _testAxis.z );
		// project all 3 vertices of the triangle onto the separating axis
		const p0 = v0.dot( _testAxis );
		const p1 = v1.dot( _testAxis );
		const p2 = v2.dot( _testAxis );
		// actual test, basically see if either of the most extreme of the triangle points intersects r
		if ( Math.max( - Math.max( p0, p1, p2 ), Math.min( p0, p1, p2 ) ) > r ) {

			// points of the projected triangle are outside the projected half-length of the aabb
			// the axis is separating and we can exit
			return false;

		}

	}

	return true;

}

const _box$2 = /*@__PURE__*/ new Box3();
const _v1$5 = /*@__PURE__*/ new Vector3();
const _v2$2 = /*@__PURE__*/ new Vector3();

/**
 * An analytical 3D sphere defined by a center and radius. This class is mainly
 * used as a Bounding Sphere for 3D objects.
 */
class Sphere {

	/**
	 * Constructs a new sphere.
	 *
	 * @param {Vector3} [center=(0,0,0)] - The center of the sphere
	 * @param {number} [radius=-1] - The radius of the sphere.
	 */
	constructor( center = new Vector3(), radius = -1 ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isSphere = true;

		/**
		 * The center of the sphere
		 *
		 * @type {Vector3}
		 */
		this.center = center;

		/**
		 * The radius of the sphere.
		 *
		 * @type {number}
		 */
		this.radius = radius;

	}

	/**
	 * Sets the sphere's components by copying the given values.
	 *
	 * @param {Vector3} center - The center.
	 * @param {number} radius - The radius.
	 * @return {Sphere} A reference to this sphere.
	 */
	set( center, radius ) {

		this.center.copy( center );
		this.radius = radius;

		return this;

	}

	/**
	 * Computes the minimum bounding sphere for list of points.
	 * If the optional center point is given, it is used as the sphere's
	 * center. Otherwise, the center of the axis-aligned bounding box
	 * encompassing the points is calculated.
	 *
	 * @param {Array<Vector3>} points - A list of points in 3D space.
	 * @param {Vector3} [optionalCenter] - The center of the sphere.
	 * @return {Sphere} A reference to this sphere.
	 */
	setFromPoints( points, optionalCenter ) {

		const center = this.center;

		if ( optionalCenter !== undefined ) {

			center.copy( optionalCenter );

		} else {

			_box$2.setFromPoints( points ).getCenter( center );

		}

		let maxRadiusSq = 0;

		for ( let i = 0, il = points.length; i < il; i ++ ) {

			maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( points[ i ] ) );

		}

		this.radius = Math.sqrt( maxRadiusSq );

		return this;

	}

	/**
	 * Copies the values of the given sphere to this instance.
	 *
	 * @param {Sphere} sphere - The sphere to copy.
	 * @return {Sphere} A reference to this sphere.
	 */
	copy( sphere ) {

		this.center.copy( sphere.center );
		this.radius = sphere.radius;

		return this;

	}

	/**
	 * Returns `true` if the sphere is empty (the radius set to a negative number).
	 *
	 * Spheres with a radius of `0` contain only their center point and are not
	 * considered to be empty.
	 *
	 * @return {boolean} Whether this sphere is empty or not.
	 */
	isEmpty() {

		return ( this.radius < 0 );

	}

	/**
	 * Makes this sphere empty which means in encloses a zero space in 3D.
	 *
	 * @return {Sphere} A reference to this sphere.
	 */
	makeEmpty() {

		this.center.set( 0, 0, 0 );
		this.radius = -1;

		return this;

	}

	/**
	 * Returns `true` if this sphere contains the given point inclusive of
	 * the surface of the sphere.
	 *
	 * @param {Vector3} point - The point to check.
	 * @return {boolean} Whether this sphere contains the given point or not.
	 */
	containsPoint( point ) {

		return ( point.distanceToSquared( this.center ) <= ( this.radius * this.radius ) );

	}

	/**
	 * Returns the closest distance from the boundary of the sphere to the
	 * given point. If the sphere contains the point, the distance will
	 * be negative.
	 *
	 * @param {Vector3} point - The point to compute the distance to.
	 * @return {number} The distance to the point.
	 */
	distanceToPoint( point ) {

		return ( point.distanceTo( this.center ) - this.radius );

	}

	/**
	 * Returns `true` if this sphere intersects with the given one.
	 *
	 * @param {Sphere} sphere - The sphere to test.
	 * @return {boolean} Whether this sphere intersects with the given one or not.
	 */
	intersectsSphere( sphere ) {

		const radiusSum = this.radius + sphere.radius;

		return sphere.center.distanceToSquared( this.center ) <= ( radiusSum * radiusSum );

	}

	/**
	 * Returns `true` if this sphere intersects with the given box.
	 *
	 * @param {Box3} box - The box to test.
	 * @return {boolean} Whether this sphere intersects with the given box or not.
	 */
	intersectsBox( box ) {

		return box.intersectsSphere( this );

	}

	/**
	 * Returns `true` if this sphere intersects with the given plane.
	 *
	 * @param {Plane} plane - The plane to test.
	 * @return {boolean} Whether this sphere intersects with the given plane or not.
	 */
	intersectsPlane( plane ) {

		return Math.abs( plane.distanceToPoint( this.center ) ) <= this.radius;

	}

	/**
	 * Clamps a point within the sphere. If the point is outside the sphere, it
	 * will clamp it to the closest point on the edge of the sphere. Points
	 * already inside the sphere will not be affected.
	 *
	 * @param {Vector3} point - The plane to clamp.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The clamped point.
	 */
	clampPoint( point, target ) {

		const deltaLengthSq = this.center.distanceToSquared( point );

		target.copy( point );

		if ( deltaLengthSq > ( this.radius * this.radius ) ) {

			target.sub( this.center ).normalize();
			target.multiplyScalar( this.radius ).add( this.center );

		}

		return target;

	}

	/**
	 * Returns a bounding box that encloses this sphere.
	 *
	 * @param {Box3} target - The target box that is used to store the method's result.
	 * @return {Box3} The bounding box that encloses this sphere.
	 */
	getBoundingBox( target ) {

		if ( this.isEmpty() ) {

			// Empty sphere produces empty bounding box
			target.makeEmpty();
			return target;

		}

		target.set( this.center, this.center );
		target.expandByScalar( this.radius );

		return target;

	}

	/**
	 * Transforms this sphere with the given 4x4 transformation matrix.
	 *
	 * @param {Matrix4} matrix - The transformation matrix.
	 * @return {Sphere} A reference to this sphere.
	 */
	applyMatrix4( matrix ) {

		this.center.applyMatrix4( matrix );
		this.radius = this.radius * matrix.getMaxScaleOnAxis();

		return this;

	}

	/**
	 * Translates the sphere's center by the given offset.
	 *
	 * @param {Vector3} offset - The offset.
	 * @return {Sphere} A reference to this sphere.
	 */
	translate( offset ) {

		this.center.add( offset );

		return this;

	}

	/**
	 * Expands the boundaries of this sphere to include the given point.
	 *
	 * @param {Vector3} point - The point to include.
	 * @return {Sphere} A reference to this sphere.
	 */
	expandByPoint( point ) {

		if ( this.isEmpty() ) {

			this.center.copy( point );

			this.radius = 0;

			return this;

		}

		_v1$5.subVectors( point, this.center );

		const lengthSq = _v1$5.lengthSq();

		if ( lengthSq > ( this.radius * this.radius ) ) {

			// calculate the minimal sphere

			const length = Math.sqrt( lengthSq );

			const delta = ( length - this.radius ) * 0.5;

			this.center.addScaledVector( _v1$5, delta / length );

			this.radius += delta;

		}

		return this;

	}

	/**
	 * Expands this sphere to enclose both the original sphere and the given sphere.
	 *
	 * @param {Sphere} sphere - The sphere to include.
	 * @return {Sphere} A reference to this sphere.
	 */
	union( sphere ) {

		if ( sphere.isEmpty() ) {

			return this;

		}

		if ( this.isEmpty() ) {

			this.copy( sphere );

			return this;

		}

		if ( this.center.equals( sphere.center ) === true ) {

			 this.radius = Math.max( this.radius, sphere.radius );

		} else {

			_v2$2.subVectors( sphere.center, this.center ).setLength( sphere.radius );

			this.expandByPoint( _v1$5.copy( sphere.center ).add( _v2$2 ) );

			this.expandByPoint( _v1$5.copy( sphere.center ).sub( _v2$2 ) );

		}

		return this;

	}

	/**
	 * Returns `true` if this sphere is equal with the given one.
	 *
	 * @param {Sphere} sphere - The sphere to test for equality.
	 * @return {boolean} Whether this bounding sphere is equal with the given one.
	 */
	equals( sphere ) {

		return sphere.center.equals( this.center ) && ( sphere.radius === this.radius );

	}

	/**
	 * Returns a new sphere with copied values from this instance.
	 *
	 * @return {Sphere} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

	/**
	 * Returns a serialized structure of the bounding sphere.
	 *
	 * @return {Object} Serialized structure with fields representing the object state.
	 */
	toJSON() {

		return {
			radius: this.radius,
			center: this.center.toArray()
		};

	}

	/**
	 * Returns a serialized structure of the bounding sphere.
	 *
	 * @param {Object} json - The serialized json to set the sphere from.
	 * @return {Box3} A reference to this bounding sphere.
	 */
	fromJSON( json ) {

		this.radius = json.radius;
		this.center.fromArray( json.center );
		return this;

	}

}

const _vector1 = /*@__PURE__*/ new Vector3();
const _vector2$1 = /*@__PURE__*/ new Vector3();
const _normalMatrix = /*@__PURE__*/ new Matrix3();

/**
 * A two dimensional surface that extends infinitely in 3D space, represented
 * in [Hessian normal form]{@link http://mathworld.wolfram.com/HessianNormalForm.html}
 * by a unit length normal vector and a constant.
 */
class Plane {

	/**
	 * Constructs a new plane.
	 *
	 * @param {Vector3} [normal=(1,0,0)] - A unit length vector defining the normal of the plane.
	 * @param {number} [constant=0] - The signed distance from the origin to the plane.
	 */
	constructor( normal = new Vector3( 1, 0, 0 ), constant = 0 ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isPlane = true;

		/**
		 * A unit length vector defining the normal of the plane.
		 *
		 * @type {Vector3}
		 */
		this.normal = normal;

		/**
		 * The signed distance from the origin to the plane.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.constant = constant;

	}

	/**
	 * Sets the plane components by copying the given values.
	 *
	 * @param {Vector3} normal - The normal.
	 * @param {number} constant - The constant.
	 * @return {Plane} A reference to this plane.
	 */
	set( normal, constant ) {

		this.normal.copy( normal );
		this.constant = constant;

		return this;

	}

	/**
	 * Sets the plane components by defining `x`, `y`, `z` as the
	 * plane normal and `w` as the constant.
	 *
	 * @param {number} x - The value for the normal's x component.
	 * @param {number} y - The value for the normal's y component.
	 * @param {number} z - The value for the normal's z component.
	 * @param {number} w - The constant value.
	 * @return {Plane} A reference to this plane.
	 */
	setComponents( x, y, z, w ) {

		this.normal.set( x, y, z );
		this.constant = w;

		return this;

	}

	/**
	 * Sets the plane from the given normal and coplanar point (that is a point
	 * that lies onto the plane).
	 *
	 * @param {Vector3} normal - The normal.
	 * @param {Vector3} point - A coplanar point.
	 * @return {Plane} A reference to this plane.
	 */
	setFromNormalAndCoplanarPoint( normal, point ) {

		this.normal.copy( normal );
		this.constant = - point.dot( this.normal );

		return this;

	}

	/**
	 * Sets the plane from three coplanar points. The winding order is
	 * assumed to be counter-clockwise, and determines the direction of
	 * the plane normal.
	 *
	 * @param {Vector3} a - The first coplanar point.
	 * @param {Vector3} b - The second coplanar point.
	 * @param {Vector3} c - The third coplanar point.
	 * @return {Plane} A reference to this plane.
	 */
	setFromCoplanarPoints( a, b, c ) {

		const normal = _vector1.subVectors( c, b ).cross( _vector2$1.subVectors( a, b ) ).normalize();

		// Q: should an error be thrown if normal is zero (e.g. degenerate plane)?

		this.setFromNormalAndCoplanarPoint( normal, a );

		return this;

	}

	/**
	 * Copies the values of the given plane to this instance.
	 *
	 * @param {Plane} plane - The plane to copy.
	 * @return {Plane} A reference to this plane.
	 */
	copy( plane ) {

		this.normal.copy( plane.normal );
		this.constant = plane.constant;

		return this;

	}

	/**
	 * Normalizes the plane normal and adjusts the constant accordingly.
	 *
	 * @return {Plane} A reference to this plane.
	 */
	normalize() {

		// Note: will lead to a divide by zero if the plane is invalid.

		const inverseNormalLength = 1.0 / this.normal.length();
		this.normal.multiplyScalar( inverseNormalLength );
		this.constant *= inverseNormalLength;

		return this;

	}

	/**
	 * Negates both the plane normal and the constant.
	 *
	 * @return {Plane} A reference to this plane.
	 */
	negate() {

		this.constant *= -1;
		this.normal.negate();

		return this;

	}

	/**
	 * Returns the signed distance from the given point to this plane.
	 *
	 * @param {Vector3} point - The point to compute the distance for.
	 * @return {number} The signed distance.
	 */
	distanceToPoint( point ) {

		return this.normal.dot( point ) + this.constant;

	}

	/**
	 * Returns the signed distance from the given sphere to this plane.
	 *
	 * @param {Sphere} sphere - The sphere to compute the distance for.
	 * @return {number} The signed distance.
	 */
	distanceToSphere( sphere ) {

		return this.distanceToPoint( sphere.center ) - sphere.radius;

	}

	/**
	 * Projects a the given point onto the plane.
	 *
	 * @param {Vector3} point - The point to project.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The projected point on the plane.
	 */
	projectPoint( point, target ) {

		return target.copy( point ).addScaledVector( this.normal, - this.distanceToPoint( point ) );

	}

	/**
	 * Returns the intersection point of the passed line and the plane. Returns
	 * `null` if the line does not intersect. Returns the line's starting point if
	 * the line is coplanar with the plane.
	 *
	 * @param {Line3} line - The line to compute the intersection for.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {?Vector3} The intersection point.
	 */
	intersectLine( line, target ) {

		const direction = line.delta( _vector1 );

		const denominator = this.normal.dot( direction );

		if ( denominator === 0 ) {

			// line is coplanar, return origin
			if ( this.distanceToPoint( line.start ) === 0 ) {

				return target.copy( line.start );

			}

			// Unsure if this is the correct method to handle this case.
			return null;

		}

		const t = - ( line.start.dot( this.normal ) + this.constant ) / denominator;

		if ( t < 0 || t > 1 ) {

			return null;

		}

		return target.copy( line.start ).addScaledVector( direction, t );

	}

	/**
	 * Returns `true` if the given line segment intersects with (passes through) the plane.
	 *
	 * @param {Line3} line - The line to test.
	 * @return {boolean} Whether the given line segment intersects with the plane or not.
	 */
	intersectsLine( line ) {

		// Note: this tests if a line intersects the plane, not whether it (or its end-points) are coplanar with it.

		const startSign = this.distanceToPoint( line.start );
		const endSign = this.distanceToPoint( line.end );

		return ( startSign < 0 && endSign > 0 ) || ( endSign < 0 && startSign > 0 );

	}

	/**
	 * Returns `true` if the given bounding box intersects with the plane.
	 *
	 * @param {Box3} box - The bounding box to test.
	 * @return {boolean} Whether the given bounding box intersects with the plane or not.
	 */
	intersectsBox( box ) {

		return box.intersectsPlane( this );

	}

	/**
	 * Returns `true` if the given bounding sphere intersects with the plane.
	 *
	 * @param {Sphere} sphere - The bounding sphere to test.
	 * @return {boolean} Whether the given bounding sphere intersects with the plane or not.
	 */
	intersectsSphere( sphere ) {

		return sphere.intersectsPlane( this );

	}

	/**
	 * Returns a coplanar vector to the plane, by calculating the
	 * projection of the normal at the origin onto the plane.
	 *
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The coplanar point.
	 */
	coplanarPoint( target ) {

		return target.copy( this.normal ).multiplyScalar( - this.constant );

	}

	/**
	 * Apply a 4x4 matrix to the plane. The matrix must be an affine, homogeneous transform.
	 *
	 * The optional normal matrix can be pre-computed like so:
	 * ```js
	 * const optionalNormalMatrix = new THREE.Matrix3().getNormalMatrix( matrix );
	 * ```
	 *
	 * @param {Matrix4} matrix - The transformation matrix.
	 * @param {Matrix4} [optionalNormalMatrix] - A pre-computed normal matrix.
	 * @return {Plane} A reference to this plane.
	 */
	applyMatrix4( matrix, optionalNormalMatrix ) {

		const normalMatrix = optionalNormalMatrix || _normalMatrix.getNormalMatrix( matrix );

		const referencePoint = this.coplanarPoint( _vector1 ).applyMatrix4( matrix );

		const normal = this.normal.applyMatrix3( normalMatrix ).normalize();

		this.constant = - referencePoint.dot( normal );

		return this;

	}

	/**
	 * Translates the plane by the distance defined by the given offset vector.
	 * Note that this only affects the plane constant and will not affect the normal vector.
	 *
	 * @param {Vector3} offset - The offset vector.
	 * @return {Plane} A reference to this plane.
	 */
	translate( offset ) {

		this.constant -= offset.dot( this.normal );

		return this;

	}

	/**
	 * Returns `true` if this plane is equal with the given one.
	 *
	 * @param {Plane} plane - The plane to test for equality.
	 * @return {boolean} Whether this plane is equal with the given one.
	 */
	equals( plane ) {

		return plane.normal.equals( this.normal ) && ( plane.constant === this.constant );

	}

	/**
	 * Returns a new plane with copied values from this instance.
	 *
	 * @return {Plane} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

}

const _sphere$6 = /*@__PURE__*/ new Sphere();
const _defaultSpriteCenter = /*@__PURE__*/ new Vector2( 0.5, 0.5 );
const _vector$6 = /*@__PURE__*/ new Vector3();

/**
 * Frustums are used to determine what is inside the camera's field of view.
 * They help speed up the rendering process - objects which lie outside a camera's
 * frustum can safely be excluded from rendering.
 *
 * This class is mainly intended for use internally by a renderer.
 */
class Frustum {

	/**
	 * Constructs a new frustum.
	 *
	 * @param {Plane} [p0] - The first plane that encloses the frustum.
	 * @param {Plane} [p1] - The second plane that encloses the frustum.
	 * @param {Plane} [p2] - The third plane that encloses the frustum.
	 * @param {Plane} [p3] - The fourth plane that encloses the frustum.
	 * @param {Plane} [p4] - The fifth plane that encloses the frustum.
	 * @param {Plane} [p5] - The sixth plane that encloses the frustum.
	 */
	constructor( p0 = new Plane(), p1 = new Plane(), p2 = new Plane(), p3 = new Plane(), p4 = new Plane(), p5 = new Plane() ) {

		/**
		 * This array holds the planes that enclose the frustum.
		 *
		 * @type {Array<Plane>}
		 */
		this.planes = [ p0, p1, p2, p3, p4, p5 ];

	}

	/**
	 * Sets the frustum planes by copying the given planes.
	 *
	 * @param {Plane} [p0] - The first plane that encloses the frustum.
	 * @param {Plane} [p1] - The second plane that encloses the frustum.
	 * @param {Plane} [p2] - The third plane that encloses the frustum.
	 * @param {Plane} [p3] - The fourth plane that encloses the frustum.
	 * @param {Plane} [p4] - The fifth plane that encloses the frustum.
	 * @param {Plane} [p5] - The sixth plane that encloses the frustum.
	 * @return {Frustum} A reference to this frustum.
	 */
	set( p0, p1, p2, p3, p4, p5 ) {

		const planes = this.planes;

		planes[ 0 ].copy( p0 );
		planes[ 1 ].copy( p1 );
		planes[ 2 ].copy( p2 );
		planes[ 3 ].copy( p3 );
		planes[ 4 ].copy( p4 );
		planes[ 5 ].copy( p5 );

		return this;

	}

	/**
	 * Copies the values of the given frustum to this instance.
	 *
	 * @param {Frustum} frustum - The frustum to copy.
	 * @return {Frustum} A reference to this frustum.
	 */
	copy( frustum ) {

		const planes = this.planes;

		for ( let i = 0; i < 6; i ++ ) {

			planes[ i ].copy( frustum.planes[ i ] );

		}

		return this;

	}

	/**
	 * Sets the frustum planes from the given projection matrix.
	 *
	 * @param {Matrix4} m - The projection matrix.
	 * @param {(WebGLCoordinateSystem|WebGPUCoordinateSystem)} coordinateSystem - The coordinate system.
	 * @param {boolean} [reversedDepth=false] - Whether to use a reversed depth.
	 * @return {Frustum} A reference to this frustum.
	 */
	setFromProjectionMatrix( m, coordinateSystem = WebGLCoordinateSystem, reversedDepth = false ) {

		const planes = this.planes;
		const me = m.elements;
		const me0 = me[ 0 ], me1 = me[ 1 ], me2 = me[ 2 ], me3 = me[ 3 ];
		const me4 = me[ 4 ], me5 = me[ 5 ], me6 = me[ 6 ], me7 = me[ 7 ];
		const me8 = me[ 8 ], me9 = me[ 9 ], me10 = me[ 10 ], me11 = me[ 11 ];
		const me12 = me[ 12 ], me13 = me[ 13 ], me14 = me[ 14 ], me15 = me[ 15 ];

		planes[ 0 ].setComponents( me3 - me0, me7 - me4, me11 - me8, me15 - me12 ).normalize();
		planes[ 1 ].setComponents( me3 + me0, me7 + me4, me11 + me8, me15 + me12 ).normalize();
		planes[ 2 ].setComponents( me3 + me1, me7 + me5, me11 + me9, me15 + me13 ).normalize();
		planes[ 3 ].setComponents( me3 - me1, me7 - me5, me11 - me9, me15 - me13 ).normalize();

		if ( reversedDepth ) {

			planes[ 4 ].setComponents( me2, me6, me10, me14 ).normalize(); // far
			planes[ 5 ].setComponents( me3 - me2, me7 - me6, me11 - me10, me15 - me14 ).normalize(); // near

		} else {

			planes[ 4 ].setComponents( me3 - me2, me7 - me6, me11 - me10, me15 - me14 ).normalize(); // far

			if ( coordinateSystem === WebGLCoordinateSystem ) {

				planes[ 5 ].setComponents( me3 + me2, me7 + me6, me11 + me10, me15 + me14 ).normalize(); // near

			} else if ( coordinateSystem === WebGPUCoordinateSystem ) {

				planes[ 5 ].setComponents( me2, me6, me10, me14 ).normalize(); // near

			} else {

				throw new Error( 'THREE.Frustum.setFromProjectionMatrix(): Invalid coordinate system: ' + coordinateSystem );

			}

		}

		return this;

	}

	/**
	 * Returns `true` if the 3D object's bounding sphere is intersecting this frustum.
	 *
	 * Note that the 3D object must have a geometry so that the bounding sphere can be calculated.
	 *
	 * @param {Object3D} object - The 3D object to test.
	 * @return {boolean} Whether the 3D object's bounding sphere is intersecting this frustum or not.
	 */
	intersectsObject( object ) {

		if ( object.boundingSphere !== undefined ) {

			if ( object.boundingSphere === null ) object.computeBoundingSphere();

			_sphere$6.copy( object.boundingSphere ).applyMatrix4( object.matrixWorld );

		} else {

			const geometry = object.geometry;

			if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();

			_sphere$6.copy( geometry.boundingSphere ).applyMatrix4( object.matrixWorld );

		}

		return this.intersectsSphere( _sphere$6 );

	}

	/**
	 * Returns `true` if the given sprite is intersecting this frustum.
	 *
	 * @param {Sprite} sprite - The sprite to test.
	 * @return {boolean} Whether the sprite is intersecting this frustum or not.
	 */
	intersectsSprite( sprite ) {

		_sphere$6.center.set( 0, 0, 0 );

		const offset = _defaultSpriteCenter.distanceTo( sprite.center );

		_sphere$6.radius = 0.7071067811865476 + offset;
		_sphere$6.applyMatrix4( sprite.matrixWorld );

		return this.intersectsSphere( _sphere$6 );

	}

	/**
	 * Returns `true` if the given bounding sphere is intersecting this frustum.
	 *
	 * @param {Sphere} sphere - The bounding sphere to test.
	 * @return {boolean} Whether the bounding sphere is intersecting this frustum or not.
	 */
	intersectsSphere( sphere ) {

		const planes = this.planes;
		const center = sphere.center;
		const negRadius = - sphere.radius;

		for ( let i = 0; i < 6; i ++ ) {

			const distance = planes[ i ].distanceToPoint( center );

			if ( distance < negRadius ) {

				return false;

			}

		}

		return true;

	}

	/**
	 * Returns `true` if the given bounding box is intersecting this frustum.
	 *
	 * @param {Box3} box - The bounding box to test.
	 * @return {boolean} Whether the bounding box is intersecting this frustum or not.
	 */
	intersectsBox( box ) {

		const planes = this.planes;

		for ( let i = 0; i < 6; i ++ ) {

			const plane = planes[ i ];

			// corner at max distance

			_vector$6.x = plane.normal.x > 0 ? box.max.x : box.min.x;
			_vector$6.y = plane.normal.y > 0 ? box.max.y : box.min.y;
			_vector$6.z = plane.normal.z > 0 ? box.max.z : box.min.z;

			if ( plane.distanceToPoint( _vector$6 ) < 0 ) {

				return false;

			}

		}

		return true;

	}

	/**
	 * Returns `true` if the given point lies within the frustum.
	 *
	 * @param {Vector3} point - The point to test.
	 * @return {boolean} Whether the point lies within this frustum or not.
	 */
	containsPoint( point ) {

		const planes = this.planes;

		for ( let i = 0; i < 6; i ++ ) {

			if ( planes[ i ].distanceToPoint( point ) < 0 ) {

				return false;

			}

		}

		return true;

	}

	/**
	 * Returns a new frustum with copied values from this instance.
	 *
	 * @return {Frustum} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

}

/**
 * Represents a 4x4 matrix.
 *
 * The most common use of a 4x4 matrix in 3D computer graphics is as a transformation matrix.
 * For an introduction to transformation matrices as used in WebGL, check out [this tutorial]{@link https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices}
 *
 * This allows a 3D vector representing a point in 3D space to undergo
 * transformations such as translation, rotation, shear, scale, reflection,
 * orthogonal or perspective projection and so on, by being multiplied by the
 * matrix. This is known as `applying` the matrix to the vector.
 *
 * A Note on Row-Major and Column-Major Ordering:
 *
 * The constructor and {@link Matrix3#set} method take arguments in
 * [row-major]{@link https://en.wikipedia.org/wiki/Row-_and_column-major_order#Column-major_order}
 * order, while internally they are stored in the {@link Matrix3#elements} array in column-major order.
 * This means that calling:
 * ```js
 * const m = new THREE.Matrix4();
 * m.set( 11, 12, 13, 14,
 *        21, 22, 23, 24,
 *        31, 32, 33, 34,
 *        41, 42, 43, 44 );
 * ```
 * will result in the elements array containing:
 * ```js
 * m.elements = [ 11, 21, 31, 41,
 *                12, 22, 32, 42,
 *                13, 23, 33, 43,
 *                14, 24, 34, 44 ];
 * ```
 * and internally all calculations are performed using column-major ordering.
 * However, as the actual ordering makes no difference mathematically and
 * most people are used to thinking about matrices in row-major order, the
 * three.js documentation shows matrices in row-major order. Just bear in
 * mind that if you are reading the source code, you'll have to take the
 * transpose of any matrices outlined here to make sense of the calculations.
 */
class Matrix4 {

	/**
	 * Constructs a new 4x4 matrix. The arguments are supposed to be
	 * in row-major order. If no arguments are provided, the constructor
	 * initializes the matrix as an identity matrix.
	 *
	 * @param {number} [n11] - 1-1 matrix element.
	 * @param {number} [n12] - 1-2 matrix element.
	 * @param {number} [n13] - 1-3 matrix element.
	 * @param {number} [n14] - 1-4 matrix element.
	 * @param {number} [n21] - 2-1 matrix element.
	 * @param {number} [n22] - 2-2 matrix element.
	 * @param {number} [n23] - 2-3 matrix element.
	 * @param {number} [n24] - 2-4 matrix element.
	 * @param {number} [n31] - 3-1 matrix element.
	 * @param {number} [n32] - 3-2 matrix element.
	 * @param {number} [n33] - 3-3 matrix element.
	 * @param {number} [n34] - 3-4 matrix element.
	 * @param {number} [n41] - 4-1 matrix element.
	 * @param {number} [n42] - 4-2 matrix element.
	 * @param {number} [n43] - 4-3 matrix element.
	 * @param {number} [n44] - 4-4 matrix element.
	 */
	constructor( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		Matrix4.prototype.isMatrix4 = true;

		/**
		 * A column-major list of matrix values.
		 *
		 * @type {Array<number>}
		 */
		this.elements = [

			1, 0, 0, 0,
			0, 1, 0, 0,
			0, 0, 1, 0,
			0, 0, 0, 1

		];

		if ( n11 !== undefined ) {

			this.set( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 );

		}

	}

	/**
	 * Sets the elements of the matrix.The arguments are supposed to be
	 * in row-major order.
	 *
	 * @param {number} [n11] - 1-1 matrix element.
	 * @param {number} [n12] - 1-2 matrix element.
	 * @param {number} [n13] - 1-3 matrix element.
	 * @param {number} [n14] - 1-4 matrix element.
	 * @param {number} [n21] - 2-1 matrix element.
	 * @param {number} [n22] - 2-2 matrix element.
	 * @param {number} [n23] - 2-3 matrix element.
	 * @param {number} [n24] - 2-4 matrix element.
	 * @param {number} [n31] - 3-1 matrix element.
	 * @param {number} [n32] - 3-2 matrix element.
	 * @param {number} [n33] - 3-3 matrix element.
	 * @param {number} [n34] - 3-4 matrix element.
	 * @param {number} [n41] - 4-1 matrix element.
	 * @param {number} [n42] - 4-2 matrix element.
	 * @param {number} [n43] - 4-3 matrix element.
	 * @param {number} [n44] - 4-4 matrix element.
	 * @return {Matrix4} A reference to this matrix.
	 */
	set( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 ) {

		const te = this.elements;

		te[ 0 ] = n11; te[ 4 ] = n12; te[ 8 ] = n13; te[ 12 ] = n14;
		te[ 1 ] = n21; te[ 5 ] = n22; te[ 9 ] = n23; te[ 13 ] = n24;
		te[ 2 ] = n31; te[ 6 ] = n32; te[ 10 ] = n33; te[ 14 ] = n34;
		te[ 3 ] = n41; te[ 7 ] = n42; te[ 11 ] = n43; te[ 15 ] = n44;

		return this;

	}

	/**
	 * Sets this matrix to the 4x4 identity matrix.
	 *
	 * @return {Matrix4} A reference to this matrix.
	 */
	identity() {

		this.set(

			1, 0, 0, 0,
			0, 1, 0, 0,
			0, 0, 1, 0,
			0, 0, 0, 1

		);

		return this;

	}

	/**
	 * Returns a matrix with copied values from this instance.
	 *
	 * @return {Matrix4} A clone of this instance.
	 */
	clone() {

		return new Matrix4().fromArray( this.elements );

	}

	/**
	 * Copies the values of the given matrix to this instance.
	 *
	 * @param {Matrix4} m - The matrix to copy.
	 * @return {Matrix4} A reference to this matrix.
	 */
	copy( m ) {

		const te = this.elements;
		const me = m.elements;

		te[ 0 ] = me[ 0 ]; te[ 1 ] = me[ 1 ]; te[ 2 ] = me[ 2 ]; te[ 3 ] = me[ 3 ];
		te[ 4 ] = me[ 4 ]; te[ 5 ] = me[ 5 ]; te[ 6 ] = me[ 6 ]; te[ 7 ] = me[ 7 ];
		te[ 8 ] = me[ 8 ]; te[ 9 ] = me[ 9 ]; te[ 10 ] = me[ 10 ]; te[ 11 ] = me[ 11 ];
		te[ 12 ] = me[ 12 ]; te[ 13 ] = me[ 13 ]; te[ 14 ] = me[ 14 ]; te[ 15 ] = me[ 15 ];

		return this;

	}

	/**
	 * Copies the translation component of the given matrix
	 * into this matrix's translation component.
	 *
	 * @param {Matrix4} m - The matrix to copy the translation component.
	 * @return {Matrix4} A reference to this matrix.
	 */
	copyPosition( m ) {

		const te = this.elements, me = m.elements;

		te[ 12 ] = me[ 12 ];
		te[ 13 ] = me[ 13 ];
		te[ 14 ] = me[ 14 ];

		return this;

	}

	/**
	 * Set the upper 3x3 elements of this matrix to the values of given 3x3 matrix.
	 *
	 * @param {Matrix3} m - The 3x3 matrix.
	 * @return {Matrix4} A reference to this matrix.
	 */
	setFromMatrix3( m ) {

		const me = m.elements;

		this.set(

			me[ 0 ], me[ 3 ], me[ 6 ], 0,
			me[ 1 ], me[ 4 ], me[ 7 ], 0,
			me[ 2 ], me[ 5 ], me[ 8 ], 0,
			0, 0, 0, 1

		);

		return this;

	}

	/**
	 * Extracts the basis of this matrix into the three axis vectors provided.
	 *
	 * @param {Vector3} xAxis - The basis's x axis.
	 * @param {Vector3} yAxis - The basis's y axis.
	 * @param {Vector3} zAxis - The basis's z axis.
	 * @return {Matrix4} A reference to this matrix.
	 */
	extractBasis( xAxis, yAxis, zAxis ) {

		xAxis.setFromMatrixColumn( this, 0 );
		yAxis.setFromMatrixColumn( this, 1 );
		zAxis.setFromMatrixColumn( this, 2 );

		return this;

	}

	/**
	 * Sets the given basis vectors to this matrix.
	 *
	 * @param {Vector3} xAxis - The basis's x axis.
	 * @param {Vector3} yAxis - The basis's y axis.
	 * @param {Vector3} zAxis - The basis's z axis.
	 * @return {Matrix4} A reference to this matrix.
	 */
	makeBasis( xAxis, yAxis, zAxis ) {

		this.set(
			xAxis.x, yAxis.x, zAxis.x, 0,
			xAxis.y, yAxis.y, zAxis.y, 0,
			xAxis.z, yAxis.z, zAxis.z, 0,
			0, 0, 0, 1
		);

		return this;

	}

	/**
	 * Extracts the rotation component of the given matrix
	 * into this matrix's rotation component.
	 *
	 * Note: This method does not support reflection matrices.
	 *
	 * @param {Matrix4} m - The matrix.
	 * @return {Matrix4} A reference to this matrix.
	 */
	extractRotation( m ) {

		const te = this.elements;
		const me = m.elements;

		const scaleX = 1 / _v1$4.setFromMatrixColumn( m, 0 ).length();
		const scaleY = 1 / _v1$4.setFromMatrixColumn( m, 1 ).length();
		const scaleZ = 1 / _v1$4.setFromMatrixColumn( m, 2 ).length();

		te[ 0 ] = me[ 0 ] * scaleX;
		te[ 1 ] = me[ 1 ] * scaleX;
		te[ 2 ] = me[ 2 ] * scaleX;
		te[ 3 ] = 0;

		te[ 4 ] = me[ 4 ] * scaleY;
		te[ 5 ] = me[ 5 ] * scaleY;
		te[ 6 ] = me[ 6 ] * scaleY;
		te[ 7 ] = 0;

		te[ 8 ] = me[ 8 ] * scaleZ;
		te[ 9 ] = me[ 9 ] * scaleZ;
		te[ 10 ] = me[ 10 ] * scaleZ;
		te[ 11 ] = 0;

		te[ 12 ] = 0;
		te[ 13 ] = 0;
		te[ 14 ] = 0;
		te[ 15 ] = 1;

		return this;

	}

	/**
	 * Sets the rotation component (the upper left 3x3 matrix) of this matrix to
	 * the rotation specified by the given Euler angles. The rest of
	 * the matrix is set to the identity. Depending on the {@link Euler#order},
	 * there are six possible outcomes. See [this page]{@link https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix}
	 * for a complete list.
	 *
	 * @param {Euler} euler - The Euler angles.
	 * @return {Matrix4} A reference to this matrix.
	 */
	makeRotationFromEuler( euler ) {

		const te = this.elements;

		const x = euler.x, y = euler.y, z = euler.z;
		const a = Math.cos( x ), b = Math.sin( x );
		const c = Math.cos( y ), d = Math.sin( y );
		const e = Math.cos( z ), f = Math.sin( z );

		if ( euler.order === 'XYZ' ) {

			const ae = a * e, af = a * f, be = b * e, bf = b * f;

			te[ 0 ] = c * e;
			te[ 4 ] = - c * f;
			te[ 8 ] = d;

			te[ 1 ] = af + be * d;
			te[ 5 ] = ae - bf * d;
			te[ 9 ] = - b * c;

			te[ 2 ] = bf - ae * d;
			te[ 6 ] = be + af * d;
			te[ 10 ] = a * c;

		} else if ( euler.order === 'YXZ' ) {

			const ce = c * e, cf = c * f, de = d * e, df = d * f;

			te[ 0 ] = ce + df * b;
			te[ 4 ] = de * b - cf;
			te[ 8 ] = a * d;

			te[ 1 ] = a * f;
			te[ 5 ] = a * e;
			te[ 9 ] = - b;

			te[ 2 ] = cf * b - de;
			te[ 6 ] = df + ce * b;
			te[ 10 ] = a * c;

		} else if ( euler.order === 'ZXY' ) {

			const ce = c * e, cf = c * f, de = d * e, df = d * f;

			te[ 0 ] = ce - df * b;
			te[ 4 ] = - a * f;
			te[ 8 ] = de + cf * b;

			te[ 1 ] = cf + de * b;
			te[ 5 ] = a * e;
			te[ 9 ] = df - ce * b;

			te[ 2 ] = - a * d;
			te[ 6 ] = b;
			te[ 10 ] = a * c;

		} else if ( euler.order === 'ZYX' ) {

			const ae = a * e, af = a * f, be = b * e, bf = b * f;

			te[ 0 ] = c * e;
			te[ 4 ] = be * d - af;
			te[ 8 ] = ae * d + bf;

			te[ 1 ] = c * f;
			te[ 5 ] = bf * d + ae;
			te[ 9 ] = af * d - be;

			te[ 2 ] = - d;
			te[ 6 ] = b * c;
			te[ 10 ] = a * c;

		} else if ( euler.order === 'YZX' ) {

			const ac = a * c, ad = a * d, bc = b * c, bd = b * d;

			te[ 0 ] = c * e;
			te[ 4 ] = bd - ac * f;
			te[ 8 ] = bc * f + ad;

			te[ 1 ] = f;
			te[ 5 ] = a * e;
			te[ 9 ] = - b * e;

			te[ 2 ] = - d * e;
			te[ 6 ] = ad * f + bc;
			te[ 10 ] = ac - bd * f;

		} else if ( euler.order === 'XZY' ) {

			const ac = a * c, ad = a * d, bc = b * c, bd = b * d;

			te[ 0 ] = c * e;
			te[ 4 ] = - f;
			te[ 8 ] = d * e;

			te[ 1 ] = ac * f + bd;
			te[ 5 ] = a * e;
			te[ 9 ] = ad * f - bc;

			te[ 2 ] = bc * f - ad;
			te[ 6 ] = b * e;
			te[ 10 ] = bd * f + ac;

		}

		// bottom row
		te[ 3 ] = 0;
		te[ 7 ] = 0;
		te[ 11 ] = 0;

		// last column
		te[ 12 ] = 0;
		te[ 13 ] = 0;
		te[ 14 ] = 0;
		te[ 15 ] = 1;

		return this;

	}

	/**
	 * Sets the rotation component of this matrix to the rotation specified by
	 * the given Quaternion as outlined [here]{@link https://en.wikipedia.org/wiki/Rotation_matrix#Quaternion}
	 * The rest of the matrix is set to the identity.
	 *
	 * @param {Quaternion} q - The Quaternion.
	 * @return {Matrix4} A reference to this matrix.
	 */
	makeRotationFromQuaternion( q ) {

		return this.compose( _zero, q, _one );

	}

	/**
	 * Sets the rotation component of the transformation matrix, looking from `eye` towards
	 * `target`, and oriented by the up-direction.
	 *
	 * @param {Vector3} eye - The eye vector.
	 * @param {Vector3} target - The target vector.
	 * @param {Vector3} up - The up vector.
	 * @return {Matrix4} A reference to this matrix.
	 */
	lookAt( eye, target, up ) {

		const te = this.elements;

		_z.subVectors( eye, target );

		if ( _z.lengthSq() === 0 ) {

			// eye and target are in the same position

			_z.z = 1;

		}

		_z.normalize();
		_x.crossVectors( up, _z );

		if ( _x.lengthSq() === 0 ) {

			// up and z are parallel

			if ( Math.abs( up.z ) === 1 ) {

				_z.x += 0.0001;

			} else {

				_z.z += 0.0001;

			}

			_z.normalize();
			_x.crossVectors( up, _z );

		}

		_x.normalize();
		_y.crossVectors( _z, _x );

		te[ 0 ] = _x.x; te[ 4 ] = _y.x; te[ 8 ] = _z.x;
		te[ 1 ] = _x.y; te[ 5 ] = _y.y; te[ 9 ] = _z.y;
		te[ 2 ] = _x.z; te[ 6 ] = _y.z; te[ 10 ] = _z.z;

		return this;

	}

	/**
	 * Post-multiplies this matrix by the given 4x4 matrix.
	 *
	 * @param {Matrix4} m - The matrix to multiply with.
	 * @return {Matrix4} A reference to this matrix.
	 */
	multiply( m ) {

		return this.multiplyMatrices( this, m );

	}

	/**
	 * Pre-multiplies this matrix by the given 4x4 matrix.
	 *
	 * @param {Matrix4} m - The matrix to multiply with.
	 * @return {Matrix4} A reference to this matrix.
	 */
	premultiply( m ) {

		return this.multiplyMatrices( m, this );

	}

	/**
	 * Multiples the given 4x4 matrices and stores the result
	 * in this matrix.
	 *
	 * @param {Matrix4} a - The first matrix.
	 * @param {Matrix4} b - The second matrix.
	 * @return {Matrix4} A reference to this matrix.
	 */
	multiplyMatrices( a, b ) {

		const ae = a.elements;
		const be = b.elements;
		const te = this.elements;

		const a11 = ae[ 0 ], a12 = ae[ 4 ], a13 = ae[ 8 ], a14 = ae[ 12 ];
		const a21 = ae[ 1 ], a22 = ae[ 5 ], a23 = ae[ 9 ], a24 = ae[ 13 ];
		const a31 = ae[ 2 ], a32 = ae[ 6 ], a33 = ae[ 10 ], a34 = ae[ 14 ];
		const a41 = ae[ 3 ], a42 = ae[ 7 ], a43 = ae[ 11 ], a44 = ae[ 15 ];

		const b11 = be[ 0 ], b12 = be[ 4 ], b13 = be[ 8 ], b14 = be[ 12 ];
		const b21 = be[ 1 ], b22 = be[ 5 ], b23 = be[ 9 ], b24 = be[ 13 ];
		const b31 = be[ 2 ], b32 = be[ 6 ], b33 = be[ 10 ], b34 = be[ 14 ];
		const b41 = be[ 3 ], b42 = be[ 7 ], b43 = be[ 11 ], b44 = be[ 15 ];

		te[ 0 ] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41;
		te[ 4 ] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42;
		te[ 8 ] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43;
		te[ 12 ] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44;

		te[ 1 ] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41;
		te[ 5 ] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42;
		te[ 9 ] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43;
		te[ 13 ] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44;

		te[ 2 ] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41;
		te[ 6 ] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42;
		te[ 10 ] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43;
		te[ 14 ] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44;

		te[ 3 ] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41;
		te[ 7 ] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42;
		te[ 11 ] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43;
		te[ 15 ] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44;

		return this;

	}

	/**
	 * Multiplies every component of the matrix by the given scalar.
	 *
	 * @param {number} s - The scalar.
	 * @return {Matrix4} A reference to this matrix.
	 */
	multiplyScalar( s ) {

		const te = this.elements;

		te[ 0 ] *= s; te[ 4 ] *= s; te[ 8 ] *= s; te[ 12 ] *= s;
		te[ 1 ] *= s; te[ 5 ] *= s; te[ 9 ] *= s; te[ 13 ] *= s;
		te[ 2 ] *= s; te[ 6 ] *= s; te[ 10 ] *= s; te[ 14 ] *= s;
		te[ 3 ] *= s; te[ 7 ] *= s; te[ 11 ] *= s; te[ 15 ] *= s;

		return this;

	}

	/**
	 * Computes and returns the determinant of this matrix.
	 *
	 * Based on the method outlined [here]{@link http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.html}.
	 *
	 * @return {number} The determinant.
	 */
	determinant() {

		const te = this.elements;

		const n11 = te[ 0 ], n12 = te[ 4 ], n13 = te[ 8 ], n14 = te[ 12 ];
		const n21 = te[ 1 ], n22 = te[ 5 ], n23 = te[ 9 ], n24 = te[ 13 ];
		const n31 = te[ 2 ], n32 = te[ 6 ], n33 = te[ 10 ], n34 = te[ 14 ];
		const n41 = te[ 3 ], n42 = te[ 7 ], n43 = te[ 11 ], n44 = te[ 15 ];

		//TODO: make this more efficient

		return (
			n41 * (
				+ n14 * n23 * n32
				 - n13 * n24 * n32
				 - n14 * n22 * n33
				 + n12 * n24 * n33
				 + n13 * n22 * n34
				 - n12 * n23 * n34
			) +
			n42 * (
				+ n11 * n23 * n34
				 - n11 * n24 * n33
				 + n14 * n21 * n33
				 - n13 * n21 * n34
				 + n13 * n24 * n31
				 - n14 * n23 * n31
			) +
			n43 * (
				+ n11 * n24 * n32
				 - n11 * n22 * n34
				 - n14 * n21 * n32
				 + n12 * n21 * n34
				 + n14 * n22 * n31
				 - n12 * n24 * n31
			) +
			n44 * (
				- n13 * n22 * n31
				 - n11 * n23 * n32
				 + n11 * n22 * n33
				 + n13 * n21 * n32
				 - n12 * n21 * n33
				 + n12 * n23 * n31
			)

		);

	}

	/**
	 * Transposes this matrix in place.
	 *
	 * @return {Matrix4} A reference to this matrix.
	 */
	transpose() {

		const te = this.elements;
		let tmp;

		tmp = te[ 1 ]; te[ 1 ] = te[ 4 ]; te[ 4 ] = tmp;
		tmp = te[ 2 ]; te[ 2 ] = te[ 8 ]; te[ 8 ] = tmp;
		tmp = te[ 6 ]; te[ 6 ] = te[ 9 ]; te[ 9 ] = tmp;

		tmp = te[ 3 ]; te[ 3 ] = te[ 12 ]; te[ 12 ] = tmp;
		tmp = te[ 7 ]; te[ 7 ] = te[ 13 ]; te[ 13 ] = tmp;
		tmp = te[ 11 ]; te[ 11 ] = te[ 14 ]; te[ 14 ] = tmp;

		return this;

	}

	/**
	 * Sets the position component for this matrix from the given vector,
	 * without affecting the rest of the matrix.
	 *
	 * @param {number|Vector3} x - The x component of the vector or alternatively the vector object.
	 * @param {number} y - The y component of the vector.
	 * @param {number} z - The z component of the vector.
	 * @return {Matrix4} A reference to this matrix.
	 */
	setPosition( x, y, z ) {

		const te = this.elements;

		if ( x.isVector3 ) {

			te[ 12 ] = x.x;
			te[ 13 ] = x.y;
			te[ 14 ] = x.z;

		} else {

			te[ 12 ] = x;
			te[ 13 ] = y;
			te[ 14 ] = z;

		}

		return this;

	}

	/**
	 * Inverts this matrix, using the [analytic method]{@link https://en.wikipedia.org/wiki/Invertible_matrix#Analytic_solution}.
	 * You can not invert with a determinant of zero. If you attempt this, the method produces
	 * a zero matrix instead.
	 *
	 * @return {Matrix4} A reference to this matrix.
	 */
	invert() {

		// based on http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm
		const te = this.elements,

			n11 = te[ 0 ], n21 = te[ 1 ], n31 = te[ 2 ], n41 = te[ 3 ],
			n12 = te[ 4 ], n22 = te[ 5 ], n32 = te[ 6 ], n42 = te[ 7 ],
			n13 = te[ 8 ], n23 = te[ 9 ], n33 = te[ 10 ], n43 = te[ 11 ],
			n14 = te[ 12 ], n24 = te[ 13 ], n34 = te[ 14 ], n44 = te[ 15 ],

			t11 = n23 * n34 * n42 - n24 * n33 * n42 + n24 * n32 * n43 - n22 * n34 * n43 - n23 * n32 * n44 + n22 * n33 * n44,
			t12 = n14 * n33 * n42 - n13 * n34 * n42 - n14 * n32 * n43 + n12 * n34 * n43 + n13 * n32 * n44 - n12 * n33 * n44,
			t13 = n13 * n24 * n42 - n14 * n23 * n42 + n14 * n22 * n43 - n12 * n24 * n43 - n13 * n22 * n44 + n12 * n23 * n44,
			t14 = n14 * n23 * n32 - n13 * n24 * n32 - n14 * n22 * n33 + n12 * n24 * n33 + n13 * n22 * n34 - n12 * n23 * n34;

		const det = n11 * t11 + n21 * t12 + n31 * t13 + n41 * t14;

		if ( det === 0 ) return this.set( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 );

		const detInv = 1 / det;

		te[ 0 ] = t11 * detInv;
		te[ 1 ] = ( n24 * n33 * n41 - n23 * n34 * n41 - n24 * n31 * n43 + n21 * n34 * n43 + n23 * n31 * n44 - n21 * n33 * n44 ) * detInv;
		te[ 2 ] = ( n22 * n34 * n41 - n24 * n32 * n41 + n24 * n31 * n42 - n21 * n34 * n42 - n22 * n31 * n44 + n21 * n32 * n44 ) * detInv;
		te[ 3 ] = ( n23 * n32 * n41 - n22 * n33 * n41 - n23 * n31 * n42 + n21 * n33 * n42 + n22 * n31 * n43 - n21 * n32 * n43 ) * detInv;

		te[ 4 ] = t12 * detInv;
		te[ 5 ] = ( n13 * n34 * n41 - n14 * n33 * n41 + n14 * n31 * n43 - n11 * n34 * n43 - n13 * n31 * n44 + n11 * n33 * n44 ) * detInv;
		te[ 6 ] = ( n14 * n32 * n41 - n12 * n34 * n41 - n14 * n31 * n42 + n11 * n34 * n42 + n12 * n31 * n44 - n11 * n32 * n44 ) * detInv;
		te[ 7 ] = ( n12 * n33 * n41 - n13 * n32 * n41 + n13 * n31 * n42 - n11 * n33 * n42 - n12 * n31 * n43 + n11 * n32 * n43 ) * detInv;

		te[ 8 ] = t13 * detInv;
		te[ 9 ] = ( n14 * n23 * n41 - n13 * n24 * n41 - n14 * n21 * n43 + n11 * n24 * n43 + n13 * n21 * n44 - n11 * n23 * n44 ) * detInv;
		te[ 10 ] = ( n12 * n24 * n41 - n14 * n22 * n41 + n14 * n21 * n42 - n11 * n24 * n42 - n12 * n21 * n44 + n11 * n22 * n44 ) * detInv;
		te[ 11 ] = ( n13 * n22 * n41 - n12 * n23 * n41 - n13 * n21 * n42 + n11 * n23 * n42 + n12 * n21 * n43 - n11 * n22 * n43 ) * detInv;

		te[ 12 ] = t14 * detInv;
		te[ 13 ] = ( n13 * n24 * n31 - n14 * n23 * n31 + n14 * n21 * n33 - n11 * n24 * n33 - n13 * n21 * n34 + n11 * n23 * n34 ) * detInv;
		te[ 14 ] = ( n14 * n22 * n31 - n12 * n24 * n31 - n14 * n21 * n32 + n11 * n24 * n32 + n12 * n21 * n34 - n11 * n22 * n34 ) * detInv;
		te[ 15 ] = ( n12 * n23 * n31 - n13 * n22 * n31 + n13 * n21 * n32 - n11 * n23 * n32 - n12 * n21 * n33 + n11 * n22 * n33 ) * detInv;

		return this;

	}

	/**
	 * Multiplies the columns of this matrix by the given vector.
	 *
	 * @param {Vector3} v - The scale vector.
	 * @return {Matrix4} A reference to this matrix.
	 */
	scale( v ) {

		const te = this.elements;
		const x = v.x, y = v.y, z = v.z;

		te[ 0 ] *= x; te[ 4 ] *= y; te[ 8 ] *= z;
		te[ 1 ] *= x; te[ 5 ] *= y; te[ 9 ] *= z;
		te[ 2 ] *= x; te[ 6 ] *= y; te[ 10 ] *= z;
		te[ 3 ] *= x; te[ 7 ] *= y; te[ 11 ] *= z;

		return this;

	}

	/**
	 * Gets the maximum scale value of the three axes.
	 *
	 * @return {number} The maximum scale.
	 */
	getMaxScaleOnAxis() {

		const te = this.elements;

		const scaleXSq = te[ 0 ] * te[ 0 ] + te[ 1 ] * te[ 1 ] + te[ 2 ] * te[ 2 ];
		const scaleYSq = te[ 4 ] * te[ 4 ] + te[ 5 ] * te[ 5 ] + te[ 6 ] * te[ 6 ];
		const scaleZSq = te[ 8 ] * te[ 8 ] + te[ 9 ] * te[ 9 ] + te[ 10 ] * te[ 10 ];

		return Math.sqrt( Math.max( scaleXSq, scaleYSq, scaleZSq ) );

	}

	/**
	 * Sets this matrix as a translation transform from the given vector.
	 *
	 * @param {number|Vector3} x - The amount to translate in the X axis or alternatively a translation vector.
	 * @param {number} y - The amount to translate in the Y axis.
	 * @param {number} z - The amount to translate in the z axis.
	 * @return {Matrix4} A reference to this matrix.
	 */
	makeTranslation( x, y, z ) {

		if ( x.isVector3 ) {

			this.set(

				1, 0, 0, x.x,
				0, 1, 0, x.y,
				0, 0, 1, x.z,
				0, 0, 0, 1

			);

		} else {

			this.set(

				1, 0, 0, x,
				0, 1, 0, y,
				0, 0, 1, z,
				0, 0, 0, 1

			);

		}

		return this;

	}

	/**
	 * Sets this matrix as a rotational transformation around the X axis by
	 * the given angle.
	 *
	 * @param {number} theta - The rotation in radians.
	 * @return {Matrix4} A reference to this matrix.
	 */
	makeRotationX( theta ) {

		const c = Math.cos( theta ), s = Math.sin( theta );

		this.set(

			1, 0, 0, 0,
			0, c, - s, 0,
			0, s, c, 0,
			0, 0, 0, 1

		);

		return this;

	}

	/**
	 * Sets this matrix as a rotational transformation around the Y axis by
	 * the given angle.
	 *
	 * @param {number} theta - The rotation in radians.
	 * @return {Matrix4} A reference to this matrix.
	 */
	makeRotationY( theta ) {

		const c = Math.cos( theta ), s = Math.sin( theta );

		this.set(

			 c, 0, s, 0,
			 0, 1, 0, 0,
			- s, 0, c, 0,
			 0, 0, 0, 1

		);

		return this;

	}

	/**
	 * Sets this matrix as a rotational transformation around the Z axis by
	 * the given angle.
	 *
	 * @param {number} theta - The rotation in radians.
	 * @return {Matrix4} A reference to this matrix.
	 */
	makeRotationZ( theta ) {

		const c = Math.cos( theta ), s = Math.sin( theta );

		this.set(

			c, - s, 0, 0,
			s, c, 0, 0,
			0, 0, 1, 0,
			0, 0, 0, 1

		);

		return this;

	}

	/**
	 * Sets this matrix as a rotational transformation around the given axis by
	 * the given angle.
	 *
	 * This is a somewhat controversial but mathematically sound alternative to
	 * rotating via Quaternions. See the discussion [here]{@link https://www.gamedev.net/articles/programming/math-and-physics/do-we-really-need-quaternions-r1199}.
	 *
	 * @param {Vector3} axis - The normalized rotation axis.
	 * @param {number} angle - The rotation in radians.
	 * @return {Matrix4} A reference to this matrix.
	 */
	makeRotationAxis( axis, angle ) {

		// Based on http://www.gamedev.net/reference/articles/article1199.asp

		const c = Math.cos( angle );
		const s = Math.sin( angle );
		const t = 1 - c;
		const x = axis.x, y = axis.y, z = axis.z;
		const tx = t * x, ty = t * y;

		this.set(

			tx * x + c, tx * y - s * z, tx * z + s * y, 0,
			tx * y + s * z, ty * y + c, ty * z - s * x, 0,
			tx * z - s * y, ty * z + s * x, t * z * z + c, 0,
			0, 0, 0, 1

		);

		return this;

	}

	/**
	 * Sets this matrix as a scale transformation.
	 *
	 * @param {number} x - The amount to scale in the X axis.
	 * @param {number} y - The amount to scale in the Y axis.
	 * @param {number} z - The amount to scale in the Z axis.
	 * @return {Matrix4} A reference to this matrix.
	 */
	makeScale( x, y, z ) {

		this.set(

			x, 0, 0, 0,
			0, y, 0, 0,
			0, 0, z, 0,
			0, 0, 0, 1

		);

		return this;

	}

	/**
	 * Sets this matrix as a shear transformation.
	 *
	 * @param {number} xy - The amount to shear X by Y.
	 * @param {number} xz - The amount to shear X by Z.
	 * @param {number} yx - The amount to shear Y by X.
	 * @param {number} yz - The amount to shear Y by Z.
	 * @param {number} zx - The amount to shear Z by X.
	 * @param {number} zy - The amount to shear Z by Y.
	 * @return {Matrix4} A reference to this matrix.
	 */
	makeShear( xy, xz, yx, yz, zx, zy ) {

		this.set(

			1, yx, zx, 0,
			xy, 1, zy, 0,
			xz, yz, 1, 0,
			0, 0, 0, 1

		);

		return this;

	}

	/**
	 * Sets this matrix to the transformation composed of the given position,
	 * rotation (Quaternion) and scale.
	 *
	 * @param {Vector3} position - The position vector.
	 * @param {Quaternion} quaternion - The rotation as a Quaternion.
	 * @param {Vector3} scale - The scale vector.
	 * @return {Matrix4} A reference to this matrix.
	 */
	compose( position, quaternion, scale ) {

		const te = this.elements;

		const x = quaternion._x, y = quaternion._y, z = quaternion._z, w = quaternion._w;
		const x2 = x + x,	y2 = y + y, z2 = z + z;
		const xx = x * x2, xy = x * y2, xz = x * z2;
		const yy = y * y2, yz = y * z2, zz = z * z2;
		const wx = w * x2, wy = w * y2, wz = w * z2;

		const sx = scale.x, sy = scale.y, sz = scale.z;

		te[ 0 ] = ( 1 - ( yy + zz ) ) * sx;
		te[ 1 ] = ( xy + wz ) * sx;
		te[ 2 ] = ( xz - wy ) * sx;
		te[ 3 ] = 0;

		te[ 4 ] = ( xy - wz ) * sy;
		te[ 5 ] = ( 1 - ( xx + zz ) ) * sy;
		te[ 6 ] = ( yz + wx ) * sy;
		te[ 7 ] = 0;

		te[ 8 ] = ( xz + wy ) * sz;
		te[ 9 ] = ( yz - wx ) * sz;
		te[ 10 ] = ( 1 - ( xx + yy ) ) * sz;
		te[ 11 ] = 0;

		te[ 12 ] = position.x;
		te[ 13 ] = position.y;
		te[ 14 ] = position.z;
		te[ 15 ] = 1;

		return this;

	}

	/**
	 * Decomposes this matrix into its position, rotation and scale components
	 * and provides the result in the given objects.
	 *
	 * Note: Not all matrices are decomposable in this way. For example, if an
	 * object has a non-uniformly scaled parent, then the object's world matrix
	 * may not be decomposable, and this method may not be appropriate.
	 *
	 * @param {Vector3} position - The position vector.
	 * @param {Quaternion} quaternion - The rotation as a Quaternion.
	 * @param {Vector3} scale - The scale vector.
	 * @return {Matrix4} A reference to this matrix.
	 */
	decompose( position, quaternion, scale ) {

		const te = this.elements;

		let sx = _v1$4.set( te[ 0 ], te[ 1 ], te[ 2 ] ).length();
		const sy = _v1$4.set( te[ 4 ], te[ 5 ], te[ 6 ] ).length();
		const sz = _v1$4.set( te[ 8 ], te[ 9 ], te[ 10 ] ).length();

		// if determine is negative, we need to invert one scale
		const det = this.determinant();
		if ( det < 0 ) sx = - sx;

		position.x = te[ 12 ];
		position.y = te[ 13 ];
		position.z = te[ 14 ];

		// scale the rotation part
		_m1$4.copy( this );

		const invSX = 1 / sx;
		const invSY = 1 / sy;
		const invSZ = 1 / sz;

		_m1$4.elements[ 0 ] *= invSX;
		_m1$4.elements[ 1 ] *= invSX;
		_m1$4.elements[ 2 ] *= invSX;

		_m1$4.elements[ 4 ] *= invSY;
		_m1$4.elements[ 5 ] *= invSY;
		_m1$4.elements[ 6 ] *= invSY;

		_m1$4.elements[ 8 ] *= invSZ;
		_m1$4.elements[ 9 ] *= invSZ;
		_m1$4.elements[ 10 ] *= invSZ;

		quaternion.setFromRotationMatrix( _m1$4 );

		scale.x = sx;
		scale.y = sy;
		scale.z = sz;

		return this;

	}

	/**
	 * Creates a perspective projection matrix. This is used internally by
	 * {@link PerspectiveCamera#updateProjectionMatrix}.

	 * @param {number} left - Left boundary of the viewing frustum at the near plane.
	 * @param {number} right - Right boundary of the viewing frustum at the near plane.
	 * @param {number} top - Top boundary of the viewing frustum at the near plane.
	 * @param {number} bottom - Bottom boundary of the viewing frustum at the near plane.
	 * @param {number} near - The distance from the camera to the near plane.
	 * @param {number} far - The distance from the camera to the far plane.
	 * @param {(WebGLCoordinateSystem|WebGPUCoordinateSystem)} [coordinateSystem=WebGLCoordinateSystem] - The coordinate system.
	 * @param {boolean} [reversedDepth=false] - Whether to use a reversed depth.
	 * @return {Matrix4} A reference to this matrix.
	 */
	makePerspective( left, right, top, bottom, near, far, coordinateSystem = WebGLCoordinateSystem, reversedDepth = false ) {

		const te = this.elements;

		const x = 2 * near / ( right - left );
		const y = 2 * near / ( top - bottom );

		const a = ( right + left ) / ( right - left );
		const b = ( top + bottom ) / ( top - bottom );

		let c, d;

		if ( reversedDepth ) {

			c = near / ( far - near );
			d = ( far * near ) / ( far - near );

		} else {

			if ( coordinateSystem === WebGLCoordinateSystem ) {

				c = - ( far + near ) / ( far - near );
				d = ( -2 * far * near ) / ( far - near );

			} else if ( coordinateSystem === WebGPUCoordinateSystem ) {

				c = - far / ( far - near );
				d = ( - far * near ) / ( far - near );

			} else {

				throw new Error( 'THREE.Matrix4.makePerspective(): Invalid coordinate system: ' + coordinateSystem );

			}

		}

		te[ 0 ] = x;	te[ 4 ] = 0;	te[ 8 ] = a; 	te[ 12 ] = 0;
		te[ 1 ] = 0;	te[ 5 ] = y;	te[ 9 ] = b; 	te[ 13 ] = 0;
		te[ 2 ] = 0;	te[ 6 ] = 0;	te[ 10 ] = c; 	te[ 14 ] = d;
		te[ 3 ] = 0;	te[ 7 ] = 0;	te[ 11 ] = -1;	te[ 15 ] = 0;

		return this;

	}

	/**
	 * Creates a orthographic projection matrix. This is used internally by
	 * {@link OrthographicCamera#updateProjectionMatrix}.

	 * @param {number} left - Left boundary of the viewing frustum at the near plane.
	 * @param {number} right - Right boundary of the viewing frustum at the near plane.
	 * @param {number} top - Top boundary of the viewing frustum at the near plane.
	 * @param {number} bottom - Bottom boundary of the viewing frustum at the near plane.
	 * @param {number} near - The distance from the camera to the near plane.
	 * @param {number} far - The distance from the camera to the far plane.
	 * @param {(WebGLCoordinateSystem|WebGPUCoordinateSystem)} [coordinateSystem=WebGLCoordinateSystem] - The coordinate system.
	 * @param {boolean} [reversedDepth=false] - Whether to use a reversed depth.
	 * @return {Matrix4} A reference to this matrix.
	 */
	makeOrthographic( left, right, top, bottom, near, far, coordinateSystem = WebGLCoordinateSystem, reversedDepth = false ) {

		const te = this.elements;

		const x = 2 / ( right - left );
		const y = 2 / ( top - bottom );

		const a = - ( right + left ) / ( right - left );
		const b = - ( top + bottom ) / ( top - bottom );

		let c, d;

		if ( reversedDepth ) {

			c = 1 / ( far - near );
			d = far / ( far - near );

		} else {

			if ( coordinateSystem === WebGLCoordinateSystem ) {

				c = -2 / ( far - near );
				d = - ( far + near ) / ( far - near );

			} else if ( coordinateSystem === WebGPUCoordinateSystem ) {

				c = -1 / ( far - near );
				d = - near / ( far - near );

			} else {

				throw new Error( 'THREE.Matrix4.makeOrthographic(): Invalid coordinate system: ' + coordinateSystem );

			}

		}

		te[ 0 ] = x;		te[ 4 ] = 0;		te[ 8 ] = 0; 		te[ 12 ] = a;
		te[ 1 ] = 0; 		te[ 5 ] = y;		te[ 9 ] = 0; 		te[ 13 ] = b;
		te[ 2 ] = 0; 		te[ 6 ] = 0;		te[ 10 ] = c;		te[ 14 ] = d;
		te[ 3 ] = 0; 		te[ 7 ] = 0;		te[ 11 ] = 0;		te[ 15 ] = 1;

		return this;

	}

	/**
	 * Returns `true` if this matrix is equal with the given one.
	 *
	 * @param {Matrix4} matrix - The matrix to test for equality.
	 * @return {boolean} Whether this matrix is equal with the given one.
	 */
	equals( matrix ) {

		const te = this.elements;
		const me = matrix.elements;

		for ( let i = 0; i < 16; i ++ ) {

			if ( te[ i ] !== me[ i ] ) return false;

		}

		return true;

	}

	/**
	 * Sets the elements of the matrix from the given array.
	 *
	 * @param {Array<number>} array - The matrix elements in column-major order.
	 * @param {number} [offset=0] - Index of the first element in the array.
	 * @return {Matrix4} A reference to this matrix.
	 */
	fromArray( array, offset = 0 ) {

		for ( let i = 0; i < 16; i ++ ) {

			this.elements[ i ] = array[ i + offset ];

		}

		return this;

	}

	/**
	 * Writes the elements of this matrix to the given array. If no array is provided,
	 * the method returns a new instance.
	 *
	 * @param {Array<number>} [array=[]] - The target array holding the matrix elements in column-major order.
	 * @param {number} [offset=0] - Index of the first element in the array.
	 * @return {Array<number>} The matrix elements in column-major order.
	 */
	toArray( array = [], offset = 0 ) {

		const te = this.elements;

		array[ offset ] = te[ 0 ];
		array[ offset + 1 ] = te[ 1 ];
		array[ offset + 2 ] = te[ 2 ];
		array[ offset + 3 ] = te[ 3 ];

		array[ offset + 4 ] = te[ 4 ];
		array[ offset + 5 ] = te[ 5 ];
		array[ offset + 6 ] = te[ 6 ];
		array[ offset + 7 ] = te[ 7 ];

		array[ offset + 8 ] = te[ 8 ];
		array[ offset + 9 ] = te[ 9 ];
		array[ offset + 10 ] = te[ 10 ];
		array[ offset + 11 ] = te[ 11 ];

		array[ offset + 12 ] = te[ 12 ];
		array[ offset + 13 ] = te[ 13 ];
		array[ offset + 14 ] = te[ 14 ];
		array[ offset + 15 ] = te[ 15 ];

		return array;

	}

}

const _v1$4 = /*@__PURE__*/ new Vector3();
const _m1$4 = /*@__PURE__*/ new Matrix4();
const _zero = /*@__PURE__*/ new Vector3( 0, 0, 0 );
const _one = /*@__PURE__*/ new Vector3( 1, 1, 1 );
const _x = /*@__PURE__*/ new Vector3();
const _y = /*@__PURE__*/ new Vector3();
const _z = /*@__PURE__*/ new Vector3();

function WebGLAnimation() {

	let context = null;
	let isAnimating = false;
	let animationLoop = null;
	let requestId = null;

	function onAnimationFrame( time, frame ) {

		animationLoop( time, frame );

		requestId = context.requestAnimationFrame( onAnimationFrame );

	}

	return {

		start: function () {

			if ( isAnimating === true ) return;
			if ( animationLoop === null ) return;

			requestId = context.requestAnimationFrame( onAnimationFrame );

			isAnimating = true;

		},

		stop: function () {

			context.cancelAnimationFrame( requestId );

			isAnimating = false;

		},

		setAnimationLoop: function ( callback ) {

			animationLoop = callback;

		},

		setContext: function ( value ) {

			context = value;

		}

	};

}

function WebGLAttributes( gl ) {

	const buffers = new WeakMap();

	function createBuffer( attribute, bufferType ) {

		const array = attribute.array;
		const usage = attribute.usage;
		const size = array.byteLength;

		const buffer = gl.createBuffer();

		gl.bindBuffer( bufferType, buffer );
		gl.bufferData( bufferType, array, usage );

		attribute.onUploadCallback();

		let type;

		if ( array instanceof Float32Array ) {

			type = gl.FLOAT;

		} else if ( typeof Float16Array !== 'undefined' && array instanceof Float16Array ) {

			type = gl.HALF_FLOAT;

		} else if ( array instanceof Uint16Array ) {

			if ( attribute.isFloat16BufferAttribute ) {

				type = gl.HALF_FLOAT;

			} else {

				type = gl.UNSIGNED_SHORT;

			}

		} else if ( array instanceof Int16Array ) {

			type = gl.SHORT;

		} else if ( array instanceof Uint32Array ) {

			type = gl.UNSIGNED_INT;

		} else if ( array instanceof Int32Array ) {

			type = gl.INT;

		} else if ( array instanceof Int8Array ) {

			type = gl.BYTE;

		} else if ( array instanceof Uint8Array ) {

			type = gl.UNSIGNED_BYTE;

		} else if ( array instanceof Uint8ClampedArray ) {

			type = gl.UNSIGNED_BYTE;

		} else {

			throw new Error( 'THREE.WebGLAttributes: Unsupported buffer data format: ' + array );

		}

		return {
			buffer: buffer,
			type: type,
			bytesPerElement: array.BYTES_PER_ELEMENT,
			version: attribute.version,
			size: size
		};

	}

	function updateBuffer( buffer, attribute, bufferType ) {

		const array = attribute.array;
		const updateRanges = attribute.updateRanges;

		gl.bindBuffer( bufferType, buffer );

		if ( updateRanges.length === 0 ) {

			// Not using update ranges
			gl.bufferSubData( bufferType, 0, array );

		} else {

			// Before applying update ranges, we merge any adjacent / overlapping
			// ranges to reduce load on `gl.bufferSubData`. Empirically, this has led
			// to performance improvements for applications which make heavy use of
			// update ranges. Likely due to GPU command overhead.
			//
			// Note that to reduce garbage collection between frames, we merge the
			// update ranges in-place. This is safe because this method will clear the
			// update ranges once updated.

			updateRanges.sort( ( a, b ) => a.start - b.start );

			// To merge the update ranges in-place, we work from left to right in the
			// existing updateRanges array, merging ranges. This may result in a final
			// array which is smaller than the original. This index tracks the last
			// index representing a merged range, any data after this index can be
			// trimmed once the merge algorithm is completed.
			let mergeIndex = 0;

			for ( let i = 1; i < updateRanges.length; i ++ ) {

				const previousRange = updateRanges[ mergeIndex ];
				const range = updateRanges[ i ];

				// We add one here to merge adjacent ranges. This is safe because ranges
				// operate over positive integers.
				if ( range.start <= previousRange.start + previousRange.count + 1 ) {

					previousRange.count = Math.max(
						previousRange.count,
						range.start + range.count - previousRange.start
					);

				} else {

					++ mergeIndex;
					updateRanges[ mergeIndex ] = range;

				}

			}

			// Trim the array to only contain the merged ranges.
			updateRanges.length = mergeIndex + 1;

			for ( let i = 0, l = updateRanges.length; i < l; i ++ ) {

				const range = updateRanges[ i ];

				gl.bufferSubData( bufferType, range.start * array.BYTES_PER_ELEMENT,
					array, range.start, range.count );

			}

			attribute.clearUpdateRanges();

		}

		attribute.onUploadCallback();

	}

	//

	function get( attribute ) {

		if ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;

		return buffers.get( attribute );

	}

	function remove( attribute ) {

		if ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;

		const data = buffers.get( attribute );

		if ( data ) {

			gl.deleteBuffer( data.buffer );

			buffers.delete( attribute );

		}

	}

	function update( attribute, bufferType ) {

		if ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;

		if ( attribute.isGLBufferAttribute ) {

			const cached = buffers.get( attribute );

			if ( ! cached || cached.version < attribute.version ) {

				buffers.set( attribute, {
					buffer: attribute.buffer,
					type: attribute.type,
					bytesPerElement: attribute.elementSize,
					version: attribute.version
				} );

			}

			return;

		}

		const data = buffers.get( attribute );

		if ( data === undefined ) {

			buffers.set( attribute, createBuffer( attribute, bufferType ) );

		} else if ( data.version < attribute.version ) {

			if ( data.size !== attribute.array.byteLength ) {

				throw new Error( 'THREE.WebGLAttributes: The size of the buffer attribute\'s array buffer does not match the original size. Resizing buffer attributes is not supported.' );

			}

			updateBuffer( data.buffer, attribute, bufferType );

			data.version = attribute.version;

		}

	}

	return {

		get: get,
		remove: remove,
		update: update

	};

}

const _vector$5 = /*@__PURE__*/ new Vector3();
const _vector2 = /*@__PURE__*/ new Vector2();

let _id$2 = 0;

/**
 * This class stores data for an attribute (such as vertex positions, face
 * indices, normals, colors, UVs, and any custom attributes ) associated with
 * a geometry, which allows for more efficient passing of data to the GPU.
 *
 * When working with vector-like data, the `fromBufferAttribute( attribute, index )`
 * helper methods on vector and color class might be helpful. E.g. {@link Vector3#fromBufferAttribute}.
 */
class BufferAttribute {

	/**
	 * Constructs a new buffer attribute.
	 *
	 * @param {TypedArray} array - The array holding the attribute data.
	 * @param {number} itemSize - The item size.
	 * @param {boolean} [normalized=false] - Whether the data are normalized or not.
	 */
	constructor( array, itemSize, normalized = false ) {

		if ( Array.isArray( array ) ) {

			throw new TypeError( 'THREE.BufferAttribute: array should be a Typed Array.' );

		}

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isBufferAttribute = true;

		/**
		 * The ID of the buffer attribute.
		 *
		 * @name BufferAttribute#id
		 * @type {number}
		 * @readonly
		 */
		Object.defineProperty( this, 'id', { value: _id$2 ++ } );

		/**
		 * The name of the buffer attribute.
		 *
		 * @type {string}
		 */
		this.name = '';

		/**
		 * The array holding the attribute data. It should have `itemSize * numVertices`
		 * elements, where `numVertices` is the number of vertices in the associated geometry.
		 *
		 * @type {TypedArray}
		 */
		this.array = array;

		/**
		 * The number of values of the array that should be associated with a particular vertex.
		 * For instance, if this attribute is storing a 3-component vector (such as a position,
		 * normal, or color), then the value should be `3`.
		 *
		 * @type {number}
		 */
		this.itemSize = itemSize;

		/**
		 * Represents the number of items this buffer attribute stores. It is internally computed
		 * by dividing the `array` length by the `itemSize`.
		 *
		 * @type {number}
		 * @readonly
		 */
		this.count = array !== undefined ? array.length / itemSize : 0;

		/**
		 * Applies to integer data only. Indicates how the underlying data in the buffer maps to
		 * the values in the GLSL code. For instance, if `array` is an instance of `UInt16Array`,
		 * and `normalized` is `true`, the values `0 - +65535` in the array data will be mapped to
		 * `0.0f - +1.0f` in the GLSL attribute. If `normalized` is `false`, the values will be converted
		 * to floats unmodified, i.e. `65535` becomes `65535.0f`.
		 *
		 * @type {boolean}
		 */
		this.normalized = normalized;

		/**
		 * Defines the intended usage pattern of the data store for optimization purposes.
		 *
		 * Note: After the initial use of a buffer, its usage cannot be changed. Instead,
		 * instantiate a new one and set the desired usage before the next render.
		 *
		 * @type {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)}
		 * @default StaticDrawUsage
		 */
		this.usage = StaticDrawUsage;

		/**
		 * This can be used to only update some components of stored vectors (for example, just the
		 * component related to color). Use the `addUpdateRange()` function to add ranges to this array.
		 *
		 * @type {Array<Object>}
		 */
		this.updateRanges = [];

		/**
		 * Configures the bound GPU type for use in shaders.
		 *
		 * Note: this only has an effect for integer arrays and is not configurable for float arrays.
		 * For lower precision float types, use `Float16BufferAttribute`.
		 *
		 * @type {(FloatType|IntType)}
		 * @default FloatType
		 */
		this.gpuType = FloatType;

		/**
		 * A version number, incremented every time the `needsUpdate` is set to `true`.
		 *
		 * @type {number}
		 */
		this.version = 0;

	}

	/**
	 * A callback function that is executed after the renderer has transferred the attribute
	 * array data to the GPU.
	 */
	onUploadCallback() {}

	/**
	 * Flag to indicate that this attribute has changed and should be re-sent to
	 * the GPU. Set this to `true` when you modify the value of the array.
	 *
	 * @type {number}
	 * @default false
	 * @param {boolean} value
	 */
	set needsUpdate( value ) {

		if ( value === true ) this.version ++;

	}

	/**
	 * Sets the usage of this buffer attribute.
	 *
	 * @param {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)} value - The usage to set.
	 * @return {BufferAttribute} A reference to this buffer attribute.
	 */
	setUsage( value ) {

		this.usage = value;

		return this;

	}

	/**
	 * Adds a range of data in the data array to be updated on the GPU.
	 *
	 * @param {number} start - Position at which to start update.
	 * @param {number} count - The number of components to update.
	 */
	addUpdateRange( start, count ) {

		this.updateRanges.push( { start, count } );

	}

	/**
	 * Clears the update ranges.
	 */
	clearUpdateRanges() {

		this.updateRanges.length = 0;

	}

	/**
	 * Copies the values of the given buffer attribute to this instance.
	 *
	 * @param {BufferAttribute} source - The buffer attribute to copy.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	copy( source ) {

		this.name = source.name;
		this.array = new source.array.constructor( source.array );
		this.itemSize = source.itemSize;
		this.count = source.count;
		this.normalized = source.normalized;

		this.usage = source.usage;
		this.gpuType = source.gpuType;

		return this;

	}

	/**
	 * Copies a vector from the given buffer attribute to this one. The start
	 * and destination position in the attribute buffers are represented by the
	 * given indices.
	 *
	 * @param {number} index1 - The destination index into this buffer attribute.
	 * @param {BufferAttribute} attribute - The buffer attribute to copy from.
	 * @param {number} index2 - The source index into the given buffer attribute.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	copyAt( index1, attribute, index2 ) {

		index1 *= this.itemSize;
		index2 *= attribute.itemSize;

		for ( let i = 0, l = this.itemSize; i < l; i ++ ) {

			this.array[ index1 + i ] = attribute.array[ index2 + i ];

		}

		return this;

	}

	/**
	 * Copies the given array data into this buffer attribute.
	 *
	 * @param {(TypedArray|Array)} array - The array to copy.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	copyArray( array ) {

		this.array.set( array );

		return this;

	}

	/**
	 * Applies the given 3x3 matrix to the given attribute. Works with
	 * item size `2` and `3`.
	 *
	 * @param {Matrix3} m - The matrix to apply.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	applyMatrix3( m ) {

		if ( this.itemSize === 2 ) {

			for ( let i = 0, l = this.count; i < l; i ++ ) {

				_vector2.fromBufferAttribute( this, i );
				_vector2.applyMatrix3( m );

				this.setXY( i, _vector2.x, _vector2.y );

			}

		} else if ( this.itemSize === 3 ) {

			for ( let i = 0, l = this.count; i < l; i ++ ) {

				_vector$5.fromBufferAttribute( this, i );
				_vector$5.applyMatrix3( m );

				this.setXYZ( i, _vector$5.x, _vector$5.y, _vector$5.z );

			}

		}

		return this;

	}

	/**
	 * Applies the given 4x4 matrix to the given attribute. Only works with
	 * item size `3`.
	 *
	 * @param {Matrix4} m - The matrix to apply.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	applyMatrix4( m ) {

		for ( let i = 0, l = this.count; i < l; i ++ ) {

			_vector$5.fromBufferAttribute( this, i );

			_vector$5.applyMatrix4( m );

			this.setXYZ( i, _vector$5.x, _vector$5.y, _vector$5.z );

		}

		return this;

	}

	/**
	 * Applies the given 3x3 normal matrix to the given attribute. Only works with
	 * item size `3`.
	 *
	 * @param {Matrix3} m - The normal matrix to apply.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	applyNormalMatrix( m ) {

		for ( let i = 0, l = this.count; i < l; i ++ ) {

			_vector$5.fromBufferAttribute( this, i );

			_vector$5.applyNormalMatrix( m );

			this.setXYZ( i, _vector$5.x, _vector$5.y, _vector$5.z );

		}

		return this;

	}

	/**
	 * Applies the given 4x4 matrix to the given attribute. Only works with
	 * item size `3` and with direction vectors.
	 *
	 * @param {Matrix4} m - The matrix to apply.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	transformDirection( m ) {

		for ( let i = 0, l = this.count; i < l; i ++ ) {

			_vector$5.fromBufferAttribute( this, i );

			_vector$5.transformDirection( m );

			this.setXYZ( i, _vector$5.x, _vector$5.y, _vector$5.z );

		}

		return this;

	}

	/**
	 * Sets the given array data in the buffer attribute.
	 *
	 * @param {(TypedArray|Array)} value - The array data to set.
	 * @param {number} [offset=0] - The offset in this buffer attribute's array.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	set( value, offset = 0 ) {

		// Matching BufferAttribute constructor, do not normalize the array.
		this.array.set( value, offset );

		return this;

	}

	/**
	 * Returns the given component of the vector at the given index.
	 *
	 * @param {number} index - The index into the buffer attribute.
	 * @param {number} component - The component index.
	 * @return {number} The returned value.
	 */
	getComponent( index, component ) {

		let value = this.array[ index * this.itemSize + component ];

		if ( this.normalized ) value = denormalize( value, this.array );

		return value;

	}

	/**
	 * Sets the given value to the given component of the vector at the given index.
	 *
	 * @param {number} index - The index into the buffer attribute.
	 * @param {number} component - The component index.
	 * @param {number} value - The value to set.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	setComponent( index, component, value ) {

		if ( this.normalized ) value = normalize$1( value, this.array );

		this.array[ index * this.itemSize + component ] = value;

		return this;

	}

	/**
	 * Returns the x component of the vector at the given index.
	 *
	 * @param {number} index - The index into the buffer attribute.
	 * @return {number} The x component.
	 */
	getX( index ) {

		let x = this.array[ index * this.itemSize ];

		if ( this.normalized ) x = denormalize( x, this.array );

		return x;

	}

	/**
	 * Sets the x component of the vector at the given index.
	 *
	 * @param {number} index - The index into the buffer attribute.
	 * @param {number} x - The value to set.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	setX( index, x ) {

		if ( this.normalized ) x = normalize$1( x, this.array );

		this.array[ index * this.itemSize ] = x;

		return this;

	}

	/**
	 * Returns the y component of the vector at the given index.
	 *
	 * @param {number} index - The index into the buffer attribute.
	 * @return {number} The y component.
	 */
	getY( index ) {

		let y = this.array[ index * this.itemSize + 1 ];

		if ( this.normalized ) y = denormalize( y, this.array );

		return y;

	}

	/**
	 * Sets the y component of the vector at the given index.
	 *
	 * @param {number} index - The index into the buffer attribute.
	 * @param {number} y - The value to set.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	setY( index, y ) {

		if ( this.normalized ) y = normalize$1( y, this.array );

		this.array[ index * this.itemSize + 1 ] = y;

		return this;

	}

	/**
	 * Returns the z component of the vector at the given index.
	 *
	 * @param {number} index - The index into the buffer attribute.
	 * @return {number} The z component.
	 */
	getZ( index ) {

		let z = this.array[ index * this.itemSize + 2 ];

		if ( this.normalized ) z = denormalize( z, this.array );

		return z;

	}

	/**
	 * Sets the z component of the vector at the given index.
	 *
	 * @param {number} index - The index into the buffer attribute.
	 * @param {number} z - The value to set.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	setZ( index, z ) {

		if ( this.normalized ) z = normalize$1( z, this.array );

		this.array[ index * this.itemSize + 2 ] = z;

		return this;

	}

	/**
	 * Returns the w component of the vector at the given index.
	 *
	 * @param {number} index - The index into the buffer attribute.
	 * @return {number} The w component.
	 */
	getW( index ) {

		let w = this.array[ index * this.itemSize + 3 ];

		if ( this.normalized ) w = denormalize( w, this.array );

		return w;

	}

	/**
	 * Sets the w component of the vector at the given index.
	 *
	 * @param {number} index - The index into the buffer attribute.
	 * @param {number} w - The value to set.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	setW( index, w ) {

		if ( this.normalized ) w = normalize$1( w, this.array );

		this.array[ index * this.itemSize + 3 ] = w;

		return this;

	}

	/**
	 * Sets the x and y component of the vector at the given index.
	 *
	 * @param {number} index - The index into the buffer attribute.
	 * @param {number} x - The value for the x component to set.
	 * @param {number} y - The value for the y component to set.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	setXY( index, x, y ) {

		index *= this.itemSize;

		if ( this.normalized ) {

			x = normalize$1( x, this.array );
			y = normalize$1( y, this.array );

		}

		this.array[ index + 0 ] = x;
		this.array[ index + 1 ] = y;

		return this;

	}

	/**
	 * Sets the x, y and z component of the vector at the given index.
	 *
	 * @param {number} index - The index into the buffer attribute.
	 * @param {number} x - The value for the x component to set.
	 * @param {number} y - The value for the y component to set.
	 * @param {number} z - The value for the z component to set.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	setXYZ( index, x, y, z ) {

		index *= this.itemSize;

		if ( this.normalized ) {

			x = normalize$1( x, this.array );
			y = normalize$1( y, this.array );
			z = normalize$1( z, this.array );

		}

		this.array[ index + 0 ] = x;
		this.array[ index + 1 ] = y;
		this.array[ index + 2 ] = z;

		return this;

	}

	/**
	 * Sets the x, y, z and w component of the vector at the given index.
	 *
	 * @param {number} index - The index into the buffer attribute.
	 * @param {number} x - The value for the x component to set.
	 * @param {number} y - The value for the y component to set.
	 * @param {number} z - The value for the z component to set.
	 * @param {number} w - The value for the w component to set.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	setXYZW( index, x, y, z, w ) {

		index *= this.itemSize;

		if ( this.normalized ) {

			x = normalize$1( x, this.array );
			y = normalize$1( y, this.array );
			z = normalize$1( z, this.array );
			w = normalize$1( w, this.array );

		}

		this.array[ index + 0 ] = x;
		this.array[ index + 1 ] = y;
		this.array[ index + 2 ] = z;
		this.array[ index + 3 ] = w;

		return this;

	}

	/**
	 * Sets the given callback function that is executed after the Renderer has transferred
	 * the attribute array data to the GPU. Can be used to perform clean-up operations after
	 * the upload when attribute data are not needed anymore on the CPU side.
	 *
	 * @param {Function} callback - The `onUpload()` callback.
	 * @return {BufferAttribute} A reference to this instance.
	 */
	onUpload( callback ) {

		this.onUploadCallback = callback;

		return this;

	}

	/**
	 * Returns a new buffer attribute with copied values from this instance.
	 *
	 * @return {BufferAttribute} A clone of this instance.
	 */
	clone() {

		return new this.constructor( this.array, this.itemSize ).copy( this );

	}

	/**
	 * Serializes the buffer attribute into JSON.
	 *
	 * @return {Object} A JSON object representing the serialized buffer attribute.
	 */
	toJSON() {

		const data = {
			itemSize: this.itemSize,
			type: this.array.constructor.name,
			array: Array.from( this.array ),
			normalized: this.normalized
		};

		if ( this.name !== '' ) data.name = this.name;
		if ( this.usage !== StaticDrawUsage ) data.usage = this.usage;

		return data;

	}

}

/**
 * Convenient class that can be used when creating a `UInt16` buffer attribute with
 * a plain `Array` instance.
 *
 * @augments BufferAttribute
 */
class Uint16BufferAttribute extends BufferAttribute {

	/**
	 * Constructs a new buffer attribute.
	 *
	 * @param {(Array<number>|Uint16Array)} array - The array holding the attribute data.
	 * @param {number} itemSize - The item size.
	 * @param {boolean} [normalized=false] - Whether the data are normalized or not.
	 */
	constructor( array, itemSize, normalized ) {

		super( new Uint16Array( array ), itemSize, normalized );

	}

}

/**
 * Convenient class that can be used when creating a `UInt32` buffer attribute with
 * a plain `Array` instance.
 *
 * @augments BufferAttribute
 */
class Uint32BufferAttribute extends BufferAttribute {

	/**
	 * Constructs a new buffer attribute.
	 *
	 * @param {(Array<number>|Uint32Array)} array - The array holding the attribute data.
	 * @param {number} itemSize - The item size.
	 * @param {boolean} [normalized=false] - Whether the data are normalized or not.
	 */
	constructor( array, itemSize, normalized ) {

		super( new Uint32Array( array ), itemSize, normalized );

	}

}

/**
 * Convenient class that can be used when creating a `Float32` buffer attribute with
 * a plain `Array` instance.
 *
 * @augments BufferAttribute
 */
class Float32BufferAttribute extends BufferAttribute {

	/**
	 * Constructs a new buffer attribute.
	 *
	 * @param {(Array<number>|Float32Array)} array - The array holding the attribute data.
	 * @param {number} itemSize - The item size.
	 * @param {boolean} [normalized=false] - Whether the data are normalized or not.
	 */
	constructor( array, itemSize, normalized ) {

		super( new Float32Array( array ), itemSize, normalized );

	}

}

const _matrix$2 = /*@__PURE__*/ new Matrix4();
const _quaternion$1 = /*@__PURE__*/ new Quaternion();

/**
 * A class representing Euler angles.
 *
 * Euler angles describe a rotational transformation by rotating an object on
 * its various axes in specified amounts per axis, and a specified axis
 * order.
 *
 * Iterating through an instance will yield its components (x, y, z,
 * order) in the corresponding order.
 *
 * ```js
 * const a = new THREE.Euler( 0, 1, 1.57, 'XYZ' );
 * const b = new THREE.Vector3( 1, 0, 1 );
 * b.applyEuler(a);
 * ```
 */
class Euler {

	/**
	 * Constructs a new euler instance.
	 *
	 * @param {number} [x=0] - The angle of the x axis in radians.
	 * @param {number} [y=0] - The angle of the y axis in radians.
	 * @param {number} [z=0] - The angle of the z axis in radians.
	 * @param {string} [order=Euler.DEFAULT_ORDER] - A string representing the order that the rotations are applied.
	 */
	constructor( x = 0, y = 0, z = 0, order = Euler.DEFAULT_ORDER ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isEuler = true;

		this._x = x;
		this._y = y;
		this._z = z;
		this._order = order;

	}

	/**
	 * The angle of the x axis in radians.
	 *
	 * @type {number}
	 * @default 0
	 */
	get x() {

		return this._x;

	}

	set x( value ) {

		this._x = value;
		this._onChangeCallback();

	}

	/**
	 * The angle of the y axis in radians.
	 *
	 * @type {number}
	 * @default 0
	 */
	get y() {

		return this._y;

	}

	set y( value ) {

		this._y = value;
		this._onChangeCallback();

	}

	/**
	 * The angle of the z axis in radians.
	 *
	 * @type {number}
	 * @default 0
	 */
	get z() {

		return this._z;

	}

	set z( value ) {

		this._z = value;
		this._onChangeCallback();

	}

	/**
	 * A string representing the order that the rotations are applied.
	 *
	 * @type {string}
	 * @default 'XYZ'
	 */
	get order() {

		return this._order;

	}

	set order( value ) {

		this._order = value;
		this._onChangeCallback();

	}

	/**
	 * Sets the Euler components.
	 *
	 * @param {number} x - The angle of the x axis in radians.
	 * @param {number} y - The angle of the y axis in radians.
	 * @param {number} z - The angle of the z axis in radians.
	 * @param {string} [order] - A string representing the order that the rotations are applied.
	 * @return {Euler} A reference to this Euler instance.
	 */
	set( x, y, z, order = this._order ) {

		this._x = x;
		this._y = y;
		this._z = z;
		this._order = order;

		this._onChangeCallback();

		return this;

	}

	/**
	 * Returns a new Euler instance with copied values from this instance.
	 *
	 * @return {Euler} A clone of this instance.
	 */
	clone() {

		return new this.constructor( this._x, this._y, this._z, this._order );

	}

	/**
	 * Copies the values of the given Euler instance to this instance.
	 *
	 * @param {Euler} euler - The Euler instance to copy.
	 * @return {Euler} A reference to this Euler instance.
	 */
	copy( euler ) {

		this._x = euler._x;
		this._y = euler._y;
		this._z = euler._z;
		this._order = euler._order;

		this._onChangeCallback();

		return this;

	}

	/**
	 * Sets the angles of this Euler instance from a pure rotation matrix.
	 *
	 * @param {Matrix4} m - A 4x4 matrix of which the upper 3x3 of matrix is a pure rotation matrix (i.e. unscaled).
	 * @param {string} [order] - A string representing the order that the rotations are applied.
	 * @param {boolean} [update=true] - Whether the internal `onChange` callback should be executed or not.
	 * @return {Euler} A reference to this Euler instance.
	 */
	setFromRotationMatrix( m, order = this._order, update = true ) {

		const te = m.elements;
		const m11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ];
		const m21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ];
		const m31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ];

		switch ( order ) {

			case 'XYZ':

				this._y = Math.asin( clamp( m13, -1, 1 ) );

				if ( Math.abs( m13 ) < 0.9999999 ) {

					this._x = Math.atan2( - m23, m33 );
					this._z = Math.atan2( - m12, m11 );

				} else {

					this._x = Math.atan2( m32, m22 );
					this._z = 0;

				}

				break;

			case 'YXZ':

				this._x = Math.asin( - clamp( m23, -1, 1 ) );

				if ( Math.abs( m23 ) < 0.9999999 ) {

					this._y = Math.atan2( m13, m33 );
					this._z = Math.atan2( m21, m22 );

				} else {

					this._y = Math.atan2( - m31, m11 );
					this._z = 0;

				}

				break;

			case 'ZXY':

				this._x = Math.asin( clamp( m32, -1, 1 ) );

				if ( Math.abs( m32 ) < 0.9999999 ) {

					this._y = Math.atan2( - m31, m33 );
					this._z = Math.atan2( - m12, m22 );

				} else {

					this._y = 0;
					this._z = Math.atan2( m21, m11 );

				}

				break;

			case 'ZYX':

				this._y = Math.asin( - clamp( m31, -1, 1 ) );

				if ( Math.abs( m31 ) < 0.9999999 ) {

					this._x = Math.atan2( m32, m33 );
					this._z = Math.atan2( m21, m11 );

				} else {

					this._x = 0;
					this._z = Math.atan2( - m12, m22 );

				}

				break;

			case 'YZX':

				this._z = Math.asin( clamp( m21, -1, 1 ) );

				if ( Math.abs( m21 ) < 0.9999999 ) {

					this._x = Math.atan2( - m23, m22 );
					this._y = Math.atan2( - m31, m11 );

				} else {

					this._x = 0;
					this._y = Math.atan2( m13, m33 );

				}

				break;

			case 'XZY':

				this._z = Math.asin( - clamp( m12, -1, 1 ) );

				if ( Math.abs( m12 ) < 0.9999999 ) {

					this._x = Math.atan2( m32, m22 );
					this._y = Math.atan2( m13, m11 );

				} else {

					this._x = Math.atan2( - m23, m33 );
					this._y = 0;

				}

				break;

			default:

				console.warn( 'THREE.Euler: .setFromRotationMatrix() encountered an unknown order: ' + order );

		}

		this._order = order;

		if ( update === true ) this._onChangeCallback();

		return this;

	}

	/**
	 * Sets the angles of this Euler instance from a normalized quaternion.
	 *
	 * @param {Quaternion} q - A normalized Quaternion.
	 * @param {string} [order] - A string representing the order that the rotations are applied.
	 * @param {boolean} [update=true] - Whether the internal `onChange` callback should be executed or not.
	 * @return {Euler} A reference to this Euler instance.
	 */
	setFromQuaternion( q, order, update ) {

		_matrix$2.makeRotationFromQuaternion( q );

		return this.setFromRotationMatrix( _matrix$2, order, update );

	}

	/**
	 * Sets the angles of this Euler instance from the given vector.
	 *
	 * @param {Vector3} v - The vector.
	 * @param {string} [order] - A string representing the order that the rotations are applied.
	 * @return {Euler} A reference to this Euler instance.
	 */
	setFromVector3( v, order = this._order ) {

		return this.set( v.x, v.y, v.z, order );

	}

	/**
	 * Resets the euler angle with a new order by creating a quaternion from this
	 * euler angle and then setting this euler angle with the quaternion and the
	 * new order.
	 *
	 * Warning: This discards revolution information.
	 *
	 * @param {string} [newOrder] - A string representing the new order that the rotations are applied.
	 * @return {Euler} A reference to this Euler instance.
	 */
	reorder( newOrder ) {

		_quaternion$1.setFromEuler( this );

		return this.setFromQuaternion( _quaternion$1, newOrder );

	}

	/**
	 * Returns `true` if this Euler instance is equal with the given one.
	 *
	 * @param {Euler} euler - The Euler instance to test for equality.
	 * @return {boolean} Whether this Euler instance is equal with the given one.
	 */
	equals( euler ) {

		return ( euler._x === this._x ) && ( euler._y === this._y ) && ( euler._z === this._z ) && ( euler._order === this._order );

	}

	/**
	 * Sets this Euler instance's components to values from the given array. The first three
	 * entries of the array are assign to the x,y and z components. An optional fourth entry
	 * defines the Euler order.
	 *
	 * @param {Array<number,number,number,?string>} array - An array holding the Euler component values.
	 * @return {Euler} A reference to this Euler instance.
	 */
	fromArray( array ) {

		this._x = array[ 0 ];
		this._y = array[ 1 ];
		this._z = array[ 2 ];
		if ( array[ 3 ] !== undefined ) this._order = array[ 3 ];

		this._onChangeCallback();

		return this;

	}

	/**
	 * Writes the components of this Euler instance to the given array. If no array is provided,
	 * the method returns a new instance.
	 *
	 * @param {Array<number,number,number,string>} [array=[]] - The target array holding the Euler components.
	 * @param {number} [offset=0] - Index of the first element in the array.
	 * @return {Array<number,number,number,string>} The Euler components.
	 */
	toArray( array = [], offset = 0 ) {

		array[ offset ] = this._x;
		array[ offset + 1 ] = this._y;
		array[ offset + 2 ] = this._z;
		array[ offset + 3 ] = this._order;

		return array;

	}

	_onChange( callback ) {

		this._onChangeCallback = callback;

		return this;

	}

	_onChangeCallback() {}

	*[ Symbol.iterator ]() {

		yield this._x;
		yield this._y;
		yield this._z;
		yield this._order;

	}

}

/**
 * The default Euler angle order.
 *
 * @static
 * @type {string}
 * @default 'XYZ'
 */
Euler.DEFAULT_ORDER = 'XYZ';

/**
 * A layers object assigns an 3D object to 1 or more of 32
 * layers numbered `0` to `31` - internally the layers are stored as a
 * bit mask], and by default all 3D objects are a member of layer `0`.
 *
 * This can be used to control visibility - an object must share a layer with
 * a camera to be visible when that camera's view is
 * rendered.
 *
 * All classes that inherit from {@link Object3D} have an `layers` property which
 * is an instance of this class.
 */
class Layers {

	/**
	 * Constructs a new layers instance, with membership
	 * initially set to layer `0`.
	 */
	constructor() {

		/**
		 * A bit mask storing which of the 32 layers this layers object is currently
		 * a member of.
		 *
		 * @type {number}
		 */
		this.mask = 1 | 0;

	}

	/**
	 * Sets membership to the given layer, and remove membership all other layers.
	 *
	 * @param {number} layer - The layer to set.
	 */
	set( layer ) {

		this.mask = ( 1 << layer | 0 ) >>> 0;

	}

	/**
	 * Adds membership of the given layer.
	 *
	 * @param {number} layer - The layer to enable.
	 */
	enable( layer ) {

		this.mask |= 1 << layer | 0;

	}

	/**
	 * Adds membership to all layers.
	 */
	enableAll() {

		this.mask = 0xffffffff | 0;

	}

	/**
	 * Toggles the membership of the given layer.
	 *
	 * @param {number} layer - The layer to toggle.
	 */
	toggle( layer ) {

		this.mask ^= 1 << layer | 0;

	}

	/**
	 * Removes membership of the given layer.
	 *
	 * @param {number} layer - The layer to enable.
	 */
	disable( layer ) {

		this.mask &= ~ ( 1 << layer | 0 );

	}

	/**
	 * Removes the membership from all layers.
	 */
	disableAll() {

		this.mask = 0;

	}

	/**
	 * Returns `true` if this and the given layers object have at least one
	 * layer in common.
	 *
	 * @param {Layers} layers - The layers to test.
	 * @return {boolean } Whether this and the given layers object have at least one layer in common or not.
	 */
	test( layers ) {

		return ( this.mask & layers.mask ) !== 0;

	}

	/**
	 * Returns `true` if the given layer is enabled.
	 *
	 * @param {number} layer - The layer to test.
	 * @return {boolean } Whether the given layer is enabled or not.
	 */
	isEnabled( layer ) {

		return ( this.mask & ( 1 << layer | 0 ) ) !== 0;

	}

}

let _object3DId = 0;

const _v1$3 = /*@__PURE__*/ new Vector3();
const _q1 = /*@__PURE__*/ new Quaternion();
const _m1$3 = /*@__PURE__*/ new Matrix4();
const _target = /*@__PURE__*/ new Vector3();

const _position$1 = /*@__PURE__*/ new Vector3();
const _scale = /*@__PURE__*/ new Vector3();
const _quaternion = /*@__PURE__*/ new Quaternion();

const _xAxis = /*@__PURE__*/ new Vector3( 1, 0, 0 );
const _yAxis = /*@__PURE__*/ new Vector3( 0, 1, 0 );
const _zAxis = /*@__PURE__*/ new Vector3( 0, 0, 1 );

/**
 * Fires when the object has been added to its parent object.
 *
 * @event Object3D#added
 * @type {Object}
 */
const _addedEvent = { type: 'added' };

/**
 * Fires when the object has been removed from its parent object.
 *
 * @event Object3D#removed
 * @type {Object}
 */
const _removedEvent = { type: 'removed' };

/**
 * Fires when a new child object has been added.
 *
 * @event Object3D#childadded
 * @type {Object}
 */
const _childaddedEvent = { type: 'childadded', child: null };

/**
 * Fires when a child object has been removed.
 *
 * @event Object3D#childremoved
 * @type {Object}
 */
const _childremovedEvent = { type: 'childremoved', child: null };

/**
 * This is the base class for most objects in three.js and provides a set of
 * properties and methods for manipulating objects in 3D space.
 *
 * @augments EventDispatcher
 */
class Object3D extends EventDispatcher {

	/**
	 * Constructs a new 3D object.
	 */
	constructor() {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isObject3D = true;

		/**
		 * The ID of the 3D object.
		 *
		 * @name Object3D#id
		 * @type {number}
		 * @readonly
		 */
		Object.defineProperty( this, 'id', { value: _object3DId ++ } );

		/**
		 * The UUID of the 3D object.
		 *
		 * @type {string}
		 * @readonly
		 */
		this.uuid = generateUUID();

		/**
		 * The name of the 3D object.
		 *
		 * @type {string}
		 */
		this.name = '';

		/**
		 * The type property is used for detecting the object type
		 * in context of serialization/deserialization.
		 *
		 * @type {string}
		 * @readonly
		 */
		this.type = 'Object3D';

		/**
		 * A reference to the parent object.
		 *
		 * @type {?Object3D}
		 * @default null
		 */
		this.parent = null;

		/**
		 * An array holding the child 3D objects of this instance.
		 *
		 * @type {Array<Object3D>}
		 */
		this.children = [];

		/**
		 * Defines the `up` direction of the 3D object which influences
		 * the orientation via methods like {@link Object3D#lookAt}.
		 *
		 * The default values for all 3D objects is defined by `Object3D.DEFAULT_UP`.
		 *
		 * @type {Vector3}
		 */
		this.up = Object3D.DEFAULT_UP.clone();

		const position = new Vector3();
		const rotation = new Euler();
		const quaternion = new Quaternion();
		const scale = new Vector3( 1, 1, 1 );

		function onRotationChange() {

			quaternion.setFromEuler( rotation, false );

		}

		function onQuaternionChange() {

			rotation.setFromQuaternion( quaternion, undefined, false );

		}

		rotation._onChange( onRotationChange );
		quaternion._onChange( onQuaternionChange );

		Object.defineProperties( this, {
			/**
			 * Represents the object's local position.
			 *
			 * @name Object3D#position
			 * @type {Vector3}
			 * @default (0,0,0)
			 */
			position: {
				configurable: true,
				enumerable: true,
				value: position
			},
			/**
			 * Represents the object's local rotation as Euler angles, in radians.
			 *
			 * @name Object3D#rotation
			 * @type {Euler}
			 * @default (0,0,0)
			 */
			rotation: {
				configurable: true,
				enumerable: true,
				value: rotation
			},
			/**
			 * Represents the object's local rotation as Quaternions.
			 *
			 * @name Object3D#quaternion
			 * @type {Quaternion}
			 */
			quaternion: {
				configurable: true,
				enumerable: true,
				value: quaternion
			},
			/**
			 * Represents the object's local scale.
			 *
			 * @name Object3D#scale
			 * @type {Vector3}
			 * @default (1,1,1)
			 */
			scale: {
				configurable: true,
				enumerable: true,
				value: scale
			},
			/**
			 * Represents the object's model-view matrix.
			 *
			 * @name Object3D#modelViewMatrix
			 * @type {Matrix4}
			 */
			modelViewMatrix: {
				value: new Matrix4()
			},
			/**
			 * Represents the object's normal matrix.
			 *
			 * @name Object3D#normalMatrix
			 * @type {Matrix3}
			 */
			normalMatrix: {
				value: new Matrix3()
			}
		} );

		/**
		 * Represents the object's transformation matrix in local space.
		 *
		 * @type {Matrix4}
		 */
		this.matrix = new Matrix4();

		/**
		 * Represents the object's transformation matrix in world space.
		 * If the 3D object has no parent, then it's identical to the local transformation matrix
		 *
		 * @type {Matrix4}
		 */
		this.matrixWorld = new Matrix4();

		/**
		 * When set to `true`, the engine automatically computes the local matrix from position,
		 * rotation and scale every frame.
		 *
		 * The default values for all 3D objects is defined by `Object3D.DEFAULT_MATRIX_AUTO_UPDATE`.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.matrixAutoUpdate = Object3D.DEFAULT_MATRIX_AUTO_UPDATE;

		/**
		 * When set to `true`, the engine automatically computes the world matrix from the current local
		 * matrix and the object's transformation hierarchy.
		 *
		 * The default values for all 3D objects is defined by `Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE`.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.matrixWorldAutoUpdate = Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE; // checked by the renderer

		/**
		 * When set to `true`, it calculates the world matrix in that frame and resets this property
		 * to `false`.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.matrixWorldNeedsUpdate = false;

		/**
		 * The layer membership of the 3D object. The 3D object is only visible if it has
		 * at least one layer in common with the camera in use. This property can also be
		 * used to filter out unwanted objects in ray-intersection tests when using {@link Raycaster}.
		 *
		 * @type {Layers}
		 */
		this.layers = new Layers();

		/**
		 * When set to `true`, the 3D object gets rendered.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.visible = true;

		/**
		 * When set to `true`, the 3D object gets rendered into shadow maps.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.castShadow = false;

		/**
		 * When set to `true`, the 3D object is affected by shadows in the scene.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.receiveShadow = false;

		/**
		 * When set to `true`, the 3D object is honored by view frustum culling.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.frustumCulled = true;

		/**
		 * This value allows the default rendering order of scene graph objects to be
		 * overridden although opaque and transparent objects remain sorted independently.
		 * When this property is set for an instance of {@link Group},all descendants
		 * objects will be sorted and rendered together. Sorting is from lowest to highest
		 * render order.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.renderOrder = 0;

		/**
		 * An array holding the animation clips of the 3D object.
		 *
		 * @type {Array<AnimationClip>}
		 */
		this.animations = [];

		/**
		 * Custom depth material to be used when rendering to the depth map. Can only be used
		 * in context of meshes. When shadow-casting with a {@link DirectionalLight} or {@link SpotLight},
		 * if you are modifying vertex positions in the vertex shader you must specify a custom depth
		 * material for proper shadows.
		 *
		 * Only relevant in context of {@link WebGLRenderer}.
		 *
		 * @type {(Material|undefined)}
		 * @default undefined
		 */
		this.customDepthMaterial = undefined;

		/**
		 * Same as {@link Object3D#customDepthMaterial}, but used with {@link PointLight}.
		 *
		 * Only relevant in context of {@link WebGLRenderer}.
		 *
		 * @type {(Material|undefined)}
		 * @default undefined
		 */
		this.customDistanceMaterial = undefined;

		/**
		 * An object that can be used to store custom data about the 3D object. It
		 * should not hold references to functions as these will not be cloned.
		 *
		 * @type {Object}
		 */
		this.userData = {};

	}

	/**
	 * A callback that is executed immediately before a 3D object is rendered to a shadow map.
	 *
	 * @param {Renderer|WebGLRenderer} renderer - The renderer.
	 * @param {Object3D} object - The 3D object.
	 * @param {Camera} camera - The camera that is used to render the scene.
	 * @param {Camera} shadowCamera - The shadow camera.
	 * @param {BufferGeometry} geometry - The 3D object's geometry.
	 * @param {Material} depthMaterial - The depth material.
	 * @param {Object} group - The geometry group data.
	 */
	onBeforeShadow( /* renderer, object, camera, shadowCamera, geometry, depthMaterial, group */ ) {}

	/**
	 * A callback that is executed immediately after a 3D object is rendered to a shadow map.
	 *
	 * @param {Renderer|WebGLRenderer} renderer - The renderer.
	 * @param {Object3D} object - The 3D object.
	 * @param {Camera} camera - The camera that is used to render the scene.
	 * @param {Camera} shadowCamera - The shadow camera.
	 * @param {BufferGeometry} geometry - The 3D object's geometry.
	 * @param {Material} depthMaterial - The depth material.
	 * @param {Object} group - The geometry group data.
	 */
	onAfterShadow( /* renderer, object, camera, shadowCamera, geometry, depthMaterial, group */ ) {}

	/**
	 * A callback that is executed immediately before a 3D object is rendered.
	 *
	 * @param {Renderer|WebGLRenderer} renderer - The renderer.
	 * @param {Object3D} object - The 3D object.
	 * @param {Camera} camera - The camera that is used to render the scene.
	 * @param {BufferGeometry} geometry - The 3D object's geometry.
	 * @param {Material} material - The 3D object's material.
	 * @param {Object} group - The geometry group data.
	 */
	onBeforeRender( /* renderer, scene, camera, geometry, material, group */ ) {}

	/**
	 * A callback that is executed immediately after a 3D object is rendered.
	 *
	 * @param {Renderer|WebGLRenderer} renderer - The renderer.
	 * @param {Object3D} object - The 3D object.
	 * @param {Camera} camera - The camera that is used to render the scene.
	 * @param {BufferGeometry} geometry - The 3D object's geometry.
	 * @param {Material} material - The 3D object's material.
	 * @param {Object} group - The geometry group data.
	 */
	onAfterRender( /* renderer, scene, camera, geometry, material, group */ ) {}

	/**
	 * Applies the given transformation matrix to the object and updates the object's position,
	 * rotation and scale.
	 *
	 * @param {Matrix4} matrix - The transformation matrix.
	 */
	applyMatrix4( matrix ) {

		if ( this.matrixAutoUpdate ) this.updateMatrix();

		this.matrix.premultiply( matrix );

		this.matrix.decompose( this.position, this.quaternion, this.scale );

	}

	/**
	 * Applies a rotation represented by given the quaternion to the 3D object.
	 *
	 * @param {Quaternion} q - The quaternion.
	 * @return {Object3D} A reference to this instance.
	 */
	applyQuaternion( q ) {

		this.quaternion.premultiply( q );

		return this;

	}

	/**
	 * Sets the given rotation represented as an axis/angle couple to the 3D object.
	 *
	 * @param {Vector3} axis - The (normalized) axis vector.
	 * @param {number} angle - The angle in radians.
	 */
	setRotationFromAxisAngle( axis, angle ) {

		// assumes axis is normalized

		this.quaternion.setFromAxisAngle( axis, angle );

	}

	/**
	 * Sets the given rotation represented as Euler angles to the 3D object.
	 *
	 * @param {Euler} euler - The Euler angles.
	 */
	setRotationFromEuler( euler ) {

		this.quaternion.setFromEuler( euler, true );

	}

	/**
	 * Sets the given rotation represented as rotation matrix to the 3D object.
	 *
	 * @param {Matrix4} m - Although a 4x4 matrix is expected, the upper 3x3 portion must be
	 * a pure rotation matrix (i.e, unscaled).
	 */
	setRotationFromMatrix( m ) {

		// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)

		this.quaternion.setFromRotationMatrix( m );

	}

	/**
	 * Sets the given rotation represented as a Quaternion to the 3D object.
	 *
	 * @param {Quaternion} q - The Quaternion
	 */
	setRotationFromQuaternion( q ) {

		// assumes q is normalized

		this.quaternion.copy( q );

	}

	/**
	 * Rotates the 3D object along an axis in local space.
	 *
	 * @param {Vector3} axis - The (normalized) axis vector.
	 * @param {number} angle - The angle in radians.
	 * @return {Object3D} A reference to this instance.
	 */
	rotateOnAxis( axis, angle ) {

		// rotate object on axis in object space
		// axis is assumed to be normalized

		_q1.setFromAxisAngle( axis, angle );

		this.quaternion.multiply( _q1 );

		return this;

	}

	/**
	 * Rotates the 3D object along an axis in world space.
	 *
	 * @param {Vector3} axis - The (normalized) axis vector.
	 * @param {number} angle - The angle in radians.
	 * @return {Object3D} A reference to this instance.
	 */
	rotateOnWorldAxis( axis, angle ) {

		// rotate object on axis in world space
		// axis is assumed to be normalized
		// method assumes no rotated parent

		_q1.setFromAxisAngle( axis, angle );

		this.quaternion.premultiply( _q1 );

		return this;

	}

	/**
	 * Rotates the 3D object around its X axis in local space.
	 *
	 * @param {number} angle - The angle in radians.
	 * @return {Object3D} A reference to this instance.
	 */
	rotateX( angle ) {

		return this.rotateOnAxis( _xAxis, angle );

	}

	/**
	 * Rotates the 3D object around its Y axis in local space.
	 *
	 * @param {number} angle - The angle in radians.
	 * @return {Object3D} A reference to this instance.
	 */
	rotateY( angle ) {

		return this.rotateOnAxis( _yAxis, angle );

	}

	/**
	 * Rotates the 3D object around its Z axis in local space.
	 *
	 * @param {number} angle - The angle in radians.
	 * @return {Object3D} A reference to this instance.
	 */
	rotateZ( angle ) {

		return this.rotateOnAxis( _zAxis, angle );

	}

	/**
	 * Translate the 3D object by a distance along the given axis in local space.
	 *
	 * @param {Vector3} axis - The (normalized) axis vector.
	 * @param {number} distance - The distance in world units.
	 * @return {Object3D} A reference to this instance.
	 */
	translateOnAxis( axis, distance ) {

		// translate object by distance along axis in object space
		// axis is assumed to be normalized

		_v1$3.copy( axis ).applyQuaternion( this.quaternion );

		this.position.add( _v1$3.multiplyScalar( distance ) );

		return this;

	}

	/**
	 * Translate the 3D object by a distance along its X-axis in local space.
	 *
	 * @param {number} distance - The distance in world units.
	 * @return {Object3D} A reference to this instance.
	 */
	translateX( distance ) {

		return this.translateOnAxis( _xAxis, distance );

	}

	/**
	 * Translate the 3D object by a distance along its Y-axis in local space.
	 *
	 * @param {number} distance - The distance in world units.
	 * @return {Object3D} A reference to this instance.
	 */
	translateY( distance ) {

		return this.translateOnAxis( _yAxis, distance );

	}

	/**
	 * Translate the 3D object by a distance along its Z-axis in local space.
	 *
	 * @param {number} distance - The distance in world units.
	 * @return {Object3D} A reference to this instance.
	 */
	translateZ( distance ) {

		return this.translateOnAxis( _zAxis, distance );

	}

	/**
	 * Converts the given vector from this 3D object's local space to world space.
	 *
	 * @param {Vector3} vector - The vector to convert.
	 * @return {Vector3} The converted vector.
	 */
	localToWorld( vector ) {

		this.updateWorldMatrix( true, false );

		return vector.applyMatrix4( this.matrixWorld );

	}

	/**
	 * Converts the given vector from this 3D object's word space to local space.
	 *
	 * @param {Vector3} vector - The vector to convert.
	 * @return {Vector3} The converted vector.
	 */
	worldToLocal( vector ) {

		this.updateWorldMatrix( true, false );

		return vector.applyMatrix4( _m1$3.copy( this.matrixWorld ).invert() );

	}

	/**
	 * Rotates the object to face a point in world space.
	 *
	 * This method does not support objects having non-uniformly-scaled parent(s).
	 *
	 * @param {number|Vector3} x - The x coordinate in world space. Alternatively, a vector representing a position in world space
	 * @param {number} [y] - The y coordinate in world space.
	 * @param {number} [z] - The z coordinate in world space.
	 */
	lookAt( x, y, z ) {

		// This method does not support objects having non-uniformly-scaled parent(s)

		if ( x.isVector3 ) {

			_target.copy( x );

		} else {

			_target.set( x, y, z );

		}

		const parent = this.parent;

		this.updateWorldMatrix( true, false );

		_position$1.setFromMatrixPosition( this.matrixWorld );

		if ( this.isCamera || this.isLight ) {

			_m1$3.lookAt( _position$1, _target, this.up );

		} else {

			_m1$3.lookAt( _target, _position$1, this.up );

		}

		this.quaternion.setFromRotationMatrix( _m1$3 );

		if ( parent ) {

			_m1$3.extractRotation( parent.matrixWorld );
			_q1.setFromRotationMatrix( _m1$3 );
			this.quaternion.premultiply( _q1.invert() );

		}

	}

	/**
	 * Adds the given 3D object as a child to this 3D object. An arbitrary number of
	 * objects may be added. Any current parent on an object passed in here will be
	 * removed, since an object can have at most one parent.
	 *
	 * @fires Object3D#added
	 * @fires Object3D#childadded
	 * @param {Object3D} object - The 3D object to add.
	 * @return {Object3D} A reference to this instance.
	 */
	add( object ) {

		if ( arguments.length > 1 ) {

			for ( let i = 0; i < arguments.length; i ++ ) {

				this.add( arguments[ i ] );

			}

			return this;

		}

		if ( object === this ) {

			console.error( 'THREE.Object3D.add: object can\'t be added as a child of itself.', object );
			return this;

		}

		if ( object && object.isObject3D ) {

			object.removeFromParent();
			object.parent = this;
			this.children.push( object );

			object.dispatchEvent( _addedEvent );

			_childaddedEvent.child = object;
			this.dispatchEvent( _childaddedEvent );
			_childaddedEvent.child = null;

		} else {

			console.error( 'THREE.Object3D.add: object not an instance of THREE.Object3D.', object );

		}

		return this;

	}

	/**
	 * Removes the given 3D object as child from this 3D object.
	 * An arbitrary number of objects may be removed.
	 *
	 * @fires Object3D#removed
	 * @fires Object3D#childremoved
	 * @param {Object3D} object - The 3D object to remove.
	 * @return {Object3D} A reference to this instance.
	 */
	remove( object ) {

		if ( arguments.length > 1 ) {

			for ( let i = 0; i < arguments.length; i ++ ) {

				this.remove( arguments[ i ] );

			}

			return this;

		}

		const index = this.children.indexOf( object );

		if ( index !== -1 ) {

			object.parent = null;
			this.children.splice( index, 1 );

			object.dispatchEvent( _removedEvent );

			_childremovedEvent.child = object;
			this.dispatchEvent( _childremovedEvent );
			_childremovedEvent.child = null;

		}

		return this;

	}

	/**
	 * Removes this 3D object from its current parent.
	 *
	 * @fires Object3D#removed
	 * @fires Object3D#childremoved
	 * @return {Object3D} A reference to this instance.
	 */
	removeFromParent() {

		const parent = this.parent;

		if ( parent !== null ) {

			parent.remove( this );

		}

		return this;

	}

	/**
	 * Removes all child objects.
	 *
	 * @fires Object3D#removed
	 * @fires Object3D#childremoved
	 * @return {Object3D} A reference to this instance.
	 */
	clear() {

		return this.remove( ... this.children );

	}

	/**
	 * Adds the given 3D object as a child of this 3D object, while maintaining the object's world
	 * transform. This method does not support scene graphs having non-uniformly-scaled nodes(s).
	 *
	 * @fires Object3D#added
	 * @fires Object3D#childadded
	 * @param {Object3D} object - The 3D object to attach.
	 * @return {Object3D} A reference to this instance.
	 */
	attach( object ) {

		// adds object as a child of this, while maintaining the object's world transform

		// Note: This method does not support scene graphs having non-uniformly-scaled nodes(s)

		this.updateWorldMatrix( true, false );

		_m1$3.copy( this.matrixWorld ).invert();

		if ( object.parent !== null ) {

			object.parent.updateWorldMatrix( true, false );

			_m1$3.multiply( object.parent.matrixWorld );

		}

		object.applyMatrix4( _m1$3 );

		object.removeFromParent();
		object.parent = this;
		this.children.push( object );

		object.updateWorldMatrix( false, true );

		object.dispatchEvent( _addedEvent );

		_childaddedEvent.child = object;
		this.dispatchEvent( _childaddedEvent );
		_childaddedEvent.child = null;

		return this;

	}

	/**
	 * Searches through the 3D object and its children, starting with the 3D object
	 * itself, and returns the first with a matching ID.
	 *
	 * @param {number} id - The id.
	 * @return {Object3D|undefined} The found 3D object. Returns `undefined` if no 3D object has been found.
	 */
	getObjectById( id ) {

		return this.getObjectByProperty( 'id', id );

	}

	/**
	 * Searches through the 3D object and its children, starting with the 3D object
	 * itself, and returns the first with a matching name.
	 *
	 * @param {string} name - The name.
	 * @return {Object3D|undefined} The found 3D object. Returns `undefined` if no 3D object has been found.
	 */
	getObjectByName( name ) {

		return this.getObjectByProperty( 'name', name );

	}

	/**
	 * Searches through the 3D object and its children, starting with the 3D object
	 * itself, and returns the first with a matching property value.
	 *
	 * @param {string} name - The name of the property.
	 * @param {any} value - The value.
	 * @return {Object3D|undefined} The found 3D object. Returns `undefined` if no 3D object has been found.
	 */
	getObjectByProperty( name, value ) {

		if ( this[ name ] === value ) return this;

		for ( let i = 0, l = this.children.length; i < l; i ++ ) {

			const child = this.children[ i ];
			const object = child.getObjectByProperty( name, value );

			if ( object !== undefined ) {

				return object;

			}

		}

		return undefined;

	}

	/**
	 * Searches through the 3D object and its children, starting with the 3D object
	 * itself, and returns all 3D objects with a matching property value.
	 *
	 * @param {string} name - The name of the property.
	 * @param {any} value - The value.
	 * @param {Array<Object3D>} result - The method stores the result in this array.
	 * @return {Array<Object3D>} The found 3D objects.
	 */
	getObjectsByProperty( name, value, result = [] ) {

		if ( this[ name ] === value ) result.push( this );

		const children = this.children;

		for ( let i = 0, l = children.length; i < l; i ++ ) {

			children[ i ].getObjectsByProperty( name, value, result );

		}

		return result;

	}

	/**
	 * Returns a vector representing the position of the 3D object in world space.
	 *
	 * @param {Vector3} target - The target vector the result is stored to.
	 * @return {Vector3} The 3D object's position in world space.
	 */
	getWorldPosition( target ) {

		this.updateWorldMatrix( true, false );

		return target.setFromMatrixPosition( this.matrixWorld );

	}

	/**
	 * Returns a Quaternion representing the position of the 3D object in world space.
	 *
	 * @param {Quaternion} target - The target Quaternion the result is stored to.
	 * @return {Quaternion} The 3D object's rotation in world space.
	 */
	getWorldQuaternion( target ) {

		this.updateWorldMatrix( true, false );

		this.matrixWorld.decompose( _position$1, target, _scale );

		return target;

	}

	/**
	 * Returns a vector representing the scale of the 3D object in world space.
	 *
	 * @param {Vector3} target - The target vector the result is stored to.
	 * @return {Vector3} The 3D object's scale in world space.
	 */
	getWorldScale( target ) {

		this.updateWorldMatrix( true, false );

		this.matrixWorld.decompose( _position$1, _quaternion, target );

		return target;

	}

	/**
	 * Returns a vector representing the ("look") direction of the 3D object in world space.
	 *
	 * @param {Vector3} target - The target vector the result is stored to.
	 * @return {Vector3} The 3D object's direction in world space.
	 */
	getWorldDirection( target ) {

		this.updateWorldMatrix( true, false );

		const e = this.matrixWorld.elements;

		return target.set( e[ 8 ], e[ 9 ], e[ 10 ] ).normalize();

	}

	/**
	 * Abstract method to get intersections between a casted ray and this
	 * 3D object. Renderable 3D objects such as {@link Mesh}, {@link Line} or {@link Points}
	 * implement this method in order to use raycasting.
	 *
	 * @abstract
	 * @param {Raycaster} raycaster - The raycaster.
	 * @param {Array<Object>} intersects - An array holding the result of the method.
	 */
	raycast( /* raycaster, intersects */ ) {}

	/**
	 * Executes the callback on this 3D object and all descendants.
	 *
	 * Note: Modifying the scene graph inside the callback is discouraged.
	 *
	 * @param {Function} callback - A callback function that allows to process the current 3D object.
	 */
	traverse( callback ) {

		callback( this );

		const children = this.children;

		for ( let i = 0, l = children.length; i < l; i ++ ) {

			children[ i ].traverse( callback );

		}

	}

	/**
	 * Like {@link Object3D#traverse}, but the callback will only be executed for visible 3D objects.
	 * Descendants of invisible 3D objects are not traversed.
	 *
	 * Note: Modifying the scene graph inside the callback is discouraged.
	 *
	 * @param {Function} callback - A callback function that allows to process the current 3D object.
	 */
	traverseVisible( callback ) {

		if ( this.visible === false ) return;

		callback( this );

		const children = this.children;

		for ( let i = 0, l = children.length; i < l; i ++ ) {

			children[ i ].traverseVisible( callback );

		}

	}

	/**
	 * Like {@link Object3D#traverse}, but the callback will only be executed for all ancestors.
	 *
	 * Note: Modifying the scene graph inside the callback is discouraged.
	 *
	 * @param {Function} callback - A callback function that allows to process the current 3D object.
	 */
	traverseAncestors( callback ) {

		const parent = this.parent;

		if ( parent !== null ) {

			callback( parent );

			parent.traverseAncestors( callback );

		}

	}

	/**
	 * Updates the transformation matrix in local space by computing it from the current
	 * position, rotation and scale values.
	 */
	updateMatrix() {

		this.matrix.compose( this.position, this.quaternion, this.scale );

		this.matrixWorldNeedsUpdate = true;

	}

	/**
	 * Updates the transformation matrix in world space of this 3D objects and its descendants.
	 *
	 * To ensure correct results, this method also recomputes the 3D object's transformation matrix in
	 * local space. The computation of the local and world matrix can be controlled with the
	 * {@link Object3D#matrixAutoUpdate} and {@link Object3D#matrixWorldAutoUpdate} flags which are both
	 * `true` by default.  Set these flags to `false` if you need more control over the update matrix process.
	 *
	 * @param {boolean} [force=false] - When set to `true`, a recomputation of world matrices is forced even
	 * when {@link Object3D#matrixWorldAutoUpdate} is set to `false`.
	 */
	updateMatrixWorld( force ) {

		if ( this.matrixAutoUpdate ) this.updateMatrix();

		if ( this.matrixWorldNeedsUpdate || force ) {

			if ( this.matrixWorldAutoUpdate === true ) {

				if ( this.parent === null ) {

					this.matrixWorld.copy( this.matrix );

				} else {

					this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );

				}

			}

			this.matrixWorldNeedsUpdate = false;

			force = true;

		}

		// make sure descendants are updated if required

		const children = this.children;

		for ( let i = 0, l = children.length; i < l; i ++ ) {

			const child = children[ i ];

			child.updateMatrixWorld( force );

		}

	}

	/**
	 * An alternative version of {@link Object3D#updateMatrixWorld} with more control over the
	 * update of ancestor and descendant nodes.
	 *
	 * @param {boolean} [updateParents=false] Whether ancestor nodes should be updated or not.
	 * @param {boolean} [updateChildren=false] Whether descendant nodes should be updated or not.
	 */
	updateWorldMatrix( updateParents, updateChildren ) {

		const parent = this.parent;

		if ( updateParents === true && parent !== null ) {

			parent.updateWorldMatrix( true, false );

		}

		if ( this.matrixAutoUpdate ) this.updateMatrix();

		if ( this.matrixWorldAutoUpdate === true ) {

			if ( this.parent === null ) {

				this.matrixWorld.copy( this.matrix );

			} else {

				this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );

			}

		}

		// make sure descendants are updated

		if ( updateChildren === true ) {

			const children = this.children;

			for ( let i = 0, l = children.length; i < l; i ++ ) {

				const child = children[ i ];

				child.updateWorldMatrix( false, true );

			}

		}

	}

	/**
	 * Serializes the 3D object into JSON.
	 *
	 * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.
	 * @return {Object} A JSON object representing the serialized 3D object.
	 * @see {@link ObjectLoader#parse}
	 */
	toJSON( meta ) {

		// meta is a string when called from JSON.stringify
		const isRootObject = ( meta === undefined || typeof meta === 'string' );

		const output = {};

		// meta is a hash used to collect geometries, materials.
		// not providing it implies that this is the root object
		// being serialized.
		if ( isRootObject ) {

			// initialize meta obj
			meta = {
				geometries: {},
				materials: {},
				textures: {},
				images: {},
				shapes: {},
				skeletons: {},
				animations: {},
				nodes: {}
			};

			output.metadata = {
				version: 4.7,
				type: 'Object',
				generator: 'Object3D.toJSON'
			};

		}

		// standard Object3D serialization

		const object = {};

		object.uuid = this.uuid;
		object.type = this.type;

		if ( this.name !== '' ) object.name = this.name;
		if ( this.castShadow === true ) object.castShadow = true;
		if ( this.receiveShadow === true ) object.receiveShadow = true;
		if ( this.visible === false ) object.visible = false;
		if ( this.frustumCulled === false ) object.frustumCulled = false;
		if ( this.renderOrder !== 0 ) object.renderOrder = this.renderOrder;
		if ( Object.keys( this.userData ).length > 0 ) object.userData = this.userData;

		object.layers = this.layers.mask;
		object.matrix = this.matrix.toArray();
		object.up = this.up.toArray();

		if ( this.matrixAutoUpdate === false ) object.matrixAutoUpdate = false;

		// object specific properties

		if ( this.isInstancedMesh ) {

			object.type = 'InstancedMesh';
			object.count = this.count;
			object.instanceMatrix = this.instanceMatrix.toJSON();
			if ( this.instanceColor !== null ) object.instanceColor = this.instanceColor.toJSON();

		}

		if ( this.isBatchedMesh ) {

			object.type = 'BatchedMesh';
			object.perObjectFrustumCulled = this.perObjectFrustumCulled;
			object.sortObjects = this.sortObjects;

			object.drawRanges = this._drawRanges;
			object.reservedRanges = this._reservedRanges;

			object.geometryInfo = this._geometryInfo.map( info => ( {
				...info,
				boundingBox: info.boundingBox ? info.boundingBox.toJSON() : undefined,
				boundingSphere: info.boundingSphere ? info.boundingSphere.toJSON() : undefined
			} ) );
			object.instanceInfo = this._instanceInfo.map( info => ( { ...info } ) );

			object.availableInstanceIds = this._availableInstanceIds.slice();
			object.availableGeometryIds = this._availableGeometryIds.slice();

			object.nextIndexStart = this._nextIndexStart;
			object.nextVertexStart = this._nextVertexStart;
			object.geometryCount = this._geometryCount;

			object.maxInstanceCount = this._maxInstanceCount;
			object.maxVertexCount = this._maxVertexCount;
			object.maxIndexCount = this._maxIndexCount;

			object.geometryInitialized = this._geometryInitialized;

			object.matricesTexture = this._matricesTexture.toJSON( meta );

			object.indirectTexture = this._indirectTexture.toJSON( meta );

			if ( this._colorsTexture !== null ) {

				object.colorsTexture = this._colorsTexture.toJSON( meta );

			}

			if ( this.boundingSphere !== null ) {

				object.boundingSphere = this.boundingSphere.toJSON();

			}

			if ( this.boundingBox !== null ) {

				object.boundingBox = this.boundingBox.toJSON();

			}

		}

		//

		function serialize( library, element ) {

			if ( library[ element.uuid ] === undefined ) {

				library[ element.uuid ] = element.toJSON( meta );

			}

			return element.uuid;

		}

		if ( this.isScene ) {

			if ( this.background ) {

				if ( this.background.isColor ) {

					object.background = this.background.toJSON();

				} else if ( this.background.isTexture ) {

					object.background = this.background.toJSON( meta ).uuid;

				}

			}

			if ( this.environment && this.environment.isTexture && this.environment.isRenderTargetTexture !== true ) {

				object.environment = this.environment.toJSON( meta ).uuid;

			}

		} else if ( this.isMesh || this.isLine || this.isPoints ) {

			object.geometry = serialize( meta.geometries, this.geometry );

			const parameters = this.geometry.parameters;

			if ( parameters !== undefined && parameters.shapes !== undefined ) {

				const shapes = parameters.shapes;

				if ( Array.isArray( shapes ) ) {

					for ( let i = 0, l = shapes.length; i < l; i ++ ) {

						const shape = shapes[ i ];

						serialize( meta.shapes, shape );

					}

				} else {

					serialize( meta.shapes, shapes );

				}

			}

		}

		if ( this.isSkinnedMesh ) {

			object.bindMode = this.bindMode;
			object.bindMatrix = this.bindMatrix.toArray();

			if ( this.skeleton !== undefined ) {

				serialize( meta.skeletons, this.skeleton );

				object.skeleton = this.skeleton.uuid;

			}

		}

		if ( this.material !== undefined ) {

			if ( Array.isArray( this.material ) ) {

				const uuids = [];

				for ( let i = 0, l = this.material.length; i < l; i ++ ) {

					uuids.push( serialize( meta.materials, this.material[ i ] ) );

				}

				object.material = uuids;

			} else {

				object.material = serialize( meta.materials, this.material );

			}

		}

		//

		if ( this.children.length > 0 ) {

			object.children = [];

			for ( let i = 0; i < this.children.length; i ++ ) {

				object.children.push( this.children[ i ].toJSON( meta ).object );

			}

		}

		//

		if ( this.animations.length > 0 ) {

			object.animations = [];

			for ( let i = 0; i < this.animations.length; i ++ ) {

				const animation = this.animations[ i ];

				object.animations.push( serialize( meta.animations, animation ) );

			}

		}

		if ( isRootObject ) {

			const geometries = extractFromCache( meta.geometries );
			const materials = extractFromCache( meta.materials );
			const textures = extractFromCache( meta.textures );
			const images = extractFromCache( meta.images );
			const shapes = extractFromCache( meta.shapes );
			const skeletons = extractFromCache( meta.skeletons );
			const animations = extractFromCache( meta.animations );
			const nodes = extractFromCache( meta.nodes );

			if ( geometries.length > 0 ) output.geometries = geometries;
			if ( materials.length > 0 ) output.materials = materials;
			if ( textures.length > 0 ) output.textures = textures;
			if ( images.length > 0 ) output.images = images;
			if ( shapes.length > 0 ) output.shapes = shapes;
			if ( skeletons.length > 0 ) output.skeletons = skeletons;
			if ( animations.length > 0 ) output.animations = animations;
			if ( nodes.length > 0 ) output.nodes = nodes;

		}

		output.object = object;

		return output;

		// extract data from the cache hash
		// remove metadata on each item
		// and return as array
		function extractFromCache( cache ) {

			const values = [];
			for ( const key in cache ) {

				const data = cache[ key ];
				delete data.metadata;
				values.push( data );

			}

			return values;

		}

	}

	/**
	 * Returns a new 3D object with copied values from this instance.
	 *
	 * @param {boolean} [recursive=true] - When set to `true`, descendants of the 3D object are also cloned.
	 * @return {Object3D} A clone of this instance.
	 */
	clone( recursive ) {

		return new this.constructor().copy( this, recursive );

	}

	/**
	 * Copies the values of the given 3D object to this instance.
	 *
	 * @param {Object3D} source - The 3D object to copy.
	 * @param {boolean} [recursive=true] - When set to `true`, descendants of the 3D object are cloned.
	 * @return {Object3D} A reference to this instance.
	 */
	copy( source, recursive = true ) {

		this.name = source.name;

		this.up.copy( source.up );

		this.position.copy( source.position );
		this.rotation.order = source.rotation.order;
		this.quaternion.copy( source.quaternion );
		this.scale.copy( source.scale );

		this.matrix.copy( source.matrix );
		this.matrixWorld.copy( source.matrixWorld );

		this.matrixAutoUpdate = source.matrixAutoUpdate;

		this.matrixWorldAutoUpdate = source.matrixWorldAutoUpdate;
		this.matrixWorldNeedsUpdate = source.matrixWorldNeedsUpdate;

		this.layers.mask = source.layers.mask;
		this.visible = source.visible;

		this.castShadow = source.castShadow;
		this.receiveShadow = source.receiveShadow;

		this.frustumCulled = source.frustumCulled;
		this.renderOrder = source.renderOrder;

		this.animations = source.animations.slice();

		this.userData = JSON.parse( JSON.stringify( source.userData ) );

		if ( recursive === true ) {

			for ( let i = 0; i < source.children.length; i ++ ) {

				const child = source.children[ i ];
				this.add( child.clone() );

			}

		}

		return this;

	}

}

/**
 * The default up direction for objects, also used as the default
 * position for {@link DirectionalLight} and {@link HemisphereLight}.
 *
 * @static
 * @type {Vector3}
 * @default (0,1,0)
 */
Object3D.DEFAULT_UP = /*@__PURE__*/ new Vector3( 0, 1, 0 );

/**
 * The default setting for {@link Object3D#matrixAutoUpdate} for
 * newly created 3D objects.
 *
 * @static
 * @type {boolean}
 * @default true
 */
Object3D.DEFAULT_MATRIX_AUTO_UPDATE = true;

/**
 * The default setting for {@link Object3D#matrixWorldAutoUpdate} for
 * newly created 3D objects.
 *
 * @static
 * @type {boolean}
 * @default true
 */
Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE = true;

let _id$1 = 0;

const _m1$2 = /*@__PURE__*/ new Matrix4();
const _obj = /*@__PURE__*/ new Object3D();
const _offset = /*@__PURE__*/ new Vector3();
const _box$1 = /*@__PURE__*/ new Box3();
const _boxMorphTargets = /*@__PURE__*/ new Box3();
const _vector$4 = /*@__PURE__*/ new Vector3();

/**
 * A representation of mesh, line, or point geometry. Includes vertex
 * positions, face indices, normals, colors, UVs, and custom attributes
 * within buffers, reducing the cost of passing all this data to the GPU.
 *
 * ```js
 * const geometry = new THREE.BufferGeometry();
 * // create a simple square shape. We duplicate the top left and bottom right
 * // vertices because each vertex needs to appear once per triangle.
 * const vertices = new Float32Array( [
 * 	-1.0, -1.0,  1.0, // v0
 * 	 1.0, -1.0,  1.0, // v1
 * 	 1.0,  1.0,  1.0, // v2
 *
 * 	 1.0,  1.0,  1.0, // v3
 * 	-1.0,  1.0,  1.0, // v4
 * 	-1.0, -1.0,  1.0  // v5
 * ] );
 * // itemSize = 3 because there are 3 values (components) per vertex
 * geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
 * const material = new THREE.MeshBasicMaterial( { color: 0xff0000 } );
 * const mesh = new THREE.Mesh( geometry, material );
 * ```
 *
 * @augments EventDispatcher
 */
class BufferGeometry extends EventDispatcher {

	/**
	 * Constructs a new geometry.
	 */
	constructor() {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isBufferGeometry = true;

		/**
		 * The ID of the geometry.
		 *
		 * @name BufferGeometry#id
		 * @type {number}
		 * @readonly
		 */
		Object.defineProperty( this, 'id', { value: _id$1 ++ } );

		/**
		 * The UUID of the geometry.
		 *
		 * @type {string}
		 * @readonly
		 */
		this.uuid = generateUUID();

		/**
		 * The name of the geometry.
		 *
		 * @type {string}
		 */
		this.name = '';
		this.type = 'BufferGeometry';

		/**
		 * Allows for vertices to be re-used across multiple triangles; this is
		 * called using "indexed triangles". Each triangle is associated with the
		 * indices of three vertices. This attribute therefore stores the index of
		 * each vertex for each triangular face. If this attribute is not set, the
		 * renderer assumes that each three contiguous positions represent a single triangle.
		 *
		 * @type {?BufferAttribute}
		 * @default null
		 */
		this.index = null;

		/**
		 * A (storage) buffer attribute which was generated with a compute shader and
		 * now defines indirect draw calls.
		 *
		 * Can only be used with {@link WebGPURenderer} and a WebGPU backend.
		 *
		 * @type {?BufferAttribute}
		 * @default null
		 */
		this.indirect = null;

		/**
		 * This dictionary has as id the name of the attribute to be set and as value
		 * the buffer attribute to set it to. Rather than accessing this property directly,
		 * use `setAttribute()` and `getAttribute()` to access attributes of this geometry.
		 *
		 * @type {Object<string,(BufferAttribute|InterleavedBufferAttribute)>}
		 */
		this.attributes = {};

		/**
		 * This dictionary holds the morph targets of the geometry.
		 *
		 * Note: Once the geometry has been rendered, the morph attribute data cannot
		 * be changed. You will have to call `dispose()?, and create a new geometry instance.
		 *
		 * @type {Object}
		 */
		this.morphAttributes = {};

		/**
		 * Used to control the morph target behavior; when set to `true`, the morph
		 * target data is treated as relative offsets, rather than as absolute
		 * positions/normals.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.morphTargetsRelative = false;

		/**
		 * Split the geometry into groups, each of which will be rendered in a
		 * separate draw call. This allows an array of materials to be used with the geometry.
		 *
		 * Use `addGroup()` and `clearGroups()` to edit groups, rather than modifying this array directly.
		 *
		 * Every vertex and index must belong to exactly one group — groups must not share vertices or
		 * indices, and must not leave vertices or indices unused.
		 *
		 * @type {Array<Object>}
		 */
		this.groups = [];

		/**
		 * Bounding box for the geometry which can be calculated with `computeBoundingBox()`.
		 *
		 * @type {?Box3}
		 * @default null
		 */
		this.boundingBox = null;

		/**
		 * Bounding sphere for the geometry which can be calculated with `computeBoundingSphere()`.
		 *
		 * @type {?Sphere}
		 * @default null
		 */
		this.boundingSphere = null;

		/**
		 * Determines the part of the geometry to render. This should not be set directly,
		 * instead use `setDrawRange()`.
		 *
		 * @type {{start:number,count:number}}
		 */
		this.drawRange = { start: 0, count: Infinity };

		/**
		 * An object that can be used to store custom data about the geometry.
		 * It should not hold references to functions as these will not be cloned.
		 *
		 * @type {Object}
		 */
		this.userData = {};

	}

	/**
	 * Returns the index of this geometry.
	 *
	 * @return {?BufferAttribute} The index. Returns `null` if no index is defined.
	 */
	getIndex() {

		return this.index;

	}

	/**
	 * Sets the given index to this geometry.
	 *
	 * @param {Array<number>|BufferAttribute} index - The index to set.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	setIndex( index ) {

		if ( Array.isArray( index ) ) {

			this.index = new ( arrayNeedsUint32( index ) ? Uint32BufferAttribute : Uint16BufferAttribute )( index, 1 );

		} else {

			this.index = index;

		}

		return this;

	}

	/**
	 * Sets the given indirect attribute to this geometry.
	 *
	 * @param {BufferAttribute} indirect - The attribute holding indirect draw calls.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	setIndirect( indirect ) {

		this.indirect = indirect;

		return this;

	}

	/**
	 * Returns the indirect attribute of this geometry.
	 *
	 * @return {?BufferAttribute} The indirect attribute. Returns `null` if no indirect attribute is defined.
	 */
	getIndirect() {

		return this.indirect;

	}

	/**
	 * Returns the buffer attribute for the given name.
	 *
	 * @param {string} name - The attribute name.
	 * @return {BufferAttribute|InterleavedBufferAttribute|undefined} The buffer attribute.
	 * Returns `undefined` if not attribute has been found.
	 */
	getAttribute( name ) {

		return this.attributes[ name ];

	}

	/**
	 * Sets the given attribute for the given name.
	 *
	 * @param {string} name - The attribute name.
	 * @param {BufferAttribute|InterleavedBufferAttribute} attribute - The attribute to set.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	setAttribute( name, attribute ) {

		this.attributes[ name ] = attribute;

		return this;

	}

	/**
	 * Deletes the attribute for the given name.
	 *
	 * @param {string} name - The attribute name to delete.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	deleteAttribute( name ) {

		delete this.attributes[ name ];

		return this;

	}

	/**
	 * Returns `true` if this geometry has an attribute for the given name.
	 *
	 * @param {string} name - The attribute name.
	 * @return {boolean} Whether this geometry has an attribute for the given name or not.
	 */
	hasAttribute( name ) {

		return this.attributes[ name ] !== undefined;

	}

	/**
	 * Adds a group to this geometry.
	 *
	 * @param {number} start - The first element in this draw call. That is the first
	 * vertex for non-indexed geometry, otherwise the first triangle index.
	 * @param {number} count - Specifies how many vertices (or indices) are part of this group.
	 * @param {number} [materialIndex=0] - The material array index to use.
	 */
	addGroup( start, count, materialIndex = 0 ) {

		this.groups.push( {

			start: start,
			count: count,
			materialIndex: materialIndex

		} );

	}

	/**
	 * Clears all groups.
	 */
	clearGroups() {

		this.groups = [];

	}

	/**
	 * Sets the draw range for this geometry.
	 *
	 * @param {number} start - The first vertex for non-indexed geometry, otherwise the first triangle index.
	 * @param {number} count - For non-indexed BufferGeometry, `count` is the number of vertices to render.
	 * For indexed BufferGeometry, `count` is the number of indices to render.
	 */
	setDrawRange( start, count ) {

		this.drawRange.start = start;
		this.drawRange.count = count;

	}

	/**
	 * Applies the given 4x4 transformation matrix to the geometry.
	 *
	 * @param {Matrix4} matrix - The matrix to apply.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	applyMatrix4( matrix ) {

		const position = this.attributes.position;

		if ( position !== undefined ) {

			position.applyMatrix4( matrix );

			position.needsUpdate = true;

		}

		const normal = this.attributes.normal;

		if ( normal !== undefined ) {

			const normalMatrix = new Matrix3().getNormalMatrix( matrix );

			normal.applyNormalMatrix( normalMatrix );

			normal.needsUpdate = true;

		}

		const tangent = this.attributes.tangent;

		if ( tangent !== undefined ) {

			tangent.transformDirection( matrix );

			tangent.needsUpdate = true;

		}

		if ( this.boundingBox !== null ) {

			this.computeBoundingBox();

		}

		if ( this.boundingSphere !== null ) {

			this.computeBoundingSphere();

		}

		return this;

	}

	/**
	 * Applies the rotation represented by the Quaternion to the geometry.
	 *
	 * @param {Quaternion} q - The Quaternion to apply.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	applyQuaternion( q ) {

		_m1$2.makeRotationFromQuaternion( q );

		this.applyMatrix4( _m1$2 );

		return this;

	}

	/**
	 * Rotates the geometry about the X axis. This is typically done as a one time
	 * operation, and not during a loop. Use {@link Object3D#rotation} for typical
	 * real-time mesh rotation.
	 *
	 * @param {number} angle - The angle in radians.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	rotateX( angle ) {

		// rotate geometry around world x-axis

		_m1$2.makeRotationX( angle );

		this.applyMatrix4( _m1$2 );

		return this;

	}

	/**
	 * Rotates the geometry about the Y axis. This is typically done as a one time
	 * operation, and not during a loop. Use {@link Object3D#rotation} for typical
	 * real-time mesh rotation.
	 *
	 * @param {number} angle - The angle in radians.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	rotateY( angle ) {

		// rotate geometry around world y-axis

		_m1$2.makeRotationY( angle );

		this.applyMatrix4( _m1$2 );

		return this;

	}

	/**
	 * Rotates the geometry about the Z axis. This is typically done as a one time
	 * operation, and not during a loop. Use {@link Object3D#rotation} for typical
	 * real-time mesh rotation.
	 *
	 * @param {number} angle - The angle in radians.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	rotateZ( angle ) {

		// rotate geometry around world z-axis

		_m1$2.makeRotationZ( angle );

		this.applyMatrix4( _m1$2 );

		return this;

	}

	/**
	 * Translates the geometry. This is typically done as a one time
	 * operation, and not during a loop. Use {@link Object3D#position} for typical
	 * real-time mesh rotation.
	 *
	 * @param {number} x - The x offset.
	 * @param {number} y - The y offset.
	 * @param {number} z - The z offset.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	translate( x, y, z ) {

		// translate geometry

		_m1$2.makeTranslation( x, y, z );

		this.applyMatrix4( _m1$2 );

		return this;

	}

	/**
	 * Scales the geometry. This is typically done as a one time
	 * operation, and not during a loop. Use {@link Object3D#scale} for typical
	 * real-time mesh rotation.
	 *
	 * @param {number} x - The x scale.
	 * @param {number} y - The y scale.
	 * @param {number} z - The z scale.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	scale( x, y, z ) {

		// scale geometry

		_m1$2.makeScale( x, y, z );

		this.applyMatrix4( _m1$2 );

		return this;

	}

	/**
	 * Rotates the geometry to face a point in 3D space. This is typically done as a one time
	 * operation, and not during a loop. Use {@link Object3D#lookAt} for typical
	 * real-time mesh rotation.
	 *
	 * @param {Vector3} vector - The target point.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	lookAt( vector ) {

		_obj.lookAt( vector );

		_obj.updateMatrix();

		this.applyMatrix4( _obj.matrix );

		return this;

	}

	/**
	 * Center the geometry based on its bounding box.
	 *
	 * @return {BufferGeometry} A reference to this instance.
	 */
	center() {

		this.computeBoundingBox();

		this.boundingBox.getCenter( _offset ).negate();

		this.translate( _offset.x, _offset.y, _offset.z );

		return this;

	}

	/**
	 * Defines a geometry by creating a `position` attribute based on the given array of points. The array
	 * can hold 2D or 3D vectors. When using two-dimensional data, the `z` coordinate for all vertices is
	 * set to `0`.
	 *
	 * If the method is used with an existing `position` attribute, the vertex data are overwritten with the
	 * data from the array. The length of the array must match the vertex count.
	 *
	 * @param {Array<Vector2>|Array<Vector3>} points - The points.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	setFromPoints( points ) {

		const positionAttribute = this.getAttribute( 'position' );

		if ( positionAttribute === undefined ) {

			const position = [];

			for ( let i = 0, l = points.length; i < l; i ++ ) {

				const point = points[ i ];
				position.push( point.x, point.y, point.z || 0 );

			}

			this.setAttribute( 'position', new Float32BufferAttribute( position, 3 ) );

		} else {

			const l = Math.min( points.length, positionAttribute.count ); // make sure data do not exceed buffer size

			for ( let i = 0; i < l; i ++ ) {

				const point = points[ i ];
				positionAttribute.setXYZ( i, point.x, point.y, point.z || 0 );

			}

			if ( points.length > positionAttribute.count ) {

				console.warn( 'THREE.BufferGeometry: Buffer size too small for points data. Use .dispose() and create a new geometry.' );

			}

			positionAttribute.needsUpdate = true;

		}

		return this;

	}

	/**
	 * Computes the bounding box of the geometry, and updates the `boundingBox` member.
	 * The bounding box is not computed by the engine; it must be computed by your app.
	 * You may need to recompute the bounding box if the geometry vertices are modified.
	 */
	computeBoundingBox() {

		if ( this.boundingBox === null ) {

			this.boundingBox = new Box3();

		}

		const position = this.attributes.position;
		const morphAttributesPosition = this.morphAttributes.position;

		if ( position && position.isGLBufferAttribute ) {

			console.error( 'THREE.BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box.', this );

			this.boundingBox.set(
				new Vector3( - Infinity, - Infinity, - Infinity ),
				new Vector3( + Infinity, + Infinity, + Infinity )
			);

			return;

		}

		if ( position !== undefined ) {

			this.boundingBox.setFromBufferAttribute( position );

			// process morph attributes if present

			if ( morphAttributesPosition ) {

				for ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {

					const morphAttribute = morphAttributesPosition[ i ];
					_box$1.setFromBufferAttribute( morphAttribute );

					if ( this.morphTargetsRelative ) {

						_vector$4.addVectors( this.boundingBox.min, _box$1.min );
						this.boundingBox.expandByPoint( _vector$4 );

						_vector$4.addVectors( this.boundingBox.max, _box$1.max );
						this.boundingBox.expandByPoint( _vector$4 );

					} else {

						this.boundingBox.expandByPoint( _box$1.min );
						this.boundingBox.expandByPoint( _box$1.max );

					}

				}

			}

		} else {

			this.boundingBox.makeEmpty();

		}

		if ( isNaN( this.boundingBox.min.x ) || isNaN( this.boundingBox.min.y ) || isNaN( this.boundingBox.min.z ) ) {

			console.error( 'THREE.BufferGeometry.computeBoundingBox(): Computed min/max have NaN values. The "position" attribute is likely to have NaN values.', this );

		}

	}

	/**
	 * Computes the bounding sphere of the geometry, and updates the `boundingSphere` member.
	 * The engine automatically computes the bounding sphere when it is needed, e.g., for ray casting or view frustum culling.
	 * You may need to recompute the bounding sphere if the geometry vertices are modified.
	 */
	computeBoundingSphere() {

		if ( this.boundingSphere === null ) {

			this.boundingSphere = new Sphere();

		}

		const position = this.attributes.position;
		const morphAttributesPosition = this.morphAttributes.position;

		if ( position && position.isGLBufferAttribute ) {

			console.error( 'THREE.BufferGeometry.computeBoundingSphere(): GLBufferAttribute requires a manual bounding sphere.', this );

			this.boundingSphere.set( new Vector3(), Infinity );

			return;

		}

		if ( position ) {

			// first, find the center of the bounding sphere

			const center = this.boundingSphere.center;

			_box$1.setFromBufferAttribute( position );

			// process morph attributes if present

			if ( morphAttributesPosition ) {

				for ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {

					const morphAttribute = morphAttributesPosition[ i ];
					_boxMorphTargets.setFromBufferAttribute( morphAttribute );

					if ( this.morphTargetsRelative ) {

						_vector$4.addVectors( _box$1.min, _boxMorphTargets.min );
						_box$1.expandByPoint( _vector$4 );

						_vector$4.addVectors( _box$1.max, _boxMorphTargets.max );
						_box$1.expandByPoint( _vector$4 );

					} else {

						_box$1.expandByPoint( _boxMorphTargets.min );
						_box$1.expandByPoint( _boxMorphTargets.max );

					}

				}

			}

			_box$1.getCenter( center );

			// second, try to find a boundingSphere with a radius smaller than the
			// boundingSphere of the boundingBox: sqrt(3) smaller in the best case

			let maxRadiusSq = 0;

			for ( let i = 0, il = position.count; i < il; i ++ ) {

				_vector$4.fromBufferAttribute( position, i );

				maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector$4 ) );

			}

			// process morph attributes if present

			if ( morphAttributesPosition ) {

				for ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {

					const morphAttribute = morphAttributesPosition[ i ];
					const morphTargetsRelative = this.morphTargetsRelative;

					for ( let j = 0, jl = morphAttribute.count; j < jl; j ++ ) {

						_vector$4.fromBufferAttribute( morphAttribute, j );

						if ( morphTargetsRelative ) {

							_offset.fromBufferAttribute( position, j );
							_vector$4.add( _offset );

						}

						maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector$4 ) );

					}

				}

			}

			this.boundingSphere.radius = Math.sqrt( maxRadiusSq );

			if ( isNaN( this.boundingSphere.radius ) ) {

				console.error( 'THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values.', this );

			}

		}

	}

	/**
	 * Calculates and adds a tangent attribute to this geometry.
	 *
	 * The computation is only supported for indexed geometries and if position, normal, and uv attributes
	 * are defined. When using a tangent space normal map, prefer the MikkTSpace algorithm provided by
	 * {@link BufferGeometryUtils#computeMikkTSpaceTangents} instead.
	 */
	computeTangents() {

		const index = this.index;
		const attributes = this.attributes;

		// based on http://www.terathon.com/code/tangent.html
		// (per vertex tangents)

		if ( index === null ||
			 attributes.position === undefined ||
			 attributes.normal === undefined ||
			 attributes.uv === undefined ) {

			console.error( 'THREE.BufferGeometry: .computeTangents() failed. Missing required attributes (index, position, normal or uv)' );
			return;

		}

		const positionAttribute = attributes.position;
		const normalAttribute = attributes.normal;
		const uvAttribute = attributes.uv;

		if ( this.hasAttribute( 'tangent' ) === false ) {

			this.setAttribute( 'tangent', new BufferAttribute( new Float32Array( 4 * positionAttribute.count ), 4 ) );

		}

		const tangentAttribute = this.getAttribute( 'tangent' );

		const tan1 = [], tan2 = [];

		for ( let i = 0; i < positionAttribute.count; i ++ ) {

			tan1[ i ] = new Vector3();
			tan2[ i ] = new Vector3();

		}

		const vA = new Vector3(),
			vB = new Vector3(),
			vC = new Vector3(),

			uvA = new Vector2(),
			uvB = new Vector2(),
			uvC = new Vector2(),

			sdir = new Vector3(),
			tdir = new Vector3();

		function handleTriangle( a, b, c ) {

			vA.fromBufferAttribute( positionAttribute, a );
			vB.fromBufferAttribute( positionAttribute, b );
			vC.fromBufferAttribute( positionAttribute, c );

			uvA.fromBufferAttribute( uvAttribute, a );
			uvB.fromBufferAttribute( uvAttribute, b );
			uvC.fromBufferAttribute( uvAttribute, c );

			vB.sub( vA );
			vC.sub( vA );

			uvB.sub( uvA );
			uvC.sub( uvA );

			const r = 1.0 / ( uvB.x * uvC.y - uvC.x * uvB.y );

			// silently ignore degenerate uv triangles having coincident or colinear vertices

			if ( ! isFinite( r ) ) return;

			sdir.copy( vB ).multiplyScalar( uvC.y ).addScaledVector( vC, - uvB.y ).multiplyScalar( r );
			tdir.copy( vC ).multiplyScalar( uvB.x ).addScaledVector( vB, - uvC.x ).multiplyScalar( r );

			tan1[ a ].add( sdir );
			tan1[ b ].add( sdir );
			tan1[ c ].add( sdir );

			tan2[ a ].add( tdir );
			tan2[ b ].add( tdir );
			tan2[ c ].add( tdir );

		}

		let groups = this.groups;

		if ( groups.length === 0 ) {

			groups = [ {
				start: 0,
				count: index.count
			} ];

		}

		for ( let i = 0, il = groups.length; i < il; ++ i ) {

			const group = groups[ i ];

			const start = group.start;
			const count = group.count;

			for ( let j = start, jl = start + count; j < jl; j += 3 ) {

				handleTriangle(
					index.getX( j + 0 ),
					index.getX( j + 1 ),
					index.getX( j + 2 )
				);

			}

		}

		const tmp = new Vector3(), tmp2 = new Vector3();
		const n = new Vector3(), n2 = new Vector3();

		function handleVertex( v ) {

			n.fromBufferAttribute( normalAttribute, v );
			n2.copy( n );

			const t = tan1[ v ];

			// Gram-Schmidt orthogonalize

			tmp.copy( t );
			tmp.sub( n.multiplyScalar( n.dot( t ) ) ).normalize();

			// Calculate handedness

			tmp2.crossVectors( n2, t );
			const test = tmp2.dot( tan2[ v ] );
			const w = ( test < 0.0 ) ? -1 : 1.0;

			tangentAttribute.setXYZW( v, tmp.x, tmp.y, tmp.z, w );

		}

		for ( let i = 0, il = groups.length; i < il; ++ i ) {

			const group = groups[ i ];

			const start = group.start;
			const count = group.count;

			for ( let j = start, jl = start + count; j < jl; j += 3 ) {

				handleVertex( index.getX( j + 0 ) );
				handleVertex( index.getX( j + 1 ) );
				handleVertex( index.getX( j + 2 ) );

			}

		}

	}

	/**
	 * Computes vertex normals for the given vertex data. For indexed geometries, the method sets
	 * each vertex normal to be the average of the face normals of the faces that share that vertex.
	 * For non-indexed geometries, vertices are not shared, and the method sets each vertex normal
	 * to be the same as the face normal.
	 */
	computeVertexNormals() {

		const index = this.index;
		const positionAttribute = this.getAttribute( 'position' );

		if ( positionAttribute !== undefined ) {

			let normalAttribute = this.getAttribute( 'normal' );

			if ( normalAttribute === undefined ) {

				normalAttribute = new BufferAttribute( new Float32Array( positionAttribute.count * 3 ), 3 );
				this.setAttribute( 'normal', normalAttribute );

			} else {

				// reset existing normals to zero

				for ( let i = 0, il = normalAttribute.count; i < il; i ++ ) {

					normalAttribute.setXYZ( i, 0, 0, 0 );

				}

			}

			const pA = new Vector3(), pB = new Vector3(), pC = new Vector3();
			const nA = new Vector3(), nB = new Vector3(), nC = new Vector3();
			const cb = new Vector3(), ab = new Vector3();

			// indexed elements

			if ( index ) {

				for ( let i = 0, il = index.count; i < il; i += 3 ) {

					const vA = index.getX( i + 0 );
					const vB = index.getX( i + 1 );
					const vC = index.getX( i + 2 );

					pA.fromBufferAttribute( positionAttribute, vA );
					pB.fromBufferAttribute( positionAttribute, vB );
					pC.fromBufferAttribute( positionAttribute, vC );

					cb.subVectors( pC, pB );
					ab.subVectors( pA, pB );
					cb.cross( ab );

					nA.fromBufferAttribute( normalAttribute, vA );
					nB.fromBufferAttribute( normalAttribute, vB );
					nC.fromBufferAttribute( normalAttribute, vC );

					nA.add( cb );
					nB.add( cb );
					nC.add( cb );

					normalAttribute.setXYZ( vA, nA.x, nA.y, nA.z );
					normalAttribute.setXYZ( vB, nB.x, nB.y, nB.z );
					normalAttribute.setXYZ( vC, nC.x, nC.y, nC.z );

				}

			} else {

				// non-indexed elements (unconnected triangle soup)

				for ( let i = 0, il = positionAttribute.count; i < il; i += 3 ) {

					pA.fromBufferAttribute( positionAttribute, i + 0 );
					pB.fromBufferAttribute( positionAttribute, i + 1 );
					pC.fromBufferAttribute( positionAttribute, i + 2 );

					cb.subVectors( pC, pB );
					ab.subVectors( pA, pB );
					cb.cross( ab );

					normalAttribute.setXYZ( i + 0, cb.x, cb.y, cb.z );
					normalAttribute.setXYZ( i + 1, cb.x, cb.y, cb.z );
					normalAttribute.setXYZ( i + 2, cb.x, cb.y, cb.z );

				}

			}

			this.normalizeNormals();

			normalAttribute.needsUpdate = true;

		}

	}

	/**
	 * Ensures every normal vector in a geometry will have a magnitude of `1`. This will
	 * correct lighting on the geometry surfaces.
	 */
	normalizeNormals() {

		const normals = this.attributes.normal;

		for ( let i = 0, il = normals.count; i < il; i ++ ) {

			_vector$4.fromBufferAttribute( normals, i );

			_vector$4.normalize();

			normals.setXYZ( i, _vector$4.x, _vector$4.y, _vector$4.z );

		}

	}

	/**
	 * Return a new non-index version of this indexed geometry. If the geometry
	 * is already non-indexed, the method is a NOOP.
	 *
	 * @return {BufferGeometry} The non-indexed version of this indexed geometry.
	 */
	toNonIndexed() {

		function convertBufferAttribute( attribute, indices ) {

			const array = attribute.array;
			const itemSize = attribute.itemSize;
			const normalized = attribute.normalized;

			const array2 = new array.constructor( indices.length * itemSize );

			let index = 0, index2 = 0;

			for ( let i = 0, l = indices.length; i < l; i ++ ) {

				if ( attribute.isInterleavedBufferAttribute ) {

					index = indices[ i ] * attribute.data.stride + attribute.offset;

				} else {

					index = indices[ i ] * itemSize;

				}

				for ( let j = 0; j < itemSize; j ++ ) {

					array2[ index2 ++ ] = array[ index ++ ];

				}

			}

			return new BufferAttribute( array2, itemSize, normalized );

		}

		//

		if ( this.index === null ) {

			console.warn( 'THREE.BufferGeometry.toNonIndexed(): BufferGeometry is already non-indexed.' );
			return this;

		}

		const geometry2 = new BufferGeometry();

		const indices = this.index.array;
		const attributes = this.attributes;

		// attributes

		for ( const name in attributes ) {

			const attribute = attributes[ name ];

			const newAttribute = convertBufferAttribute( attribute, indices );

			geometry2.setAttribute( name, newAttribute );

		}

		// morph attributes

		const morphAttributes = this.morphAttributes;

		for ( const name in morphAttributes ) {

			const morphArray = [];
			const morphAttribute = morphAttributes[ name ]; // morphAttribute: array of Float32BufferAttributes

			for ( let i = 0, il = morphAttribute.length; i < il; i ++ ) {

				const attribute = morphAttribute[ i ];

				const newAttribute = convertBufferAttribute( attribute, indices );

				morphArray.push( newAttribute );

			}

			geometry2.morphAttributes[ name ] = morphArray;

		}

		geometry2.morphTargetsRelative = this.morphTargetsRelative;

		// groups

		const groups = this.groups;

		for ( let i = 0, l = groups.length; i < l; i ++ ) {

			const group = groups[ i ];
			geometry2.addGroup( group.start, group.count, group.materialIndex );

		}

		return geometry2;

	}

	/**
	 * Serializes the geometry into JSON.
	 *
	 * @return {Object} A JSON object representing the serialized geometry.
	 */
	toJSON() {

		const data = {
			metadata: {
				version: 4.7,
				type: 'BufferGeometry',
				generator: 'BufferGeometry.toJSON'
			}
		};

		// standard BufferGeometry serialization

		data.uuid = this.uuid;
		data.type = this.type;
		if ( this.name !== '' ) data.name = this.name;
		if ( Object.keys( this.userData ).length > 0 ) data.userData = this.userData;

		if ( this.parameters !== undefined ) {

			const parameters = this.parameters;

			for ( const key in parameters ) {

				if ( parameters[ key ] !== undefined ) data[ key ] = parameters[ key ];

			}

			return data;

		}

		// for simplicity the code assumes attributes are not shared across geometries, see #15811

		data.data = { attributes: {} };

		const index = this.index;

		if ( index !== null ) {

			data.data.index = {
				type: index.array.constructor.name,
				array: Array.prototype.slice.call( index.array )
			};

		}

		const attributes = this.attributes;

		for ( const key in attributes ) {

			const attribute = attributes[ key ];

			data.data.attributes[ key ] = attribute.toJSON( data.data );

		}

		const morphAttributes = {};
		let hasMorphAttributes = false;

		for ( const key in this.morphAttributes ) {

			const attributeArray = this.morphAttributes[ key ];

			const array = [];

			for ( let i = 0, il = attributeArray.length; i < il; i ++ ) {

				const attribute = attributeArray[ i ];

				array.push( attribute.toJSON( data.data ) );

			}

			if ( array.length > 0 ) {

				morphAttributes[ key ] = array;

				hasMorphAttributes = true;

			}

		}

		if ( hasMorphAttributes ) {

			data.data.morphAttributes = morphAttributes;
			data.data.morphTargetsRelative = this.morphTargetsRelative;

		}

		const groups = this.groups;

		if ( groups.length > 0 ) {

			data.data.groups = JSON.parse( JSON.stringify( groups ) );

		}

		const boundingSphere = this.boundingSphere;

		if ( boundingSphere !== null ) {

			data.data.boundingSphere = boundingSphere.toJSON();

		}

		return data;

	}

	/**
	 * Returns a new geometry with copied values from this instance.
	 *
	 * @return {BufferGeometry} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

	/**
	 * Copies the values of the given geometry to this instance.
	 *
	 * @param {BufferGeometry} source - The geometry to copy.
	 * @return {BufferGeometry} A reference to this instance.
	 */
	copy( source ) {

		// reset

		this.index = null;
		this.attributes = {};
		this.morphAttributes = {};
		this.groups = [];
		this.boundingBox = null;
		this.boundingSphere = null;

		// used for storing cloned, shared data

		const data = {};

		// name

		this.name = source.name;

		// index

		const index = source.index;

		if ( index !== null ) {

			this.setIndex( index.clone() );

		}

		// attributes

		const attributes = source.attributes;

		for ( const name in attributes ) {

			const attribute = attributes[ name ];
			this.setAttribute( name, attribute.clone( data ) );

		}

		// morph attributes

		const morphAttributes = source.morphAttributes;

		for ( const name in morphAttributes ) {

			const array = [];
			const morphAttribute = morphAttributes[ name ]; // morphAttribute: array of Float32BufferAttributes

			for ( let i = 0, l = morphAttribute.length; i < l; i ++ ) {

				array.push( morphAttribute[ i ].clone( data ) );

			}

			this.morphAttributes[ name ] = array;

		}

		this.morphTargetsRelative = source.morphTargetsRelative;

		// groups

		const groups = source.groups;

		for ( let i = 0, l = groups.length; i < l; i ++ ) {

			const group = groups[ i ];
			this.addGroup( group.start, group.count, group.materialIndex );

		}

		// bounding box

		const boundingBox = source.boundingBox;

		if ( boundingBox !== null ) {

			this.boundingBox = boundingBox.clone();

		}

		// bounding sphere

		const boundingSphere = source.boundingSphere;

		if ( boundingSphere !== null ) {

			this.boundingSphere = boundingSphere.clone();

		}

		// draw range

		this.drawRange.start = source.drawRange.start;
		this.drawRange.count = source.drawRange.count;

		// user data

		this.userData = source.userData;

		return this;

	}

	/**
	 * Frees the GPU-related resources allocated by this instance. Call this
	 * method whenever this instance is no longer used in your app.
	 *
	 * @fires BufferGeometry#dispose
	 */
	dispose() {

		this.dispatchEvent( { type: 'dispose' } );

	}

}

/**
 * A geometry class for a rectangular cuboid with a given width, height, and depth.
 * On creation, the cuboid is centred on the origin, with each edge parallel to one
 * of the axes.
 *
 * ```js
 * const geometry = new THREE.BoxGeometry( 1, 1, 1 );
 * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
 * const cube = new THREE.Mesh( geometry, material );
 * scene.add( cube );
 * ```
 *
 * @augments BufferGeometry
 */
class BoxGeometry extends BufferGeometry {

	/**
	 * Constructs a new box geometry.
	 *
	 * @param {number} [width=1] - The width. That is, the length of the edges parallel to the X axis.
	 * @param {number} [height=1] - The height. That is, the length of the edges parallel to the Y axis.
	 * @param {number} [depth=1] - The depth. That is, the length of the edges parallel to the Z axis.
	 * @param {number} [widthSegments=1] - Number of segmented rectangular faces along the width of the sides.
	 * @param {number} [heightSegments=1] - Number of segmented rectangular faces along the height of the sides.
	 * @param {number} [depthSegments=1] - Number of segmented rectangular faces along the depth of the sides.
	 */
	constructor( width = 1, height = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1 ) {

		super();

		this.type = 'BoxGeometry';

		/**
		 * Holds the constructor parameters that have been
		 * used to generate the geometry. Any modification
		 * after instantiation does not change the geometry.
		 *
		 * @type {Object}
		 */
		this.parameters = {
			width: width,
			height: height,
			depth: depth,
			widthSegments: widthSegments,
			heightSegments: heightSegments,
			depthSegments: depthSegments
		};

		const scope = this;

		// segments

		widthSegments = Math.floor( widthSegments );
		heightSegments = Math.floor( heightSegments );
		depthSegments = Math.floor( depthSegments );

		// buffers

		const indices = [];
		const vertices = [];
		const normals = [];
		const uvs = [];

		// helper variables

		let numberOfVertices = 0;
		let groupStart = 0;

		// build each side of the box geometry

		buildPlane( 'z', 'y', 'x', -1, -1, depth, height, width, depthSegments, heightSegments, 0 ); // px
		buildPlane( 'z', 'y', 'x', 1, -1, depth, height, - width, depthSegments, heightSegments, 1 ); // nx
		buildPlane( 'x', 'z', 'y', 1, 1, width, depth, height, widthSegments, depthSegments, 2 ); // py
		buildPlane( 'x', 'z', 'y', 1, -1, width, depth, - height, widthSegments, depthSegments, 3 ); // ny
		buildPlane( 'x', 'y', 'z', 1, -1, width, height, depth, widthSegments, heightSegments, 4 ); // pz
		buildPlane( 'x', 'y', 'z', -1, -1, width, height, - depth, widthSegments, heightSegments, 5 ); // nz

		// build geometry

		this.setIndex( indices );
		this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
		this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
		this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

		function buildPlane( u, v, w, udir, vdir, width, height, depth, gridX, gridY, materialIndex ) {

			const segmentWidth = width / gridX;
			const segmentHeight = height / gridY;

			const widthHalf = width / 2;
			const heightHalf = height / 2;
			const depthHalf = depth / 2;

			const gridX1 = gridX + 1;
			const gridY1 = gridY + 1;

			let vertexCounter = 0;
			let groupCount = 0;

			const vector = new Vector3();

			// generate vertices, normals and uvs

			for ( let iy = 0; iy < gridY1; iy ++ ) {

				const y = iy * segmentHeight - heightHalf;

				for ( let ix = 0; ix < gridX1; ix ++ ) {

					const x = ix * segmentWidth - widthHalf;

					// set values to correct vector component

					vector[ u ] = x * udir;
					vector[ v ] = y * vdir;
					vector[ w ] = depthHalf;

					// now apply vector to vertex buffer

					vertices.push( vector.x, vector.y, vector.z );

					// set values to correct vector component

					vector[ u ] = 0;
					vector[ v ] = 0;
					vector[ w ] = depth > 0 ? 1 : -1;

					// now apply vector to normal buffer

					normals.push( vector.x, vector.y, vector.z );

					// uvs

					uvs.push( ix / gridX );
					uvs.push( 1 - ( iy / gridY ) );

					// counters

					vertexCounter += 1;

				}

			}

			// indices

			// 1. you need three indices to draw a single face
			// 2. a single segment consists of two faces
			// 3. so we need to generate six (2*3) indices per segment

			for ( let iy = 0; iy < gridY; iy ++ ) {

				for ( let ix = 0; ix < gridX; ix ++ ) {

					const a = numberOfVertices + ix + gridX1 * iy;
					const b = numberOfVertices + ix + gridX1 * ( iy + 1 );
					const c = numberOfVertices + ( ix + 1 ) + gridX1 * ( iy + 1 );
					const d = numberOfVertices + ( ix + 1 ) + gridX1 * iy;

					// faces

					indices.push( a, b, d );
					indices.push( b, c, d );

					// increase counter

					groupCount += 6;

				}

			}

			// add a group to the geometry. this will ensure multi material support

			scope.addGroup( groupStart, groupCount, materialIndex );

			// calculate new start value for groups

			groupStart += groupCount;

			// update total number of vertices

			numberOfVertices += vertexCounter;

		}

	}

	copy( source ) {

		super.copy( source );

		this.parameters = Object.assign( {}, source.parameters );

		return this;

	}

	/**
	 * Factory method for creating an instance of this class from the given
	 * JSON object.
	 *
	 * @param {Object} data - A JSON object representing the serialized geometry.
	 * @return {BoxGeometry} A new instance.
	 */
	static fromJSON( data ) {

		return new BoxGeometry( data.width, data.height, data.depth, data.widthSegments, data.heightSegments, data.depthSegments );

	}

}

/**
 * A geometry class for representing a plane.
 *
 * ```js
 * const geometry = new THREE.PlaneGeometry( 1, 1 );
 * const material = new THREE.MeshBasicMaterial( { color: 0xffff00, side: THREE.DoubleSide } );
 * const plane = new THREE.Mesh( geometry, material );
 * scene.add( plane );
 * ```
 *
 * @augments BufferGeometry
 */
class PlaneGeometry extends BufferGeometry {

	/**
	 * Constructs a new plane geometry.
	 *
	 * @param {number} [width=1] - The width along the X axis.
	 * @param {number} [height=1] - The height along the Y axis
	 * @param {number} [widthSegments=1] - The number of segments along the X axis.
	 * @param {number} [heightSegments=1] - The number of segments along the Y axis.
	 */
	constructor( width = 1, height = 1, widthSegments = 1, heightSegments = 1 ) {

		super();

		this.type = 'PlaneGeometry';

		/**
		 * Holds the constructor parameters that have been
		 * used to generate the geometry. Any modification
		 * after instantiation does not change the geometry.
		 *
		 * @type {Object}
		 */
		this.parameters = {
			width: width,
			height: height,
			widthSegments: widthSegments,
			heightSegments: heightSegments
		};

		const width_half = width / 2;
		const height_half = height / 2;

		const gridX = Math.floor( widthSegments );
		const gridY = Math.floor( heightSegments );

		const gridX1 = gridX + 1;
		const gridY1 = gridY + 1;

		const segment_width = width / gridX;
		const segment_height = height / gridY;

		//

		const indices = [];
		const vertices = [];
		const normals = [];
		const uvs = [];

		for ( let iy = 0; iy < gridY1; iy ++ ) {

			const y = iy * segment_height - height_half;

			for ( let ix = 0; ix < gridX1; ix ++ ) {

				const x = ix * segment_width - width_half;

				vertices.push( x, - y, 0 );

				normals.push( 0, 0, 1 );

				uvs.push( ix / gridX );
				uvs.push( 1 - ( iy / gridY ) );

			}

		}

		for ( let iy = 0; iy < gridY; iy ++ ) {

			for ( let ix = 0; ix < gridX; ix ++ ) {

				const a = ix + gridX1 * iy;
				const b = ix + gridX1 * ( iy + 1 );
				const c = ( ix + 1 ) + gridX1 * ( iy + 1 );
				const d = ( ix + 1 ) + gridX1 * iy;

				indices.push( a, b, d );
				indices.push( b, c, d );

			}

		}

		this.setIndex( indices );
		this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
		this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
		this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

	}

	copy( source ) {

		super.copy( source );

		this.parameters = Object.assign( {}, source.parameters );

		return this;

	}

	/**
	 * Factory method for creating an instance of this class from the given
	 * JSON object.
	 *
	 * @param {Object} data - A JSON object representing the serialized geometry.
	 * @return {PlaneGeometry} A new instance.
	 */
	static fromJSON( data ) {

		return new PlaneGeometry( data.width, data.height, data.widthSegments, data.heightSegments );

	}

}

let _materialId = 0;

/**
 * Abstract base class for materials.
 *
 * Materials define the appearance of renderable 3D objects.
 *
 * @abstract
 * @augments EventDispatcher
 */
class Material extends EventDispatcher {

	/**
	 * Constructs a new material.
	 */
	constructor() {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isMaterial = true;

		/**
		 * The ID of the material.
		 *
		 * @name Material#id
		 * @type {number}
		 * @readonly
		 */
		Object.defineProperty( this, 'id', { value: _materialId ++ } );

		/**
		 * The UUID of the material.
		 *
		 * @type {string}
		 * @readonly
		 */
		this.uuid = generateUUID();

		/**
		 * The name of the material.
		 *
		 * @type {string}
		 */
		this.name = '';

		/**
		 * The type property is used for detecting the object type
		 * in context of serialization/deserialization.
		 *
		 * @type {string}
		 * @readonly
		 */
		this.type = 'Material';

		/**
		 * Defines the blending type of the material.
		 *
		 * It must be set to `CustomBlending` if custom blending properties like
		 * {@link Material#blendSrc}, {@link Material#blendDst} or {@link Material#blendEquation}
		 * should have any effect.
		 *
		 * @type {(NoBlending|NormalBlending|AdditiveBlending|SubtractiveBlending|MultiplyBlending|CustomBlending)}
		 * @default NormalBlending
		 */
		this.blending = NormalBlending;

		/**
		 * Defines which side of faces will be rendered - front, back or both.
		 *
		 * @type {(FrontSide|BackSide|DoubleSide)}
		 * @default FrontSide
		 */
		this.side = FrontSide;

		/**
		 * If set to `true`, vertex colors should be used.
		 *
		 * The engine supports RGB and RGBA vertex colors depending on whether a three (RGB) or
		 * four (RGBA) component color buffer attribute is used.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.vertexColors = false;

		/**
		 * Defines how transparent the material is.
		 * A value of `0.0` indicates fully transparent, `1.0` is fully opaque.
		 *
		 * If the {@link Material#transparent} is not set to `true`,
		 * the material will remain fully opaque and this value will only affect its color.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.opacity = 1;

		/**
		 * Defines whether this material is transparent. This has an effect on
		 * rendering as transparent objects need special treatment and are rendered
		 * after non-transparent objects.
		 *
		 * When set to true, the extent to which the material is transparent is
		 * controlled by {@link Material#opacity}.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.transparent = false;

		/**
		 * Enables alpha hashed transparency, an alternative to {@link Material#transparent} or
		 * {@link Material#alphaTest}. The material will not be rendered if opacity is lower than
		 * a random threshold. Randomization introduces some grain or noise, but approximates alpha
		 * blending without the associated problems of sorting. Using TAA can reduce the resulting noise.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.alphaHash = false;

		/**
		 * Defines the blending source factor.
		 *
		 * @type {(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}
		 * @default SrcAlphaFactor
		 */
		this.blendSrc = SrcAlphaFactor;

		/**
		 * Defines the blending destination factor.
		 *
		 * @type {(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}
		 * @default OneMinusSrcAlphaFactor
		 */
		this.blendDst = OneMinusSrcAlphaFactor;

		/**
		 * Defines the blending equation.
		 *
		 * @type {(AddEquation|SubtractEquation|ReverseSubtractEquation|MinEquation|MaxEquation)}
		 * @default AddEquation
		 */
		this.blendEquation = AddEquation;

		/**
		 * Defines the blending source alpha factor.
		 *
		 * @type {?(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}
		 * @default null
		 */
		this.blendSrcAlpha = null;

		/**
		 * Defines the blending destination alpha factor.
		 *
		 * @type {?(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}
		 * @default null
		 */
		this.blendDstAlpha = null;

		/**
		 * Defines the blending equation of the alpha channel.
		 *
		 * @type {?(AddEquation|SubtractEquation|ReverseSubtractEquation|MinEquation|MaxEquation)}
		 * @default null
		 */
		this.blendEquationAlpha = null;

		/**
		 * Represents the RGB values of the constant blend color.
		 *
		 * This property has only an effect when using custom blending with `ConstantColor` or `OneMinusConstantColor`.
		 *
		 * @type {Color}
		 * @default (0,0,0)
		 */
		this.blendColor = new Color( 0, 0, 0 );

		/**
		 * Represents the alpha value of the constant blend color.
		 *
		 * This property has only an effect when using custom blending with `ConstantAlpha` or `OneMinusConstantAlpha`.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.blendAlpha = 0;

		/**
		 * Defines the depth function.
		 *
		 * @type {(NeverDepth|AlwaysDepth|LessDepth|LessEqualDepth|EqualDepth|GreaterEqualDepth|GreaterDepth|NotEqualDepth)}
		 * @default LessEqualDepth
		 */
		this.depthFunc = LessEqualDepth;

		/**
		 * Whether to have depth test enabled when rendering this material.
		 * When the depth test is disabled, the depth write will also be implicitly disabled.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.depthTest = true;

		/**
		 * Whether rendering this material has any effect on the depth buffer.
		 *
		 * When drawing 2D overlays it can be useful to disable the depth writing in
		 * order to layer several things together without creating z-index artifacts.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.depthWrite = true;

		/**
		 * The bit mask to use when writing to the stencil buffer.
		 *
		 * @type {number}
		 * @default 0xff
		 */
		this.stencilWriteMask = 0xff;

		/**
		 * The stencil comparison function to use.
		 *
		 * @type {NeverStencilFunc|LessStencilFunc|EqualStencilFunc|LessEqualStencilFunc|GreaterStencilFunc|NotEqualStencilFunc|GreaterEqualStencilFunc|AlwaysStencilFunc}
		 * @default AlwaysStencilFunc
		 */
		this.stencilFunc = AlwaysStencilFunc;

		/**
		 * The value to use when performing stencil comparisons or stencil operations.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.stencilRef = 0;

		/**
		 * The bit mask to use when comparing against the stencil buffer.
		 *
		 * @type {number}
		 * @default 0xff
		 */
		this.stencilFuncMask = 0xff;

		/**
		 * Which stencil operation to perform when the comparison function returns `false`.
		 *
		 * @type {ZeroStencilOp|KeepStencilOp|ReplaceStencilOp|IncrementStencilOp|DecrementStencilOp|IncrementWrapStencilOp|DecrementWrapStencilOp|InvertStencilOp}
		 * @default KeepStencilOp
		 */
		this.stencilFail = KeepStencilOp;

		/**
		 * Which stencil operation to perform when the comparison function returns
		 * `true` but the depth test fails.
		 *
		 * @type {ZeroStencilOp|KeepStencilOp|ReplaceStencilOp|IncrementStencilOp|DecrementStencilOp|IncrementWrapStencilOp|DecrementWrapStencilOp|InvertStencilOp}
		 * @default KeepStencilOp
		 */
		this.stencilZFail = KeepStencilOp;

		/**
		 * Which stencil operation to perform when the comparison function returns
		 * `true` and the depth test passes.
		 *
		 * @type {ZeroStencilOp|KeepStencilOp|ReplaceStencilOp|IncrementStencilOp|DecrementStencilOp|IncrementWrapStencilOp|DecrementWrapStencilOp|InvertStencilOp}
		 * @default KeepStencilOp
		 */
		this.stencilZPass = KeepStencilOp;

		/**
		 * Whether stencil operations are performed against the stencil buffer. In
		 * order to perform writes or comparisons against the stencil buffer this
		 * value must be `true`.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.stencilWrite = false;

		/**
		 * User-defined clipping planes specified as THREE.Plane objects in world
		 * space. These planes apply to the objects this material is attached to.
		 * Points in space whose signed distance to the plane is negative are clipped
		 * (not rendered). This requires {@link WebGLRenderer#localClippingEnabled} to
		 * be `true`.
		 *
		 * @type {?Array<Plane>}
		 * @default null
		 */
		this.clippingPlanes = null;

		/**
		 * Changes the behavior of clipping planes so that only their intersection is
		 * clipped, rather than their union.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.clipIntersection = false;

		/**
		 * Defines whether to clip shadows according to the clipping planes specified
		 * on this material.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.clipShadows = false;

		/**
		 * Defines which side of faces cast shadows. If `null`, the side casting shadows
		 * is determined as follows:
		 *
		 * - When {@link Material#side} is set to `FrontSide`, the back side cast shadows.
		 * - When {@link Material#side} is set to `BackSide`, the front side cast shadows.
		 * - When {@link Material#side} is set to `DoubleSide`, both sides cast shadows.
		 *
		 * @type {?(FrontSide|BackSide|DoubleSide)}
		 * @default null
		 */
		this.shadowSide = null;

		/**
		 * Whether to render the material's color.
		 *
		 * This can be used in conjunction with {@link Object3D#renderOder} to create invisible
		 * objects that occlude other objects.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.colorWrite = true;

		/**
		 * Override the renderer's default precision for this material.
		 *
		 * @type {?('highp'|'mediump'|'lowp')}
		 * @default null
		 */
		this.precision = null;

		/**
		 * Whether to use polygon offset or not. When enabled, each fragment's depth value will
		 * be offset after it is interpolated from the depth values of the appropriate vertices.
		 * The offset is added before the depth test is performed and before the value is written
		 * into the depth buffer.
		 *
		 * Can be useful for rendering hidden-line images, for applying decals to surfaces, and for
		 * rendering solids with highlighted edges.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.polygonOffset = false;

		/**
		 * Specifies a scale factor that is used to create a variable depth offset for each polygon.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.polygonOffsetFactor = 0;

		/**
		 * Is multiplied by an implementation-specific value to create a constant depth offset.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.polygonOffsetUnits = 0;

		/**
		 * Whether to apply dithering to the color to remove the appearance of banding.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.dithering = false;

		/**
		 * Whether alpha to coverage should be enabled or not. Can only be used with MSAA-enabled contexts
		 * (meaning when the renderer was created with *antialias* parameter set to `true`). Enabling this
		 * will smooth aliasing on clip plane edges and alphaTest-clipped edges.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.alphaToCoverage = false;

		/**
		 * Whether to premultiply the alpha (transparency) value.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.premultipliedAlpha = false;

		/**
		 * Whether double-sided, transparent objects should be rendered with a single pass or not.
		 *
		 * The engine renders double-sided, transparent objects with two draw calls (back faces first,
		 * then front faces) to mitigate transparency artifacts. There are scenarios however where this
		 * approach produces no quality gains but still doubles draw calls e.g. when rendering flat
		 * vegetation like grass sprites. In these cases, set the `forceSinglePass` flag to `true` to
		 * disable the two pass rendering to avoid performance issues.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.forceSinglePass = false;

		/**
		 * Whether it's possible to override the material with {@link Scene#overrideMaterial} or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.allowOverride = true;

		/**
		 * Defines whether 3D objects using this material are visible.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.visible = true;

		/**
		 * Defines whether this material is tone mapped according to the renderer's tone mapping setting.
		 *
		 * It is ignored when rendering to a render target or using post processing or when using
		 * `WebGPURenderer`. In all these cases, all materials are honored by tone mapping.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.toneMapped = true;

		/**
		 * An object that can be used to store custom data about the Material. It
		 * should not hold references to functions as these will not be cloned.
		 *
		 * @type {Object}
		 */
		this.userData = {};

		/**
		 * This starts at `0` and counts how many times {@link Material#needsUpdate} is set to `true`.
		 *
		 * @type {number}
		 * @readonly
		 * @default 0
		 */
		this.version = 0;

		this._alphaTest = 0;

	}

	/**
	 * Sets the alpha value to be used when running an alpha test. The material
	 * will not be rendered if the opacity is lower than this value.
	 *
	 * @type {number}
	 * @readonly
	 * @default 0
	 */
	get alphaTest() {

		return this._alphaTest;

	}

	set alphaTest( value ) {

		if ( this._alphaTest > 0 !== value > 0 ) {

			this.version ++;

		}

		this._alphaTest = value;

	}

	/**
	 * An optional callback that is executed immediately before the material is used to render a 3D object.
	 *
	 * This method can only be used when rendering with {@link WebGLRenderer}.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {Scene} scene - The scene.
	 * @param {Camera} camera - The camera that is used to render the scene.
	 * @param {BufferGeometry} geometry - The 3D object's geometry.
	 * @param {Object3D} object - The 3D object.
	 * @param {Object} group - The geometry group data.
	 */
	onBeforeRender( /* renderer, scene, camera, geometry, object, group */ ) {}

	/**
	 * An optional callback that is executed immediately before the shader
	 * program is compiled. This function is called with the shader source code
	 * as a parameter. Useful for the modification of built-in materials.
	 *
	 * This method can only be used when rendering with {@link WebGLRenderer}. The
	 * recommended approach when customizing materials is to use `WebGPURenderer` with the new
	 * Node Material system and [TSL]{@link https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language}.
	 *
	 * @param {{vertexShader:string,fragmentShader:string,uniforms:Object}} shaderobject - The object holds the uniforms and the vertex and fragment shader source.
	 * @param {WebGLRenderer} renderer - A reference to the renderer.
	 */
	onBeforeCompile( /* shaderobject, renderer */ ) {}

	/**
	 * In case {@link Material#onBeforeCompile} is used, this callback can be used to identify
	 * values of settings used in `onBeforeCompile()`, so three.js can reuse a cached
	 * shader or recompile the shader for this material as needed.
	 *
	 * This method can only be used when rendering with {@link WebGLRenderer}.
	 *
	 * @return {string} The custom program cache key.
	 */
	customProgramCacheKey() {

		return this.onBeforeCompile.toString();

	}

	/**
	 * This method can be used to set default values from parameter objects.
	 * It is a generic implementation so it can be used with different types
	 * of materials.
	 *
	 * @param {Object} [values] - The material values to set.
	 */
	setValues( values ) {

		if ( values === undefined ) return;

		for ( const key in values ) {

			const newValue = values[ key ];

			if ( newValue === undefined ) {

				console.warn( `THREE.Material: parameter '${ key }' has value of undefined.` );
				continue;

			}

			const currentValue = this[ key ];

			if ( currentValue === undefined ) {

				console.warn( `THREE.Material: '${ key }' is not a property of THREE.${ this.type }.` );
				continue;

			}

			if ( currentValue && currentValue.isColor ) {

				currentValue.set( newValue );

			} else if ( ( currentValue && currentValue.isVector3 ) && ( newValue && newValue.isVector3 ) ) {

				currentValue.copy( newValue );

			} else {

				this[ key ] = newValue;

			}

		}

	}

	/**
	 * Serializes the material into JSON.
	 *
	 * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.
	 * @return {Object} A JSON object representing the serialized material.
	 * @see {@link ObjectLoader#parse}
	 */
	toJSON( meta ) {

		const isRootObject = ( meta === undefined || typeof meta === 'string' );

		if ( isRootObject ) {

			meta = {
				textures: {},
				images: {}
			};

		}

		const data = {
			metadata: {
				version: 4.7,
				type: 'Material',
				generator: 'Material.toJSON'
			}
		};

		// standard Material serialization
		data.uuid = this.uuid;
		data.type = this.type;

		if ( this.name !== '' ) data.name = this.name;

		if ( this.color && this.color.isColor ) data.color = this.color.getHex();

		if ( this.roughness !== undefined ) data.roughness = this.roughness;
		if ( this.metalness !== undefined ) data.metalness = this.metalness;

		if ( this.sheen !== undefined ) data.sheen = this.sheen;
		if ( this.sheenColor && this.sheenColor.isColor ) data.sheenColor = this.sheenColor.getHex();
		if ( this.sheenRoughness !== undefined ) data.sheenRoughness = this.sheenRoughness;
		if ( this.emissive && this.emissive.isColor ) data.emissive = this.emissive.getHex();
		if ( this.emissiveIntensity !== undefined && this.emissiveIntensity !== 1 ) data.emissiveIntensity = this.emissiveIntensity;

		if ( this.specular && this.specular.isColor ) data.specular = this.specular.getHex();
		if ( this.specularIntensity !== undefined ) data.specularIntensity = this.specularIntensity;
		if ( this.specularColor && this.specularColor.isColor ) data.specularColor = this.specularColor.getHex();
		if ( this.shininess !== undefined ) data.shininess = this.shininess;
		if ( this.clearcoat !== undefined ) data.clearcoat = this.clearcoat;
		if ( this.clearcoatRoughness !== undefined ) data.clearcoatRoughness = this.clearcoatRoughness;

		if ( this.clearcoatMap && this.clearcoatMap.isTexture ) {

			data.clearcoatMap = this.clearcoatMap.toJSON( meta ).uuid;

		}

		if ( this.clearcoatRoughnessMap && this.clearcoatRoughnessMap.isTexture ) {

			data.clearcoatRoughnessMap = this.clearcoatRoughnessMap.toJSON( meta ).uuid;

		}

		if ( this.clearcoatNormalMap && this.clearcoatNormalMap.isTexture ) {

			data.clearcoatNormalMap = this.clearcoatNormalMap.toJSON( meta ).uuid;
			data.clearcoatNormalScale = this.clearcoatNormalScale.toArray();

		}

		if ( this.sheenColorMap && this.sheenColorMap.isTexture ) {

			data.sheenColorMap = this.sheenColorMap.toJSON( meta ).uuid;

		}

		if ( this.sheenRoughnessMap && this.sheenRoughnessMap.isTexture ) {

			data.sheenRoughnessMap = this.sheenRoughnessMap.toJSON( meta ).uuid;

		}

		if ( this.dispersion !== undefined ) data.dispersion = this.dispersion;

		if ( this.iridescence !== undefined ) data.iridescence = this.iridescence;
		if ( this.iridescenceIOR !== undefined ) data.iridescenceIOR = this.iridescenceIOR;
		if ( this.iridescenceThicknessRange !== undefined ) data.iridescenceThicknessRange = this.iridescenceThicknessRange;

		if ( this.iridescenceMap && this.iridescenceMap.isTexture ) {

			data.iridescenceMap = this.iridescenceMap.toJSON( meta ).uuid;

		}

		if ( this.iridescenceThicknessMap && this.iridescenceThicknessMap.isTexture ) {

			data.iridescenceThicknessMap = this.iridescenceThicknessMap.toJSON( meta ).uuid;

		}

		if ( this.anisotropy !== undefined ) data.anisotropy = this.anisotropy;
		if ( this.anisotropyRotation !== undefined ) data.anisotropyRotation = this.anisotropyRotation;

		if ( this.anisotropyMap && this.anisotropyMap.isTexture ) {

			data.anisotropyMap = this.anisotropyMap.toJSON( meta ).uuid;

		}

		if ( this.map && this.map.isTexture ) data.map = this.map.toJSON( meta ).uuid;
		if ( this.matcap && this.matcap.isTexture ) data.matcap = this.matcap.toJSON( meta ).uuid;
		if ( this.alphaMap && this.alphaMap.isTexture ) data.alphaMap = this.alphaMap.toJSON( meta ).uuid;

		if ( this.lightMap && this.lightMap.isTexture ) {

			data.lightMap = this.lightMap.toJSON( meta ).uuid;
			data.lightMapIntensity = this.lightMapIntensity;

		}

		if ( this.aoMap && this.aoMap.isTexture ) {

			data.aoMap = this.aoMap.toJSON( meta ).uuid;
			data.aoMapIntensity = this.aoMapIntensity;

		}

		if ( this.bumpMap && this.bumpMap.isTexture ) {

			data.bumpMap = this.bumpMap.toJSON( meta ).uuid;
			data.bumpScale = this.bumpScale;

		}

		if ( this.normalMap && this.normalMap.isTexture ) {

			data.normalMap = this.normalMap.toJSON( meta ).uuid;
			data.normalMapType = this.normalMapType;
			data.normalScale = this.normalScale.toArray();

		}

		if ( this.displacementMap && this.displacementMap.isTexture ) {

			data.displacementMap = this.displacementMap.toJSON( meta ).uuid;
			data.displacementScale = this.displacementScale;
			data.displacementBias = this.displacementBias;

		}

		if ( this.roughnessMap && this.roughnessMap.isTexture ) data.roughnessMap = this.roughnessMap.toJSON( meta ).uuid;
		if ( this.metalnessMap && this.metalnessMap.isTexture ) data.metalnessMap = this.metalnessMap.toJSON( meta ).uuid;

		if ( this.emissiveMap && this.emissiveMap.isTexture ) data.emissiveMap = this.emissiveMap.toJSON( meta ).uuid;
		if ( this.specularMap && this.specularMap.isTexture ) data.specularMap = this.specularMap.toJSON( meta ).uuid;
		if ( this.specularIntensityMap && this.specularIntensityMap.isTexture ) data.specularIntensityMap = this.specularIntensityMap.toJSON( meta ).uuid;
		if ( this.specularColorMap && this.specularColorMap.isTexture ) data.specularColorMap = this.specularColorMap.toJSON( meta ).uuid;

		if ( this.envMap && this.envMap.isTexture ) {

			data.envMap = this.envMap.toJSON( meta ).uuid;

			if ( this.combine !== undefined ) data.combine = this.combine;

		}

		if ( this.envMapRotation !== undefined ) data.envMapRotation = this.envMapRotation.toArray();
		if ( this.envMapIntensity !== undefined ) data.envMapIntensity = this.envMapIntensity;
		if ( this.reflectivity !== undefined ) data.reflectivity = this.reflectivity;
		if ( this.refractionRatio !== undefined ) data.refractionRatio = this.refractionRatio;

		if ( this.gradientMap && this.gradientMap.isTexture ) {

			data.gradientMap = this.gradientMap.toJSON( meta ).uuid;

		}

		if ( this.transmission !== undefined ) data.transmission = this.transmission;
		if ( this.transmissionMap && this.transmissionMap.isTexture ) data.transmissionMap = this.transmissionMap.toJSON( meta ).uuid;
		if ( this.thickness !== undefined ) data.thickness = this.thickness;
		if ( this.thicknessMap && this.thicknessMap.isTexture ) data.thicknessMap = this.thicknessMap.toJSON( meta ).uuid;
		if ( this.attenuationDistance !== undefined && this.attenuationDistance !== Infinity ) data.attenuationDistance = this.attenuationDistance;
		if ( this.attenuationColor !== undefined ) data.attenuationColor = this.attenuationColor.getHex();

		if ( this.size !== undefined ) data.size = this.size;
		if ( this.shadowSide !== null ) data.shadowSide = this.shadowSide;
		if ( this.sizeAttenuation !== undefined ) data.sizeAttenuation = this.sizeAttenuation;

		if ( this.blending !== NormalBlending ) data.blending = this.blending;
		if ( this.side !== FrontSide ) data.side = this.side;
		if ( this.vertexColors === true ) data.vertexColors = true;

		if ( this.opacity < 1 ) data.opacity = this.opacity;
		if ( this.transparent === true ) data.transparent = true;

		if ( this.blendSrc !== SrcAlphaFactor ) data.blendSrc = this.blendSrc;
		if ( this.blendDst !== OneMinusSrcAlphaFactor ) data.blendDst = this.blendDst;
		if ( this.blendEquation !== AddEquation ) data.blendEquation = this.blendEquation;
		if ( this.blendSrcAlpha !== null ) data.blendSrcAlpha = this.blendSrcAlpha;
		if ( this.blendDstAlpha !== null ) data.blendDstAlpha = this.blendDstAlpha;
		if ( this.blendEquationAlpha !== null ) data.blendEquationAlpha = this.blendEquationAlpha;
		if ( this.blendColor && this.blendColor.isColor ) data.blendColor = this.blendColor.getHex();
		if ( this.blendAlpha !== 0 ) data.blendAlpha = this.blendAlpha;

		if ( this.depthFunc !== LessEqualDepth ) data.depthFunc = this.depthFunc;
		if ( this.depthTest === false ) data.depthTest = this.depthTest;
		if ( this.depthWrite === false ) data.depthWrite = this.depthWrite;
		if ( this.colorWrite === false ) data.colorWrite = this.colorWrite;

		if ( this.stencilWriteMask !== 0xff ) data.stencilWriteMask = this.stencilWriteMask;
		if ( this.stencilFunc !== AlwaysStencilFunc ) data.stencilFunc = this.stencilFunc;
		if ( this.stencilRef !== 0 ) data.stencilRef = this.stencilRef;
		if ( this.stencilFuncMask !== 0xff ) data.stencilFuncMask = this.stencilFuncMask;
		if ( this.stencilFail !== KeepStencilOp ) data.stencilFail = this.stencilFail;
		if ( this.stencilZFail !== KeepStencilOp ) data.stencilZFail = this.stencilZFail;
		if ( this.stencilZPass !== KeepStencilOp ) data.stencilZPass = this.stencilZPass;
		if ( this.stencilWrite === true ) data.stencilWrite = this.stencilWrite;

		// rotation (SpriteMaterial)
		if ( this.rotation !== undefined && this.rotation !== 0 ) data.rotation = this.rotation;

		if ( this.polygonOffset === true ) data.polygonOffset = true;
		if ( this.polygonOffsetFactor !== 0 ) data.polygonOffsetFactor = this.polygonOffsetFactor;
		if ( this.polygonOffsetUnits !== 0 ) data.polygonOffsetUnits = this.polygonOffsetUnits;

		if ( this.linewidth !== undefined && this.linewidth !== 1 ) data.linewidth = this.linewidth;
		if ( this.dashSize !== undefined ) data.dashSize = this.dashSize;
		if ( this.gapSize !== undefined ) data.gapSize = this.gapSize;
		if ( this.scale !== undefined ) data.scale = this.scale;

		if ( this.dithering === true ) data.dithering = true;

		if ( this.alphaTest > 0 ) data.alphaTest = this.alphaTest;
		if ( this.alphaHash === true ) data.alphaHash = true;
		if ( this.alphaToCoverage === true ) data.alphaToCoverage = true;
		if ( this.premultipliedAlpha === true ) data.premultipliedAlpha = true;
		if ( this.forceSinglePass === true ) data.forceSinglePass = true;

		if ( this.wireframe === true ) data.wireframe = true;
		if ( this.wireframeLinewidth > 1 ) data.wireframeLinewidth = this.wireframeLinewidth;
		if ( this.wireframeLinecap !== 'round' ) data.wireframeLinecap = this.wireframeLinecap;
		if ( this.wireframeLinejoin !== 'round' ) data.wireframeLinejoin = this.wireframeLinejoin;

		if ( this.flatShading === true ) data.flatShading = true;

		if ( this.visible === false ) data.visible = false;

		if ( this.toneMapped === false ) data.toneMapped = false;

		if ( this.fog === false ) data.fog = false;

		if ( Object.keys( this.userData ).length > 0 ) data.userData = this.userData;

		// TODO: Copied from Object3D.toJSON

		function extractFromCache( cache ) {

			const values = [];

			for ( const key in cache ) {

				const data = cache[ key ];
				delete data.metadata;
				values.push( data );

			}

			return values;

		}

		if ( isRootObject ) {

			const textures = extractFromCache( meta.textures );
			const images = extractFromCache( meta.images );

			if ( textures.length > 0 ) data.textures = textures;
			if ( images.length > 0 ) data.images = images;

		}

		return data;

	}

	/**
	 * Returns a new material with copied values from this instance.
	 *
	 * @return {Material} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

	/**
	 * Copies the values of the given material to this instance.
	 *
	 * @param {Material} source - The material to copy.
	 * @return {Material} A reference to this instance.
	 */
	copy( source ) {

		this.name = source.name;

		this.blending = source.blending;
		this.side = source.side;
		this.vertexColors = source.vertexColors;

		this.opacity = source.opacity;
		this.transparent = source.transparent;

		this.blendSrc = source.blendSrc;
		this.blendDst = source.blendDst;
		this.blendEquation = source.blendEquation;
		this.blendSrcAlpha = source.blendSrcAlpha;
		this.blendDstAlpha = source.blendDstAlpha;
		this.blendEquationAlpha = source.blendEquationAlpha;
		this.blendColor.copy( source.blendColor );
		this.blendAlpha = source.blendAlpha;

		this.depthFunc = source.depthFunc;
		this.depthTest = source.depthTest;
		this.depthWrite = source.depthWrite;

		this.stencilWriteMask = source.stencilWriteMask;
		this.stencilFunc = source.stencilFunc;
		this.stencilRef = source.stencilRef;
		this.stencilFuncMask = source.stencilFuncMask;
		this.stencilFail = source.stencilFail;
		this.stencilZFail = source.stencilZFail;
		this.stencilZPass = source.stencilZPass;
		this.stencilWrite = source.stencilWrite;

		const srcPlanes = source.clippingPlanes;
		let dstPlanes = null;

		if ( srcPlanes !== null ) {

			const n = srcPlanes.length;
			dstPlanes = new Array( n );

			for ( let i = 0; i !== n; ++ i ) {

				dstPlanes[ i ] = srcPlanes[ i ].clone();

			}

		}

		this.clippingPlanes = dstPlanes;
		this.clipIntersection = source.clipIntersection;
		this.clipShadows = source.clipShadows;

		this.shadowSide = source.shadowSide;

		this.colorWrite = source.colorWrite;

		this.precision = source.precision;

		this.polygonOffset = source.polygonOffset;
		this.polygonOffsetFactor = source.polygonOffsetFactor;
		this.polygonOffsetUnits = source.polygonOffsetUnits;

		this.dithering = source.dithering;

		this.alphaTest = source.alphaTest;
		this.alphaHash = source.alphaHash;
		this.alphaToCoverage = source.alphaToCoverage;
		this.premultipliedAlpha = source.premultipliedAlpha;
		this.forceSinglePass = source.forceSinglePass;

		this.visible = source.visible;

		this.toneMapped = source.toneMapped;

		this.userData = JSON.parse( JSON.stringify( source.userData ) );

		return this;

	}

	/**
	 * Frees the GPU-related resources allocated by this instance. Call this
	 * method whenever this instance is no longer used in your app.
	 *
	 * @fires Material#dispose
	 */
	dispose() {

		/**
		 * Fires when the material has been disposed of.
		 *
		 * @event Material#dispose
		 * @type {Object}
		 */
		this.dispatchEvent( { type: 'dispose' } );

	}

	/**
	 * Setting this property to `true` indicates the engine the material
	 * needs to be recompiled.
	 *
	 * @type {boolean}
	 * @default false
	 * @param {boolean} value
	 */
	set needsUpdate( value ) {

		if ( value === true ) this.version ++;

	}

}

// Uniform Utilities

function cloneUniforms( src ) {

	const dst = {};

	for ( const u in src ) {

		dst[ u ] = {};

		for ( const p in src[ u ] ) {

			const property = src[ u ][ p ];

			if ( property && ( property.isColor ||
				property.isMatrix3 || property.isMatrix4 ||
				property.isVector2 || property.isVector3 || property.isVector4 ||
				property.isTexture || property.isQuaternion ) ) {

				if ( property.isRenderTargetTexture ) {

					console.warn( 'UniformsUtils: Textures of render targets cannot be cloned via cloneUniforms() or mergeUniforms().' );
					dst[ u ][ p ] = null;

				} else {

					dst[ u ][ p ] = property.clone();

				}

			} else if ( Array.isArray( property ) ) {

				dst[ u ][ p ] = property.slice();

			} else {

				dst[ u ][ p ] = property;

			}

		}

	}

	return dst;

}

function mergeUniforms( uniforms ) {

	const merged = {};

	for ( let u = 0; u < uniforms.length; u ++ ) {

		const tmp = cloneUniforms( uniforms[ u ] );

		for ( const p in tmp ) {

			merged[ p ] = tmp[ p ];

		}

	}

	return merged;

}

function cloneUniformsGroups( src ) {

	const dst = [];

	for ( let u = 0; u < src.length; u ++ ) {

		dst.push( src[ u ].clone() );

	}

	return dst;

}

function getUnlitUniformColorSpace( renderer ) {

	const currentRenderTarget = renderer.getRenderTarget();

	if ( currentRenderTarget === null ) {

		// https://github.com/mrdoob/three.js/pull/23937#issuecomment-1111067398
		return renderer.outputColorSpace;

	}

	// https://github.com/mrdoob/three.js/issues/27868
	if ( currentRenderTarget.isXRRenderTarget === true ) {

		return currentRenderTarget.texture.colorSpace;

	}

	return ColorManagement.workingColorSpace;

}

// Legacy

const UniformsUtils = { clone: cloneUniforms, merge: mergeUniforms };

var default_vertex = "void main() {\n\tgl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n}";

var default_fragment = "void main() {\n\tgl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 );\n}";

/**
 * A material rendered with custom shaders. A shader is a small program written in GLSL.
 * that runs on the GPU. You may want to use a custom shader if you need to implement an
 * effect not included with any of the built-in materials.
 *
 * There are the following notes to bear in mind when using a `ShaderMaterial`:
 *
 * - `ShaderMaterial` can only be used with {@link WebGLRenderer}.
 * - Built in attributes and uniforms are passed to the shaders along with your code. If
 * you don't want that, use {@link RawShaderMaterial} instead.
 * - You can use the directive `#pragma unroll_loop_start` and `#pragma unroll_loop_end`
 * in order to unroll a `for` loop in GLSL by the shader preprocessor. The directive has
 * to be placed right above the loop. The loop formatting has to correspond to a defined standard.
 *   - The loop has to be [normalized]{@link https://en.wikipedia.org/wiki/Normalized_loop}.
 *   - The loop variable has to be *i*.
 *   - The value `UNROLLED_LOOP_INDEX` will be replaced with the explicitly
 * value of *i* for the given iteration and can be used in preprocessor
 * statements.
 *
 * ```js
 * const material = new THREE.ShaderMaterial( {
 * 	uniforms: {
 * 		time: { value: 1.0 },
 * 		resolution: { value: new THREE.Vector2() }
 * 	},
 * 	vertexShader: document.getElementById( 'vertexShader' ).textContent,
 * 	fragmentShader: document.getElementById( 'fragmentShader' ).textContent
 * } );
 * ```
 *
 * @augments Material
 */
class ShaderMaterial extends Material {

	/**
	 * Constructs a new shader material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isShaderMaterial = true;

		this.type = 'ShaderMaterial';

		/**
		 * Defines custom constants using `#define` directives within the GLSL code
		 * for both the vertex shader and the fragment shader; each key/value pair
		 * yields another directive.
		 * ```js
		 * defines: {
		 * 	FOO: 15,
		 * 	BAR: true
		 * }
		 * ```
		 * Yields the lines:
		 * ```
		 * #define FOO 15
		 * #define BAR true
		 * ```
		 *
		 * @type {Object}
		 */
		this.defines = {};

		/**
		 * An object of the form:
		 * ```js
		 * {
		 * 	"uniform1": { value: 1.0 },
		 * 	"uniform2": { value: 2 }
		 * }
		 * ```
		 * specifying the uniforms to be passed to the shader code; keys are uniform
		 * names, values are definitions of the form
		 * ```
		 * {
		 * 	value: 1.0
		 * }
		 * ```
		 * where `value` is the value of the uniform. Names must match the name of
		 * the uniform, as defined in the GLSL code. Note that uniforms are refreshed
		 * on every frame, so updating the value of the uniform will immediately
		 * update the value available to the GLSL code.
		 *
		 * @type {Object}
		 */
		this.uniforms = {};

		/**
		 * An array holding uniforms groups for configuring UBOs.
		 *
		 * @type {Array<UniformsGroup>}
		 */
		this.uniformsGroups = [];

		/**
		 * Vertex shader GLSL code. This is the actual code for the shader.
		 *
		 * @type {string}
		 */
		this.vertexShader = default_vertex;

		/**
		 * Fragment shader GLSL code. This is the actual code for the shader.
		 *
		 * @type {string}
		 */
		this.fragmentShader = default_fragment;

		/**
		 * Controls line thickness or lines.
		 *
		 * WebGL and WebGPU ignore this setting and always render line primitives with a
		 * width of one pixel.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.linewidth = 1;

		/**
		 * Renders the geometry as a wireframe.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.wireframe = false;

		/**
		 * Controls the thickness of the wireframe.
		 *
		 * WebGL and WebGPU ignore this property and always render
		 * 1 pixel wide lines.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.wireframeLinewidth = 1;

		/**
		 * Define whether the material color is affected by global fog settings; `true`
		 * to pass fog uniforms to the shader.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.fog = false;

		/**
		 * Defines whether this material uses lighting; `true` to pass uniform data
		 * related to lighting to this shader.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.lights = false;

		/**
		 * Defines whether this material supports clipping; `true` to let the renderer
		 * pass the clippingPlanes uniform.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.clipping = false;

		/**
		 * Overwritten and set to `true` by default.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.forceSinglePass = true;

		/**
		 * This object allows to enable certain WebGL 2 extensions.
		 *
		 * - clipCullDistance: set to `true` to use vertex shader clipping
		 * - multiDraw: set to `true` to use vertex shader multi_draw / enable gl_DrawID
		 *
		 * @type {{clipCullDistance:false,multiDraw:false}}
		 */
		this.extensions = {
			clipCullDistance: false, // set to use vertex shader clipping
			multiDraw: false // set to use vertex shader multi_draw / enable gl_DrawID
		};

		/**
		 * When the rendered geometry doesn't include these attributes but the
		 * material does, these default values will be passed to the shaders. This
		 * avoids errors when buffer data is missing.
		 *
		 * - color: [ 1, 1, 1 ]
		 * - uv: [ 0, 0 ]
		 * - uv1: [ 0, 0 ]
		 *
		 * @type {Object}
		 */
		this.defaultAttributeValues = {
			'color': [ 1, 1, 1 ],
			'uv': [ 0, 0 ],
			'uv1': [ 0, 0 ]
		};

		/**
		 * If set, this calls [gl.bindAttribLocation]{@link https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bindAttribLocation}
		 * to bind a generic vertex index to an attribute variable.
		 *
		 * @type {string|undefined}
		 * @default undefined
		 */
		this.index0AttributeName = undefined;

		/**
		 * Can be used to force a uniform update while changing uniforms in
		 * {@link Object3D#onBeforeRender}.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.uniformsNeedUpdate = false;

		/**
		 * Defines the GLSL version of custom shader code.
		 *
		 * @type {?(GLSL1|GLSL3)}
		 * @default null
		 */
		this.glslVersion = null;

		if ( parameters !== undefined ) {

			this.setValues( parameters );

		}

	}

	copy( source ) {

		super.copy( source );

		this.fragmentShader = source.fragmentShader;
		this.vertexShader = source.vertexShader;

		this.uniforms = cloneUniforms( source.uniforms );
		this.uniformsGroups = cloneUniformsGroups( source.uniformsGroups );

		this.defines = Object.assign( {}, source.defines );

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;

		this.fog = source.fog;
		this.lights = source.lights;
		this.clipping = source.clipping;

		this.extensions = Object.assign( {}, source.extensions );

		this.glslVersion = source.glslVersion;

		return this;

	}

	toJSON( meta ) {

		const data = super.toJSON( meta );

		data.glslVersion = this.glslVersion;
		data.uniforms = {};

		for ( const name in this.uniforms ) {

			const uniform = this.uniforms[ name ];
			const value = uniform.value;

			if ( value && value.isTexture ) {

				data.uniforms[ name ] = {
					type: 't',
					value: value.toJSON( meta ).uuid
				};

			} else if ( value && value.isColor ) {

				data.uniforms[ name ] = {
					type: 'c',
					value: value.getHex()
				};

			} else if ( value && value.isVector2 ) {

				data.uniforms[ name ] = {
					type: 'v2',
					value: value.toArray()
				};

			} else if ( value && value.isVector3 ) {

				data.uniforms[ name ] = {
					type: 'v3',
					value: value.toArray()
				};

			} else if ( value && value.isVector4 ) {

				data.uniforms[ name ] = {
					type: 'v4',
					value: value.toArray()
				};

			} else if ( value && value.isMatrix3 ) {

				data.uniforms[ name ] = {
					type: 'm3',
					value: value.toArray()
				};

			} else if ( value && value.isMatrix4 ) {

				data.uniforms[ name ] = {
					type: 'm4',
					value: value.toArray()
				};

			} else {

				data.uniforms[ name ] = {
					value: value
				};

				// note: the array variants v2v, v3v, v4v, m4v and tv are not supported so far

			}

		}

		if ( Object.keys( this.defines ).length > 0 ) data.defines = this.defines;

		data.vertexShader = this.vertexShader;
		data.fragmentShader = this.fragmentShader;

		data.lights = this.lights;
		data.clipping = this.clipping;

		const extensions = {};

		for ( const key in this.extensions ) {

			if ( this.extensions[ key ] === true ) extensions[ key ] = true;

		}

		if ( Object.keys( extensions ).length > 0 ) data.extensions = extensions;

		return data;

	}

}

const _vector$3 = /*@__PURE__*/ new Vector3();
const _segCenter = /*@__PURE__*/ new Vector3();
const _segDir = /*@__PURE__*/ new Vector3();
const _diff = /*@__PURE__*/ new Vector3();

const _edge1 = /*@__PURE__*/ new Vector3();
const _edge2 = /*@__PURE__*/ new Vector3();
const _normal$1 = /*@__PURE__*/ new Vector3();

/**
 * A ray that emits from an origin in a certain direction. The class is used by
 * {@link Raycaster} to assist with raycasting. Raycasting is used for
 * mouse picking (working out what objects in the 3D space the mouse is over)
 * amongst other things.
 */
class Ray {

	/**
	 * Constructs a new ray.
	 *
	 * @param {Vector3} [origin=(0,0,0)] - The origin of the ray.
	 * @param {Vector3} [direction=(0,0,-1)] - The (normalized) direction of the ray.
	 */
	constructor( origin = new Vector3(), direction = new Vector3( 0, 0, -1 ) ) {

		/**
		 * The origin of the ray.
		 *
		 * @type {Vector3}
		 */
		this.origin = origin;

		/**
		 * The (normalized) direction of the ray.
		 *
		 * @type {Vector3}
		 */
		this.direction = direction;

	}

	/**
	 * Sets the ray's components by copying the given values.
	 *
	 * @param {Vector3} origin - The origin.
	 * @param {Vector3} direction - The direction.
	 * @return {Ray} A reference to this ray.
	 */
	set( origin, direction ) {

		this.origin.copy( origin );
		this.direction.copy( direction );

		return this;

	}

	/**
	 * Copies the values of the given ray to this instance.
	 *
	 * @param {Ray} ray - The ray to copy.
	 * @return {Ray} A reference to this ray.
	 */
	copy( ray ) {

		this.origin.copy( ray.origin );
		this.direction.copy( ray.direction );

		return this;

	}

	/**
	 * Returns a vector that is located at a given distance along this ray.
	 *
	 * @param {number} t - The distance along the ray to retrieve a position for.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} A position on the ray.
	 */
	at( t, target ) {

		return target.copy( this.origin ).addScaledVector( this.direction, t );

	}

	/**
	 * Adjusts the direction of the ray to point at the given vector in world space.
	 *
	 * @param {Vector3} v - The target position.
	 * @return {Ray} A reference to this ray.
	 */
	lookAt( v ) {

		this.direction.copy( v ).sub( this.origin ).normalize();

		return this;

	}

	/**
	 * Shift the origin of this ray along its direction by the given distance.
	 *
	 * @param {number} t - The distance along the ray to interpolate.
	 * @return {Ray} A reference to this ray.
	 */
	recast( t ) {

		this.origin.copy( this.at( t, _vector$3 ) );

		return this;

	}

	/**
	 * Returns the point along this ray that is closest to the given point.
	 *
	 * @param {Vector3} point - A point in 3D space to get the closet location on the ray for.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The closest point on this ray.
	 */
	closestPointToPoint( point, target ) {

		target.subVectors( point, this.origin );

		const directionDistance = target.dot( this.direction );

		if ( directionDistance < 0 ) {

			return target.copy( this.origin );

		}

		return target.copy( this.origin ).addScaledVector( this.direction, directionDistance );

	}

	/**
	 * Returns the distance of the closest approach between this ray and the given point.
	 *
	 * @param {Vector3} point - A point in 3D space to compute the distance to.
	 * @return {number} The distance.
	 */
	distanceToPoint( point ) {

		return Math.sqrt( this.distanceSqToPoint( point ) );

	}

	/**
	 * Returns the squared distance of the closest approach between this ray and the given point.
	 *
	 * @param {Vector3} point - A point in 3D space to compute the distance to.
	 * @return {number} The squared distance.
	 */
	distanceSqToPoint( point ) {

		const directionDistance = _vector$3.subVectors( point, this.origin ).dot( this.direction );

		// point behind the ray

		if ( directionDistance < 0 ) {

			return this.origin.distanceToSquared( point );

		}

		_vector$3.copy( this.origin ).addScaledVector( this.direction, directionDistance );

		return _vector$3.distanceToSquared( point );

	}

	/**
	 * Returns the squared distance between this ray and the given line segment.
	 *
	 * @param {Vector3} v0 - The start point of the line segment.
	 * @param {Vector3} v1 - The end point of the line segment.
	 * @param {Vector3} [optionalPointOnRay] - When provided, it receives the point on this ray that is closest to the segment.
	 * @param {Vector3} [optionalPointOnSegment] - When provided, it receives the point on the line segment that is closest to this ray.
	 * @return {number} The squared distance.
	 */
	distanceSqToSegment( v0, v1, optionalPointOnRay, optionalPointOnSegment ) {

		// from https://github.com/pmjoniak/GeometricTools/blob/master/GTEngine/Include/Mathematics/GteDistRaySegment.h
		// It returns the min distance between the ray and the segment
		// defined by v0 and v1
		// It can also set two optional targets :
		// - The closest point on the ray
		// - The closest point on the segment

		_segCenter.copy( v0 ).add( v1 ).multiplyScalar( 0.5 );
		_segDir.copy( v1 ).sub( v0 ).normalize();
		_diff.copy( this.origin ).sub( _segCenter );

		const segExtent = v0.distanceTo( v1 ) * 0.5;
		const a01 = - this.direction.dot( _segDir );
		const b0 = _diff.dot( this.direction );
		const b1 = - _diff.dot( _segDir );
		const c = _diff.lengthSq();
		const det = Math.abs( 1 - a01 * a01 );
		let s0, s1, sqrDist, extDet;

		if ( det > 0 ) {

			// The ray and segment are not parallel.

			s0 = a01 * b1 - b0;
			s1 = a01 * b0 - b1;
			extDet = segExtent * det;

			if ( s0 >= 0 ) {

				if ( s1 >= - extDet ) {

					if ( s1 <= extDet ) {

						// region 0
						// Minimum at interior points of ray and segment.

						const invDet = 1 / det;
						s0 *= invDet;
						s1 *= invDet;
						sqrDist = s0 * ( s0 + a01 * s1 + 2 * b0 ) + s1 * ( a01 * s0 + s1 + 2 * b1 ) + c;

					} else {

						// region 1

						s1 = segExtent;
						s0 = Math.max( 0, - ( a01 * s1 + b0 ) );
						sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;

					}

				} else {

					// region 5

					s1 = - segExtent;
					s0 = Math.max( 0, - ( a01 * s1 + b0 ) );
					sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;

				}

			} else {

				if ( s1 <= - extDet ) {

					// region 4

					s0 = Math.max( 0, - ( - a01 * segExtent + b0 ) );
					s1 = ( s0 > 0 ) ? - segExtent : Math.min( Math.max( - segExtent, - b1 ), segExtent );
					sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;

				} else if ( s1 <= extDet ) {

					// region 3

					s0 = 0;
					s1 = Math.min( Math.max( - segExtent, - b1 ), segExtent );
					sqrDist = s1 * ( s1 + 2 * b1 ) + c;

				} else {

					// region 2

					s0 = Math.max( 0, - ( a01 * segExtent + b0 ) );
					s1 = ( s0 > 0 ) ? segExtent : Math.min( Math.max( - segExtent, - b1 ), segExtent );
					sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;

				}

			}

		} else {

			// Ray and segment are parallel.

			s1 = ( a01 > 0 ) ? - segExtent : segExtent;
			s0 = Math.max( 0, - ( a01 * s1 + b0 ) );
			sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;

		}

		if ( optionalPointOnRay ) {

			optionalPointOnRay.copy( this.origin ).addScaledVector( this.direction, s0 );

		}

		if ( optionalPointOnSegment ) {

			optionalPointOnSegment.copy( _segCenter ).addScaledVector( _segDir, s1 );

		}

		return sqrDist;

	}

	/**
	 * Intersects this ray with the given sphere, returning the intersection
	 * point or `null` if there is no intersection.
	 *
	 * @param {Sphere} sphere - The sphere to intersect.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {?Vector3} The intersection point.
	 */
	intersectSphere( sphere, target ) {

		_vector$3.subVectors( sphere.center, this.origin );
		const tca = _vector$3.dot( this.direction );
		const d2 = _vector$3.dot( _vector$3 ) - tca * tca;
		const radius2 = sphere.radius * sphere.radius;

		if ( d2 > radius2 ) return null;

		const thc = Math.sqrt( radius2 - d2 );

		// t0 = first intersect point - entrance on front of sphere
		const t0 = tca - thc;

		// t1 = second intersect point - exit point on back of sphere
		const t1 = tca + thc;

		// test to see if t1 is behind the ray - if so, return null
		if ( t1 < 0 ) return null;

		// test to see if t0 is behind the ray:
		// if it is, the ray is inside the sphere, so return the second exit point scaled by t1,
		// in order to always return an intersect point that is in front of the ray.
		if ( t0 < 0 ) return this.at( t1, target );

		// else t0 is in front of the ray, so return the first collision point scaled by t0
		return this.at( t0, target );

	}

	/**
	 * Returns `true` if this ray intersects with the given sphere.
	 *
	 * @param {Sphere} sphere - The sphere to intersect.
	 * @return {boolean} Whether this ray intersects with the given sphere or not.
	 */
	intersectsSphere( sphere ) {

		if ( sphere.radius < 0 ) return false; // handle empty spheres, see #31187

		return this.distanceSqToPoint( sphere.center ) <= ( sphere.radius * sphere.radius );

	}

	/**
	 * Computes the distance from the ray's origin to the given plane. Returns `null` if the ray
	 * does not intersect with the plane.
	 *
	 * @param {Plane} plane - The plane to compute the distance to.
	 * @return {?number} Whether this ray intersects with the given sphere or not.
	 */
	distanceToPlane( plane ) {

		const denominator = plane.normal.dot( this.direction );

		if ( denominator === 0 ) {

			// line is coplanar, return origin
			if ( plane.distanceToPoint( this.origin ) === 0 ) {

				return 0;

			}

			// Null is preferable to undefined since undefined means.... it is undefined

			return null;

		}

		const t = - ( this.origin.dot( plane.normal ) + plane.constant ) / denominator;

		// Return if the ray never intersects the plane

		return t >= 0 ? t : null;

	}

	/**
	 * Intersects this ray with the given plane, returning the intersection
	 * point or `null` if there is no intersection.
	 *
	 * @param {Plane} plane - The plane to intersect.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {?Vector3} The intersection point.
	 */
	intersectPlane( plane, target ) {

		const t = this.distanceToPlane( plane );

		if ( t === null ) {

			return null;

		}

		return this.at( t, target );

	}

	/**
	 * Returns `true` if this ray intersects with the given plane.
	 *
	 * @param {Plane} plane - The plane to intersect.
	 * @return {boolean} Whether this ray intersects with the given plane or not.
	 */
	intersectsPlane( plane ) {

		// check if the ray lies on the plane first

		const distToPoint = plane.distanceToPoint( this.origin );

		if ( distToPoint === 0 ) {

			return true;

		}

		const denominator = plane.normal.dot( this.direction );

		if ( denominator * distToPoint < 0 ) {

			return true;

		}

		// ray origin is behind the plane (and is pointing behind it)

		return false;

	}

	/**
	 * Intersects this ray with the given bounding box, returning the intersection
	 * point or `null` if there is no intersection.
	 *
	 * @param {Box3} box - The box to intersect.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {?Vector3} The intersection point.
	 */
	intersectBox( box, target ) {

		let tmin, tmax, tymin, tymax, tzmin, tzmax;

		const invdirx = 1 / this.direction.x,
			invdiry = 1 / this.direction.y,
			invdirz = 1 / this.direction.z;

		const origin = this.origin;

		if ( invdirx >= 0 ) {

			tmin = ( box.min.x - origin.x ) * invdirx;
			tmax = ( box.max.x - origin.x ) * invdirx;

		} else {

			tmin = ( box.max.x - origin.x ) * invdirx;
			tmax = ( box.min.x - origin.x ) * invdirx;

		}

		if ( invdiry >= 0 ) {

			tymin = ( box.min.y - origin.y ) * invdiry;
			tymax = ( box.max.y - origin.y ) * invdiry;

		} else {

			tymin = ( box.max.y - origin.y ) * invdiry;
			tymax = ( box.min.y - origin.y ) * invdiry;

		}

		if ( ( tmin > tymax ) || ( tymin > tmax ) ) return null;

		if ( tymin > tmin || isNaN( tmin ) ) tmin = tymin;

		if ( tymax < tmax || isNaN( tmax ) ) tmax = tymax;

		if ( invdirz >= 0 ) {

			tzmin = ( box.min.z - origin.z ) * invdirz;
			tzmax = ( box.max.z - origin.z ) * invdirz;

		} else {

			tzmin = ( box.max.z - origin.z ) * invdirz;
			tzmax = ( box.min.z - origin.z ) * invdirz;

		}

		if ( ( tmin > tzmax ) || ( tzmin > tmax ) ) return null;

		if ( tzmin > tmin || tmin !== tmin ) tmin = tzmin;

		if ( tzmax < tmax || tmax !== tmax ) tmax = tzmax;

		//return point closest to the ray (positive side)

		if ( tmax < 0 ) return null;

		return this.at( tmin >= 0 ? tmin : tmax, target );

	}

	/**
	 * Returns `true` if this ray intersects with the given box.
	 *
	 * @param {Box3} box - The box to intersect.
	 * @return {boolean} Whether this ray intersects with the given box or not.
	 */
	intersectsBox( box ) {

		return this.intersectBox( box, _vector$3 ) !== null;

	}

	/**
	 * Intersects this ray with the given triangle, returning the intersection
	 * point or `null` if there is no intersection.
	 *
	 * @param {Vector3} a - The first vertex of the triangle.
	 * @param {Vector3} b - The second vertex of the triangle.
	 * @param {Vector3} c - The third vertex of the triangle.
	 * @param {boolean} backfaceCulling - Whether to use backface culling or not.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {?Vector3} The intersection point.
	 */
	intersectTriangle( a, b, c, backfaceCulling, target ) {

		// Compute the offset origin, edges, and normal.

		// from https://github.com/pmjoniak/GeometricTools/blob/master/GTEngine/Include/Mathematics/GteIntrRay3Triangle3.h

		_edge1.subVectors( b, a );
		_edge2.subVectors( c, a );
		_normal$1.crossVectors( _edge1, _edge2 );

		// Solve Q + t*D = b1*E1 + b2*E2 (Q = kDiff, D = ray direction,
		// E1 = kEdge1, E2 = kEdge2, N = Cross(E1,E2)) by
		//   |Dot(D,N)|*b1 = sign(Dot(D,N))*Dot(D,Cross(Q,E2))
		//   |Dot(D,N)|*b2 = sign(Dot(D,N))*Dot(D,Cross(E1,Q))
		//   |Dot(D,N)|*t = -sign(Dot(D,N))*Dot(Q,N)
		let DdN = this.direction.dot( _normal$1 );
		let sign;

		if ( DdN > 0 ) {

			if ( backfaceCulling ) return null;
			sign = 1;

		} else if ( DdN < 0 ) {

			sign = -1;
			DdN = - DdN;

		} else {

			return null;

		}

		_diff.subVectors( this.origin, a );
		const DdQxE2 = sign * this.direction.dot( _edge2.crossVectors( _diff, _edge2 ) );

		// b1 < 0, no intersection
		if ( DdQxE2 < 0 ) {

			return null;

		}

		const DdE1xQ = sign * this.direction.dot( _edge1.cross( _diff ) );

		// b2 < 0, no intersection
		if ( DdE1xQ < 0 ) {

			return null;

		}

		// b1+b2 > 1, no intersection
		if ( DdQxE2 + DdE1xQ > DdN ) {

			return null;

		}

		// Line intersects triangle, check if ray does.
		const QdN = - sign * _diff.dot( _normal$1 );

		// t < 0, no intersection
		if ( QdN < 0 ) {

			return null;

		}

		// Ray intersects triangle.
		return this.at( QdN / DdN, target );

	}

	/**
	 * Transforms this ray with the given 4x4 transformation matrix.
	 *
	 * @param {Matrix4} matrix4 - The transformation matrix.
	 * @return {Ray} A reference to this ray.
	 */
	applyMatrix4( matrix4 ) {

		this.origin.applyMatrix4( matrix4 );
		this.direction.transformDirection( matrix4 );

		return this;

	}

	/**
	 * Returns `true` if this ray is equal with the given one.
	 *
	 * @param {Ray} ray - The ray to test for equality.
	 * @return {boolean} Whether this ray is equal with the given one.
	 */
	equals( ray ) {

		return ray.origin.equals( this.origin ) && ray.direction.equals( this.direction );

	}

	/**
	 * Returns a new ray with copied values from this instance.
	 *
	 * @return {Ray} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

}

const _v0$2 = /*@__PURE__*/ new Vector3();
const _v1$2 = /*@__PURE__*/ new Vector3();
const _v2$1 = /*@__PURE__*/ new Vector3();
const _v3$1 = /*@__PURE__*/ new Vector3();

const _vab = /*@__PURE__*/ new Vector3();
const _vac = /*@__PURE__*/ new Vector3();
const _vbc = /*@__PURE__*/ new Vector3();
const _vap = /*@__PURE__*/ new Vector3();
const _vbp = /*@__PURE__*/ new Vector3();
const _vcp = /*@__PURE__*/ new Vector3();

const _v40 = /*@__PURE__*/ new Vector4();
const _v41 = /*@__PURE__*/ new Vector4();
const _v42 = /*@__PURE__*/ new Vector4();

/**
 * A geometric triangle as defined by three vectors representing its three corners.
 */
class Triangle {

	/**
	 * Constructs a new triangle.
	 *
	 * @param {Vector3} [a=(0,0,0)] - The first corner of the triangle.
	 * @param {Vector3} [b=(0,0,0)] - The second corner of the triangle.
	 * @param {Vector3} [c=(0,0,0)] - The third corner of the triangle.
	 */
	constructor( a = new Vector3(), b = new Vector3(), c = new Vector3() ) {

		/**
		 * The first corner of the triangle.
		 *
		 * @type {Vector3}
		 */
		this.a = a;

		/**
		 * The second corner of the triangle.
		 *
		 * @type {Vector3}
		 */
		this.b = b;

		/**
		 * The third corner of the triangle.
		 *
		 * @type {Vector3}
		 */
		this.c = c;

	}

	/**
	 * Computes the normal vector of a triangle.
	 *
	 * @param {Vector3} a - The first corner of the triangle.
	 * @param {Vector3} b - The second corner of the triangle.
	 * @param {Vector3} c - The third corner of the triangle.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The triangle's normal.
	 */
	static getNormal( a, b, c, target ) {

		target.subVectors( c, b );
		_v0$2.subVectors( a, b );
		target.cross( _v0$2 );

		const targetLengthSq = target.lengthSq();
		if ( targetLengthSq > 0 ) {

			return target.multiplyScalar( 1 / Math.sqrt( targetLengthSq ) );

		}

		return target.set( 0, 0, 0 );

	}

	/**
	 * Computes a barycentric coordinates from the given vector.
	 * Returns `null` if the triangle is degenerate.
	 *
	 * @param {Vector3} point - A point in 3D space.
	 * @param {Vector3} a - The first corner of the triangle.
	 * @param {Vector3} b - The second corner of the triangle.
	 * @param {Vector3} c - The third corner of the triangle.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {?Vector3} The barycentric coordinates for the given point
	 */
	static getBarycoord( point, a, b, c, target ) {

		// based on: http://www.blackpawn.com/texts/pointinpoly/default.html

		_v0$2.subVectors( c, a );
		_v1$2.subVectors( b, a );
		_v2$1.subVectors( point, a );

		const dot00 = _v0$2.dot( _v0$2 );
		const dot01 = _v0$2.dot( _v1$2 );
		const dot02 = _v0$2.dot( _v2$1 );
		const dot11 = _v1$2.dot( _v1$2 );
		const dot12 = _v1$2.dot( _v2$1 );

		const denom = ( dot00 * dot11 - dot01 * dot01 );

		// collinear or singular triangle
		if ( denom === 0 ) {

			target.set( 0, 0, 0 );
			return null;

		}

		const invDenom = 1 / denom;
		const u = ( dot11 * dot02 - dot01 * dot12 ) * invDenom;
		const v = ( dot00 * dot12 - dot01 * dot02 ) * invDenom;

		// barycentric coordinates must always sum to 1
		return target.set( 1 - u - v, v, u );

	}

	/**
	 * Returns `true` if the given point, when projected onto the plane of the
	 * triangle, lies within the triangle.
	 *
	 * @param {Vector3} point - The point in 3D space to test.
	 * @param {Vector3} a - The first corner of the triangle.
	 * @param {Vector3} b - The second corner of the triangle.
	 * @param {Vector3} c - The third corner of the triangle.
	 * @return {boolean} Whether the given point, when projected onto the plane of the
	 * triangle, lies within the triangle or not.
	 */
	static containsPoint( point, a, b, c ) {

		// if the triangle is degenerate then we can't contain a point
		if ( this.getBarycoord( point, a, b, c, _v3$1 ) === null ) {

			return false;

		}

		return ( _v3$1.x >= 0 ) && ( _v3$1.y >= 0 ) && ( ( _v3$1.x + _v3$1.y ) <= 1 );

	}

	/**
	 * Computes the value barycentrically interpolated for the given point on the
	 * triangle. Returns `null` if the triangle is degenerate.
	 *
	 * @param {Vector3} point - Position of interpolated point.
	 * @param {Vector3} p1 - The first corner of the triangle.
	 * @param {Vector3} p2 - The second corner of the triangle.
	 * @param {Vector3} p3 - The third corner of the triangle.
	 * @param {Vector3} v1 - Value to interpolate of first vertex.
	 * @param {Vector3} v2 - Value to interpolate of second vertex.
	 * @param {Vector3} v3 - Value to interpolate of third vertex.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {?Vector3} The interpolated value.
	 */
	static getInterpolation( point, p1, p2, p3, v1, v2, v3, target ) {

		if ( this.getBarycoord( point, p1, p2, p3, _v3$1 ) === null ) {

			target.x = 0;
			target.y = 0;
			if ( 'z' in target ) target.z = 0;
			if ( 'w' in target ) target.w = 0;
			return null;

		}

		target.setScalar( 0 );
		target.addScaledVector( v1, _v3$1.x );
		target.addScaledVector( v2, _v3$1.y );
		target.addScaledVector( v3, _v3$1.z );

		return target;

	}

	/**
	 * Computes the value barycentrically interpolated for the given attribute and indices.
	 *
	 * @param {BufferAttribute} attr - The attribute to interpolate.
	 * @param {number} i1 - Index of first vertex.
	 * @param {number} i2 - Index of second vertex.
	 * @param {number} i3 - Index of third vertex.
	 * @param {Vector3} barycoord - The barycoordinate value to use to interpolate.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The interpolated attribute value.
	 */
	static getInterpolatedAttribute( attr, i1, i2, i3, barycoord, target ) {

		_v40.setScalar( 0 );
		_v41.setScalar( 0 );
		_v42.setScalar( 0 );

		_v40.fromBufferAttribute( attr, i1 );
		_v41.fromBufferAttribute( attr, i2 );
		_v42.fromBufferAttribute( attr, i3 );

		target.setScalar( 0 );
		target.addScaledVector( _v40, barycoord.x );
		target.addScaledVector( _v41, barycoord.y );
		target.addScaledVector( _v42, barycoord.z );

		return target;

	}

	/**
	 * Returns `true` if the triangle is oriented towards the given direction.
	 *
	 * @param {Vector3} a - The first corner of the triangle.
	 * @param {Vector3} b - The second corner of the triangle.
	 * @param {Vector3} c - The third corner of the triangle.
	 * @param {Vector3} direction - The (normalized) direction vector.
	 * @return {boolean} Whether the triangle is oriented towards the given direction or not.
	 */
	static isFrontFacing( a, b, c, direction ) {

		_v0$2.subVectors( c, b );
		_v1$2.subVectors( a, b );

		// strictly front facing
		return ( _v0$2.cross( _v1$2 ).dot( direction ) < 0 ) ? true : false;

	}

	/**
	 * Sets the triangle's vertices by copying the given values.
	 *
	 * @param {Vector3} a - The first corner of the triangle.
	 * @param {Vector3} b - The second corner of the triangle.
	 * @param {Vector3} c - The third corner of the triangle.
	 * @return {Triangle} A reference to this triangle.
	 */
	set( a, b, c ) {

		this.a.copy( a );
		this.b.copy( b );
		this.c.copy( c );

		return this;

	}

	/**
	 * Sets the triangle's vertices by copying the given array values.
	 *
	 * @param {Array<Vector3>} points - An array with 3D points.
	 * @param {number} i0 - The array index representing the first corner of the triangle.
	 * @param {number} i1 - The array index representing the second corner of the triangle.
	 * @param {number} i2 - The array index representing the third corner of the triangle.
	 * @return {Triangle} A reference to this triangle.
	 */
	setFromPointsAndIndices( points, i0, i1, i2 ) {

		this.a.copy( points[ i0 ] );
		this.b.copy( points[ i1 ] );
		this.c.copy( points[ i2 ] );

		return this;

	}

	/**
	 * Sets the triangle's vertices by copying the given attribute values.
	 *
	 * @param {BufferAttribute} attribute - A buffer attribute with 3D points data.
	 * @param {number} i0 - The attribute index representing the first corner of the triangle.
	 * @param {number} i1 - The attribute index representing the second corner of the triangle.
	 * @param {number} i2 - The attribute index representing the third corner of the triangle.
	 * @return {Triangle} A reference to this triangle.
	 */
	setFromAttributeAndIndices( attribute, i0, i1, i2 ) {

		this.a.fromBufferAttribute( attribute, i0 );
		this.b.fromBufferAttribute( attribute, i1 );
		this.c.fromBufferAttribute( attribute, i2 );

		return this;

	}

	/**
	 * Returns a new triangle with copied values from this instance.
	 *
	 * @return {Triangle} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

	/**
	 * Copies the values of the given triangle to this instance.
	 *
	 * @param {Triangle} triangle - The triangle to copy.
	 * @return {Triangle} A reference to this triangle.
	 */
	copy( triangle ) {

		this.a.copy( triangle.a );
		this.b.copy( triangle.b );
		this.c.copy( triangle.c );

		return this;

	}

	/**
	 * Computes the area of the triangle.
	 *
	 * @return {number} The triangle's area.
	 */
	getArea() {

		_v0$2.subVectors( this.c, this.b );
		_v1$2.subVectors( this.a, this.b );

		return _v0$2.cross( _v1$2 ).length() * 0.5;

	}

	/**
	 * Computes the midpoint of the triangle.
	 *
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The triangle's midpoint.
	 */
	getMidpoint( target ) {

		return target.addVectors( this.a, this.b ).add( this.c ).multiplyScalar( 1 / 3 );

	}

	/**
	 * Computes the normal of the triangle.
	 *
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The triangle's normal.
	 */
	getNormal( target ) {

		return Triangle.getNormal( this.a, this.b, this.c, target );

	}

	/**
	 * Computes a plane the triangle lies within.
	 *
	 * @param {Plane} target - The target vector that is used to store the method's result.
	 * @return {Plane} The plane the triangle lies within.
	 */
	getPlane( target ) {

		return target.setFromCoplanarPoints( this.a, this.b, this.c );

	}

	/**
	 * Computes a barycentric coordinates from the given vector.
	 * Returns `null` if the triangle is degenerate.
	 *
	 * @param {Vector3} point - A point in 3D space.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {?Vector3} The barycentric coordinates for the given point
	 */
	getBarycoord( point, target ) {

		return Triangle.getBarycoord( point, this.a, this.b, this.c, target );

	}

	/**
	 * Computes the value barycentrically interpolated for the given point on the
	 * triangle. Returns `null` if the triangle is degenerate.
	 *
	 * @param {Vector3} point - Position of interpolated point.
	 * @param {Vector3} v1 - Value to interpolate of first vertex.
	 * @param {Vector3} v2 - Value to interpolate of second vertex.
	 * @param {Vector3} v3 - Value to interpolate of third vertex.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {?Vector3} The interpolated value.
	 */
	getInterpolation( point, v1, v2, v3, target ) {

		return Triangle.getInterpolation( point, this.a, this.b, this.c, v1, v2, v3, target );

	}

	/**
	 * Returns `true` if the given point, when projected onto the plane of the
	 * triangle, lies within the triangle.
	 *
	 * @param {Vector3} point - The point in 3D space to test.
	 * @return {boolean} Whether the given point, when projected onto the plane of the
	 * triangle, lies within the triangle or not.
	 */
	containsPoint( point ) {

		return Triangle.containsPoint( point, this.a, this.b, this.c );

	}

	/**
	 * Returns `true` if the triangle is oriented towards the given direction.
	 *
	 * @param {Vector3} direction - The (normalized) direction vector.
	 * @return {boolean} Whether the triangle is oriented towards the given direction or not.
	 */
	isFrontFacing( direction ) {

		return Triangle.isFrontFacing( this.a, this.b, this.c, direction );

	}

	/**
	 * Returns `true` if this triangle intersects with the given box.
	 *
	 * @param {Box3} box - The box to intersect.
	 * @return {boolean} Whether this triangle intersects with the given box or not.
	 */
	intersectsBox( box ) {

		return box.intersectsTriangle( this );

	}

	/**
	 * Returns the closest point on the triangle to the given point.
	 *
	 * @param {Vector3} p - The point to compute the closest point for.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The closest point on the triangle.
	 */
	closestPointToPoint( p, target ) {

		const a = this.a, b = this.b, c = this.c;
		let v, w;

		// algorithm thanks to Real-Time Collision Detection by Christer Ericson,
		// published by Morgan Kaufmann Publishers, (c) 2005 Elsevier Inc.,
		// under the accompanying license; see chapter 5.1.5 for detailed explanation.
		// basically, we're distinguishing which of the voronoi regions of the triangle
		// the point lies in with the minimum amount of redundant computation.

		_vab.subVectors( b, a );
		_vac.subVectors( c, a );
		_vap.subVectors( p, a );
		const d1 = _vab.dot( _vap );
		const d2 = _vac.dot( _vap );
		if ( d1 <= 0 && d2 <= 0 ) {

			// vertex region of A; barycentric coords (1, 0, 0)
			return target.copy( a );

		}

		_vbp.subVectors( p, b );
		const d3 = _vab.dot( _vbp );
		const d4 = _vac.dot( _vbp );
		if ( d3 >= 0 && d4 <= d3 ) {

			// vertex region of B; barycentric coords (0, 1, 0)
			return target.copy( b );

		}

		const vc = d1 * d4 - d3 * d2;
		if ( vc <= 0 && d1 >= 0 && d3 <= 0 ) {

			v = d1 / ( d1 - d3 );
			// edge region of AB; barycentric coords (1-v, v, 0)
			return target.copy( a ).addScaledVector( _vab, v );

		}

		_vcp.subVectors( p, c );
		const d5 = _vab.dot( _vcp );
		const d6 = _vac.dot( _vcp );
		if ( d6 >= 0 && d5 <= d6 ) {

			// vertex region of C; barycentric coords (0, 0, 1)
			return target.copy( c );

		}

		const vb = d5 * d2 - d1 * d6;
		if ( vb <= 0 && d2 >= 0 && d6 <= 0 ) {

			w = d2 / ( d2 - d6 );
			// edge region of AC; barycentric coords (1-w, 0, w)
			return target.copy( a ).addScaledVector( _vac, w );

		}

		const va = d3 * d6 - d5 * d4;
		if ( va <= 0 && ( d4 - d3 ) >= 0 && ( d5 - d6 ) >= 0 ) {

			_vbc.subVectors( c, b );
			w = ( d4 - d3 ) / ( ( d4 - d3 ) + ( d5 - d6 ) );
			// edge region of BC; barycentric coords (0, 1-w, w)
			return target.copy( b ).addScaledVector( _vbc, w ); // edge region of BC

		}

		// face region
		const denom = 1 / ( va + vb + vc );
		// u = va * denom
		v = vb * denom;
		w = vc * denom;

		return target.copy( a ).addScaledVector( _vab, v ).addScaledVector( _vac, w );

	}

	/**
	 * Returns `true` if this triangle is equal with the given one.
	 *
	 * @param {Triangle} triangle - The triangle to test for equality.
	 * @return {boolean} Whether this triangle is equal with the given one.
	 */
	equals( triangle ) {

		return triangle.a.equals( this.a ) && triangle.b.equals( this.b ) && triangle.c.equals( this.c );

	}

}

/**
 * A material for drawing geometries in a simple shaded (flat or wireframe) way.
 *
 * This material is not affected by lights.
 *
 * @augments Material
 */
class MeshBasicMaterial extends Material {

	/**
	 * Constructs a new mesh basic material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isMeshBasicMaterial = true;

		this.type = 'MeshBasicMaterial';

		/**
		 * Color of the material.
		 *
		 * @type {Color}
		 * @default (1,1,1)
		 */
		this.color = new Color( 0xffffff ); // diffuse

		/**
		 * The color map. May optionally include an alpha channel, typically combined
		 * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
		 * color is modulated by the diffuse `color`.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.map = null;

		/**
		 * The light map. Requires a second set of UVs.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.lightMap = null;

		/**
		 * Intensity of the baked light.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.lightMapIntensity = 1.0;

		/**
		 * The red channel of this texture is used as the ambient occlusion map.
		 * Requires a second set of UVs.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.aoMap = null;

		/**
		 * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`
		 * disables ambient occlusion. Where intensity is `1` and the AO map's
		 * red channel is also `1`, ambient light is fully occluded on a surface.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.aoMapIntensity = 1.0;

		/**
		 * Specular map used by the material.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.specularMap = null;

		/**
		 * The alpha map is a grayscale texture that controls the opacity across the
		 * surface (black: fully transparent; white: fully opaque).
		 *
		 * Only the color of the texture is used, ignoring the alpha channel if one
		 * exists. For RGB and RGBA textures, the renderer will use the green channel
		 * when sampling this texture due to the extra bit of precision provided for
		 * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
		 * luminance/alpha textures will also still work as expected.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.alphaMap = null;

		/**
		 * The environment map.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.envMap = null;

		/**
		 * The rotation of the environment map in radians.
		 *
		 * @type {Euler}
		 * @default (0,0,0)
		 */
		this.envMapRotation = new Euler();

		/**
		 * How to combine the result of the surface's color with the environment map, if any.
		 *
		 * When set to `MixOperation`, the {@link MeshBasicMaterial#reflectivity} is used to
		 * blend between the two colors.
		 *
		 * @type {(MultiplyOperation|MixOperation|AddOperation)}
		 * @default MultiplyOperation
		 */
		this.combine = MultiplyOperation;

		/**
		 * How much the environment map affects the surface.
		 * The valid range is between `0` (no reflections) and `1` (full reflections).
		 *
		 * @type {number}
		 * @default 1
		 */
		this.reflectivity = 1;

		/**
		 * The index of refraction (IOR) of air (approximately 1) divided by the
		 * index of refraction of the material. It is used with environment mapping
		 * modes {@link CubeRefractionMapping} and {@link EquirectangularRefractionMapping}.
		 * The refraction ratio should not exceed `1`.
		 *
		 * @type {number}
		 * @default 0.98
		 */
		this.refractionRatio = 0.98;

		/**
		 * Renders the geometry as a wireframe.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.wireframe = false;

		/**
		 * Controls the thickness of the wireframe.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.wireframeLinewidth = 1;

		/**
		 * Defines appearance of wireframe ends.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {('round'|'bevel'|'miter')}
		 * @default 'round'
		 */
		this.wireframeLinecap = 'round';

		/**
		 * Defines appearance of wireframe joints.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {('round'|'bevel'|'miter')}
		 * @default 'round'
		 */
		this.wireframeLinejoin = 'round';

		/**
		 * Whether the material is affected by fog or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.fog = true;

		this.setValues( parameters );

	}

	copy( source ) {

		super.copy( source );

		this.color.copy( source.color );

		this.map = source.map;

		this.lightMap = source.lightMap;
		this.lightMapIntensity = source.lightMapIntensity;

		this.aoMap = source.aoMap;
		this.aoMapIntensity = source.aoMapIntensity;

		this.specularMap = source.specularMap;

		this.alphaMap = source.alphaMap;

		this.envMap = source.envMap;
		this.envMapRotation.copy( source.envMapRotation );
		this.combine = source.combine;
		this.reflectivity = source.reflectivity;
		this.refractionRatio = source.refractionRatio;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;
		this.wireframeLinecap = source.wireframeLinecap;
		this.wireframeLinejoin = source.wireframeLinejoin;

		this.fog = source.fog;

		return this;

	}

}

const _inverseMatrix$3 = /*@__PURE__*/ new Matrix4();
const _ray$3 = /*@__PURE__*/ new Ray();
const _sphere$5 = /*@__PURE__*/ new Sphere();
const _sphereHitAt = /*@__PURE__*/ new Vector3();

const _vA$1 = /*@__PURE__*/ new Vector3();
const _vB$1 = /*@__PURE__*/ new Vector3();
const _vC$1 = /*@__PURE__*/ new Vector3();

const _tempA = /*@__PURE__*/ new Vector3();
const _morphA = /*@__PURE__*/ new Vector3();

const _intersectionPoint = /*@__PURE__*/ new Vector3();
const _intersectionPointWorld = /*@__PURE__*/ new Vector3();

/**
 * Class representing triangular polygon mesh based objects.
 *
 * ```js
 * const geometry = new THREE.BoxGeometry( 1, 1, 1 );
 * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
 * const mesh = new THREE.Mesh( geometry, material );
 * scene.add( mesh );
 * ```
 *
 * @augments Object3D
 */
class Mesh extends Object3D {

	/**
	 * Constructs a new mesh.
	 *
	 * @param {BufferGeometry} [geometry] - The mesh geometry.
	 * @param {Material|Array<Material>} [material] - The mesh material.
	 */
	constructor( geometry = new BufferGeometry(), material = new MeshBasicMaterial() ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isMesh = true;

		this.type = 'Mesh';

		/**
		 * The mesh geometry.
		 *
		 * @type {BufferGeometry}
		 */
		this.geometry = geometry;

		/**
		 * The mesh material.
		 *
		 * @type {Material|Array<Material>}
		 * @default MeshBasicMaterial
		 */
		this.material = material;

		/**
		 * A dictionary representing the morph targets in the geometry. The key is the
		 * morph targets name, the value its attribute index. This member is `undefined`
		 * by default and only set when morph targets are detected in the geometry.
		 *
		 * @type {Object<String,number>|undefined}
		 * @default undefined
		 */
		this.morphTargetDictionary = undefined;

		/**
		 * An array of weights typically in the range `[0,1]` that specify how much of the morph
		 * is applied. This member is `undefined` by default and only set when morph targets are
		 * detected in the geometry.
		 *
		 * @type {Array<number>|undefined}
		 * @default undefined
		 */
		this.morphTargetInfluences = undefined;

		/**
		 * The number of instances of this mesh.
		 * Can only be used with {@link WebGPURenderer}.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.count = 1;

		this.updateMorphTargets();

	}

	copy( source, recursive ) {

		super.copy( source, recursive );

		if ( source.morphTargetInfluences !== undefined ) {

			this.morphTargetInfluences = source.morphTargetInfluences.slice();

		}

		if ( source.morphTargetDictionary !== undefined ) {

			this.morphTargetDictionary = Object.assign( {}, source.morphTargetDictionary );

		}

		this.material = Array.isArray( source.material ) ? source.material.slice() : source.material;
		this.geometry = source.geometry;

		return this;

	}

	/**
	 * Sets the values of {@link Mesh#morphTargetDictionary} and {@link Mesh#morphTargetInfluences}
	 * to make sure existing morph targets can influence this 3D object.
	 */
	updateMorphTargets() {

		const geometry = this.geometry;

		const morphAttributes = geometry.morphAttributes;
		const keys = Object.keys( morphAttributes );

		if ( keys.length > 0 ) {

			const morphAttribute = morphAttributes[ keys[ 0 ] ];

			if ( morphAttribute !== undefined ) {

				this.morphTargetInfluences = [];
				this.morphTargetDictionary = {};

				for ( let m = 0, ml = morphAttribute.length; m < ml; m ++ ) {

					const name = morphAttribute[ m ].name || String( m );

					this.morphTargetInfluences.push( 0 );
					this.morphTargetDictionary[ name ] = m;

				}

			}

		}

	}

	/**
	 * Returns the local-space position of the vertex at the given index, taking into
	 * account the current animation state of both morph targets and skinning.
	 *
	 * @param {number} index - The vertex index.
	 * @param {Vector3} target - The target object that is used to store the method's result.
	 * @return {Vector3} The vertex position in local space.
	 */
	getVertexPosition( index, target ) {

		const geometry = this.geometry;
		const position = geometry.attributes.position;
		const morphPosition = geometry.morphAttributes.position;
		const morphTargetsRelative = geometry.morphTargetsRelative;

		target.fromBufferAttribute( position, index );

		const morphInfluences = this.morphTargetInfluences;

		if ( morphPosition && morphInfluences ) {

			_morphA.set( 0, 0, 0 );

			for ( let i = 0, il = morphPosition.length; i < il; i ++ ) {

				const influence = morphInfluences[ i ];
				const morphAttribute = morphPosition[ i ];

				if ( influence === 0 ) continue;

				_tempA.fromBufferAttribute( morphAttribute, index );

				if ( morphTargetsRelative ) {

					_morphA.addScaledVector( _tempA, influence );

				} else {

					_morphA.addScaledVector( _tempA.sub( target ), influence );

				}

			}

			target.add( _morphA );

		}

		return target;

	}

	/**
	 * Computes intersection points between a casted ray and this line.
	 *
	 * @param {Raycaster} raycaster - The raycaster.
	 * @param {Array<Object>} intersects - The target array that holds the intersection points.
	 */
	raycast( raycaster, intersects ) {

		const geometry = this.geometry;
		const material = this.material;
		const matrixWorld = this.matrixWorld;

		if ( material === undefined ) return;

		// test with bounding sphere in world space

		if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();

		_sphere$5.copy( geometry.boundingSphere );
		_sphere$5.applyMatrix4( matrixWorld );

		// check distance from ray origin to bounding sphere

		_ray$3.copy( raycaster.ray ).recast( raycaster.near );

		if ( _sphere$5.containsPoint( _ray$3.origin ) === false ) {

			if ( _ray$3.intersectSphere( _sphere$5, _sphereHitAt ) === null ) return;

			if ( _ray$3.origin.distanceToSquared( _sphereHitAt ) > ( raycaster.far - raycaster.near ) ** 2 ) return;

		}

		// convert ray to local space of mesh

		_inverseMatrix$3.copy( matrixWorld ).invert();
		_ray$3.copy( raycaster.ray ).applyMatrix4( _inverseMatrix$3 );

		// test with bounding box in local space

		if ( geometry.boundingBox !== null ) {

			if ( _ray$3.intersectsBox( geometry.boundingBox ) === false ) return;

		}

		// test for intersections with geometry

		this._computeIntersections( raycaster, intersects, _ray$3 );

	}

	_computeIntersections( raycaster, intersects, rayLocalSpace ) {

		let intersection;

		const geometry = this.geometry;
		const material = this.material;

		const index = geometry.index;
		const position = geometry.attributes.position;
		const uv = geometry.attributes.uv;
		const uv1 = geometry.attributes.uv1;
		const normal = geometry.attributes.normal;
		const groups = geometry.groups;
		const drawRange = geometry.drawRange;

		if ( index !== null ) {

			// indexed buffer geometry

			if ( Array.isArray( material ) ) {

				for ( let i = 0, il = groups.length; i < il; i ++ ) {

					const group = groups[ i ];
					const groupMaterial = material[ group.materialIndex ];

					const start = Math.max( group.start, drawRange.start );
					const end = Math.min( index.count, Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) ) );

					for ( let j = start, jl = end; j < jl; j += 3 ) {

						const a = index.getX( j );
						const b = index.getX( j + 1 );
						const c = index.getX( j + 2 );

						intersection = checkGeometryIntersection( this, groupMaterial, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c );

						if ( intersection ) {

							intersection.faceIndex = Math.floor( j / 3 ); // triangle number in indexed buffer semantics
							intersection.face.materialIndex = group.materialIndex;
							intersects.push( intersection );

						}

					}

				}

			} else {

				const start = Math.max( 0, drawRange.start );
				const end = Math.min( index.count, ( drawRange.start + drawRange.count ) );

				for ( let i = start, il = end; i < il; i += 3 ) {

					const a = index.getX( i );
					const b = index.getX( i + 1 );
					const c = index.getX( i + 2 );

					intersection = checkGeometryIntersection( this, material, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c );

					if ( intersection ) {

						intersection.faceIndex = Math.floor( i / 3 ); // triangle number in indexed buffer semantics
						intersects.push( intersection );

					}

				}

			}

		} else if ( position !== undefined ) {

			// non-indexed buffer geometry

			if ( Array.isArray( material ) ) {

				for ( let i = 0, il = groups.length; i < il; i ++ ) {

					const group = groups[ i ];
					const groupMaterial = material[ group.materialIndex ];

					const start = Math.max( group.start, drawRange.start );
					const end = Math.min( position.count, Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) ) );

					for ( let j = start, jl = end; j < jl; j += 3 ) {

						const a = j;
						const b = j + 1;
						const c = j + 2;

						intersection = checkGeometryIntersection( this, groupMaterial, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c );

						if ( intersection ) {

							intersection.faceIndex = Math.floor( j / 3 ); // triangle number in non-indexed buffer semantics
							intersection.face.materialIndex = group.materialIndex;
							intersects.push( intersection );

						}

					}

				}

			} else {

				const start = Math.max( 0, drawRange.start );
				const end = Math.min( position.count, ( drawRange.start + drawRange.count ) );

				for ( let i = start, il = end; i < il; i += 3 ) {

					const a = i;
					const b = i + 1;
					const c = i + 2;

					intersection = checkGeometryIntersection( this, material, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c );

					if ( intersection ) {

						intersection.faceIndex = Math.floor( i / 3 ); // triangle number in non-indexed buffer semantics
						intersects.push( intersection );

					}

				}

			}

		}

	}

}

function checkIntersection$1( object, material, raycaster, ray, pA, pB, pC, point ) {

	let intersect;

	if ( material.side === BackSide ) {

		intersect = ray.intersectTriangle( pC, pB, pA, true, point );

	} else {

		intersect = ray.intersectTriangle( pA, pB, pC, ( material.side === FrontSide ), point );

	}

	if ( intersect === null ) return null;

	_intersectionPointWorld.copy( point );
	_intersectionPointWorld.applyMatrix4( object.matrixWorld );

	const distance = raycaster.ray.origin.distanceTo( _intersectionPointWorld );

	if ( distance < raycaster.near || distance > raycaster.far ) return null;

	return {
		distance: distance,
		point: _intersectionPointWorld.clone(),
		object: object
	};

}

function checkGeometryIntersection( object, material, raycaster, ray, uv, uv1, normal, a, b, c ) {

	object.getVertexPosition( a, _vA$1 );
	object.getVertexPosition( b, _vB$1 );
	object.getVertexPosition( c, _vC$1 );

	const intersection = checkIntersection$1( object, material, raycaster, ray, _vA$1, _vB$1, _vC$1, _intersectionPoint );

	if ( intersection ) {

		const barycoord = new Vector3();
		Triangle.getBarycoord( _intersectionPoint, _vA$1, _vB$1, _vC$1, barycoord );

		if ( uv ) {

			intersection.uv = Triangle.getInterpolatedAttribute( uv, a, b, c, barycoord, new Vector2() );

		}

		if ( uv1 ) {

			intersection.uv1 = Triangle.getInterpolatedAttribute( uv1, a, b, c, barycoord, new Vector2() );

		}

		if ( normal ) {

			intersection.normal = Triangle.getInterpolatedAttribute( normal, a, b, c, barycoord, new Vector3() );

			if ( intersection.normal.dot( ray.direction ) > 0 ) {

				intersection.normal.multiplyScalar( -1 );

			}

		}

		const face = {
			a: a,
			b: b,
			c: c,
			normal: new Vector3(),
			materialIndex: 0
		};

		Triangle.getNormal( _vA$1, _vB$1, _vC$1, face.normal );

		intersection.face = face;
		intersection.barycoord = barycoord;

	}

	return intersection;

}

var alphahash_fragment = "#ifdef USE_ALPHAHASH\n\tif ( diffuseColor.a < getAlphaHashThreshold( vPosition ) ) discard;\n#endif";

var alphahash_pars_fragment = "#ifdef USE_ALPHAHASH\n\tconst float ALPHA_HASH_SCALE = 0.05;\n\tfloat hash2D( vec2 value ) {\n\t\treturn fract( 1.0e4 * sin( 17.0 * value.x + 0.1 * value.y ) * ( 0.1 + abs( sin( 13.0 * value.y + value.x ) ) ) );\n\t}\n\tfloat hash3D( vec3 value ) {\n\t\treturn hash2D( vec2( hash2D( value.xy ), value.z ) );\n\t}\n\tfloat getAlphaHashThreshold( vec3 position ) {\n\t\tfloat maxDeriv = max(\n\t\t\tlength( dFdx( position.xyz ) ),\n\t\t\tlength( dFdy( position.xyz ) )\n\t\t);\n\t\tfloat pixScale = 1.0 / ( ALPHA_HASH_SCALE * maxDeriv );\n\t\tvec2 pixScales = vec2(\n\t\t\texp2( floor( log2( pixScale ) ) ),\n\t\t\texp2( ceil( log2( pixScale ) ) )\n\t\t);\n\t\tvec2 alpha = vec2(\n\t\t\thash3D( floor( pixScales.x * position.xyz ) ),\n\t\t\thash3D( floor( pixScales.y * position.xyz ) )\n\t\t);\n\t\tfloat lerpFactor = fract( log2( pixScale ) );\n\t\tfloat x = ( 1.0 - lerpFactor ) * alpha.x + lerpFactor * alpha.y;\n\t\tfloat a = min( lerpFactor, 1.0 - lerpFactor );\n\t\tvec3 cases = vec3(\n\t\t\tx * x / ( 2.0 * a * ( 1.0 - a ) ),\n\t\t\t( x - 0.5 * a ) / ( 1.0 - a ),\n\t\t\t1.0 - ( ( 1.0 - x ) * ( 1.0 - x ) / ( 2.0 * a * ( 1.0 - a ) ) )\n\t\t);\n\t\tfloat threshold = ( x < ( 1.0 - a ) )\n\t\t\t? ( ( x < a ) ? cases.x : cases.y )\n\t\t\t: cases.z;\n\t\treturn clamp( threshold , 1.0e-6, 1.0 );\n\t}\n#endif";

var alphamap_fragment = "#ifdef USE_ALPHAMAP\n\tdiffuseColor.a *= texture2D( alphaMap, vAlphaMapUv ).g;\n#endif";

var alphamap_pars_fragment = "#ifdef USE_ALPHAMAP\n\tuniform sampler2D alphaMap;\n#endif";

var alphatest_fragment = "#ifdef USE_ALPHATEST\n\t#ifdef ALPHA_TO_COVERAGE\n\tdiffuseColor.a = smoothstep( alphaTest, alphaTest + fwidth( diffuseColor.a ), diffuseColor.a );\n\tif ( diffuseColor.a == 0.0 ) discard;\n\t#else\n\tif ( diffuseColor.a < alphaTest ) discard;\n\t#endif\n#endif";

var alphatest_pars_fragment = "#ifdef USE_ALPHATEST\n\tuniform float alphaTest;\n#endif";

var aomap_fragment = "#ifdef USE_AOMAP\n\tfloat ambientOcclusion = ( texture2D( aoMap, vAoMapUv ).r - 1.0 ) * aoMapIntensity + 1.0;\n\treflectedLight.indirectDiffuse *= ambientOcclusion;\n\t#if defined( USE_CLEARCOAT ) \n\t\tclearcoatSpecularIndirect *= ambientOcclusion;\n\t#endif\n\t#if defined( USE_SHEEN ) \n\t\tsheenSpecularIndirect *= ambientOcclusion;\n\t#endif\n\t#if defined( USE_ENVMAP ) && defined( STANDARD )\n\t\tfloat dotNV = saturate( dot( geometryNormal, geometryViewDir ) );\n\t\treflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness );\n\t#endif\n#endif";

var aomap_pars_fragment = "#ifdef USE_AOMAP\n\tuniform sampler2D aoMap;\n\tuniform float aoMapIntensity;\n#endif";

var batching_pars_vertex = "#ifdef USE_BATCHING\n\t#if ! defined( GL_ANGLE_multi_draw )\n\t#define gl_DrawID _gl_DrawID\n\tuniform int _gl_DrawID;\n\t#endif\n\tuniform highp sampler2D batchingTexture;\n\tuniform highp usampler2D batchingIdTexture;\n\tmat4 getBatchingMatrix( const in float i ) {\n\t\tint size = textureSize( batchingTexture, 0 ).x;\n\t\tint j = int( i ) * 4;\n\t\tint x = j % size;\n\t\tint y = j / size;\n\t\tvec4 v1 = texelFetch( batchingTexture, ivec2( x, y ), 0 );\n\t\tvec4 v2 = texelFetch( batchingTexture, ivec2( x + 1, y ), 0 );\n\t\tvec4 v3 = texelFetch( batchingTexture, ivec2( x + 2, y ), 0 );\n\t\tvec4 v4 = texelFetch( batchingTexture, ivec2( x + 3, y ), 0 );\n\t\treturn mat4( v1, v2, v3, v4 );\n\t}\n\tfloat getIndirectIndex( const in int i ) {\n\t\tint size = textureSize( batchingIdTexture, 0 ).x;\n\t\tint x = i % size;\n\t\tint y = i / size;\n\t\treturn float( texelFetch( batchingIdTexture, ivec2( x, y ), 0 ).r );\n\t}\n#endif\n#ifdef USE_BATCHING_COLOR\n\tuniform sampler2D batchingColorTexture;\n\tvec3 getBatchingColor( const in float i ) {\n\t\tint size = textureSize( batchingColorTexture, 0 ).x;\n\t\tint j = int( i );\n\t\tint x = j % size;\n\t\tint y = j / size;\n\t\treturn texelFetch( batchingColorTexture, ivec2( x, y ), 0 ).rgb;\n\t}\n#endif";

var batching_vertex = "#ifdef USE_BATCHING\n\tmat4 batchingMatrix = getBatchingMatrix( getIndirectIndex( gl_DrawID ) );\n#endif";

var begin_vertex = "vec3 transformed = vec3( position );\n#ifdef USE_ALPHAHASH\n\tvPosition = vec3( position );\n#endif";

var beginnormal_vertex = "vec3 objectNormal = vec3( normal );\n#ifdef USE_TANGENT\n\tvec3 objectTangent = vec3( tangent.xyz );\n#endif";

var bsdfs = "float G_BlinnPhong_Implicit( ) {\n\treturn 0.25;\n}\nfloat D_BlinnPhong( const in float shininess, const in float dotNH ) {\n\treturn RECIPROCAL_PI * ( shininess * 0.5 + 1.0 ) * pow( dotNH, shininess );\n}\nvec3 BRDF_BlinnPhong( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in vec3 specularColor, const in float shininess ) {\n\tvec3 halfDir = normalize( lightDir + viewDir );\n\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\tfloat dotVH = saturate( dot( viewDir, halfDir ) );\n\tvec3 F = F_Schlick( specularColor, 1.0, dotVH );\n\tfloat G = G_BlinnPhong_Implicit( );\n\tfloat D = D_BlinnPhong( shininess, dotNH );\n\treturn F * ( G * D );\n} // validated";

var iridescence_fragment = "#ifdef USE_IRIDESCENCE\n\tconst mat3 XYZ_TO_REC709 = mat3(\n\t\t 3.2404542, -0.9692660,  0.0556434,\n\t\t-1.5371385,  1.8760108, -0.2040259,\n\t\t-0.4985314,  0.0415560,  1.0572252\n\t);\n\tvec3 Fresnel0ToIor( vec3 fresnel0 ) {\n\t\tvec3 sqrtF0 = sqrt( fresnel0 );\n\t\treturn ( vec3( 1.0 ) + sqrtF0 ) / ( vec3( 1.0 ) - sqrtF0 );\n\t}\n\tvec3 IorToFresnel0( vec3 transmittedIor, float incidentIor ) {\n\t\treturn pow2( ( transmittedIor - vec3( incidentIor ) ) / ( transmittedIor + vec3( incidentIor ) ) );\n\t}\n\tfloat IorToFresnel0( float transmittedIor, float incidentIor ) {\n\t\treturn pow2( ( transmittedIor - incidentIor ) / ( transmittedIor + incidentIor ));\n\t}\n\tvec3 evalSensitivity( float OPD, vec3 shift ) {\n\t\tfloat phase = 2.0 * PI * OPD * 1.0e-9;\n\t\tvec3 val = vec3( 5.4856e-13, 4.4201e-13, 5.2481e-13 );\n\t\tvec3 pos = vec3( 1.6810e+06, 1.7953e+06, 2.2084e+06 );\n\t\tvec3 var = vec3( 4.3278e+09, 9.3046e+09, 6.6121e+09 );\n\t\tvec3 xyz = val * sqrt( 2.0 * PI * var ) * cos( pos * phase + shift ) * exp( - pow2( phase ) * var );\n\t\txyz.x += 9.7470e-14 * sqrt( 2.0 * PI * 4.5282e+09 ) * cos( 2.2399e+06 * phase + shift[ 0 ] ) * exp( - 4.5282e+09 * pow2( phase ) );\n\t\txyz /= 1.0685e-7;\n\t\tvec3 rgb = XYZ_TO_REC709 * xyz;\n\t\treturn rgb;\n\t}\n\tvec3 evalIridescence( float outsideIOR, float eta2, float cosTheta1, float thinFilmThickness, vec3 baseF0 ) {\n\t\tvec3 I;\n\t\tfloat iridescenceIOR = mix( outsideIOR, eta2, smoothstep( 0.0, 0.03, thinFilmThickness ) );\n\t\tfloat sinTheta2Sq = pow2( outsideIOR / iridescenceIOR ) * ( 1.0 - pow2( cosTheta1 ) );\n\t\tfloat cosTheta2Sq = 1.0 - sinTheta2Sq;\n\t\tif ( cosTheta2Sq < 0.0 ) {\n\t\t\treturn vec3( 1.0 );\n\t\t}\n\t\tfloat cosTheta2 = sqrt( cosTheta2Sq );\n\t\tfloat R0 = IorToFresnel0( iridescenceIOR, outsideIOR );\n\t\tfloat R12 = F_Schlick( R0, 1.0, cosTheta1 );\n\t\tfloat T121 = 1.0 - R12;\n\t\tfloat phi12 = 0.0;\n\t\tif ( iridescenceIOR < outsideIOR ) phi12 = PI;\n\t\tfloat phi21 = PI - phi12;\n\t\tvec3 baseIOR = Fresnel0ToIor( clamp( baseF0, 0.0, 0.9999 ) );\t\tvec3 R1 = IorToFresnel0( baseIOR, iridescenceIOR );\n\t\tvec3 R23 = F_Schlick( R1, 1.0, cosTheta2 );\n\t\tvec3 phi23 = vec3( 0.0 );\n\t\tif ( baseIOR[ 0 ] < iridescenceIOR ) phi23[ 0 ] = PI;\n\t\tif ( baseIOR[ 1 ] < iridescenceIOR ) phi23[ 1 ] = PI;\n\t\tif ( baseIOR[ 2 ] < iridescenceIOR ) phi23[ 2 ] = PI;\n\t\tfloat OPD = 2.0 * iridescenceIOR * thinFilmThickness * cosTheta2;\n\t\tvec3 phi = vec3( phi21 ) + phi23;\n\t\tvec3 R123 = clamp( R12 * R23, 1e-5, 0.9999 );\n\t\tvec3 r123 = sqrt( R123 );\n\t\tvec3 Rs = pow2( T121 ) * R23 / ( vec3( 1.0 ) - R123 );\n\t\tvec3 C0 = R12 + Rs;\n\t\tI = C0;\n\t\tvec3 Cm = Rs - T121;\n\t\tfor ( int m = 1; m <= 2; ++ m ) {\n\t\t\tCm *= r123;\n\t\t\tvec3 Sm = 2.0 * evalSensitivity( float( m ) * OPD, float( m ) * phi );\n\t\t\tI += Cm * Sm;\n\t\t}\n\t\treturn max( I, vec3( 0.0 ) );\n\t}\n#endif";

var bumpmap_pars_fragment = "#ifdef USE_BUMPMAP\n\tuniform sampler2D bumpMap;\n\tuniform float bumpScale;\n\tvec2 dHdxy_fwd() {\n\t\tvec2 dSTdx = dFdx( vBumpMapUv );\n\t\tvec2 dSTdy = dFdy( vBumpMapUv );\n\t\tfloat Hll = bumpScale * texture2D( bumpMap, vBumpMapUv ).x;\n\t\tfloat dBx = bumpScale * texture2D( bumpMap, vBumpMapUv + dSTdx ).x - Hll;\n\t\tfloat dBy = bumpScale * texture2D( bumpMap, vBumpMapUv + dSTdy ).x - Hll;\n\t\treturn vec2( dBx, dBy );\n\t}\n\tvec3 perturbNormalArb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy, float faceDirection ) {\n\t\tvec3 vSigmaX = normalize( dFdx( surf_pos.xyz ) );\n\t\tvec3 vSigmaY = normalize( dFdy( surf_pos.xyz ) );\n\t\tvec3 vN = surf_norm;\n\t\tvec3 R1 = cross( vSigmaY, vN );\n\t\tvec3 R2 = cross( vN, vSigmaX );\n\t\tfloat fDet = dot( vSigmaX, R1 ) * faceDirection;\n\t\tvec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 );\n\t\treturn normalize( abs( fDet ) * surf_norm - vGrad );\n\t}\n#endif";

var clipping_planes_fragment = "#if NUM_CLIPPING_PLANES > 0\n\tvec4 plane;\n\t#ifdef ALPHA_TO_COVERAGE\n\t\tfloat distanceToPlane, distanceGradient;\n\t\tfloat clipOpacity = 1.0;\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\n\t\t\tplane = clippingPlanes[ i ];\n\t\t\tdistanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w;\n\t\t\tdistanceGradient = fwidth( distanceToPlane ) / 2.0;\n\t\t\tclipOpacity *= smoothstep( - distanceGradient, distanceGradient, distanceToPlane );\n\t\t\tif ( clipOpacity == 0.0 ) discard;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\n\t\t\tfloat unionClipOpacity = 1.0;\n\t\t\t#pragma unroll_loop_start\n\t\t\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\n\t\t\t\tplane = clippingPlanes[ i ];\n\t\t\t\tdistanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w;\n\t\t\t\tdistanceGradient = fwidth( distanceToPlane ) / 2.0;\n\t\t\t\tunionClipOpacity *= 1.0 - smoothstep( - distanceGradient, distanceGradient, distanceToPlane );\n\t\t\t}\n\t\t\t#pragma unroll_loop_end\n\t\t\tclipOpacity *= 1.0 - unionClipOpacity;\n\t\t#endif\n\t\tdiffuseColor.a *= clipOpacity;\n\t\tif ( diffuseColor.a == 0.0 ) discard;\n\t#else\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\n\t\t\tplane = clippingPlanes[ i ];\n\t\t\tif ( dot( vClipPosition, plane.xyz ) > plane.w ) discard;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\n\t\t\tbool clipped = true;\n\t\t\t#pragma unroll_loop_start\n\t\t\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\n\t\t\t\tplane = clippingPlanes[ i ];\n\t\t\t\tclipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped;\n\t\t\t}\n\t\t\t#pragma unroll_loop_end\n\t\t\tif ( clipped ) discard;\n\t\t#endif\n\t#endif\n#endif";

var clipping_planes_pars_fragment = "#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n\tuniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ];\n#endif";

var clipping_planes_pars_vertex = "#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n#endif";

var clipping_planes_vertex = "#if NUM_CLIPPING_PLANES > 0\n\tvClipPosition = - mvPosition.xyz;\n#endif";

var color_fragment = "#if defined( USE_COLOR_ALPHA )\n\tdiffuseColor *= vColor;\n#elif defined( USE_COLOR )\n\tdiffuseColor.rgb *= vColor;\n#endif";

var color_pars_fragment = "#if defined( USE_COLOR_ALPHA )\n\tvarying vec4 vColor;\n#elif defined( USE_COLOR )\n\tvarying vec3 vColor;\n#endif";

var color_pars_vertex = "#if defined( USE_COLOR_ALPHA )\n\tvarying vec4 vColor;\n#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR )\n\tvarying vec3 vColor;\n#endif";

var color_vertex = "#if defined( USE_COLOR_ALPHA )\n\tvColor = vec4( 1.0 );\n#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR )\n\tvColor = vec3( 1.0 );\n#endif\n#ifdef USE_COLOR\n\tvColor *= color;\n#endif\n#ifdef USE_INSTANCING_COLOR\n\tvColor.xyz *= instanceColor.xyz;\n#endif\n#ifdef USE_BATCHING_COLOR\n\tvec3 batchingColor = getBatchingColor( getIndirectIndex( gl_DrawID ) );\n\tvColor.xyz *= batchingColor.xyz;\n#endif";

var common$1 = "#define PI 3.141592653589793\n#define PI2 6.283185307179586\n#define PI_HALF 1.5707963267948966\n#define RECIPROCAL_PI 0.3183098861837907\n#define RECIPROCAL_PI2 0.15915494309189535\n#define EPSILON 1e-6\n#ifndef saturate\n#define saturate( a ) clamp( a, 0.0, 1.0 )\n#endif\n#define whiteComplement( a ) ( 1.0 - saturate( a ) )\nfloat pow2( const in float x ) { return x*x; }\nvec3 pow2( const in vec3 x ) { return x*x; }\nfloat pow3( const in float x ) { return x*x*x; }\nfloat pow4( const in float x ) { float x2 = x*x; return x2*x2; }\nfloat max3( const in vec3 v ) { return max( max( v.x, v.y ), v.z ); }\nfloat average( const in vec3 v ) { return dot( v, vec3( 0.3333333 ) ); }\nhighp float rand( const in vec2 uv ) {\n\tconst highp float a = 12.9898, b = 78.233, c = 43758.5453;\n\thighp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );\n\treturn fract( sin( sn ) * c );\n}\n#ifdef HIGH_PRECISION\n\tfloat precisionSafeLength( vec3 v ) { return length( v ); }\n#else\n\tfloat precisionSafeLength( vec3 v ) {\n\t\tfloat maxComponent = max3( abs( v ) );\n\t\treturn length( v / maxComponent ) * maxComponent;\n\t}\n#endif\nstruct IncidentLight {\n\tvec3 color;\n\tvec3 direction;\n\tbool visible;\n};\nstruct ReflectedLight {\n\tvec3 directDiffuse;\n\tvec3 directSpecular;\n\tvec3 indirectDiffuse;\n\tvec3 indirectSpecular;\n};\n#ifdef USE_ALPHAHASH\n\tvarying vec3 vPosition;\n#endif\nvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\n}\nvec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );\n}\nmat3 transposeMat3( const in mat3 m ) {\n\tmat3 tmp;\n\ttmp[ 0 ] = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x );\n\ttmp[ 1 ] = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y );\n\ttmp[ 2 ] = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z );\n\treturn tmp;\n}\nbool isPerspectiveMatrix( mat4 m ) {\n\treturn m[ 2 ][ 3 ] == - 1.0;\n}\nvec2 equirectUv( in vec3 dir ) {\n\tfloat u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5;\n\tfloat v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5;\n\treturn vec2( u, v );\n}\nvec3 BRDF_Lambert( const in vec3 diffuseColor ) {\n\treturn RECIPROCAL_PI * diffuseColor;\n}\nvec3 F_Schlick( const in vec3 f0, const in float f90, const in float dotVH ) {\n\tfloat fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH );\n\treturn f0 * ( 1.0 - fresnel ) + ( f90 * fresnel );\n}\nfloat F_Schlick( const in float f0, const in float f90, const in float dotVH ) {\n\tfloat fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH );\n\treturn f0 * ( 1.0 - fresnel ) + ( f90 * fresnel );\n} // validated";

var cube_uv_reflection_fragment = "#ifdef ENVMAP_TYPE_CUBE_UV\n\t#define cubeUV_minMipLevel 4.0\n\t#define cubeUV_minTileSize 16.0\n\tfloat getFace( vec3 direction ) {\n\t\tvec3 absDirection = abs( direction );\n\t\tfloat face = - 1.0;\n\t\tif ( absDirection.x > absDirection.z ) {\n\t\t\tif ( absDirection.x > absDirection.y )\n\t\t\t\tface = direction.x > 0.0 ? 0.0 : 3.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t} else {\n\t\t\tif ( absDirection.z > absDirection.y )\n\t\t\t\tface = direction.z > 0.0 ? 2.0 : 5.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t}\n\t\treturn face;\n\t}\n\tvec2 getUV( vec3 direction, float face ) {\n\t\tvec2 uv;\n\t\tif ( face == 0.0 ) {\n\t\t\tuv = vec2( direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 1.0 ) {\n\t\t\tuv = vec2( - direction.x, - direction.z ) / abs( direction.y );\n\t\t} else if ( face == 2.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.y ) / abs( direction.z );\n\t\t} else if ( face == 3.0 ) {\n\t\t\tuv = vec2( - direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 4.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.z ) / abs( direction.y );\n\t\t} else {\n\t\t\tuv = vec2( direction.x, direction.y ) / abs( direction.z );\n\t\t}\n\t\treturn 0.5 * ( uv + 1.0 );\n\t}\n\tvec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) {\n\t\tfloat face = getFace( direction );\n\t\tfloat filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 );\n\t\tmipInt = max( mipInt, cubeUV_minMipLevel );\n\t\tfloat faceSize = exp2( mipInt );\n\t\thighp vec2 uv = getUV( direction, face ) * ( faceSize - 2.0 ) + 1.0;\n\t\tif ( face > 2.0 ) {\n\t\t\tuv.y += faceSize;\n\t\t\tface -= 3.0;\n\t\t}\n\t\tuv.x += face * faceSize;\n\t\tuv.x += filterInt * 3.0 * cubeUV_minTileSize;\n\t\tuv.y += 4.0 * ( exp2( CUBEUV_MAX_MIP ) - faceSize );\n\t\tuv.x *= CUBEUV_TEXEL_WIDTH;\n\t\tuv.y *= CUBEUV_TEXEL_HEIGHT;\n\t\t#ifdef texture2DGradEXT\n\t\t\treturn texture2DGradEXT( envMap, uv, vec2( 0.0 ), vec2( 0.0 ) ).rgb;\n\t\t#else\n\t\t\treturn texture2D( envMap, uv ).rgb;\n\t\t#endif\n\t}\n\t#define cubeUV_r0 1.0\n\t#define cubeUV_m0 - 2.0\n\t#define cubeUV_r1 0.8\n\t#define cubeUV_m1 - 1.0\n\t#define cubeUV_r4 0.4\n\t#define cubeUV_m4 2.0\n\t#define cubeUV_r5 0.305\n\t#define cubeUV_m5 3.0\n\t#define cubeUV_r6 0.21\n\t#define cubeUV_m6 4.0\n\tfloat roughnessToMip( float roughness ) {\n\t\tfloat mip = 0.0;\n\t\tif ( roughness >= cubeUV_r1 ) {\n\t\t\tmip = ( cubeUV_r0 - roughness ) * ( cubeUV_m1 - cubeUV_m0 ) / ( cubeUV_r0 - cubeUV_r1 ) + cubeUV_m0;\n\t\t} else if ( roughness >= cubeUV_r4 ) {\n\t\t\tmip = ( cubeUV_r1 - roughness ) * ( cubeUV_m4 - cubeUV_m1 ) / ( cubeUV_r1 - cubeUV_r4 ) + cubeUV_m1;\n\t\t} else if ( roughness >= cubeUV_r5 ) {\n\t\t\tmip = ( cubeUV_r4 - roughness ) * ( cubeUV_m5 - cubeUV_m4 ) / ( cubeUV_r4 - cubeUV_r5 ) + cubeUV_m4;\n\t\t} else if ( roughness >= cubeUV_r6 ) {\n\t\t\tmip = ( cubeUV_r5 - roughness ) * ( cubeUV_m6 - cubeUV_m5 ) / ( cubeUV_r5 - cubeUV_r6 ) + cubeUV_m5;\n\t\t} else {\n\t\t\tmip = - 2.0 * log2( 1.16 * roughness );\t\t}\n\t\treturn mip;\n\t}\n\tvec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) {\n\t\tfloat mip = clamp( roughnessToMip( roughness ), cubeUV_m0, CUBEUV_MAX_MIP );\n\t\tfloat mipF = fract( mip );\n\t\tfloat mipInt = floor( mip );\n\t\tvec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt );\n\t\tif ( mipF == 0.0 ) {\n\t\t\treturn vec4( color0, 1.0 );\n\t\t} else {\n\t\t\tvec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 );\n\t\t\treturn vec4( mix( color0, color1, mipF ), 1.0 );\n\t\t}\n\t}\n#endif";

var defaultnormal_vertex = "vec3 transformedNormal = objectNormal;\n#ifdef USE_TANGENT\n\tvec3 transformedTangent = objectTangent;\n#endif\n#ifdef USE_BATCHING\n\tmat3 bm = mat3( batchingMatrix );\n\ttransformedNormal /= vec3( dot( bm[ 0 ], bm[ 0 ] ), dot( bm[ 1 ], bm[ 1 ] ), dot( bm[ 2 ], bm[ 2 ] ) );\n\ttransformedNormal = bm * transformedNormal;\n\t#ifdef USE_TANGENT\n\t\ttransformedTangent = bm * transformedTangent;\n\t#endif\n#endif\n#ifdef USE_INSTANCING\n\tmat3 im = mat3( instanceMatrix );\n\ttransformedNormal /= vec3( dot( im[ 0 ], im[ 0 ] ), dot( im[ 1 ], im[ 1 ] ), dot( im[ 2 ], im[ 2 ] ) );\n\ttransformedNormal = im * transformedNormal;\n\t#ifdef USE_TANGENT\n\t\ttransformedTangent = im * transformedTangent;\n\t#endif\n#endif\ntransformedNormal = normalMatrix * transformedNormal;\n#ifdef FLIP_SIDED\n\ttransformedNormal = - transformedNormal;\n#endif\n#ifdef USE_TANGENT\n\ttransformedTangent = ( modelViewMatrix * vec4( transformedTangent, 0.0 ) ).xyz;\n\t#ifdef FLIP_SIDED\n\t\ttransformedTangent = - transformedTangent;\n\t#endif\n#endif";

var displacementmap_pars_vertex = "#ifdef USE_DISPLACEMENTMAP\n\tuniform sampler2D displacementMap;\n\tuniform float displacementScale;\n\tuniform float displacementBias;\n#endif";

var displacementmap_vertex = "#ifdef USE_DISPLACEMENTMAP\n\ttransformed += normalize( objectNormal ) * ( texture2D( displacementMap, vDisplacementMapUv ).x * displacementScale + displacementBias );\n#endif";

var emissivemap_fragment = "#ifdef USE_EMISSIVEMAP\n\tvec4 emissiveColor = texture2D( emissiveMap, vEmissiveMapUv );\n\t#ifdef DECODE_VIDEO_TEXTURE_EMISSIVE\n\t\temissiveColor = sRGBTransferEOTF( emissiveColor );\n\t#endif\n\ttotalEmissiveRadiance *= emissiveColor.rgb;\n#endif";

var emissivemap_pars_fragment = "#ifdef USE_EMISSIVEMAP\n\tuniform sampler2D emissiveMap;\n#endif";

var colorspace_fragment = "gl_FragColor = linearToOutputTexel( gl_FragColor );";

var colorspace_pars_fragment = "vec4 LinearTransferOETF( in vec4 value ) {\n\treturn value;\n}\nvec4 sRGBTransferEOTF( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a );\n}\nvec4 sRGBTransferOETF( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );\n}";

var envmap_fragment = "#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvec3 cameraToFrag;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToFrag = normalize( vWorldPosition - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvec3 reflectVec = reflect( cameraToFrag, worldNormal );\n\t\t#else\n\t\t\tvec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio );\n\t\t#endif\n\t#else\n\t\tvec3 reflectVec = vReflect;\n\t#endif\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tvec4 envColor = textureCube( envMap, envMapRotation * vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) );\n\t#else\n\t\tvec4 envColor = vec4( 0.0 );\n\t#endif\n\t#ifdef ENVMAP_BLENDING_MULTIPLY\n\t\toutgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_MIX )\n\t\toutgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_ADD )\n\t\toutgoingLight += envColor.xyz * specularStrength * reflectivity;\n\t#endif\n#endif";

var envmap_common_pars_fragment = "#ifdef USE_ENVMAP\n\tuniform float envMapIntensity;\n\tuniform float flipEnvMap;\n\tuniform mat3 envMapRotation;\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tuniform samplerCube envMap;\n\t#else\n\t\tuniform sampler2D envMap;\n\t#endif\n\t\n#endif";

var envmap_pars_fragment = "#ifdef USE_ENVMAP\n\tuniform float reflectivity;\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\tvarying vec3 vWorldPosition;\n\t\tuniform float refractionRatio;\n\t#else\n\t\tvarying vec3 vReflect;\n\t#endif\n#endif";

var envmap_pars_vertex = "#ifdef USE_ENVMAP\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\t\n\t\tvarying vec3 vWorldPosition;\n\t#else\n\t\tvarying vec3 vReflect;\n\t\tuniform float refractionRatio;\n\t#endif\n#endif";

var envmap_vertex = "#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvWorldPosition = worldPosition.xyz;\n\t#else\n\t\tvec3 cameraToVertex;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToVertex = normalize( worldPosition.xyz - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvReflect = reflect( cameraToVertex, worldNormal );\n\t\t#else\n\t\t\tvReflect = refract( cameraToVertex, worldNormal, refractionRatio );\n\t\t#endif\n\t#endif\n#endif";

var fog_vertex = "#ifdef USE_FOG\n\tvFogDepth = - mvPosition.z;\n#endif";

var fog_pars_vertex = "#ifdef USE_FOG\n\tvarying float vFogDepth;\n#endif";

var fog_fragment = "#ifdef USE_FOG\n\t#ifdef FOG_EXP2\n\t\tfloat fogFactor = 1.0 - exp( - fogDensity * fogDensity * vFogDepth * vFogDepth );\n\t#else\n\t\tfloat fogFactor = smoothstep( fogNear, fogFar, vFogDepth );\n\t#endif\n\tgl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );\n#endif";

var fog_pars_fragment = "#ifdef USE_FOG\n\tuniform vec3 fogColor;\n\tvarying float vFogDepth;\n\t#ifdef FOG_EXP2\n\t\tuniform float fogDensity;\n\t#else\n\t\tuniform float fogNear;\n\t\tuniform float fogFar;\n\t#endif\n#endif";

var gradientmap_pars_fragment = "#ifdef USE_GRADIENTMAP\n\tuniform sampler2D gradientMap;\n#endif\nvec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) {\n\tfloat dotNL = dot( normal, lightDirection );\n\tvec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 );\n\t#ifdef USE_GRADIENTMAP\n\t\treturn vec3( texture2D( gradientMap, coord ).r );\n\t#else\n\t\tvec2 fw = fwidth( coord ) * 0.5;\n\t\treturn mix( vec3( 0.7 ), vec3( 1.0 ), smoothstep( 0.7 - fw.x, 0.7 + fw.x, coord.x ) );\n\t#endif\n}";

var lightmap_pars_fragment = "#ifdef USE_LIGHTMAP\n\tuniform sampler2D lightMap;\n\tuniform float lightMapIntensity;\n#endif";

var lights_lambert_fragment = "LambertMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;\nmaterial.specularStrength = specularStrength;";

var lights_lambert_pars_fragment = "varying vec3 vViewPosition;\nstruct LambertMaterial {\n\tvec3 diffuseColor;\n\tfloat specularStrength;\n};\nvoid RE_Direct_Lambert( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometryNormal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_Lambert\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Lambert";

var lights_pars_begin = "uniform bool receiveShadow;\nuniform vec3 ambientLightColor;\n#if defined( USE_LIGHT_PROBES )\n\tuniform vec3 lightProbe[ 9 ];\n#endif\nvec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {\n\tfloat x = normal.x, y = normal.y, z = normal.z;\n\tvec3 result = shCoefficients[ 0 ] * 0.886227;\n\tresult += shCoefficients[ 1 ] * 2.0 * 0.511664 * y;\n\tresult += shCoefficients[ 2 ] * 2.0 * 0.511664 * z;\n\tresult += shCoefficients[ 3 ] * 2.0 * 0.511664 * x;\n\tresult += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y;\n\tresult += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z;\n\tresult += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 );\n\tresult += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z;\n\tresult += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y );\n\treturn result;\n}\nvec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in vec3 normal ) {\n\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\n\tvec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe );\n\treturn irradiance;\n}\nvec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {\n\tvec3 irradiance = ambientLightColor;\n\treturn irradiance;\n}\nfloat getDistanceAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) {\n\tfloat distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 );\n\tif ( cutoffDistance > 0.0 ) {\n\t\tdistanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) );\n\t}\n\treturn distanceFalloff;\n}\nfloat getSpotAttenuation( const in float coneCosine, const in float penumbraCosine, const in float angleCosine ) {\n\treturn smoothstep( coneCosine, penumbraCosine, angleCosine );\n}\n#if NUM_DIR_LIGHTS > 0\n\tstruct DirectionalLight {\n\t\tvec3 direction;\n\t\tvec3 color;\n\t};\n\tuniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];\n\tvoid getDirectionalLightInfo( const in DirectionalLight directionalLight, out IncidentLight light ) {\n\t\tlight.color = directionalLight.color;\n\t\tlight.direction = directionalLight.direction;\n\t\tlight.visible = true;\n\t}\n#endif\n#if NUM_POINT_LIGHTS > 0\n\tstruct PointLight {\n\t\tvec3 position;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t};\n\tuniform PointLight pointLights[ NUM_POINT_LIGHTS ];\n\tvoid getPointLightInfo( const in PointLight pointLight, const in vec3 geometryPosition, out IncidentLight light ) {\n\t\tvec3 lVector = pointLight.position - geometryPosition;\n\t\tlight.direction = normalize( lVector );\n\t\tfloat lightDistance = length( lVector );\n\t\tlight.color = pointLight.color;\n\t\tlight.color *= getDistanceAttenuation( lightDistance, pointLight.distance, pointLight.decay );\n\t\tlight.visible = ( light.color != vec3( 0.0 ) );\n\t}\n#endif\n#if NUM_SPOT_LIGHTS > 0\n\tstruct SpotLight {\n\t\tvec3 position;\n\t\tvec3 direction;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t\tfloat coneCos;\n\t\tfloat penumbraCos;\n\t};\n\tuniform SpotLight spotLights[ NUM_SPOT_LIGHTS ];\n\tvoid getSpotLightInfo( const in SpotLight spotLight, const in vec3 geometryPosition, out IncidentLight light ) {\n\t\tvec3 lVector = spotLight.position - geometryPosition;\n\t\tlight.direction = normalize( lVector );\n\t\tfloat angleCos = dot( light.direction, spotLight.direction );\n\t\tfloat spotAttenuation = getSpotAttenuation( spotLight.coneCos, spotLight.penumbraCos, angleCos );\n\t\tif ( spotAttenuation > 0.0 ) {\n\t\t\tfloat lightDistance = length( lVector );\n\t\t\tlight.color = spotLight.color * spotAttenuation;\n\t\t\tlight.color *= getDistanceAttenuation( lightDistance, spotLight.distance, spotLight.decay );\n\t\t\tlight.visible = ( light.color != vec3( 0.0 ) );\n\t\t} else {\n\t\t\tlight.color = vec3( 0.0 );\n\t\t\tlight.visible = false;\n\t\t}\n\t}\n#endif\n#if NUM_RECT_AREA_LIGHTS > 0\n\tstruct RectAreaLight {\n\t\tvec3 color;\n\t\tvec3 position;\n\t\tvec3 halfWidth;\n\t\tvec3 halfHeight;\n\t};\n\tuniform sampler2D ltc_1;\tuniform sampler2D ltc_2;\n\tuniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ];\n#endif\n#if NUM_HEMI_LIGHTS > 0\n\tstruct HemisphereLight {\n\t\tvec3 direction;\n\t\tvec3 skyColor;\n\t\tvec3 groundColor;\n\t};\n\tuniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ];\n\tvec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in vec3 normal ) {\n\t\tfloat dotNL = dot( normal, hemiLight.direction );\n\t\tfloat hemiDiffuseWeight = 0.5 * dotNL + 0.5;\n\t\tvec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight );\n\t\treturn irradiance;\n\t}\n#endif";

var envmap_physical_pars_fragment = "#ifdef USE_ENVMAP\n\tvec3 getIBLIrradiance( const in vec3 normal ) {\n\t\t#ifdef ENVMAP_TYPE_CUBE_UV\n\t\t\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\n\t\t\tvec4 envMapColor = textureCubeUV( envMap, envMapRotation * worldNormal, 1.0 );\n\t\t\treturn PI * envMapColor.rgb * envMapIntensity;\n\t\t#else\n\t\t\treturn vec3( 0.0 );\n\t\t#endif\n\t}\n\tvec3 getIBLRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness ) {\n\t\t#ifdef ENVMAP_TYPE_CUBE_UV\n\t\t\tvec3 reflectVec = reflect( - viewDir, normal );\n\t\t\treflectVec = normalize( mix( reflectVec, normal, roughness * roughness) );\n\t\t\treflectVec = inverseTransformDirection( reflectVec, viewMatrix );\n\t\t\tvec4 envMapColor = textureCubeUV( envMap, envMapRotation * reflectVec, roughness );\n\t\t\treturn envMapColor.rgb * envMapIntensity;\n\t\t#else\n\t\t\treturn vec3( 0.0 );\n\t\t#endif\n\t}\n\t#ifdef USE_ANISOTROPY\n\t\tvec3 getIBLAnisotropyRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness, const in vec3 bitangent, const in float anisotropy ) {\n\t\t\t#ifdef ENVMAP_TYPE_CUBE_UV\n\t\t\t\tvec3 bentNormal = cross( bitangent, viewDir );\n\t\t\t\tbentNormal = normalize( cross( bentNormal, bitangent ) );\n\t\t\t\tbentNormal = normalize( mix( bentNormal, normal, pow2( pow2( 1.0 - anisotropy * ( 1.0 - roughness ) ) ) ) );\n\t\t\t\treturn getIBLRadiance( viewDir, bentNormal, roughness );\n\t\t\t#else\n\t\t\t\treturn vec3( 0.0 );\n\t\t\t#endif\n\t\t}\n\t#endif\n#endif";

var lights_toon_fragment = "ToonMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;";

var lights_toon_pars_fragment = "varying vec3 vViewPosition;\nstruct ToonMaterial {\n\tvec3 diffuseColor;\n};\nvoid RE_Direct_Toon( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\n\tvec3 irradiance = getGradientIrradiance( geometryNormal, directLight.direction ) * directLight.color;\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_Toon\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Toon";

var lights_phong_fragment = "BlinnPhongMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;\nmaterial.specularColor = specular;\nmaterial.specularShininess = shininess;\nmaterial.specularStrength = specularStrength;";

var lights_phong_pars_fragment = "varying vec3 vViewPosition;\nstruct BlinnPhongMaterial {\n\tvec3 diffuseColor;\n\tvec3 specularColor;\n\tfloat specularShininess;\n\tfloat specularStrength;\n};\nvoid RE_Direct_BlinnPhong( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometryNormal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n\treflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometryViewDir, geometryNormal, material.specularColor, material.specularShininess ) * material.specularStrength;\n}\nvoid RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_BlinnPhong\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_BlinnPhong";

var lights_physical_fragment = "PhysicalMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb * ( 1.0 - metalnessFactor );\nvec3 dxy = max( abs( dFdx( nonPerturbedNormal ) ), abs( dFdy( nonPerturbedNormal ) ) );\nfloat geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );\nmaterial.roughness = max( roughnessFactor, 0.0525 );material.roughness += geometryRoughness;\nmaterial.roughness = min( material.roughness, 1.0 );\n#ifdef IOR\n\tmaterial.ior = ior;\n\t#ifdef USE_SPECULAR\n\t\tfloat specularIntensityFactor = specularIntensity;\n\t\tvec3 specularColorFactor = specularColor;\n\t\t#ifdef USE_SPECULAR_COLORMAP\n\t\t\tspecularColorFactor *= texture2D( specularColorMap, vSpecularColorMapUv ).rgb;\n\t\t#endif\n\t\t#ifdef USE_SPECULAR_INTENSITYMAP\n\t\t\tspecularIntensityFactor *= texture2D( specularIntensityMap, vSpecularIntensityMapUv ).a;\n\t\t#endif\n\t\tmaterial.specularF90 = mix( specularIntensityFactor, 1.0, metalnessFactor );\n\t#else\n\t\tfloat specularIntensityFactor = 1.0;\n\t\tvec3 specularColorFactor = vec3( 1.0 );\n\t\tmaterial.specularF90 = 1.0;\n\t#endif\n\tmaterial.specularColor = mix( min( pow2( ( material.ior - 1.0 ) / ( material.ior + 1.0 ) ) * specularColorFactor, vec3( 1.0 ) ) * specularIntensityFactor, diffuseColor.rgb, metalnessFactor );\n#else\n\tmaterial.specularColor = mix( vec3( 0.04 ), diffuseColor.rgb, metalnessFactor );\n\tmaterial.specularF90 = 1.0;\n#endif\n#ifdef USE_CLEARCOAT\n\tmaterial.clearcoat = clearcoat;\n\tmaterial.clearcoatRoughness = clearcoatRoughness;\n\tmaterial.clearcoatF0 = vec3( 0.04 );\n\tmaterial.clearcoatF90 = 1.0;\n\t#ifdef USE_CLEARCOATMAP\n\t\tmaterial.clearcoat *= texture2D( clearcoatMap, vClearcoatMapUv ).x;\n\t#endif\n\t#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\t\tmaterial.clearcoatRoughness *= texture2D( clearcoatRoughnessMap, vClearcoatRoughnessMapUv ).y;\n\t#endif\n\tmaterial.clearcoat = saturate( material.clearcoat );\tmaterial.clearcoatRoughness = max( material.clearcoatRoughness, 0.0525 );\n\tmaterial.clearcoatRoughness += geometryRoughness;\n\tmaterial.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 );\n#endif\n#ifdef USE_DISPERSION\n\tmaterial.dispersion = dispersion;\n#endif\n#ifdef USE_IRIDESCENCE\n\tmaterial.iridescence = iridescence;\n\tmaterial.iridescenceIOR = iridescenceIOR;\n\t#ifdef USE_IRIDESCENCEMAP\n\t\tmaterial.iridescence *= texture2D( iridescenceMap, vIridescenceMapUv ).r;\n\t#endif\n\t#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\t\tmaterial.iridescenceThickness = (iridescenceThicknessMaximum - iridescenceThicknessMinimum) * texture2D( iridescenceThicknessMap, vIridescenceThicknessMapUv ).g + iridescenceThicknessMinimum;\n\t#else\n\t\tmaterial.iridescenceThickness = iridescenceThicknessMaximum;\n\t#endif\n#endif\n#ifdef USE_SHEEN\n\tmaterial.sheenColor = sheenColor;\n\t#ifdef USE_SHEEN_COLORMAP\n\t\tmaterial.sheenColor *= texture2D( sheenColorMap, vSheenColorMapUv ).rgb;\n\t#endif\n\tmaterial.sheenRoughness = clamp( sheenRoughness, 0.07, 1.0 );\n\t#ifdef USE_SHEEN_ROUGHNESSMAP\n\t\tmaterial.sheenRoughness *= texture2D( sheenRoughnessMap, vSheenRoughnessMapUv ).a;\n\t#endif\n#endif\n#ifdef USE_ANISOTROPY\n\t#ifdef USE_ANISOTROPYMAP\n\t\tmat2 anisotropyMat = mat2( anisotropyVector.x, anisotropyVector.y, - anisotropyVector.y, anisotropyVector.x );\n\t\tvec3 anisotropyPolar = texture2D( anisotropyMap, vAnisotropyMapUv ).rgb;\n\t\tvec2 anisotropyV = anisotropyMat * normalize( 2.0 * anisotropyPolar.rg - vec2( 1.0 ) ) * anisotropyPolar.b;\n\t#else\n\t\tvec2 anisotropyV = anisotropyVector;\n\t#endif\n\tmaterial.anisotropy = length( anisotropyV );\n\tif( material.anisotropy == 0.0 ) {\n\t\tanisotropyV = vec2( 1.0, 0.0 );\n\t} else {\n\t\tanisotropyV /= material.anisotropy;\n\t\tmaterial.anisotropy = saturate( material.anisotropy );\n\t}\n\tmaterial.alphaT = mix( pow2( material.roughness ), 1.0, pow2( material.anisotropy ) );\n\tmaterial.anisotropyT = tbn[ 0 ] * anisotropyV.x + tbn[ 1 ] * anisotropyV.y;\n\tmaterial.anisotropyB = tbn[ 1 ] * anisotropyV.x - tbn[ 0 ] * anisotropyV.y;\n#endif";

var lights_physical_pars_fragment = "struct PhysicalMaterial {\n\tvec3 diffuseColor;\n\tfloat roughness;\n\tvec3 specularColor;\n\tfloat specularF90;\n\tfloat dispersion;\n\t#ifdef USE_CLEARCOAT\n\t\tfloat clearcoat;\n\t\tfloat clearcoatRoughness;\n\t\tvec3 clearcoatF0;\n\t\tfloat clearcoatF90;\n\t#endif\n\t#ifdef USE_IRIDESCENCE\n\t\tfloat iridescence;\n\t\tfloat iridescenceIOR;\n\t\tfloat iridescenceThickness;\n\t\tvec3 iridescenceFresnel;\n\t\tvec3 iridescenceF0;\n\t#endif\n\t#ifdef USE_SHEEN\n\t\tvec3 sheenColor;\n\t\tfloat sheenRoughness;\n\t#endif\n\t#ifdef IOR\n\t\tfloat ior;\n\t#endif\n\t#ifdef USE_TRANSMISSION\n\t\tfloat transmission;\n\t\tfloat transmissionAlpha;\n\t\tfloat thickness;\n\t\tfloat attenuationDistance;\n\t\tvec3 attenuationColor;\n\t#endif\n\t#ifdef USE_ANISOTROPY\n\t\tfloat anisotropy;\n\t\tfloat alphaT;\n\t\tvec3 anisotropyT;\n\t\tvec3 anisotropyB;\n\t#endif\n};\nvec3 clearcoatSpecularDirect = vec3( 0.0 );\nvec3 clearcoatSpecularIndirect = vec3( 0.0 );\nvec3 sheenSpecularDirect = vec3( 0.0 );\nvec3 sheenSpecularIndirect = vec3(0.0 );\nvec3 Schlick_to_F0( const in vec3 f, const in float f90, const in float dotVH ) {\n    float x = clamp( 1.0 - dotVH, 0.0, 1.0 );\n    float x2 = x * x;\n    float x5 = clamp( x * x2 * x2, 0.0, 0.9999 );\n    return ( f - vec3( f90 ) * x5 ) / ( 1.0 - x5 );\n}\nfloat V_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) {\n\tfloat a2 = pow2( alpha );\n\tfloat gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) );\n\tfloat gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) );\n\treturn 0.5 / max( gv + gl, EPSILON );\n}\nfloat D_GGX( const in float alpha, const in float dotNH ) {\n\tfloat a2 = pow2( alpha );\n\tfloat denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0;\n\treturn RECIPROCAL_PI * a2 / pow2( denom );\n}\n#ifdef USE_ANISOTROPY\n\tfloat V_GGX_SmithCorrelated_Anisotropic( const in float alphaT, const in float alphaB, const in float dotTV, const in float dotBV, const in float dotTL, const in float dotBL, const in float dotNV, const in float dotNL ) {\n\t\tfloat gv = dotNL * length( vec3( alphaT * dotTV, alphaB * dotBV, dotNV ) );\n\t\tfloat gl = dotNV * length( vec3( alphaT * dotTL, alphaB * dotBL, dotNL ) );\n\t\tfloat v = 0.5 / ( gv + gl );\n\t\treturn saturate(v);\n\t}\n\tfloat D_GGX_Anisotropic( const in float alphaT, const in float alphaB, const in float dotNH, const in float dotTH, const in float dotBH ) {\n\t\tfloat a2 = alphaT * alphaB;\n\t\thighp vec3 v = vec3( alphaB * dotTH, alphaT * dotBH, a2 * dotNH );\n\t\thighp float v2 = dot( v, v );\n\t\tfloat w2 = a2 / v2;\n\t\treturn RECIPROCAL_PI * a2 * pow2 ( w2 );\n\t}\n#endif\n#ifdef USE_CLEARCOAT\n\tvec3 BRDF_GGX_Clearcoat( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material) {\n\t\tvec3 f0 = material.clearcoatF0;\n\t\tfloat f90 = material.clearcoatF90;\n\t\tfloat roughness = material.clearcoatRoughness;\n\t\tfloat alpha = pow2( roughness );\n\t\tvec3 halfDir = normalize( lightDir + viewDir );\n\t\tfloat dotNL = saturate( dot( normal, lightDir ) );\n\t\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\t\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\t\tfloat dotVH = saturate( dot( viewDir, halfDir ) );\n\t\tvec3 F = F_Schlick( f0, f90, dotVH );\n\t\tfloat V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV );\n\t\tfloat D = D_GGX( alpha, dotNH );\n\t\treturn F * ( V * D );\n\t}\n#endif\nvec3 BRDF_GGX( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) {\n\tvec3 f0 = material.specularColor;\n\tfloat f90 = material.specularF90;\n\tfloat roughness = material.roughness;\n\tfloat alpha = pow2( roughness );\n\tvec3 halfDir = normalize( lightDir + viewDir );\n\tfloat dotNL = saturate( dot( normal, lightDir ) );\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\tfloat dotVH = saturate( dot( viewDir, halfDir ) );\n\tvec3 F = F_Schlick( f0, f90, dotVH );\n\t#ifdef USE_IRIDESCENCE\n\t\tF = mix( F, material.iridescenceFresnel, material.iridescence );\n\t#endif\n\t#ifdef USE_ANISOTROPY\n\t\tfloat dotTL = dot( material.anisotropyT, lightDir );\n\t\tfloat dotTV = dot( material.anisotropyT, viewDir );\n\t\tfloat dotTH = dot( material.anisotropyT, halfDir );\n\t\tfloat dotBL = dot( material.anisotropyB, lightDir );\n\t\tfloat dotBV = dot( material.anisotropyB, viewDir );\n\t\tfloat dotBH = dot( material.anisotropyB, halfDir );\n\t\tfloat V = V_GGX_SmithCorrelated_Anisotropic( material.alphaT, alpha, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL );\n\t\tfloat D = D_GGX_Anisotropic( material.alphaT, alpha, dotNH, dotTH, dotBH );\n\t#else\n\t\tfloat V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV );\n\t\tfloat D = D_GGX( alpha, dotNH );\n\t#endif\n\treturn F * ( V * D );\n}\nvec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) {\n\tconst float LUT_SIZE = 64.0;\n\tconst float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE;\n\tconst float LUT_BIAS = 0.5 / LUT_SIZE;\n\tfloat dotNV = saturate( dot( N, V ) );\n\tvec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) );\n\tuv = uv * LUT_SCALE + LUT_BIAS;\n\treturn uv;\n}\nfloat LTC_ClippedSphereFormFactor( const in vec3 f ) {\n\tfloat l = length( f );\n\treturn max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 );\n}\nvec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) {\n\tfloat x = dot( v1, v2 );\n\tfloat y = abs( x );\n\tfloat a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y;\n\tfloat b = 3.4175940 + ( 4.1616724 + y ) * y;\n\tfloat v = a / b;\n\tfloat theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v;\n\treturn cross( v1, v2 ) * theta_sintheta;\n}\nvec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) {\n\tvec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ];\n\tvec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ];\n\tvec3 lightNormal = cross( v1, v2 );\n\tif( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 );\n\tvec3 T1, T2;\n\tT1 = normalize( V - N * dot( V, N ) );\n\tT2 = - cross( N, T1 );\n\tmat3 mat = mInv * transposeMat3( mat3( T1, T2, N ) );\n\tvec3 coords[ 4 ];\n\tcoords[ 0 ] = mat * ( rectCoords[ 0 ] - P );\n\tcoords[ 1 ] = mat * ( rectCoords[ 1 ] - P );\n\tcoords[ 2 ] = mat * ( rectCoords[ 2 ] - P );\n\tcoords[ 3 ] = mat * ( rectCoords[ 3 ] - P );\n\tcoords[ 0 ] = normalize( coords[ 0 ] );\n\tcoords[ 1 ] = normalize( coords[ 1 ] );\n\tcoords[ 2 ] = normalize( coords[ 2 ] );\n\tcoords[ 3 ] = normalize( coords[ 3 ] );\n\tvec3 vectorFormFactor = vec3( 0.0 );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] );\n\tfloat result = LTC_ClippedSphereFormFactor( vectorFormFactor );\n\treturn vec3( result );\n}\n#if defined( USE_SHEEN )\nfloat D_Charlie( float roughness, float dotNH ) {\n\tfloat alpha = pow2( roughness );\n\tfloat invAlpha = 1.0 / alpha;\n\tfloat cos2h = dotNH * dotNH;\n\tfloat sin2h = max( 1.0 - cos2h, 0.0078125 );\n\treturn ( 2.0 + invAlpha ) * pow( sin2h, invAlpha * 0.5 ) / ( 2.0 * PI );\n}\nfloat V_Neubelt( float dotNV, float dotNL ) {\n\treturn saturate( 1.0 / ( 4.0 * ( dotNL + dotNV - dotNL * dotNV ) ) );\n}\nvec3 BRDF_Sheen( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, vec3 sheenColor, const in float sheenRoughness ) {\n\tvec3 halfDir = normalize( lightDir + viewDir );\n\tfloat dotNL = saturate( dot( normal, lightDir ) );\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\tfloat D = D_Charlie( sheenRoughness, dotNH );\n\tfloat V = V_Neubelt( dotNV, dotNL );\n\treturn sheenColor * ( D * V );\n}\n#endif\nfloat IBLSheenBRDF( const in vec3 normal, const in vec3 viewDir, const in float roughness ) {\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tfloat r2 = roughness * roughness;\n\tfloat a = roughness < 0.25 ? -339.2 * r2 + 161.4 * roughness - 25.9 : -8.48 * r2 + 14.3 * roughness - 9.95;\n\tfloat b = roughness < 0.25 ? 44.0 * r2 - 23.7 * roughness + 3.26 : 1.97 * r2 - 3.27 * roughness + 0.72;\n\tfloat DG = exp( a * dotNV + b ) + ( roughness < 0.25 ? 0.0 : 0.1 * ( roughness - 0.25 ) );\n\treturn saturate( DG * RECIPROCAL_PI );\n}\nvec2 DFGApprox( const in vec3 normal, const in vec3 viewDir, const in float roughness ) {\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tconst vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 );\n\tconst vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 );\n\tvec4 r = roughness * c0 + c1;\n\tfloat a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y;\n\tvec2 fab = vec2( - 1.04, 1.04 ) * a004 + r.zw;\n\treturn fab;\n}\nvec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness ) {\n\tvec2 fab = DFGApprox( normal, viewDir, roughness );\n\treturn specularColor * fab.x + specularF90 * fab.y;\n}\n#ifdef USE_IRIDESCENCE\nvoid computeMultiscatteringIridescence( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float iridescence, const in vec3 iridescenceF0, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {\n#else\nvoid computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {\n#endif\n\tvec2 fab = DFGApprox( normal, viewDir, roughness );\n\t#ifdef USE_IRIDESCENCE\n\t\tvec3 Fr = mix( specularColor, iridescenceF0, iridescence );\n\t#else\n\t\tvec3 Fr = specularColor;\n\t#endif\n\tvec3 FssEss = Fr * fab.x + specularF90 * fab.y;\n\tfloat Ess = fab.x + fab.y;\n\tfloat Ems = 1.0 - Ess;\n\tvec3 Favg = Fr + ( 1.0 - Fr ) * 0.047619;\tvec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg );\n\tsingleScatter += FssEss;\n\tmultiScatter += Fms * Ems;\n}\n#if NUM_RECT_AREA_LIGHTS > 0\n\tvoid RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\t\tvec3 normal = geometryNormal;\n\t\tvec3 viewDir = geometryViewDir;\n\t\tvec3 position = geometryPosition;\n\t\tvec3 lightPos = rectAreaLight.position;\n\t\tvec3 halfWidth = rectAreaLight.halfWidth;\n\t\tvec3 halfHeight = rectAreaLight.halfHeight;\n\t\tvec3 lightColor = rectAreaLight.color;\n\t\tfloat roughness = material.roughness;\n\t\tvec3 rectCoords[ 4 ];\n\t\trectCoords[ 0 ] = lightPos + halfWidth - halfHeight;\t\trectCoords[ 1 ] = lightPos - halfWidth - halfHeight;\n\t\trectCoords[ 2 ] = lightPos - halfWidth + halfHeight;\n\t\trectCoords[ 3 ] = lightPos + halfWidth + halfHeight;\n\t\tvec2 uv = LTC_Uv( normal, viewDir, roughness );\n\t\tvec4 t1 = texture2D( ltc_1, uv );\n\t\tvec4 t2 = texture2D( ltc_2, uv );\n\t\tmat3 mInv = mat3(\n\t\t\tvec3( t1.x, 0, t1.y ),\n\t\t\tvec3(    0, 1,    0 ),\n\t\t\tvec3( t1.z, 0, t1.w )\n\t\t);\n\t\tvec3 fresnel = ( material.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y );\n\t\treflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords );\n\t\treflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords );\n\t}\n#endif\nvoid RE_Direct_Physical( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometryNormal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\t#ifdef USE_CLEARCOAT\n\t\tfloat dotNLcc = saturate( dot( geometryClearcoatNormal, directLight.direction ) );\n\t\tvec3 ccIrradiance = dotNLcc * directLight.color;\n\t\tclearcoatSpecularDirect += ccIrradiance * BRDF_GGX_Clearcoat( directLight.direction, geometryViewDir, geometryClearcoatNormal, material );\n\t#endif\n\t#ifdef USE_SHEEN\n\t\tsheenSpecularDirect += irradiance * BRDF_Sheen( directLight.direction, geometryViewDir, geometryNormal, material.sheenColor, material.sheenRoughness );\n\t#endif\n\treflectedLight.directSpecular += irradiance * BRDF_GGX( directLight.direction, geometryViewDir, geometryNormal, material );\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) {\n\t#ifdef USE_CLEARCOAT\n\t\tclearcoatSpecularIndirect += clearcoatRadiance * EnvironmentBRDF( geometryClearcoatNormal, geometryViewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness );\n\t#endif\n\t#ifdef USE_SHEEN\n\t\tsheenSpecularIndirect += irradiance * material.sheenColor * IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness );\n\t#endif\n\tvec3 singleScattering = vec3( 0.0 );\n\tvec3 multiScattering = vec3( 0.0 );\n\tvec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI;\n\t#ifdef USE_IRIDESCENCE\n\t\tcomputeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnel, material.roughness, singleScattering, multiScattering );\n\t#else\n\t\tcomputeMultiscattering( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness, singleScattering, multiScattering );\n\t#endif\n\tvec3 totalScattering = singleScattering + multiScattering;\n\tvec3 diffuse = material.diffuseColor * ( 1.0 - max( max( totalScattering.r, totalScattering.g ), totalScattering.b ) );\n\treflectedLight.indirectSpecular += radiance * singleScattering;\n\treflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance;\n\treflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance;\n}\n#define RE_Direct\t\t\t\tRE_Direct_Physical\n#define RE_Direct_RectArea\t\tRE_Direct_RectArea_Physical\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Physical\n#define RE_IndirectSpecular\t\tRE_IndirectSpecular_Physical\nfloat computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) {\n\treturn saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion );\n}";

var lights_fragment_begin = "\nvec3 geometryPosition = - vViewPosition;\nvec3 geometryNormal = normal;\nvec3 geometryViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition );\nvec3 geometryClearcoatNormal = vec3( 0.0 );\n#ifdef USE_CLEARCOAT\n\tgeometryClearcoatNormal = clearcoatNormal;\n#endif\n#ifdef USE_IRIDESCENCE\n\tfloat dotNVi = saturate( dot( normal, geometryViewDir ) );\n\tif ( material.iridescenceThickness == 0.0 ) {\n\t\tmaterial.iridescence = 0.0;\n\t} else {\n\t\tmaterial.iridescence = saturate( material.iridescence );\n\t}\n\tif ( material.iridescence > 0.0 ) {\n\t\tmaterial.iridescenceFresnel = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor );\n\t\tmaterial.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNVi );\n\t}\n#endif\nIncidentLight directLight;\n#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct )\n\tPointLight pointLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0\n\tPointLightShadow pointLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {\n\t\tpointLight = pointLights[ i ];\n\t\tgetPointLightInfo( pointLight, geometryPosition, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS )\n\t\tpointLightShadow = pointLightShadows[ i ];\n\t\tdirectLight.color *= ( directLight.visible && receiveShadow ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowIntensity, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct )\n\tSpotLight spotLight;\n\tvec4 spotColor;\n\tvec3 spotLightCoord;\n\tbool inSpotLightMap;\n\t#if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0\n\tSpotLightShadow spotLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {\n\t\tspotLight = spotLights[ i ];\n\t\tgetSpotLightInfo( spotLight, geometryPosition, directLight );\n\t\t#if ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS )\n\t\t#define SPOT_LIGHT_MAP_INDEX UNROLLED_LOOP_INDEX\n\t\t#elif ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\t#define SPOT_LIGHT_MAP_INDEX NUM_SPOT_LIGHT_MAPS\n\t\t#else\n\t\t#define SPOT_LIGHT_MAP_INDEX ( UNROLLED_LOOP_INDEX - NUM_SPOT_LIGHT_SHADOWS + NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS )\n\t\t#endif\n\t\t#if ( SPOT_LIGHT_MAP_INDEX < NUM_SPOT_LIGHT_MAPS )\n\t\t\tspotLightCoord = vSpotLightCoord[ i ].xyz / vSpotLightCoord[ i ].w;\n\t\t\tinSpotLightMap = all( lessThan( abs( spotLightCoord * 2. - 1. ), vec3( 1.0 ) ) );\n\t\t\tspotColor = texture2D( spotLightMap[ SPOT_LIGHT_MAP_INDEX ], spotLightCoord.xy );\n\t\t\tdirectLight.color = inSpotLightMap ? directLight.color * spotColor.rgb : directLight.color;\n\t\t#endif\n\t\t#undef SPOT_LIGHT_MAP_INDEX\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\tspotLightShadow = spotLightShadows[ i ];\n\t\tdirectLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowIntensity, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotLightCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )\n\tDirectionalLight directionalLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0\n\tDirectionalLightShadow directionalLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\n\t\tdirectionalLight = directionalLights[ i ];\n\t\tgetDirectionalLightInfo( directionalLight, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )\n\t\tdirectionalLightShadow = directionalLightShadows[ i ];\n\t\tdirectLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowIntensity, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea )\n\tRectAreaLight rectAreaLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) {\n\t\trectAreaLight = rectAreaLights[ i ];\n\t\tRE_Direct_RectArea( rectAreaLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if defined( RE_IndirectDiffuse )\n\tvec3 iblIrradiance = vec3( 0.0 );\n\tvec3 irradiance = getAmbientLightIrradiance( ambientLightColor );\n\t#if defined( USE_LIGHT_PROBES )\n\t\tirradiance += getLightProbeIrradiance( lightProbe, geometryNormal );\n\t#endif\n\t#if ( NUM_HEMI_LIGHTS > 0 )\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {\n\t\t\tirradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometryNormal );\n\t\t}\n\t\t#pragma unroll_loop_end\n\t#endif\n#endif\n#if defined( RE_IndirectSpecular )\n\tvec3 radiance = vec3( 0.0 );\n\tvec3 clearcoatRadiance = vec3( 0.0 );\n#endif";

var lights_fragment_maps = "#if defined( RE_IndirectDiffuse )\n\t#ifdef USE_LIGHTMAP\n\t\tvec4 lightMapTexel = texture2D( lightMap, vLightMapUv );\n\t\tvec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity;\n\t\tirradiance += lightMapIrradiance;\n\t#endif\n\t#if defined( USE_ENVMAP ) && defined( STANDARD ) && defined( ENVMAP_TYPE_CUBE_UV )\n\t\tiblIrradiance += getIBLIrradiance( geometryNormal );\n\t#endif\n#endif\n#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular )\n\t#ifdef USE_ANISOTROPY\n\t\tradiance += getIBLAnisotropyRadiance( geometryViewDir, geometryNormal, material.roughness, material.anisotropyB, material.anisotropy );\n\t#else\n\t\tradiance += getIBLRadiance( geometryViewDir, geometryNormal, material.roughness );\n\t#endif\n\t#ifdef USE_CLEARCOAT\n\t\tclearcoatRadiance += getIBLRadiance( geometryViewDir, geometryClearcoatNormal, material.clearcoatRoughness );\n\t#endif\n#endif";

var lights_fragment_end = "#if defined( RE_IndirectDiffuse )\n\tRE_IndirectDiffuse( irradiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n#endif\n#if defined( RE_IndirectSpecular )\n\tRE_IndirectSpecular( radiance, iblIrradiance, clearcoatRadiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n#endif";

var logdepthbuf_fragment = "#if defined( USE_LOGARITHMIC_DEPTH_BUFFER )\n\tgl_FragDepth = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;\n#endif";

var logdepthbuf_pars_fragment = "#if defined( USE_LOGARITHMIC_DEPTH_BUFFER )\n\tuniform float logDepthBufFC;\n\tvarying float vFragDepth;\n\tvarying float vIsPerspective;\n#endif";

var logdepthbuf_pars_vertex = "#ifdef USE_LOGARITHMIC_DEPTH_BUFFER\n\tvarying float vFragDepth;\n\tvarying float vIsPerspective;\n#endif";

var logdepthbuf_vertex = "#ifdef USE_LOGARITHMIC_DEPTH_BUFFER\n\tvFragDepth = 1.0 + gl_Position.w;\n\tvIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) );\n#endif";

var map_fragment = "#ifdef USE_MAP\n\tvec4 sampledDiffuseColor = texture2D( map, vMapUv );\n\t#ifdef DECODE_VIDEO_TEXTURE\n\t\tsampledDiffuseColor = sRGBTransferEOTF( sampledDiffuseColor );\n\t#endif\n\tdiffuseColor *= sampledDiffuseColor;\n#endif";

var map_pars_fragment = "#ifdef USE_MAP\n\tuniform sampler2D map;\n#endif";

var map_particle_fragment = "#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\n\t#if defined( USE_POINTS_UV )\n\t\tvec2 uv = vUv;\n\t#else\n\t\tvec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy;\n\t#endif\n#endif\n#ifdef USE_MAP\n\tdiffuseColor *= texture2D( map, uv );\n#endif\n#ifdef USE_ALPHAMAP\n\tdiffuseColor.a *= texture2D( alphaMap, uv ).g;\n#endif";

var map_particle_pars_fragment = "#if defined( USE_POINTS_UV )\n\tvarying vec2 vUv;\n#else\n\t#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\n\t\tuniform mat3 uvTransform;\n\t#endif\n#endif\n#ifdef USE_MAP\n\tuniform sampler2D map;\n#endif\n#ifdef USE_ALPHAMAP\n\tuniform sampler2D alphaMap;\n#endif";

var metalnessmap_fragment = "float metalnessFactor = metalness;\n#ifdef USE_METALNESSMAP\n\tvec4 texelMetalness = texture2D( metalnessMap, vMetalnessMapUv );\n\tmetalnessFactor *= texelMetalness.b;\n#endif";

var metalnessmap_pars_fragment = "#ifdef USE_METALNESSMAP\n\tuniform sampler2D metalnessMap;\n#endif";

var morphinstance_vertex = "#ifdef USE_INSTANCING_MORPH\n\tfloat morphTargetInfluences[ MORPHTARGETS_COUNT ];\n\tfloat morphTargetBaseInfluence = texelFetch( morphTexture, ivec2( 0, gl_InstanceID ), 0 ).r;\n\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\tmorphTargetInfluences[i] =  texelFetch( morphTexture, ivec2( i + 1, gl_InstanceID ), 0 ).r;\n\t}\n#endif";

var morphcolor_vertex = "#if defined( USE_MORPHCOLORS )\n\tvColor *= morphTargetBaseInfluence;\n\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\t#if defined( USE_COLOR_ALPHA )\n\t\t\tif ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ) * morphTargetInfluences[ i ];\n\t\t#elif defined( USE_COLOR )\n\t\t\tif ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb * morphTargetInfluences[ i ];\n\t\t#endif\n\t}\n#endif";

var morphnormal_vertex = "#ifdef USE_MORPHNORMALS\n\tobjectNormal *= morphTargetBaseInfluence;\n\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\tif ( morphTargetInfluences[ i ] != 0.0 ) objectNormal += getMorph( gl_VertexID, i, 1 ).xyz * morphTargetInfluences[ i ];\n\t}\n#endif";

var morphtarget_pars_vertex = "#ifdef USE_MORPHTARGETS\n\t#ifndef USE_INSTANCING_MORPH\n\t\tuniform float morphTargetBaseInfluence;\n\t\tuniform float morphTargetInfluences[ MORPHTARGETS_COUNT ];\n\t#endif\n\tuniform sampler2DArray morphTargetsTexture;\n\tuniform ivec2 morphTargetsTextureSize;\n\tvec4 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset ) {\n\t\tint texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset;\n\t\tint y = texelIndex / morphTargetsTextureSize.x;\n\t\tint x = texelIndex - y * morphTargetsTextureSize.x;\n\t\tivec3 morphUV = ivec3( x, y, morphTargetIndex );\n\t\treturn texelFetch( morphTargetsTexture, morphUV, 0 );\n\t}\n#endif";

var morphtarget_vertex = "#ifdef USE_MORPHTARGETS\n\ttransformed *= morphTargetBaseInfluence;\n\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\tif ( morphTargetInfluences[ i ] != 0.0 ) transformed += getMorph( gl_VertexID, i, 0 ).xyz * morphTargetInfluences[ i ];\n\t}\n#endif";

var normal_fragment_begin = "float faceDirection = gl_FrontFacing ? 1.0 : - 1.0;\n#ifdef FLAT_SHADED\n\tvec3 fdx = dFdx( vViewPosition );\n\tvec3 fdy = dFdy( vViewPosition );\n\tvec3 normal = normalize( cross( fdx, fdy ) );\n#else\n\tvec3 normal = normalize( vNormal );\n\t#ifdef DOUBLE_SIDED\n\t\tnormal *= faceDirection;\n\t#endif\n#endif\n#if defined( USE_NORMALMAP_TANGENTSPACE ) || defined( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY )\n\t#ifdef USE_TANGENT\n\t\tmat3 tbn = mat3( normalize( vTangent ), normalize( vBitangent ), normal );\n\t#else\n\t\tmat3 tbn = getTangentFrame( - vViewPosition, normal,\n\t\t#if defined( USE_NORMALMAP )\n\t\t\tvNormalMapUv\n\t\t#elif defined( USE_CLEARCOAT_NORMALMAP )\n\t\t\tvClearcoatNormalMapUv\n\t\t#else\n\t\t\tvUv\n\t\t#endif\n\t\t);\n\t#endif\n\t#if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED )\n\t\ttbn[0] *= faceDirection;\n\t\ttbn[1] *= faceDirection;\n\t#endif\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\t#ifdef USE_TANGENT\n\t\tmat3 tbn2 = mat3( normalize( vTangent ), normalize( vBitangent ), normal );\n\t#else\n\t\tmat3 tbn2 = getTangentFrame( - vViewPosition, normal, vClearcoatNormalMapUv );\n\t#endif\n\t#if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED )\n\t\ttbn2[0] *= faceDirection;\n\t\ttbn2[1] *= faceDirection;\n\t#endif\n#endif\nvec3 nonPerturbedNormal = normal;";

var normal_fragment_maps = "#ifdef USE_NORMALMAP_OBJECTSPACE\n\tnormal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0;\n\t#ifdef FLIP_SIDED\n\t\tnormal = - normal;\n\t#endif\n\t#ifdef DOUBLE_SIDED\n\t\tnormal = normal * faceDirection;\n\t#endif\n\tnormal = normalize( normalMatrix * normal );\n#elif defined( USE_NORMALMAP_TANGENTSPACE )\n\tvec3 mapN = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0;\n\tmapN.xy *= normalScale;\n\tnormal = normalize( tbn * mapN );\n#elif defined( USE_BUMPMAP )\n\tnormal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection );\n#endif";

var normal_pars_fragment = "#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif";

var normal_pars_vertex = "#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif";

var normal_vertex = "#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n\t#ifdef USE_TANGENT\n\t\tvTangent = normalize( transformedTangent );\n\t\tvBitangent = normalize( cross( vNormal, vTangent ) * tangent.w );\n\t#endif\n#endif";

var normalmap_pars_fragment = "#ifdef USE_NORMALMAP\n\tuniform sampler2D normalMap;\n\tuniform vec2 normalScale;\n#endif\n#ifdef USE_NORMALMAP_OBJECTSPACE\n\tuniform mat3 normalMatrix;\n#endif\n#if ! defined ( USE_TANGENT ) && ( defined ( USE_NORMALMAP_TANGENTSPACE ) || defined ( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) )\n\tmat3 getTangentFrame( vec3 eye_pos, vec3 surf_norm, vec2 uv ) {\n\t\tvec3 q0 = dFdx( eye_pos.xyz );\n\t\tvec3 q1 = dFdy( eye_pos.xyz );\n\t\tvec2 st0 = dFdx( uv.st );\n\t\tvec2 st1 = dFdy( uv.st );\n\t\tvec3 N = surf_norm;\n\t\tvec3 q1perp = cross( q1, N );\n\t\tvec3 q0perp = cross( N, q0 );\n\t\tvec3 T = q1perp * st0.x + q0perp * st1.x;\n\t\tvec3 B = q1perp * st0.y + q0perp * st1.y;\n\t\tfloat det = max( dot( T, T ), dot( B, B ) );\n\t\tfloat scale = ( det == 0.0 ) ? 0.0 : inversesqrt( det );\n\t\treturn mat3( T * scale, B * scale, N );\n\t}\n#endif";

var clearcoat_normal_fragment_begin = "#ifdef USE_CLEARCOAT\n\tvec3 clearcoatNormal = nonPerturbedNormal;\n#endif";

var clearcoat_normal_fragment_maps = "#ifdef USE_CLEARCOAT_NORMALMAP\n\tvec3 clearcoatMapN = texture2D( clearcoatNormalMap, vClearcoatNormalMapUv ).xyz * 2.0 - 1.0;\n\tclearcoatMapN.xy *= clearcoatNormalScale;\n\tclearcoatNormal = normalize( tbn2 * clearcoatMapN );\n#endif";

var clearcoat_pars_fragment = "#ifdef USE_CLEARCOATMAP\n\tuniform sampler2D clearcoatMap;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tuniform sampler2D clearcoatNormalMap;\n\tuniform vec2 clearcoatNormalScale;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tuniform sampler2D clearcoatRoughnessMap;\n#endif";

var iridescence_pars_fragment = "#ifdef USE_IRIDESCENCEMAP\n\tuniform sampler2D iridescenceMap;\n#endif\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\tuniform sampler2D iridescenceThicknessMap;\n#endif";

var opaque_fragment = "#ifdef OPAQUE\ndiffuseColor.a = 1.0;\n#endif\n#ifdef USE_TRANSMISSION\ndiffuseColor.a *= material.transmissionAlpha;\n#endif\ngl_FragColor = vec4( outgoingLight, diffuseColor.a );";

var packing = "vec3 packNormalToRGB( const in vec3 normal ) {\n\treturn normalize( normal ) * 0.5 + 0.5;\n}\nvec3 unpackRGBToNormal( const in vec3 rgb ) {\n\treturn 2.0 * rgb.xyz - 1.0;\n}\nconst float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.;const float ShiftRight8 = 1. / 256.;\nconst float Inv255 = 1. / 255.;\nconst vec4 PackFactors = vec4( 1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0 );\nconst vec2 UnpackFactors2 = vec2( UnpackDownscale, 1.0 / PackFactors.g );\nconst vec3 UnpackFactors3 = vec3( UnpackDownscale / PackFactors.rg, 1.0 / PackFactors.b );\nconst vec4 UnpackFactors4 = vec4( UnpackDownscale / PackFactors.rgb, 1.0 / PackFactors.a );\nvec4 packDepthToRGBA( const in float v ) {\n\tif( v <= 0.0 )\n\t\treturn vec4( 0., 0., 0., 0. );\n\tif( v >= 1.0 )\n\t\treturn vec4( 1., 1., 1., 1. );\n\tfloat vuf;\n\tfloat af = modf( v * PackFactors.a, vuf );\n\tfloat bf = modf( vuf * ShiftRight8, vuf );\n\tfloat gf = modf( vuf * ShiftRight8, vuf );\n\treturn vec4( vuf * Inv255, gf * PackUpscale, bf * PackUpscale, af );\n}\nvec3 packDepthToRGB( const in float v ) {\n\tif( v <= 0.0 )\n\t\treturn vec3( 0., 0., 0. );\n\tif( v >= 1.0 )\n\t\treturn vec3( 1., 1., 1. );\n\tfloat vuf;\n\tfloat bf = modf( v * PackFactors.b, vuf );\n\tfloat gf = modf( vuf * ShiftRight8, vuf );\n\treturn vec3( vuf * Inv255, gf * PackUpscale, bf );\n}\nvec2 packDepthToRG( const in float v ) {\n\tif( v <= 0.0 )\n\t\treturn vec2( 0., 0. );\n\tif( v >= 1.0 )\n\t\treturn vec2( 1., 1. );\n\tfloat vuf;\n\tfloat gf = modf( v * 256., vuf );\n\treturn vec2( vuf * Inv255, gf );\n}\nfloat unpackRGBAToDepth( const in vec4 v ) {\n\treturn dot( v, UnpackFactors4 );\n}\nfloat unpackRGBToDepth( const in vec3 v ) {\n\treturn dot( v, UnpackFactors3 );\n}\nfloat unpackRGToDepth( const in vec2 v ) {\n\treturn v.r * UnpackFactors2.r + v.g * UnpackFactors2.g;\n}\nvec4 pack2HalfToRGBA( const in vec2 v ) {\n\tvec4 r = vec4( v.x, fract( v.x * 255.0 ), v.y, fract( v.y * 255.0 ) );\n\treturn vec4( r.x - r.y / 255.0, r.y, r.z - r.w / 255.0, r.w );\n}\nvec2 unpackRGBATo2Half( const in vec4 v ) {\n\treturn vec2( v.x + ( v.y / 255.0 ), v.z + ( v.w / 255.0 ) );\n}\nfloat viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn ( viewZ + near ) / ( near - far );\n}\nfloat orthographicDepthToViewZ( const in float depth, const in float near, const in float far ) {\n\treturn depth * ( near - far ) - near;\n}\nfloat viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn ( ( near + viewZ ) * far ) / ( ( far - near ) * viewZ );\n}\nfloat perspectiveDepthToViewZ( const in float depth, const in float near, const in float far ) {\n\treturn ( near * far ) / ( ( far - near ) * depth - far );\n}";

var premultiplied_alpha_fragment = "#ifdef PREMULTIPLIED_ALPHA\n\tgl_FragColor.rgb *= gl_FragColor.a;\n#endif";

var project_vertex = "vec4 mvPosition = vec4( transformed, 1.0 );\n#ifdef USE_BATCHING\n\tmvPosition = batchingMatrix * mvPosition;\n#endif\n#ifdef USE_INSTANCING\n\tmvPosition = instanceMatrix * mvPosition;\n#endif\nmvPosition = modelViewMatrix * mvPosition;\ngl_Position = projectionMatrix * mvPosition;";

var dithering_fragment = "#ifdef DITHERING\n\tgl_FragColor.rgb = dithering( gl_FragColor.rgb );\n#endif";

var dithering_pars_fragment = "#ifdef DITHERING\n\tvec3 dithering( vec3 color ) {\n\t\tfloat grid_position = rand( gl_FragCoord.xy );\n\t\tvec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 );\n\t\tdither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position );\n\t\treturn color + dither_shift_RGB;\n\t}\n#endif";

var roughnessmap_fragment = "float roughnessFactor = roughness;\n#ifdef USE_ROUGHNESSMAP\n\tvec4 texelRoughness = texture2D( roughnessMap, vRoughnessMapUv );\n\troughnessFactor *= texelRoughness.g;\n#endif";

var roughnessmap_pars_fragment = "#ifdef USE_ROUGHNESSMAP\n\tuniform sampler2D roughnessMap;\n#endif";

var shadowmap_pars_fragment = "#if NUM_SPOT_LIGHT_COORDS > 0\n\tvarying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ];\n#endif\n#if NUM_SPOT_LIGHT_MAPS > 0\n\tuniform sampler2D spotLightMap[ NUM_SPOT_LIGHT_MAPS ];\n#endif\n#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tstruct DirectionalLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\tstruct SpotLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tstruct PointLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t\tfloat shadowCameraNear;\n\t\t\tfloat shadowCameraFar;\n\t\t};\n\t\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\n\t#endif\n\tfloat texture2DCompare( sampler2D depths, vec2 uv, float compare ) {\n\t\tfloat depth = unpackRGBAToDepth( texture2D( depths, uv ) );\n\t\t#ifdef USE_REVERSED_DEPTH_BUFFER\n\t\t\treturn step( depth, compare );\n\t\t#else\n\t\t\treturn step( compare, depth );\n\t\t#endif\n\t}\n\tvec2 texture2DDistribution( sampler2D shadow, vec2 uv ) {\n\t\treturn unpackRGBATo2Half( texture2D( shadow, uv ) );\n\t}\n\tfloat VSMShadow( sampler2D shadow, vec2 uv, float compare ) {\n\t\tfloat occlusion = 1.0;\n\t\tvec2 distribution = texture2DDistribution( shadow, uv );\n\t\t#ifdef USE_REVERSED_DEPTH_BUFFER\n\t\t\tfloat hard_shadow = step( distribution.x, compare );\n\t\t#else\n\t\t\tfloat hard_shadow = step( compare, distribution.x );\n\t\t#endif\n\t\tif ( hard_shadow != 1.0 ) {\n\t\t\tfloat distance = compare - distribution.x;\n\t\t\tfloat variance = max( 0.00000, distribution.y * distribution.y );\n\t\t\tfloat softness_probability = variance / (variance + distance * distance );\t\t\tsoftness_probability = clamp( ( softness_probability - 0.3 ) / ( 0.95 - 0.3 ), 0.0, 1.0 );\t\t\tocclusion = clamp( max( hard_shadow, softness_probability ), 0.0, 1.0 );\n\t\t}\n\t\treturn occlusion;\n\t}\n\tfloat getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) {\n\t\tfloat shadow = 1.0;\n\t\tshadowCoord.xyz /= shadowCoord.w;\n\t\tshadowCoord.z += shadowBias;\n\t\tbool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0;\n\t\tbool frustumTest = inFrustum && shadowCoord.z <= 1.0;\n\t\tif ( frustumTest ) {\n\t\t#if defined( SHADOWMAP_TYPE_PCF )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx0 = - texelSize.x * shadowRadius;\n\t\t\tfloat dy0 = - texelSize.y * shadowRadius;\n\t\t\tfloat dx1 = + texelSize.x * shadowRadius;\n\t\t\tfloat dy1 = + texelSize.y * shadowRadius;\n\t\t\tfloat dx2 = dx0 / 2.0;\n\t\t\tfloat dy2 = dy0 / 2.0;\n\t\t\tfloat dx3 = dx1 / 2.0;\n\t\t\tfloat dy3 = dy1 / 2.0;\n\t\t\tshadow = (\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z )\n\t\t\t) * ( 1.0 / 17.0 );\n\t\t#elif defined( SHADOWMAP_TYPE_PCF_SOFT )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx = texelSize.x;\n\t\t\tfloat dy = texelSize.y;\n\t\t\tvec2 uv = shadowCoord.xy;\n\t\t\tvec2 f = fract( uv * shadowMapSize + 0.5 );\n\t\t\tuv -= f * texelSize;\n\t\t\tshadow = (\n\t\t\t\ttexture2DCompare( shadowMap, uv, shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( dx, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( 0.0, dy ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + texelSize, shadowCoord.z ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( -dx, 0.0 ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 0.0 ), shadowCoord.z ),\n\t\t\t\t\t f.x ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( -dx, dy ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, dy ), shadowCoord.z ),\n\t\t\t\t\t f.x ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( 0.0, -dy ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 0.0, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t f.y ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t f.y ) +\n\t\t\t\tmix( mix( texture2DCompare( shadowMap, uv + vec2( -dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t\t  texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t\t  f.x ),\n\t\t\t\t\t mix( texture2DCompare( shadowMap, uv + vec2( -dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t\t  texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t\t  f.x ),\n\t\t\t\t\t f.y )\n\t\t\t) * ( 1.0 / 9.0 );\n\t\t#elif defined( SHADOWMAP_TYPE_VSM )\n\t\t\tshadow = VSMShadow( shadowMap, shadowCoord.xy, shadowCoord.z );\n\t\t#else\n\t\t\tshadow = texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z );\n\t\t#endif\n\t\t}\n\t\treturn mix( 1.0, shadow, shadowIntensity );\n\t}\n\tvec2 cubeToUV( vec3 v, float texelSizeY ) {\n\t\tvec3 absV = abs( v );\n\t\tfloat scaleToCube = 1.0 / max( absV.x, max( absV.y, absV.z ) );\n\t\tabsV *= scaleToCube;\n\t\tv *= scaleToCube * ( 1.0 - 2.0 * texelSizeY );\n\t\tvec2 planar = v.xy;\n\t\tfloat almostATexel = 1.5 * texelSizeY;\n\t\tfloat almostOne = 1.0 - almostATexel;\n\t\tif ( absV.z >= almostOne ) {\n\t\t\tif ( v.z > 0.0 )\n\t\t\t\tplanar.x = 4.0 - v.x;\n\t\t} else if ( absV.x >= almostOne ) {\n\t\t\tfloat signX = sign( v.x );\n\t\t\tplanar.x = v.z * signX + 2.0 * signX;\n\t\t} else if ( absV.y >= almostOne ) {\n\t\t\tfloat signY = sign( v.y );\n\t\t\tplanar.x = v.x + 2.0 * signY + 2.0;\n\t\t\tplanar.y = v.z * signY - 2.0;\n\t\t}\n\t\treturn vec2( 0.125, 0.25 ) * planar + vec2( 0.375, 0.75 );\n\t}\n\tfloat getPointShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) {\n\t\tfloat shadow = 1.0;\n\t\tvec3 lightToPosition = shadowCoord.xyz;\n\t\t\n\t\tfloat lightToPositionLength = length( lightToPosition );\n\t\tif ( lightToPositionLength - shadowCameraFar <= 0.0 && lightToPositionLength - shadowCameraNear >= 0.0 ) {\n\t\t\tfloat dp = ( lightToPositionLength - shadowCameraNear ) / ( shadowCameraFar - shadowCameraNear );\t\t\tdp += shadowBias;\n\t\t\tvec3 bd3D = normalize( lightToPosition );\n\t\t\tvec2 texelSize = vec2( 1.0 ) / ( shadowMapSize * vec2( 4.0, 2.0 ) );\n\t\t\t#if defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_PCF_SOFT ) || defined( SHADOWMAP_TYPE_VSM )\n\t\t\t\tvec2 offset = vec2( - 1, 1 ) * shadowRadius * texelSize.y;\n\t\t\t\tshadow = (\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyy, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyy, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyx, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyx, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxy, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxy, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxx, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxx, texelSize.y ), dp )\n\t\t\t\t) * ( 1.0 / 9.0 );\n\t\t\t#else\n\t\t\t\tshadow = texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp );\n\t\t\t#endif\n\t\t}\n\t\treturn mix( 1.0, shadow, shadowIntensity );\n\t}\n#endif";

var shadowmap_pars_vertex = "#if NUM_SPOT_LIGHT_COORDS > 0\n\tuniform mat4 spotLightMatrix[ NUM_SPOT_LIGHT_COORDS ];\n\tvarying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ];\n#endif\n#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\tuniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tstruct DirectionalLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t\tstruct SpotLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\tuniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tstruct PointLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t\tfloat shadowCameraNear;\n\t\t\tfloat shadowCameraFar;\n\t\t};\n\t\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\n\t#endif\n#endif";

var shadowmap_vertex = "#if ( defined( USE_SHADOWMAP ) && ( NUM_DIR_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 )\n\tvec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\n\tvec4 shadowWorldPosition;\n#endif\n#if defined( USE_SHADOWMAP )\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\n\t\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );\n\t\t\tvDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\n\t\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * pointLightShadows[ i ].shadowNormalBias, 0 );\n\t\t\tvPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t#endif\n#endif\n#if NUM_SPOT_LIGHT_COORDS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHT_COORDS; i ++ ) {\n\t\tshadowWorldPosition = worldPosition;\n\t\t#if ( defined( USE_SHADOWMAP ) && UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\t\tshadowWorldPosition.xyz += shadowWorldNormal * spotLightShadows[ i ].shadowNormalBias;\n\t\t#endif\n\t\tvSpotLightCoord[ i ] = spotLightMatrix[ i ] * shadowWorldPosition;\n\t}\n\t#pragma unroll_loop_end\n#endif";

var shadowmask_pars_fragment = "float getShadowMask() {\n\tfloat shadow = 1.0;\n\t#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\tDirectionalLightShadow directionalLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\n\t\tdirectionalLight = directionalLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowIntensity, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\tSpotLightShadow spotLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) {\n\t\tspotLight = spotLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowIntensity, spotLight.shadowBias, spotLight.shadowRadius, vSpotLightCoord[ i ] ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\tPointLightShadow pointLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\n\t\tpointLight = pointLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowIntensity, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#endif\n\treturn shadow;\n}";

var skinbase_vertex = "#ifdef USE_SKINNING\n\tmat4 boneMatX = getBoneMatrix( skinIndex.x );\n\tmat4 boneMatY = getBoneMatrix( skinIndex.y );\n\tmat4 boneMatZ = getBoneMatrix( skinIndex.z );\n\tmat4 boneMatW = getBoneMatrix( skinIndex.w );\n#endif";

var skinning_pars_vertex = "#ifdef USE_SKINNING\n\tuniform mat4 bindMatrix;\n\tuniform mat4 bindMatrixInverse;\n\tuniform highp sampler2D boneTexture;\n\tmat4 getBoneMatrix( const in float i ) {\n\t\tint size = textureSize( boneTexture, 0 ).x;\n\t\tint j = int( i ) * 4;\n\t\tint x = j % size;\n\t\tint y = j / size;\n\t\tvec4 v1 = texelFetch( boneTexture, ivec2( x, y ), 0 );\n\t\tvec4 v2 = texelFetch( boneTexture, ivec2( x + 1, y ), 0 );\n\t\tvec4 v3 = texelFetch( boneTexture, ivec2( x + 2, y ), 0 );\n\t\tvec4 v4 = texelFetch( boneTexture, ivec2( x + 3, y ), 0 );\n\t\treturn mat4( v1, v2, v3, v4 );\n\t}\n#endif";

var skinning_vertex = "#ifdef USE_SKINNING\n\tvec4 skinVertex = bindMatrix * vec4( transformed, 1.0 );\n\tvec4 skinned = vec4( 0.0 );\n\tskinned += boneMatX * skinVertex * skinWeight.x;\n\tskinned += boneMatY * skinVertex * skinWeight.y;\n\tskinned += boneMatZ * skinVertex * skinWeight.z;\n\tskinned += boneMatW * skinVertex * skinWeight.w;\n\ttransformed = ( bindMatrixInverse * skinned ).xyz;\n#endif";

var skinnormal_vertex = "#ifdef USE_SKINNING\n\tmat4 skinMatrix = mat4( 0.0 );\n\tskinMatrix += skinWeight.x * boneMatX;\n\tskinMatrix += skinWeight.y * boneMatY;\n\tskinMatrix += skinWeight.z * boneMatZ;\n\tskinMatrix += skinWeight.w * boneMatW;\n\tskinMatrix = bindMatrixInverse * skinMatrix * bindMatrix;\n\tobjectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz;\n\t#ifdef USE_TANGENT\n\t\tobjectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz;\n\t#endif\n#endif";

var specularmap_fragment = "float specularStrength;\n#ifdef USE_SPECULARMAP\n\tvec4 texelSpecular = texture2D( specularMap, vSpecularMapUv );\n\tspecularStrength = texelSpecular.r;\n#else\n\tspecularStrength = 1.0;\n#endif";

var specularmap_pars_fragment = "#ifdef USE_SPECULARMAP\n\tuniform sampler2D specularMap;\n#endif";

var tonemapping_fragment = "#if defined( TONE_MAPPING )\n\tgl_FragColor.rgb = toneMapping( gl_FragColor.rgb );\n#endif";

var tonemapping_pars_fragment = "#ifndef saturate\n#define saturate( a ) clamp( a, 0.0, 1.0 )\n#endif\nuniform float toneMappingExposure;\nvec3 LinearToneMapping( vec3 color ) {\n\treturn saturate( toneMappingExposure * color );\n}\nvec3 ReinhardToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\treturn saturate( color / ( vec3( 1.0 ) + color ) );\n}\nvec3 CineonToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\tcolor = max( vec3( 0.0 ), color - 0.004 );\n\treturn pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) );\n}\nvec3 RRTAndODTFit( vec3 v ) {\n\tvec3 a = v * ( v + 0.0245786 ) - 0.000090537;\n\tvec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081;\n\treturn a / b;\n}\nvec3 ACESFilmicToneMapping( vec3 color ) {\n\tconst mat3 ACESInputMat = mat3(\n\t\tvec3( 0.59719, 0.07600, 0.02840 ),\t\tvec3( 0.35458, 0.90834, 0.13383 ),\n\t\tvec3( 0.04823, 0.01566, 0.83777 )\n\t);\n\tconst mat3 ACESOutputMat = mat3(\n\t\tvec3(  1.60475, -0.10208, -0.00327 ),\t\tvec3( -0.53108,  1.10813, -0.07276 ),\n\t\tvec3( -0.07367, -0.00605,  1.07602 )\n\t);\n\tcolor *= toneMappingExposure / 0.6;\n\tcolor = ACESInputMat * color;\n\tcolor = RRTAndODTFit( color );\n\tcolor = ACESOutputMat * color;\n\treturn saturate( color );\n}\nconst mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3(\n\tvec3( 1.6605, - 0.1246, - 0.0182 ),\n\tvec3( - 0.5876, 1.1329, - 0.1006 ),\n\tvec3( - 0.0728, - 0.0083, 1.1187 )\n);\nconst mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3(\n\tvec3( 0.6274, 0.0691, 0.0164 ),\n\tvec3( 0.3293, 0.9195, 0.0880 ),\n\tvec3( 0.0433, 0.0113, 0.8956 )\n);\nvec3 agxDefaultContrastApprox( vec3 x ) {\n\tvec3 x2 = x * x;\n\tvec3 x4 = x2 * x2;\n\treturn + 15.5 * x4 * x2\n\t\t- 40.14 * x4 * x\n\t\t+ 31.96 * x4\n\t\t- 6.868 * x2 * x\n\t\t+ 0.4298 * x2\n\t\t+ 0.1191 * x\n\t\t- 0.00232;\n}\nvec3 AgXToneMapping( vec3 color ) {\n\tconst mat3 AgXInsetMatrix = mat3(\n\t\tvec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ),\n\t\tvec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ),\n\t\tvec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 )\n\t);\n\tconst mat3 AgXOutsetMatrix = mat3(\n\t\tvec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ),\n\t\tvec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ),\n\t\tvec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 )\n\t);\n\tconst float AgxMinEv = - 12.47393;\tconst float AgxMaxEv = 4.026069;\n\tcolor *= toneMappingExposure;\n\tcolor = LINEAR_SRGB_TO_LINEAR_REC2020 * color;\n\tcolor = AgXInsetMatrix * color;\n\tcolor = max( color, 1e-10 );\tcolor = log2( color );\n\tcolor = ( color - AgxMinEv ) / ( AgxMaxEv - AgxMinEv );\n\tcolor = clamp( color, 0.0, 1.0 );\n\tcolor = agxDefaultContrastApprox( color );\n\tcolor = AgXOutsetMatrix * color;\n\tcolor = pow( max( vec3( 0.0 ), color ), vec3( 2.2 ) );\n\tcolor = LINEAR_REC2020_TO_LINEAR_SRGB * color;\n\tcolor = clamp( color, 0.0, 1.0 );\n\treturn color;\n}\nvec3 NeutralToneMapping( vec3 color ) {\n\tconst float StartCompression = 0.8 - 0.04;\n\tconst float Desaturation = 0.15;\n\tcolor *= toneMappingExposure;\n\tfloat x = min( color.r, min( color.g, color.b ) );\n\tfloat offset = x < 0.08 ? x - 6.25 * x * x : 0.04;\n\tcolor -= offset;\n\tfloat peak = max( color.r, max( color.g, color.b ) );\n\tif ( peak < StartCompression ) return color;\n\tfloat d = 1. - StartCompression;\n\tfloat newPeak = 1. - d * d / ( peak + d - StartCompression );\n\tcolor *= newPeak / peak;\n\tfloat g = 1. - 1. / ( Desaturation * ( peak - newPeak ) + 1. );\n\treturn mix( color, vec3( newPeak ), g );\n}\nvec3 CustomToneMapping( vec3 color ) { return color; }";

var transmission_fragment = "#ifdef USE_TRANSMISSION\n\tmaterial.transmission = transmission;\n\tmaterial.transmissionAlpha = 1.0;\n\tmaterial.thickness = thickness;\n\tmaterial.attenuationDistance = attenuationDistance;\n\tmaterial.attenuationColor = attenuationColor;\n\t#ifdef USE_TRANSMISSIONMAP\n\t\tmaterial.transmission *= texture2D( transmissionMap, vTransmissionMapUv ).r;\n\t#endif\n\t#ifdef USE_THICKNESSMAP\n\t\tmaterial.thickness *= texture2D( thicknessMap, vThicknessMapUv ).g;\n\t#endif\n\tvec3 pos = vWorldPosition;\n\tvec3 v = normalize( cameraPosition - pos );\n\tvec3 n = inverseTransformDirection( normal, viewMatrix );\n\tvec4 transmitted = getIBLVolumeRefraction(\n\t\tn, v, material.roughness, material.diffuseColor, material.specularColor, material.specularF90,\n\t\tpos, modelMatrix, viewMatrix, projectionMatrix, material.dispersion, material.ior, material.thickness,\n\t\tmaterial.attenuationColor, material.attenuationDistance );\n\tmaterial.transmissionAlpha = mix( material.transmissionAlpha, transmitted.a, material.transmission );\n\ttotalDiffuse = mix( totalDiffuse, transmitted.rgb, material.transmission );\n#endif";

var transmission_pars_fragment = "#ifdef USE_TRANSMISSION\n\tuniform float transmission;\n\tuniform float thickness;\n\tuniform float attenuationDistance;\n\tuniform vec3 attenuationColor;\n\t#ifdef USE_TRANSMISSIONMAP\n\t\tuniform sampler2D transmissionMap;\n\t#endif\n\t#ifdef USE_THICKNESSMAP\n\t\tuniform sampler2D thicknessMap;\n\t#endif\n\tuniform vec2 transmissionSamplerSize;\n\tuniform sampler2D transmissionSamplerMap;\n\tuniform mat4 modelMatrix;\n\tuniform mat4 projectionMatrix;\n\tvarying vec3 vWorldPosition;\n\tfloat w0( float a ) {\n\t\treturn ( 1.0 / 6.0 ) * ( a * ( a * ( - a + 3.0 ) - 3.0 ) + 1.0 );\n\t}\n\tfloat w1( float a ) {\n\t\treturn ( 1.0 / 6.0 ) * ( a *  a * ( 3.0 * a - 6.0 ) + 4.0 );\n\t}\n\tfloat w2( float a ){\n\t\treturn ( 1.0 / 6.0 ) * ( a * ( a * ( - 3.0 * a + 3.0 ) + 3.0 ) + 1.0 );\n\t}\n\tfloat w3( float a ) {\n\t\treturn ( 1.0 / 6.0 ) * ( a * a * a );\n\t}\n\tfloat g0( float a ) {\n\t\treturn w0( a ) + w1( a );\n\t}\n\tfloat g1( float a ) {\n\t\treturn w2( a ) + w3( a );\n\t}\n\tfloat h0( float a ) {\n\t\treturn - 1.0 + w1( a ) / ( w0( a ) + w1( a ) );\n\t}\n\tfloat h1( float a ) {\n\t\treturn 1.0 + w3( a ) / ( w2( a ) + w3( a ) );\n\t}\n\tvec4 bicubic( sampler2D tex, vec2 uv, vec4 texelSize, float lod ) {\n\t\tuv = uv * texelSize.zw + 0.5;\n\t\tvec2 iuv = floor( uv );\n\t\tvec2 fuv = fract( uv );\n\t\tfloat g0x = g0( fuv.x );\n\t\tfloat g1x = g1( fuv.x );\n\t\tfloat h0x = h0( fuv.x );\n\t\tfloat h1x = h1( fuv.x );\n\t\tfloat h0y = h0( fuv.y );\n\t\tfloat h1y = h1( fuv.y );\n\t\tvec2 p0 = ( vec2( iuv.x + h0x, iuv.y + h0y ) - 0.5 ) * texelSize.xy;\n\t\tvec2 p1 = ( vec2( iuv.x + h1x, iuv.y + h0y ) - 0.5 ) * texelSize.xy;\n\t\tvec2 p2 = ( vec2( iuv.x + h0x, iuv.y + h1y ) - 0.5 ) * texelSize.xy;\n\t\tvec2 p3 = ( vec2( iuv.x + h1x, iuv.y + h1y ) - 0.5 ) * texelSize.xy;\n\t\treturn g0( fuv.y ) * ( g0x * textureLod( tex, p0, lod ) + g1x * textureLod( tex, p1, lod ) ) +\n\t\t\tg1( fuv.y ) * ( g0x * textureLod( tex, p2, lod ) + g1x * textureLod( tex, p3, lod ) );\n\t}\n\tvec4 textureBicubic( sampler2D sampler, vec2 uv, float lod ) {\n\t\tvec2 fLodSize = vec2( textureSize( sampler, int( lod ) ) );\n\t\tvec2 cLodSize = vec2( textureSize( sampler, int( lod + 1.0 ) ) );\n\t\tvec2 fLodSizeInv = 1.0 / fLodSize;\n\t\tvec2 cLodSizeInv = 1.0 / cLodSize;\n\t\tvec4 fSample = bicubic( sampler, uv, vec4( fLodSizeInv, fLodSize ), floor( lod ) );\n\t\tvec4 cSample = bicubic( sampler, uv, vec4( cLodSizeInv, cLodSize ), ceil( lod ) );\n\t\treturn mix( fSample, cSample, fract( lod ) );\n\t}\n\tvec3 getVolumeTransmissionRay( const in vec3 n, const in vec3 v, const in float thickness, const in float ior, const in mat4 modelMatrix ) {\n\t\tvec3 refractionVector = refract( - v, normalize( n ), 1.0 / ior );\n\t\tvec3 modelScale;\n\t\tmodelScale.x = length( vec3( modelMatrix[ 0 ].xyz ) );\n\t\tmodelScale.y = length( vec3( modelMatrix[ 1 ].xyz ) );\n\t\tmodelScale.z = length( vec3( modelMatrix[ 2 ].xyz ) );\n\t\treturn normalize( refractionVector ) * thickness * modelScale;\n\t}\n\tfloat applyIorToRoughness( const in float roughness, const in float ior ) {\n\t\treturn roughness * clamp( ior * 2.0 - 2.0, 0.0, 1.0 );\n\t}\n\tvec4 getTransmissionSample( const in vec2 fragCoord, const in float roughness, const in float ior ) {\n\t\tfloat lod = log2( transmissionSamplerSize.x ) * applyIorToRoughness( roughness, ior );\n\t\treturn textureBicubic( transmissionSamplerMap, fragCoord.xy, lod );\n\t}\n\tvec3 volumeAttenuation( const in float transmissionDistance, const in vec3 attenuationColor, const in float attenuationDistance ) {\n\t\tif ( isinf( attenuationDistance ) ) {\n\t\t\treturn vec3( 1.0 );\n\t\t} else {\n\t\t\tvec3 attenuationCoefficient = -log( attenuationColor ) / attenuationDistance;\n\t\t\tvec3 transmittance = exp( - attenuationCoefficient * transmissionDistance );\t\t\treturn transmittance;\n\t\t}\n\t}\n\tvec4 getIBLVolumeRefraction( const in vec3 n, const in vec3 v, const in float roughness, const in vec3 diffuseColor,\n\t\tconst in vec3 specularColor, const in float specularF90, const in vec3 position, const in mat4 modelMatrix,\n\t\tconst in mat4 viewMatrix, const in mat4 projMatrix, const in float dispersion, const in float ior, const in float thickness,\n\t\tconst in vec3 attenuationColor, const in float attenuationDistance ) {\n\t\tvec4 transmittedLight;\n\t\tvec3 transmittance;\n\t\t#ifdef USE_DISPERSION\n\t\t\tfloat halfSpread = ( ior - 1.0 ) * 0.025 * dispersion;\n\t\t\tvec3 iors = vec3( ior - halfSpread, ior, ior + halfSpread );\n\t\t\tfor ( int i = 0; i < 3; i ++ ) {\n\t\t\t\tvec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, iors[ i ], modelMatrix );\n\t\t\t\tvec3 refractedRayExit = position + transmissionRay;\n\t\t\t\tvec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 );\n\t\t\t\tvec2 refractionCoords = ndcPos.xy / ndcPos.w;\n\t\t\t\trefractionCoords += 1.0;\n\t\t\t\trefractionCoords /= 2.0;\n\t\t\t\tvec4 transmissionSample = getTransmissionSample( refractionCoords, roughness, iors[ i ] );\n\t\t\t\ttransmittedLight[ i ] = transmissionSample[ i ];\n\t\t\t\ttransmittedLight.a += transmissionSample.a;\n\t\t\t\ttransmittance[ i ] = diffuseColor[ i ] * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance )[ i ];\n\t\t\t}\n\t\t\ttransmittedLight.a /= 3.0;\n\t\t#else\n\t\t\tvec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix );\n\t\t\tvec3 refractedRayExit = position + transmissionRay;\n\t\t\tvec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 );\n\t\t\tvec2 refractionCoords = ndcPos.xy / ndcPos.w;\n\t\t\trefractionCoords += 1.0;\n\t\t\trefractionCoords /= 2.0;\n\t\t\ttransmittedLight = getTransmissionSample( refractionCoords, roughness, ior );\n\t\t\ttransmittance = diffuseColor * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance );\n\t\t#endif\n\t\tvec3 attenuatedColor = transmittance * transmittedLight.rgb;\n\t\tvec3 F = EnvironmentBRDF( n, v, specularColor, specularF90, roughness );\n\t\tfloat transmittanceFactor = ( transmittance.r + transmittance.g + transmittance.b ) / 3.0;\n\t\treturn vec4( ( 1.0 - F ) * attenuatedColor, 1.0 - ( 1.0 - transmittedLight.a ) * transmittanceFactor );\n\t}\n#endif";

var uv_pars_fragment = "#if defined( USE_UV ) || defined( USE_ANISOTROPY )\n\tvarying vec2 vUv;\n#endif\n#ifdef USE_MAP\n\tvarying vec2 vMapUv;\n#endif\n#ifdef USE_ALPHAMAP\n\tvarying vec2 vAlphaMapUv;\n#endif\n#ifdef USE_LIGHTMAP\n\tvarying vec2 vLightMapUv;\n#endif\n#ifdef USE_AOMAP\n\tvarying vec2 vAoMapUv;\n#endif\n#ifdef USE_BUMPMAP\n\tvarying vec2 vBumpMapUv;\n#endif\n#ifdef USE_NORMALMAP\n\tvarying vec2 vNormalMapUv;\n#endif\n#ifdef USE_EMISSIVEMAP\n\tvarying vec2 vEmissiveMapUv;\n#endif\n#ifdef USE_METALNESSMAP\n\tvarying vec2 vMetalnessMapUv;\n#endif\n#ifdef USE_ROUGHNESSMAP\n\tvarying vec2 vRoughnessMapUv;\n#endif\n#ifdef USE_ANISOTROPYMAP\n\tvarying vec2 vAnisotropyMapUv;\n#endif\n#ifdef USE_CLEARCOATMAP\n\tvarying vec2 vClearcoatMapUv;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tvarying vec2 vClearcoatNormalMapUv;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tvarying vec2 vClearcoatRoughnessMapUv;\n#endif\n#ifdef USE_IRIDESCENCEMAP\n\tvarying vec2 vIridescenceMapUv;\n#endif\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\tvarying vec2 vIridescenceThicknessMapUv;\n#endif\n#ifdef USE_SHEEN_COLORMAP\n\tvarying vec2 vSheenColorMapUv;\n#endif\n#ifdef USE_SHEEN_ROUGHNESSMAP\n\tvarying vec2 vSheenRoughnessMapUv;\n#endif\n#ifdef USE_SPECULARMAP\n\tvarying vec2 vSpecularMapUv;\n#endif\n#ifdef USE_SPECULAR_COLORMAP\n\tvarying vec2 vSpecularColorMapUv;\n#endif\n#ifdef USE_SPECULAR_INTENSITYMAP\n\tvarying vec2 vSpecularIntensityMapUv;\n#endif\n#ifdef USE_TRANSMISSIONMAP\n\tuniform mat3 transmissionMapTransform;\n\tvarying vec2 vTransmissionMapUv;\n#endif\n#ifdef USE_THICKNESSMAP\n\tuniform mat3 thicknessMapTransform;\n\tvarying vec2 vThicknessMapUv;\n#endif";

var uv_pars_vertex = "#if defined( USE_UV ) || defined( USE_ANISOTROPY )\n\tvarying vec2 vUv;\n#endif\n#ifdef USE_MAP\n\tuniform mat3 mapTransform;\n\tvarying vec2 vMapUv;\n#endif\n#ifdef USE_ALPHAMAP\n\tuniform mat3 alphaMapTransform;\n\tvarying vec2 vAlphaMapUv;\n#endif\n#ifdef USE_LIGHTMAP\n\tuniform mat3 lightMapTransform;\n\tvarying vec2 vLightMapUv;\n#endif\n#ifdef USE_AOMAP\n\tuniform mat3 aoMapTransform;\n\tvarying vec2 vAoMapUv;\n#endif\n#ifdef USE_BUMPMAP\n\tuniform mat3 bumpMapTransform;\n\tvarying vec2 vBumpMapUv;\n#endif\n#ifdef USE_NORMALMAP\n\tuniform mat3 normalMapTransform;\n\tvarying vec2 vNormalMapUv;\n#endif\n#ifdef USE_DISPLACEMENTMAP\n\tuniform mat3 displacementMapTransform;\n\tvarying vec2 vDisplacementMapUv;\n#endif\n#ifdef USE_EMISSIVEMAP\n\tuniform mat3 emissiveMapTransform;\n\tvarying vec2 vEmissiveMapUv;\n#endif\n#ifdef USE_METALNESSMAP\n\tuniform mat3 metalnessMapTransform;\n\tvarying vec2 vMetalnessMapUv;\n#endif\n#ifdef USE_ROUGHNESSMAP\n\tuniform mat3 roughnessMapTransform;\n\tvarying vec2 vRoughnessMapUv;\n#endif\n#ifdef USE_ANISOTROPYMAP\n\tuniform mat3 anisotropyMapTransform;\n\tvarying vec2 vAnisotropyMapUv;\n#endif\n#ifdef USE_CLEARCOATMAP\n\tuniform mat3 clearcoatMapTransform;\n\tvarying vec2 vClearcoatMapUv;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tuniform mat3 clearcoatNormalMapTransform;\n\tvarying vec2 vClearcoatNormalMapUv;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tuniform mat3 clearcoatRoughnessMapTransform;\n\tvarying vec2 vClearcoatRoughnessMapUv;\n#endif\n#ifdef USE_SHEEN_COLORMAP\n\tuniform mat3 sheenColorMapTransform;\n\tvarying vec2 vSheenColorMapUv;\n#endif\n#ifdef USE_SHEEN_ROUGHNESSMAP\n\tuniform mat3 sheenRoughnessMapTransform;\n\tvarying vec2 vSheenRoughnessMapUv;\n#endif\n#ifdef USE_IRIDESCENCEMAP\n\tuniform mat3 iridescenceMapTransform;\n\tvarying vec2 vIridescenceMapUv;\n#endif\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\tuniform mat3 iridescenceThicknessMapTransform;\n\tvarying vec2 vIridescenceThicknessMapUv;\n#endif\n#ifdef USE_SPECULARMAP\n\tuniform mat3 specularMapTransform;\n\tvarying vec2 vSpecularMapUv;\n#endif\n#ifdef USE_SPECULAR_COLORMAP\n\tuniform mat3 specularColorMapTransform;\n\tvarying vec2 vSpecularColorMapUv;\n#endif\n#ifdef USE_SPECULAR_INTENSITYMAP\n\tuniform mat3 specularIntensityMapTransform;\n\tvarying vec2 vSpecularIntensityMapUv;\n#endif\n#ifdef USE_TRANSMISSIONMAP\n\tuniform mat3 transmissionMapTransform;\n\tvarying vec2 vTransmissionMapUv;\n#endif\n#ifdef USE_THICKNESSMAP\n\tuniform mat3 thicknessMapTransform;\n\tvarying vec2 vThicknessMapUv;\n#endif";

var uv_vertex = "#if defined( USE_UV ) || defined( USE_ANISOTROPY )\n\tvUv = vec3( uv, 1 ).xy;\n#endif\n#ifdef USE_MAP\n\tvMapUv = ( mapTransform * vec3( MAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_ALPHAMAP\n\tvAlphaMapUv = ( alphaMapTransform * vec3( ALPHAMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_LIGHTMAP\n\tvLightMapUv = ( lightMapTransform * vec3( LIGHTMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_AOMAP\n\tvAoMapUv = ( aoMapTransform * vec3( AOMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_BUMPMAP\n\tvBumpMapUv = ( bumpMapTransform * vec3( BUMPMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_NORMALMAP\n\tvNormalMapUv = ( normalMapTransform * vec3( NORMALMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_DISPLACEMENTMAP\n\tvDisplacementMapUv = ( displacementMapTransform * vec3( DISPLACEMENTMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_EMISSIVEMAP\n\tvEmissiveMapUv = ( emissiveMapTransform * vec3( EMISSIVEMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_METALNESSMAP\n\tvMetalnessMapUv = ( metalnessMapTransform * vec3( METALNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_ROUGHNESSMAP\n\tvRoughnessMapUv = ( roughnessMapTransform * vec3( ROUGHNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_ANISOTROPYMAP\n\tvAnisotropyMapUv = ( anisotropyMapTransform * vec3( ANISOTROPYMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_CLEARCOATMAP\n\tvClearcoatMapUv = ( clearcoatMapTransform * vec3( CLEARCOATMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tvClearcoatNormalMapUv = ( clearcoatNormalMapTransform * vec3( CLEARCOAT_NORMALMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tvClearcoatRoughnessMapUv = ( clearcoatRoughnessMapTransform * vec3( CLEARCOAT_ROUGHNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_IRIDESCENCEMAP\n\tvIridescenceMapUv = ( iridescenceMapTransform * vec3( IRIDESCENCEMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\tvIridescenceThicknessMapUv = ( iridescenceThicknessMapTransform * vec3( IRIDESCENCE_THICKNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SHEEN_COLORMAP\n\tvSheenColorMapUv = ( sheenColorMapTransform * vec3( SHEEN_COLORMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SHEEN_ROUGHNESSMAP\n\tvSheenRoughnessMapUv = ( sheenRoughnessMapTransform * vec3( SHEEN_ROUGHNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SPECULARMAP\n\tvSpecularMapUv = ( specularMapTransform * vec3( SPECULARMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SPECULAR_COLORMAP\n\tvSpecularColorMapUv = ( specularColorMapTransform * vec3( SPECULAR_COLORMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SPECULAR_INTENSITYMAP\n\tvSpecularIntensityMapUv = ( specularIntensityMapTransform * vec3( SPECULAR_INTENSITYMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_TRANSMISSIONMAP\n\tvTransmissionMapUv = ( transmissionMapTransform * vec3( TRANSMISSIONMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_THICKNESSMAP\n\tvThicknessMapUv = ( thicknessMapTransform * vec3( THICKNESSMAP_UV, 1 ) ).xy;\n#endif";

var worldpos_vertex = "#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0\n\tvec4 worldPosition = vec4( transformed, 1.0 );\n\t#ifdef USE_BATCHING\n\t\tworldPosition = batchingMatrix * worldPosition;\n\t#endif\n\t#ifdef USE_INSTANCING\n\t\tworldPosition = instanceMatrix * worldPosition;\n\t#endif\n\tworldPosition = modelMatrix * worldPosition;\n#endif";

const vertex$h = "varying vec2 vUv;\nuniform mat3 uvTransform;\nvoid main() {\n\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\n\tgl_Position = vec4( position.xy, 1.0, 1.0 );\n}";

const fragment$h = "uniform sampler2D t2D;\nuniform float backgroundIntensity;\nvarying vec2 vUv;\nvoid main() {\n\tvec4 texColor = texture2D( t2D, vUv );\n\t#ifdef DECODE_VIDEO_TEXTURE\n\t\ttexColor = vec4( mix( pow( texColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), texColor.rgb * 0.0773993808, vec3( lessThanEqual( texColor.rgb, vec3( 0.04045 ) ) ) ), texColor.w );\n\t#endif\n\ttexColor.rgb *= backgroundIntensity;\n\tgl_FragColor = texColor;\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n}";

const vertex$g = "varying vec3 vWorldDirection;\n#include <common>\nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include <begin_vertex>\n\t#include <project_vertex>\n\tgl_Position.z = gl_Position.w;\n}";

const fragment$g = "#ifdef ENVMAP_TYPE_CUBE\n\tuniform samplerCube envMap;\n#elif defined( ENVMAP_TYPE_CUBE_UV )\n\tuniform sampler2D envMap;\n#endif\nuniform float flipEnvMap;\nuniform float backgroundBlurriness;\nuniform float backgroundIntensity;\nuniform mat3 backgroundRotation;\nvarying vec3 vWorldDirection;\n#include <cube_uv_reflection_fragment>\nvoid main() {\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tvec4 texColor = textureCube( envMap, backgroundRotation * vec3( flipEnvMap * vWorldDirection.x, vWorldDirection.yz ) );\n\t#elif defined( ENVMAP_TYPE_CUBE_UV )\n\t\tvec4 texColor = textureCubeUV( envMap, backgroundRotation * vWorldDirection, backgroundBlurriness );\n\t#else\n\t\tvec4 texColor = vec4( 0.0, 0.0, 0.0, 1.0 );\n\t#endif\n\ttexColor.rgb *= backgroundIntensity;\n\tgl_FragColor = texColor;\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n}";

const vertex$f = "varying vec3 vWorldDirection;\n#include <common>\nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include <begin_vertex>\n\t#include <project_vertex>\n\tgl_Position.z = gl_Position.w;\n}";

const fragment$f = "uniform samplerCube tCube;\nuniform float tFlip;\nuniform float opacity;\nvarying vec3 vWorldDirection;\nvoid main() {\n\tvec4 texColor = textureCube( tCube, vec3( tFlip * vWorldDirection.x, vWorldDirection.yz ) );\n\tgl_FragColor = texColor;\n\tgl_FragColor.a *= opacity;\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n}";

const vertex$e = "#include <common>\n#include <batching_pars_vertex>\n#include <uv_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\t#include <uv_vertex>\n\t#include <batching_vertex>\n\t#include <skinbase_vertex>\n\t#include <morphinstance_vertex>\n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include <beginnormal_vertex>\n\t\t#include <morphnormal_vertex>\n\t\t#include <skinnormal_vertex>\n\t#endif\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\tvHighPrecisionZW = gl_Position.zw;\n}";

const fragment$e = "#if DEPTH_PACKING == 3200\n\tuniform float opacity;\n#endif\n#include <common>\n#include <packing>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <alphatest_pars_fragment>\n#include <alphahash_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\tvec4 diffuseColor = vec4( 1.0 );\n\t#include <clipping_planes_fragment>\n\t#if DEPTH_PACKING == 3200\n\t\tdiffuseColor.a = opacity;\n\t#endif\n\t#include <map_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <alphahash_fragment>\n\t#include <logdepthbuf_fragment>\n\t#ifdef USE_REVERSED_DEPTH_BUFFER\n\t\tfloat fragCoordZ = vHighPrecisionZW[ 0 ] / vHighPrecisionZW[ 1 ];\n\t#else\n\t\tfloat fragCoordZ = 0.5 * vHighPrecisionZW[ 0 ] / vHighPrecisionZW[ 1 ] + 0.5;\n\t#endif\n\t#if DEPTH_PACKING == 3200\n\t\tgl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );\n\t#elif DEPTH_PACKING == 3201\n\t\tgl_FragColor = packDepthToRGBA( fragCoordZ );\n\t#elif DEPTH_PACKING == 3202\n\t\tgl_FragColor = vec4( packDepthToRGB( fragCoordZ ), 1.0 );\n\t#elif DEPTH_PACKING == 3203\n\t\tgl_FragColor = vec4( packDepthToRG( fragCoordZ ), 0.0, 1.0 );\n\t#endif\n}";

const vertex$d = "#define DISTANCE\nvarying vec3 vWorldPosition;\n#include <common>\n#include <batching_pars_vertex>\n#include <uv_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <batching_vertex>\n\t#include <skinbase_vertex>\n\t#include <morphinstance_vertex>\n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include <beginnormal_vertex>\n\t\t#include <morphnormal_vertex>\n\t\t#include <skinnormal_vertex>\n\t#endif\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <worldpos_vertex>\n\t#include <clipping_planes_vertex>\n\tvWorldPosition = worldPosition.xyz;\n}";

const fragment$d = "#define DISTANCE\nuniform vec3 referencePosition;\nuniform float nearDistance;\nuniform float farDistance;\nvarying vec3 vWorldPosition;\n#include <common>\n#include <packing>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <alphatest_pars_fragment>\n#include <alphahash_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main () {\n\tvec4 diffuseColor = vec4( 1.0 );\n\t#include <clipping_planes_fragment>\n\t#include <map_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <alphahash_fragment>\n\tfloat dist = length( vWorldPosition - referencePosition );\n\tdist = ( dist - nearDistance ) / ( farDistance - nearDistance );\n\tdist = saturate( dist );\n\tgl_FragColor = packDepthToRGBA( dist );\n}";

const vertex$c = "varying vec3 vWorldDirection;\n#include <common>\nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include <begin_vertex>\n\t#include <project_vertex>\n}";

const fragment$c = "uniform sampler2D tEquirect;\nvarying vec3 vWorldDirection;\n#include <common>\nvoid main() {\n\tvec3 direction = normalize( vWorldDirection );\n\tvec2 sampleUV = equirectUv( direction );\n\tgl_FragColor = texture2D( tEquirect, sampleUV );\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n}";

const vertex$b = "uniform float scale;\nattribute float lineDistance;\nvarying float vLineDistance;\n#include <common>\n#include <uv_pars_vertex>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\tvLineDistance = scale * lineDistance;\n\t#include <uv_vertex>\n\t#include <color_vertex>\n\t#include <morphinstance_vertex>\n\t#include <morphcolor_vertex>\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\t#include <fog_vertex>\n}";

const fragment$b = "uniform vec3 diffuse;\nuniform float opacity;\nuniform float dashSize;\nuniform float totalSize;\nvarying float vLineDistance;\n#include <common>\n#include <color_pars_fragment>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <fog_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <clipping_planes_fragment>\n\tif ( mod( vLineDistance, totalSize ) > dashSize ) {\n\t\tdiscard;\n\t}\n\tvec3 outgoingLight = vec3( 0.0 );\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <color_fragment>\n\toutgoingLight = diffuseColor.rgb;\n\t#include <opaque_fragment>\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n}";

const vertex$a = "#include <common>\n#include <batching_pars_vertex>\n#include <uv_pars_vertex>\n#include <envmap_pars_vertex>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <color_vertex>\n\t#include <morphinstance_vertex>\n\t#include <morphcolor_vertex>\n\t#include <batching_vertex>\n\t#if defined ( USE_ENVMAP ) || defined ( USE_SKINNING )\n\t\t#include <beginnormal_vertex>\n\t\t#include <morphnormal_vertex>\n\t\t#include <skinbase_vertex>\n\t\t#include <skinnormal_vertex>\n\t\t#include <defaultnormal_vertex>\n\t#endif\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\t#include <worldpos_vertex>\n\t#include <envmap_vertex>\n\t#include <fog_vertex>\n}";

const fragment$a = "uniform vec3 diffuse;\nuniform float opacity;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include <common>\n#include <dithering_pars_fragment>\n#include <color_pars_fragment>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <alphatest_pars_fragment>\n#include <alphahash_pars_fragment>\n#include <aomap_pars_fragment>\n#include <lightmap_pars_fragment>\n#include <envmap_common_pars_fragment>\n#include <envmap_pars_fragment>\n#include <fog_pars_fragment>\n#include <specularmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <clipping_planes_fragment>\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <color_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <alphahash_fragment>\n\t#include <specularmap_fragment>\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\t#ifdef USE_LIGHTMAP\n\t\tvec4 lightMapTexel = texture2D( lightMap, vLightMapUv );\n\t\treflectedLight.indirectDiffuse += lightMapTexel.rgb * lightMapIntensity * RECIPROCAL_PI;\n\t#else\n\t\treflectedLight.indirectDiffuse += vec3( 1.0 );\n\t#endif\n\t#include <aomap_fragment>\n\treflectedLight.indirectDiffuse *= diffuseColor.rgb;\n\tvec3 outgoingLight = reflectedLight.indirectDiffuse;\n\t#include <envmap_fragment>\n\t#include <opaque_fragment>\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n\t#include <dithering_fragment>\n}";

const vertex$9 = "#define LAMBERT\nvarying vec3 vViewPosition;\n#include <common>\n#include <batching_pars_vertex>\n#include <uv_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <envmap_pars_vertex>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <normal_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <shadowmap_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <color_vertex>\n\t#include <morphinstance_vertex>\n\t#include <morphcolor_vertex>\n\t#include <batching_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n\t#include <normal_vertex>\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\tvViewPosition = - mvPosition.xyz;\n\t#include <worldpos_vertex>\n\t#include <envmap_vertex>\n\t#include <shadowmap_vertex>\n\t#include <fog_vertex>\n}";

const fragment$9 = "#define LAMBERT\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\n#include <common>\n#include <packing>\n#include <dithering_pars_fragment>\n#include <color_pars_fragment>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <alphatest_pars_fragment>\n#include <alphahash_pars_fragment>\n#include <aomap_pars_fragment>\n#include <lightmap_pars_fragment>\n#include <emissivemap_pars_fragment>\n#include <envmap_common_pars_fragment>\n#include <envmap_pars_fragment>\n#include <fog_pars_fragment>\n#include <bsdfs>\n#include <lights_pars_begin>\n#include <normal_pars_fragment>\n#include <lights_lambert_pars_fragment>\n#include <shadowmap_pars_fragment>\n#include <bumpmap_pars_fragment>\n#include <normalmap_pars_fragment>\n#include <specularmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <clipping_planes_fragment>\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <color_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <alphahash_fragment>\n\t#include <specularmap_fragment>\n\t#include <normal_fragment_begin>\n\t#include <normal_fragment_maps>\n\t#include <emissivemap_fragment>\n\t#include <lights_lambert_fragment>\n\t#include <lights_fragment_begin>\n\t#include <lights_fragment_maps>\n\t#include <lights_fragment_end>\n\t#include <aomap_fragment>\n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\t#include <envmap_fragment>\n\t#include <opaque_fragment>\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n\t#include <dithering_fragment>\n}";

const vertex$8 = "#define MATCAP\nvarying vec3 vViewPosition;\n#include <common>\n#include <batching_pars_vertex>\n#include <uv_pars_vertex>\n#include <color_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <fog_pars_vertex>\n#include <normal_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <color_vertex>\n\t#include <morphinstance_vertex>\n\t#include <morphcolor_vertex>\n\t#include <batching_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n\t#include <normal_vertex>\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\t#include <fog_vertex>\n\tvViewPosition = - mvPosition.xyz;\n}";

const fragment$8 = "#define MATCAP\nuniform vec3 diffuse;\nuniform float opacity;\nuniform sampler2D matcap;\nvarying vec3 vViewPosition;\n#include <common>\n#include <dithering_pars_fragment>\n#include <color_pars_fragment>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <alphatest_pars_fragment>\n#include <alphahash_pars_fragment>\n#include <fog_pars_fragment>\n#include <normal_pars_fragment>\n#include <bumpmap_pars_fragment>\n#include <normalmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <clipping_planes_fragment>\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <color_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <alphahash_fragment>\n\t#include <normal_fragment_begin>\n\t#include <normal_fragment_maps>\n\tvec3 viewDir = normalize( vViewPosition );\n\tvec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) );\n\tvec3 y = cross( viewDir, x );\n\tvec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5;\n\t#ifdef USE_MATCAP\n\t\tvec4 matcapColor = texture2D( matcap, uv );\n\t#else\n\t\tvec4 matcapColor = vec4( vec3( mix( 0.2, 0.8, uv.y ) ), 1.0 );\n\t#endif\n\tvec3 outgoingLight = diffuseColor.rgb * matcapColor.rgb;\n\t#include <opaque_fragment>\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n\t#include <dithering_fragment>\n}";

const vertex$7 = "#define NORMAL\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE )\n\tvarying vec3 vViewPosition;\n#endif\n#include <common>\n#include <batching_pars_vertex>\n#include <uv_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <normal_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <batching_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphinstance_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n\t#include <normal_vertex>\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE )\n\tvViewPosition = - mvPosition.xyz;\n#endif\n}";

const fragment$7 = "#define NORMAL\nuniform float opacity;\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE )\n\tvarying vec3 vViewPosition;\n#endif\n#include <packing>\n#include <uv_pars_fragment>\n#include <normal_pars_fragment>\n#include <bumpmap_pars_fragment>\n#include <normalmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\tvec4 diffuseColor = vec4( 0.0, 0.0, 0.0, opacity );\n\t#include <clipping_planes_fragment>\n\t#include <logdepthbuf_fragment>\n\t#include <normal_fragment_begin>\n\t#include <normal_fragment_maps>\n\tgl_FragColor = vec4( packNormalToRGB( normal ), diffuseColor.a );\n\t#ifdef OPAQUE\n\t\tgl_FragColor.a = 1.0;\n\t#endif\n}";

const vertex$6 = "#define PHONG\nvarying vec3 vViewPosition;\n#include <common>\n#include <batching_pars_vertex>\n#include <uv_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <envmap_pars_vertex>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <normal_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <shadowmap_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <color_vertex>\n\t#include <morphcolor_vertex>\n\t#include <batching_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphinstance_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n\t#include <normal_vertex>\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\tvViewPosition = - mvPosition.xyz;\n\t#include <worldpos_vertex>\n\t#include <envmap_vertex>\n\t#include <shadowmap_vertex>\n\t#include <fog_vertex>\n}";

const fragment$6 = "#define PHONG\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform vec3 specular;\nuniform float shininess;\nuniform float opacity;\n#include <common>\n#include <packing>\n#include <dithering_pars_fragment>\n#include <color_pars_fragment>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <alphatest_pars_fragment>\n#include <alphahash_pars_fragment>\n#include <aomap_pars_fragment>\n#include <lightmap_pars_fragment>\n#include <emissivemap_pars_fragment>\n#include <envmap_common_pars_fragment>\n#include <envmap_pars_fragment>\n#include <fog_pars_fragment>\n#include <bsdfs>\n#include <lights_pars_begin>\n#include <normal_pars_fragment>\n#include <lights_phong_pars_fragment>\n#include <shadowmap_pars_fragment>\n#include <bumpmap_pars_fragment>\n#include <normalmap_pars_fragment>\n#include <specularmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <clipping_planes_fragment>\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <color_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <alphahash_fragment>\n\t#include <specularmap_fragment>\n\t#include <normal_fragment_begin>\n\t#include <normal_fragment_maps>\n\t#include <emissivemap_fragment>\n\t#include <lights_phong_fragment>\n\t#include <lights_fragment_begin>\n\t#include <lights_fragment_maps>\n\t#include <lights_fragment_end>\n\t#include <aomap_fragment>\n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;\n\t#include <envmap_fragment>\n\t#include <opaque_fragment>\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n\t#include <dithering_fragment>\n}";

const vertex$5 = "#define STANDARD\nvarying vec3 vViewPosition;\n#ifdef USE_TRANSMISSION\n\tvarying vec3 vWorldPosition;\n#endif\n#include <common>\n#include <batching_pars_vertex>\n#include <uv_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <normal_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <shadowmap_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <color_vertex>\n\t#include <morphinstance_vertex>\n\t#include <morphcolor_vertex>\n\t#include <batching_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n\t#include <normal_vertex>\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\tvViewPosition = - mvPosition.xyz;\n\t#include <worldpos_vertex>\n\t#include <shadowmap_vertex>\n\t#include <fog_vertex>\n#ifdef USE_TRANSMISSION\n\tvWorldPosition = worldPosition.xyz;\n#endif\n}";

const fragment$5 = "#define STANDARD\n#ifdef PHYSICAL\n\t#define IOR\n\t#define USE_SPECULAR\n#endif\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float roughness;\nuniform float metalness;\nuniform float opacity;\n#ifdef IOR\n\tuniform float ior;\n#endif\n#ifdef USE_SPECULAR\n\tuniform float specularIntensity;\n\tuniform vec3 specularColor;\n\t#ifdef USE_SPECULAR_COLORMAP\n\t\tuniform sampler2D specularColorMap;\n\t#endif\n\t#ifdef USE_SPECULAR_INTENSITYMAP\n\t\tuniform sampler2D specularIntensityMap;\n\t#endif\n#endif\n#ifdef USE_CLEARCOAT\n\tuniform float clearcoat;\n\tuniform float clearcoatRoughness;\n#endif\n#ifdef USE_DISPERSION\n\tuniform float dispersion;\n#endif\n#ifdef USE_IRIDESCENCE\n\tuniform float iridescence;\n\tuniform float iridescenceIOR;\n\tuniform float iridescenceThicknessMinimum;\n\tuniform float iridescenceThicknessMaximum;\n#endif\n#ifdef USE_SHEEN\n\tuniform vec3 sheenColor;\n\tuniform float sheenRoughness;\n\t#ifdef USE_SHEEN_COLORMAP\n\t\tuniform sampler2D sheenColorMap;\n\t#endif\n\t#ifdef USE_SHEEN_ROUGHNESSMAP\n\t\tuniform sampler2D sheenRoughnessMap;\n\t#endif\n#endif\n#ifdef USE_ANISOTROPY\n\tuniform vec2 anisotropyVector;\n\t#ifdef USE_ANISOTROPYMAP\n\t\tuniform sampler2D anisotropyMap;\n\t#endif\n#endif\nvarying vec3 vViewPosition;\n#include <common>\n#include <packing>\n#include <dithering_pars_fragment>\n#include <color_pars_fragment>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <alphatest_pars_fragment>\n#include <alphahash_pars_fragment>\n#include <aomap_pars_fragment>\n#include <lightmap_pars_fragment>\n#include <emissivemap_pars_fragment>\n#include <iridescence_fragment>\n#include <cube_uv_reflection_fragment>\n#include <envmap_common_pars_fragment>\n#include <envmap_physical_pars_fragment>\n#include <fog_pars_fragment>\n#include <lights_pars_begin>\n#include <normal_pars_fragment>\n#include <lights_physical_pars_fragment>\n#include <transmission_pars_fragment>\n#include <shadowmap_pars_fragment>\n#include <bumpmap_pars_fragment>\n#include <normalmap_pars_fragment>\n#include <clearcoat_pars_fragment>\n#include <iridescence_pars_fragment>\n#include <roughnessmap_pars_fragment>\n#include <metalnessmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <clipping_planes_fragment>\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <color_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <alphahash_fragment>\n\t#include <roughnessmap_fragment>\n\t#include <metalnessmap_fragment>\n\t#include <normal_fragment_begin>\n\t#include <normal_fragment_maps>\n\t#include <clearcoat_normal_fragment_begin>\n\t#include <clearcoat_normal_fragment_maps>\n\t#include <emissivemap_fragment>\n\t#include <lights_physical_fragment>\n\t#include <lights_fragment_begin>\n\t#include <lights_fragment_maps>\n\t#include <lights_fragment_end>\n\t#include <aomap_fragment>\n\tvec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse;\n\tvec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular;\n\t#include <transmission_fragment>\n\tvec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;\n\t#ifdef USE_SHEEN\n\t\tfloat sheenEnergyComp = 1.0 - 0.157 * max3( material.sheenColor );\n\t\toutgoingLight = outgoingLight * sheenEnergyComp + sheenSpecularDirect + sheenSpecularIndirect;\n\t#endif\n\t#ifdef USE_CLEARCOAT\n\t\tfloat dotNVcc = saturate( dot( geometryClearcoatNormal, geometryViewDir ) );\n\t\tvec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc );\n\t\toutgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + ( clearcoatSpecularDirect + clearcoatSpecularIndirect ) * material.clearcoat;\n\t#endif\n\t#include <opaque_fragment>\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n\t#include <dithering_fragment>\n}";

const vertex$4 = "#define TOON\nvarying vec3 vViewPosition;\n#include <common>\n#include <batching_pars_vertex>\n#include <uv_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <normal_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <shadowmap_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <color_vertex>\n\t#include <morphinstance_vertex>\n\t#include <morphcolor_vertex>\n\t#include <batching_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n\t#include <normal_vertex>\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\tvViewPosition = - mvPosition.xyz;\n\t#include <worldpos_vertex>\n\t#include <shadowmap_vertex>\n\t#include <fog_vertex>\n}";

const fragment$4 = "#define TOON\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\n#include <common>\n#include <packing>\n#include <dithering_pars_fragment>\n#include <color_pars_fragment>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <alphatest_pars_fragment>\n#include <alphahash_pars_fragment>\n#include <aomap_pars_fragment>\n#include <lightmap_pars_fragment>\n#include <emissivemap_pars_fragment>\n#include <gradientmap_pars_fragment>\n#include <fog_pars_fragment>\n#include <bsdfs>\n#include <lights_pars_begin>\n#include <normal_pars_fragment>\n#include <lights_toon_pars_fragment>\n#include <shadowmap_pars_fragment>\n#include <bumpmap_pars_fragment>\n#include <normalmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <clipping_planes_fragment>\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <color_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <alphahash_fragment>\n\t#include <normal_fragment_begin>\n\t#include <normal_fragment_maps>\n\t#include <emissivemap_fragment>\n\t#include <lights_toon_fragment>\n\t#include <lights_fragment_begin>\n\t#include <lights_fragment_maps>\n\t#include <lights_fragment_end>\n\t#include <aomap_fragment>\n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\t#include <opaque_fragment>\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n\t#include <dithering_fragment>\n}";

const vertex$3 = "uniform float size;\nuniform float scale;\n#include <common>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\n#ifdef USE_POINTS_UV\n\tvarying vec2 vUv;\n\tuniform mat3 uvTransform;\n#endif\nvoid main() {\n\t#ifdef USE_POINTS_UV\n\t\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\n\t#endif\n\t#include <color_vertex>\n\t#include <morphinstance_vertex>\n\t#include <morphcolor_vertex>\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <project_vertex>\n\tgl_PointSize = size;\n\t#ifdef USE_SIZEATTENUATION\n\t\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\n\t\tif ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z );\n\t#endif\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\t#include <worldpos_vertex>\n\t#include <fog_vertex>\n}";

const fragment$3 = "uniform vec3 diffuse;\nuniform float opacity;\n#include <common>\n#include <color_pars_fragment>\n#include <map_particle_pars_fragment>\n#include <alphatest_pars_fragment>\n#include <alphahash_pars_fragment>\n#include <fog_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <clipping_planes_fragment>\n\tvec3 outgoingLight = vec3( 0.0 );\n\t#include <logdepthbuf_fragment>\n\t#include <map_particle_fragment>\n\t#include <color_fragment>\n\t#include <alphatest_fragment>\n\t#include <alphahash_fragment>\n\toutgoingLight = diffuseColor.rgb;\n\t#include <opaque_fragment>\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n}";

const vertex$2 = "#include <common>\n#include <batching_pars_vertex>\n#include <fog_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <shadowmap_pars_vertex>\nvoid main() {\n\t#include <batching_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphinstance_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <worldpos_vertex>\n\t#include <shadowmap_vertex>\n\t#include <fog_vertex>\n}";

const fragment$2 = "uniform vec3 color;\nuniform float opacity;\n#include <common>\n#include <packing>\n#include <fog_pars_fragment>\n#include <bsdfs>\n#include <lights_pars_begin>\n#include <logdepthbuf_pars_fragment>\n#include <shadowmap_pars_fragment>\n#include <shadowmask_pars_fragment>\nvoid main() {\n\t#include <logdepthbuf_fragment>\n\tgl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) );\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n\t#include <fog_fragment>\n}";

const vertex$1 = "uniform float rotation;\nuniform vec2 center;\n#include <common>\n#include <uv_pars_vertex>\n#include <fog_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\tvec4 mvPosition = modelViewMatrix[ 3 ];\n\tvec2 scale = vec2( length( modelMatrix[ 0 ].xyz ), length( modelMatrix[ 1 ].xyz ) );\n\t#ifndef USE_SIZEATTENUATION\n\t\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\n\t\tif ( isPerspective ) scale *= - mvPosition.z;\n\t#endif\n\tvec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale;\n\tvec2 rotatedPosition;\n\trotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;\n\trotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;\n\tmvPosition.xy += rotatedPosition;\n\tgl_Position = projectionMatrix * mvPosition;\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\t#include <fog_vertex>\n}";

const fragment$1 = "uniform vec3 diffuse;\nuniform float opacity;\n#include <common>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <alphatest_pars_fragment>\n#include <alphahash_pars_fragment>\n#include <fog_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <clipping_planes_fragment>\n\tvec3 outgoingLight = vec3( 0.0 );\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <alphahash_fragment>\n\toutgoingLight = diffuseColor.rgb;\n\t#include <opaque_fragment>\n\t#include <tonemapping_fragment>\n\t#include <colorspace_fragment>\n\t#include <fog_fragment>\n}";

const ShaderChunk = {
	alphahash_fragment: alphahash_fragment,
	alphahash_pars_fragment: alphahash_pars_fragment,
	alphamap_fragment: alphamap_fragment,
	alphamap_pars_fragment: alphamap_pars_fragment,
	alphatest_fragment: alphatest_fragment,
	alphatest_pars_fragment: alphatest_pars_fragment,
	aomap_fragment: aomap_fragment,
	aomap_pars_fragment: aomap_pars_fragment,
	batching_pars_vertex: batching_pars_vertex,
	batching_vertex: batching_vertex,
	begin_vertex: begin_vertex,
	beginnormal_vertex: beginnormal_vertex,
	bsdfs: bsdfs,
	iridescence_fragment: iridescence_fragment,
	bumpmap_pars_fragment: bumpmap_pars_fragment,
	clipping_planes_fragment: clipping_planes_fragment,
	clipping_planes_pars_fragment: clipping_planes_pars_fragment,
	clipping_planes_pars_vertex: clipping_planes_pars_vertex,
	clipping_planes_vertex: clipping_planes_vertex,
	color_fragment: color_fragment,
	color_pars_fragment: color_pars_fragment,
	color_pars_vertex: color_pars_vertex,
	color_vertex: color_vertex,
	common: common$1,
	cube_uv_reflection_fragment: cube_uv_reflection_fragment,
	defaultnormal_vertex: defaultnormal_vertex,
	displacementmap_pars_vertex: displacementmap_pars_vertex,
	displacementmap_vertex: displacementmap_vertex,
	emissivemap_fragment: emissivemap_fragment,
	emissivemap_pars_fragment: emissivemap_pars_fragment,
	colorspace_fragment: colorspace_fragment,
	colorspace_pars_fragment: colorspace_pars_fragment,
	envmap_fragment: envmap_fragment,
	envmap_common_pars_fragment: envmap_common_pars_fragment,
	envmap_pars_fragment: envmap_pars_fragment,
	envmap_pars_vertex: envmap_pars_vertex,
	envmap_physical_pars_fragment: envmap_physical_pars_fragment,
	envmap_vertex: envmap_vertex,
	fog_vertex: fog_vertex,
	fog_pars_vertex: fog_pars_vertex,
	fog_fragment: fog_fragment,
	fog_pars_fragment: fog_pars_fragment,
	gradientmap_pars_fragment: gradientmap_pars_fragment,
	lightmap_pars_fragment: lightmap_pars_fragment,
	lights_lambert_fragment: lights_lambert_fragment,
	lights_lambert_pars_fragment: lights_lambert_pars_fragment,
	lights_pars_begin: lights_pars_begin,
	lights_toon_fragment: lights_toon_fragment,
	lights_toon_pars_fragment: lights_toon_pars_fragment,
	lights_phong_fragment: lights_phong_fragment,
	lights_phong_pars_fragment: lights_phong_pars_fragment,
	lights_physical_fragment: lights_physical_fragment,
	lights_physical_pars_fragment: lights_physical_pars_fragment,
	lights_fragment_begin: lights_fragment_begin,
	lights_fragment_maps: lights_fragment_maps,
	lights_fragment_end: lights_fragment_end,
	logdepthbuf_fragment: logdepthbuf_fragment,
	logdepthbuf_pars_fragment: logdepthbuf_pars_fragment,
	logdepthbuf_pars_vertex: logdepthbuf_pars_vertex,
	logdepthbuf_vertex: logdepthbuf_vertex,
	map_fragment: map_fragment,
	map_pars_fragment: map_pars_fragment,
	map_particle_fragment: map_particle_fragment,
	map_particle_pars_fragment: map_particle_pars_fragment,
	metalnessmap_fragment: metalnessmap_fragment,
	metalnessmap_pars_fragment: metalnessmap_pars_fragment,
	morphinstance_vertex: morphinstance_vertex,
	morphcolor_vertex: morphcolor_vertex,
	morphnormal_vertex: morphnormal_vertex,
	morphtarget_pars_vertex: morphtarget_pars_vertex,
	morphtarget_vertex: morphtarget_vertex,
	normal_fragment_begin: normal_fragment_begin,
	normal_fragment_maps: normal_fragment_maps,
	normal_pars_fragment: normal_pars_fragment,
	normal_pars_vertex: normal_pars_vertex,
	normal_vertex: normal_vertex,
	normalmap_pars_fragment: normalmap_pars_fragment,
	clearcoat_normal_fragment_begin: clearcoat_normal_fragment_begin,
	clearcoat_normal_fragment_maps: clearcoat_normal_fragment_maps,
	clearcoat_pars_fragment: clearcoat_pars_fragment,
	iridescence_pars_fragment: iridescence_pars_fragment,
	opaque_fragment: opaque_fragment,
	packing: packing,
	premultiplied_alpha_fragment: premultiplied_alpha_fragment,
	project_vertex: project_vertex,
	dithering_fragment: dithering_fragment,
	dithering_pars_fragment: dithering_pars_fragment,
	roughnessmap_fragment: roughnessmap_fragment,
	roughnessmap_pars_fragment: roughnessmap_pars_fragment,
	shadowmap_pars_fragment: shadowmap_pars_fragment,
	shadowmap_pars_vertex: shadowmap_pars_vertex,
	shadowmap_vertex: shadowmap_vertex,
	shadowmask_pars_fragment: shadowmask_pars_fragment,
	skinbase_vertex: skinbase_vertex,
	skinning_pars_vertex: skinning_pars_vertex,
	skinning_vertex: skinning_vertex,
	skinnormal_vertex: skinnormal_vertex,
	specularmap_fragment: specularmap_fragment,
	specularmap_pars_fragment: specularmap_pars_fragment,
	tonemapping_fragment: tonemapping_fragment,
	tonemapping_pars_fragment: tonemapping_pars_fragment,
	transmission_fragment: transmission_fragment,
	transmission_pars_fragment: transmission_pars_fragment,
	uv_pars_fragment: uv_pars_fragment,
	uv_pars_vertex: uv_pars_vertex,
	uv_vertex: uv_vertex,
	worldpos_vertex: worldpos_vertex,

	background_vert: vertex$h,
	background_frag: fragment$h,
	backgroundCube_vert: vertex$g,
	backgroundCube_frag: fragment$g,
	cube_vert: vertex$f,
	cube_frag: fragment$f,
	depth_vert: vertex$e,
	depth_frag: fragment$e,
	distanceRGBA_vert: vertex$d,
	distanceRGBA_frag: fragment$d,
	equirect_vert: vertex$c,
	equirect_frag: fragment$c,
	linedashed_vert: vertex$b,
	linedashed_frag: fragment$b,
	meshbasic_vert: vertex$a,
	meshbasic_frag: fragment$a,
	meshlambert_vert: vertex$9,
	meshlambert_frag: fragment$9,
	meshmatcap_vert: vertex$8,
	meshmatcap_frag: fragment$8,
	meshnormal_vert: vertex$7,
	meshnormal_frag: fragment$7,
	meshphong_vert: vertex$6,
	meshphong_frag: fragment$6,
	meshphysical_vert: vertex$5,
	meshphysical_frag: fragment$5,
	meshtoon_vert: vertex$4,
	meshtoon_frag: fragment$4,
	points_vert: vertex$3,
	points_frag: fragment$3,
	shadow_vert: vertex$2,
	shadow_frag: fragment$2,
	sprite_vert: vertex$1,
	sprite_frag: fragment$1
};

// Uniforms library for shared webgl shaders
const UniformsLib = {

	common: {

		diffuse: { value: /*@__PURE__*/ new Color( 0xffffff ) },
		opacity: { value: 1.0 },

		map: { value: null },
		mapTransform: { value: /*@__PURE__*/ new Matrix3() },

		alphaMap: { value: null },
		alphaMapTransform: { value: /*@__PURE__*/ new Matrix3() },

		alphaTest: { value: 0 }

	},

	specularmap: {

		specularMap: { value: null },
		specularMapTransform: { value: /*@__PURE__*/ new Matrix3() }

	},

	envmap: {

		envMap: { value: null },
		envMapRotation: { value: /*@__PURE__*/ new Matrix3() },
		flipEnvMap: { value: -1 },
		reflectivity: { value: 1.0 }, // basic, lambert, phong
		ior: { value: 1.5 }, // physical
		refractionRatio: { value: 0.98 }, // basic, lambert, phong

	},

	aomap: {

		aoMap: { value: null },
		aoMapIntensity: { value: 1 },
		aoMapTransform: { value: /*@__PURE__*/ new Matrix3() }

	},

	lightmap: {

		lightMap: { value: null },
		lightMapIntensity: { value: 1 },
		lightMapTransform: { value: /*@__PURE__*/ new Matrix3() }

	},

	bumpmap: {

		bumpMap: { value: null },
		bumpMapTransform: { value: /*@__PURE__*/ new Matrix3() },
		bumpScale: { value: 1 }

	},

	normalmap: {

		normalMap: { value: null },
		normalMapTransform: { value: /*@__PURE__*/ new Matrix3() },
		normalScale: { value: /*@__PURE__*/ new Vector2( 1, 1 ) }

	},

	displacementmap: {

		displacementMap: { value: null },
		displacementMapTransform: { value: /*@__PURE__*/ new Matrix3() },
		displacementScale: { value: 1 },
		displacementBias: { value: 0 }

	},

	emissivemap: {

		emissiveMap: { value: null },
		emissiveMapTransform: { value: /*@__PURE__*/ new Matrix3() }

	},

	metalnessmap: {

		metalnessMap: { value: null },
		metalnessMapTransform: { value: /*@__PURE__*/ new Matrix3() }

	},

	roughnessmap: {

		roughnessMap: { value: null },
		roughnessMapTransform: { value: /*@__PURE__*/ new Matrix3() }

	},

	gradientmap: {

		gradientMap: { value: null }

	},

	fog: {

		fogDensity: { value: 0.00025 },
		fogNear: { value: 1 },
		fogFar: { value: 2000 },
		fogColor: { value: /*@__PURE__*/ new Color( 0xffffff ) }

	},

	lights: {

		ambientLightColor: { value: [] },

		lightProbe: { value: [] },

		directionalLights: { value: [], properties: {
			direction: {},
			color: {}
		} },

		directionalLightShadows: { value: [], properties: {
			shadowIntensity: 1,
			shadowBias: {},
			shadowNormalBias: {},
			shadowRadius: {},
			shadowMapSize: {}
		} },

		directionalShadowMap: { value: [] },
		directionalShadowMatrix: { value: [] },

		spotLights: { value: [], properties: {
			color: {},
			position: {},
			direction: {},
			distance: {},
			coneCos: {},
			penumbraCos: {},
			decay: {}
		} },

		spotLightShadows: { value: [], properties: {
			shadowIntensity: 1,
			shadowBias: {},
			shadowNormalBias: {},
			shadowRadius: {},
			shadowMapSize: {}
		} },

		spotLightMap: { value: [] },
		spotShadowMap: { value: [] },
		spotLightMatrix: { value: [] },

		pointLights: { value: [], properties: {
			color: {},
			position: {},
			decay: {},
			distance: {}
		} },

		pointLightShadows: { value: [], properties: {
			shadowIntensity: 1,
			shadowBias: {},
			shadowNormalBias: {},
			shadowRadius: {},
			shadowMapSize: {},
			shadowCameraNear: {},
			shadowCameraFar: {}
		} },

		pointShadowMap: { value: [] },
		pointShadowMatrix: { value: [] },

		hemisphereLights: { value: [], properties: {
			direction: {},
			skyColor: {},
			groundColor: {}
		} },

		// TODO (abelnation): RectAreaLight BRDF data needs to be moved from example to main src
		rectAreaLights: { value: [], properties: {
			color: {},
			position: {},
			width: {},
			height: {}
		} },

		ltc_1: { value: null },
		ltc_2: { value: null }

	},

	points: {

		diffuse: { value: /*@__PURE__*/ new Color( 0xffffff ) },
		opacity: { value: 1.0 },
		size: { value: 1.0 },
		scale: { value: 1.0 },
		map: { value: null },
		alphaMap: { value: null },
		alphaMapTransform: { value: /*@__PURE__*/ new Matrix3() },
		alphaTest: { value: 0 },
		uvTransform: { value: /*@__PURE__*/ new Matrix3() }

	},

	sprite: {

		diffuse: { value: /*@__PURE__*/ new Color( 0xffffff ) },
		opacity: { value: 1.0 },
		center: { value: /*@__PURE__*/ new Vector2( 0.5, 0.5 ) },
		rotation: { value: 0.0 },
		map: { value: null },
		mapTransform: { value: /*@__PURE__*/ new Matrix3() },
		alphaMap: { value: null },
		alphaMapTransform: { value: /*@__PURE__*/ new Matrix3() },
		alphaTest: { value: 0 }

	}

};

const ShaderLib = {

	basic: {

		uniforms: /*@__PURE__*/ mergeUniforms( [
			UniformsLib.common,
			UniformsLib.specularmap,
			UniformsLib.envmap,
			UniformsLib.aomap,
			UniformsLib.lightmap,
			UniformsLib.fog
		] ),

		vertexShader: ShaderChunk.meshbasic_vert,
		fragmentShader: ShaderChunk.meshbasic_frag

	},

	lambert: {

		uniforms: /*@__PURE__*/ mergeUniforms( [
			UniformsLib.common,
			UniformsLib.specularmap,
			UniformsLib.envmap,
			UniformsLib.aomap,
			UniformsLib.lightmap,
			UniformsLib.emissivemap,
			UniformsLib.bumpmap,
			UniformsLib.normalmap,
			UniformsLib.displacementmap,
			UniformsLib.fog,
			UniformsLib.lights,
			{
				emissive: { value: /*@__PURE__*/ new Color( 0x000000 ) }
			}
		] ),

		vertexShader: ShaderChunk.meshlambert_vert,
		fragmentShader: ShaderChunk.meshlambert_frag

	},

	phong: {

		uniforms: /*@__PURE__*/ mergeUniforms( [
			UniformsLib.common,
			UniformsLib.specularmap,
			UniformsLib.envmap,
			UniformsLib.aomap,
			UniformsLib.lightmap,
			UniformsLib.emissivemap,
			UniformsLib.bumpmap,
			UniformsLib.normalmap,
			UniformsLib.displacementmap,
			UniformsLib.fog,
			UniformsLib.lights,
			{
				emissive: { value: /*@__PURE__*/ new Color( 0x000000 ) },
				specular: { value: /*@__PURE__*/ new Color( 0x111111 ) },
				shininess: { value: 30 }
			}
		] ),

		vertexShader: ShaderChunk.meshphong_vert,
		fragmentShader: ShaderChunk.meshphong_frag

	},

	standard: {

		uniforms: /*@__PURE__*/ mergeUniforms( [
			UniformsLib.common,
			UniformsLib.envmap,
			UniformsLib.aomap,
			UniformsLib.lightmap,
			UniformsLib.emissivemap,
			UniformsLib.bumpmap,
			UniformsLib.normalmap,
			UniformsLib.displacementmap,
			UniformsLib.roughnessmap,
			UniformsLib.metalnessmap,
			UniformsLib.fog,
			UniformsLib.lights,
			{
				emissive: { value: /*@__PURE__*/ new Color( 0x000000 ) },
				roughness: { value: 1.0 },
				metalness: { value: 0.0 },
				envMapIntensity: { value: 1 }
			}
		] ),

		vertexShader: ShaderChunk.meshphysical_vert,
		fragmentShader: ShaderChunk.meshphysical_frag

	},

	toon: {

		uniforms: /*@__PURE__*/ mergeUniforms( [
			UniformsLib.common,
			UniformsLib.aomap,
			UniformsLib.lightmap,
			UniformsLib.emissivemap,
			UniformsLib.bumpmap,
			UniformsLib.normalmap,
			UniformsLib.displacementmap,
			UniformsLib.gradientmap,
			UniformsLib.fog,
			UniformsLib.lights,
			{
				emissive: { value: /*@__PURE__*/ new Color( 0x000000 ) }
			}
		] ),

		vertexShader: ShaderChunk.meshtoon_vert,
		fragmentShader: ShaderChunk.meshtoon_frag

	},

	matcap: {

		uniforms: /*@__PURE__*/ mergeUniforms( [
			UniformsLib.common,
			UniformsLib.bumpmap,
			UniformsLib.normalmap,
			UniformsLib.displacementmap,
			UniformsLib.fog,
			{
				matcap: { value: null }
			}
		] ),

		vertexShader: ShaderChunk.meshmatcap_vert,
		fragmentShader: ShaderChunk.meshmatcap_frag

	},

	points: {

		uniforms: /*@__PURE__*/ mergeUniforms( [
			UniformsLib.points,
			UniformsLib.fog
		] ),

		vertexShader: ShaderChunk.points_vert,
		fragmentShader: ShaderChunk.points_frag

	},

	dashed: {

		uniforms: /*@__PURE__*/ mergeUniforms( [
			UniformsLib.common,
			UniformsLib.fog,
			{
				scale: { value: 1 },
				dashSize: { value: 1 },
				totalSize: { value: 2 }
			}
		] ),

		vertexShader: ShaderChunk.linedashed_vert,
		fragmentShader: ShaderChunk.linedashed_frag

	},

	depth: {

		uniforms: /*@__PURE__*/ mergeUniforms( [
			UniformsLib.common,
			UniformsLib.displacementmap
		] ),

		vertexShader: ShaderChunk.depth_vert,
		fragmentShader: ShaderChunk.depth_frag

	},

	normal: {

		uniforms: /*@__PURE__*/ mergeUniforms( [
			UniformsLib.common,
			UniformsLib.bumpmap,
			UniformsLib.normalmap,
			UniformsLib.displacementmap,
			{
				opacity: { value: 1.0 }
			}
		] ),

		vertexShader: ShaderChunk.meshnormal_vert,
		fragmentShader: ShaderChunk.meshnormal_frag

	},

	sprite: {

		uniforms: /*@__PURE__*/ mergeUniforms( [
			UniformsLib.sprite,
			UniformsLib.fog
		] ),

		vertexShader: ShaderChunk.sprite_vert,
		fragmentShader: ShaderChunk.sprite_frag

	},

	background: {

		uniforms: {
			uvTransform: { value: /*@__PURE__*/ new Matrix3() },
			t2D: { value: null },
			backgroundIntensity: { value: 1 }
		},

		vertexShader: ShaderChunk.background_vert,
		fragmentShader: ShaderChunk.background_frag

	},

	backgroundCube: {

		uniforms: {
			envMap: { value: null },
			flipEnvMap: { value: -1 },
			backgroundBlurriness: { value: 0 },
			backgroundIntensity: { value: 1 },
			backgroundRotation: { value: /*@__PURE__*/ new Matrix3() }
		},

		vertexShader: ShaderChunk.backgroundCube_vert,
		fragmentShader: ShaderChunk.backgroundCube_frag

	},

	cube: {

		uniforms: {
			tCube: { value: null },
			tFlip: { value: -1 },
			opacity: { value: 1.0 }
		},

		vertexShader: ShaderChunk.cube_vert,
		fragmentShader: ShaderChunk.cube_frag

	},

	equirect: {

		uniforms: {
			tEquirect: { value: null },
		},

		vertexShader: ShaderChunk.equirect_vert,
		fragmentShader: ShaderChunk.equirect_frag

	},

	distanceRGBA: {

		uniforms: /*@__PURE__*/ mergeUniforms( [
			UniformsLib.common,
			UniformsLib.displacementmap,
			{
				referencePosition: { value: /*@__PURE__*/ new Vector3() },
				nearDistance: { value: 1 },
				farDistance: { value: 1000 }
			}
		] ),

		vertexShader: ShaderChunk.distanceRGBA_vert,
		fragmentShader: ShaderChunk.distanceRGBA_frag

	},

	shadow: {

		uniforms: /*@__PURE__*/ mergeUniforms( [
			UniformsLib.lights,
			UniformsLib.fog,
			{
				color: { value: /*@__PURE__*/ new Color( 0x00000 ) },
				opacity: { value: 1.0 }
			},
		] ),

		vertexShader: ShaderChunk.shadow_vert,
		fragmentShader: ShaderChunk.shadow_frag

	}

};

ShaderLib.physical = {

	uniforms: /*@__PURE__*/ mergeUniforms( [
		ShaderLib.standard.uniforms,
		{
			clearcoat: { value: 0 },
			clearcoatMap: { value: null },
			clearcoatMapTransform: { value: /*@__PURE__*/ new Matrix3() },
			clearcoatNormalMap: { value: null },
			clearcoatNormalMapTransform: { value: /*@__PURE__*/ new Matrix3() },
			clearcoatNormalScale: { value: /*@__PURE__*/ new Vector2( 1, 1 ) },
			clearcoatRoughness: { value: 0 },
			clearcoatRoughnessMap: { value: null },
			clearcoatRoughnessMapTransform: { value: /*@__PURE__*/ new Matrix3() },
			dispersion: { value: 0 },
			iridescence: { value: 0 },
			iridescenceMap: { value: null },
			iridescenceMapTransform: { value: /*@__PURE__*/ new Matrix3() },
			iridescenceIOR: { value: 1.3 },
			iridescenceThicknessMinimum: { value: 100 },
			iridescenceThicknessMaximum: { value: 400 },
			iridescenceThicknessMap: { value: null },
			iridescenceThicknessMapTransform: { value: /*@__PURE__*/ new Matrix3() },
			sheen: { value: 0 },
			sheenColor: { value: /*@__PURE__*/ new Color( 0x000000 ) },
			sheenColorMap: { value: null },
			sheenColorMapTransform: { value: /*@__PURE__*/ new Matrix3() },
			sheenRoughness: { value: 1 },
			sheenRoughnessMap: { value: null },
			sheenRoughnessMapTransform: { value: /*@__PURE__*/ new Matrix3() },
			transmission: { value: 0 },
			transmissionMap: { value: null },
			transmissionMapTransform: { value: /*@__PURE__*/ new Matrix3() },
			transmissionSamplerSize: { value: /*@__PURE__*/ new Vector2() },
			transmissionSamplerMap: { value: null },
			thickness: { value: 0 },
			thicknessMap: { value: null },
			thicknessMapTransform: { value: /*@__PURE__*/ new Matrix3() },
			attenuationDistance: { value: 0 },
			attenuationColor: { value: /*@__PURE__*/ new Color( 0x000000 ) },
			specularColor: { value: /*@__PURE__*/ new Color( 1, 1, 1 ) },
			specularColorMap: { value: null },
			specularColorMapTransform: { value: /*@__PURE__*/ new Matrix3() },
			specularIntensity: { value: 1 },
			specularIntensityMap: { value: null },
			specularIntensityMapTransform: { value: /*@__PURE__*/ new Matrix3() },
			anisotropyVector: { value: /*@__PURE__*/ new Vector2() },
			anisotropyMap: { value: null },
			anisotropyMapTransform: { value: /*@__PURE__*/ new Matrix3() },
		}
	] ),

	vertexShader: ShaderChunk.meshphysical_vert,
	fragmentShader: ShaderChunk.meshphysical_frag

};

const _rgb = { r: 0, b: 0, g: 0 };
const _e1$1 = /*@__PURE__*/ new Euler();
const _m1$1 = /*@__PURE__*/ new Matrix4();

function WebGLBackground( renderer, cubemaps, cubeuvmaps, state, objects, alpha, premultipliedAlpha ) {

	const clearColor = new Color( 0x000000 );
	let clearAlpha = alpha === true ? 0 : 1;

	let planeMesh;
	let boxMesh;

	let currentBackground = null;
	let currentBackgroundVersion = 0;
	let currentTonemapping = null;

	function getBackground( scene ) {

		let background = scene.isScene === true ? scene.background : null;

		if ( background && background.isTexture ) {

			const usePMREM = scene.backgroundBlurriness > 0; // use PMREM if the user wants to blur the background
			background = ( usePMREM ? cubeuvmaps : cubemaps ).get( background );

		}

		return background;

	}

	function render( scene ) {

		let forceClear = false;
		const background = getBackground( scene );

		if ( background === null ) {

			setClear( clearColor, clearAlpha );

		} else if ( background && background.isColor ) {

			setClear( background, 1 );
			forceClear = true;

		}

		const environmentBlendMode = renderer.xr.getEnvironmentBlendMode();

		if ( environmentBlendMode === 'additive' ) {

			state.buffers.color.setClear( 0, 0, 0, 1, premultipliedAlpha );

		} else if ( environmentBlendMode === 'alpha-blend' ) {

			state.buffers.color.setClear( 0, 0, 0, 0, premultipliedAlpha );

		}

		if ( renderer.autoClear || forceClear ) {

			// buffers might not be writable which is required to ensure a correct clear

			state.buffers.depth.setTest( true );
			state.buffers.depth.setMask( true );
			state.buffers.color.setMask( true );

			renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );

		}

	}

	function addToRenderList( renderList, scene ) {

		const background = getBackground( scene );

		if ( background && ( background.isCubeTexture || background.mapping === CubeUVReflectionMapping ) ) {

			if ( boxMesh === undefined ) {

				boxMesh = new Mesh(
					new BoxGeometry( 1, 1, 1 ),
					new ShaderMaterial( {
						name: 'BackgroundCubeMaterial',
						uniforms: cloneUniforms( ShaderLib.backgroundCube.uniforms ),
						vertexShader: ShaderLib.backgroundCube.vertexShader,
						fragmentShader: ShaderLib.backgroundCube.fragmentShader,
						side: BackSide,
						depthTest: false,
						depthWrite: false,
						fog: false,
						allowOverride: false
					} )
				);

				boxMesh.geometry.deleteAttribute( 'normal' );
				boxMesh.geometry.deleteAttribute( 'uv' );

				boxMesh.onBeforeRender = function ( renderer, scene, camera ) {

					this.matrixWorld.copyPosition( camera.matrixWorld );

				};

				// add "envMap" material property so the renderer can evaluate it like for built-in materials
				Object.defineProperty( boxMesh.material, 'envMap', {

					get: function () {

						return this.uniforms.envMap.value;

					}

				} );

				objects.update( boxMesh );

			}

			_e1$1.copy( scene.backgroundRotation );

			// accommodate left-handed frame
			_e1$1.x *= -1; _e1$1.y *= -1; _e1$1.z *= -1;

			if ( background.isCubeTexture && background.isRenderTargetTexture === false ) {

				// environment maps which are not cube render targets or PMREMs follow a different convention
				_e1$1.y *= -1;
				_e1$1.z *= -1;

			}

			boxMesh.material.uniforms.envMap.value = background;
			boxMesh.material.uniforms.flipEnvMap.value = ( background.isCubeTexture && background.isRenderTargetTexture === false ) ? -1 : 1;
			boxMesh.material.uniforms.backgroundBlurriness.value = scene.backgroundBlurriness;
			boxMesh.material.uniforms.backgroundIntensity.value = scene.backgroundIntensity;
			boxMesh.material.uniforms.backgroundRotation.value.setFromMatrix4( _m1$1.makeRotationFromEuler( _e1$1 ) );
			boxMesh.material.toneMapped = ColorManagement.getTransfer( background.colorSpace ) !== SRGBTransfer;

			if ( currentBackground !== background ||
				currentBackgroundVersion !== background.version ||
				currentTonemapping !== renderer.toneMapping ) {

				boxMesh.material.needsUpdate = true;

				currentBackground = background;
				currentBackgroundVersion = background.version;
				currentTonemapping = renderer.toneMapping;

			}

			boxMesh.layers.enableAll();

			// push to the pre-sorted opaque render list
			renderList.unshift( boxMesh, boxMesh.geometry, boxMesh.material, 0, 0, null );

		} else if ( background && background.isTexture ) {

			if ( planeMesh === undefined ) {

				planeMesh = new Mesh(
					new PlaneGeometry( 2, 2 ),
					new ShaderMaterial( {
						name: 'BackgroundMaterial',
						uniforms: cloneUniforms( ShaderLib.background.uniforms ),
						vertexShader: ShaderLib.background.vertexShader,
						fragmentShader: ShaderLib.background.fragmentShader,
						side: FrontSide,
						depthTest: false,
						depthWrite: false,
						fog: false,
						allowOverride: false
					} )
				);

				planeMesh.geometry.deleteAttribute( 'normal' );

				// add "map" material property so the renderer can evaluate it like for built-in materials
				Object.defineProperty( planeMesh.material, 'map', {

					get: function () {

						return this.uniforms.t2D.value;

					}

				} );

				objects.update( planeMesh );

			}

			planeMesh.material.uniforms.t2D.value = background;
			planeMesh.material.uniforms.backgroundIntensity.value = scene.backgroundIntensity;
			planeMesh.material.toneMapped = ColorManagement.getTransfer( background.colorSpace ) !== SRGBTransfer;

			if ( background.matrixAutoUpdate === true ) {

				background.updateMatrix();

			}

			planeMesh.material.uniforms.uvTransform.value.copy( background.matrix );

			if ( currentBackground !== background ||
				currentBackgroundVersion !== background.version ||
				currentTonemapping !== renderer.toneMapping ) {

				planeMesh.material.needsUpdate = true;

				currentBackground = background;
				currentBackgroundVersion = background.version;
				currentTonemapping = renderer.toneMapping;

			}

			planeMesh.layers.enableAll();

			// push to the pre-sorted opaque render list
			renderList.unshift( planeMesh, planeMesh.geometry, planeMesh.material, 0, 0, null );

		}

	}

	function setClear( color, alpha ) {

		color.getRGB( _rgb, getUnlitUniformColorSpace( renderer ) );

		state.buffers.color.setClear( _rgb.r, _rgb.g, _rgb.b, alpha, premultipliedAlpha );

	}

	function dispose() {

		if ( boxMesh !== undefined ) {

			boxMesh.geometry.dispose();
			boxMesh.material.dispose();

			boxMesh = undefined;

		}

		if ( planeMesh !== undefined ) {

			planeMesh.geometry.dispose();
			planeMesh.material.dispose();

			planeMesh = undefined;

		}

	}

	return {

		getClearColor: function () {

			return clearColor;

		},
		setClearColor: function ( color, alpha = 1 ) {

			clearColor.set( color );
			clearAlpha = alpha;
			setClear( clearColor, clearAlpha );

		},
		getClearAlpha: function () {

			return clearAlpha;

		},
		setClearAlpha: function ( alpha ) {

			clearAlpha = alpha;
			setClear( clearColor, clearAlpha );

		},
		render: render,
		addToRenderList: addToRenderList,
		dispose: dispose

	};

}

function WebGLBindingStates( gl, attributes ) {

	const maxVertexAttributes = gl.getParameter( gl.MAX_VERTEX_ATTRIBS );

	const bindingStates = {};

	const defaultState = createBindingState( null );
	let currentState = defaultState;
	let forceUpdate = false;

	function setup( object, material, program, geometry, index ) {

		let updateBuffers = false;

		const state = getBindingState( geometry, program, material );

		if ( currentState !== state ) {

			currentState = state;
			bindVertexArrayObject( currentState.object );

		}

		updateBuffers = needsUpdate( object, geometry, program, index );

		if ( updateBuffers ) saveCache( object, geometry, program, index );

		if ( index !== null ) {

			attributes.update( index, gl.ELEMENT_ARRAY_BUFFER );

		}

		if ( updateBuffers || forceUpdate ) {

			forceUpdate = false;

			setupVertexAttributes( object, material, program, geometry );

			if ( index !== null ) {

				gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, attributes.get( index ).buffer );

			}

		}

	}

	function createVertexArrayObject() {

		return gl.createVertexArray();

	}

	function bindVertexArrayObject( vao ) {

		return gl.bindVertexArray( vao );

	}

	function deleteVertexArrayObject( vao ) {

		return gl.deleteVertexArray( vao );

	}

	function getBindingState( geometry, program, material ) {

		const wireframe = ( material.wireframe === true );

		let programMap = bindingStates[ geometry.id ];

		if ( programMap === undefined ) {

			programMap = {};
			bindingStates[ geometry.id ] = programMap;

		}

		let stateMap = programMap[ program.id ];

		if ( stateMap === undefined ) {

			stateMap = {};
			programMap[ program.id ] = stateMap;

		}

		let state = stateMap[ wireframe ];

		if ( state === undefined ) {

			state = createBindingState( createVertexArrayObject() );
			stateMap[ wireframe ] = state;

		}

		return state;

	}

	function createBindingState( vao ) {

		const newAttributes = [];
		const enabledAttributes = [];
		const attributeDivisors = [];

		for ( let i = 0; i < maxVertexAttributes; i ++ ) {

			newAttributes[ i ] = 0;
			enabledAttributes[ i ] = 0;
			attributeDivisors[ i ] = 0;

		}

		return {

			// for backward compatibility on non-VAO support browser
			geometry: null,
			program: null,
			wireframe: false,

			newAttributes: newAttributes,
			enabledAttributes: enabledAttributes,
			attributeDivisors: attributeDivisors,
			object: vao,
			attributes: {},
			index: null

		};

	}

	function needsUpdate( object, geometry, program, index ) {

		const cachedAttributes = currentState.attributes;
		const geometryAttributes = geometry.attributes;

		let attributesNum = 0;

		const programAttributes = program.getAttributes();

		for ( const name in programAttributes ) {

			const programAttribute = programAttributes[ name ];

			if ( programAttribute.location >= 0 ) {

				const cachedAttribute = cachedAttributes[ name ];
				let geometryAttribute = geometryAttributes[ name ];

				if ( geometryAttribute === undefined ) {

					if ( name === 'instanceMatrix' && object.instanceMatrix ) geometryAttribute = object.instanceMatrix;
					if ( name === 'instanceColor' && object.instanceColor ) geometryAttribute = object.instanceColor;

				}

				if ( cachedAttribute === undefined ) return true;

				if ( cachedAttribute.attribute !== geometryAttribute ) return true;

				if ( geometryAttribute && cachedAttribute.data !== geometryAttribute.data ) return true;

				attributesNum ++;

			}

		}

		if ( currentState.attributesNum !== attributesNum ) return true;

		if ( currentState.index !== index ) return true;

		return false;

	}

	function saveCache( object, geometry, program, index ) {

		const cache = {};
		const attributes = geometry.attributes;
		let attributesNum = 0;

		const programAttributes = program.getAttributes();

		for ( const name in programAttributes ) {

			const programAttribute = programAttributes[ name ];

			if ( programAttribute.location >= 0 ) {

				let attribute = attributes[ name ];

				if ( attribute === undefined ) {

					if ( name === 'instanceMatrix' && object.instanceMatrix ) attribute = object.instanceMatrix;
					if ( name === 'instanceColor' && object.instanceColor ) attribute = object.instanceColor;

				}

				const data = {};
				data.attribute = attribute;

				if ( attribute && attribute.data ) {

					data.data = attribute.data;

				}

				cache[ name ] = data;

				attributesNum ++;

			}

		}

		currentState.attributes = cache;
		currentState.attributesNum = attributesNum;

		currentState.index = index;

	}

	function initAttributes() {

		const newAttributes = currentState.newAttributes;

		for ( let i = 0, il = newAttributes.length; i < il; i ++ ) {

			newAttributes[ i ] = 0;

		}

	}

	function enableAttribute( attribute ) {

		enableAttributeAndDivisor( attribute, 0 );

	}

	function enableAttributeAndDivisor( attribute, meshPerAttribute ) {

		const newAttributes = currentState.newAttributes;
		const enabledAttributes = currentState.enabledAttributes;
		const attributeDivisors = currentState.attributeDivisors;

		newAttributes[ attribute ] = 1;

		if ( enabledAttributes[ attribute ] === 0 ) {

			gl.enableVertexAttribArray( attribute );
			enabledAttributes[ attribute ] = 1;

		}

		if ( attributeDivisors[ attribute ] !== meshPerAttribute ) {

			gl.vertexAttribDivisor( attribute, meshPerAttribute );
			attributeDivisors[ attribute ] = meshPerAttribute;

		}

	}

	function disableUnusedAttributes() {

		const newAttributes = currentState.newAttributes;
		const enabledAttributes = currentState.enabledAttributes;

		for ( let i = 0, il = enabledAttributes.length; i < il; i ++ ) {

			if ( enabledAttributes[ i ] !== newAttributes[ i ] ) {

				gl.disableVertexAttribArray( i );
				enabledAttributes[ i ] = 0;

			}

		}

	}

	function vertexAttribPointer( index, size, type, normalized, stride, offset, integer ) {

		if ( integer === true ) {

			gl.vertexAttribIPointer( index, size, type, stride, offset );

		} else {

			gl.vertexAttribPointer( index, size, type, normalized, stride, offset );

		}

	}

	function setupVertexAttributes( object, material, program, geometry ) {

		initAttributes();

		const geometryAttributes = geometry.attributes;

		const programAttributes = program.getAttributes();

		const materialDefaultAttributeValues = material.defaultAttributeValues;

		for ( const name in programAttributes ) {

			const programAttribute = programAttributes[ name ];

			if ( programAttribute.location >= 0 ) {

				let geometryAttribute = geometryAttributes[ name ];

				if ( geometryAttribute === undefined ) {

					if ( name === 'instanceMatrix' && object.instanceMatrix ) geometryAttribute = object.instanceMatrix;
					if ( name === 'instanceColor' && object.instanceColor ) geometryAttribute = object.instanceColor;

				}

				if ( geometryAttribute !== undefined ) {

					const normalized = geometryAttribute.normalized;
					const size = geometryAttribute.itemSize;

					const attribute = attributes.get( geometryAttribute );

					// TODO Attribute may not be available on context restore

					if ( attribute === undefined ) continue;

					const buffer = attribute.buffer;
					const type = attribute.type;
					const bytesPerElement = attribute.bytesPerElement;

					// check for integer attributes

					const integer = ( type === gl.INT || type === gl.UNSIGNED_INT || geometryAttribute.gpuType === IntType );

					if ( geometryAttribute.isInterleavedBufferAttribute ) {

						const data = geometryAttribute.data;
						const stride = data.stride;
						const offset = geometryAttribute.offset;

						if ( data.isInstancedInterleavedBuffer ) {

							for ( let i = 0; i < programAttribute.locationSize; i ++ ) {

								enableAttributeAndDivisor( programAttribute.location + i, data.meshPerAttribute );

							}

							if ( object.isInstancedMesh !== true && geometry._maxInstanceCount === undefined ) {

								geometry._maxInstanceCount = data.meshPerAttribute * data.count;

							}

						} else {

							for ( let i = 0; i < programAttribute.locationSize; i ++ ) {

								enableAttribute( programAttribute.location + i );

							}

						}

						gl.bindBuffer( gl.ARRAY_BUFFER, buffer );

						for ( let i = 0; i < programAttribute.locationSize; i ++ ) {

							vertexAttribPointer(
								programAttribute.location + i,
								size / programAttribute.locationSize,
								type,
								normalized,
								stride * bytesPerElement,
								( offset + ( size / programAttribute.locationSize ) * i ) * bytesPerElement,
								integer
							);

						}

					} else {

						if ( geometryAttribute.isInstancedBufferAttribute ) {

							for ( let i = 0; i < programAttribute.locationSize; i ++ ) {

								enableAttributeAndDivisor( programAttribute.location + i, geometryAttribute.meshPerAttribute );

							}

							if ( object.isInstancedMesh !== true && geometry._maxInstanceCount === undefined ) {

								geometry._maxInstanceCount = geometryAttribute.meshPerAttribute * geometryAttribute.count;

							}

						} else {

							for ( let i = 0; i < programAttribute.locationSize; i ++ ) {

								enableAttribute( programAttribute.location + i );

							}

						}

						gl.bindBuffer( gl.ARRAY_BUFFER, buffer );

						for ( let i = 0; i < programAttribute.locationSize; i ++ ) {

							vertexAttribPointer(
								programAttribute.location + i,
								size / programAttribute.locationSize,
								type,
								normalized,
								size * bytesPerElement,
								( size / programAttribute.locationSize ) * i * bytesPerElement,
								integer
							);

						}

					}

				} else if ( materialDefaultAttributeValues !== undefined ) {

					const value = materialDefaultAttributeValues[ name ];

					if ( value !== undefined ) {

						switch ( value.length ) {

							case 2:
								gl.vertexAttrib2fv( programAttribute.location, value );
								break;

							case 3:
								gl.vertexAttrib3fv( programAttribute.location, value );
								break;

							case 4:
								gl.vertexAttrib4fv( programAttribute.location, value );
								break;

							default:
								gl.vertexAttrib1fv( programAttribute.location, value );

						}

					}

				}

			}

		}

		disableUnusedAttributes();

	}

	function dispose() {

		reset();

		for ( const geometryId in bindingStates ) {

			const programMap = bindingStates[ geometryId ];

			for ( const programId in programMap ) {

				const stateMap = programMap[ programId ];

				for ( const wireframe in stateMap ) {

					deleteVertexArrayObject( stateMap[ wireframe ].object );

					delete stateMap[ wireframe ];

				}

				delete programMap[ programId ];

			}

			delete bindingStates[ geometryId ];

		}

	}

	function releaseStatesOfGeometry( geometry ) {

		if ( bindingStates[ geometry.id ] === undefined ) return;

		const programMap = bindingStates[ geometry.id ];

		for ( const programId in programMap ) {

			const stateMap = programMap[ programId ];

			for ( const wireframe in stateMap ) {

				deleteVertexArrayObject( stateMap[ wireframe ].object );

				delete stateMap[ wireframe ];

			}

			delete programMap[ programId ];

		}

		delete bindingStates[ geometry.id ];

	}

	function releaseStatesOfProgram( program ) {

		for ( const geometryId in bindingStates ) {

			const programMap = bindingStates[ geometryId ];

			if ( programMap[ program.id ] === undefined ) continue;

			const stateMap = programMap[ program.id ];

			for ( const wireframe in stateMap ) {

				deleteVertexArrayObject( stateMap[ wireframe ].object );

				delete stateMap[ wireframe ];

			}

			delete programMap[ program.id ];

		}

	}

	function reset() {

		resetDefaultState();
		forceUpdate = true;

		if ( currentState === defaultState ) return;

		currentState = defaultState;
		bindVertexArrayObject( currentState.object );

	}

	// for backward-compatibility

	function resetDefaultState() {

		defaultState.geometry = null;
		defaultState.program = null;
		defaultState.wireframe = false;

	}

	return {

		setup: setup,
		reset: reset,
		resetDefaultState: resetDefaultState,
		dispose: dispose,
		releaseStatesOfGeometry: releaseStatesOfGeometry,
		releaseStatesOfProgram: releaseStatesOfProgram,

		initAttributes: initAttributes,
		enableAttribute: enableAttribute,
		disableUnusedAttributes: disableUnusedAttributes

	};

}

function WebGLBufferRenderer( gl, extensions, info ) {

	let mode;

	function setMode( value ) {

		mode = value;

	}

	function render( start, count ) {

		gl.drawArrays( mode, start, count );

		info.update( count, mode, 1 );

	}

	function renderInstances( start, count, primcount ) {

		if ( primcount === 0 ) return;

		gl.drawArraysInstanced( mode, start, count, primcount );

		info.update( count, mode, primcount );

	}

	function renderMultiDraw( starts, counts, drawCount ) {

		if ( drawCount === 0 ) return;

		const extension = extensions.get( 'WEBGL_multi_draw' );
		extension.multiDrawArraysWEBGL( mode, starts, 0, counts, 0, drawCount );

		let elementCount = 0;
		for ( let i = 0; i < drawCount; i ++ ) {

			elementCount += counts[ i ];

		}

		info.update( elementCount, mode, 1 );

	}

	function renderMultiDrawInstances( starts, counts, drawCount, primcount ) {

		if ( drawCount === 0 ) return;

		const extension = extensions.get( 'WEBGL_multi_draw' );

		if ( extension === null ) {

			for ( let i = 0; i < starts.length; i ++ ) {

				renderInstances( starts[ i ], counts[ i ], primcount[ i ] );

			}

		} else {

			extension.multiDrawArraysInstancedWEBGL( mode, starts, 0, counts, 0, primcount, 0, drawCount );

			let elementCount = 0;
			for ( let i = 0; i < drawCount; i ++ ) {

				elementCount += counts[ i ] * primcount[ i ];

			}

			info.update( elementCount, mode, 1 );

		}

	}

	//

	this.setMode = setMode;
	this.render = render;
	this.renderInstances = renderInstances;
	this.renderMultiDraw = renderMultiDraw;
	this.renderMultiDrawInstances = renderMultiDrawInstances;

}

function WebGLCapabilities( gl, extensions, parameters, utils ) {

	let maxAnisotropy;

	function getMaxAnisotropy() {

		if ( maxAnisotropy !== undefined ) return maxAnisotropy;

		if ( extensions.has( 'EXT_texture_filter_anisotropic' ) === true ) {

			const extension = extensions.get( 'EXT_texture_filter_anisotropic' );

			maxAnisotropy = gl.getParameter( extension.MAX_TEXTURE_MAX_ANISOTROPY_EXT );

		} else {

			maxAnisotropy = 0;

		}

		return maxAnisotropy;

	}

	function textureFormatReadable( textureFormat ) {

		if ( textureFormat !== RGBAFormat && utils.convert( textureFormat ) !== gl.getParameter( gl.IMPLEMENTATION_COLOR_READ_FORMAT ) ) {

			return false;

		}

		return true;

	}

	function textureTypeReadable( textureType ) {

		const halfFloatSupportedByExt = ( textureType === HalfFloatType ) && ( extensions.has( 'EXT_color_buffer_half_float' ) || extensions.has( 'EXT_color_buffer_float' ) );

		if ( textureType !== UnsignedByteType && utils.convert( textureType ) !== gl.getParameter( gl.IMPLEMENTATION_COLOR_READ_TYPE ) && // Edge and Chrome Mac < 52 (#9513)
			textureType !== FloatType && ! halfFloatSupportedByExt ) {

			return false;

		}

		return true;

	}

	function getMaxPrecision( precision ) {

		if ( precision === 'highp' ) {

			if ( gl.getShaderPrecisionFormat( gl.VERTEX_SHADER, gl.HIGH_FLOAT ).precision > 0 &&
				gl.getShaderPrecisionFormat( gl.FRAGMENT_SHADER, gl.HIGH_FLOAT ).precision > 0 ) {

				return 'highp';

			}

			precision = 'mediump';

		}

		if ( precision === 'mediump' ) {

			if ( gl.getShaderPrecisionFormat( gl.VERTEX_SHADER, gl.MEDIUM_FLOAT ).precision > 0 &&
				gl.getShaderPrecisionFormat( gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT ).precision > 0 ) {

				return 'mediump';

			}

		}

		return 'lowp';

	}

	let precision = parameters.precision !== undefined ? parameters.precision : 'highp';
	const maxPrecision = getMaxPrecision( precision );

	if ( maxPrecision !== precision ) {

		console.warn( 'THREE.WebGLRenderer:', precision, 'not supported, using', maxPrecision, 'instead.' );
		precision = maxPrecision;

	}

	const logarithmicDepthBuffer = parameters.logarithmicDepthBuffer === true;
	const reversedDepthBuffer = parameters.reversedDepthBuffer === true && extensions.has( 'EXT_clip_control' );

	const maxTextures = gl.getParameter( gl.MAX_TEXTURE_IMAGE_UNITS );
	const maxVertexTextures = gl.getParameter( gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS );
	const maxTextureSize = gl.getParameter( gl.MAX_TEXTURE_SIZE );
	const maxCubemapSize = gl.getParameter( gl.MAX_CUBE_MAP_TEXTURE_SIZE );

	const maxAttributes = gl.getParameter( gl.MAX_VERTEX_ATTRIBS );
	const maxVertexUniforms = gl.getParameter( gl.MAX_VERTEX_UNIFORM_VECTORS );
	const maxVaryings = gl.getParameter( gl.MAX_VARYING_VECTORS );
	const maxFragmentUniforms = gl.getParameter( gl.MAX_FRAGMENT_UNIFORM_VECTORS );

	const vertexTextures = maxVertexTextures > 0;

	const maxSamples = gl.getParameter( gl.MAX_SAMPLES );

	return {

		isWebGL2: true, // keeping this for backwards compatibility

		getMaxAnisotropy: getMaxAnisotropy,
		getMaxPrecision: getMaxPrecision,

		textureFormatReadable: textureFormatReadable,
		textureTypeReadable: textureTypeReadable,

		precision: precision,
		logarithmicDepthBuffer: logarithmicDepthBuffer,
		reversedDepthBuffer: reversedDepthBuffer,

		maxTextures: maxTextures,
		maxVertexTextures: maxVertexTextures,
		maxTextureSize: maxTextureSize,
		maxCubemapSize: maxCubemapSize,

		maxAttributes: maxAttributes,
		maxVertexUniforms: maxVertexUniforms,
		maxVaryings: maxVaryings,
		maxFragmentUniforms: maxFragmentUniforms,

		vertexTextures: vertexTextures,

		maxSamples: maxSamples

	};

}

function WebGLClipping( properties ) {

	const scope = this;

	let globalState = null,
		numGlobalPlanes = 0,
		localClippingEnabled = false,
		renderingShadows = false;

	const plane = new Plane(),
		viewNormalMatrix = new Matrix3(),

		uniform = { value: null, needsUpdate: false };

	this.uniform = uniform;
	this.numPlanes = 0;
	this.numIntersection = 0;

	this.init = function ( planes, enableLocalClipping ) {

		const enabled =
			planes.length !== 0 ||
			enableLocalClipping ||
			// enable state of previous frame - the clipping code has to
			// run another frame in order to reset the state:
			numGlobalPlanes !== 0 ||
			localClippingEnabled;

		localClippingEnabled = enableLocalClipping;

		numGlobalPlanes = planes.length;

		return enabled;

	};

	this.beginShadows = function () {

		renderingShadows = true;
		projectPlanes( null );

	};

	this.endShadows = function () {

		renderingShadows = false;

	};

	this.setGlobalState = function ( planes, camera ) {

		globalState = projectPlanes( planes, camera, 0 );

	};

	this.setState = function ( material, camera, useCache ) {

		const planes = material.clippingPlanes,
			clipIntersection = material.clipIntersection,
			clipShadows = material.clipShadows;

		const materialProperties = properties.get( material );

		if ( ! localClippingEnabled || planes === null || planes.length === 0 || renderingShadows && ! clipShadows ) {

			// there's no local clipping

			if ( renderingShadows ) {

				// there's no global clipping

				projectPlanes( null );

			} else {

				resetGlobalState();

			}

		} else {

			const nGlobal = renderingShadows ? 0 : numGlobalPlanes,
				lGlobal = nGlobal * 4;

			let dstArray = materialProperties.clippingState || null;

			uniform.value = dstArray; // ensure unique state

			dstArray = projectPlanes( planes, camera, lGlobal, useCache );

			for ( let i = 0; i !== lGlobal; ++ i ) {

				dstArray[ i ] = globalState[ i ];

			}

			materialProperties.clippingState = dstArray;
			this.numIntersection = clipIntersection ? this.numPlanes : 0;
			this.numPlanes += nGlobal;

		}


	};

	function resetGlobalState() {

		if ( uniform.value !== globalState ) {

			uniform.value = globalState;
			uniform.needsUpdate = numGlobalPlanes > 0;

		}

		scope.numPlanes = numGlobalPlanes;
		scope.numIntersection = 0;

	}

	function projectPlanes( planes, camera, dstOffset, skipTransform ) {

		const nPlanes = planes !== null ? planes.length : 0;
		let dstArray = null;

		if ( nPlanes !== 0 ) {

			dstArray = uniform.value;

			if ( skipTransform !== true || dstArray === null ) {

				const flatSize = dstOffset + nPlanes * 4,
					viewMatrix = camera.matrixWorldInverse;

				viewNormalMatrix.getNormalMatrix( viewMatrix );

				if ( dstArray === null || dstArray.length < flatSize ) {

					dstArray = new Float32Array( flatSize );

				}

				for ( let i = 0, i4 = dstOffset; i !== nPlanes; ++ i, i4 += 4 ) {

					plane.copy( planes[ i ] ).applyMatrix4( viewMatrix, viewNormalMatrix );

					plane.normal.toArray( dstArray, i4 );
					dstArray[ i4 + 3 ] = plane.constant;

				}

			}

			uniform.value = dstArray;
			uniform.needsUpdate = true;

		}

		scope.numPlanes = nPlanes;
		scope.numIntersection = 0;

		return dstArray;

	}

}

/**
 * Abstract base class for cameras. This class should always be inherited
 * when you build a new camera.
 *
 * @abstract
 * @augments Object3D
 */
class Camera extends Object3D {

	/**
	 * Constructs a new camera.
	 */
	constructor() {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isCamera = true;

		this.type = 'Camera';

		/**
		 * The inverse of the camera's world matrix.
		 *
		 * @type {Matrix4}
		 */
		this.matrixWorldInverse = new Matrix4();

		/**
		 * The camera's projection matrix.
		 *
		 * @type {Matrix4}
		 */
		this.projectionMatrix = new Matrix4();

		/**
		 * The inverse of the camera's projection matrix.
		 *
		 * @type {Matrix4}
		 */
		this.projectionMatrixInverse = new Matrix4();

		/**
		 * The coordinate system in which the camera is used.
		 *
		 * @type {(WebGLCoordinateSystem|WebGPUCoordinateSystem)}
		 */
		this.coordinateSystem = WebGLCoordinateSystem;

		this._reversedDepth = false;

	}

	/**
	 * The flag that indicates whether the camera uses a reversed depth buffer.
	 *
	 * @type {boolean}
	 * @default false
	 */
	get reversedDepth() {

		return this._reversedDepth;

	}

	copy( source, recursive ) {

		super.copy( source, recursive );

		this.matrixWorldInverse.copy( source.matrixWorldInverse );

		this.projectionMatrix.copy( source.projectionMatrix );
		this.projectionMatrixInverse.copy( source.projectionMatrixInverse );

		this.coordinateSystem = source.coordinateSystem;

		return this;

	}

	/**
	 * Returns a vector representing the ("look") direction of the 3D object in world space.
	 *
	 * This method is overwritten since cameras have a different forward vector compared to other
	 * 3D objects. A camera looks down its local, negative z-axis by default.
	 *
	 * @param {Vector3} target - The target vector the result is stored to.
	 * @return {Vector3} The 3D object's direction in world space.
	 */
	getWorldDirection( target ) {

		return super.getWorldDirection( target ).negate();

	}

	updateMatrixWorld( force ) {

		super.updateMatrixWorld( force );

		this.matrixWorldInverse.copy( this.matrixWorld ).invert();

	}

	updateWorldMatrix( updateParents, updateChildren ) {

		super.updateWorldMatrix( updateParents, updateChildren );

		this.matrixWorldInverse.copy( this.matrixWorld ).invert();

	}

	clone() {

		return new this.constructor().copy( this );

	}

}

const _v3 = /*@__PURE__*/ new Vector3();
const _minTarget = /*@__PURE__*/ new Vector2();
const _maxTarget = /*@__PURE__*/ new Vector2();

/**
 * Camera that uses [perspective projection]{@link https://en.wikipedia.org/wiki/Perspective_(graphical)}.
 *
 * This projection mode is designed to mimic the way the human eye sees. It
 * is the most common projection mode used for rendering a 3D scene.
 *
 * ```js
 * const camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000 );
 * scene.add( camera );
 * ```
 *
 * @augments Camera
 */
class PerspectiveCamera extends Camera {

	/**
	 * Constructs a new perspective camera.
	 *
	 * @param {number} [fov=50] - The vertical field of view.
	 * @param {number} [aspect=1] - The aspect ratio.
	 * @param {number} [near=0.1] - The camera's near plane.
	 * @param {number} [far=2000] - The camera's far plane.
	 */
	constructor( fov = 50, aspect = 1, near = 0.1, far = 2000 ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isPerspectiveCamera = true;

		this.type = 'PerspectiveCamera';

		/**
		 * The vertical field of view, from bottom to top of view,
		 * in degrees.
		 *
		 * @type {number}
		 * @default 50
		 */
		this.fov = fov;

		/**
		 * The zoom factor of the camera.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.zoom = 1;

		/**
		 * The camera's near plane. The valid range is greater than `0`
		 * and less than the current value of {@link PerspectiveCamera#far}.
		 *
		 * Note that, unlike for the {@link OrthographicCamera}, `0` is <em>not</em> a
		 * valid value for a perspective camera's near plane.
		 *
		 * @type {number}
		 * @default 0.1
		 */
		this.near = near;

		/**
		 * The camera's far plane. Must be greater than the
		 * current value of {@link PerspectiveCamera#near}.
		 *
		 * @type {number}
		 * @default 2000
		 */
		this.far = far;

		/**
		 * Object distance used for stereoscopy and depth-of-field effects. This
		 * parameter does not influence the projection matrix unless a
		 * {@link StereoCamera} is being used.
		 *
		 * @type {number}
		 * @default 10
		 */
		this.focus = 10;

		/**
		 * The aspect ratio, usually the canvas width / canvas height.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.aspect = aspect;

		/**
		 * Represents the frustum window specification. This property should not be edited
		 * directly but via {@link PerspectiveCamera#setViewOffset} and {@link PerspectiveCamera#clearViewOffset}.
		 *
		 * @type {?Object}
		 * @default null
		 */
		this.view = null;

		/**
		 * Film size used for the larger axis. Default is `35` (millimeters). This
		 * parameter does not influence the projection matrix unless {@link PerspectiveCamera#filmOffset}
		 * is set to a nonzero value.
		 *
		 * @type {number}
		 * @default 35
		 */
		this.filmGauge = 35;

		/**
		 * Horizontal off-center offset in the same unit as {@link PerspectiveCamera#filmGauge}.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.filmOffset = 0;

		this.updateProjectionMatrix();

	}

	copy( source, recursive ) {

		super.copy( source, recursive );

		this.fov = source.fov;
		this.zoom = source.zoom;

		this.near = source.near;
		this.far = source.far;
		this.focus = source.focus;

		this.aspect = source.aspect;
		this.view = source.view === null ? null : Object.assign( {}, source.view );

		this.filmGauge = source.filmGauge;
		this.filmOffset = source.filmOffset;

		return this;

	}

	/**
	 * Sets the FOV by focal length in respect to the current {@link PerspectiveCamera#filmGauge}.
	 *
	 * The default film gauge is 35, so that the focal length can be specified for
	 * a 35mm (full frame) camera.
	 *
	 * @param {number} focalLength - Values for focal length and film gauge must have the same unit.
	 */
	setFocalLength( focalLength ) {

		/** see {@link http://www.bobatkins.com/photography/technical/field_of_view.html} */
		const vExtentSlope = 0.5 * this.getFilmHeight() / focalLength;

		this.fov = RAD2DEG * 2 * Math.atan( vExtentSlope );
		this.updateProjectionMatrix();

	}

	/**
	 * Returns the focal length from the current {@link PerspectiveCamera#fov} and
	 * {@link PerspectiveCamera#filmGauge}.
	 *
	 * @return {number} The computed focal length.
	 */
	getFocalLength() {

		const vExtentSlope = Math.tan( DEG2RAD * 0.5 * this.fov );

		return 0.5 * this.getFilmHeight() / vExtentSlope;

	}

	/**
	 * Returns the current vertical field of view angle in degrees considering {@link PerspectiveCamera#zoom}.
	 *
	 * @return {number} The effective FOV.
	 */
	getEffectiveFOV() {

		return RAD2DEG * 2 * Math.atan(
			Math.tan( DEG2RAD * 0.5 * this.fov ) / this.zoom );

	}

	/**
	 * Returns the width of the image on the film. If {@link PerspectiveCamera#aspect} is greater than or
	 * equal to one (landscape format), the result equals {@link PerspectiveCamera#filmGauge}.
	 *
	 * @return {number} The film width.
	 */
	getFilmWidth() {

		// film not completely covered in portrait format (aspect < 1)
		return this.filmGauge * Math.min( this.aspect, 1 );

	}

	/**
	 * Returns the height of the image on the film. If {@link PerspectiveCamera#aspect} is greater than or
	 * equal to one (landscape format), the result equals {@link PerspectiveCamera#filmGauge}.
	 *
	 * @return {number} The film width.
	 */
	getFilmHeight() {

		// film not completely covered in landscape format (aspect > 1)
		return this.filmGauge / Math.max( this.aspect, 1 );

	}

	/**
	 * Computes the 2D bounds of the camera's viewable rectangle at a given distance along the viewing direction.
	 * Sets `minTarget` and `maxTarget` to the coordinates of the lower-left and upper-right corners of the view rectangle.
	 *
	 * @param {number} distance - The viewing distance.
	 * @param {Vector2} minTarget - The lower-left corner of the view rectangle is written into this vector.
	 * @param {Vector2} maxTarget - The upper-right corner of the view rectangle is written into this vector.
	 */
	getViewBounds( distance, minTarget, maxTarget ) {

		_v3.set( -1, -1, 0.5 ).applyMatrix4( this.projectionMatrixInverse );

		minTarget.set( _v3.x, _v3.y ).multiplyScalar( - distance / _v3.z );

		_v3.set( 1, 1, 0.5 ).applyMatrix4( this.projectionMatrixInverse );

		maxTarget.set( _v3.x, _v3.y ).multiplyScalar( - distance / _v3.z );

	}

	/**
	 * Computes the width and height of the camera's viewable rectangle at a given distance along the viewing direction.
	 *
	 * @param {number} distance - The viewing distance.
	 * @param {Vector2} target - The target vector that is used to store result where x is width and y is height.
	 * @returns {Vector2} The view size.
	 */
	getViewSize( distance, target ) {

		this.getViewBounds( distance, _minTarget, _maxTarget );

		return target.subVectors( _maxTarget, _minTarget );

	}

	/**
	 * Sets an offset in a larger frustum. This is useful for multi-window or
	 * multi-monitor/multi-machine setups.
	 *
	 * For example, if you have 3x2 monitors and each monitor is 1920x1080 and
	 * the monitors are in grid like this
	 *```
	 *   +---+---+---+
	 *   | A | B | C |
	 *   +---+---+---+
	 *   | D | E | F |
	 *   +---+---+---+
	 *```
	 * then for each monitor you would call it like this:
	 *```js
	 * const w = 1920;
	 * const h = 1080;
	 * const fullWidth = w * 3;
	 * const fullHeight = h * 2;
	 *
	 * // --A--
	 * camera.setViewOffset( fullWidth, fullHeight, w * 0, h * 0, w, h );
	 * // --B--
	 * camera.setViewOffset( fullWidth, fullHeight, w * 1, h * 0, w, h );
	 * // --C--
	 * camera.setViewOffset( fullWidth, fullHeight, w * 2, h * 0, w, h );
	 * // --D--
	 * camera.setViewOffset( fullWidth, fullHeight, w * 0, h * 1, w, h );
	 * // --E--
	 * camera.setViewOffset( fullWidth, fullHeight, w * 1, h * 1, w, h );
	 * // --F--
	 * camera.setViewOffset( fullWidth, fullHeight, w * 2, h * 1, w, h );
	 * ```
	 *
	 * Note there is no reason monitors have to be the same size or in a grid.
	 *
	 * @param {number} fullWidth - The full width of multiview setup.
	 * @param {number} fullHeight - The full height of multiview setup.
	 * @param {number} x - The horizontal offset of the subcamera.
	 * @param {number} y - The vertical offset of the subcamera.
	 * @param {number} width - The width of subcamera.
	 * @param {number} height - The height of subcamera.
	 */
	setViewOffset( fullWidth, fullHeight, x, y, width, height ) {

		this.aspect = fullWidth / fullHeight;

		if ( this.view === null ) {

			this.view = {
				enabled: true,
				fullWidth: 1,
				fullHeight: 1,
				offsetX: 0,
				offsetY: 0,
				width: 1,
				height: 1
			};

		}

		this.view.enabled = true;
		this.view.fullWidth = fullWidth;
		this.view.fullHeight = fullHeight;
		this.view.offsetX = x;
		this.view.offsetY = y;
		this.view.width = width;
		this.view.height = height;

		this.updateProjectionMatrix();

	}

	/**
	 * Removes the view offset from the projection matrix.
	 */
	clearViewOffset() {

		if ( this.view !== null ) {

			this.view.enabled = false;

		}

		this.updateProjectionMatrix();

	}

	/**
	 * Updates the camera's projection matrix. Must be called after any change of
	 * camera properties.
	 */
	updateProjectionMatrix() {

		const near = this.near;
		let top = near * Math.tan( DEG2RAD * 0.5 * this.fov ) / this.zoom;
		let height = 2 * top;
		let width = this.aspect * height;
		let left = -0.5 * width;
		const view = this.view;

		if ( this.view !== null && this.view.enabled ) {

			const fullWidth = view.fullWidth,
				fullHeight = view.fullHeight;

			left += view.offsetX * width / fullWidth;
			top -= view.offsetY * height / fullHeight;
			width *= view.width / fullWidth;
			height *= view.height / fullHeight;

		}

		const skew = this.filmOffset;
		if ( skew !== 0 ) left += near * skew / this.getFilmWidth();

		this.projectionMatrix.makePerspective( left, left + width, top, top - height, near, this.far, this.coordinateSystem, this.reversedDepth );

		this.projectionMatrixInverse.copy( this.projectionMatrix ).invert();

	}

	toJSON( meta ) {

		const data = super.toJSON( meta );

		data.object.fov = this.fov;
		data.object.zoom = this.zoom;

		data.object.near = this.near;
		data.object.far = this.far;
		data.object.focus = this.focus;

		data.object.aspect = this.aspect;

		if ( this.view !== null ) data.object.view = Object.assign( {}, this.view );

		data.object.filmGauge = this.filmGauge;
		data.object.filmOffset = this.filmOffset;

		return data;

	}

}

const fov = -90; // negative fov is not an error
const aspect = 1;

/**
 * A special type of camera that is positioned in 3D space to render its surroundings into a
 * cube render target. The render target can then be used as an environment map for rendering
 * realtime reflections in your scene.
 *
 * ```js
 * // Create cube render target
 * const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256, { generateMipmaps: true, minFilter: THREE.LinearMipmapLinearFilter } );
 *
 * // Create cube camera
 * const cubeCamera = new THREE.CubeCamera( 1, 100000, cubeRenderTarget );
 * scene.add( cubeCamera );
 *
 * // Create car
 * const chromeMaterial = new THREE.MeshLambertMaterial( { color: 0xffffff, envMap: cubeRenderTarget.texture } );
 * const car = new THREE.Mesh( carGeometry, chromeMaterial );
 * scene.add( car );
 *
 * // Update the render target cube
 * car.visible = false;
 * cubeCamera.position.copy( car.position );
 * cubeCamera.update( renderer, scene );
 *
 * // Render the scene
 * car.visible = true;
 * renderer.render( scene, camera );
 * ```
 *
 * @augments Object3D
 */
class CubeCamera extends Object3D {

	/**
	 * Constructs a new cube camera.
	 *
	 * @param {number} near - The camera's near plane.
	 * @param {number} far - The camera's far plane.
	 * @param {WebGLCubeRenderTarget} renderTarget - The cube render target.
	 */
	constructor( near, far, renderTarget ) {

		super();

		this.type = 'CubeCamera';

		/**
		 * A reference to the cube render target.
		 *
		 * @type {WebGLCubeRenderTarget}
		 */
		this.renderTarget = renderTarget;

		/**
		 * The current active coordinate system.
		 *
		 * @type {?(WebGLCoordinateSystem|WebGPUCoordinateSystem)}
		 * @default null
		 */
		this.coordinateSystem = null;

		/**
		 * The current active mipmap level
		 *
		 * @type {number}
		 * @default 0
		 */
		this.activeMipmapLevel = 0;

		const cameraPX = new PerspectiveCamera( fov, aspect, near, far );
		cameraPX.layers = this.layers;
		this.add( cameraPX );

		const cameraNX = new PerspectiveCamera( fov, aspect, near, far );
		cameraNX.layers = this.layers;
		this.add( cameraNX );

		const cameraPY = new PerspectiveCamera( fov, aspect, near, far );
		cameraPY.layers = this.layers;
		this.add( cameraPY );

		const cameraNY = new PerspectiveCamera( fov, aspect, near, far );
		cameraNY.layers = this.layers;
		this.add( cameraNY );

		const cameraPZ = new PerspectiveCamera( fov, aspect, near, far );
		cameraPZ.layers = this.layers;
		this.add( cameraPZ );

		const cameraNZ = new PerspectiveCamera( fov, aspect, near, far );
		cameraNZ.layers = this.layers;
		this.add( cameraNZ );

	}

	/**
	 * Must be called when the coordinate system of the cube camera is changed.
	 */
	updateCoordinateSystem() {

		const coordinateSystem = this.coordinateSystem;

		const cameras = this.children.concat();

		const [ cameraPX, cameraNX, cameraPY, cameraNY, cameraPZ, cameraNZ ] = cameras;

		for ( const camera of cameras ) this.remove( camera );

		if ( coordinateSystem === WebGLCoordinateSystem ) {

			cameraPX.up.set( 0, 1, 0 );
			cameraPX.lookAt( 1, 0, 0 );

			cameraNX.up.set( 0, 1, 0 );
			cameraNX.lookAt( -1, 0, 0 );

			cameraPY.up.set( 0, 0, -1 );
			cameraPY.lookAt( 0, 1, 0 );

			cameraNY.up.set( 0, 0, 1 );
			cameraNY.lookAt( 0, -1, 0 );

			cameraPZ.up.set( 0, 1, 0 );
			cameraPZ.lookAt( 0, 0, 1 );

			cameraNZ.up.set( 0, 1, 0 );
			cameraNZ.lookAt( 0, 0, -1 );

		} else if ( coordinateSystem === WebGPUCoordinateSystem ) {

			cameraPX.up.set( 0, -1, 0 );
			cameraPX.lookAt( -1, 0, 0 );

			cameraNX.up.set( 0, -1, 0 );
			cameraNX.lookAt( 1, 0, 0 );

			cameraPY.up.set( 0, 0, 1 );
			cameraPY.lookAt( 0, 1, 0 );

			cameraNY.up.set( 0, 0, -1 );
			cameraNY.lookAt( 0, -1, 0 );

			cameraPZ.up.set( 0, -1, 0 );
			cameraPZ.lookAt( 0, 0, 1 );

			cameraNZ.up.set( 0, -1, 0 );
			cameraNZ.lookAt( 0, 0, -1 );

		} else {

			throw new Error( 'THREE.CubeCamera.updateCoordinateSystem(): Invalid coordinate system: ' + coordinateSystem );

		}

		for ( const camera of cameras ) {

			this.add( camera );

			camera.updateMatrixWorld();

		}

	}

	/**
	 * Calling this method will render the given scene with the given renderer
	 * into the cube render target of the camera.
	 *
	 * @param {(Renderer|WebGLRenderer)} renderer - The renderer.
	 * @param {Scene} scene - The scene to render.
	 */
	update( renderer, scene ) {

		if ( this.parent === null ) this.updateMatrixWorld();

		const { renderTarget, activeMipmapLevel } = this;

		if ( this.coordinateSystem !== renderer.coordinateSystem ) {

			this.coordinateSystem = renderer.coordinateSystem;

			this.updateCoordinateSystem();

		}

		const [ cameraPX, cameraNX, cameraPY, cameraNY, cameraPZ, cameraNZ ] = this.children;

		const currentRenderTarget = renderer.getRenderTarget();
		const currentActiveCubeFace = renderer.getActiveCubeFace();
		const currentActiveMipmapLevel = renderer.getActiveMipmapLevel();

		const currentXrEnabled = renderer.xr.enabled;

		renderer.xr.enabled = false;

		const generateMipmaps = renderTarget.texture.generateMipmaps;

		renderTarget.texture.generateMipmaps = false;

		renderer.setRenderTarget( renderTarget, 0, activeMipmapLevel );
		renderer.render( scene, cameraPX );

		renderer.setRenderTarget( renderTarget, 1, activeMipmapLevel );
		renderer.render( scene, cameraNX );

		renderer.setRenderTarget( renderTarget, 2, activeMipmapLevel );
		renderer.render( scene, cameraPY );

		renderer.setRenderTarget( renderTarget, 3, activeMipmapLevel );
		renderer.render( scene, cameraNY );

		renderer.setRenderTarget( renderTarget, 4, activeMipmapLevel );
		renderer.render( scene, cameraPZ );

		// mipmaps are generated during the last call of render()
		// at this point, all sides of the cube render target are defined

		renderTarget.texture.generateMipmaps = generateMipmaps;

		renderer.setRenderTarget( renderTarget, 5, activeMipmapLevel );
		renderer.render( scene, cameraNZ );

		renderer.setRenderTarget( currentRenderTarget, currentActiveCubeFace, currentActiveMipmapLevel );

		renderer.xr.enabled = currentXrEnabled;

		renderTarget.texture.needsPMREMUpdate = true;

	}

}

/**
 * Creates a cube texture made up of six images.
 *
 * ```js
 * const loader = new THREE.CubeTextureLoader();
 * loader.setPath( 'textures/cube/pisa/' );
 *
 * const textureCube = loader.load( [
 * 	'px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png'
 * ] );
 *
 * const material = new THREE.MeshBasicMaterial( { color: 0xffffff, envMap: textureCube } );
 * ```
 *
 * @augments Texture
 */
class CubeTexture extends Texture {

	/**
	 * Constructs a new cube texture.
	 *
	 * @param {Array<Image>} [images=[]] - An array holding a image for each side of a cube.
	 * @param {number} [mapping=CubeReflectionMapping] - The texture mapping.
	 * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
	 * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
	 * @param {number} [magFilter=LinearFilter] - The mag filter value.
	 * @param {number} [minFilter=LinearMipmapLinearFilter] - The min filter value.
	 * @param {number} [format=RGBAFormat] - The texture format.
	 * @param {number} [type=UnsignedByteType] - The texture type.
	 * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
	 * @param {string} [colorSpace=NoColorSpace] - The color space value.
	 */
	constructor( images = [], mapping = CubeReflectionMapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, colorSpace ) {

		super( images, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, colorSpace );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isCubeTexture = true;

		/**
		 * If set to `true`, the texture is flipped along the vertical axis when
		 * uploaded to the GPU.
		 *
		 * Overwritten and set to `false` by default.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.flipY = false;

	}

	/**
	 * Alias for {@link CubeTexture#image}.
	 *
	 * @type {Array<Image>}
	 */
	get images() {

		return this.image;

	}

	set images( value ) {

		this.image = value;

	}

}

/**
 * A cube render target used in context of {@link WebGLRenderer}.
 *
 * @augments WebGLRenderTarget
 */
class WebGLCubeRenderTarget extends WebGLRenderTarget {

	/**
	 * Constructs a new cube render target.
	 *
	 * @param {number} [size=1] - The size of the render target.
	 * @param {RenderTarget~Options} [options] - The configuration object.
	 */
	constructor( size = 1, options = {} ) {

		super( size, size, options );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isWebGLCubeRenderTarget = true;

		const image = { width: size, height: size, depth: 1 };
		const images = [ image, image, image, image, image, image ];

		/**
		 * Overwritten with a different texture type.
		 *
		 * @type {DataArrayTexture}
		 */
		this.texture = new CubeTexture( images );
		this._setTextureOptions( options );

		// By convention -- likely based on the RenderMan spec from the 1990's -- cube maps are specified by WebGL (and three.js)
		// in a coordinate system in which positive-x is to the right when looking up the positive-z axis -- in other words,
		// in a left-handed coordinate system. By continuing this convention, preexisting cube maps continued to render correctly.

		// three.js uses a right-handed coordinate system. So environment maps used in three.js appear to have px and nx swapped
		// and the flag isRenderTargetTexture controls this conversion. The flip is not required when using WebGLCubeRenderTarget.texture
		// as a cube texture (this is detected when isRenderTargetTexture is set to true for cube textures).

		this.texture.isRenderTargetTexture = true;

	}

	/**
	 * Converts the given equirectangular texture to a cube map.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {Texture} texture - The equirectangular texture.
	 * @return {WebGLCubeRenderTarget} A reference to this cube render target.
	 */
	fromEquirectangularTexture( renderer, texture ) {

		this.texture.type = texture.type;
		this.texture.colorSpace = texture.colorSpace;

		this.texture.generateMipmaps = texture.generateMipmaps;
		this.texture.minFilter = texture.minFilter;
		this.texture.magFilter = texture.magFilter;

		const shader = {

			uniforms: {
				tEquirect: { value: null },
			},

			vertexShader: /* glsl */`

				varying vec3 vWorldDirection;

				vec3 transformDirection( in vec3 dir, in mat4 matrix ) {

					return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );

				}

				void main() {

					vWorldDirection = transformDirection( position, modelMatrix );

					#include <begin_vertex>
					#include <project_vertex>

				}
			`,

			fragmentShader: /* glsl */`

				uniform sampler2D tEquirect;

				varying vec3 vWorldDirection;

				#include <common>

				void main() {

					vec3 direction = normalize( vWorldDirection );

					vec2 sampleUV = equirectUv( direction );

					gl_FragColor = texture2D( tEquirect, sampleUV );

				}
			`
		};

		const geometry = new BoxGeometry( 5, 5, 5 );

		const material = new ShaderMaterial( {

			name: 'CubemapFromEquirect',

			uniforms: cloneUniforms( shader.uniforms ),
			vertexShader: shader.vertexShader,
			fragmentShader: shader.fragmentShader,
			side: BackSide,
			blending: NoBlending

		} );

		material.uniforms.tEquirect.value = texture;

		const mesh = new Mesh( geometry, material );

		const currentMinFilter = texture.minFilter;

		// Avoid blurred poles
		if ( texture.minFilter === LinearMipmapLinearFilter ) texture.minFilter = LinearFilter;

		const camera = new CubeCamera( 1, 10, this );
		camera.update( renderer, mesh );

		texture.minFilter = currentMinFilter;

		mesh.geometry.dispose();
		mesh.material.dispose();

		return this;

	}

	/**
	 * Clears this cube render target.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {boolean} [color=true] - Whether the color buffer should be cleared or not.
	 * @param {boolean} [depth=true] - Whether the depth buffer should be cleared or not.
	 * @param {boolean} [stencil=true] - Whether the stencil buffer should be cleared or not.
	 */
	clear( renderer, color = true, depth = true, stencil = true ) {

		const currentRenderTarget = renderer.getRenderTarget();

		for ( let i = 0; i < 6; i ++ ) {

			renderer.setRenderTarget( this, i );

			renderer.clear( color, depth, stencil );

		}

		renderer.setRenderTarget( currentRenderTarget );

	}

}

function WebGLCubeMaps( renderer ) {

	let cubemaps = new WeakMap();

	function mapTextureMapping( texture, mapping ) {

		if ( mapping === EquirectangularReflectionMapping ) {

			texture.mapping = CubeReflectionMapping;

		} else if ( mapping === EquirectangularRefractionMapping ) {

			texture.mapping = CubeRefractionMapping;

		}

		return texture;

	}

	function get( texture ) {

		if ( texture && texture.isTexture ) {

			const mapping = texture.mapping;

			if ( mapping === EquirectangularReflectionMapping || mapping === EquirectangularRefractionMapping ) {

				if ( cubemaps.has( texture ) ) {

					const cubemap = cubemaps.get( texture ).texture;
					return mapTextureMapping( cubemap, texture.mapping );

				} else {

					const image = texture.image;

					if ( image && image.height > 0 ) {

						const renderTarget = new WebGLCubeRenderTarget( image.height );
						renderTarget.fromEquirectangularTexture( renderer, texture );
						cubemaps.set( texture, renderTarget );

						texture.addEventListener( 'dispose', onTextureDispose );

						return mapTextureMapping( renderTarget.texture, texture.mapping );

					} else {

						// image not yet ready. try the conversion next frame

						return null;

					}

				}

			}

		}

		return texture;

	}

	function onTextureDispose( event ) {

		const texture = event.target;

		texture.removeEventListener( 'dispose', onTextureDispose );

		const cubemap = cubemaps.get( texture );

		if ( cubemap !== undefined ) {

			cubemaps.delete( texture );
			cubemap.dispose();

		}

	}

	function dispose() {

		cubemaps = new WeakMap();

	}

	return {
		get: get,
		dispose: dispose
	};

}

/**
 * Camera that uses [orthographic projection]{@link https://en.wikipedia.org/wiki/Orthographic_projection}.
 *
 * In this projection mode, an object's size in the rendered image stays
 * constant regardless of its distance from the camera. This can be useful
 * for rendering 2D scenes and UI elements, amongst other things.
 *
 * ```js
 * const camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 );
 * scene.add( camera );
 * ```
 *
 * @augments Camera
 */
class OrthographicCamera extends Camera {

	/**
	 * Constructs a new orthographic camera.
	 *
	 * @param {number} [left=-1] - The left plane of the camera's frustum.
	 * @param {number} [right=1] - The right plane of the camera's frustum.
	 * @param {number} [top=1] - The top plane of the camera's frustum.
	 * @param {number} [bottom=-1] - The bottom plane of the camera's frustum.
	 * @param {number} [near=0.1] - The camera's near plane.
	 * @param {number} [far=2000] - The camera's far plane.
	 */
	constructor( left = -1, right = 1, top = 1, bottom = -1, near = 0.1, far = 2000 ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isOrthographicCamera = true;

		this.type = 'OrthographicCamera';

		/**
		 * The zoom factor of the camera.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.zoom = 1;

		/**
		 * Represents the frustum window specification. This property should not be edited
		 * directly but via {@link PerspectiveCamera#setViewOffset} and {@link PerspectiveCamera#clearViewOffset}.
		 *
		 * @type {?Object}
		 * @default null
		 */
		this.view = null;

		/**
		 * The left plane of the camera's frustum.
		 *
		 * @type {number}
		 * @default -1
		 */
		this.left = left;

		/**
		 * The right plane of the camera's frustum.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.right = right;

		/**
		 * The top plane of the camera's frustum.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.top = top;

		/**
		 * The bottom plane of the camera's frustum.
		 *
		 * @type {number}
		 * @default -1
		 */
		this.bottom = bottom;

		/**
		 * The camera's near plane. The valid range is greater than `0`
		 * and less than the current value of {@link OrthographicCamera#far}.
		 *
		 * Note that, unlike for the {@link PerspectiveCamera}, `0` is a
		 * valid value for an orthographic camera's near plane.
		 *
		 * @type {number}
		 * @default 0.1
		 */
		this.near = near;

		/**
		 * The camera's far plane. Must be greater than the
		 * current value of {@link OrthographicCamera#near}.
		 *
		 * @type {number}
		 * @default 2000
		 */
		this.far = far;

		this.updateProjectionMatrix();

	}

	copy( source, recursive ) {

		super.copy( source, recursive );

		this.left = source.left;
		this.right = source.right;
		this.top = source.top;
		this.bottom = source.bottom;
		this.near = source.near;
		this.far = source.far;

		this.zoom = source.zoom;
		this.view = source.view === null ? null : Object.assign( {}, source.view );

		return this;

	}

	/**
	 * Sets an offset in a larger frustum. This is useful for multi-window or
	 * multi-monitor/multi-machine setups.
	 *
	 * @param {number} fullWidth - The full width of multiview setup.
	 * @param {number} fullHeight - The full height of multiview setup.
	 * @param {number} x - The horizontal offset of the subcamera.
	 * @param {number} y - The vertical offset of the subcamera.
	 * @param {number} width - The width of subcamera.
	 * @param {number} height - The height of subcamera.
	 * @see {@link PerspectiveCamera#setViewOffset}
	 */
	setViewOffset( fullWidth, fullHeight, x, y, width, height ) {

		if ( this.view === null ) {

			this.view = {
				enabled: true,
				fullWidth: 1,
				fullHeight: 1,
				offsetX: 0,
				offsetY: 0,
				width: 1,
				height: 1
			};

		}

		this.view.enabled = true;
		this.view.fullWidth = fullWidth;
		this.view.fullHeight = fullHeight;
		this.view.offsetX = x;
		this.view.offsetY = y;
		this.view.width = width;
		this.view.height = height;

		this.updateProjectionMatrix();

	}

	/**
	 * Removes the view offset from the projection matrix.
	 */
	clearViewOffset() {

		if ( this.view !== null ) {

			this.view.enabled = false;

		}

		this.updateProjectionMatrix();

	}

	/**
	 * Updates the camera's projection matrix. Must be called after any change of
	 * camera properties.
	 */
	updateProjectionMatrix() {

		const dx = ( this.right - this.left ) / ( 2 * this.zoom );
		const dy = ( this.top - this.bottom ) / ( 2 * this.zoom );
		const cx = ( this.right + this.left ) / 2;
		const cy = ( this.top + this.bottom ) / 2;

		let left = cx - dx;
		let right = cx + dx;
		let top = cy + dy;
		let bottom = cy - dy;

		if ( this.view !== null && this.view.enabled ) {

			const scaleW = ( this.right - this.left ) / this.view.fullWidth / this.zoom;
			const scaleH = ( this.top - this.bottom ) / this.view.fullHeight / this.zoom;

			left += scaleW * this.view.offsetX;
			right = left + scaleW * this.view.width;
			top -= scaleH * this.view.offsetY;
			bottom = top - scaleH * this.view.height;

		}

		this.projectionMatrix.makeOrthographic( left, right, top, bottom, this.near, this.far, this.coordinateSystem, this.reversedDepth );

		this.projectionMatrixInverse.copy( this.projectionMatrix ).invert();

	}

	toJSON( meta ) {

		const data = super.toJSON( meta );

		data.object.zoom = this.zoom;
		data.object.left = this.left;
		data.object.right = this.right;
		data.object.top = this.top;
		data.object.bottom = this.bottom;
		data.object.near = this.near;
		data.object.far = this.far;

		if ( this.view !== null ) data.object.view = Object.assign( {}, this.view );

		return data;

	}

}

const LOD_MIN = 4;

// The standard deviations (radians) associated with the extra mips. These are
// chosen to approximate a Trowbridge-Reitz distribution function times the
// geometric shadowing function. These sigma values squared must match the
// variance #defines in cube_uv_reflection_fragment.glsl.js.
const EXTRA_LOD_SIGMA = [ 0.125, 0.215, 0.35, 0.446, 0.526, 0.582 ];

// The maximum length of the blur for loop. Smaller sigmas will use fewer
// samples and exit early, but not recompile the shader.
const MAX_SAMPLES = 20;

const _flatCamera = /*@__PURE__*/ new OrthographicCamera();
const _clearColor = /*@__PURE__*/ new Color();
let _oldTarget = null;
let _oldActiveCubeFace = 0;
let _oldActiveMipmapLevel = 0;
let _oldXrEnabled = false;

// Golden Ratio
const PHI = ( 1 + Math.sqrt( 5 ) ) / 2;
const INV_PHI = 1 / PHI;

// Vertices of a dodecahedron (except the opposites, which represent the
// same axis), used as axis directions evenly spread on a sphere.
const _axisDirections = [
	/*@__PURE__*/ new Vector3( - PHI, INV_PHI, 0 ),
	/*@__PURE__*/ new Vector3( PHI, INV_PHI, 0 ),
	/*@__PURE__*/ new Vector3( - INV_PHI, 0, PHI ),
	/*@__PURE__*/ new Vector3( INV_PHI, 0, PHI ),
	/*@__PURE__*/ new Vector3( 0, PHI, - INV_PHI ),
	/*@__PURE__*/ new Vector3( 0, PHI, INV_PHI ),
	/*@__PURE__*/ new Vector3( -1, 1, -1 ),
	/*@__PURE__*/ new Vector3( 1, 1, -1 ),
	/*@__PURE__*/ new Vector3( -1, 1, 1 ),
	/*@__PURE__*/ new Vector3( 1, 1, 1 ) ];

const _origin = /*@__PURE__*/ new Vector3();

/**
 * This class generates a Prefiltered, Mipmapped Radiance Environment Map
 * (PMREM) from a cubeMap environment texture. This allows different levels of
 * blur to be quickly accessed based on material roughness. It is packed into a
 * special CubeUV format that allows us to perform custom interpolation so that
 * we can support nonlinear formats such as RGBE. Unlike a traditional mipmap
 * chain, it only goes down to the LOD_MIN level (above), and then creates extra
 * even more filtered 'mips' at the same LOD_MIN resolution, associated with
 * higher roughness levels. In this way we maintain resolution to smoothly
 * interpolate diffuse lighting while limiting sampling computation.
 *
 * Paper: Fast, Accurate Image-Based Lighting:
 * {@link https://drive.google.com/file/d/15y8r_UpKlU9SvV4ILb0C3qCPecS8pvLz/view}
*/
class PMREMGenerator {

	/**
	 * Constructs a new PMREM generator.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 */
	constructor( renderer ) {

		this._renderer = renderer;
		this._pingPongRenderTarget = null;

		this._lodMax = 0;
		this._cubeSize = 0;
		this._lodPlanes = [];
		this._sizeLods = [];
		this._sigmas = [];

		this._blurMaterial = null;
		this._cubemapMaterial = null;
		this._equirectMaterial = null;

		this._compileMaterial( this._blurMaterial );

	}

	/**
	 * Generates a PMREM from a supplied Scene, which can be faster than using an
	 * image if networking bandwidth is low. Optional sigma specifies a blur radius
	 * in radians to be applied to the scene before PMREM generation. Optional near
	 * and far planes ensure the scene is rendered in its entirety.
	 *
	 * @param {Scene} scene - The scene to be captured.
	 * @param {number} [sigma=0] - The blur radius in radians.
	 * @param {number} [near=0.1] - The near plane distance.
	 * @param {number} [far=100] - The far plane distance.
	 * @param {Object} [options={}] - The configuration options.
	 * @param {number} [options.size=256] - The texture size of the PMREM.
	 * @param {Vector3} [options.renderTarget=origin] - The position of the internal cube camera that renders the scene.
	 * @return {WebGLRenderTarget} The resulting PMREM.
	 */
	fromScene( scene, sigma = 0, near = 0.1, far = 100, options = {} ) {

		const {
			size = 256,
			position = _origin,
		} = options;

		_oldTarget = this._renderer.getRenderTarget();
		_oldActiveCubeFace = this._renderer.getActiveCubeFace();
		_oldActiveMipmapLevel = this._renderer.getActiveMipmapLevel();
		_oldXrEnabled = this._renderer.xr.enabled;

		this._renderer.xr.enabled = false;

		this._setSize( size );

		const cubeUVRenderTarget = this._allocateTargets();
		cubeUVRenderTarget.depthBuffer = true;

		this._sceneToCubeUV( scene, near, far, cubeUVRenderTarget, position );

		if ( sigma > 0 ) {

			this._blur( cubeUVRenderTarget, 0, 0, sigma );

		}

		this._applyPMREM( cubeUVRenderTarget );
		this._cleanup( cubeUVRenderTarget );

		return cubeUVRenderTarget;

	}

	/**
	 * Generates a PMREM from an equirectangular texture, which can be either LDR
	 * or HDR. The ideal input image size is 1k (1024 x 512),
	 * as this matches best with the 256 x 256 cubemap output.
	 *
	 * @param {Texture} equirectangular - The equirectangular texture to be converted.
	 * @param {?WebGLRenderTarget} [renderTarget=null] - The render target to use.
	 * @return {WebGLRenderTarget} The resulting PMREM.
	 */
	fromEquirectangular( equirectangular, renderTarget = null ) {

		return this._fromTexture( equirectangular, renderTarget );

	}

	/**
	 * Generates a PMREM from an cubemap texture, which can be either LDR
	 * or HDR. The ideal input cube size is 256 x 256,
	 * as this matches best with the 256 x 256 cubemap output.
	 *
	 * @param {Texture} cubemap - The cubemap texture to be converted.
	 * @param {?WebGLRenderTarget} [renderTarget=null] - The render target to use.
	 * @return {WebGLRenderTarget} The resulting PMREM.
	 */
	fromCubemap( cubemap, renderTarget = null ) {

		return this._fromTexture( cubemap, renderTarget );

	}

	/**
	 * Pre-compiles the cubemap shader. You can get faster start-up by invoking this method during
	 * your texture's network fetch for increased concurrency.
	 */
	compileCubemapShader() {

		if ( this._cubemapMaterial === null ) {

			this._cubemapMaterial = _getCubemapMaterial();
			this._compileMaterial( this._cubemapMaterial );

		}

	}

	/**
	 * Pre-compiles the equirectangular shader. You can get faster start-up by invoking this method during
	 * your texture's network fetch for increased concurrency.
	 */
	compileEquirectangularShader() {

		if ( this._equirectMaterial === null ) {

			this._equirectMaterial = _getEquirectMaterial();
			this._compileMaterial( this._equirectMaterial );

		}

	}

	/**
	 * Disposes of the PMREMGenerator's internal memory. Note that PMREMGenerator is a static class,
	 * so you should not need more than one PMREMGenerator object. If you do, calling dispose() on
	 * one of them will cause any others to also become unusable.
	 */
	dispose() {

		this._dispose();

		if ( this._cubemapMaterial !== null ) this._cubemapMaterial.dispose();
		if ( this._equirectMaterial !== null ) this._equirectMaterial.dispose();

	}

	// private interface

	_setSize( cubeSize ) {

		this._lodMax = Math.floor( Math.log2( cubeSize ) );
		this._cubeSize = Math.pow( 2, this._lodMax );

	}

	_dispose() {

		if ( this._blurMaterial !== null ) this._blurMaterial.dispose();

		if ( this._pingPongRenderTarget !== null ) this._pingPongRenderTarget.dispose();

		for ( let i = 0; i < this._lodPlanes.length; i ++ ) {

			this._lodPlanes[ i ].dispose();

		}

	}

	_cleanup( outputTarget ) {

		this._renderer.setRenderTarget( _oldTarget, _oldActiveCubeFace, _oldActiveMipmapLevel );
		this._renderer.xr.enabled = _oldXrEnabled;

		outputTarget.scissorTest = false;
		_setViewport( outputTarget, 0, 0, outputTarget.width, outputTarget.height );

	}

	_fromTexture( texture, renderTarget ) {

		if ( texture.mapping === CubeReflectionMapping || texture.mapping === CubeRefractionMapping ) {

			this._setSize( texture.image.length === 0 ? 16 : ( texture.image[ 0 ].width || texture.image[ 0 ].image.width ) );

		} else { // Equirectangular

			this._setSize( texture.image.width / 4 );

		}

		_oldTarget = this._renderer.getRenderTarget();
		_oldActiveCubeFace = this._renderer.getActiveCubeFace();
		_oldActiveMipmapLevel = this._renderer.getActiveMipmapLevel();
		_oldXrEnabled = this._renderer.xr.enabled;

		this._renderer.xr.enabled = false;

		const cubeUVRenderTarget = renderTarget || this._allocateTargets();
		this._textureToCubeUV( texture, cubeUVRenderTarget );
		this._applyPMREM( cubeUVRenderTarget );
		this._cleanup( cubeUVRenderTarget );

		return cubeUVRenderTarget;

	}

	_allocateTargets() {

		const width = 3 * Math.max( this._cubeSize, 16 * 7 );
		const height = 4 * this._cubeSize;

		const params = {
			magFilter: LinearFilter,
			minFilter: LinearFilter,
			generateMipmaps: false,
			type: HalfFloatType,
			format: RGBAFormat,
			colorSpace: LinearSRGBColorSpace,
			depthBuffer: false
		};

		const cubeUVRenderTarget = _createRenderTarget( width, height, params );

		if ( this._pingPongRenderTarget === null || this._pingPongRenderTarget.width !== width || this._pingPongRenderTarget.height !== height ) {

			if ( this._pingPongRenderTarget !== null ) {

				this._dispose();

			}

			this._pingPongRenderTarget = _createRenderTarget( width, height, params );

			const { _lodMax } = this;
			( { sizeLods: this._sizeLods, lodPlanes: this._lodPlanes, sigmas: this._sigmas } = _createPlanes( _lodMax ) );

			this._blurMaterial = _getBlurShader( _lodMax, width, height );

		}

		return cubeUVRenderTarget;

	}

	_compileMaterial( material ) {

		const tmpMesh = new Mesh( this._lodPlanes[ 0 ], material );
		this._renderer.compile( tmpMesh, _flatCamera );

	}

	_sceneToCubeUV( scene, near, far, cubeUVRenderTarget, position ) {

		const fov = 90;
		const aspect = 1;
		const cubeCamera = new PerspectiveCamera( fov, aspect, near, far );
		const upSign = [ 1, -1, 1, 1, 1, 1 ];
		const forwardSign = [ 1, 1, 1, -1, -1, -1 ];
		const renderer = this._renderer;

		const originalAutoClear = renderer.autoClear;
		const toneMapping = renderer.toneMapping;
		renderer.getClearColor( _clearColor );

		renderer.toneMapping = NoToneMapping;
		renderer.autoClear = false;

		// https://github.com/mrdoob/three.js/issues/31413#issuecomment-3095966812
		const reversedDepthBuffer = renderer.state.buffers.depth.getReversed();

		if ( reversedDepthBuffer ) {

			renderer.setRenderTarget( cubeUVRenderTarget );
			renderer.clearDepth();
			renderer.setRenderTarget( null );

		}

		const backgroundMaterial = new MeshBasicMaterial( {
			name: 'PMREM.Background',
			side: BackSide,
			depthWrite: false,
			depthTest: false,
		} );

		const backgroundBox = new Mesh( new BoxGeometry(), backgroundMaterial );

		let useSolidColor = false;
		const background = scene.background;

		if ( background ) {

			if ( background.isColor ) {

				backgroundMaterial.color.copy( background );
				scene.background = null;
				useSolidColor = true;

			}

		} else {

			backgroundMaterial.color.copy( _clearColor );
			useSolidColor = true;

		}

		for ( let i = 0; i < 6; i ++ ) {

			const col = i % 3;

			if ( col === 0 ) {

				cubeCamera.up.set( 0, upSign[ i ], 0 );
				cubeCamera.position.set( position.x, position.y, position.z );
				cubeCamera.lookAt( position.x + forwardSign[ i ], position.y, position.z );

			} else if ( col === 1 ) {

				cubeCamera.up.set( 0, 0, upSign[ i ] );
				cubeCamera.position.set( position.x, position.y, position.z );
				cubeCamera.lookAt( position.x, position.y + forwardSign[ i ], position.z );


			} else {

				cubeCamera.up.set( 0, upSign[ i ], 0 );
				cubeCamera.position.set( position.x, position.y, position.z );
				cubeCamera.lookAt( position.x, position.y, position.z + forwardSign[ i ] );

			}

			const size = this._cubeSize;

			_setViewport( cubeUVRenderTarget, col * size, i > 2 ? size : 0, size, size );

			renderer.setRenderTarget( cubeUVRenderTarget );

			if ( useSolidColor ) {

				renderer.render( backgroundBox, cubeCamera );

			}

			renderer.render( scene, cubeCamera );

		}

		backgroundBox.geometry.dispose();
		backgroundBox.material.dispose();

		renderer.toneMapping = toneMapping;
		renderer.autoClear = originalAutoClear;
		scene.background = background;

	}

	_textureToCubeUV( texture, cubeUVRenderTarget ) {

		const renderer = this._renderer;

		const isCubeTexture = ( texture.mapping === CubeReflectionMapping || texture.mapping === CubeRefractionMapping );

		if ( isCubeTexture ) {

			if ( this._cubemapMaterial === null ) {

				this._cubemapMaterial = _getCubemapMaterial();

			}

			this._cubemapMaterial.uniforms.flipEnvMap.value = ( texture.isRenderTargetTexture === false ) ? -1 : 1;

		} else {

			if ( this._equirectMaterial === null ) {

				this._equirectMaterial = _getEquirectMaterial();

			}

		}

		const material = isCubeTexture ? this._cubemapMaterial : this._equirectMaterial;
		const mesh = new Mesh( this._lodPlanes[ 0 ], material );

		const uniforms = material.uniforms;

		uniforms[ 'envMap' ].value = texture;

		const size = this._cubeSize;

		_setViewport( cubeUVRenderTarget, 0, 0, 3 * size, 2 * size );

		renderer.setRenderTarget( cubeUVRenderTarget );
		renderer.render( mesh, _flatCamera );

	}

	_applyPMREM( cubeUVRenderTarget ) {

		const renderer = this._renderer;
		const autoClear = renderer.autoClear;
		renderer.autoClear = false;
		const n = this._lodPlanes.length;

		for ( let i = 1; i < n; i ++ ) {

			const sigma = Math.sqrt( this._sigmas[ i ] * this._sigmas[ i ] - this._sigmas[ i - 1 ] * this._sigmas[ i - 1 ] );

			const poleAxis = _axisDirections[ ( n - i - 1 ) % _axisDirections.length ];

			this._blur( cubeUVRenderTarget, i - 1, i, sigma, poleAxis );

		}

		renderer.autoClear = autoClear;

	}

	/**
	 * This is a two-pass Gaussian blur for a cubemap. Normally this is done
	 * vertically and horizontally, but this breaks down on a cube. Here we apply
	 * the blur latitudinally (around the poles), and then longitudinally (towards
	 * the poles) to approximate the orthogonally-separable blur. It is least
	 * accurate at the poles, but still does a decent job.
	 *
	 * @private
	 * @param {WebGLRenderTarget} cubeUVRenderTarget
	 * @param {number} lodIn
	 * @param {number} lodOut
	 * @param {number} sigma
	 * @param {Vector3} [poleAxis]
	 */
	_blur( cubeUVRenderTarget, lodIn, lodOut, sigma, poleAxis ) {

		const pingPongRenderTarget = this._pingPongRenderTarget;

		this._halfBlur(
			cubeUVRenderTarget,
			pingPongRenderTarget,
			lodIn,
			lodOut,
			sigma,
			'latitudinal',
			poleAxis );

		this._halfBlur(
			pingPongRenderTarget,
			cubeUVRenderTarget,
			lodOut,
			lodOut,
			sigma,
			'longitudinal',
			poleAxis );

	}

	_halfBlur( targetIn, targetOut, lodIn, lodOut, sigmaRadians, direction, poleAxis ) {

		const renderer = this._renderer;
		const blurMaterial = this._blurMaterial;

		if ( direction !== 'latitudinal' && direction !== 'longitudinal' ) {

			console.error(
				'blur direction must be either latitudinal or longitudinal!' );

		}

		// Number of standard deviations at which to cut off the discrete approximation.
		const STANDARD_DEVIATIONS = 3;

		const blurMesh = new Mesh( this._lodPlanes[ lodOut ], blurMaterial );
		const blurUniforms = blurMaterial.uniforms;

		const pixels = this._sizeLods[ lodIn ] - 1;
		const radiansPerPixel = isFinite( sigmaRadians ) ? Math.PI / ( 2 * pixels ) : 2 * Math.PI / ( 2 * MAX_SAMPLES - 1 );
		const sigmaPixels = sigmaRadians / radiansPerPixel;
		const samples = isFinite( sigmaRadians ) ? 1 + Math.floor( STANDARD_DEVIATIONS * sigmaPixels ) : MAX_SAMPLES;

		if ( samples > MAX_SAMPLES ) {

			console.warn( `sigmaRadians, ${
				sigmaRadians}, is too large and will clip, as it requested ${
				samples} samples when the maximum is set to ${MAX_SAMPLES}` );

		}

		const weights = [];
		let sum = 0;

		for ( let i = 0; i < MAX_SAMPLES; ++ i ) {

			const x = i / sigmaPixels;
			const weight = Math.exp( - x * x / 2 );
			weights.push( weight );

			if ( i === 0 ) {

				sum += weight;

			} else if ( i < samples ) {

				sum += 2 * weight;

			}

		}

		for ( let i = 0; i < weights.length; i ++ ) {

			weights[ i ] = weights[ i ] / sum;

		}

		blurUniforms[ 'envMap' ].value = targetIn.texture;
		blurUniforms[ 'samples' ].value = samples;
		blurUniforms[ 'weights' ].value = weights;
		blurUniforms[ 'latitudinal' ].value = direction === 'latitudinal';

		if ( poleAxis ) {

			blurUniforms[ 'poleAxis' ].value = poleAxis;

		}

		const { _lodMax } = this;
		blurUniforms[ 'dTheta' ].value = radiansPerPixel;
		blurUniforms[ 'mipInt' ].value = _lodMax - lodIn;

		const outputSize = this._sizeLods[ lodOut ];
		const x = 3 * outputSize * ( lodOut > _lodMax - LOD_MIN ? lodOut - _lodMax + LOD_MIN : 0 );
		const y = 4 * ( this._cubeSize - outputSize );

		_setViewport( targetOut, x, y, 3 * outputSize, 2 * outputSize );
		renderer.setRenderTarget( targetOut );
		renderer.render( blurMesh, _flatCamera );

	}

}



function _createPlanes( lodMax ) {

	const lodPlanes = [];
	const sizeLods = [];
	const sigmas = [];

	let lod = lodMax;

	const totalLods = lodMax - LOD_MIN + 1 + EXTRA_LOD_SIGMA.length;

	for ( let i = 0; i < totalLods; i ++ ) {

		const sizeLod = Math.pow( 2, lod );
		sizeLods.push( sizeLod );
		let sigma = 1.0 / sizeLod;

		if ( i > lodMax - LOD_MIN ) {

			sigma = EXTRA_LOD_SIGMA[ i - lodMax + LOD_MIN - 1 ];

		} else if ( i === 0 ) {

			sigma = 0;

		}

		sigmas.push( sigma );

		const texelSize = 1.0 / ( sizeLod - 2 );
		const min = - texelSize;
		const max = 1 + texelSize;
		const uv1 = [ min, min, max, min, max, max, min, min, max, max, min, max ];

		const cubeFaces = 6;
		const vertices = 6;
		const positionSize = 3;
		const uvSize = 2;
		const faceIndexSize = 1;

		const position = new Float32Array( positionSize * vertices * cubeFaces );
		const uv = new Float32Array( uvSize * vertices * cubeFaces );
		const faceIndex = new Float32Array( faceIndexSize * vertices * cubeFaces );

		for ( let face = 0; face < cubeFaces; face ++ ) {

			const x = ( face % 3 ) * 2 / 3 - 1;
			const y = face > 2 ? 0 : -1;
			const coordinates = [
				x, y, 0,
				x + 2 / 3, y, 0,
				x + 2 / 3, y + 1, 0,
				x, y, 0,
				x + 2 / 3, y + 1, 0,
				x, y + 1, 0
			];
			position.set( coordinates, positionSize * vertices * face );
			uv.set( uv1, uvSize * vertices * face );
			const fill = [ face, face, face, face, face, face ];
			faceIndex.set( fill, faceIndexSize * vertices * face );

		}

		const planes = new BufferGeometry();
		planes.setAttribute( 'position', new BufferAttribute( position, positionSize ) );
		planes.setAttribute( 'uv', new BufferAttribute( uv, uvSize ) );
		planes.setAttribute( 'faceIndex', new BufferAttribute( faceIndex, faceIndexSize ) );
		lodPlanes.push( planes );

		if ( lod > LOD_MIN ) {

			lod --;

		}

	}

	return { lodPlanes, sizeLods, sigmas };

}

function _createRenderTarget( width, height, params ) {

	const cubeUVRenderTarget = new WebGLRenderTarget( width, height, params );
	cubeUVRenderTarget.texture.mapping = CubeUVReflectionMapping;
	cubeUVRenderTarget.texture.name = 'PMREM.cubeUv';
	cubeUVRenderTarget.scissorTest = true;
	return cubeUVRenderTarget;

}

function _setViewport( target, x, y, width, height ) {

	target.viewport.set( x, y, width, height );
	target.scissor.set( x, y, width, height );

}

function _getBlurShader( lodMax, width, height ) {

	const weights = new Float32Array( MAX_SAMPLES );
	const poleAxis = new Vector3( 0, 1, 0 );
	const shaderMaterial = new ShaderMaterial( {

		name: 'SphericalGaussianBlur',

		defines: {
			'n': MAX_SAMPLES,
			'CUBEUV_TEXEL_WIDTH': 1.0 / width,
			'CUBEUV_TEXEL_HEIGHT': 1.0 / height,
			'CUBEUV_MAX_MIP': `${lodMax}.0`,
		},

		uniforms: {
			'envMap': { value: null },
			'samples': { value: 1 },
			'weights': { value: weights },
			'latitudinal': { value: false },
			'dTheta': { value: 0 },
			'mipInt': { value: 0 },
			'poleAxis': { value: poleAxis }
		},

		vertexShader: _getCommonVertexShader(),

		fragmentShader: /* glsl */`

			precision mediump float;
			precision mediump int;

			varying vec3 vOutputDirection;

			uniform sampler2D envMap;
			uniform int samples;
			uniform float weights[ n ];
			uniform bool latitudinal;
			uniform float dTheta;
			uniform float mipInt;
			uniform vec3 poleAxis;

			#define ENVMAP_TYPE_CUBE_UV
			#include <cube_uv_reflection_fragment>

			vec3 getSample( float theta, vec3 axis ) {

				float cosTheta = cos( theta );
				// Rodrigues' axis-angle rotation
				vec3 sampleDirection = vOutputDirection * cosTheta
					+ cross( axis, vOutputDirection ) * sin( theta )
					+ axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta );

				return bilinearCubeUV( envMap, sampleDirection, mipInt );

			}

			void main() {

				vec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection );

				if ( all( equal( axis, vec3( 0.0 ) ) ) ) {

					axis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x );

				}

				axis = normalize( axis );

				gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );
				gl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis );

				for ( int i = 1; i < n; i++ ) {

					if ( i >= samples ) {

						break;

					}

					float theta = dTheta * float( i );
					gl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis );
					gl_FragColor.rgb += weights[ i ] * getSample( theta, axis );

				}

			}
		`,

		blending: NoBlending,
		depthTest: false,
		depthWrite: false

	} );

	return shaderMaterial;

}

function _getEquirectMaterial() {

	return new ShaderMaterial( {

		name: 'EquirectangularToCubeUV',

		uniforms: {
			'envMap': { value: null }
		},

		vertexShader: _getCommonVertexShader(),

		fragmentShader: /* glsl */`

			precision mediump float;
			precision mediump int;

			varying vec3 vOutputDirection;

			uniform sampler2D envMap;

			#include <common>

			void main() {

				vec3 outputDirection = normalize( vOutputDirection );
				vec2 uv = equirectUv( outputDirection );

				gl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 );

			}
		`,

		blending: NoBlending,
		depthTest: false,
		depthWrite: false

	} );

}

function _getCubemapMaterial() {

	return new ShaderMaterial( {

		name: 'CubemapToCubeUV',

		uniforms: {
			'envMap': { value: null },
			'flipEnvMap': { value: -1 }
		},

		vertexShader: _getCommonVertexShader(),

		fragmentShader: /* glsl */`

			precision mediump float;
			precision mediump int;

			uniform float flipEnvMap;

			varying vec3 vOutputDirection;

			uniform samplerCube envMap;

			void main() {

				gl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) );

			}
		`,

		blending: NoBlending,
		depthTest: false,
		depthWrite: false

	} );

}

function _getCommonVertexShader() {

	return /* glsl */`

		precision mediump float;
		precision mediump int;

		attribute float faceIndex;

		varying vec3 vOutputDirection;

		// RH coordinate system; PMREM face-indexing convention
		vec3 getDirection( vec2 uv, float face ) {

			uv = 2.0 * uv - 1.0;

			vec3 direction = vec3( uv, 1.0 );

			if ( face == 0.0 ) {

				direction = direction.zyx; // ( 1, v, u ) pos x

			} else if ( face == 1.0 ) {

				direction = direction.xzy;
				direction.xz *= -1.0; // ( -u, 1, -v ) pos y

			} else if ( face == 2.0 ) {

				direction.x *= -1.0; // ( -u, v, 1 ) pos z

			} else if ( face == 3.0 ) {

				direction = direction.zyx;
				direction.xz *= -1.0; // ( -1, v, -u ) neg x

			} else if ( face == 4.0 ) {

				direction = direction.xzy;
				direction.xy *= -1.0; // ( -u, -1, v ) neg y

			} else if ( face == 5.0 ) {

				direction.z *= -1.0; // ( u, v, -1 ) neg z

			}

			return direction;

		}

		void main() {

			vOutputDirection = getDirection( uv, faceIndex );
			gl_Position = vec4( position, 1.0 );

		}
	`;

}

function WebGLCubeUVMaps( renderer ) {

	let cubeUVmaps = new WeakMap();

	let pmremGenerator = null;

	function get( texture ) {

		if ( texture && texture.isTexture ) {

			const mapping = texture.mapping;

			const isEquirectMap = ( mapping === EquirectangularReflectionMapping || mapping === EquirectangularRefractionMapping );
			const isCubeMap = ( mapping === CubeReflectionMapping || mapping === CubeRefractionMapping );

			// equirect/cube map to cubeUV conversion

			if ( isEquirectMap || isCubeMap ) {

				let renderTarget = cubeUVmaps.get( texture );

				const currentPMREMVersion = renderTarget !== undefined ? renderTarget.texture.pmremVersion : 0;

				if ( texture.isRenderTargetTexture && texture.pmremVersion !== currentPMREMVersion ) {

					if ( pmremGenerator === null ) pmremGenerator = new PMREMGenerator( renderer );

					renderTarget = isEquirectMap ? pmremGenerator.fromEquirectangular( texture, renderTarget ) : pmremGenerator.fromCubemap( texture, renderTarget );
					renderTarget.texture.pmremVersion = texture.pmremVersion;

					cubeUVmaps.set( texture, renderTarget );

					return renderTarget.texture;

				} else {

					if ( renderTarget !== undefined ) {

						return renderTarget.texture;

					} else {

						const image = texture.image;

						if ( ( isEquirectMap && image && image.height > 0 ) || ( isCubeMap && image && isCubeTextureComplete( image ) ) ) {

							if ( pmremGenerator === null ) pmremGenerator = new PMREMGenerator( renderer );

							renderTarget = isEquirectMap ? pmremGenerator.fromEquirectangular( texture ) : pmremGenerator.fromCubemap( texture );
							renderTarget.texture.pmremVersion = texture.pmremVersion;

							cubeUVmaps.set( texture, renderTarget );

							texture.addEventListener( 'dispose', onTextureDispose );

							return renderTarget.texture;

						} else {

							// image not yet ready. try the conversion next frame

							return null;

						}

					}

				}

			}

		}

		return texture;

	}

	function isCubeTextureComplete( image ) {

		let count = 0;
		const length = 6;

		for ( let i = 0; i < length; i ++ ) {

			if ( image[ i ] !== undefined ) count ++;

		}

		return count === length;


	}

	function onTextureDispose( event ) {

		const texture = event.target;

		texture.removeEventListener( 'dispose', onTextureDispose );

		const cubemapUV = cubeUVmaps.get( texture );

		if ( cubemapUV !== undefined ) {

			cubeUVmaps.delete( texture );
			cubemapUV.dispose();

		}

	}

	function dispose() {

		cubeUVmaps = new WeakMap();

		if ( pmremGenerator !== null ) {

			pmremGenerator.dispose();
			pmremGenerator = null;

		}

	}

	return {
		get: get,
		dispose: dispose
	};

}

function WebGLExtensions( gl ) {

	const extensions = {};

	function getExtension( name ) {

		if ( extensions[ name ] !== undefined ) {

			return extensions[ name ];

		}

		let extension;

		switch ( name ) {

			case 'WEBGL_depth_texture':
				extension = gl.getExtension( 'WEBGL_depth_texture' ) || gl.getExtension( 'MOZ_WEBGL_depth_texture' ) || gl.getExtension( 'WEBKIT_WEBGL_depth_texture' );
				break;

			case 'EXT_texture_filter_anisotropic':
				extension = gl.getExtension( 'EXT_texture_filter_anisotropic' ) || gl.getExtension( 'MOZ_EXT_texture_filter_anisotropic' ) || gl.getExtension( 'WEBKIT_EXT_texture_filter_anisotropic' );
				break;

			case 'WEBGL_compressed_texture_s3tc':
				extension = gl.getExtension( 'WEBGL_compressed_texture_s3tc' ) || gl.getExtension( 'MOZ_WEBGL_compressed_texture_s3tc' ) || gl.getExtension( 'WEBKIT_WEBGL_compressed_texture_s3tc' );
				break;

			case 'WEBGL_compressed_texture_pvrtc':
				extension = gl.getExtension( 'WEBGL_compressed_texture_pvrtc' ) || gl.getExtension( 'WEBKIT_WEBGL_compressed_texture_pvrtc' );
				break;

			default:
				extension = gl.getExtension( name );

		}

		extensions[ name ] = extension;

		return extension;

	}

	return {

		has: function ( name ) {

			return getExtension( name ) !== null;

		},

		init: function () {

			getExtension( 'EXT_color_buffer_float' );
			getExtension( 'WEBGL_clip_cull_distance' );
			getExtension( 'OES_texture_float_linear' );
			getExtension( 'EXT_color_buffer_half_float' );
			getExtension( 'WEBGL_multisampled_render_to_texture' );
			getExtension( 'WEBGL_render_shared_exponent' );

		},

		get: function ( name ) {

			const extension = getExtension( name );

			if ( extension === null ) {

				warnOnce( 'THREE.WebGLRenderer: ' + name + ' extension not supported.' );

			}

			return extension;

		}

	};

}

function WebGLGeometries( gl, attributes, info, bindingStates ) {

	const geometries = {};
	const wireframeAttributes = new WeakMap();

	function onGeometryDispose( event ) {

		const geometry = event.target;

		if ( geometry.index !== null ) {

			attributes.remove( geometry.index );

		}

		for ( const name in geometry.attributes ) {

			attributes.remove( geometry.attributes[ name ] );

		}

		geometry.removeEventListener( 'dispose', onGeometryDispose );

		delete geometries[ geometry.id ];

		const attribute = wireframeAttributes.get( geometry );

		if ( attribute ) {

			attributes.remove( attribute );
			wireframeAttributes.delete( geometry );

		}

		bindingStates.releaseStatesOfGeometry( geometry );

		if ( geometry.isInstancedBufferGeometry === true ) {

			delete geometry._maxInstanceCount;

		}

		//

		info.memory.geometries --;

	}

	function get( object, geometry ) {

		if ( geometries[ geometry.id ] === true ) return geometry;

		geometry.addEventListener( 'dispose', onGeometryDispose );

		geometries[ geometry.id ] = true;

		info.memory.geometries ++;

		return geometry;

	}

	function update( geometry ) {

		const geometryAttributes = geometry.attributes;

		// Updating index buffer in VAO now. See WebGLBindingStates.

		for ( const name in geometryAttributes ) {

			attributes.update( geometryAttributes[ name ], gl.ARRAY_BUFFER );

		}

	}

	function updateWireframeAttribute( geometry ) {

		const indices = [];

		const geometryIndex = geometry.index;
		const geometryPosition = geometry.attributes.position;
		let version = 0;

		if ( geometryIndex !== null ) {

			const array = geometryIndex.array;
			version = geometryIndex.version;

			for ( let i = 0, l = array.length; i < l; i += 3 ) {

				const a = array[ i + 0 ];
				const b = array[ i + 1 ];
				const c = array[ i + 2 ];

				indices.push( a, b, b, c, c, a );

			}

		} else if ( geometryPosition !== undefined ) {

			const array = geometryPosition.array;
			version = geometryPosition.version;

			for ( let i = 0, l = ( array.length / 3 ) - 1; i < l; i += 3 ) {

				const a = i + 0;
				const b = i + 1;
				const c = i + 2;

				indices.push( a, b, b, c, c, a );

			}

		} else {

			return;

		}

		const attribute = new ( arrayNeedsUint32( indices ) ? Uint32BufferAttribute : Uint16BufferAttribute )( indices, 1 );
		attribute.version = version;

		// Updating index buffer in VAO now. See WebGLBindingStates

		//

		const previousAttribute = wireframeAttributes.get( geometry );

		if ( previousAttribute ) attributes.remove( previousAttribute );

		//

		wireframeAttributes.set( geometry, attribute );

	}

	function getWireframeAttribute( geometry ) {

		const currentAttribute = wireframeAttributes.get( geometry );

		if ( currentAttribute ) {

			const geometryIndex = geometry.index;

			if ( geometryIndex !== null ) {

				// if the attribute is obsolete, create a new one

				if ( currentAttribute.version < geometryIndex.version ) {

					updateWireframeAttribute( geometry );

				}

			}

		} else {

			updateWireframeAttribute( geometry );

		}

		return wireframeAttributes.get( geometry );

	}

	return {

		get: get,
		update: update,

		getWireframeAttribute: getWireframeAttribute

	};

}

function WebGLIndexedBufferRenderer( gl, extensions, info ) {

	let mode;

	function setMode( value ) {

		mode = value;

	}

	let type, bytesPerElement;

	function setIndex( value ) {

		type = value.type;
		bytesPerElement = value.bytesPerElement;

	}

	function render( start, count ) {

		gl.drawElements( mode, count, type, start * bytesPerElement );

		info.update( count, mode, 1 );

	}

	function renderInstances( start, count, primcount ) {

		if ( primcount === 0 ) return;

		gl.drawElementsInstanced( mode, count, type, start * bytesPerElement, primcount );

		info.update( count, mode, primcount );

	}

	function renderMultiDraw( starts, counts, drawCount ) {

		if ( drawCount === 0 ) return;

		const extension = extensions.get( 'WEBGL_multi_draw' );
		extension.multiDrawElementsWEBGL( mode, counts, 0, type, starts, 0, drawCount );

		let elementCount = 0;
		for ( let i = 0; i < drawCount; i ++ ) {

			elementCount += counts[ i ];

		}

		info.update( elementCount, mode, 1 );


	}

	function renderMultiDrawInstances( starts, counts, drawCount, primcount ) {

		if ( drawCount === 0 ) return;

		const extension = extensions.get( 'WEBGL_multi_draw' );

		if ( extension === null ) {

			for ( let i = 0; i < starts.length; i ++ ) {

				renderInstances( starts[ i ] / bytesPerElement, counts[ i ], primcount[ i ] );

			}

		} else {

			extension.multiDrawElementsInstancedWEBGL( mode, counts, 0, type, starts, 0, primcount, 0, drawCount );

			let elementCount = 0;
			for ( let i = 0; i < drawCount; i ++ ) {

				elementCount += counts[ i ] * primcount[ i ];

			}

			info.update( elementCount, mode, 1 );

		}

	}

	//

	this.setMode = setMode;
	this.setIndex = setIndex;
	this.render = render;
	this.renderInstances = renderInstances;
	this.renderMultiDraw = renderMultiDraw;
	this.renderMultiDrawInstances = renderMultiDrawInstances;

}

function WebGLInfo( gl ) {

	const memory = {
		geometries: 0,
		textures: 0
	};

	const render = {
		frame: 0,
		calls: 0,
		triangles: 0,
		points: 0,
		lines: 0
	};

	function update( count, mode, instanceCount ) {

		render.calls ++;

		switch ( mode ) {

			case gl.TRIANGLES:
				render.triangles += instanceCount * ( count / 3 );
				break;

			case gl.LINES:
				render.lines += instanceCount * ( count / 2 );
				break;

			case gl.LINE_STRIP:
				render.lines += instanceCount * ( count - 1 );
				break;

			case gl.LINE_LOOP:
				render.lines += instanceCount * count;
				break;

			case gl.POINTS:
				render.points += instanceCount * count;
				break;

			default:
				console.error( 'THREE.WebGLInfo: Unknown draw mode:', mode );
				break;

		}

	}

	function reset() {

		render.calls = 0;
		render.triangles = 0;
		render.points = 0;
		render.lines = 0;

	}

	return {
		memory: memory,
		render: render,
		programs: null,
		autoReset: true,
		reset: reset,
		update: update
	};

}

/**
 * Creates an array of textures directly from raw buffer data.
 *
 * @augments Texture
 */
class DataArrayTexture extends Texture {

	/**
	 * Constructs a new data array texture.
	 *
	 * @param {?TypedArray} [data=null] - The buffer data.
	 * @param {number} [width=1] - The width of the texture.
	 * @param {number} [height=1] - The height of the texture.
	 * @param {number} [depth=1] - The depth of the texture.
	 */
	constructor( data = null, width = 1, height = 1, depth = 1 ) {

		super( null );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isDataArrayTexture = true;

		/**
		 * The image definition of a data texture.
		 *
		 * @type {{data:TypedArray,width:number,height:number,depth:number}}
		 */
		this.image = { data, width, height, depth };

		/**
		 * How the texture is sampled when a texel covers more than one pixel.
		 *
		 * Overwritten and set to `NearestFilter` by default.
		 *
		 * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
		 * @default NearestFilter
		 */
		this.magFilter = NearestFilter;

		/**
		 * How the texture is sampled when a texel covers less than one pixel.
		 *
		 * Overwritten and set to `NearestFilter` by default.
		 *
		 * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
		 * @default NearestFilter
		 */
		this.minFilter = NearestFilter;

		/**
		 * This defines how the texture is wrapped in the depth and corresponds to
		 * *W* in UVW mapping.
		 *
		 * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}
		 * @default ClampToEdgeWrapping
		 */
		this.wrapR = ClampToEdgeWrapping;

		/**
		 * Whether to generate mipmaps (if possible) for a texture.
		 *
		 * Overwritten and set to `false` by default.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.generateMipmaps = false;

		/**
		 * If set to `true`, the texture is flipped along the vertical axis when
		 * uploaded to the GPU.
		 *
		 * Overwritten and set to `false` by default.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.flipY = false;

		/**
		 * Specifies the alignment requirements for the start of each pixel row in memory.
		 *
		 * Overwritten and set to `1` by default.
		 *
		 * @type {boolean}
		 * @default 1
		 */
		this.unpackAlignment = 1;

		/**
		 * A set of all layers which need to be updated in the texture.
		 *
		 * @type {Set<number>}
		 */
		this.layerUpdates = new Set();

	}

	/**
	 * Describes that a specific layer of the texture needs to be updated.
	 * Normally when {@link Texture#needsUpdate} is set to `true`, the
	 * entire data texture array is sent to the GPU. Marking specific
	 * layers will only transmit subsets of all mipmaps associated with a
	 * specific depth in the array which is often much more performant.
	 *
	 * @param {number} layerIndex - The layer index that should be updated.
	 */
	addLayerUpdate( layerIndex ) {

		this.layerUpdates.add( layerIndex );

	}

	/**
	 * Resets the layer updates registry.
	 */
	clearLayerUpdates() {

		this.layerUpdates.clear();

	}

}

function WebGLMorphtargets( gl, capabilities, textures ) {

	const morphTextures = new WeakMap();
	const morph = new Vector4();

	function update( object, geometry, program ) {

		const objectInfluences = object.morphTargetInfluences;

		// the following encodes morph targets into an array of data textures. Each layer represents a single morph target.

		const morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;
		const morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;

		let entry = morphTextures.get( geometry );

		if ( entry === undefined || entry.count !== morphTargetsCount ) {

			if ( entry !== undefined ) entry.texture.dispose();

			const hasMorphPosition = geometry.morphAttributes.position !== undefined;
			const hasMorphNormals = geometry.morphAttributes.normal !== undefined;
			const hasMorphColors = geometry.morphAttributes.color !== undefined;

			const morphTargets = geometry.morphAttributes.position || [];
			const morphNormals = geometry.morphAttributes.normal || [];
			const morphColors = geometry.morphAttributes.color || [];

			let vertexDataCount = 0;

			if ( hasMorphPosition === true ) vertexDataCount = 1;
			if ( hasMorphNormals === true ) vertexDataCount = 2;
			if ( hasMorphColors === true ) vertexDataCount = 3;

			let width = geometry.attributes.position.count * vertexDataCount;
			let height = 1;

			if ( width > capabilities.maxTextureSize ) {

				height = Math.ceil( width / capabilities.maxTextureSize );
				width = capabilities.maxTextureSize;

			}

			const buffer = new Float32Array( width * height * 4 * morphTargetsCount );

			const texture = new DataArrayTexture( buffer, width, height, morphTargetsCount );
			texture.type = FloatType;
			texture.needsUpdate = true;

			// fill buffer

			const vertexDataStride = vertexDataCount * 4;

			for ( let i = 0; i < morphTargetsCount; i ++ ) {

				const morphTarget = morphTargets[ i ];
				const morphNormal = morphNormals[ i ];
				const morphColor = morphColors[ i ];

				const offset = width * height * 4 * i;

				for ( let j = 0; j < morphTarget.count; j ++ ) {

					const stride = j * vertexDataStride;

					if ( hasMorphPosition === true ) {

						morph.fromBufferAttribute( morphTarget, j );

						buffer[ offset + stride + 0 ] = morph.x;
						buffer[ offset + stride + 1 ] = morph.y;
						buffer[ offset + stride + 2 ] = morph.z;
						buffer[ offset + stride + 3 ] = 0;

					}

					if ( hasMorphNormals === true ) {

						morph.fromBufferAttribute( morphNormal, j );

						buffer[ offset + stride + 4 ] = morph.x;
						buffer[ offset + stride + 5 ] = morph.y;
						buffer[ offset + stride + 6 ] = morph.z;
						buffer[ offset + stride + 7 ] = 0;

					}

					if ( hasMorphColors === true ) {

						morph.fromBufferAttribute( morphColor, j );

						buffer[ offset + stride + 8 ] = morph.x;
						buffer[ offset + stride + 9 ] = morph.y;
						buffer[ offset + stride + 10 ] = morph.z;
						buffer[ offset + stride + 11 ] = ( morphColor.itemSize === 4 ) ? morph.w : 1;

					}

				}

			}

			entry = {
				count: morphTargetsCount,
				texture: texture,
				size: new Vector2( width, height )
			};

			morphTextures.set( geometry, entry );

			function disposeTexture() {

				texture.dispose();

				morphTextures.delete( geometry );

				geometry.removeEventListener( 'dispose', disposeTexture );

			}

			geometry.addEventListener( 'dispose', disposeTexture );

		}

		//
		if ( object.isInstancedMesh === true && object.morphTexture !== null ) {

			program.getUniforms().setValue( gl, 'morphTexture', object.morphTexture, textures );

		} else {

			let morphInfluencesSum = 0;

			for ( let i = 0; i < objectInfluences.length; i ++ ) {

				morphInfluencesSum += objectInfluences[ i ];

			}

			const morphBaseInfluence = geometry.morphTargetsRelative ? 1 : 1 - morphInfluencesSum;


			program.getUniforms().setValue( gl, 'morphTargetBaseInfluence', morphBaseInfluence );
			program.getUniforms().setValue( gl, 'morphTargetInfluences', objectInfluences );

		}

		program.getUniforms().setValue( gl, 'morphTargetsTexture', entry.texture, textures );
		program.getUniforms().setValue( gl, 'morphTargetsTextureSize', entry.size );

	}

	return {

		update: update

	};

}

function WebGLObjects( gl, geometries, attributes, info ) {

	let updateMap = new WeakMap();

	function update( object ) {

		const frame = info.render.frame;

		const geometry = object.geometry;
		const buffergeometry = geometries.get( object, geometry );

		// Update once per frame

		if ( updateMap.get( buffergeometry ) !== frame ) {

			geometries.update( buffergeometry );

			updateMap.set( buffergeometry, frame );

		}

		if ( object.isInstancedMesh ) {

			if ( object.hasEventListener( 'dispose', onInstancedMeshDispose ) === false ) {

				object.addEventListener( 'dispose', onInstancedMeshDispose );

			}

			if ( updateMap.get( object ) !== frame ) {

				attributes.update( object.instanceMatrix, gl.ARRAY_BUFFER );

				if ( object.instanceColor !== null ) {

					attributes.update( object.instanceColor, gl.ARRAY_BUFFER );

				}

				updateMap.set( object, frame );

			}

		}

		if ( object.isSkinnedMesh ) {

			const skeleton = object.skeleton;

			if ( updateMap.get( skeleton ) !== frame ) {

				skeleton.update();

				updateMap.set( skeleton, frame );

			}

		}

		return buffergeometry;

	}

	function dispose() {

		updateMap = new WeakMap();

	}

	function onInstancedMeshDispose( event ) {

		const instancedMesh = event.target;

		instancedMesh.removeEventListener( 'dispose', onInstancedMeshDispose );

		attributes.remove( instancedMesh.instanceMatrix );

		if ( instancedMesh.instanceColor !== null ) attributes.remove( instancedMesh.instanceColor );

	}

	return {

		update: update,
		dispose: dispose

	};

}

/**
 * Creates a three-dimensional texture from raw data, with parameters to
 * divide it into width, height, and depth.
 *
 * @augments Texture
 */
class Data3DTexture extends Texture {

	/**
	 * Constructs a new data array texture.
	 *
	 * @param {?TypedArray} [data=null] - The buffer data.
	 * @param {number} [width=1] - The width of the texture.
	 * @param {number} [height=1] - The height of the texture.
	 * @param {number} [depth=1] - The depth of the texture.
	 */
	constructor( data = null, width = 1, height = 1, depth = 1 ) {

		// We're going to add .setXXX() methods for setting properties later.
		// Users can still set in Data3DTexture directly.
		//
		//	const texture = new THREE.Data3DTexture( data, width, height, depth );
		// 	texture.anisotropy = 16;
		//
		// See #14839

		super( null );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isData3DTexture = true;

		/**
		 * The image definition of a data texture.
		 *
		 * @type {{data:TypedArray,width:number,height:number,depth:number}}
		 */
		this.image = { data, width, height, depth };

		/**
		 * How the texture is sampled when a texel covers more than one pixel.
		 *
		 * Overwritten and set to `NearestFilter` by default.
		 *
		 * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
		 * @default NearestFilter
		 */
		this.magFilter = NearestFilter;

		/**
		 * How the texture is sampled when a texel covers less than one pixel.
		 *
		 * Overwritten and set to `NearestFilter` by default.
		 *
		 * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
		 * @default NearestFilter
		 */
		this.minFilter = NearestFilter;

		/**
		 * This defines how the texture is wrapped in the depth and corresponds to
		 * *W* in UVW mapping.
		 *
		 * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}
		 * @default ClampToEdgeWrapping
		 */
		this.wrapR = ClampToEdgeWrapping;

		/**
		 * Whether to generate mipmaps (if possible) for a texture.
		 *
		 * Overwritten and set to `false` by default.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.generateMipmaps = false;

		/**
		 * If set to `true`, the texture is flipped along the vertical axis when
		 * uploaded to the GPU.
		 *
		 * Overwritten and set to `false` by default.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.flipY = false;

		/**
		 * Specifies the alignment requirements for the start of each pixel row in memory.
		 *
		 * Overwritten and set to `1` by default.
		 *
		 * @type {boolean}
		 * @default 1
		 */
		this.unpackAlignment = 1;

	}

}

/**
 * This class can be used to automatically save the depth information of a
 * rendering into a texture.
 *
 * @augments Texture
 */
class DepthTexture extends Texture {

	/**
	 * Constructs a new depth texture.
	 *
	 * @param {number} width - The width of the texture.
	 * @param {number} height - The height of the texture.
	 * @param {number} [type=UnsignedIntType] - The texture type.
	 * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.
	 * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
	 * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
	 * @param {number} [magFilter=LinearFilter] - The mag filter value.
	 * @param {number} [minFilter=LinearFilter] - The min filter value.
	 * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
	 * @param {number} [format=DepthFormat] - The texture format.
	 * @param {number} [depth=1] - The depth of the texture.
	 */
	constructor( width, height, type = UnsignedIntType, mapping, wrapS, wrapT, magFilter = NearestFilter, minFilter = NearestFilter, anisotropy, format = DepthFormat, depth = 1 ) {

		if ( format !== DepthFormat && format !== DepthStencilFormat ) {

			throw new Error( 'DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat' );

		}

		const image = { width: width, height: height, depth: depth };

		super( image, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isDepthTexture = true;

		/**
		 * If set to `true`, the texture is flipped along the vertical axis when
		 * uploaded to the GPU.
		 *
		 * Overwritten and set to `false` by default.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.flipY = false;

		/**
		 * Whether to generate mipmaps (if possible) for a texture.
		 *
		 * Overwritten and set to `false` by default.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.generateMipmaps = false;

		/**
		 * Code corresponding to the depth compare function.
		 *
		 * @type {?(NeverCompare|LessCompare|EqualCompare|LessEqualCompare|GreaterCompare|NotEqualCompare|GreaterEqualCompare|AlwaysCompare)}
		 * @default null
		 */
		this.compareFunction = null;

	}


	copy( source ) {

		super.copy( source );

		this.source = new Source( Object.assign( {}, source.image ) ); // see #30540
		this.compareFunction = source.compareFunction;

		return this;

	}

	toJSON( meta ) {

		const data = super.toJSON( meta );

		if ( this.compareFunction !== null ) data.compareFunction = this.compareFunction;

		return data;

	}

}

/**
 * Uniforms of a program.
 * Those form a tree structure with a special top-level container for the root,
 * which you get by calling 'new WebGLUniforms( gl, program )'.
 *
 *
 * Properties of inner nodes including the top-level container:
 *
 * .seq - array of nested uniforms
 * .map - nested uniforms by name
 *
 *
 * Methods of all nodes except the top-level container:
 *
 * .setValue( gl, value, [textures] )
 *
 * 		uploads a uniform value(s)
 *  	the 'textures' parameter is needed for sampler uniforms
 *
 *
 * Static methods of the top-level container (textures factorizations):
 *
 * .upload( gl, seq, values, textures )
 *
 * 		sets uniforms in 'seq' to 'values[id].value'
 *
 * .seqWithValue( seq, values ) : filteredSeq
 *
 * 		filters 'seq' entries with corresponding entry in values
 *
 *
 * Methods of the top-level container (textures factorizations):
 *
 * .setValue( gl, name, value, textures )
 *
 * 		sets uniform with  name 'name' to 'value'
 *
 * .setOptional( gl, obj, prop )
 *
 * 		like .set for an optional property of the object
 *
 */


const emptyTexture = /*@__PURE__*/ new Texture();

const emptyShadowTexture = /*@__PURE__*/ new DepthTexture( 1, 1 );

const emptyArrayTexture = /*@__PURE__*/ new DataArrayTexture();
const empty3dTexture = /*@__PURE__*/ new Data3DTexture();
const emptyCubeTexture = /*@__PURE__*/ new CubeTexture();

// --- Utilities ---

// Array Caches (provide typed arrays for temporary by size)

const arrayCacheF32 = [];
const arrayCacheI32 = [];

// Float32Array caches used for uploading Matrix uniforms

const mat4array = new Float32Array( 16 );
const mat3array = new Float32Array( 9 );
const mat2array = new Float32Array( 4 );

// Flattening for arrays of vectors and matrices

function flatten( array, nBlocks, blockSize ) {

	const firstElem = array[ 0 ];

	if ( firstElem <= 0 || firstElem > 0 ) return array;
	// unoptimized: ! isNaN( firstElem )
	// see http://jacksondunstan.com/articles/983

	const n = nBlocks * blockSize;
	let r = arrayCacheF32[ n ];

	if ( r === undefined ) {

		r = new Float32Array( n );
		arrayCacheF32[ n ] = r;

	}

	if ( nBlocks !== 0 ) {

		firstElem.toArray( r, 0 );

		for ( let i = 1, offset = 0; i !== nBlocks; ++ i ) {

			offset += blockSize;
			array[ i ].toArray( r, offset );

		}

	}

	return r;

}

function arraysEqual( a, b ) {

	if ( a.length !== b.length ) return false;

	for ( let i = 0, l = a.length; i < l; i ++ ) {

		if ( a[ i ] !== b[ i ] ) return false;

	}

	return true;

}

function copyArray( a, b ) {

	for ( let i = 0, l = b.length; i < l; i ++ ) {

		a[ i ] = b[ i ];

	}

}

// Texture unit allocation

function allocTexUnits( textures, n ) {

	let r = arrayCacheI32[ n ];

	if ( r === undefined ) {

		r = new Int32Array( n );
		arrayCacheI32[ n ] = r;

	}

	for ( let i = 0; i !== n; ++ i ) {

		r[ i ] = textures.allocateTextureUnit();

	}

	return r;

}

// --- Setters ---

// Note: Defining these methods externally, because they come in a bunch
// and this way their names minify.

// Single scalar

function setValueV1f( gl, v ) {

	const cache = this.cache;

	if ( cache[ 0 ] === v ) return;

	gl.uniform1f( this.addr, v );

	cache[ 0 ] = v;

}

// Single float vector (from flat array or THREE.VectorN)

function setValueV2f( gl, v ) {

	const cache = this.cache;

	if ( v.x !== undefined ) {

		if ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y ) {

			gl.uniform2f( this.addr, v.x, v.y );

			cache[ 0 ] = v.x;
			cache[ 1 ] = v.y;

		}

	} else {

		if ( arraysEqual( cache, v ) ) return;

		gl.uniform2fv( this.addr, v );

		copyArray( cache, v );

	}

}

function setValueV3f( gl, v ) {

	const cache = this.cache;

	if ( v.x !== undefined ) {

		if ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z ) {

			gl.uniform3f( this.addr, v.x, v.y, v.z );

			cache[ 0 ] = v.x;
			cache[ 1 ] = v.y;
			cache[ 2 ] = v.z;

		}

	} else if ( v.r !== undefined ) {

		if ( cache[ 0 ] !== v.r || cache[ 1 ] !== v.g || cache[ 2 ] !== v.b ) {

			gl.uniform3f( this.addr, v.r, v.g, v.b );

			cache[ 0 ] = v.r;
			cache[ 1 ] = v.g;
			cache[ 2 ] = v.b;

		}

	} else {

		if ( arraysEqual( cache, v ) ) return;

		gl.uniform3fv( this.addr, v );

		copyArray( cache, v );

	}

}

function setValueV4f( gl, v ) {

	const cache = this.cache;

	if ( v.x !== undefined ) {

		if ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z || cache[ 3 ] !== v.w ) {

			gl.uniform4f( this.addr, v.x, v.y, v.z, v.w );

			cache[ 0 ] = v.x;
			cache[ 1 ] = v.y;
			cache[ 2 ] = v.z;
			cache[ 3 ] = v.w;

		}

	} else {

		if ( arraysEqual( cache, v ) ) return;

		gl.uniform4fv( this.addr, v );

		copyArray( cache, v );

	}

}

// Single matrix (from flat array or THREE.MatrixN)

function setValueM2( gl, v ) {

	const cache = this.cache;
	const elements = v.elements;

	if ( elements === undefined ) {

		if ( arraysEqual( cache, v ) ) return;

		gl.uniformMatrix2fv( this.addr, false, v );

		copyArray( cache, v );

	} else {

		if ( arraysEqual( cache, elements ) ) return;

		mat2array.set( elements );

		gl.uniformMatrix2fv( this.addr, false, mat2array );

		copyArray( cache, elements );

	}

}

function setValueM3( gl, v ) {

	const cache = this.cache;
	const elements = v.elements;

	if ( elements === undefined ) {

		if ( arraysEqual( cache, v ) ) return;

		gl.uniformMatrix3fv( this.addr, false, v );

		copyArray( cache, v );

	} else {

		if ( arraysEqual( cache, elements ) ) return;

		mat3array.set( elements );

		gl.uniformMatrix3fv( this.addr, false, mat3array );

		copyArray( cache, elements );

	}

}

function setValueM4( gl, v ) {

	const cache = this.cache;
	const elements = v.elements;

	if ( elements === undefined ) {

		if ( arraysEqual( cache, v ) ) return;

		gl.uniformMatrix4fv( this.addr, false, v );

		copyArray( cache, v );

	} else {

		if ( arraysEqual( cache, elements ) ) return;

		mat4array.set( elements );

		gl.uniformMatrix4fv( this.addr, false, mat4array );

		copyArray( cache, elements );

	}

}

// Single integer / boolean

function setValueV1i( gl, v ) {

	const cache = this.cache;

	if ( cache[ 0 ] === v ) return;

	gl.uniform1i( this.addr, v );

	cache[ 0 ] = v;

}

// Single integer / boolean vector (from flat array or THREE.VectorN)

function setValueV2i( gl, v ) {

	const cache = this.cache;

	if ( v.x !== undefined ) {

		if ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y ) {

			gl.uniform2i( this.addr, v.x, v.y );

			cache[ 0 ] = v.x;
			cache[ 1 ] = v.y;

		}

	} else {

		if ( arraysEqual( cache, v ) ) return;

		gl.uniform2iv( this.addr, v );

		copyArray( cache, v );

	}

}

function setValueV3i( gl, v ) {

	const cache = this.cache;

	if ( v.x !== undefined ) {

		if ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z ) {

			gl.uniform3i( this.addr, v.x, v.y, v.z );

			cache[ 0 ] = v.x;
			cache[ 1 ] = v.y;
			cache[ 2 ] = v.z;

		}

	} else {

		if ( arraysEqual( cache, v ) ) return;

		gl.uniform3iv( this.addr, v );

		copyArray( cache, v );

	}

}

function setValueV4i( gl, v ) {

	const cache = this.cache;

	if ( v.x !== undefined ) {

		if ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z || cache[ 3 ] !== v.w ) {

			gl.uniform4i( this.addr, v.x, v.y, v.z, v.w );

			cache[ 0 ] = v.x;
			cache[ 1 ] = v.y;
			cache[ 2 ] = v.z;
			cache[ 3 ] = v.w;

		}

	} else {

		if ( arraysEqual( cache, v ) ) return;

		gl.uniform4iv( this.addr, v );

		copyArray( cache, v );

	}

}

// Single unsigned integer

function setValueV1ui( gl, v ) {

	const cache = this.cache;

	if ( cache[ 0 ] === v ) return;

	gl.uniform1ui( this.addr, v );

	cache[ 0 ] = v;

}

// Single unsigned integer vector (from flat array or THREE.VectorN)

function setValueV2ui( gl, v ) {

	const cache = this.cache;

	if ( v.x !== undefined ) {

		if ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y ) {

			gl.uniform2ui( this.addr, v.x, v.y );

			cache[ 0 ] = v.x;
			cache[ 1 ] = v.y;

		}

	} else {

		if ( arraysEqual( cache, v ) ) return;

		gl.uniform2uiv( this.addr, v );

		copyArray( cache, v );

	}

}

function setValueV3ui( gl, v ) {

	const cache = this.cache;

	if ( v.x !== undefined ) {

		if ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z ) {

			gl.uniform3ui( this.addr, v.x, v.y, v.z );

			cache[ 0 ] = v.x;
			cache[ 1 ] = v.y;
			cache[ 2 ] = v.z;

		}

	} else {

		if ( arraysEqual( cache, v ) ) return;

		gl.uniform3uiv( this.addr, v );

		copyArray( cache, v );

	}

}

function setValueV4ui( gl, v ) {

	const cache = this.cache;

	if ( v.x !== undefined ) {

		if ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z || cache[ 3 ] !== v.w ) {

			gl.uniform4ui( this.addr, v.x, v.y, v.z, v.w );

			cache[ 0 ] = v.x;
			cache[ 1 ] = v.y;
			cache[ 2 ] = v.z;
			cache[ 3 ] = v.w;

		}

	} else {

		if ( arraysEqual( cache, v ) ) return;

		gl.uniform4uiv( this.addr, v );

		copyArray( cache, v );

	}

}


// Single texture (2D / Cube)

function setValueT1( gl, v, textures ) {

	const cache = this.cache;
	const unit = textures.allocateTextureUnit();

	if ( cache[ 0 ] !== unit ) {

		gl.uniform1i( this.addr, unit );
		cache[ 0 ] = unit;

	}

	let emptyTexture2D;

	if ( this.type === gl.SAMPLER_2D_SHADOW ) {

		emptyShadowTexture.compareFunction = LessEqualCompare; // #28670
		emptyTexture2D = emptyShadowTexture;

	} else {

		emptyTexture2D = emptyTexture;

	}

	textures.setTexture2D( v || emptyTexture2D, unit );

}

function setValueT3D1( gl, v, textures ) {

	const cache = this.cache;
	const unit = textures.allocateTextureUnit();

	if ( cache[ 0 ] !== unit ) {

		gl.uniform1i( this.addr, unit );
		cache[ 0 ] = unit;

	}

	textures.setTexture3D( v || empty3dTexture, unit );

}

function setValueT6( gl, v, textures ) {

	const cache = this.cache;
	const unit = textures.allocateTextureUnit();

	if ( cache[ 0 ] !== unit ) {

		gl.uniform1i( this.addr, unit );
		cache[ 0 ] = unit;

	}

	textures.setTextureCube( v || emptyCubeTexture, unit );

}

function setValueT2DArray1( gl, v, textures ) {

	const cache = this.cache;
	const unit = textures.allocateTextureUnit();

	if ( cache[ 0 ] !== unit ) {

		gl.uniform1i( this.addr, unit );
		cache[ 0 ] = unit;

	}

	textures.setTexture2DArray( v || emptyArrayTexture, unit );

}

// Helper to pick the right setter for the singular case

function getSingularSetter( type ) {

	switch ( type ) {

		case 0x1406: return setValueV1f; // FLOAT
		case 0x8b50: return setValueV2f; // _VEC2
		case 0x8b51: return setValueV3f; // _VEC3
		case 0x8b52: return setValueV4f; // _VEC4

		case 0x8b5a: return setValueM2; // _MAT2
		case 0x8b5b: return setValueM3; // _MAT3
		case 0x8b5c: return setValueM4; // _MAT4

		case 0x1404: case 0x8b56: return setValueV1i; // INT, BOOL
		case 0x8b53: case 0x8b57: return setValueV2i; // _VEC2
		case 0x8b54: case 0x8b58: return setValueV3i; // _VEC3
		case 0x8b55: case 0x8b59: return setValueV4i; // _VEC4

		case 0x1405: return setValueV1ui; // UINT
		case 0x8dc6: return setValueV2ui; // _VEC2
		case 0x8dc7: return setValueV3ui; // _VEC3
		case 0x8dc8: return setValueV4ui; // _VEC4

		case 0x8b5e: // SAMPLER_2D
		case 0x8d66: // SAMPLER_EXTERNAL_OES
		case 0x8dca: // INT_SAMPLER_2D
		case 0x8dd2: // UNSIGNED_INT_SAMPLER_2D
		case 0x8b62: // SAMPLER_2D_SHADOW
			return setValueT1;

		case 0x8b5f: // SAMPLER_3D
		case 0x8dcb: // INT_SAMPLER_3D
		case 0x8dd3: // UNSIGNED_INT_SAMPLER_3D
			return setValueT3D1;

		case 0x8b60: // SAMPLER_CUBE
		case 0x8dcc: // INT_SAMPLER_CUBE
		case 0x8dd4: // UNSIGNED_INT_SAMPLER_CUBE
		case 0x8dc5: // SAMPLER_CUBE_SHADOW
			return setValueT6;

		case 0x8dc1: // SAMPLER_2D_ARRAY
		case 0x8dcf: // INT_SAMPLER_2D_ARRAY
		case 0x8dd7: // UNSIGNED_INT_SAMPLER_2D_ARRAY
		case 0x8dc4: // SAMPLER_2D_ARRAY_SHADOW
			return setValueT2DArray1;

	}

}


// Array of scalars

function setValueV1fArray( gl, v ) {

	gl.uniform1fv( this.addr, v );

}

// Array of vectors (from flat array or array of THREE.VectorN)

function setValueV2fArray( gl, v ) {

	const data = flatten( v, this.size, 2 );

	gl.uniform2fv( this.addr, data );

}

function setValueV3fArray( gl, v ) {

	const data = flatten( v, this.size, 3 );

	gl.uniform3fv( this.addr, data );

}

function setValueV4fArray( gl, v ) {

	const data = flatten( v, this.size, 4 );

	gl.uniform4fv( this.addr, data );

}

// Array of matrices (from flat array or array of THREE.MatrixN)

function setValueM2Array( gl, v ) {

	const data = flatten( v, this.size, 4 );

	gl.uniformMatrix2fv( this.addr, false, data );

}

function setValueM3Array( gl, v ) {

	const data = flatten( v, this.size, 9 );

	gl.uniformMatrix3fv( this.addr, false, data );

}

function setValueM4Array( gl, v ) {

	const data = flatten( v, this.size, 16 );

	gl.uniformMatrix4fv( this.addr, false, data );

}

// Array of integer / boolean

function setValueV1iArray( gl, v ) {

	gl.uniform1iv( this.addr, v );

}

// Array of integer / boolean vectors (from flat array)

function setValueV2iArray( gl, v ) {

	gl.uniform2iv( this.addr, v );

}

function setValueV3iArray( gl, v ) {

	gl.uniform3iv( this.addr, v );

}

function setValueV4iArray( gl, v ) {

	gl.uniform4iv( this.addr, v );

}

// Array of unsigned integer

function setValueV1uiArray( gl, v ) {

	gl.uniform1uiv( this.addr, v );

}

// Array of unsigned integer vectors (from flat array)

function setValueV2uiArray( gl, v ) {

	gl.uniform2uiv( this.addr, v );

}

function setValueV3uiArray( gl, v ) {

	gl.uniform3uiv( this.addr, v );

}

function setValueV4uiArray( gl, v ) {

	gl.uniform4uiv( this.addr, v );

}


// Array of textures (2D / 3D / Cube / 2DArray)

function setValueT1Array( gl, v, textures ) {

	const cache = this.cache;

	const n = v.length;

	const units = allocTexUnits( textures, n );

	if ( ! arraysEqual( cache, units ) ) {

		gl.uniform1iv( this.addr, units );

		copyArray( cache, units );

	}

	for ( let i = 0; i !== n; ++ i ) {

		textures.setTexture2D( v[ i ] || emptyTexture, units[ i ] );

	}

}

function setValueT3DArray( gl, v, textures ) {

	const cache = this.cache;

	const n = v.length;

	const units = allocTexUnits( textures, n );

	if ( ! arraysEqual( cache, units ) ) {

		gl.uniform1iv( this.addr, units );

		copyArray( cache, units );

	}

	for ( let i = 0; i !== n; ++ i ) {

		textures.setTexture3D( v[ i ] || empty3dTexture, units[ i ] );

	}

}

function setValueT6Array( gl, v, textures ) {

	const cache = this.cache;

	const n = v.length;

	const units = allocTexUnits( textures, n );

	if ( ! arraysEqual( cache, units ) ) {

		gl.uniform1iv( this.addr, units );

		copyArray( cache, units );

	}

	for ( let i = 0; i !== n; ++ i ) {

		textures.setTextureCube( v[ i ] || emptyCubeTexture, units[ i ] );

	}

}

function setValueT2DArrayArray( gl, v, textures ) {

	const cache = this.cache;

	const n = v.length;

	const units = allocTexUnits( textures, n );

	if ( ! arraysEqual( cache, units ) ) {

		gl.uniform1iv( this.addr, units );

		copyArray( cache, units );

	}

	for ( let i = 0; i !== n; ++ i ) {

		textures.setTexture2DArray( v[ i ] || emptyArrayTexture, units[ i ] );

	}

}


// Helper to pick the right setter for a pure (bottom-level) array

function getPureArraySetter( type ) {

	switch ( type ) {

		case 0x1406: return setValueV1fArray; // FLOAT
		case 0x8b50: return setValueV2fArray; // _VEC2
		case 0x8b51: return setValueV3fArray; // _VEC3
		case 0x8b52: return setValueV4fArray; // _VEC4

		case 0x8b5a: return setValueM2Array; // _MAT2
		case 0x8b5b: return setValueM3Array; // _MAT3
		case 0x8b5c: return setValueM4Array; // _MAT4

		case 0x1404: case 0x8b56: return setValueV1iArray; // INT, BOOL
		case 0x8b53: case 0x8b57: return setValueV2iArray; // _VEC2
		case 0x8b54: case 0x8b58: return setValueV3iArray; // _VEC3
		case 0x8b55: case 0x8b59: return setValueV4iArray; // _VEC4

		case 0x1405: return setValueV1uiArray; // UINT
		case 0x8dc6: return setValueV2uiArray; // _VEC2
		case 0x8dc7: return setValueV3uiArray; // _VEC3
		case 0x8dc8: return setValueV4uiArray; // _VEC4

		case 0x8b5e: // SAMPLER_2D
		case 0x8d66: // SAMPLER_EXTERNAL_OES
		case 0x8dca: // INT_SAMPLER_2D
		case 0x8dd2: // UNSIGNED_INT_SAMPLER_2D
		case 0x8b62: // SAMPLER_2D_SHADOW
			return setValueT1Array;

		case 0x8b5f: // SAMPLER_3D
		case 0x8dcb: // INT_SAMPLER_3D
		case 0x8dd3: // UNSIGNED_INT_SAMPLER_3D
			return setValueT3DArray;

		case 0x8b60: // SAMPLER_CUBE
		case 0x8dcc: // INT_SAMPLER_CUBE
		case 0x8dd4: // UNSIGNED_INT_SAMPLER_CUBE
		case 0x8dc5: // SAMPLER_CUBE_SHADOW
			return setValueT6Array;

		case 0x8dc1: // SAMPLER_2D_ARRAY
		case 0x8dcf: // INT_SAMPLER_2D_ARRAY
		case 0x8dd7: // UNSIGNED_INT_SAMPLER_2D_ARRAY
		case 0x8dc4: // SAMPLER_2D_ARRAY_SHADOW
			return setValueT2DArrayArray;

	}

}

// --- Uniform Classes ---

class SingleUniform {

	constructor( id, activeInfo, addr ) {

		this.id = id;
		this.addr = addr;
		this.cache = [];
		this.type = activeInfo.type;
		this.setValue = getSingularSetter( activeInfo.type );

		// this.path = activeInfo.name; // DEBUG

	}

}

class PureArrayUniform {

	constructor( id, activeInfo, addr ) {

		this.id = id;
		this.addr = addr;
		this.cache = [];
		this.type = activeInfo.type;
		this.size = activeInfo.size;
		this.setValue = getPureArraySetter( activeInfo.type );

		// this.path = activeInfo.name; // DEBUG

	}

}

class StructuredUniform {

	constructor( id ) {

		this.id = id;

		this.seq = [];
		this.map = {};

	}

	setValue( gl, value, textures ) {

		const seq = this.seq;

		for ( let i = 0, n = seq.length; i !== n; ++ i ) {

			const u = seq[ i ];
			u.setValue( gl, value[ u.id ], textures );

		}

	}

}

// --- Top-level ---

// Parser - builds up the property tree from the path strings

const RePathPart = /(\w+)(\])?(\[|\.)?/g;

// extracts
// 	- the identifier (member name or array index)
//  - followed by an optional right bracket (found when array index)
//  - followed by an optional left bracket or dot (type of subscript)
//
// Note: These portions can be read in a non-overlapping fashion and
// allow straightforward parsing of the hierarchy that WebGL encodes
// in the uniform names.

function addUniform( container, uniformObject ) {

	container.seq.push( uniformObject );
	container.map[ uniformObject.id ] = uniformObject;

}

function parseUniform( activeInfo, addr, container ) {

	const path = activeInfo.name,
		pathLength = path.length;

	// reset RegExp object, because of the early exit of a previous run
	RePathPart.lastIndex = 0;

	while ( true ) {

		const match = RePathPart.exec( path ),
			matchEnd = RePathPart.lastIndex;

		let id = match[ 1 ];
		const idIsIndex = match[ 2 ] === ']',
			subscript = match[ 3 ];

		if ( idIsIndex ) id = id | 0; // convert to integer

		if ( subscript === undefined || subscript === '[' && matchEnd + 2 === pathLength ) {

			// bare name or "pure" bottom-level array "[0]" suffix

			addUniform( container, subscript === undefined ?
				new SingleUniform( id, activeInfo, addr ) :
				new PureArrayUniform( id, activeInfo, addr ) );

			break;

		} else {

			// step into inner node / create it in case it doesn't exist

			const map = container.map;
			let next = map[ id ];

			if ( next === undefined ) {

				next = new StructuredUniform( id );
				addUniform( container, next );

			}

			container = next;

		}

	}

}

// Root Container

class WebGLUniforms {

	constructor( gl, program ) {

		this.seq = [];
		this.map = {};

		const n = gl.getProgramParameter( program, gl.ACTIVE_UNIFORMS );

		for ( let i = 0; i < n; ++ i ) {

			const info = gl.getActiveUniform( program, i ),
				addr = gl.getUniformLocation( program, info.name );

			parseUniform( info, addr, this );

		}

	}

	setValue( gl, name, value, textures ) {

		const u = this.map[ name ];

		if ( u !== undefined ) u.setValue( gl, value, textures );

	}

	setOptional( gl, object, name ) {

		const v = object[ name ];

		if ( v !== undefined ) this.setValue( gl, name, v );

	}

	static upload( gl, seq, values, textures ) {

		for ( let i = 0, n = seq.length; i !== n; ++ i ) {

			const u = seq[ i ],
				v = values[ u.id ];

			if ( v.needsUpdate !== false ) {

				// note: always updating when .needsUpdate is undefined
				u.setValue( gl, v.value, textures );

			}

		}

	}

	static seqWithValue( seq, values ) {

		const r = [];

		for ( let i = 0, n = seq.length; i !== n; ++ i ) {

			const u = seq[ i ];
			if ( u.id in values ) r.push( u );

		}

		return r;

	}

}

function WebGLShader( gl, type, string ) {

	const shader = gl.createShader( type );

	gl.shaderSource( shader, string );
	gl.compileShader( shader );

	return shader;

}

// From https://www.khronos.org/registry/webgl/extensions/KHR_parallel_shader_compile/
const COMPLETION_STATUS_KHR = 0x91B1;

let programIdCount = 0;

function handleSource( string, errorLine ) {

	const lines = string.split( '\n' );
	const lines2 = [];

	const from = Math.max( errorLine - 6, 0 );
	const to = Math.min( errorLine + 6, lines.length );

	for ( let i = from; i < to; i ++ ) {

		const line = i + 1;
		lines2.push( `${line === errorLine ? '>' : ' '} ${line}: ${lines[ i ]}` );

	}

	return lines2.join( '\n' );

}

const _m0 = /*@__PURE__*/ new Matrix3();

function getEncodingComponents( colorSpace ) {

	ColorManagement._getMatrix( _m0, ColorManagement.workingColorSpace, colorSpace );

	const encodingMatrix = `mat3( ${ _m0.elements.map( ( v ) => v.toFixed( 4 ) ) } )`;

	switch ( ColorManagement.getTransfer( colorSpace ) ) {

		case LinearTransfer:
			return [ encodingMatrix, 'LinearTransferOETF' ];

		case SRGBTransfer:
			return [ encodingMatrix, 'sRGBTransferOETF' ];

		default:
			console.warn( 'THREE.WebGLProgram: Unsupported color space: ', colorSpace );
			return [ encodingMatrix, 'LinearTransferOETF' ];

	}

}

function getShaderErrors( gl, shader, type ) {

	const status = gl.getShaderParameter( shader, gl.COMPILE_STATUS );

	const shaderInfoLog = gl.getShaderInfoLog( shader ) || '';
	const errors = shaderInfoLog.trim();

	if ( status && errors === '' ) return '';

	const errorMatches = /ERROR: 0:(\d+)/.exec( errors );
	if ( errorMatches ) {

		// --enable-privileged-webgl-extension
		// console.log( '**' + type + '**', gl.getExtension( 'WEBGL_debug_shaders' ).getTranslatedShaderSource( shader ) );

		const errorLine = parseInt( errorMatches[ 1 ] );
		return type.toUpperCase() + '\n\n' + errors + '\n\n' + handleSource( gl.getShaderSource( shader ), errorLine );

	} else {

		return errors;

	}

}

function getTexelEncodingFunction( functionName, colorSpace ) {

	const components = getEncodingComponents( colorSpace );

	return [

		`vec4 ${functionName}( vec4 value ) {`,

		`	return ${components[ 1 ]}( vec4( value.rgb * ${components[ 0 ]}, value.a ) );`,

		'}',

	].join( '\n' );

}

function getToneMappingFunction( functionName, toneMapping ) {

	let toneMappingName;

	switch ( toneMapping ) {

		case LinearToneMapping:
			toneMappingName = 'Linear';
			break;

		case ReinhardToneMapping:
			toneMappingName = 'Reinhard';
			break;

		case CineonToneMapping:
			toneMappingName = 'Cineon';
			break;

		case ACESFilmicToneMapping:
			toneMappingName = 'ACESFilmic';
			break;

		case AgXToneMapping:
			toneMappingName = 'AgX';
			break;

		case NeutralToneMapping:
			toneMappingName = 'Neutral';
			break;

		case CustomToneMapping:
			toneMappingName = 'Custom';
			break;

		default:
			console.warn( 'THREE.WebGLProgram: Unsupported toneMapping:', toneMapping );
			toneMappingName = 'Linear';

	}

	return 'vec3 ' + functionName + '( vec3 color ) { return ' + toneMappingName + 'ToneMapping( color ); }';

}

const _v0$1 = /*@__PURE__*/ new Vector3();

function getLuminanceFunction() {

	ColorManagement.getLuminanceCoefficients( _v0$1 );

	const r = _v0$1.x.toFixed( 4 );
	const g = _v0$1.y.toFixed( 4 );
	const b = _v0$1.z.toFixed( 4 );

	return [

		'float luminance( const in vec3 rgb ) {',

		`	const vec3 weights = vec3( ${ r }, ${ g }, ${ b } );`,

		'	return dot( weights, rgb );',

		'}'

	].join( '\n' );

}

function generateVertexExtensions( parameters ) {

	const chunks = [
		parameters.extensionClipCullDistance ? '#extension GL_ANGLE_clip_cull_distance : require' : '',
		parameters.extensionMultiDraw ? '#extension GL_ANGLE_multi_draw : require' : '',
	];

	return chunks.filter( filterEmptyLine ).join( '\n' );

}

function generateDefines( defines ) {

	const chunks = [];

	for ( const name in defines ) {

		const value = defines[ name ];

		if ( value === false ) continue;

		chunks.push( '#define ' + name + ' ' + value );

	}

	return chunks.join( '\n' );

}

function fetchAttributeLocations( gl, program ) {

	const attributes = {};

	const n = gl.getProgramParameter( program, gl.ACTIVE_ATTRIBUTES );

	for ( let i = 0; i < n; i ++ ) {

		const info = gl.getActiveAttrib( program, i );
		const name = info.name;

		let locationSize = 1;
		if ( info.type === gl.FLOAT_MAT2 ) locationSize = 2;
		if ( info.type === gl.FLOAT_MAT3 ) locationSize = 3;
		if ( info.type === gl.FLOAT_MAT4 ) locationSize = 4;

		// console.log( 'THREE.WebGLProgram: ACTIVE VERTEX ATTRIBUTE:', name, i );

		attributes[ name ] = {
			type: info.type,
			location: gl.getAttribLocation( program, name ),
			locationSize: locationSize
		};

	}

	return attributes;

}

function filterEmptyLine( string ) {

	return string !== '';

}

function replaceLightNums( string, parameters ) {

	const numSpotLightCoords = parameters.numSpotLightShadows + parameters.numSpotLightMaps - parameters.numSpotLightShadowsWithMaps;

	return string
		.replace( /NUM_DIR_LIGHTS/g, parameters.numDirLights )
		.replace( /NUM_SPOT_LIGHTS/g, parameters.numSpotLights )
		.replace( /NUM_SPOT_LIGHT_MAPS/g, parameters.numSpotLightMaps )
		.replace( /NUM_SPOT_LIGHT_COORDS/g, numSpotLightCoords )
		.replace( /NUM_RECT_AREA_LIGHTS/g, parameters.numRectAreaLights )
		.replace( /NUM_POINT_LIGHTS/g, parameters.numPointLights )
		.replace( /NUM_HEMI_LIGHTS/g, parameters.numHemiLights )
		.replace( /NUM_DIR_LIGHT_SHADOWS/g, parameters.numDirLightShadows )
		.replace( /NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS/g, parameters.numSpotLightShadowsWithMaps )
		.replace( /NUM_SPOT_LIGHT_SHADOWS/g, parameters.numSpotLightShadows )
		.replace( /NUM_POINT_LIGHT_SHADOWS/g, parameters.numPointLightShadows );

}

function replaceClippingPlaneNums( string, parameters ) {

	return string
		.replace( /NUM_CLIPPING_PLANES/g, parameters.numClippingPlanes )
		.replace( /UNION_CLIPPING_PLANES/g, ( parameters.numClippingPlanes - parameters.numClipIntersection ) );

}

// Resolve Includes

const includePattern = /^[ \t]*#include +<([\w\d./]+)>/gm;

function resolveIncludes( string ) {

	return string.replace( includePattern, includeReplacer );

}

const shaderChunkMap = new Map();

function includeReplacer( match, include ) {

	let string = ShaderChunk[ include ];

	if ( string === undefined ) {

		const newInclude = shaderChunkMap.get( include );

		if ( newInclude !== undefined ) {

			string = ShaderChunk[ newInclude ];
			console.warn( 'THREE.WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.', include, newInclude );

		} else {

			throw new Error( 'Can not resolve #include <' + include + '>' );

		}

	}

	return resolveIncludes( string );

}

// Unroll Loops

const unrollLoopPattern = /#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;

function unrollLoops( string ) {

	return string.replace( unrollLoopPattern, loopReplacer );

}

function loopReplacer( match, start, end, snippet ) {

	let string = '';

	for ( let i = parseInt( start ); i < parseInt( end ); i ++ ) {

		string += snippet
			.replace( /\[\s*i\s*\]/g, '[ ' + i + ' ]' )
			.replace( /UNROLLED_LOOP_INDEX/g, i );

	}

	return string;

}

//

function generatePrecision( parameters ) {

	let precisionstring = `precision ${parameters.precision} float;
	precision ${parameters.precision} int;
	precision ${parameters.precision} sampler2D;
	precision ${parameters.precision} samplerCube;
	precision ${parameters.precision} sampler3D;
	precision ${parameters.precision} sampler2DArray;
	precision ${parameters.precision} sampler2DShadow;
	precision ${parameters.precision} samplerCubeShadow;
	precision ${parameters.precision} sampler2DArrayShadow;
	precision ${parameters.precision} isampler2D;
	precision ${parameters.precision} isampler3D;
	precision ${parameters.precision} isamplerCube;
	precision ${parameters.precision} isampler2DArray;
	precision ${parameters.precision} usampler2D;
	precision ${parameters.precision} usampler3D;
	precision ${parameters.precision} usamplerCube;
	precision ${parameters.precision} usampler2DArray;
	`;

	if ( parameters.precision === 'highp' ) {

		precisionstring += '\n#define HIGH_PRECISION';

	} else if ( parameters.precision === 'mediump' ) {

		precisionstring += '\n#define MEDIUM_PRECISION';

	} else if ( parameters.precision === 'lowp' ) {

		precisionstring += '\n#define LOW_PRECISION';

	}

	return precisionstring;

}

function generateShadowMapTypeDefine( parameters ) {

	let shadowMapTypeDefine = 'SHADOWMAP_TYPE_BASIC';

	if ( parameters.shadowMapType === PCFShadowMap ) {

		shadowMapTypeDefine = 'SHADOWMAP_TYPE_PCF';

	} else if ( parameters.shadowMapType === PCFSoftShadowMap ) {

		shadowMapTypeDefine = 'SHADOWMAP_TYPE_PCF_SOFT';

	} else if ( parameters.shadowMapType === VSMShadowMap ) {

		shadowMapTypeDefine = 'SHADOWMAP_TYPE_VSM';

	}

	return shadowMapTypeDefine;

}

function generateEnvMapTypeDefine( parameters ) {

	let envMapTypeDefine = 'ENVMAP_TYPE_CUBE';

	if ( parameters.envMap ) {

		switch ( parameters.envMapMode ) {

			case CubeReflectionMapping:
			case CubeRefractionMapping:
				envMapTypeDefine = 'ENVMAP_TYPE_CUBE';
				break;

			case CubeUVReflectionMapping:
				envMapTypeDefine = 'ENVMAP_TYPE_CUBE_UV';
				break;

		}

	}

	return envMapTypeDefine;

}

function generateEnvMapModeDefine( parameters ) {

	let envMapModeDefine = 'ENVMAP_MODE_REFLECTION';

	if ( parameters.envMap ) {

		switch ( parameters.envMapMode ) {

			case CubeRefractionMapping:

				envMapModeDefine = 'ENVMAP_MODE_REFRACTION';
				break;

		}

	}

	return envMapModeDefine;

}

function generateEnvMapBlendingDefine( parameters ) {

	let envMapBlendingDefine = 'ENVMAP_BLENDING_NONE';

	if ( parameters.envMap ) {

		switch ( parameters.combine ) {

			case MultiplyOperation:
				envMapBlendingDefine = 'ENVMAP_BLENDING_MULTIPLY';
				break;

			case MixOperation:
				envMapBlendingDefine = 'ENVMAP_BLENDING_MIX';
				break;

			case AddOperation:
				envMapBlendingDefine = 'ENVMAP_BLENDING_ADD';
				break;

		}

	}

	return envMapBlendingDefine;

}

function generateCubeUVSize( parameters ) {

	const imageHeight = parameters.envMapCubeUVHeight;

	if ( imageHeight === null ) return null;

	const maxMip = Math.log2( imageHeight ) - 2;

	const texelHeight = 1.0 / imageHeight;

	const texelWidth = 1.0 / ( 3 * Math.max( Math.pow( 2, maxMip ), 7 * 16 ) );

	return { texelWidth, texelHeight, maxMip };

}

function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {

	// TODO Send this event to Three.js DevTools
	// console.log( 'WebGLProgram', cacheKey );

	const gl = renderer.getContext();

	const defines = parameters.defines;

	let vertexShader = parameters.vertexShader;
	let fragmentShader = parameters.fragmentShader;

	const shadowMapTypeDefine = generateShadowMapTypeDefine( parameters );
	const envMapTypeDefine = generateEnvMapTypeDefine( parameters );
	const envMapModeDefine = generateEnvMapModeDefine( parameters );
	const envMapBlendingDefine = generateEnvMapBlendingDefine( parameters );
	const envMapCubeUVSize = generateCubeUVSize( parameters );

	const customVertexExtensions = generateVertexExtensions( parameters );

	const customDefines = generateDefines( defines );

	const program = gl.createProgram();

	let prefixVertex, prefixFragment;
	let versionString = parameters.glslVersion ? '#version ' + parameters.glslVersion + '\n' : '';

	if ( parameters.isRawShaderMaterial ) {

		prefixVertex = [

			'#define SHADER_TYPE ' + parameters.shaderType,
			'#define SHADER_NAME ' + parameters.shaderName,

			customDefines

		].filter( filterEmptyLine ).join( '\n' );

		if ( prefixVertex.length > 0 ) {

			prefixVertex += '\n';

		}

		prefixFragment = [

			'#define SHADER_TYPE ' + parameters.shaderType,
			'#define SHADER_NAME ' + parameters.shaderName,

			customDefines

		].filter( filterEmptyLine ).join( '\n' );

		if ( prefixFragment.length > 0 ) {

			prefixFragment += '\n';

		}

	} else {

		prefixVertex = [

			generatePrecision( parameters ),

			'#define SHADER_TYPE ' + parameters.shaderType,
			'#define SHADER_NAME ' + parameters.shaderName,

			customDefines,

			parameters.extensionClipCullDistance ? '#define USE_CLIP_DISTANCE' : '',
			parameters.batching ? '#define USE_BATCHING' : '',
			parameters.batchingColor ? '#define USE_BATCHING_COLOR' : '',
			parameters.instancing ? '#define USE_INSTANCING' : '',
			parameters.instancingColor ? '#define USE_INSTANCING_COLOR' : '',
			parameters.instancingMorph ? '#define USE_INSTANCING_MORPH' : '',

			parameters.useFog && parameters.fog ? '#define USE_FOG' : '',
			parameters.useFog && parameters.fogExp2 ? '#define FOG_EXP2' : '',

			parameters.map ? '#define USE_MAP' : '',
			parameters.envMap ? '#define USE_ENVMAP' : '',
			parameters.envMap ? '#define ' + envMapModeDefine : '',
			parameters.lightMap ? '#define USE_LIGHTMAP' : '',
			parameters.aoMap ? '#define USE_AOMAP' : '',
			parameters.bumpMap ? '#define USE_BUMPMAP' : '',
			parameters.normalMap ? '#define USE_NORMALMAP' : '',
			parameters.normalMapObjectSpace ? '#define USE_NORMALMAP_OBJECTSPACE' : '',
			parameters.normalMapTangentSpace ? '#define USE_NORMALMAP_TANGENTSPACE' : '',
			parameters.displacementMap ? '#define USE_DISPLACEMENTMAP' : '',
			parameters.emissiveMap ? '#define USE_EMISSIVEMAP' : '',

			parameters.anisotropy ? '#define USE_ANISOTROPY' : '',
			parameters.anisotropyMap ? '#define USE_ANISOTROPYMAP' : '',

			parameters.clearcoatMap ? '#define USE_CLEARCOATMAP' : '',
			parameters.clearcoatRoughnessMap ? '#define USE_CLEARCOAT_ROUGHNESSMAP' : '',
			parameters.clearcoatNormalMap ? '#define USE_CLEARCOAT_NORMALMAP' : '',

			parameters.iridescenceMap ? '#define USE_IRIDESCENCEMAP' : '',
			parameters.iridescenceThicknessMap ? '#define USE_IRIDESCENCE_THICKNESSMAP' : '',

			parameters.specularMap ? '#define USE_SPECULARMAP' : '',
			parameters.specularColorMap ? '#define USE_SPECULAR_COLORMAP' : '',
			parameters.specularIntensityMap ? '#define USE_SPECULAR_INTENSITYMAP' : '',

			parameters.roughnessMap ? '#define USE_ROUGHNESSMAP' : '',
			parameters.metalnessMap ? '#define USE_METALNESSMAP' : '',
			parameters.alphaMap ? '#define USE_ALPHAMAP' : '',
			parameters.alphaHash ? '#define USE_ALPHAHASH' : '',

			parameters.transmission ? '#define USE_TRANSMISSION' : '',
			parameters.transmissionMap ? '#define USE_TRANSMISSIONMAP' : '',
			parameters.thicknessMap ? '#define USE_THICKNESSMAP' : '',

			parameters.sheenColorMap ? '#define USE_SHEEN_COLORMAP' : '',
			parameters.sheenRoughnessMap ? '#define USE_SHEEN_ROUGHNESSMAP' : '',

			//

			parameters.mapUv ? '#define MAP_UV ' + parameters.mapUv : '',
			parameters.alphaMapUv ? '#define ALPHAMAP_UV ' + parameters.alphaMapUv : '',
			parameters.lightMapUv ? '#define LIGHTMAP_UV ' + parameters.lightMapUv : '',
			parameters.aoMapUv ? '#define AOMAP_UV ' + parameters.aoMapUv : '',
			parameters.emissiveMapUv ? '#define EMISSIVEMAP_UV ' + parameters.emissiveMapUv : '',
			parameters.bumpMapUv ? '#define BUMPMAP_UV ' + parameters.bumpMapUv : '',
			parameters.normalMapUv ? '#define NORMALMAP_UV ' + parameters.normalMapUv : '',
			parameters.displacementMapUv ? '#define DISPLACEMENTMAP_UV ' + parameters.displacementMapUv : '',

			parameters.metalnessMapUv ? '#define METALNESSMAP_UV ' + parameters.metalnessMapUv : '',
			parameters.roughnessMapUv ? '#define ROUGHNESSMAP_UV ' + parameters.roughnessMapUv : '',

			parameters.anisotropyMapUv ? '#define ANISOTROPYMAP_UV ' + parameters.anisotropyMapUv : '',

			parameters.clearcoatMapUv ? '#define CLEARCOATMAP_UV ' + parameters.clearcoatMapUv : '',
			parameters.clearcoatNormalMapUv ? '#define CLEARCOAT_NORMALMAP_UV ' + parameters.clearcoatNormalMapUv : '',
			parameters.clearcoatRoughnessMapUv ? '#define CLEARCOAT_ROUGHNESSMAP_UV ' + parameters.clearcoatRoughnessMapUv : '',

			parameters.iridescenceMapUv ? '#define IRIDESCENCEMAP_UV ' + parameters.iridescenceMapUv : '',
			parameters.iridescenceThicknessMapUv ? '#define IRIDESCENCE_THICKNESSMAP_UV ' + parameters.iridescenceThicknessMapUv : '',

			parameters.sheenColorMapUv ? '#define SHEEN_COLORMAP_UV ' + parameters.sheenColorMapUv : '',
			parameters.sheenRoughnessMapUv ? '#define SHEEN_ROUGHNESSMAP_UV ' + parameters.sheenRoughnessMapUv : '',

			parameters.specularMapUv ? '#define SPECULARMAP_UV ' + parameters.specularMapUv : '',
			parameters.specularColorMapUv ? '#define SPECULAR_COLORMAP_UV ' + parameters.specularColorMapUv : '',
			parameters.specularIntensityMapUv ? '#define SPECULAR_INTENSITYMAP_UV ' + parameters.specularIntensityMapUv : '',

			parameters.transmissionMapUv ? '#define TRANSMISSIONMAP_UV ' + parameters.transmissionMapUv : '',
			parameters.thicknessMapUv ? '#define THICKNESSMAP_UV ' + parameters.thicknessMapUv : '',

			//

			parameters.vertexTangents && parameters.flatShading === false ? '#define USE_TANGENT' : '',
			parameters.vertexColors ? '#define USE_COLOR' : '',
			parameters.vertexAlphas ? '#define USE_COLOR_ALPHA' : '',
			parameters.vertexUv1s ? '#define USE_UV1' : '',
			parameters.vertexUv2s ? '#define USE_UV2' : '',
			parameters.vertexUv3s ? '#define USE_UV3' : '',

			parameters.pointsUvs ? '#define USE_POINTS_UV' : '',

			parameters.flatShading ? '#define FLAT_SHADED' : '',

			parameters.skinning ? '#define USE_SKINNING' : '',

			parameters.morphTargets ? '#define USE_MORPHTARGETS' : '',
			parameters.morphNormals && parameters.flatShading === false ? '#define USE_MORPHNORMALS' : '',
			( parameters.morphColors ) ? '#define USE_MORPHCOLORS' : '',
			( parameters.morphTargetsCount > 0 ) ? '#define MORPHTARGETS_TEXTURE_STRIDE ' + parameters.morphTextureStride : '',
			( parameters.morphTargetsCount > 0 ) ? '#define MORPHTARGETS_COUNT ' + parameters.morphTargetsCount : '',
			parameters.doubleSided ? '#define DOUBLE_SIDED' : '',
			parameters.flipSided ? '#define FLIP_SIDED' : '',

			parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',
			parameters.shadowMapEnabled ? '#define ' + shadowMapTypeDefine : '',

			parameters.sizeAttenuation ? '#define USE_SIZEATTENUATION' : '',

			parameters.numLightProbes > 0 ? '#define USE_LIGHT_PROBES' : '',

			parameters.logarithmicDepthBuffer ? '#define USE_LOGARITHMIC_DEPTH_BUFFER' : '',
			parameters.reversedDepthBuffer ? '#define USE_REVERSED_DEPTH_BUFFER' : '',

			'uniform mat4 modelMatrix;',
			'uniform mat4 modelViewMatrix;',
			'uniform mat4 projectionMatrix;',
			'uniform mat4 viewMatrix;',
			'uniform mat3 normalMatrix;',
			'uniform vec3 cameraPosition;',
			'uniform bool isOrthographic;',

			'#ifdef USE_INSTANCING',

			'	attribute mat4 instanceMatrix;',

			'#endif',

			'#ifdef USE_INSTANCING_COLOR',

			'	attribute vec3 instanceColor;',

			'#endif',

			'#ifdef USE_INSTANCING_MORPH',

			'	uniform sampler2D morphTexture;',

			'#endif',

			'attribute vec3 position;',
			'attribute vec3 normal;',
			'attribute vec2 uv;',

			'#ifdef USE_UV1',

			'	attribute vec2 uv1;',

			'#endif',

			'#ifdef USE_UV2',

			'	attribute vec2 uv2;',

			'#endif',

			'#ifdef USE_UV3',

			'	attribute vec2 uv3;',

			'#endif',

			'#ifdef USE_TANGENT',

			'	attribute vec4 tangent;',

			'#endif',

			'#if defined( USE_COLOR_ALPHA )',

			'	attribute vec4 color;',

			'#elif defined( USE_COLOR )',

			'	attribute vec3 color;',

			'#endif',

			'#ifdef USE_SKINNING',

			'	attribute vec4 skinIndex;',
			'	attribute vec4 skinWeight;',

			'#endif',

			'\n'

		].filter( filterEmptyLine ).join( '\n' );

		prefixFragment = [

			generatePrecision( parameters ),

			'#define SHADER_TYPE ' + parameters.shaderType,
			'#define SHADER_NAME ' + parameters.shaderName,

			customDefines,

			parameters.useFog && parameters.fog ? '#define USE_FOG' : '',
			parameters.useFog && parameters.fogExp2 ? '#define FOG_EXP2' : '',

			parameters.alphaToCoverage ? '#define ALPHA_TO_COVERAGE' : '',
			parameters.map ? '#define USE_MAP' : '',
			parameters.matcap ? '#define USE_MATCAP' : '',
			parameters.envMap ? '#define USE_ENVMAP' : '',
			parameters.envMap ? '#define ' + envMapTypeDefine : '',
			parameters.envMap ? '#define ' + envMapModeDefine : '',
			parameters.envMap ? '#define ' + envMapBlendingDefine : '',
			envMapCubeUVSize ? '#define CUBEUV_TEXEL_WIDTH ' + envMapCubeUVSize.texelWidth : '',
			envMapCubeUVSize ? '#define CUBEUV_TEXEL_HEIGHT ' + envMapCubeUVSize.texelHeight : '',
			envMapCubeUVSize ? '#define CUBEUV_MAX_MIP ' + envMapCubeUVSize.maxMip + '.0' : '',
			parameters.lightMap ? '#define USE_LIGHTMAP' : '',
			parameters.aoMap ? '#define USE_AOMAP' : '',
			parameters.bumpMap ? '#define USE_BUMPMAP' : '',
			parameters.normalMap ? '#define USE_NORMALMAP' : '',
			parameters.normalMapObjectSpace ? '#define USE_NORMALMAP_OBJECTSPACE' : '',
			parameters.normalMapTangentSpace ? '#define USE_NORMALMAP_TANGENTSPACE' : '',
			parameters.emissiveMap ? '#define USE_EMISSIVEMAP' : '',

			parameters.anisotropy ? '#define USE_ANISOTROPY' : '',
			parameters.anisotropyMap ? '#define USE_ANISOTROPYMAP' : '',

			parameters.clearcoat ? '#define USE_CLEARCOAT' : '',
			parameters.clearcoatMap ? '#define USE_CLEARCOATMAP' : '',
			parameters.clearcoatRoughnessMap ? '#define USE_CLEARCOAT_ROUGHNESSMAP' : '',
			parameters.clearcoatNormalMap ? '#define USE_CLEARCOAT_NORMALMAP' : '',

			parameters.dispersion ? '#define USE_DISPERSION' : '',

			parameters.iridescence ? '#define USE_IRIDESCENCE' : '',
			parameters.iridescenceMap ? '#define USE_IRIDESCENCEMAP' : '',
			parameters.iridescenceThicknessMap ? '#define USE_IRIDESCENCE_THICKNESSMAP' : '',

			parameters.specularMap ? '#define USE_SPECULARMAP' : '',
			parameters.specularColorMap ? '#define USE_SPECULAR_COLORMAP' : '',
			parameters.specularIntensityMap ? '#define USE_SPECULAR_INTENSITYMAP' : '',

			parameters.roughnessMap ? '#define USE_ROUGHNESSMAP' : '',
			parameters.metalnessMap ? '#define USE_METALNESSMAP' : '',

			parameters.alphaMap ? '#define USE_ALPHAMAP' : '',
			parameters.alphaTest ? '#define USE_ALPHATEST' : '',
			parameters.alphaHash ? '#define USE_ALPHAHASH' : '',

			parameters.sheen ? '#define USE_SHEEN' : '',
			parameters.sheenColorMap ? '#define USE_SHEEN_COLORMAP' : '',
			parameters.sheenRoughnessMap ? '#define USE_SHEEN_ROUGHNESSMAP' : '',

			parameters.transmission ? '#define USE_TRANSMISSION' : '',
			parameters.transmissionMap ? '#define USE_TRANSMISSIONMAP' : '',
			parameters.thicknessMap ? '#define USE_THICKNESSMAP' : '',

			parameters.vertexTangents && parameters.flatShading === false ? '#define USE_TANGENT' : '',
			parameters.vertexColors || parameters.instancingColor || parameters.batchingColor ? '#define USE_COLOR' : '',
			parameters.vertexAlphas ? '#define USE_COLOR_ALPHA' : '',
			parameters.vertexUv1s ? '#define USE_UV1' : '',
			parameters.vertexUv2s ? '#define USE_UV2' : '',
			parameters.vertexUv3s ? '#define USE_UV3' : '',

			parameters.pointsUvs ? '#define USE_POINTS_UV' : '',

			parameters.gradientMap ? '#define USE_GRADIENTMAP' : '',

			parameters.flatShading ? '#define FLAT_SHADED' : '',

			parameters.doubleSided ? '#define DOUBLE_SIDED' : '',
			parameters.flipSided ? '#define FLIP_SIDED' : '',

			parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',
			parameters.shadowMapEnabled ? '#define ' + shadowMapTypeDefine : '',

			parameters.premultipliedAlpha ? '#define PREMULTIPLIED_ALPHA' : '',

			parameters.numLightProbes > 0 ? '#define USE_LIGHT_PROBES' : '',

			parameters.decodeVideoTexture ? '#define DECODE_VIDEO_TEXTURE' : '',
			parameters.decodeVideoTextureEmissive ? '#define DECODE_VIDEO_TEXTURE_EMISSIVE' : '',

			parameters.logarithmicDepthBuffer ? '#define USE_LOGARITHMIC_DEPTH_BUFFER' : '',
			parameters.reversedDepthBuffer ? '#define USE_REVERSED_DEPTH_BUFFER' : '',

			'uniform mat4 viewMatrix;',
			'uniform vec3 cameraPosition;',
			'uniform bool isOrthographic;',

			( parameters.toneMapping !== NoToneMapping ) ? '#define TONE_MAPPING' : '',
			( parameters.toneMapping !== NoToneMapping ) ? ShaderChunk[ 'tonemapping_pars_fragment' ] : '', // this code is required here because it is used by the toneMapping() function defined below
			( parameters.toneMapping !== NoToneMapping ) ? getToneMappingFunction( 'toneMapping', parameters.toneMapping ) : '',

			parameters.dithering ? '#define DITHERING' : '',
			parameters.opaque ? '#define OPAQUE' : '',

			ShaderChunk[ 'colorspace_pars_fragment' ], // this code is required here because it is used by the various encoding/decoding function defined below
			getTexelEncodingFunction( 'linearToOutputTexel', parameters.outputColorSpace ),
			getLuminanceFunction(),

			parameters.useDepthPacking ? '#define DEPTH_PACKING ' + parameters.depthPacking : '',

			'\n'

		].filter( filterEmptyLine ).join( '\n' );

	}

	vertexShader = resolveIncludes( vertexShader );
	vertexShader = replaceLightNums( vertexShader, parameters );
	vertexShader = replaceClippingPlaneNums( vertexShader, parameters );

	fragmentShader = resolveIncludes( fragmentShader );
	fragmentShader = replaceLightNums( fragmentShader, parameters );
	fragmentShader = replaceClippingPlaneNums( fragmentShader, parameters );

	vertexShader = unrollLoops( vertexShader );
	fragmentShader = unrollLoops( fragmentShader );

	if ( parameters.isRawShaderMaterial !== true ) {

		// GLSL 3.0 conversion for built-in materials and ShaderMaterial

		versionString = '#version 300 es\n';

		prefixVertex = [
			customVertexExtensions,
			'#define attribute in',
			'#define varying out',
			'#define texture2D texture'
		].join( '\n' ) + '\n' + prefixVertex;

		prefixFragment = [
			'#define varying in',
			( parameters.glslVersion === GLSL3 ) ? '' : 'layout(location = 0) out highp vec4 pc_fragColor;',
			( parameters.glslVersion === GLSL3 ) ? '' : '#define gl_FragColor pc_fragColor',
			'#define gl_FragDepthEXT gl_FragDepth',
			'#define texture2D texture',
			'#define textureCube texture',
			'#define texture2DProj textureProj',
			'#define texture2DLodEXT textureLod',
			'#define texture2DProjLodEXT textureProjLod',
			'#define textureCubeLodEXT textureLod',
			'#define texture2DGradEXT textureGrad',
			'#define texture2DProjGradEXT textureProjGrad',
			'#define textureCubeGradEXT textureGrad'
		].join( '\n' ) + '\n' + prefixFragment;

	}

	const vertexGlsl = versionString + prefixVertex + vertexShader;
	const fragmentGlsl = versionString + prefixFragment + fragmentShader;

	// console.log( '*VERTEX*', vertexGlsl );
	// console.log( '*FRAGMENT*', fragmentGlsl );

	const glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl );
	const glFragmentShader = WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentGlsl );

	gl.attachShader( program, glVertexShader );
	gl.attachShader( program, glFragmentShader );

	// Force a particular attribute to index 0.

	if ( parameters.index0AttributeName !== undefined ) {

		gl.bindAttribLocation( program, 0, parameters.index0AttributeName );

	} else if ( parameters.morphTargets === true ) {

		// programs with morphTargets displace position out of attribute 0
		gl.bindAttribLocation( program, 0, 'position' );

	}

	gl.linkProgram( program );

	function onFirstUse( self ) {

		// check for link errors
		if ( renderer.debug.checkShaderErrors ) {

			const programInfoLog = gl.getProgramInfoLog( program ) || '';
			const vertexShaderInfoLog = gl.getShaderInfoLog( glVertexShader ) || '';
			const fragmentShaderInfoLog = gl.getShaderInfoLog( glFragmentShader ) || '';

			const programLog = programInfoLog.trim();
			const vertexLog = vertexShaderInfoLog.trim();
			const fragmentLog = fragmentShaderInfoLog.trim();

			let runnable = true;
			let haveDiagnostics = true;

			if ( gl.getProgramParameter( program, gl.LINK_STATUS ) === false ) {

				runnable = false;

				if ( typeof renderer.debug.onShaderError === 'function' ) {

					renderer.debug.onShaderError( gl, program, glVertexShader, glFragmentShader );

				} else {

					// default error reporting

					const vertexErrors = getShaderErrors( gl, glVertexShader, 'vertex' );
					const fragmentErrors = getShaderErrors( gl, glFragmentShader, 'fragment' );

					console.error(
						'THREE.WebGLProgram: Shader Error ' + gl.getError() + ' - ' +
						'VALIDATE_STATUS ' + gl.getProgramParameter( program, gl.VALIDATE_STATUS ) + '\n\n' +
						'Material Name: ' + self.name + '\n' +
						'Material Type: ' + self.type + '\n\n' +
						'Program Info Log: ' + programLog + '\n' +
						vertexErrors + '\n' +
						fragmentErrors
					);

				}

			} else if ( programLog !== '' ) {

				console.warn( 'THREE.WebGLProgram: Program Info Log:', programLog );

			} else if ( vertexLog === '' || fragmentLog === '' ) {

				haveDiagnostics = false;

			}

			if ( haveDiagnostics ) {

				self.diagnostics = {

					runnable: runnable,

					programLog: programLog,

					vertexShader: {

						log: vertexLog,
						prefix: prefixVertex

					},

					fragmentShader: {

						log: fragmentLog,
						prefix: prefixFragment

					}

				};

			}

		}

		// Clean up

		// Crashes in iOS9 and iOS10. #18402
		// gl.detachShader( program, glVertexShader );
		// gl.detachShader( program, glFragmentShader );

		gl.deleteShader( glVertexShader );
		gl.deleteShader( glFragmentShader );

		cachedUniforms = new WebGLUniforms( gl, program );
		cachedAttributes = fetchAttributeLocations( gl, program );

	}

	// set up caching for uniform locations

	let cachedUniforms;

	this.getUniforms = function () {

		if ( cachedUniforms === undefined ) {

			// Populates cachedUniforms and cachedAttributes
			onFirstUse( this );

		}

		return cachedUniforms;

	};

	// set up caching for attribute locations

	let cachedAttributes;

	this.getAttributes = function () {

		if ( cachedAttributes === undefined ) {

			// Populates cachedAttributes and cachedUniforms
			onFirstUse( this );

		}

		return cachedAttributes;

	};

	// indicate when the program is ready to be used. if the KHR_parallel_shader_compile extension isn't supported,
	// flag the program as ready immediately. It may cause a stall when it's first used.

	let programReady = ( parameters.rendererExtensionParallelShaderCompile === false );

	this.isReady = function () {

		if ( programReady === false ) {

			programReady = gl.getProgramParameter( program, COMPLETION_STATUS_KHR );

		}

		return programReady;

	};

	// free resource

	this.destroy = function () {

		bindingStates.releaseStatesOfProgram( this );

		gl.deleteProgram( program );
		this.program = undefined;

	};

	//

	this.type = parameters.shaderType;
	this.name = parameters.shaderName;
	this.id = programIdCount ++;
	this.cacheKey = cacheKey;
	this.usedTimes = 1;
	this.program = program;
	this.vertexShader = glVertexShader;
	this.fragmentShader = glFragmentShader;

	return this;

}

let _id = 0;

class WebGLShaderCache {

	constructor() {

		this.shaderCache = new Map();
		this.materialCache = new Map();

	}

	update( material ) {

		const vertexShader = material.vertexShader;
		const fragmentShader = material.fragmentShader;

		const vertexShaderStage = this._getShaderStage( vertexShader );
		const fragmentShaderStage = this._getShaderStage( fragmentShader );

		const materialShaders = this._getShaderCacheForMaterial( material );

		if ( materialShaders.has( vertexShaderStage ) === false ) {

			materialShaders.add( vertexShaderStage );
			vertexShaderStage.usedTimes ++;

		}

		if ( materialShaders.has( fragmentShaderStage ) === false ) {

			materialShaders.add( fragmentShaderStage );
			fragmentShaderStage.usedTimes ++;

		}

		return this;

	}

	remove( material ) {

		const materialShaders = this.materialCache.get( material );

		for ( const shaderStage of materialShaders ) {

			shaderStage.usedTimes --;

			if ( shaderStage.usedTimes === 0 ) this.shaderCache.delete( shaderStage.code );

		}

		this.materialCache.delete( material );

		return this;

	}

	getVertexShaderID( material ) {

		return this._getShaderStage( material.vertexShader ).id;

	}

	getFragmentShaderID( material ) {

		return this._getShaderStage( material.fragmentShader ).id;

	}

	dispose() {

		this.shaderCache.clear();
		this.materialCache.clear();

	}

	_getShaderCacheForMaterial( material ) {

		const cache = this.materialCache;
		let set = cache.get( material );

		if ( set === undefined ) {

			set = new Set();
			cache.set( material, set );

		}

		return set;

	}

	_getShaderStage( code ) {

		const cache = this.shaderCache;
		let stage = cache.get( code );

		if ( stage === undefined ) {

			stage = new WebGLShaderStage( code );
			cache.set( code, stage );

		}

		return stage;

	}

}

class WebGLShaderStage {

	constructor( code ) {

		this.id = _id ++;

		this.code = code;
		this.usedTimes = 0;

	}

}

function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities, bindingStates, clipping ) {

	const _programLayers = new Layers();
	const _customShaders = new WebGLShaderCache();
	const _activeChannels = new Set();
	const programs = [];

	const logarithmicDepthBuffer = capabilities.logarithmicDepthBuffer;
	const SUPPORTS_VERTEX_TEXTURES = capabilities.vertexTextures;

	let precision = capabilities.precision;

	const shaderIDs = {
		MeshDepthMaterial: 'depth',
		MeshDistanceMaterial: 'distanceRGBA',
		MeshNormalMaterial: 'normal',
		MeshBasicMaterial: 'basic',
		MeshLambertMaterial: 'lambert',
		MeshPhongMaterial: 'phong',
		MeshToonMaterial: 'toon',
		MeshStandardMaterial: 'physical',
		MeshPhysicalMaterial: 'physical',
		MeshMatcapMaterial: 'matcap',
		LineBasicMaterial: 'basic',
		LineDashedMaterial: 'dashed',
		PointsMaterial: 'points',
		ShadowMaterial: 'shadow',
		SpriteMaterial: 'sprite'
	};

	function getChannel( value ) {

		_activeChannels.add( value );

		if ( value === 0 ) return 'uv';

		return `uv${ value }`;

	}

	function getParameters( material, lights, shadows, scene, object ) {

		const fog = scene.fog;
		const geometry = object.geometry;
		const environment = material.isMeshStandardMaterial ? scene.environment : null;

		const envMap = ( material.isMeshStandardMaterial ? cubeuvmaps : cubemaps ).get( material.envMap || environment );
		const envMapCubeUVHeight = ( !! envMap ) && ( envMap.mapping === CubeUVReflectionMapping ) ? envMap.image.height : null;

		const shaderID = shaderIDs[ material.type ];

		// heuristics to create shader parameters according to lights in the scene
		// (not to blow over maxLights budget)

		if ( material.precision !== null ) {

			precision = capabilities.getMaxPrecision( material.precision );

			if ( precision !== material.precision ) {

				console.warn( 'THREE.WebGLProgram.getParameters:', material.precision, 'not supported, using', precision, 'instead.' );

			}

		}

		//

		const morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;
		const morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;

		let morphTextureStride = 0;

		if ( geometry.morphAttributes.position !== undefined ) morphTextureStride = 1;
		if ( geometry.morphAttributes.normal !== undefined ) morphTextureStride = 2;
		if ( geometry.morphAttributes.color !== undefined ) morphTextureStride = 3;

		//

		let vertexShader, fragmentShader;
		let customVertexShaderID, customFragmentShaderID;

		if ( shaderID ) {

			const shader = ShaderLib[ shaderID ];

			vertexShader = shader.vertexShader;
			fragmentShader = shader.fragmentShader;

		} else {

			vertexShader = material.vertexShader;
			fragmentShader = material.fragmentShader;

			_customShaders.update( material );

			customVertexShaderID = _customShaders.getVertexShaderID( material );
			customFragmentShaderID = _customShaders.getFragmentShaderID( material );

		}

		const currentRenderTarget = renderer.getRenderTarget();
		const reversedDepthBuffer = renderer.state.buffers.depth.getReversed();

		const IS_INSTANCEDMESH = object.isInstancedMesh === true;
		const IS_BATCHEDMESH = object.isBatchedMesh === true;

		const HAS_MAP = !! material.map;
		const HAS_MATCAP = !! material.matcap;
		const HAS_ENVMAP = !! envMap;
		const HAS_AOMAP = !! material.aoMap;
		const HAS_LIGHTMAP = !! material.lightMap;
		const HAS_BUMPMAP = !! material.bumpMap;
		const HAS_NORMALMAP = !! material.normalMap;
		const HAS_DISPLACEMENTMAP = !! material.displacementMap;
		const HAS_EMISSIVEMAP = !! material.emissiveMap;

		const HAS_METALNESSMAP = !! material.metalnessMap;
		const HAS_ROUGHNESSMAP = !! material.roughnessMap;

		const HAS_ANISOTROPY = material.anisotropy > 0;
		const HAS_CLEARCOAT = material.clearcoat > 0;
		const HAS_DISPERSION = material.dispersion > 0;
		const HAS_IRIDESCENCE = material.iridescence > 0;
		const HAS_SHEEN = material.sheen > 0;
		const HAS_TRANSMISSION = material.transmission > 0;

		const HAS_ANISOTROPYMAP = HAS_ANISOTROPY && !! material.anisotropyMap;

		const HAS_CLEARCOATMAP = HAS_CLEARCOAT && !! material.clearcoatMap;
		const HAS_CLEARCOAT_NORMALMAP = HAS_CLEARCOAT && !! material.clearcoatNormalMap;
		const HAS_CLEARCOAT_ROUGHNESSMAP = HAS_CLEARCOAT && !! material.clearcoatRoughnessMap;

		const HAS_IRIDESCENCEMAP = HAS_IRIDESCENCE && !! material.iridescenceMap;
		const HAS_IRIDESCENCE_THICKNESSMAP = HAS_IRIDESCENCE && !! material.iridescenceThicknessMap;

		const HAS_SHEEN_COLORMAP = HAS_SHEEN && !! material.sheenColorMap;
		const HAS_SHEEN_ROUGHNESSMAP = HAS_SHEEN && !! material.sheenRoughnessMap;

		const HAS_SPECULARMAP = !! material.specularMap;
		const HAS_SPECULAR_COLORMAP = !! material.specularColorMap;
		const HAS_SPECULAR_INTENSITYMAP = !! material.specularIntensityMap;

		const HAS_TRANSMISSIONMAP = HAS_TRANSMISSION && !! material.transmissionMap;
		const HAS_THICKNESSMAP = HAS_TRANSMISSION && !! material.thicknessMap;

		const HAS_GRADIENTMAP = !! material.gradientMap;

		const HAS_ALPHAMAP = !! material.alphaMap;

		const HAS_ALPHATEST = material.alphaTest > 0;

		const HAS_ALPHAHASH = !! material.alphaHash;

		const HAS_EXTENSIONS = !! material.extensions;

		let toneMapping = NoToneMapping;

		if ( material.toneMapped ) {

			if ( currentRenderTarget === null || currentRenderTarget.isXRRenderTarget === true ) {

				toneMapping = renderer.toneMapping;

			}

		}

		const parameters = {

			shaderID: shaderID,
			shaderType: material.type,
			shaderName: material.name,

			vertexShader: vertexShader,
			fragmentShader: fragmentShader,
			defines: material.defines,

			customVertexShaderID: customVertexShaderID,
			customFragmentShaderID: customFragmentShaderID,

			isRawShaderMaterial: material.isRawShaderMaterial === true,
			glslVersion: material.glslVersion,

			precision: precision,

			batching: IS_BATCHEDMESH,
			batchingColor: IS_BATCHEDMESH && object._colorsTexture !== null,
			instancing: IS_INSTANCEDMESH,
			instancingColor: IS_INSTANCEDMESH && object.instanceColor !== null,
			instancingMorph: IS_INSTANCEDMESH && object.morphTexture !== null,

			supportsVertexTextures: SUPPORTS_VERTEX_TEXTURES,
			outputColorSpace: ( currentRenderTarget === null ) ? renderer.outputColorSpace : ( currentRenderTarget.isXRRenderTarget === true ? currentRenderTarget.texture.colorSpace : LinearSRGBColorSpace ),
			alphaToCoverage: !! material.alphaToCoverage,

			map: HAS_MAP,
			matcap: HAS_MATCAP,
			envMap: HAS_ENVMAP,
			envMapMode: HAS_ENVMAP && envMap.mapping,
			envMapCubeUVHeight: envMapCubeUVHeight,
			aoMap: HAS_AOMAP,
			lightMap: HAS_LIGHTMAP,
			bumpMap: HAS_BUMPMAP,
			normalMap: HAS_NORMALMAP,
			displacementMap: SUPPORTS_VERTEX_TEXTURES && HAS_DISPLACEMENTMAP,
			emissiveMap: HAS_EMISSIVEMAP,

			normalMapObjectSpace: HAS_NORMALMAP && material.normalMapType === ObjectSpaceNormalMap,
			normalMapTangentSpace: HAS_NORMALMAP && material.normalMapType === TangentSpaceNormalMap,

			metalnessMap: HAS_METALNESSMAP,
			roughnessMap: HAS_ROUGHNESSMAP,

			anisotropy: HAS_ANISOTROPY,
			anisotropyMap: HAS_ANISOTROPYMAP,

			clearcoat: HAS_CLEARCOAT,
			clearcoatMap: HAS_CLEARCOATMAP,
			clearcoatNormalMap: HAS_CLEARCOAT_NORMALMAP,
			clearcoatRoughnessMap: HAS_CLEARCOAT_ROUGHNESSMAP,

			dispersion: HAS_DISPERSION,

			iridescence: HAS_IRIDESCENCE,
			iridescenceMap: HAS_IRIDESCENCEMAP,
			iridescenceThicknessMap: HAS_IRIDESCENCE_THICKNESSMAP,

			sheen: HAS_SHEEN,
			sheenColorMap: HAS_SHEEN_COLORMAP,
			sheenRoughnessMap: HAS_SHEEN_ROUGHNESSMAP,

			specularMap: HAS_SPECULARMAP,
			specularColorMap: HAS_SPECULAR_COLORMAP,
			specularIntensityMap: HAS_SPECULAR_INTENSITYMAP,

			transmission: HAS_TRANSMISSION,
			transmissionMap: HAS_TRANSMISSIONMAP,
			thicknessMap: HAS_THICKNESSMAP,

			gradientMap: HAS_GRADIENTMAP,

			opaque: material.transparent === false && material.blending === NormalBlending && material.alphaToCoverage === false,

			alphaMap: HAS_ALPHAMAP,
			alphaTest: HAS_ALPHATEST,
			alphaHash: HAS_ALPHAHASH,

			combine: material.combine,

			//

			mapUv: HAS_MAP && getChannel( material.map.channel ),
			aoMapUv: HAS_AOMAP && getChannel( material.aoMap.channel ),
			lightMapUv: HAS_LIGHTMAP && getChannel( material.lightMap.channel ),
			bumpMapUv: HAS_BUMPMAP && getChannel( material.bumpMap.channel ),
			normalMapUv: HAS_NORMALMAP && getChannel( material.normalMap.channel ),
			displacementMapUv: HAS_DISPLACEMENTMAP && getChannel( material.displacementMap.channel ),
			emissiveMapUv: HAS_EMISSIVEMAP && getChannel( material.emissiveMap.channel ),

			metalnessMapUv: HAS_METALNESSMAP && getChannel( material.metalnessMap.channel ),
			roughnessMapUv: HAS_ROUGHNESSMAP && getChannel( material.roughnessMap.channel ),

			anisotropyMapUv: HAS_ANISOTROPYMAP && getChannel( material.anisotropyMap.channel ),

			clearcoatMapUv: HAS_CLEARCOATMAP && getChannel( material.clearcoatMap.channel ),
			clearcoatNormalMapUv: HAS_CLEARCOAT_NORMALMAP && getChannel( material.clearcoatNormalMap.channel ),
			clearcoatRoughnessMapUv: HAS_CLEARCOAT_ROUGHNESSMAP && getChannel( material.clearcoatRoughnessMap.channel ),

			iridescenceMapUv: HAS_IRIDESCENCEMAP && getChannel( material.iridescenceMap.channel ),
			iridescenceThicknessMapUv: HAS_IRIDESCENCE_THICKNESSMAP && getChannel( material.iridescenceThicknessMap.channel ),

			sheenColorMapUv: HAS_SHEEN_COLORMAP && getChannel( material.sheenColorMap.channel ),
			sheenRoughnessMapUv: HAS_SHEEN_ROUGHNESSMAP && getChannel( material.sheenRoughnessMap.channel ),

			specularMapUv: HAS_SPECULARMAP && getChannel( material.specularMap.channel ),
			specularColorMapUv: HAS_SPECULAR_COLORMAP && getChannel( material.specularColorMap.channel ),
			specularIntensityMapUv: HAS_SPECULAR_INTENSITYMAP && getChannel( material.specularIntensityMap.channel ),

			transmissionMapUv: HAS_TRANSMISSIONMAP && getChannel( material.transmissionMap.channel ),
			thicknessMapUv: HAS_THICKNESSMAP && getChannel( material.thicknessMap.channel ),

			alphaMapUv: HAS_ALPHAMAP && getChannel( material.alphaMap.channel ),

			//

			vertexTangents: !! geometry.attributes.tangent && ( HAS_NORMALMAP || HAS_ANISOTROPY ),
			vertexColors: material.vertexColors,
			vertexAlphas: material.vertexColors === true && !! geometry.attributes.color && geometry.attributes.color.itemSize === 4,

			pointsUvs: object.isPoints === true && !! geometry.attributes.uv && ( HAS_MAP || HAS_ALPHAMAP ),

			fog: !! fog,
			useFog: material.fog === true,
			fogExp2: ( !! fog && fog.isFogExp2 ),

			flatShading: ( material.flatShading === true && material.wireframe === false ),

			sizeAttenuation: material.sizeAttenuation === true,
			logarithmicDepthBuffer: logarithmicDepthBuffer,
			reversedDepthBuffer: reversedDepthBuffer,

			skinning: object.isSkinnedMesh === true,

			morphTargets: geometry.morphAttributes.position !== undefined,
			morphNormals: geometry.morphAttributes.normal !== undefined,
			morphColors: geometry.morphAttributes.color !== undefined,
			morphTargetsCount: morphTargetsCount,
			morphTextureStride: morphTextureStride,

			numDirLights: lights.directional.length,
			numPointLights: lights.point.length,
			numSpotLights: lights.spot.length,
			numSpotLightMaps: lights.spotLightMap.length,
			numRectAreaLights: lights.rectArea.length,
			numHemiLights: lights.hemi.length,

			numDirLightShadows: lights.directionalShadowMap.length,
			numPointLightShadows: lights.pointShadowMap.length,
			numSpotLightShadows: lights.spotShadowMap.length,
			numSpotLightShadowsWithMaps: lights.numSpotLightShadowsWithMaps,

			numLightProbes: lights.numLightProbes,

			numClippingPlanes: clipping.numPlanes,
			numClipIntersection: clipping.numIntersection,

			dithering: material.dithering,

			shadowMapEnabled: renderer.shadowMap.enabled && shadows.length > 0,
			shadowMapType: renderer.shadowMap.type,

			toneMapping: toneMapping,

			decodeVideoTexture: HAS_MAP && ( material.map.isVideoTexture === true ) && ( ColorManagement.getTransfer( material.map.colorSpace ) === SRGBTransfer ),
			decodeVideoTextureEmissive: HAS_EMISSIVEMAP && ( material.emissiveMap.isVideoTexture === true ) && ( ColorManagement.getTransfer( material.emissiveMap.colorSpace ) === SRGBTransfer ),

			premultipliedAlpha: material.premultipliedAlpha,

			doubleSided: material.side === DoubleSide,
			flipSided: material.side === BackSide,

			useDepthPacking: material.depthPacking >= 0,
			depthPacking: material.depthPacking || 0,

			index0AttributeName: material.index0AttributeName,

			extensionClipCullDistance: HAS_EXTENSIONS && material.extensions.clipCullDistance === true && extensions.has( 'WEBGL_clip_cull_distance' ),
			extensionMultiDraw: ( HAS_EXTENSIONS && material.extensions.multiDraw === true || IS_BATCHEDMESH ) && extensions.has( 'WEBGL_multi_draw' ),

			rendererExtensionParallelShaderCompile: extensions.has( 'KHR_parallel_shader_compile' ),

			customProgramCacheKey: material.customProgramCacheKey()

		};

		// the usage of getChannel() determines the active texture channels for this shader

		parameters.vertexUv1s = _activeChannels.has( 1 );
		parameters.vertexUv2s = _activeChannels.has( 2 );
		parameters.vertexUv3s = _activeChannels.has( 3 );

		_activeChannels.clear();

		return parameters;

	}

	function getProgramCacheKey( parameters ) {

		const array = [];

		if ( parameters.shaderID ) {

			array.push( parameters.shaderID );

		} else {

			array.push( parameters.customVertexShaderID );
			array.push( parameters.customFragmentShaderID );

		}

		if ( parameters.defines !== undefined ) {

			for ( const name in parameters.defines ) {

				array.push( name );
				array.push( parameters.defines[ name ] );

			}

		}

		if ( parameters.isRawShaderMaterial === false ) {

			getProgramCacheKeyParameters( array, parameters );
			getProgramCacheKeyBooleans( array, parameters );
			array.push( renderer.outputColorSpace );

		}

		array.push( parameters.customProgramCacheKey );

		return array.join();

	}

	function getProgramCacheKeyParameters( array, parameters ) {

		array.push( parameters.precision );
		array.push( parameters.outputColorSpace );
		array.push( parameters.envMapMode );
		array.push( parameters.envMapCubeUVHeight );
		array.push( parameters.mapUv );
		array.push( parameters.alphaMapUv );
		array.push( parameters.lightMapUv );
		array.push( parameters.aoMapUv );
		array.push( parameters.bumpMapUv );
		array.push( parameters.normalMapUv );
		array.push( parameters.displacementMapUv );
		array.push( parameters.emissiveMapUv );
		array.push( parameters.metalnessMapUv );
		array.push( parameters.roughnessMapUv );
		array.push( parameters.anisotropyMapUv );
		array.push( parameters.clearcoatMapUv );
		array.push( parameters.clearcoatNormalMapUv );
		array.push( parameters.clearcoatRoughnessMapUv );
		array.push( parameters.iridescenceMapUv );
		array.push( parameters.iridescenceThicknessMapUv );
		array.push( parameters.sheenColorMapUv );
		array.push( parameters.sheenRoughnessMapUv );
		array.push( parameters.specularMapUv );
		array.push( parameters.specularColorMapUv );
		array.push( parameters.specularIntensityMapUv );
		array.push( parameters.transmissionMapUv );
		array.push( parameters.thicknessMapUv );
		array.push( parameters.combine );
		array.push( parameters.fogExp2 );
		array.push( parameters.sizeAttenuation );
		array.push( parameters.morphTargetsCount );
		array.push( parameters.morphAttributeCount );
		array.push( parameters.numDirLights );
		array.push( parameters.numPointLights );
		array.push( parameters.numSpotLights );
		array.push( parameters.numSpotLightMaps );
		array.push( parameters.numHemiLights );
		array.push( parameters.numRectAreaLights );
		array.push( parameters.numDirLightShadows );
		array.push( parameters.numPointLightShadows );
		array.push( parameters.numSpotLightShadows );
		array.push( parameters.numSpotLightShadowsWithMaps );
		array.push( parameters.numLightProbes );
		array.push( parameters.shadowMapType );
		array.push( parameters.toneMapping );
		array.push( parameters.numClippingPlanes );
		array.push( parameters.numClipIntersection );
		array.push( parameters.depthPacking );

	}

	function getProgramCacheKeyBooleans( array, parameters ) {

		_programLayers.disableAll();

		if ( parameters.supportsVertexTextures )
			_programLayers.enable( 0 );
		if ( parameters.instancing )
			_programLayers.enable( 1 );
		if ( parameters.instancingColor )
			_programLayers.enable( 2 );
		if ( parameters.instancingMorph )
			_programLayers.enable( 3 );
		if ( parameters.matcap )
			_programLayers.enable( 4 );
		if ( parameters.envMap )
			_programLayers.enable( 5 );
		if ( parameters.normalMapObjectSpace )
			_programLayers.enable( 6 );
		if ( parameters.normalMapTangentSpace )
			_programLayers.enable( 7 );
		if ( parameters.clearcoat )
			_programLayers.enable( 8 );
		if ( parameters.iridescence )
			_programLayers.enable( 9 );
		if ( parameters.alphaTest )
			_programLayers.enable( 10 );
		if ( parameters.vertexColors )
			_programLayers.enable( 11 );
		if ( parameters.vertexAlphas )
			_programLayers.enable( 12 );
		if ( parameters.vertexUv1s )
			_programLayers.enable( 13 );
		if ( parameters.vertexUv2s )
			_programLayers.enable( 14 );
		if ( parameters.vertexUv3s )
			_programLayers.enable( 15 );
		if ( parameters.vertexTangents )
			_programLayers.enable( 16 );
		if ( parameters.anisotropy )
			_programLayers.enable( 17 );
		if ( parameters.alphaHash )
			_programLayers.enable( 18 );
		if ( parameters.batching )
			_programLayers.enable( 19 );
		if ( parameters.dispersion )
			_programLayers.enable( 20 );
		if ( parameters.batchingColor )
			_programLayers.enable( 21 );
		if ( parameters.gradientMap )
			_programLayers.enable( 22 );

		array.push( _programLayers.mask );
		_programLayers.disableAll();

		if ( parameters.fog )
			_programLayers.enable( 0 );
		if ( parameters.useFog )
			_programLayers.enable( 1 );
		if ( parameters.flatShading )
			_programLayers.enable( 2 );
		if ( parameters.logarithmicDepthBuffer )
			_programLayers.enable( 3 );
		if ( parameters.reversedDepthBuffer )
			_programLayers.enable( 4 );
		if ( parameters.skinning )
			_programLayers.enable( 5 );
		if ( parameters.morphTargets )
			_programLayers.enable( 6 );
		if ( parameters.morphNormals )
			_programLayers.enable( 7 );
		if ( parameters.morphColors )
			_programLayers.enable( 8 );
		if ( parameters.premultipliedAlpha )
			_programLayers.enable( 9 );
		if ( parameters.shadowMapEnabled )
			_programLayers.enable( 10 );
		if ( parameters.doubleSided )
			_programLayers.enable( 11 );
		if ( parameters.flipSided )
			_programLayers.enable( 12 );
		if ( parameters.useDepthPacking )
			_programLayers.enable( 13 );
		if ( parameters.dithering )
			_programLayers.enable( 14 );
		if ( parameters.transmission )
			_programLayers.enable( 15 );
		if ( parameters.sheen )
			_programLayers.enable( 16 );
		if ( parameters.opaque )
			_programLayers.enable( 17 );
		if ( parameters.pointsUvs )
			_programLayers.enable( 18 );
		if ( parameters.decodeVideoTexture )
			_programLayers.enable( 19 );
		if ( parameters.decodeVideoTextureEmissive )
			_programLayers.enable( 20 );
		if ( parameters.alphaToCoverage )
			_programLayers.enable( 21 );

		array.push( _programLayers.mask );

	}

	function getUniforms( material ) {

		const shaderID = shaderIDs[ material.type ];
		let uniforms;

		if ( shaderID ) {

			const shader = ShaderLib[ shaderID ];
			uniforms = UniformsUtils.clone( shader.uniforms );

		} else {

			uniforms = material.uniforms;

		}

		return uniforms;

	}

	function acquireProgram( parameters, cacheKey ) {

		let program;

		// Check if code has been already compiled
		for ( let p = 0, pl = programs.length; p < pl; p ++ ) {

			const preexistingProgram = programs[ p ];

			if ( preexistingProgram.cacheKey === cacheKey ) {

				program = preexistingProgram;
				++ program.usedTimes;

				break;

			}

		}

		if ( program === undefined ) {

			program = new WebGLProgram( renderer, cacheKey, parameters, bindingStates );
			programs.push( program );

		}

		return program;

	}

	function releaseProgram( program ) {

		if ( -- program.usedTimes === 0 ) {

			// Remove from unordered set
			const i = programs.indexOf( program );
			programs[ i ] = programs[ programs.length - 1 ];
			programs.pop();

			// Free WebGL resources
			program.destroy();

		}

	}

	function releaseShaderCache( material ) {

		_customShaders.remove( material );

	}

	function dispose() {

		_customShaders.dispose();

	}

	return {
		getParameters: getParameters,
		getProgramCacheKey: getProgramCacheKey,
		getUniforms: getUniforms,
		acquireProgram: acquireProgram,
		releaseProgram: releaseProgram,
		releaseShaderCache: releaseShaderCache,
		// Exposed for resource monitoring & error feedback via renderer.info:
		programs: programs,
		dispose: dispose
	};

}

function WebGLProperties() {

	let properties = new WeakMap();

	function has( object ) {

		return properties.has( object );

	}

	function get( object ) {

		let map = properties.get( object );

		if ( map === undefined ) {

			map = {};
			properties.set( object, map );

		}

		return map;

	}

	function remove( object ) {

		properties.delete( object );

	}

	function update( object, key, value ) {

		properties.get( object )[ key ] = value;

	}

	function dispose() {

		properties = new WeakMap();

	}

	return {
		has: has,
		get: get,
		remove: remove,
		update: update,
		dispose: dispose
	};

}

function painterSortStable( a, b ) {

	if ( a.groupOrder !== b.groupOrder ) {

		return a.groupOrder - b.groupOrder;

	} else if ( a.renderOrder !== b.renderOrder ) {

		return a.renderOrder - b.renderOrder;

	} else if ( a.material.id !== b.material.id ) {

		return a.material.id - b.material.id;

	} else if ( a.z !== b.z ) {

		return a.z - b.z;

	} else {

		return a.id - b.id;

	}

}

function reversePainterSortStable( a, b ) {

	if ( a.groupOrder !== b.groupOrder ) {

		return a.groupOrder - b.groupOrder;

	} else if ( a.renderOrder !== b.renderOrder ) {

		return a.renderOrder - b.renderOrder;

	} else if ( a.z !== b.z ) {

		return b.z - a.z;

	} else {

		return a.id - b.id;

	}

}


function WebGLRenderList() {

	const renderItems = [];
	let renderItemsIndex = 0;

	const opaque = [];
	const transmissive = [];
	const transparent = [];

	function init() {

		renderItemsIndex = 0;

		opaque.length = 0;
		transmissive.length = 0;
		transparent.length = 0;

	}

	function getNextRenderItem( object, geometry, material, groupOrder, z, group ) {

		let renderItem = renderItems[ renderItemsIndex ];

		if ( renderItem === undefined ) {

			renderItem = {
				id: object.id,
				object: object,
				geometry: geometry,
				material: material,
				groupOrder: groupOrder,
				renderOrder: object.renderOrder,
				z: z,
				group: group
			};

			renderItems[ renderItemsIndex ] = renderItem;

		} else {

			renderItem.id = object.id;
			renderItem.object = object;
			renderItem.geometry = geometry;
			renderItem.material = material;
			renderItem.groupOrder = groupOrder;
			renderItem.renderOrder = object.renderOrder;
			renderItem.z = z;
			renderItem.group = group;

		}

		renderItemsIndex ++;

		return renderItem;

	}

	function push( object, geometry, material, groupOrder, z, group ) {

		const renderItem = getNextRenderItem( object, geometry, material, groupOrder, z, group );

		if ( material.transmission > 0.0 ) {

			transmissive.push( renderItem );

		} else if ( material.transparent === true ) {

			transparent.push( renderItem );

		} else {

			opaque.push( renderItem );

		}

	}

	function unshift( object, geometry, material, groupOrder, z, group ) {

		const renderItem = getNextRenderItem( object, geometry, material, groupOrder, z, group );

		if ( material.transmission > 0.0 ) {

			transmissive.unshift( renderItem );

		} else if ( material.transparent === true ) {

			transparent.unshift( renderItem );

		} else {

			opaque.unshift( renderItem );

		}

	}

	function sort( customOpaqueSort, customTransparentSort ) {

		if ( opaque.length > 1 ) opaque.sort( customOpaqueSort || painterSortStable );
		if ( transmissive.length > 1 ) transmissive.sort( customTransparentSort || reversePainterSortStable );
		if ( transparent.length > 1 ) transparent.sort( customTransparentSort || reversePainterSortStable );

	}

	function finish() {

		// Clear references from inactive renderItems in the list

		for ( let i = renderItemsIndex, il = renderItems.length; i < il; i ++ ) {

			const renderItem = renderItems[ i ];

			if ( renderItem.id === null ) break;

			renderItem.id = null;
			renderItem.object = null;
			renderItem.geometry = null;
			renderItem.material = null;
			renderItem.group = null;

		}

	}

	return {

		opaque: opaque,
		transmissive: transmissive,
		transparent: transparent,

		init: init,
		push: push,
		unshift: unshift,
		finish: finish,

		sort: sort
	};

}

function WebGLRenderLists() {

	let lists = new WeakMap();

	function get( scene, renderCallDepth ) {

		const listArray = lists.get( scene );
		let list;

		if ( listArray === undefined ) {

			list = new WebGLRenderList();
			lists.set( scene, [ list ] );

		} else {

			if ( renderCallDepth >= listArray.length ) {

				list = new WebGLRenderList();
				listArray.push( list );

			} else {

				list = listArray[ renderCallDepth ];

			}

		}

		return list;

	}

	function dispose() {

		lists = new WeakMap();

	}

	return {
		get: get,
		dispose: dispose
	};

}

function UniformsCache() {

	const lights = {};

	return {

		get: function ( light ) {

			if ( lights[ light.id ] !== undefined ) {

				return lights[ light.id ];

			}

			let uniforms;

			switch ( light.type ) {

				case 'DirectionalLight':
					uniforms = {
						direction: new Vector3(),
						color: new Color()
					};
					break;

				case 'SpotLight':
					uniforms = {
						position: new Vector3(),
						direction: new Vector3(),
						color: new Color(),
						distance: 0,
						coneCos: 0,
						penumbraCos: 0,
						decay: 0
					};
					break;

				case 'PointLight':
					uniforms = {
						position: new Vector3(),
						color: new Color(),
						distance: 0,
						decay: 0
					};
					break;

				case 'HemisphereLight':
					uniforms = {
						direction: new Vector3(),
						skyColor: new Color(),
						groundColor: new Color()
					};
					break;

				case 'RectAreaLight':
					uniforms = {
						color: new Color(),
						position: new Vector3(),
						halfWidth: new Vector3(),
						halfHeight: new Vector3()
					};
					break;

			}

			lights[ light.id ] = uniforms;

			return uniforms;

		}

	};

}

function ShadowUniformsCache() {

	const lights = {};

	return {

		get: function ( light ) {

			if ( lights[ light.id ] !== undefined ) {

				return lights[ light.id ];

			}

			let uniforms;

			switch ( light.type ) {

				case 'DirectionalLight':
					uniforms = {
						shadowIntensity: 1,
						shadowBias: 0,
						shadowNormalBias: 0,
						shadowRadius: 1,
						shadowMapSize: new Vector2()
					};
					break;

				case 'SpotLight':
					uniforms = {
						shadowIntensity: 1,
						shadowBias: 0,
						shadowNormalBias: 0,
						shadowRadius: 1,
						shadowMapSize: new Vector2()
					};
					break;

				case 'PointLight':
					uniforms = {
						shadowIntensity: 1,
						shadowBias: 0,
						shadowNormalBias: 0,
						shadowRadius: 1,
						shadowMapSize: new Vector2(),
						shadowCameraNear: 1,
						shadowCameraFar: 1000
					};
					break;

				// TODO (abelnation): set RectAreaLight shadow uniforms

			}

			lights[ light.id ] = uniforms;

			return uniforms;

		}

	};

}



let nextVersion = 0;

function shadowCastingAndTexturingLightsFirst( lightA, lightB ) {

	return ( lightB.castShadow ? 2 : 0 ) - ( lightA.castShadow ? 2 : 0 ) + ( lightB.map ? 1 : 0 ) - ( lightA.map ? 1 : 0 );

}

function WebGLLights( extensions ) {

	const cache = new UniformsCache();

	const shadowCache = ShadowUniformsCache();

	const state = {

		version: 0,

		hash: {
			directionalLength: -1,
			pointLength: -1,
			spotLength: -1,
			rectAreaLength: -1,
			hemiLength: -1,

			numDirectionalShadows: -1,
			numPointShadows: -1,
			numSpotShadows: -1,
			numSpotMaps: -1,

			numLightProbes: -1
		},

		ambient: [ 0, 0, 0 ],
		probe: [],
		directional: [],
		directionalShadow: [],
		directionalShadowMap: [],
		directionalShadowMatrix: [],
		spot: [],
		spotLightMap: [],
		spotShadow: [],
		spotShadowMap: [],
		spotLightMatrix: [],
		rectArea: [],
		rectAreaLTC1: null,
		rectAreaLTC2: null,
		point: [],
		pointShadow: [],
		pointShadowMap: [],
		pointShadowMatrix: [],
		hemi: [],
		numSpotLightShadowsWithMaps: 0,
		numLightProbes: 0

	};

	for ( let i = 0; i < 9; i ++ ) state.probe.push( new Vector3() );

	const vector3 = new Vector3();
	const matrix4 = new Matrix4();
	const matrix42 = new Matrix4();

	function setup( lights ) {

		let r = 0, g = 0, b = 0;

		for ( let i = 0; i < 9; i ++ ) state.probe[ i ].set( 0, 0, 0 );

		let directionalLength = 0;
		let pointLength = 0;
		let spotLength = 0;
		let rectAreaLength = 0;
		let hemiLength = 0;

		let numDirectionalShadows = 0;
		let numPointShadows = 0;
		let numSpotShadows = 0;
		let numSpotMaps = 0;
		let numSpotShadowsWithMaps = 0;

		let numLightProbes = 0;

		// ordering : [shadow casting + map texturing, map texturing, shadow casting, none ]
		lights.sort( shadowCastingAndTexturingLightsFirst );

		for ( let i = 0, l = lights.length; i < l; i ++ ) {

			const light = lights[ i ];

			const color = light.color;
			const intensity = light.intensity;
			const distance = light.distance;

			const shadowMap = ( light.shadow && light.shadow.map ) ? light.shadow.map.texture : null;

			if ( light.isAmbientLight ) {

				r += color.r * intensity;
				g += color.g * intensity;
				b += color.b * intensity;

			} else if ( light.isLightProbe ) {

				for ( let j = 0; j < 9; j ++ ) {

					state.probe[ j ].addScaledVector( light.sh.coefficients[ j ], intensity );

				}

				numLightProbes ++;

			} else if ( light.isDirectionalLight ) {

				const uniforms = cache.get( light );

				uniforms.color.copy( light.color ).multiplyScalar( light.intensity );

				if ( light.castShadow ) {

					const shadow = light.shadow;

					const shadowUniforms = shadowCache.get( light );

					shadowUniforms.shadowIntensity = shadow.intensity;
					shadowUniforms.shadowBias = shadow.bias;
					shadowUniforms.shadowNormalBias = shadow.normalBias;
					shadowUniforms.shadowRadius = shadow.radius;
					shadowUniforms.shadowMapSize = shadow.mapSize;

					state.directionalShadow[ directionalLength ] = shadowUniforms;
					state.directionalShadowMap[ directionalLength ] = shadowMap;
					state.directionalShadowMatrix[ directionalLength ] = light.shadow.matrix;

					numDirectionalShadows ++;

				}

				state.directional[ directionalLength ] = uniforms;

				directionalLength ++;

			} else if ( light.isSpotLight ) {

				const uniforms = cache.get( light );

				uniforms.position.setFromMatrixPosition( light.matrixWorld );

				uniforms.color.copy( color ).multiplyScalar( intensity );
				uniforms.distance = distance;

				uniforms.coneCos = Math.cos( light.angle );
				uniforms.penumbraCos = Math.cos( light.angle * ( 1 - light.penumbra ) );
				uniforms.decay = light.decay;

				state.spot[ spotLength ] = uniforms;

				const shadow = light.shadow;

				if ( light.map ) {

					state.spotLightMap[ numSpotMaps ] = light.map;
					numSpotMaps ++;

					// make sure the lightMatrix is up to date
					// TODO : do it if required only
					shadow.updateMatrices( light );

					if ( light.castShadow ) numSpotShadowsWithMaps ++;

				}

				state.spotLightMatrix[ spotLength ] = shadow.matrix;

				if ( light.castShadow ) {

					const shadowUniforms = shadowCache.get( light );

					shadowUniforms.shadowIntensity = shadow.intensity;
					shadowUniforms.shadowBias = shadow.bias;
					shadowUniforms.shadowNormalBias = shadow.normalBias;
					shadowUniforms.shadowRadius = shadow.radius;
					shadowUniforms.shadowMapSize = shadow.mapSize;

					state.spotShadow[ spotLength ] = shadowUniforms;
					state.spotShadowMap[ spotLength ] = shadowMap;

					numSpotShadows ++;

				}

				spotLength ++;

			} else if ( light.isRectAreaLight ) {

				const uniforms = cache.get( light );

				uniforms.color.copy( color ).multiplyScalar( intensity );

				uniforms.halfWidth.set( light.width * 0.5, 0.0, 0.0 );
				uniforms.halfHeight.set( 0.0, light.height * 0.5, 0.0 );

				state.rectArea[ rectAreaLength ] = uniforms;

				rectAreaLength ++;

			} else if ( light.isPointLight ) {

				const uniforms = cache.get( light );

				uniforms.color.copy( light.color ).multiplyScalar( light.intensity );
				uniforms.distance = light.distance;
				uniforms.decay = light.decay;

				if ( light.castShadow ) {

					const shadow = light.shadow;

					const shadowUniforms = shadowCache.get( light );

					shadowUniforms.shadowIntensity = shadow.intensity;
					shadowUniforms.shadowBias = shadow.bias;
					shadowUniforms.shadowNormalBias = shadow.normalBias;
					shadowUniforms.shadowRadius = shadow.radius;
					shadowUniforms.shadowMapSize = shadow.mapSize;
					shadowUniforms.shadowCameraNear = shadow.camera.near;
					shadowUniforms.shadowCameraFar = shadow.camera.far;

					state.pointShadow[ pointLength ] = shadowUniforms;
					state.pointShadowMap[ pointLength ] = shadowMap;
					state.pointShadowMatrix[ pointLength ] = light.shadow.matrix;

					numPointShadows ++;

				}

				state.point[ pointLength ] = uniforms;

				pointLength ++;

			} else if ( light.isHemisphereLight ) {

				const uniforms = cache.get( light );

				uniforms.skyColor.copy( light.color ).multiplyScalar( intensity );
				uniforms.groundColor.copy( light.groundColor ).multiplyScalar( intensity );

				state.hemi[ hemiLength ] = uniforms;

				hemiLength ++;

			}

		}

		if ( rectAreaLength > 0 ) {

			if ( extensions.has( 'OES_texture_float_linear' ) === true ) {

				state.rectAreaLTC1 = UniformsLib.LTC_FLOAT_1;
				state.rectAreaLTC2 = UniformsLib.LTC_FLOAT_2;

			} else {

				state.rectAreaLTC1 = UniformsLib.LTC_HALF_1;
				state.rectAreaLTC2 = UniformsLib.LTC_HALF_2;

			}

		}

		state.ambient[ 0 ] = r;
		state.ambient[ 1 ] = g;
		state.ambient[ 2 ] = b;

		const hash = state.hash;

		if ( hash.directionalLength !== directionalLength ||
			hash.pointLength !== pointLength ||
			hash.spotLength !== spotLength ||
			hash.rectAreaLength !== rectAreaLength ||
			hash.hemiLength !== hemiLength ||
			hash.numDirectionalShadows !== numDirectionalShadows ||
			hash.numPointShadows !== numPointShadows ||
			hash.numSpotShadows !== numSpotShadows ||
			hash.numSpotMaps !== numSpotMaps ||
			hash.numLightProbes !== numLightProbes ) {

			state.directional.length = directionalLength;
			state.spot.length = spotLength;
			state.rectArea.length = rectAreaLength;
			state.point.length = pointLength;
			state.hemi.length = hemiLength;

			state.directionalShadow.length = numDirectionalShadows;
			state.directionalShadowMap.length = numDirectionalShadows;
			state.pointShadow.length = numPointShadows;
			state.pointShadowMap.length = numPointShadows;
			state.spotShadow.length = numSpotShadows;
			state.spotShadowMap.length = numSpotShadows;
			state.directionalShadowMatrix.length = numDirectionalShadows;
			state.pointShadowMatrix.length = numPointShadows;
			state.spotLightMatrix.length = numSpotShadows + numSpotMaps - numSpotShadowsWithMaps;
			state.spotLightMap.length = numSpotMaps;
			state.numSpotLightShadowsWithMaps = numSpotShadowsWithMaps;
			state.numLightProbes = numLightProbes;

			hash.directionalLength = directionalLength;
			hash.pointLength = pointLength;
			hash.spotLength = spotLength;
			hash.rectAreaLength = rectAreaLength;
			hash.hemiLength = hemiLength;

			hash.numDirectionalShadows = numDirectionalShadows;
			hash.numPointShadows = numPointShadows;
			hash.numSpotShadows = numSpotShadows;
			hash.numSpotMaps = numSpotMaps;

			hash.numLightProbes = numLightProbes;

			state.version = nextVersion ++;

		}

	}

	function setupView( lights, camera ) {

		let directionalLength = 0;
		let pointLength = 0;
		let spotLength = 0;
		let rectAreaLength = 0;
		let hemiLength = 0;

		const viewMatrix = camera.matrixWorldInverse;

		for ( let i = 0, l = lights.length; i < l; i ++ ) {

			const light = lights[ i ];

			if ( light.isDirectionalLight ) {

				const uniforms = state.directional[ directionalLength ];

				uniforms.direction.setFromMatrixPosition( light.matrixWorld );
				vector3.setFromMatrixPosition( light.target.matrixWorld );
				uniforms.direction.sub( vector3 );
				uniforms.direction.transformDirection( viewMatrix );

				directionalLength ++;

			} else if ( light.isSpotLight ) {

				const uniforms = state.spot[ spotLength ];

				uniforms.position.setFromMatrixPosition( light.matrixWorld );
				uniforms.position.applyMatrix4( viewMatrix );

				uniforms.direction.setFromMatrixPosition( light.matrixWorld );
				vector3.setFromMatrixPosition( light.target.matrixWorld );
				uniforms.direction.sub( vector3 );
				uniforms.direction.transformDirection( viewMatrix );

				spotLength ++;

			} else if ( light.isRectAreaLight ) {

				const uniforms = state.rectArea[ rectAreaLength ];

				uniforms.position.setFromMatrixPosition( light.matrixWorld );
				uniforms.position.applyMatrix4( viewMatrix );

				// extract local rotation of light to derive width/height half vectors
				matrix42.identity();
				matrix4.copy( light.matrixWorld );
				matrix4.premultiply( viewMatrix );
				matrix42.extractRotation( matrix4 );

				uniforms.halfWidth.set( light.width * 0.5, 0.0, 0.0 );
				uniforms.halfHeight.set( 0.0, light.height * 0.5, 0.0 );

				uniforms.halfWidth.applyMatrix4( matrix42 );
				uniforms.halfHeight.applyMatrix4( matrix42 );

				rectAreaLength ++;

			} else if ( light.isPointLight ) {

				const uniforms = state.point[ pointLength ];

				uniforms.position.setFromMatrixPosition( light.matrixWorld );
				uniforms.position.applyMatrix4( viewMatrix );

				pointLength ++;

			} else if ( light.isHemisphereLight ) {

				const uniforms = state.hemi[ hemiLength ];

				uniforms.direction.setFromMatrixPosition( light.matrixWorld );
				uniforms.direction.transformDirection( viewMatrix );

				hemiLength ++;

			}

		}

	}

	return {
		setup: setup,
		setupView: setupView,
		state: state
	};

}

function WebGLRenderState( extensions ) {

	const lights = new WebGLLights( extensions );

	const lightsArray = [];
	const shadowsArray = [];

	function init( camera ) {

		state.camera = camera;

		lightsArray.length = 0;
		shadowsArray.length = 0;

	}

	function pushLight( light ) {

		lightsArray.push( light );

	}

	function pushShadow( shadowLight ) {

		shadowsArray.push( shadowLight );

	}

	function setupLights() {

		lights.setup( lightsArray );

	}

	function setupLightsView( camera ) {

		lights.setupView( lightsArray, camera );

	}

	const state = {
		lightsArray: lightsArray,
		shadowsArray: shadowsArray,

		camera: null,

		lights: lights,

		transmissionRenderTarget: {}
	};

	return {
		init: init,
		state: state,
		setupLights: setupLights,
		setupLightsView: setupLightsView,

		pushLight: pushLight,
		pushShadow: pushShadow
	};

}

function WebGLRenderStates( extensions ) {

	let renderStates = new WeakMap();

	function get( scene, renderCallDepth = 0 ) {

		const renderStateArray = renderStates.get( scene );
		let renderState;

		if ( renderStateArray === undefined ) {

			renderState = new WebGLRenderState( extensions );
			renderStates.set( scene, [ renderState ] );

		} else {

			if ( renderCallDepth >= renderStateArray.length ) {

				renderState = new WebGLRenderState( extensions );
				renderStateArray.push( renderState );

			} else {

				renderState = renderStateArray[ renderCallDepth ];

			}

		}

		return renderState;

	}

	function dispose() {

		renderStates = new WeakMap();

	}

	return {
		get: get,
		dispose: dispose
	};

}

/**
 * A material for drawing geometry by depth. Depth is based off of the camera
 * near and far plane. White is nearest, black is farthest.
 *
 * @augments Material
 */
class MeshDepthMaterial extends Material {

	/**
	 * Constructs a new mesh depth material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isMeshDepthMaterial = true;

		this.type = 'MeshDepthMaterial';

		/**
		 * Type for depth packing.
		 *
		 * @type {(BasicDepthPacking|RGBADepthPacking|RGBDepthPacking|RGDepthPacking)}
		 * @default BasicDepthPacking
		 */
		this.depthPacking = BasicDepthPacking;

		/**
		 * The color map. May optionally include an alpha channel, typically combined
		 * with {@link Material#transparent} or {@link Material#alphaTest}.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.map = null;

		/**
		 * The alpha map is a grayscale texture that controls the opacity across the
		 * surface (black: fully transparent; white: fully opaque).
		 *
		 * Only the color of the texture is used, ignoring the alpha channel if one
		 * exists. For RGB and RGBA textures, the renderer will use the green channel
		 * when sampling this texture due to the extra bit of precision provided for
		 * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
		 * luminance/alpha textures will also still work as expected.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.alphaMap = null;

		/**
		 * The displacement map affects the position of the mesh's vertices. Unlike
		 * other maps which only affect the light and shade of the material the
		 * displaced vertices can cast shadows, block other objects, and otherwise
		 * act as real geometry. The displacement texture is an image where the value
		 * of each pixel (white being the highest) is mapped against, and
		 * repositions, the vertices of the mesh.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.displacementMap = null;

		/**
		 * How much the displacement map affects the mesh (where black is no
		 * displacement, and white is maximum displacement). Without a displacement
		 * map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementScale = 1;

		/**
		 * The offset of the displacement map's values on the mesh's vertices.
		 * The bias is added to the scaled sample of the displacement map.
		 * Without a displacement map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementBias = 0;

		/**
		 * Renders the geometry as a wireframe.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.wireframe = false;

		/**
		 * Controls the thickness of the wireframe.
		 *
		 * WebGL and WebGPU ignore this property and always render
		 * 1 pixel wide lines.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.wireframeLinewidth = 1;

		this.setValues( parameters );

	}

	copy( source ) {

		super.copy( source );

		this.depthPacking = source.depthPacking;

		this.map = source.map;

		this.alphaMap = source.alphaMap;

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;

		return this;

	}

}

/**
 * A material used internally for implementing shadow mapping with
 * point lights.
 *
 * Can also be used to customize the shadow casting of an object by assigning
 * an instance of `MeshDistanceMaterial` to {@link Object3D#customDistanceMaterial}.
 * The following examples demonstrates this approach in order to ensure
 * transparent parts of objects do not cast shadows.
 *
 * @augments Material
 */
class MeshDistanceMaterial extends Material {

	/**
	 * Constructs a new mesh distance material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isMeshDistanceMaterial = true;

		this.type = 'MeshDistanceMaterial';

		/**
		 * The color map. May optionally include an alpha channel, typically combined
		 * with {@link Material#transparent} or {@link Material#alphaTest}.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.map = null;

		/**
		 * The alpha map is a grayscale texture that controls the opacity across the
		 * surface (black: fully transparent; white: fully opaque).
		 *
		 * Only the color of the texture is used, ignoring the alpha channel if one
		 * exists. For RGB and RGBA textures, the renderer will use the green channel
		 * when sampling this texture due to the extra bit of precision provided for
		 * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
		 * luminance/alpha textures will also still work as expected.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.alphaMap = null;

		/**
		 * The displacement map affects the position of the mesh's vertices. Unlike
		 * other maps which only affect the light and shade of the material the
		 * displaced vertices can cast shadows, block other objects, and otherwise
		 * act as real geometry. The displacement texture is an image where the value
		 * of each pixel (white being the highest) is mapped against, and
		 * repositions, the vertices of the mesh.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.displacementMap = null;

		/**
		 * How much the displacement map affects the mesh (where black is no
		 * displacement, and white is maximum displacement). Without a displacement
		 * map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementScale = 1;

		/**
		 * The offset of the displacement map's values on the mesh's vertices.
		 * The bias is added to the scaled sample of the displacement map.
		 * Without a displacement map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementBias = 0;

		this.setValues( parameters );

	}

	copy( source ) {

		super.copy( source );

		this.map = source.map;

		this.alphaMap = source.alphaMap;

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		return this;

	}

}

const vertex = "void main() {\n\tgl_Position = vec4( position, 1.0 );\n}";

const fragment = "uniform sampler2D shadow_pass;\nuniform vec2 resolution;\nuniform float radius;\n#include <packing>\nvoid main() {\n\tconst float samples = float( VSM_SAMPLES );\n\tfloat mean = 0.0;\n\tfloat squared_mean = 0.0;\n\tfloat uvStride = samples <= 1.0 ? 0.0 : 2.0 / ( samples - 1.0 );\n\tfloat uvStart = samples <= 1.0 ? 0.0 : - 1.0;\n\tfor ( float i = 0.0; i < samples; i ++ ) {\n\t\tfloat uvOffset = uvStart + i * uvStride;\n\t\t#ifdef HORIZONTAL_PASS\n\t\t\tvec2 distribution = unpackRGBATo2Half( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( uvOffset, 0.0 ) * radius ) / resolution ) );\n\t\t\tmean += distribution.x;\n\t\t\tsquared_mean += distribution.y * distribution.y + distribution.x * distribution.x;\n\t\t#else\n\t\t\tfloat depth = unpackRGBAToDepth( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, uvOffset ) * radius ) / resolution ) );\n\t\t\tmean += depth;\n\t\t\tsquared_mean += depth * depth;\n\t\t#endif\n\t}\n\tmean = mean / samples;\n\tsquared_mean = squared_mean / samples;\n\tfloat std_dev = sqrt( squared_mean - mean * mean );\n\tgl_FragColor = pack2HalfToRGBA( vec2( mean, std_dev ) );\n}";

function WebGLShadowMap( renderer, objects, capabilities ) {

	let _frustum = new Frustum();

	const _shadowMapSize = new Vector2(),
		_viewportSize = new Vector2(),

		_viewport = new Vector4(),

		_depthMaterial = new MeshDepthMaterial( { depthPacking: RGBADepthPacking } ),
		_distanceMaterial = new MeshDistanceMaterial(),

		_materialCache = {},

		_maxTextureSize = capabilities.maxTextureSize;

	const shadowSide = { [ FrontSide ]: BackSide, [ BackSide ]: FrontSide, [ DoubleSide ]: DoubleSide };

	const shadowMaterialVertical = new ShaderMaterial( {
		defines: {
			VSM_SAMPLES: 8
		},
		uniforms: {
			shadow_pass: { value: null },
			resolution: { value: new Vector2() },
			radius: { value: 4.0 }
		},

		vertexShader: vertex,
		fragmentShader: fragment

	} );

	const shadowMaterialHorizontal = shadowMaterialVertical.clone();
	shadowMaterialHorizontal.defines.HORIZONTAL_PASS = 1;

	const fullScreenTri = new BufferGeometry();
	fullScreenTri.setAttribute(
		'position',
		new BufferAttribute(
			new Float32Array( [ -1, -1, 0.5, 3, -1, 0.5, -1, 3, 0.5 ] ),
			3
		)
	);

	const fullScreenMesh = new Mesh( fullScreenTri, shadowMaterialVertical );

	const scope = this;

	this.enabled = false;

	this.autoUpdate = true;
	this.needsUpdate = false;

	this.type = PCFShadowMap;
	let _previousType = this.type;

	this.render = function ( lights, scene, camera ) {

		if ( scope.enabled === false ) return;
		if ( scope.autoUpdate === false && scope.needsUpdate === false ) return;

		if ( lights.length === 0 ) return;

		const currentRenderTarget = renderer.getRenderTarget();
		const activeCubeFace = renderer.getActiveCubeFace();
		const activeMipmapLevel = renderer.getActiveMipmapLevel();

		const _state = renderer.state;

		// Set GL state for depth map.
		_state.setBlending( NoBlending );

		if ( _state.buffers.depth.getReversed() === true ) {

			_state.buffers.color.setClear( 0, 0, 0, 0 );

		} else {

			_state.buffers.color.setClear( 1, 1, 1, 1 );

		}

		_state.buffers.depth.setTest( true );
		_state.setScissorTest( false );

		// check for shadow map type changes

		const toVSM = ( _previousType !== VSMShadowMap && this.type === VSMShadowMap );
		const fromVSM = ( _previousType === VSMShadowMap && this.type !== VSMShadowMap );

		// render depth map

		for ( let i = 0, il = lights.length; i < il; i ++ ) {

			const light = lights[ i ];
			const shadow = light.shadow;

			if ( shadow === undefined ) {

				console.warn( 'THREE.WebGLShadowMap:', light, 'has no shadow.' );
				continue;

			}

			if ( shadow.autoUpdate === false && shadow.needsUpdate === false ) continue;

			_shadowMapSize.copy( shadow.mapSize );

			const shadowFrameExtents = shadow.getFrameExtents();

			_shadowMapSize.multiply( shadowFrameExtents );

			_viewportSize.copy( shadow.mapSize );

			if ( _shadowMapSize.x > _maxTextureSize || _shadowMapSize.y > _maxTextureSize ) {

				if ( _shadowMapSize.x > _maxTextureSize ) {

					_viewportSize.x = Math.floor( _maxTextureSize / shadowFrameExtents.x );
					_shadowMapSize.x = _viewportSize.x * shadowFrameExtents.x;
					shadow.mapSize.x = _viewportSize.x;

				}

				if ( _shadowMapSize.y > _maxTextureSize ) {

					_viewportSize.y = Math.floor( _maxTextureSize / shadowFrameExtents.y );
					_shadowMapSize.y = _viewportSize.y * shadowFrameExtents.y;
					shadow.mapSize.y = _viewportSize.y;

				}

			}

			if ( shadow.map === null || toVSM === true || fromVSM === true ) {

				const pars = ( this.type !== VSMShadowMap ) ? { minFilter: NearestFilter, magFilter: NearestFilter } : {};

				if ( shadow.map !== null ) {

					shadow.map.dispose();

				}

				shadow.map = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y, pars );
				shadow.map.texture.name = light.name + '.shadowMap';

				shadow.camera.updateProjectionMatrix();

			}

			renderer.setRenderTarget( shadow.map );
			renderer.clear();

			const viewportCount = shadow.getViewportCount();

			for ( let vp = 0; vp < viewportCount; vp ++ ) {

				const viewport = shadow.getViewport( vp );

				_viewport.set(
					_viewportSize.x * viewport.x,
					_viewportSize.y * viewport.y,
					_viewportSize.x * viewport.z,
					_viewportSize.y * viewport.w
				);

				_state.viewport( _viewport );

				shadow.updateMatrices( light, vp );

				_frustum = shadow.getFrustum();

				renderObject( scene, camera, shadow.camera, light, this.type );

			}

			// do blur pass for VSM

			if ( shadow.isPointLightShadow !== true && this.type === VSMShadowMap ) {

				VSMPass( shadow, camera );

			}

			shadow.needsUpdate = false;

		}

		_previousType = this.type;

		scope.needsUpdate = false;

		renderer.setRenderTarget( currentRenderTarget, activeCubeFace, activeMipmapLevel );

	};

	function VSMPass( shadow, camera ) {

		const geometry = objects.update( fullScreenMesh );

		if ( shadowMaterialVertical.defines.VSM_SAMPLES !== shadow.blurSamples ) {

			shadowMaterialVertical.defines.VSM_SAMPLES = shadow.blurSamples;
			shadowMaterialHorizontal.defines.VSM_SAMPLES = shadow.blurSamples;

			shadowMaterialVertical.needsUpdate = true;
			shadowMaterialHorizontal.needsUpdate = true;

		}

		if ( shadow.mapPass === null ) {

			shadow.mapPass = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y );

		}

		// vertical pass

		shadowMaterialVertical.uniforms.shadow_pass.value = shadow.map.texture;
		shadowMaterialVertical.uniforms.resolution.value = shadow.mapSize;
		shadowMaterialVertical.uniforms.radius.value = shadow.radius;
		renderer.setRenderTarget( shadow.mapPass );
		renderer.clear();
		renderer.renderBufferDirect( camera, null, geometry, shadowMaterialVertical, fullScreenMesh, null );

		// horizontal pass

		shadowMaterialHorizontal.uniforms.shadow_pass.value = shadow.mapPass.texture;
		shadowMaterialHorizontal.uniforms.resolution.value = shadow.mapSize;
		shadowMaterialHorizontal.uniforms.radius.value = shadow.radius;
		renderer.setRenderTarget( shadow.map );
		renderer.clear();
		renderer.renderBufferDirect( camera, null, geometry, shadowMaterialHorizontal, fullScreenMesh, null );

	}

	function getDepthMaterial( object, material, light, type ) {

		let result = null;

		const customMaterial = ( light.isPointLight === true ) ? object.customDistanceMaterial : object.customDepthMaterial;

		if ( customMaterial !== undefined ) {

			result = customMaterial;

		} else {

			result = ( light.isPointLight === true ) ? _distanceMaterial : _depthMaterial;

			if ( ( renderer.localClippingEnabled && material.clipShadows === true && Array.isArray( material.clippingPlanes ) && material.clippingPlanes.length !== 0 ) ||
				( material.displacementMap && material.displacementScale !== 0 ) ||
				( material.alphaMap && material.alphaTest > 0 ) ||
				( material.map && material.alphaTest > 0 ) ||
				( material.alphaToCoverage === true ) ) {

				// in this case we need a unique material instance reflecting the
				// appropriate state

				const keyA = result.uuid, keyB = material.uuid;

				let materialsForVariant = _materialCache[ keyA ];

				if ( materialsForVariant === undefined ) {

					materialsForVariant = {};
					_materialCache[ keyA ] = materialsForVariant;

				}

				let cachedMaterial = materialsForVariant[ keyB ];

				if ( cachedMaterial === undefined ) {

					cachedMaterial = result.clone();
					materialsForVariant[ keyB ] = cachedMaterial;
					material.addEventListener( 'dispose', onMaterialDispose );

				}

				result = cachedMaterial;

			}

		}

		result.visible = material.visible;
		result.wireframe = material.wireframe;

		if ( type === VSMShadowMap ) {

			result.side = ( material.shadowSide !== null ) ? material.shadowSide : material.side;

		} else {

			result.side = ( material.shadowSide !== null ) ? material.shadowSide : shadowSide[ material.side ];

		}

		result.alphaMap = material.alphaMap;
		result.alphaTest = ( material.alphaToCoverage === true ) ? 0.5 : material.alphaTest; // approximate alphaToCoverage by using a fixed alphaTest value
		result.map = material.map;

		result.clipShadows = material.clipShadows;
		result.clippingPlanes = material.clippingPlanes;
		result.clipIntersection = material.clipIntersection;

		result.displacementMap = material.displacementMap;
		result.displacementScale = material.displacementScale;
		result.displacementBias = material.displacementBias;

		result.wireframeLinewidth = material.wireframeLinewidth;
		result.linewidth = material.linewidth;

		if ( light.isPointLight === true && result.isMeshDistanceMaterial === true ) {

			const materialProperties = renderer.properties.get( result );
			materialProperties.light = light;

		}

		return result;

	}

	function renderObject( object, camera, shadowCamera, light, type ) {

		if ( object.visible === false ) return;

		const visible = object.layers.test( camera.layers );

		if ( visible && ( object.isMesh || object.isLine || object.isPoints ) ) {

			if ( ( object.castShadow || ( object.receiveShadow && type === VSMShadowMap ) ) && ( ! object.frustumCulled || _frustum.intersectsObject( object ) ) ) {

				object.modelViewMatrix.multiplyMatrices( shadowCamera.matrixWorldInverse, object.matrixWorld );

				const geometry = objects.update( object );
				const material = object.material;

				if ( Array.isArray( material ) ) {

					const groups = geometry.groups;

					for ( let k = 0, kl = groups.length; k < kl; k ++ ) {

						const group = groups[ k ];
						const groupMaterial = material[ group.materialIndex ];

						if ( groupMaterial && groupMaterial.visible ) {

							const depthMaterial = getDepthMaterial( object, groupMaterial, light, type );

							object.onBeforeShadow( renderer, object, camera, shadowCamera, geometry, depthMaterial, group );

							renderer.renderBufferDirect( shadowCamera, null, geometry, depthMaterial, object, group );

							object.onAfterShadow( renderer, object, camera, shadowCamera, geometry, depthMaterial, group );

						}

					}

				} else if ( material.visible ) {

					const depthMaterial = getDepthMaterial( object, material, light, type );

					object.onBeforeShadow( renderer, object, camera, shadowCamera, geometry, depthMaterial, null );

					renderer.renderBufferDirect( shadowCamera, null, geometry, depthMaterial, object, null );

					object.onAfterShadow( renderer, object, camera, shadowCamera, geometry, depthMaterial, null );

				}

			}

		}

		const children = object.children;

		for ( let i = 0, l = children.length; i < l; i ++ ) {

			renderObject( children[ i ], camera, shadowCamera, light, type );

		}

	}

	function onMaterialDispose( event ) {

		const material = event.target;

		material.removeEventListener( 'dispose', onMaterialDispose );

		// make sure to remove the unique distance/depth materials used for shadow map rendering

		for ( const id in _materialCache ) {

			const cache = _materialCache[ id ];

			const uuid = event.target.uuid;

			if ( uuid in cache ) {

				const shadowMaterial = cache[ uuid ];
				shadowMaterial.dispose();
				delete cache[ uuid ];

			}

		}

	}

}

const reversedFuncs = {
	[ NeverDepth ]: AlwaysDepth,
	[ LessDepth ]: GreaterDepth,
	[ EqualDepth ]: NotEqualDepth,
	[ LessEqualDepth ]: GreaterEqualDepth,

	[ AlwaysDepth ]: NeverDepth,
	[ GreaterDepth ]: LessDepth,
	[ NotEqualDepth ]: EqualDepth,
	[ GreaterEqualDepth ]: LessEqualDepth,
};

function WebGLState( gl, extensions ) {

	function ColorBuffer() {

		let locked = false;

		const color = new Vector4();
		let currentColorMask = null;
		const currentColorClear = new Vector4( 0, 0, 0, 0 );

		return {

			setMask: function ( colorMask ) {

				if ( currentColorMask !== colorMask && ! locked ) {

					gl.colorMask( colorMask, colorMask, colorMask, colorMask );
					currentColorMask = colorMask;

				}

			},

			setLocked: function ( lock ) {

				locked = lock;

			},

			setClear: function ( r, g, b, a, premultipliedAlpha ) {

				if ( premultipliedAlpha === true ) {

					r *= a; g *= a; b *= a;

				}

				color.set( r, g, b, a );

				if ( currentColorClear.equals( color ) === false ) {

					gl.clearColor( r, g, b, a );
					currentColorClear.copy( color );

				}

			},

			reset: function () {

				locked = false;

				currentColorMask = null;
				currentColorClear.set( -1, 0, 0, 0 ); // set to invalid state

			}

		};

	}

	function DepthBuffer() {

		let locked = false;

		let currentReversed = false;
		let currentDepthMask = null;
		let currentDepthFunc = null;
		let currentDepthClear = null;

		return {

			setReversed: function ( reversed ) {

				if ( currentReversed !== reversed ) {

					const ext = extensions.get( 'EXT_clip_control' );

					if ( reversed ) {

						ext.clipControlEXT( ext.LOWER_LEFT_EXT, ext.ZERO_TO_ONE_EXT );

					} else {

						ext.clipControlEXT( ext.LOWER_LEFT_EXT, ext.NEGATIVE_ONE_TO_ONE_EXT );

					}

					currentReversed = reversed;

					const oldDepth = currentDepthClear;
					currentDepthClear = null;
					this.setClear( oldDepth );

				}

			},

			getReversed: function () {

				return currentReversed;

			},

			setTest: function ( depthTest ) {

				if ( depthTest ) {

					enable( gl.DEPTH_TEST );

				} else {

					disable( gl.DEPTH_TEST );

				}

			},

			setMask: function ( depthMask ) {

				if ( currentDepthMask !== depthMask && ! locked ) {

					gl.depthMask( depthMask );
					currentDepthMask = depthMask;

				}

			},

			setFunc: function ( depthFunc ) {

				if ( currentReversed ) depthFunc = reversedFuncs[ depthFunc ];

				if ( currentDepthFunc !== depthFunc ) {

					switch ( depthFunc ) {

						case NeverDepth:

							gl.depthFunc( gl.NEVER );
							break;

						case AlwaysDepth:

							gl.depthFunc( gl.ALWAYS );
							break;

						case LessDepth:

							gl.depthFunc( gl.LESS );
							break;

						case LessEqualDepth:

							gl.depthFunc( gl.LEQUAL );
							break;

						case EqualDepth:

							gl.depthFunc( gl.EQUAL );
							break;

						case GreaterEqualDepth:

							gl.depthFunc( gl.GEQUAL );
							break;

						case GreaterDepth:

							gl.depthFunc( gl.GREATER );
							break;

						case NotEqualDepth:

							gl.depthFunc( gl.NOTEQUAL );
							break;

						default:

							gl.depthFunc( gl.LEQUAL );

					}

					currentDepthFunc = depthFunc;

				}

			},

			setLocked: function ( lock ) {

				locked = lock;

			},

			setClear: function ( depth ) {

				if ( currentDepthClear !== depth ) {

					if ( currentReversed ) {

						depth = 1 - depth;

					}

					gl.clearDepth( depth );
					currentDepthClear = depth;

				}

			},

			reset: function () {

				locked = false;

				currentDepthMask = null;
				currentDepthFunc = null;
				currentDepthClear = null;
				currentReversed = false;

			}

		};

	}

	function StencilBuffer() {

		let locked = false;

		let currentStencilMask = null;
		let currentStencilFunc = null;
		let currentStencilRef = null;
		let currentStencilFuncMask = null;
		let currentStencilFail = null;
		let currentStencilZFail = null;
		let currentStencilZPass = null;
		let currentStencilClear = null;

		return {

			setTest: function ( stencilTest ) {

				if ( ! locked ) {

					if ( stencilTest ) {

						enable( gl.STENCIL_TEST );

					} else {

						disable( gl.STENCIL_TEST );

					}

				}

			},

			setMask: function ( stencilMask ) {

				if ( currentStencilMask !== stencilMask && ! locked ) {

					gl.stencilMask( stencilMask );
					currentStencilMask = stencilMask;

				}

			},

			setFunc: function ( stencilFunc, stencilRef, stencilMask ) {

				if ( currentStencilFunc !== stencilFunc ||
				     currentStencilRef !== stencilRef ||
				     currentStencilFuncMask !== stencilMask ) {

					gl.stencilFunc( stencilFunc, stencilRef, stencilMask );

					currentStencilFunc = stencilFunc;
					currentStencilRef = stencilRef;
					currentStencilFuncMask = stencilMask;

				}

			},

			setOp: function ( stencilFail, stencilZFail, stencilZPass ) {

				if ( currentStencilFail !== stencilFail ||
				     currentStencilZFail !== stencilZFail ||
				     currentStencilZPass !== stencilZPass ) {

					gl.stencilOp( stencilFail, stencilZFail, stencilZPass );

					currentStencilFail = stencilFail;
					currentStencilZFail = stencilZFail;
					currentStencilZPass = stencilZPass;

				}

			},

			setLocked: function ( lock ) {

				locked = lock;

			},

			setClear: function ( stencil ) {

				if ( currentStencilClear !== stencil ) {

					gl.clearStencil( stencil );
					currentStencilClear = stencil;

				}

			},

			reset: function () {

				locked = false;

				currentStencilMask = null;
				currentStencilFunc = null;
				currentStencilRef = null;
				currentStencilFuncMask = null;
				currentStencilFail = null;
				currentStencilZFail = null;
				currentStencilZPass = null;
				currentStencilClear = null;

			}

		};

	}

	//

	const colorBuffer = new ColorBuffer();
	const depthBuffer = new DepthBuffer();
	const stencilBuffer = new StencilBuffer();

	const uboBindings = new WeakMap();
	const uboProgramMap = new WeakMap();

	let enabledCapabilities = {};

	let currentBoundFramebuffers = {};
	let currentDrawbuffers = new WeakMap();
	let defaultDrawbuffers = [];

	let currentProgram = null;

	let currentBlendingEnabled = false;
	let currentBlending = null;
	let currentBlendEquation = null;
	let currentBlendSrc = null;
	let currentBlendDst = null;
	let currentBlendEquationAlpha = null;
	let currentBlendSrcAlpha = null;
	let currentBlendDstAlpha = null;
	let currentBlendColor = new Color( 0, 0, 0 );
	let currentBlendAlpha = 0;
	let currentPremultipledAlpha = false;

	let currentFlipSided = null;
	let currentCullFace = null;

	let currentLineWidth = null;

	let currentPolygonOffsetFactor = null;
	let currentPolygonOffsetUnits = null;

	const maxTextures = gl.getParameter( gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS );

	let lineWidthAvailable = false;
	let version = 0;
	const glVersion = gl.getParameter( gl.VERSION );

	if ( glVersion.indexOf( 'WebGL' ) !== -1 ) {

		version = parseFloat( /^WebGL (\d)/.exec( glVersion )[ 1 ] );
		lineWidthAvailable = ( version >= 1.0 );

	} else if ( glVersion.indexOf( 'OpenGL ES' ) !== -1 ) {

		version = parseFloat( /^OpenGL ES (\d)/.exec( glVersion )[ 1 ] );
		lineWidthAvailable = ( version >= 2.0 );

	}

	let currentTextureSlot = null;
	let currentBoundTextures = {};

	const scissorParam = gl.getParameter( gl.SCISSOR_BOX );
	const viewportParam = gl.getParameter( gl.VIEWPORT );

	const currentScissor = new Vector4().fromArray( scissorParam );
	const currentViewport = new Vector4().fromArray( viewportParam );

	function createTexture( type, target, count, dimensions ) {

		const data = new Uint8Array( 4 ); // 4 is required to match default unpack alignment of 4.
		const texture = gl.createTexture();

		gl.bindTexture( type, texture );
		gl.texParameteri( type, gl.TEXTURE_MIN_FILTER, gl.NEAREST );
		gl.texParameteri( type, gl.TEXTURE_MAG_FILTER, gl.NEAREST );

		for ( let i = 0; i < count; i ++ ) {

			if ( type === gl.TEXTURE_3D || type === gl.TEXTURE_2D_ARRAY ) {

				gl.texImage3D( target, 0, gl.RGBA, 1, 1, dimensions, 0, gl.RGBA, gl.UNSIGNED_BYTE, data );

			} else {

				gl.texImage2D( target + i, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, data );

			}

		}

		return texture;

	}

	const emptyTextures = {};
	emptyTextures[ gl.TEXTURE_2D ] = createTexture( gl.TEXTURE_2D, gl.TEXTURE_2D, 1 );
	emptyTextures[ gl.TEXTURE_CUBE_MAP ] = createTexture( gl.TEXTURE_CUBE_MAP, gl.TEXTURE_CUBE_MAP_POSITIVE_X, 6 );
	emptyTextures[ gl.TEXTURE_2D_ARRAY ] = createTexture( gl.TEXTURE_2D_ARRAY, gl.TEXTURE_2D_ARRAY, 1, 1 );
	emptyTextures[ gl.TEXTURE_3D ] = createTexture( gl.TEXTURE_3D, gl.TEXTURE_3D, 1, 1 );

	// init

	colorBuffer.setClear( 0, 0, 0, 1 );
	depthBuffer.setClear( 1 );
	stencilBuffer.setClear( 0 );

	enable( gl.DEPTH_TEST );
	depthBuffer.setFunc( LessEqualDepth );

	setFlipSided( false );
	setCullFace( CullFaceBack );
	enable( gl.CULL_FACE );

	setBlending( NoBlending );

	//

	function enable( id ) {

		if ( enabledCapabilities[ id ] !== true ) {

			gl.enable( id );
			enabledCapabilities[ id ] = true;

		}

	}

	function disable( id ) {

		if ( enabledCapabilities[ id ] !== false ) {

			gl.disable( id );
			enabledCapabilities[ id ] = false;

		}

	}

	function bindFramebuffer( target, framebuffer ) {

		if ( currentBoundFramebuffers[ target ] !== framebuffer ) {

			gl.bindFramebuffer( target, framebuffer );

			currentBoundFramebuffers[ target ] = framebuffer;

			// gl.DRAW_FRAMEBUFFER is equivalent to gl.FRAMEBUFFER

			if ( target === gl.DRAW_FRAMEBUFFER ) {

				currentBoundFramebuffers[ gl.FRAMEBUFFER ] = framebuffer;

			}

			if ( target === gl.FRAMEBUFFER ) {

				currentBoundFramebuffers[ gl.DRAW_FRAMEBUFFER ] = framebuffer;

			}

			return true;

		}

		return false;

	}

	function drawBuffers( renderTarget, framebuffer ) {

		let drawBuffers = defaultDrawbuffers;

		let needsUpdate = false;

		if ( renderTarget ) {

			drawBuffers = currentDrawbuffers.get( framebuffer );

			if ( drawBuffers === undefined ) {

				drawBuffers = [];
				currentDrawbuffers.set( framebuffer, drawBuffers );

			}

			const textures = renderTarget.textures;

			if ( drawBuffers.length !== textures.length || drawBuffers[ 0 ] !== gl.COLOR_ATTACHMENT0 ) {

				for ( let i = 0, il = textures.length; i < il; i ++ ) {

					drawBuffers[ i ] = gl.COLOR_ATTACHMENT0 + i;

				}

				drawBuffers.length = textures.length;

				needsUpdate = true;

			}

		} else {

			if ( drawBuffers[ 0 ] !== gl.BACK ) {

				drawBuffers[ 0 ] = gl.BACK;

				needsUpdate = true;

			}

		}

		if ( needsUpdate ) {

			gl.drawBuffers( drawBuffers );

		}

	}

	function useProgram( program ) {

		if ( currentProgram !== program ) {

			gl.useProgram( program );

			currentProgram = program;

			return true;

		}

		return false;

	}

	const equationToGL = {
		[ AddEquation ]: gl.FUNC_ADD,
		[ SubtractEquation ]: gl.FUNC_SUBTRACT,
		[ ReverseSubtractEquation ]: gl.FUNC_REVERSE_SUBTRACT
	};

	equationToGL[ MinEquation ] = gl.MIN;
	equationToGL[ MaxEquation ] = gl.MAX;

	const factorToGL = {
		[ ZeroFactor ]: gl.ZERO,
		[ OneFactor ]: gl.ONE,
		[ SrcColorFactor ]: gl.SRC_COLOR,
		[ SrcAlphaFactor ]: gl.SRC_ALPHA,
		[ SrcAlphaSaturateFactor ]: gl.SRC_ALPHA_SATURATE,
		[ DstColorFactor ]: gl.DST_COLOR,
		[ DstAlphaFactor ]: gl.DST_ALPHA,
		[ OneMinusSrcColorFactor ]: gl.ONE_MINUS_SRC_COLOR,
		[ OneMinusSrcAlphaFactor ]: gl.ONE_MINUS_SRC_ALPHA,
		[ OneMinusDstColorFactor ]: gl.ONE_MINUS_DST_COLOR,
		[ OneMinusDstAlphaFactor ]: gl.ONE_MINUS_DST_ALPHA,
		[ ConstantColorFactor ]: gl.CONSTANT_COLOR,
		[ OneMinusConstantColorFactor ]: gl.ONE_MINUS_CONSTANT_COLOR,
		[ ConstantAlphaFactor ]: gl.CONSTANT_ALPHA,
		[ OneMinusConstantAlphaFactor ]: gl.ONE_MINUS_CONSTANT_ALPHA
	};

	function setBlending( blending, blendEquation, blendSrc, blendDst, blendEquationAlpha, blendSrcAlpha, blendDstAlpha, blendColor, blendAlpha, premultipliedAlpha ) {

		if ( blending === NoBlending ) {

			if ( currentBlendingEnabled === true ) {

				disable( gl.BLEND );
				currentBlendingEnabled = false;

			}

			return;

		}

		if ( currentBlendingEnabled === false ) {

			enable( gl.BLEND );
			currentBlendingEnabled = true;

		}

		if ( blending !== CustomBlending ) {

			if ( blending !== currentBlending || premultipliedAlpha !== currentPremultipledAlpha ) {

				if ( currentBlendEquation !== AddEquation || currentBlendEquationAlpha !== AddEquation ) {

					gl.blendEquation( gl.FUNC_ADD );

					currentBlendEquation = AddEquation;
					currentBlendEquationAlpha = AddEquation;

				}

				if ( premultipliedAlpha ) {

					switch ( blending ) {

						case NormalBlending:
							gl.blendFuncSeparate( gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA );
							break;

						case AdditiveBlending:
							gl.blendFunc( gl.ONE, gl.ONE );
							break;

						case SubtractiveBlending:
							gl.blendFuncSeparate( gl.ZERO, gl.ONE_MINUS_SRC_COLOR, gl.ZERO, gl.ONE );
							break;

						case MultiplyBlending:
							gl.blendFuncSeparate( gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE );
							break;

						default:
							console.error( 'THREE.WebGLState: Invalid blending: ', blending );
							break;

					}

				} else {

					switch ( blending ) {

						case NormalBlending:
							gl.blendFuncSeparate( gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA );
							break;

						case AdditiveBlending:
							gl.blendFuncSeparate( gl.SRC_ALPHA, gl.ONE, gl.ONE, gl.ONE );
							break;

						case SubtractiveBlending:
							console.error( 'THREE.WebGLState: SubtractiveBlending requires material.premultipliedAlpha = true' );
							break;

						case MultiplyBlending:
							console.error( 'THREE.WebGLState: MultiplyBlending requires material.premultipliedAlpha = true' );
							break;

						default:
							console.error( 'THREE.WebGLState: Invalid blending: ', blending );
							break;

					}

				}

				currentBlendSrc = null;
				currentBlendDst = null;
				currentBlendSrcAlpha = null;
				currentBlendDstAlpha = null;
				currentBlendColor.set( 0, 0, 0 );
				currentBlendAlpha = 0;

				currentBlending = blending;
				currentPremultipledAlpha = premultipliedAlpha;

			}

			return;

		}

		// custom blending

		blendEquationAlpha = blendEquationAlpha || blendEquation;
		blendSrcAlpha = blendSrcAlpha || blendSrc;
		blendDstAlpha = blendDstAlpha || blendDst;

		if ( blendEquation !== currentBlendEquation || blendEquationAlpha !== currentBlendEquationAlpha ) {

			gl.blendEquationSeparate( equationToGL[ blendEquation ], equationToGL[ blendEquationAlpha ] );

			currentBlendEquation = blendEquation;
			currentBlendEquationAlpha = blendEquationAlpha;

		}

		if ( blendSrc !== currentBlendSrc || blendDst !== currentBlendDst || blendSrcAlpha !== currentBlendSrcAlpha || blendDstAlpha !== currentBlendDstAlpha ) {

			gl.blendFuncSeparate( factorToGL[ blendSrc ], factorToGL[ blendDst ], factorToGL[ blendSrcAlpha ], factorToGL[ blendDstAlpha ] );

			currentBlendSrc = blendSrc;
			currentBlendDst = blendDst;
			currentBlendSrcAlpha = blendSrcAlpha;
			currentBlendDstAlpha = blendDstAlpha;

		}

		if ( blendColor.equals( currentBlendColor ) === false || blendAlpha !== currentBlendAlpha ) {

			gl.blendColor( blendColor.r, blendColor.g, blendColor.b, blendAlpha );

			currentBlendColor.copy( blendColor );
			currentBlendAlpha = blendAlpha;

		}

		currentBlending = blending;
		currentPremultipledAlpha = false;

	}

	function setMaterial( material, frontFaceCW ) {

		material.side === DoubleSide
			? disable( gl.CULL_FACE )
			: enable( gl.CULL_FACE );

		let flipSided = ( material.side === BackSide );
		if ( frontFaceCW ) flipSided = ! flipSided;

		setFlipSided( flipSided );

		( material.blending === NormalBlending && material.transparent === false )
			? setBlending( NoBlending )
			: setBlending( material.blending, material.blendEquation, material.blendSrc, material.blendDst, material.blendEquationAlpha, material.blendSrcAlpha, material.blendDstAlpha, material.blendColor, material.blendAlpha, material.premultipliedAlpha );

		depthBuffer.setFunc( material.depthFunc );
		depthBuffer.setTest( material.depthTest );
		depthBuffer.setMask( material.depthWrite );
		colorBuffer.setMask( material.colorWrite );

		const stencilWrite = material.stencilWrite;
		stencilBuffer.setTest( stencilWrite );
		if ( stencilWrite ) {

			stencilBuffer.setMask( material.stencilWriteMask );
			stencilBuffer.setFunc( material.stencilFunc, material.stencilRef, material.stencilFuncMask );
			stencilBuffer.setOp( material.stencilFail, material.stencilZFail, material.stencilZPass );

		}

		setPolygonOffset( material.polygonOffset, material.polygonOffsetFactor, material.polygonOffsetUnits );

		material.alphaToCoverage === true
			? enable( gl.SAMPLE_ALPHA_TO_COVERAGE )
			: disable( gl.SAMPLE_ALPHA_TO_COVERAGE );

	}

	//

	function setFlipSided( flipSided ) {

		if ( currentFlipSided !== flipSided ) {

			if ( flipSided ) {

				gl.frontFace( gl.CW );

			} else {

				gl.frontFace( gl.CCW );

			}

			currentFlipSided = flipSided;

		}

	}

	function setCullFace( cullFace ) {

		if ( cullFace !== CullFaceNone ) {

			enable( gl.CULL_FACE );

			if ( cullFace !== currentCullFace ) {

				if ( cullFace === CullFaceBack ) {

					gl.cullFace( gl.BACK );

				} else if ( cullFace === CullFaceFront ) {

					gl.cullFace( gl.FRONT );

				} else {

					gl.cullFace( gl.FRONT_AND_BACK );

				}

			}

		} else {

			disable( gl.CULL_FACE );

		}

		currentCullFace = cullFace;

	}

	function setLineWidth( width ) {

		if ( width !== currentLineWidth ) {

			if ( lineWidthAvailable ) gl.lineWidth( width );

			currentLineWidth = width;

		}

	}

	function setPolygonOffset( polygonOffset, factor, units ) {

		if ( polygonOffset ) {

			enable( gl.POLYGON_OFFSET_FILL );

			if ( currentPolygonOffsetFactor !== factor || currentPolygonOffsetUnits !== units ) {

				gl.polygonOffset( factor, units );

				currentPolygonOffsetFactor = factor;
				currentPolygonOffsetUnits = units;

			}

		} else {

			disable( gl.POLYGON_OFFSET_FILL );

		}

	}

	function setScissorTest( scissorTest ) {

		if ( scissorTest ) {

			enable( gl.SCISSOR_TEST );

		} else {

			disable( gl.SCISSOR_TEST );

		}

	}

	// texture

	function activeTexture( webglSlot ) {

		if ( webglSlot === undefined ) webglSlot = gl.TEXTURE0 + maxTextures - 1;

		if ( currentTextureSlot !== webglSlot ) {

			gl.activeTexture( webglSlot );
			currentTextureSlot = webglSlot;

		}

	}

	function bindTexture( webglType, webglTexture, webglSlot ) {

		if ( webglSlot === undefined ) {

			if ( currentTextureSlot === null ) {

				webglSlot = gl.TEXTURE0 + maxTextures - 1;

			} else {

				webglSlot = currentTextureSlot;

			}

		}

		let boundTexture = currentBoundTextures[ webglSlot ];

		if ( boundTexture === undefined ) {

			boundTexture = { type: undefined, texture: undefined };
			currentBoundTextures[ webglSlot ] = boundTexture;

		}

		if ( boundTexture.type !== webglType || boundTexture.texture !== webglTexture ) {

			if ( currentTextureSlot !== webglSlot ) {

				gl.activeTexture( webglSlot );
				currentTextureSlot = webglSlot;

			}

			gl.bindTexture( webglType, webglTexture || emptyTextures[ webglType ] );

			boundTexture.type = webglType;
			boundTexture.texture = webglTexture;

		}

	}

	function unbindTexture() {

		const boundTexture = currentBoundTextures[ currentTextureSlot ];

		if ( boundTexture !== undefined && boundTexture.type !== undefined ) {

			gl.bindTexture( boundTexture.type, null );

			boundTexture.type = undefined;
			boundTexture.texture = undefined;

		}

	}

	function compressedTexImage2D() {

		try {

			gl.compressedTexImage2D( ...arguments );

		} catch ( error ) {

			console.error( 'THREE.WebGLState:', error );

		}

	}

	function compressedTexImage3D() {

		try {

			gl.compressedTexImage3D( ...arguments );

		} catch ( error ) {

			console.error( 'THREE.WebGLState:', error );

		}

	}

	function texSubImage2D() {

		try {

			gl.texSubImage2D( ...arguments );

		} catch ( error ) {

			console.error( 'THREE.WebGLState:', error );

		}

	}

	function texSubImage3D() {

		try {

			gl.texSubImage3D( ...arguments );

		} catch ( error ) {

			console.error( 'THREE.WebGLState:', error );

		}

	}

	function compressedTexSubImage2D() {

		try {

			gl.compressedTexSubImage2D( ...arguments );

		} catch ( error ) {

			console.error( 'THREE.WebGLState:', error );

		}

	}

	function compressedTexSubImage3D() {

		try {

			gl.compressedTexSubImage3D( ...arguments );

		} catch ( error ) {

			console.error( 'THREE.WebGLState:', error );

		}

	}

	function texStorage2D() {

		try {

			gl.texStorage2D( ...arguments );

		} catch ( error ) {

			console.error( 'THREE.WebGLState:', error );

		}

	}

	function texStorage3D() {

		try {

			gl.texStorage3D( ...arguments );

		} catch ( error ) {

			console.error( 'THREE.WebGLState:', error );

		}

	}

	function texImage2D() {

		try {

			gl.texImage2D( ...arguments );

		} catch ( error ) {

			console.error( 'THREE.WebGLState:', error );

		}

	}

	function texImage3D() {

		try {

			gl.texImage3D( ...arguments );

		} catch ( error ) {

			console.error( 'THREE.WebGLState:', error );

		}

	}

	//

	function scissor( scissor ) {

		if ( currentScissor.equals( scissor ) === false ) {

			gl.scissor( scissor.x, scissor.y, scissor.z, scissor.w );
			currentScissor.copy( scissor );

		}

	}

	function viewport( viewport ) {

		if ( currentViewport.equals( viewport ) === false ) {

			gl.viewport( viewport.x, viewport.y, viewport.z, viewport.w );
			currentViewport.copy( viewport );

		}

	}

	function updateUBOMapping( uniformsGroup, program ) {

		let mapping = uboProgramMap.get( program );

		if ( mapping === undefined ) {

			mapping = new WeakMap();

			uboProgramMap.set( program, mapping );

		}

		let blockIndex = mapping.get( uniformsGroup );

		if ( blockIndex === undefined ) {

			blockIndex = gl.getUniformBlockIndex( program, uniformsGroup.name );

			mapping.set( uniformsGroup, blockIndex );

		}

	}

	function uniformBlockBinding( uniformsGroup, program ) {

		const mapping = uboProgramMap.get( program );
		const blockIndex = mapping.get( uniformsGroup );

		if ( uboBindings.get( program ) !== blockIndex ) {

			// bind shader specific block index to global block point
			gl.uniformBlockBinding( program, blockIndex, uniformsGroup.__bindingPointIndex );

			uboBindings.set( program, blockIndex );

		}

	}

	//

	function reset() {

		// reset state

		gl.disable( gl.BLEND );
		gl.disable( gl.CULL_FACE );
		gl.disable( gl.DEPTH_TEST );
		gl.disable( gl.POLYGON_OFFSET_FILL );
		gl.disable( gl.SCISSOR_TEST );
		gl.disable( gl.STENCIL_TEST );
		gl.disable( gl.SAMPLE_ALPHA_TO_COVERAGE );

		gl.blendEquation( gl.FUNC_ADD );
		gl.blendFunc( gl.ONE, gl.ZERO );
		gl.blendFuncSeparate( gl.ONE, gl.ZERO, gl.ONE, gl.ZERO );
		gl.blendColor( 0, 0, 0, 0 );

		gl.colorMask( true, true, true, true );
		gl.clearColor( 0, 0, 0, 0 );

		gl.depthMask( true );
		gl.depthFunc( gl.LESS );

		depthBuffer.setReversed( false );

		gl.clearDepth( 1 );

		gl.stencilMask( 0xffffffff );
		gl.stencilFunc( gl.ALWAYS, 0, 0xffffffff );
		gl.stencilOp( gl.KEEP, gl.KEEP, gl.KEEP );
		gl.clearStencil( 0 );

		gl.cullFace( gl.BACK );
		gl.frontFace( gl.CCW );

		gl.polygonOffset( 0, 0 );

		gl.activeTexture( gl.TEXTURE0 );

		gl.bindFramebuffer( gl.FRAMEBUFFER, null );
		gl.bindFramebuffer( gl.DRAW_FRAMEBUFFER, null );
		gl.bindFramebuffer( gl.READ_FRAMEBUFFER, null );

		gl.useProgram( null );

		gl.lineWidth( 1 );

		gl.scissor( 0, 0, gl.canvas.width, gl.canvas.height );
		gl.viewport( 0, 0, gl.canvas.width, gl.canvas.height );

		// reset internals

		enabledCapabilities = {};

		currentTextureSlot = null;
		currentBoundTextures = {};

		currentBoundFramebuffers = {};
		currentDrawbuffers = new WeakMap();
		defaultDrawbuffers = [];

		currentProgram = null;

		currentBlendingEnabled = false;
		currentBlending = null;
		currentBlendEquation = null;
		currentBlendSrc = null;
		currentBlendDst = null;
		currentBlendEquationAlpha = null;
		currentBlendSrcAlpha = null;
		currentBlendDstAlpha = null;
		currentBlendColor = new Color( 0, 0, 0 );
		currentBlendAlpha = 0;
		currentPremultipledAlpha = false;

		currentFlipSided = null;
		currentCullFace = null;

		currentLineWidth = null;

		currentPolygonOffsetFactor = null;
		currentPolygonOffsetUnits = null;

		currentScissor.set( 0, 0, gl.canvas.width, gl.canvas.height );
		currentViewport.set( 0, 0, gl.canvas.width, gl.canvas.height );

		colorBuffer.reset();
		depthBuffer.reset();
		stencilBuffer.reset();

	}

	return {

		buffers: {
			color: colorBuffer,
			depth: depthBuffer,
			stencil: stencilBuffer
		},

		enable: enable,
		disable: disable,

		bindFramebuffer: bindFramebuffer,
		drawBuffers: drawBuffers,

		useProgram: useProgram,

		setBlending: setBlending,
		setMaterial: setMaterial,

		setFlipSided: setFlipSided,
		setCullFace: setCullFace,

		setLineWidth: setLineWidth,
		setPolygonOffset: setPolygonOffset,

		setScissorTest: setScissorTest,

		activeTexture: activeTexture,
		bindTexture: bindTexture,
		unbindTexture: unbindTexture,
		compressedTexImage2D: compressedTexImage2D,
		compressedTexImage3D: compressedTexImage3D,
		texImage2D: texImage2D,
		texImage3D: texImage3D,

		updateUBOMapping: updateUBOMapping,
		uniformBlockBinding: uniformBlockBinding,

		texStorage2D: texStorage2D,
		texStorage3D: texStorage3D,
		texSubImage2D: texSubImage2D,
		texSubImage3D: texSubImage3D,
		compressedTexSubImage2D: compressedTexSubImage2D,
		compressedTexSubImage3D: compressedTexSubImage3D,

		scissor: scissor,
		viewport: viewport,

		reset: reset

	};

}

/**
 * Determines how many bytes must be used to represent the texture.
 *
 * @param {number} width - The width of the texture.
 * @param {number} height - The height of the texture.
 * @param {number} format - The texture's format.
 * @param {number} type - The texture's type.
 * @return {number} The byte length.
 */
function getByteLength( width, height, format, type ) {

	const typeByteLength = getTextureTypeByteLength( type );

	switch ( format ) {

		// https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glTexImage2D.xhtml
		case AlphaFormat:
			return width * height;
		case RedFormat:
			return ( ( width * height ) / typeByteLength.components ) * typeByteLength.byteLength;
		case RedIntegerFormat:
			return ( ( width * height ) / typeByteLength.components ) * typeByteLength.byteLength;
		case RGFormat:
			return ( ( width * height * 2 ) / typeByteLength.components ) * typeByteLength.byteLength;
		case RGIntegerFormat:
			return ( ( width * height * 2 ) / typeByteLength.components ) * typeByteLength.byteLength;
		case RGBFormat:
			return ( ( width * height * 3 ) / typeByteLength.components ) * typeByteLength.byteLength;
		case RGBAFormat:
			return ( ( width * height * 4 ) / typeByteLength.components ) * typeByteLength.byteLength;
		case RGBAIntegerFormat:
			return ( ( width * height * 4 ) / typeByteLength.components ) * typeByteLength.byteLength;

		// https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_s3tc_srgb/
		case RGB_S3TC_DXT1_Format:
		case RGBA_S3TC_DXT1_Format:
			return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 8;
		case RGBA_S3TC_DXT3_Format:
		case RGBA_S3TC_DXT5_Format:
			return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 16;

		// https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_pvrtc/
		case RGB_PVRTC_2BPPV1_Format:
		case RGBA_PVRTC_2BPPV1_Format:
			return ( Math.max( width, 16 ) * Math.max( height, 8 ) ) / 4;
		case RGB_PVRTC_4BPPV1_Format:
		case RGBA_PVRTC_4BPPV1_Format:
			return ( Math.max( width, 8 ) * Math.max( height, 8 ) ) / 2;

		// https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_etc/
		case RGB_ETC1_Format:
		case RGB_ETC2_Format:
			return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 8;
		case RGBA_ETC2_EAC_Format:
			return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 16;

		// https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/
		case RGBA_ASTC_4x4_Format:
			return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 16;
		case RGBA_ASTC_5x4_Format:
			return Math.floor( ( width + 4 ) / 5 ) * Math.floor( ( height + 3 ) / 4 ) * 16;
		case RGBA_ASTC_5x5_Format:
			return Math.floor( ( width + 4 ) / 5 ) * Math.floor( ( height + 4 ) / 5 ) * 16;
		case RGBA_ASTC_6x5_Format:
			return Math.floor( ( width + 5 ) / 6 ) * Math.floor( ( height + 4 ) / 5 ) * 16;
		case RGBA_ASTC_6x6_Format:
			return Math.floor( ( width + 5 ) / 6 ) * Math.floor( ( height + 5 ) / 6 ) * 16;
		case RGBA_ASTC_8x5_Format:
			return Math.floor( ( width + 7 ) / 8 ) * Math.floor( ( height + 4 ) / 5 ) * 16;
		case RGBA_ASTC_8x6_Format:
			return Math.floor( ( width + 7 ) / 8 ) * Math.floor( ( height + 5 ) / 6 ) * 16;
		case RGBA_ASTC_8x8_Format:
			return Math.floor( ( width + 7 ) / 8 ) * Math.floor( ( height + 7 ) / 8 ) * 16;
		case RGBA_ASTC_10x5_Format:
			return Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 4 ) / 5 ) * 16;
		case RGBA_ASTC_10x6_Format:
			return Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 5 ) / 6 ) * 16;
		case RGBA_ASTC_10x8_Format:
			return Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 7 ) / 8 ) * 16;
		case RGBA_ASTC_10x10_Format:
			return Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 9 ) / 10 ) * 16;
		case RGBA_ASTC_12x10_Format:
			return Math.floor( ( width + 11 ) / 12 ) * Math.floor( ( height + 9 ) / 10 ) * 16;
		case RGBA_ASTC_12x12_Format:
			return Math.floor( ( width + 11 ) / 12 ) * Math.floor( ( height + 11 ) / 12 ) * 16;

		// https://registry.khronos.org/webgl/extensions/EXT_texture_compression_bptc/
		case RGBA_BPTC_Format:
		case RGB_BPTC_SIGNED_Format:
		case RGB_BPTC_UNSIGNED_Format:
			return Math.ceil( width / 4 ) * Math.ceil( height / 4 ) * 16;

		// https://registry.khronos.org/webgl/extensions/EXT_texture_compression_rgtc/
		case RED_RGTC1_Format:
		case SIGNED_RED_RGTC1_Format:
			return Math.ceil( width / 4 ) * Math.ceil( height / 4 ) * 8;
		case RED_GREEN_RGTC2_Format:
		case SIGNED_RED_GREEN_RGTC2_Format:
			return Math.ceil( width / 4 ) * Math.ceil( height / 4 ) * 16;

	}

	throw new Error(
		`Unable to determine texture byte length for ${format} format.`,
	);

}

function getTextureTypeByteLength( type ) {

	switch ( type ) {

		case UnsignedByteType:
		case ByteType:
			return { byteLength: 1, components: 1 };
		case UnsignedShortType:
		case ShortType:
		case HalfFloatType:
			return { byteLength: 2, components: 1 };
		case UnsignedShort4444Type:
		case UnsignedShort5551Type:
			return { byteLength: 2, components: 4 };
		case UnsignedIntType:
		case IntType:
		case FloatType:
			return { byteLength: 4, components: 1 };
		case UnsignedInt5999Type:
		case UnsignedInt101111Type:
			return { byteLength: 4, components: 3 };

	}

	throw new Error( `Unknown texture type ${type}.` );

}

function WebGLTextures( _gl, extensions, state, properties, capabilities, utils, info ) {

	const multisampledRTTExt = extensions.has( 'WEBGL_multisampled_render_to_texture' ) ? extensions.get( 'WEBGL_multisampled_render_to_texture' ) : null;
	const supportsInvalidateFramebuffer = typeof navigator === 'undefined' ? false : /OculusBrowser/g.test( navigator.userAgent );

	const _imageDimensions = new Vector2();
	const _videoTextures = new WeakMap();
	let _canvas;

	const _sources = new WeakMap(); // maps WebglTexture objects to instances of Source

	// cordova iOS (as of 5.0) still uses UIWebView, which provides OffscreenCanvas,
	// also OffscreenCanvas.getContext("webgl"), but not OffscreenCanvas.getContext("2d")!
	// Some implementations may only implement OffscreenCanvas partially (e.g. lacking 2d).

	let useOffscreenCanvas = false;

	try {

		useOffscreenCanvas = typeof OffscreenCanvas !== 'undefined'
			// eslint-disable-next-line compat/compat
			&& ( new OffscreenCanvas( 1, 1 ).getContext( '2d' ) ) !== null;

	} catch ( err ) {

		// Ignore any errors

	}

	function createCanvas( width, height ) {

		// Use OffscreenCanvas when available. Specially needed in web workers

		return useOffscreenCanvas ?
			// eslint-disable-next-line compat/compat
			new OffscreenCanvas( width, height ) : createElementNS( 'canvas' );

	}

	function resizeImage( image, needsNewCanvas, maxSize ) {

		let scale = 1;

		const dimensions = getDimensions( image );

		// handle case if texture exceeds max size

		if ( dimensions.width > maxSize || dimensions.height > maxSize ) {

			scale = maxSize / Math.max( dimensions.width, dimensions.height );

		}

		// only perform resize if necessary

		if ( scale < 1 ) {

			// only perform resize for certain image types

			if ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||
				( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||
				( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ||
				( typeof VideoFrame !== 'undefined' && image instanceof VideoFrame ) ) {

				const width = Math.floor( scale * dimensions.width );
				const height = Math.floor( scale * dimensions.height );

				if ( _canvas === undefined ) _canvas = createCanvas( width, height );

				// cube textures can't reuse the same canvas

				const canvas = needsNewCanvas ? createCanvas( width, height ) : _canvas;

				canvas.width = width;
				canvas.height = height;

				const context = canvas.getContext( '2d' );
				context.drawImage( image, 0, 0, width, height );

				console.warn( 'THREE.WebGLRenderer: Texture has been resized from (' + dimensions.width + 'x' + dimensions.height + ') to (' + width + 'x' + height + ').' );

				return canvas;

			} else {

				if ( 'data' in image ) {

					console.warn( 'THREE.WebGLRenderer: Image in DataTexture is too big (' + dimensions.width + 'x' + dimensions.height + ').' );

				}

				return image;

			}

		}

		return image;

	}

	function textureNeedsGenerateMipmaps( texture ) {

		return texture.generateMipmaps;

	}

	function generateMipmap( target ) {

		_gl.generateMipmap( target );

	}

	function getTargetType( texture ) {

		if ( texture.isWebGLCubeRenderTarget ) return _gl.TEXTURE_CUBE_MAP;
		if ( texture.isWebGL3DRenderTarget ) return _gl.TEXTURE_3D;
		if ( texture.isWebGLArrayRenderTarget || texture.isCompressedArrayTexture ) return _gl.TEXTURE_2D_ARRAY;
		return _gl.TEXTURE_2D;

	}

	function getInternalFormat( internalFormatName, glFormat, glType, colorSpace, forceLinearTransfer = false ) {

		if ( internalFormatName !== null ) {

			if ( _gl[ internalFormatName ] !== undefined ) return _gl[ internalFormatName ];

			console.warn( 'THREE.WebGLRenderer: Attempt to use non-existing WebGL internal format \'' + internalFormatName + '\'' );

		}

		let internalFormat = glFormat;

		if ( glFormat === _gl.RED ) {

			if ( glType === _gl.FLOAT ) internalFormat = _gl.R32F;
			if ( glType === _gl.HALF_FLOAT ) internalFormat = _gl.R16F;
			if ( glType === _gl.UNSIGNED_BYTE ) internalFormat = _gl.R8;

		}

		if ( glFormat === _gl.RED_INTEGER ) {

			if ( glType === _gl.UNSIGNED_BYTE ) internalFormat = _gl.R8UI;
			if ( glType === _gl.UNSIGNED_SHORT ) internalFormat = _gl.R16UI;
			if ( glType === _gl.UNSIGNED_INT ) internalFormat = _gl.R32UI;
			if ( glType === _gl.BYTE ) internalFormat = _gl.R8I;
			if ( glType === _gl.SHORT ) internalFormat = _gl.R16I;
			if ( glType === _gl.INT ) internalFormat = _gl.R32I;

		}

		if ( glFormat === _gl.RG ) {

			if ( glType === _gl.FLOAT ) internalFormat = _gl.RG32F;
			if ( glType === _gl.HALF_FLOAT ) internalFormat = _gl.RG16F;
			if ( glType === _gl.UNSIGNED_BYTE ) internalFormat = _gl.RG8;

		}

		if ( glFormat === _gl.RG_INTEGER ) {

			if ( glType === _gl.UNSIGNED_BYTE ) internalFormat = _gl.RG8UI;
			if ( glType === _gl.UNSIGNED_SHORT ) internalFormat = _gl.RG16UI;
			if ( glType === _gl.UNSIGNED_INT ) internalFormat = _gl.RG32UI;
			if ( glType === _gl.BYTE ) internalFormat = _gl.RG8I;
			if ( glType === _gl.SHORT ) internalFormat = _gl.RG16I;
			if ( glType === _gl.INT ) internalFormat = _gl.RG32I;

		}

		if ( glFormat === _gl.RGB_INTEGER ) {

			if ( glType === _gl.UNSIGNED_BYTE ) internalFormat = _gl.RGB8UI;
			if ( glType === _gl.UNSIGNED_SHORT ) internalFormat = _gl.RGB16UI;
			if ( glType === _gl.UNSIGNED_INT ) internalFormat = _gl.RGB32UI;
			if ( glType === _gl.BYTE ) internalFormat = _gl.RGB8I;
			if ( glType === _gl.SHORT ) internalFormat = _gl.RGB16I;
			if ( glType === _gl.INT ) internalFormat = _gl.RGB32I;

		}

		if ( glFormat === _gl.RGBA_INTEGER ) {

			if ( glType === _gl.UNSIGNED_BYTE ) internalFormat = _gl.RGBA8UI;
			if ( glType === _gl.UNSIGNED_SHORT ) internalFormat = _gl.RGBA16UI;
			if ( glType === _gl.UNSIGNED_INT ) internalFormat = _gl.RGBA32UI;
			if ( glType === _gl.BYTE ) internalFormat = _gl.RGBA8I;
			if ( glType === _gl.SHORT ) internalFormat = _gl.RGBA16I;
			if ( glType === _gl.INT ) internalFormat = _gl.RGBA32I;

		}

		if ( glFormat === _gl.RGB ) {

			if ( glType === _gl.UNSIGNED_INT_5_9_9_9_REV ) internalFormat = _gl.RGB9_E5;
			if ( glType === _gl.UNSIGNED_INT_10F_11F_11F_REV ) internalFormat = _gl.R11F_G11F_B10F;

		}

		if ( glFormat === _gl.RGBA ) {

			const transfer = forceLinearTransfer ? LinearTransfer : ColorManagement.getTransfer( colorSpace );

			if ( glType === _gl.FLOAT ) internalFormat = _gl.RGBA32F;
			if ( glType === _gl.HALF_FLOAT ) internalFormat = _gl.RGBA16F;
			if ( glType === _gl.UNSIGNED_BYTE ) internalFormat = ( transfer === SRGBTransfer ) ? _gl.SRGB8_ALPHA8 : _gl.RGBA8;
			if ( glType === _gl.UNSIGNED_SHORT_4_4_4_4 ) internalFormat = _gl.RGBA4;
			if ( glType === _gl.UNSIGNED_SHORT_5_5_5_1 ) internalFormat = _gl.RGB5_A1;

		}

		if ( internalFormat === _gl.R16F || internalFormat === _gl.R32F ||
			internalFormat === _gl.RG16F || internalFormat === _gl.RG32F ||
			internalFormat === _gl.RGBA16F || internalFormat === _gl.RGBA32F ) {

			extensions.get( 'EXT_color_buffer_float' );

		}

		return internalFormat;

	}

	function getInternalDepthFormat( useStencil, depthType ) {

		let glInternalFormat;
		if ( useStencil ) {

			if ( depthType === null || depthType === UnsignedIntType || depthType === UnsignedInt248Type ) {

				glInternalFormat = _gl.DEPTH24_STENCIL8;

			} else if ( depthType === FloatType ) {

				glInternalFormat = _gl.DEPTH32F_STENCIL8;

			} else if ( depthType === UnsignedShortType ) {

				glInternalFormat = _gl.DEPTH24_STENCIL8;
				console.warn( 'DepthTexture: 16 bit depth attachment is not supported with stencil. Using 24-bit attachment.' );

			}

		} else {

			if ( depthType === null || depthType === UnsignedIntType || depthType === UnsignedInt248Type ) {

				glInternalFormat = _gl.DEPTH_COMPONENT24;

			} else if ( depthType === FloatType ) {

				glInternalFormat = _gl.DEPTH_COMPONENT32F;

			} else if ( depthType === UnsignedShortType ) {

				glInternalFormat = _gl.DEPTH_COMPONENT16;

			}

		}

		return glInternalFormat;

	}

	function getMipLevels( texture, image ) {

		if ( textureNeedsGenerateMipmaps( texture ) === true || ( texture.isFramebufferTexture && texture.minFilter !== NearestFilter && texture.minFilter !== LinearFilter ) ) {

			return Math.log2( Math.max( image.width, image.height ) ) + 1;

		} else if ( texture.mipmaps !== undefined && texture.mipmaps.length > 0 ) {

			// user-defined mipmaps

			return texture.mipmaps.length;

		} else if ( texture.isCompressedTexture && Array.isArray( texture.image ) ) {

			return image.mipmaps.length;

		} else {

			// texture without mipmaps (only base level)

			return 1;

		}

	}

	//

	function onTextureDispose( event ) {

		const texture = event.target;

		texture.removeEventListener( 'dispose', onTextureDispose );

		deallocateTexture( texture );

		if ( texture.isVideoTexture ) {

			_videoTextures.delete( texture );

		}

	}

	function onRenderTargetDispose( event ) {

		const renderTarget = event.target;

		renderTarget.removeEventListener( 'dispose', onRenderTargetDispose );

		deallocateRenderTarget( renderTarget );

	}

	//

	function deallocateTexture( texture ) {

		const textureProperties = properties.get( texture );

		if ( textureProperties.__webglInit === undefined ) return;

		// check if it's necessary to remove the WebGLTexture object

		const source = texture.source;
		const webglTextures = _sources.get( source );

		if ( webglTextures ) {

			const webglTexture = webglTextures[ textureProperties.__cacheKey ];
			webglTexture.usedTimes --;

			// the WebGLTexture object is not used anymore, remove it

			if ( webglTexture.usedTimes === 0 ) {

				deleteTexture( texture );

			}

			// remove the weak map entry if no WebGLTexture uses the source anymore

			if ( Object.keys( webglTextures ).length === 0 ) {

				_sources.delete( source );

			}

		}

		properties.remove( texture );

	}

	function deleteTexture( texture ) {

		const textureProperties = properties.get( texture );
		_gl.deleteTexture( textureProperties.__webglTexture );

		const source = texture.source;
		const webglTextures = _sources.get( source );
		delete webglTextures[ textureProperties.__cacheKey ];

		info.memory.textures --;

	}

	function deallocateRenderTarget( renderTarget ) {

		const renderTargetProperties = properties.get( renderTarget );

		if ( renderTarget.depthTexture ) {

			renderTarget.depthTexture.dispose();

			properties.remove( renderTarget.depthTexture );

		}

		if ( renderTarget.isWebGLCubeRenderTarget ) {

			for ( let i = 0; i < 6; i ++ ) {

				if ( Array.isArray( renderTargetProperties.__webglFramebuffer[ i ] ) ) {

					for ( let level = 0; level < renderTargetProperties.__webglFramebuffer[ i ].length; level ++ ) _gl.deleteFramebuffer( renderTargetProperties.__webglFramebuffer[ i ][ level ] );

				} else {

					_gl.deleteFramebuffer( renderTargetProperties.__webglFramebuffer[ i ] );

				}

				if ( renderTargetProperties.__webglDepthbuffer ) _gl.deleteRenderbuffer( renderTargetProperties.__webglDepthbuffer[ i ] );

			}

		} else {

			if ( Array.isArray( renderTargetProperties.__webglFramebuffer ) ) {

				for ( let level = 0; level < renderTargetProperties.__webglFramebuffer.length; level ++ ) _gl.deleteFramebuffer( renderTargetProperties.__webglFramebuffer[ level ] );

			} else {

				_gl.deleteFramebuffer( renderTargetProperties.__webglFramebuffer );

			}

			if ( renderTargetProperties.__webglDepthbuffer ) _gl.deleteRenderbuffer( renderTargetProperties.__webglDepthbuffer );
			if ( renderTargetProperties.__webglMultisampledFramebuffer ) _gl.deleteFramebuffer( renderTargetProperties.__webglMultisampledFramebuffer );

			if ( renderTargetProperties.__webglColorRenderbuffer ) {

				for ( let i = 0; i < renderTargetProperties.__webglColorRenderbuffer.length; i ++ ) {

					if ( renderTargetProperties.__webglColorRenderbuffer[ i ] ) _gl.deleteRenderbuffer( renderTargetProperties.__webglColorRenderbuffer[ i ] );

				}

			}

			if ( renderTargetProperties.__webglDepthRenderbuffer ) _gl.deleteRenderbuffer( renderTargetProperties.__webglDepthRenderbuffer );

		}

		const textures = renderTarget.textures;

		for ( let i = 0, il = textures.length; i < il; i ++ ) {

			const attachmentProperties = properties.get( textures[ i ] );

			if ( attachmentProperties.__webglTexture ) {

				_gl.deleteTexture( attachmentProperties.__webglTexture );

				info.memory.textures --;

			}

			properties.remove( textures[ i ] );

		}

		properties.remove( renderTarget );

	}

	//

	let textureUnits = 0;

	function resetTextureUnits() {

		textureUnits = 0;

	}

	function allocateTextureUnit() {

		const textureUnit = textureUnits;

		if ( textureUnit >= capabilities.maxTextures ) {

			console.warn( 'THREE.WebGLTextures: Trying to use ' + textureUnit + ' texture units while this GPU supports only ' + capabilities.maxTextures );

		}

		textureUnits += 1;

		return textureUnit;

	}

	function getTextureCacheKey( texture ) {

		const array = [];

		array.push( texture.wrapS );
		array.push( texture.wrapT );
		array.push( texture.wrapR || 0 );
		array.push( texture.magFilter );
		array.push( texture.minFilter );
		array.push( texture.anisotropy );
		array.push( texture.internalFormat );
		array.push( texture.format );
		array.push( texture.type );
		array.push( texture.generateMipmaps );
		array.push( texture.premultiplyAlpha );
		array.push( texture.flipY );
		array.push( texture.unpackAlignment );
		array.push( texture.colorSpace );

		return array.join();

	}

	//

	function setTexture2D( texture, slot ) {

		const textureProperties = properties.get( texture );

		if ( texture.isVideoTexture ) updateVideoTexture( texture );

		if ( texture.isRenderTargetTexture === false && texture.isExternalTexture !== true && texture.version > 0 && textureProperties.__version !== texture.version ) {

			const image = texture.image;

			if ( image === null ) {

				console.warn( 'THREE.WebGLRenderer: Texture marked for update but no image data found.' );

			} else if ( image.complete === false ) {

				console.warn( 'THREE.WebGLRenderer: Texture marked for update but image is incomplete' );

			} else {

				uploadTexture( textureProperties, texture, slot );
				return;

			}

		} else if ( texture.isExternalTexture ) {

			textureProperties.__webglTexture = texture.sourceTexture ? texture.sourceTexture : null;

		}

		state.bindTexture( _gl.TEXTURE_2D, textureProperties.__webglTexture, _gl.TEXTURE0 + slot );

	}

	function setTexture2DArray( texture, slot ) {

		const textureProperties = properties.get( texture );

		if ( texture.isRenderTargetTexture === false && texture.version > 0 && textureProperties.__version !== texture.version ) {

			uploadTexture( textureProperties, texture, slot );
			return;

		}

		state.bindTexture( _gl.TEXTURE_2D_ARRAY, textureProperties.__webglTexture, _gl.TEXTURE0 + slot );

	}

	function setTexture3D( texture, slot ) {

		const textureProperties = properties.get( texture );

		if ( texture.isRenderTargetTexture === false && texture.version > 0 && textureProperties.__version !== texture.version ) {

			uploadTexture( textureProperties, texture, slot );
			return;

		}

		state.bindTexture( _gl.TEXTURE_3D, textureProperties.__webglTexture, _gl.TEXTURE0 + slot );

	}

	function setTextureCube( texture, slot ) {

		const textureProperties = properties.get( texture );

		if ( texture.version > 0 && textureProperties.__version !== texture.version ) {

			uploadCubeTexture( textureProperties, texture, slot );
			return;

		}

		state.bindTexture( _gl.TEXTURE_CUBE_MAP, textureProperties.__webglTexture, _gl.TEXTURE0 + slot );

	}

	const wrappingToGL = {
		[ RepeatWrapping ]: _gl.REPEAT,
		[ ClampToEdgeWrapping ]: _gl.CLAMP_TO_EDGE,
		[ MirroredRepeatWrapping ]: _gl.MIRRORED_REPEAT
	};

	const filterToGL = {
		[ NearestFilter ]: _gl.NEAREST,
		[ NearestMipmapNearestFilter ]: _gl.NEAREST_MIPMAP_NEAREST,
		[ NearestMipmapLinearFilter ]: _gl.NEAREST_MIPMAP_LINEAR,

		[ LinearFilter ]: _gl.LINEAR,
		[ LinearMipmapNearestFilter ]: _gl.LINEAR_MIPMAP_NEAREST,
		[ LinearMipmapLinearFilter ]: _gl.LINEAR_MIPMAP_LINEAR
	};

	const compareToGL = {
		[ NeverCompare ]: _gl.NEVER,
		[ AlwaysCompare ]: _gl.ALWAYS,
		[ LessCompare ]: _gl.LESS,
		[ LessEqualCompare ]: _gl.LEQUAL,
		[ EqualCompare ]: _gl.EQUAL,
		[ GreaterEqualCompare ]: _gl.GEQUAL,
		[ GreaterCompare ]: _gl.GREATER,
		[ NotEqualCompare ]: _gl.NOTEQUAL
	};

	function setTextureParameters( textureType, texture ) {

		if ( texture.type === FloatType && extensions.has( 'OES_texture_float_linear' ) === false &&
			( texture.magFilter === LinearFilter || texture.magFilter === LinearMipmapNearestFilter || texture.magFilter === NearestMipmapLinearFilter || texture.magFilter === LinearMipmapLinearFilter ||
			texture.minFilter === LinearFilter || texture.minFilter === LinearMipmapNearestFilter || texture.minFilter === NearestMipmapLinearFilter || texture.minFilter === LinearMipmapLinearFilter ) ) {

			console.warn( 'THREE.WebGLRenderer: Unable to use linear filtering with floating point textures. OES_texture_float_linear not supported on this device.' );

		}

		_gl.texParameteri( textureType, _gl.TEXTURE_WRAP_S, wrappingToGL[ texture.wrapS ] );
		_gl.texParameteri( textureType, _gl.TEXTURE_WRAP_T, wrappingToGL[ texture.wrapT ] );

		if ( textureType === _gl.TEXTURE_3D || textureType === _gl.TEXTURE_2D_ARRAY ) {

			_gl.texParameteri( textureType, _gl.TEXTURE_WRAP_R, wrappingToGL[ texture.wrapR ] );

		}

		_gl.texParameteri( textureType, _gl.TEXTURE_MAG_FILTER, filterToGL[ texture.magFilter ] );
		_gl.texParameteri( textureType, _gl.TEXTURE_MIN_FILTER, filterToGL[ texture.minFilter ] );

		if ( texture.compareFunction ) {

			_gl.texParameteri( textureType, _gl.TEXTURE_COMPARE_MODE, _gl.COMPARE_REF_TO_TEXTURE );
			_gl.texParameteri( textureType, _gl.TEXTURE_COMPARE_FUNC, compareToGL[ texture.compareFunction ] );

		}

		if ( extensions.has( 'EXT_texture_filter_anisotropic' ) === true ) {

			if ( texture.magFilter === NearestFilter ) return;
			if ( texture.minFilter !== NearestMipmapLinearFilter && texture.minFilter !== LinearMipmapLinearFilter ) return;
			if ( texture.type === FloatType && extensions.has( 'OES_texture_float_linear' ) === false ) return; // verify extension

			if ( texture.anisotropy > 1 || properties.get( texture ).__currentAnisotropy ) {

				const extension = extensions.get( 'EXT_texture_filter_anisotropic' );
				_gl.texParameterf( textureType, extension.TEXTURE_MAX_ANISOTROPY_EXT, Math.min( texture.anisotropy, capabilities.getMaxAnisotropy() ) );
				properties.get( texture ).__currentAnisotropy = texture.anisotropy;

			}

		}

	}

	function initTexture( textureProperties, texture ) {

		let forceUpload = false;

		if ( textureProperties.__webglInit === undefined ) {

			textureProperties.__webglInit = true;

			texture.addEventListener( 'dispose', onTextureDispose );

		}

		// create Source <-> WebGLTextures mapping if necessary

		const source = texture.source;
		let webglTextures = _sources.get( source );

		if ( webglTextures === undefined ) {

			webglTextures = {};
			_sources.set( source, webglTextures );

		}

		// check if there is already a WebGLTexture object for the given texture parameters

		const textureCacheKey = getTextureCacheKey( texture );

		if ( textureCacheKey !== textureProperties.__cacheKey ) {

			// if not, create a new instance of WebGLTexture

			if ( webglTextures[ textureCacheKey ] === undefined ) {

				// create new entry

				webglTextures[ textureCacheKey ] = {
					texture: _gl.createTexture(),
					usedTimes: 0
				};

				info.memory.textures ++;

				// when a new instance of WebGLTexture was created, a texture upload is required
				// even if the image contents are identical

				forceUpload = true;

			}

			webglTextures[ textureCacheKey ].usedTimes ++;

			// every time the texture cache key changes, it's necessary to check if an instance of
			// WebGLTexture can be deleted in order to avoid a memory leak.

			const webglTexture = webglTextures[ textureProperties.__cacheKey ];

			if ( webglTexture !== undefined ) {

				webglTextures[ textureProperties.__cacheKey ].usedTimes --;

				if ( webglTexture.usedTimes === 0 ) {

					deleteTexture( texture );

				}

			}

			// store references to cache key and WebGLTexture object

			textureProperties.__cacheKey = textureCacheKey;
			textureProperties.__webglTexture = webglTextures[ textureCacheKey ].texture;

		}

		return forceUpload;

	}

	function getRow( index, rowLength, componentStride ) {

		return Math.floor( Math.floor( index / componentStride ) / rowLength );

	}

	function updateTexture( texture, image, glFormat, glType ) {

		const componentStride = 4; // only RGBA supported

		const updateRanges = texture.updateRanges;

		if ( updateRanges.length === 0 ) {

			state.texSubImage2D( _gl.TEXTURE_2D, 0, 0, 0, image.width, image.height, glFormat, glType, image.data );

		} else {

			// Before applying update ranges, we merge any adjacent / overlapping
			// ranges to reduce load on `gl.texSubImage2D`. Empirically, this has led
			// to performance improvements for applications which make heavy use of
			// update ranges. Likely due to GPU command overhead.
			//
			// Note that to reduce garbage collection between frames, we merge the
			// update ranges in-place. This is safe because this method will clear the
			// update ranges once updated.

			updateRanges.sort( ( a, b ) => a.start - b.start );

			// To merge the update ranges in-place, we work from left to right in the
			// existing updateRanges array, merging ranges. This may result in a final
			// array which is smaller than the original. This index tracks the last
			// index representing a merged range, any data after this index can be
			// trimmed once the merge algorithm is completed.
			let mergeIndex = 0;

			for ( let i = 1; i < updateRanges.length; i ++ ) {

				const previousRange = updateRanges[ mergeIndex ];
				const range = updateRanges[ i ];

				// Only merge if in the same row and overlapping/adjacent
				const previousEnd = previousRange.start + previousRange.count;
				const currentRow = getRow( range.start, image.width, componentStride );
				const previousRow = getRow( previousRange.start, image.width, componentStride );

				// We add one here to merge adjacent ranges. This is safe because ranges
				// operate over positive integers.
				if (
					range.start <= previousEnd + 1 &&
					currentRow === previousRow &&
					getRow( range.start + range.count - 1, image.width, componentStride ) === currentRow // ensure range doesn't spill
				) {

					previousRange.count = Math.max(
						previousRange.count,
						range.start + range.count - previousRange.start
					);

				} else {

					++ mergeIndex;
					updateRanges[ mergeIndex ] = range;

				}


			}

			// Trim the array to only contain the merged ranges.
			updateRanges.length = mergeIndex + 1;

			const currentUnpackRowLen = _gl.getParameter( _gl.UNPACK_ROW_LENGTH );
			const currentUnpackSkipPixels = _gl.getParameter( _gl.UNPACK_SKIP_PIXELS );
			const currentUnpackSkipRows = _gl.getParameter( _gl.UNPACK_SKIP_ROWS );

			_gl.pixelStorei( _gl.UNPACK_ROW_LENGTH, image.width );

			for ( let i = 0, l = updateRanges.length; i < l; i ++ ) {

				const range = updateRanges[ i ];

				const pixelStart = Math.floor( range.start / componentStride );
				const pixelCount = Math.ceil( range.count / componentStride );

				const x = pixelStart % image.width;
				const y = Math.floor( pixelStart / image.width );

				// Assumes update ranges refer to contiguous memory
				const width = pixelCount;
				const height = 1;

				_gl.pixelStorei( _gl.UNPACK_SKIP_PIXELS, x );
				_gl.pixelStorei( _gl.UNPACK_SKIP_ROWS, y );

				state.texSubImage2D( _gl.TEXTURE_2D, 0, x, y, width, height, glFormat, glType, image.data );

			}

			texture.clearUpdateRanges();

			_gl.pixelStorei( _gl.UNPACK_ROW_LENGTH, currentUnpackRowLen );
			_gl.pixelStorei( _gl.UNPACK_SKIP_PIXELS, currentUnpackSkipPixels );
			_gl.pixelStorei( _gl.UNPACK_SKIP_ROWS, currentUnpackSkipRows );

		}

	}

	function uploadTexture( textureProperties, texture, slot ) {

		let textureType = _gl.TEXTURE_2D;

		if ( texture.isDataArrayTexture || texture.isCompressedArrayTexture ) textureType = _gl.TEXTURE_2D_ARRAY;
		if ( texture.isData3DTexture ) textureType = _gl.TEXTURE_3D;

		const forceUpload = initTexture( textureProperties, texture );
		const source = texture.source;

		state.bindTexture( textureType, textureProperties.__webglTexture, _gl.TEXTURE0 + slot );

		const sourceProperties = properties.get( source );

		if ( source.version !== sourceProperties.__version || forceUpload === true ) {

			state.activeTexture( _gl.TEXTURE0 + slot );

			const workingPrimaries = ColorManagement.getPrimaries( ColorManagement.workingColorSpace );
			const texturePrimaries = texture.colorSpace === NoColorSpace ? null : ColorManagement.getPrimaries( texture.colorSpace );
			const unpackConversion = texture.colorSpace === NoColorSpace || workingPrimaries === texturePrimaries ? _gl.NONE : _gl.BROWSER_DEFAULT_WEBGL;

			_gl.pixelStorei( _gl.UNPACK_FLIP_Y_WEBGL, texture.flipY );
			_gl.pixelStorei( _gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultiplyAlpha );
			_gl.pixelStorei( _gl.UNPACK_ALIGNMENT, texture.unpackAlignment );
			_gl.pixelStorei( _gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, unpackConversion );

			let image = resizeImage( texture.image, false, capabilities.maxTextureSize );
			image = verifyColorSpace( texture, image );

			const glFormat = utils.convert( texture.format, texture.colorSpace );

			const glType = utils.convert( texture.type );
			let glInternalFormat = getInternalFormat( texture.internalFormat, glFormat, glType, texture.colorSpace, texture.isVideoTexture );

			setTextureParameters( textureType, texture );

			let mipmap;
			const mipmaps = texture.mipmaps;

			const useTexStorage = ( texture.isVideoTexture !== true );
			const allocateMemory = ( sourceProperties.__version === undefined ) || ( forceUpload === true );
			const dataReady = source.dataReady;
			const levels = getMipLevels( texture, image );

			if ( texture.isDepthTexture ) {

				glInternalFormat = getInternalDepthFormat( texture.format === DepthStencilFormat, texture.type );

				//

				if ( allocateMemory ) {

					if ( useTexStorage ) {

						state.texStorage2D( _gl.TEXTURE_2D, 1, glInternalFormat, image.width, image.height );

					} else {

						state.texImage2D( _gl.TEXTURE_2D, 0, glInternalFormat, image.width, image.height, 0, glFormat, glType, null );

					}

				}

			} else if ( texture.isDataTexture ) {

				// use manually created mipmaps if available
				// if there are no manual mipmaps
				// set 0 level mipmap and then use GL to generate other mipmap levels

				if ( mipmaps.length > 0 ) {

					if ( useTexStorage && allocateMemory ) {

						state.texStorage2D( _gl.TEXTURE_2D, levels, glInternalFormat, mipmaps[ 0 ].width, mipmaps[ 0 ].height );

					}

					for ( let i = 0, il = mipmaps.length; i < il; i ++ ) {

						mipmap = mipmaps[ i ];

						if ( useTexStorage ) {

							if ( dataReady ) {

								state.texSubImage2D( _gl.TEXTURE_2D, i, 0, 0, mipmap.width, mipmap.height, glFormat, glType, mipmap.data );

							}

						} else {

							state.texImage2D( _gl.TEXTURE_2D, i, glInternalFormat, mipmap.width, mipmap.height, 0, glFormat, glType, mipmap.data );

						}

					}

					texture.generateMipmaps = false;

				} else {

					if ( useTexStorage ) {

						if ( allocateMemory ) {

							state.texStorage2D( _gl.TEXTURE_2D, levels, glInternalFormat, image.width, image.height );

						}

						if ( dataReady ) {

							updateTexture( texture, image, glFormat, glType );

						}

					} else {

						state.texImage2D( _gl.TEXTURE_2D, 0, glInternalFormat, image.width, image.height, 0, glFormat, glType, image.data );

					}

				}

			} else if ( texture.isCompressedTexture ) {

				if ( texture.isCompressedArrayTexture ) {

					if ( useTexStorage && allocateMemory ) {

						state.texStorage3D( _gl.TEXTURE_2D_ARRAY, levels, glInternalFormat, mipmaps[ 0 ].width, mipmaps[ 0 ].height, image.depth );

					}

					for ( let i = 0, il = mipmaps.length; i < il; i ++ ) {

						mipmap = mipmaps[ i ];

						if ( texture.format !== RGBAFormat ) {

							if ( glFormat !== null ) {

								if ( useTexStorage ) {

									if ( dataReady ) {

										if ( texture.layerUpdates.size > 0 ) {

											const layerByteLength = getByteLength( mipmap.width, mipmap.height, texture.format, texture.type );

											for ( const layerIndex of texture.layerUpdates ) {

												const layerData = mipmap.data.subarray(
													layerIndex * layerByteLength / mipmap.data.BYTES_PER_ELEMENT,
													( layerIndex + 1 ) * layerByteLength / mipmap.data.BYTES_PER_ELEMENT
												);
												state.compressedTexSubImage3D( _gl.TEXTURE_2D_ARRAY, i, 0, 0, layerIndex, mipmap.width, mipmap.height, 1, glFormat, layerData );

											}

											texture.clearLayerUpdates();

										} else {

											state.compressedTexSubImage3D( _gl.TEXTURE_2D_ARRAY, i, 0, 0, 0, mipmap.width, mipmap.height, image.depth, glFormat, mipmap.data );

										}

									}

								} else {

									state.compressedTexImage3D( _gl.TEXTURE_2D_ARRAY, i, glInternalFormat, mipmap.width, mipmap.height, image.depth, 0, mipmap.data, 0, 0 );

								}

							} else {

								console.warn( 'THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()' );

							}

						} else {

							if ( useTexStorage ) {

								if ( dataReady ) {

									state.texSubImage3D( _gl.TEXTURE_2D_ARRAY, i, 0, 0, 0, mipmap.width, mipmap.height, image.depth, glFormat, glType, mipmap.data );

								}

							} else {

								state.texImage3D( _gl.TEXTURE_2D_ARRAY, i, glInternalFormat, mipmap.width, mipmap.height, image.depth, 0, glFormat, glType, mipmap.data );

							}

						}

					}

				} else {

					if ( useTexStorage && allocateMemory ) {

						state.texStorage2D( _gl.TEXTURE_2D, levels, glInternalFormat, mipmaps[ 0 ].width, mipmaps[ 0 ].height );

					}

					for ( let i = 0, il = mipmaps.length; i < il; i ++ ) {

						mipmap = mipmaps[ i ];

						if ( texture.format !== RGBAFormat ) {

							if ( glFormat !== null ) {

								if ( useTexStorage ) {

									if ( dataReady ) {

										state.compressedTexSubImage2D( _gl.TEXTURE_2D, i, 0, 0, mipmap.width, mipmap.height, glFormat, mipmap.data );

									}

								} else {

									state.compressedTexImage2D( _gl.TEXTURE_2D, i, glInternalFormat, mipmap.width, mipmap.height, 0, mipmap.data );

								}

							} else {

								console.warn( 'THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()' );

							}

						} else {

							if ( useTexStorage ) {

								if ( dataReady ) {

									state.texSubImage2D( _gl.TEXTURE_2D, i, 0, 0, mipmap.width, mipmap.height, glFormat, glType, mipmap.data );

								}

							} else {

								state.texImage2D( _gl.TEXTURE_2D, i, glInternalFormat, mipmap.width, mipmap.height, 0, glFormat, glType, mipmap.data );

							}

						}

					}

				}

			} else if ( texture.isDataArrayTexture ) {

				if ( useTexStorage ) {

					if ( allocateMemory ) {

						state.texStorage3D( _gl.TEXTURE_2D_ARRAY, levels, glInternalFormat, image.width, image.height, image.depth );

					}

					if ( dataReady ) {

						if ( texture.layerUpdates.size > 0 ) {

							const layerByteLength = getByteLength( image.width, image.height, texture.format, texture.type );

							for ( const layerIndex of texture.layerUpdates ) {

								const layerData = image.data.subarray(
									layerIndex * layerByteLength / image.data.BYTES_PER_ELEMENT,
									( layerIndex + 1 ) * layerByteLength / image.data.BYTES_PER_ELEMENT
								);
								state.texSubImage3D( _gl.TEXTURE_2D_ARRAY, 0, 0, 0, layerIndex, image.width, image.height, 1, glFormat, glType, layerData );

							}

							texture.clearLayerUpdates();

						} else {

							state.texSubImage3D( _gl.TEXTURE_2D_ARRAY, 0, 0, 0, 0, image.width, image.height, image.depth, glFormat, glType, image.data );

						}

					}

				} else {

					state.texImage3D( _gl.TEXTURE_2D_ARRAY, 0, glInternalFormat, image.width, image.height, image.depth, 0, glFormat, glType, image.data );

				}

			} else if ( texture.isData3DTexture ) {

				if ( useTexStorage ) {

					if ( allocateMemory ) {

						state.texStorage3D( _gl.TEXTURE_3D, levels, glInternalFormat, image.width, image.height, image.depth );

					}

					if ( dataReady ) {

						state.texSubImage3D( _gl.TEXTURE_3D, 0, 0, 0, 0, image.width, image.height, image.depth, glFormat, glType, image.data );

					}

				} else {

					state.texImage3D( _gl.TEXTURE_3D, 0, glInternalFormat, image.width, image.height, image.depth, 0, glFormat, glType, image.data );

				}

			} else if ( texture.isFramebufferTexture ) {

				if ( allocateMemory ) {

					if ( useTexStorage ) {

						state.texStorage2D( _gl.TEXTURE_2D, levels, glInternalFormat, image.width, image.height );

					} else {

						let width = image.width, height = image.height;

						for ( let i = 0; i < levels; i ++ ) {

							state.texImage2D( _gl.TEXTURE_2D, i, glInternalFormat, width, height, 0, glFormat, glType, null );

							width >>= 1;
							height >>= 1;

						}

					}

				}

			} else {

				// regular Texture (image, video, canvas)

				// use manually created mipmaps if available
				// if there are no manual mipmaps
				// set 0 level mipmap and then use GL to generate other mipmap levels

				if ( mipmaps.length > 0 ) {

					if ( useTexStorage && allocateMemory ) {

						const dimensions = getDimensions( mipmaps[ 0 ] );

						state.texStorage2D( _gl.TEXTURE_2D, levels, glInternalFormat, dimensions.width, dimensions.height );

					}

					for ( let i = 0, il = mipmaps.length; i < il; i ++ ) {

						mipmap = mipmaps[ i ];

						if ( useTexStorage ) {

							if ( dataReady ) {

								state.texSubImage2D( _gl.TEXTURE_2D, i, 0, 0, glFormat, glType, mipmap );

							}

						} else {

							state.texImage2D( _gl.TEXTURE_2D, i, glInternalFormat, glFormat, glType, mipmap );

						}

					}

					texture.generateMipmaps = false;

				} else {

					if ( useTexStorage ) {

						if ( allocateMemory ) {

							const dimensions = getDimensions( image );

							state.texStorage2D( _gl.TEXTURE_2D, levels, glInternalFormat, dimensions.width, dimensions.height );

						}

						if ( dataReady ) {

							state.texSubImage2D( _gl.TEXTURE_2D, 0, 0, 0, glFormat, glType, image );

						}

					} else {

						state.texImage2D( _gl.TEXTURE_2D, 0, glInternalFormat, glFormat, glType, image );

					}

				}

			}

			if ( textureNeedsGenerateMipmaps( texture ) ) {

				generateMipmap( textureType );

			}

			sourceProperties.__version = source.version;

			if ( texture.onUpdate ) texture.onUpdate( texture );

		}

		textureProperties.__version = texture.version;

	}

	function uploadCubeTexture( textureProperties, texture, slot ) {

		if ( texture.image.length !== 6 ) return;

		const forceUpload = initTexture( textureProperties, texture );
		const source = texture.source;

		state.bindTexture( _gl.TEXTURE_CUBE_MAP, textureProperties.__webglTexture, _gl.TEXTURE0 + slot );

		const sourceProperties = properties.get( source );

		if ( source.version !== sourceProperties.__version || forceUpload === true ) {

			state.activeTexture( _gl.TEXTURE0 + slot );

			const workingPrimaries = ColorManagement.getPrimaries( ColorManagement.workingColorSpace );
			const texturePrimaries = texture.colorSpace === NoColorSpace ? null : ColorManagement.getPrimaries( texture.colorSpace );
			const unpackConversion = texture.colorSpace === NoColorSpace || workingPrimaries === texturePrimaries ? _gl.NONE : _gl.BROWSER_DEFAULT_WEBGL;

			_gl.pixelStorei( _gl.UNPACK_FLIP_Y_WEBGL, texture.flipY );
			_gl.pixelStorei( _gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultiplyAlpha );
			_gl.pixelStorei( _gl.UNPACK_ALIGNMENT, texture.unpackAlignment );
			_gl.pixelStorei( _gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, unpackConversion );

			const isCompressed = ( texture.isCompressedTexture || texture.image[ 0 ].isCompressedTexture );
			const isDataTexture = ( texture.image[ 0 ] && texture.image[ 0 ].isDataTexture );

			const cubeImage = [];

			for ( let i = 0; i < 6; i ++ ) {

				if ( ! isCompressed && ! isDataTexture ) {

					cubeImage[ i ] = resizeImage( texture.image[ i ], true, capabilities.maxCubemapSize );

				} else {

					cubeImage[ i ] = isDataTexture ? texture.image[ i ].image : texture.image[ i ];

				}

				cubeImage[ i ] = verifyColorSpace( texture, cubeImage[ i ] );

			}

			const image = cubeImage[ 0 ],
				glFormat = utils.convert( texture.format, texture.colorSpace ),
				glType = utils.convert( texture.type ),
				glInternalFormat = getInternalFormat( texture.internalFormat, glFormat, glType, texture.colorSpace );

			const useTexStorage = ( texture.isVideoTexture !== true );
			const allocateMemory = ( sourceProperties.__version === undefined ) || ( forceUpload === true );
			const dataReady = source.dataReady;
			let levels = getMipLevels( texture, image );

			setTextureParameters( _gl.TEXTURE_CUBE_MAP, texture );

			let mipmaps;

			if ( isCompressed ) {

				if ( useTexStorage && allocateMemory ) {

					state.texStorage2D( _gl.TEXTURE_CUBE_MAP, levels, glInternalFormat, image.width, image.height );

				}

				for ( let i = 0; i < 6; i ++ ) {

					mipmaps = cubeImage[ i ].mipmaps;

					for ( let j = 0; j < mipmaps.length; j ++ ) {

						const mipmap = mipmaps[ j ];

						if ( texture.format !== RGBAFormat ) {

							if ( glFormat !== null ) {

								if ( useTexStorage ) {

									if ( dataReady ) {

										state.compressedTexSubImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j, 0, 0, mipmap.width, mipmap.height, glFormat, mipmap.data );

									}

								} else {

									state.compressedTexImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j, glInternalFormat, mipmap.width, mipmap.height, 0, mipmap.data );

								}

							} else {

								console.warn( 'THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .setTextureCube()' );

							}

						} else {

							if ( useTexStorage ) {

								if ( dataReady ) {

									state.texSubImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j, 0, 0, mipmap.width, mipmap.height, glFormat, glType, mipmap.data );

								}

							} else {

								state.texImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j, glInternalFormat, mipmap.width, mipmap.height, 0, glFormat, glType, mipmap.data );

							}

						}

					}

				}

			} else {

				mipmaps = texture.mipmaps;

				if ( useTexStorage && allocateMemory ) {

					// TODO: Uniformly handle mipmap definitions
					// Normal textures and compressed cube textures define base level + mips with their mipmap array
					// Uncompressed cube textures use their mipmap array only for mips (no base level)

					if ( mipmaps.length > 0 ) levels ++;

					const dimensions = getDimensions( cubeImage[ 0 ] );

					state.texStorage2D( _gl.TEXTURE_CUBE_MAP, levels, glInternalFormat, dimensions.width, dimensions.height );

				}

				for ( let i = 0; i < 6; i ++ ) {

					if ( isDataTexture ) {

						if ( useTexStorage ) {

							if ( dataReady ) {

								state.texSubImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, 0, 0, cubeImage[ i ].width, cubeImage[ i ].height, glFormat, glType, cubeImage[ i ].data );

							}

						} else {

							state.texImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, glInternalFormat, cubeImage[ i ].width, cubeImage[ i ].height, 0, glFormat, glType, cubeImage[ i ].data );

						}

						for ( let j = 0; j < mipmaps.length; j ++ ) {

							const mipmap = mipmaps[ j ];
							const mipmapImage = mipmap.image[ i ].image;

							if ( useTexStorage ) {

								if ( dataReady ) {

									state.texSubImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j + 1, 0, 0, mipmapImage.width, mipmapImage.height, glFormat, glType, mipmapImage.data );

								}

							} else {

								state.texImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j + 1, glInternalFormat, mipmapImage.width, mipmapImage.height, 0, glFormat, glType, mipmapImage.data );

							}

						}

					} else {

						if ( useTexStorage ) {

							if ( dataReady ) {

								state.texSubImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, 0, 0, glFormat, glType, cubeImage[ i ] );

							}

						} else {

							state.texImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, glInternalFormat, glFormat, glType, cubeImage[ i ] );

						}

						for ( let j = 0; j < mipmaps.length; j ++ ) {

							const mipmap = mipmaps[ j ];

							if ( useTexStorage ) {

								if ( dataReady ) {

									state.texSubImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j + 1, 0, 0, glFormat, glType, mipmap.image[ i ] );

								}

							} else {

								state.texImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j + 1, glInternalFormat, glFormat, glType, mipmap.image[ i ] );

							}

						}

					}

				}

			}

			if ( textureNeedsGenerateMipmaps( texture ) ) {

				// We assume images for cube map have the same size.
				generateMipmap( _gl.TEXTURE_CUBE_MAP );

			}

			sourceProperties.__version = source.version;

			if ( texture.onUpdate ) texture.onUpdate( texture );

		}

		textureProperties.__version = texture.version;

	}

	// Render targets

	// Setup storage for target texture and bind it to correct framebuffer
	function setupFrameBufferTexture( framebuffer, renderTarget, texture, attachment, textureTarget, level ) {

		const glFormat = utils.convert( texture.format, texture.colorSpace );
		const glType = utils.convert( texture.type );
		const glInternalFormat = getInternalFormat( texture.internalFormat, glFormat, glType, texture.colorSpace );
		const renderTargetProperties = properties.get( renderTarget );
		const textureProperties = properties.get( texture );

		textureProperties.__renderTarget = renderTarget;

		if ( ! renderTargetProperties.__hasExternalTextures ) {

			const width = Math.max( 1, renderTarget.width >> level );
			const height = Math.max( 1, renderTarget.height >> level );

			if ( textureTarget === _gl.TEXTURE_3D || textureTarget === _gl.TEXTURE_2D_ARRAY ) {

				state.texImage3D( textureTarget, level, glInternalFormat, width, height, renderTarget.depth, 0, glFormat, glType, null );

			} else {

				state.texImage2D( textureTarget, level, glInternalFormat, width, height, 0, glFormat, glType, null );

			}

		}

		state.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer );

		if ( useMultisampledRTT( renderTarget ) ) {

			multisampledRTTExt.framebufferTexture2DMultisampleEXT( _gl.FRAMEBUFFER, attachment, textureTarget, textureProperties.__webglTexture, 0, getRenderTargetSamples( renderTarget ) );

		} else if ( textureTarget === _gl.TEXTURE_2D || ( textureTarget >= _gl.TEXTURE_CUBE_MAP_POSITIVE_X && textureTarget <= _gl.TEXTURE_CUBE_MAP_NEGATIVE_Z ) ) { // see #24753

			_gl.framebufferTexture2D( _gl.FRAMEBUFFER, attachment, textureTarget, textureProperties.__webglTexture, level );

		}

		state.bindFramebuffer( _gl.FRAMEBUFFER, null );

	}

	// Setup storage for internal depth/stencil buffers and bind to correct framebuffer
	function setupRenderBufferStorage( renderbuffer, renderTarget, isMultisample ) {

		_gl.bindRenderbuffer( _gl.RENDERBUFFER, renderbuffer );

		if ( renderTarget.depthBuffer ) {

			// retrieve the depth attachment types
			const depthTexture = renderTarget.depthTexture;
			const depthType = depthTexture && depthTexture.isDepthTexture ? depthTexture.type : null;
			const glInternalFormat = getInternalDepthFormat( renderTarget.stencilBuffer, depthType );
			const glAttachmentType = renderTarget.stencilBuffer ? _gl.DEPTH_STENCIL_ATTACHMENT : _gl.DEPTH_ATTACHMENT;

			// set up the attachment
			const samples = getRenderTargetSamples( renderTarget );
			const isUseMultisampledRTT = useMultisampledRTT( renderTarget );
			if ( isUseMultisampledRTT ) {

				multisampledRTTExt.renderbufferStorageMultisampleEXT( _gl.RENDERBUFFER, samples, glInternalFormat, renderTarget.width, renderTarget.height );

			} else if ( isMultisample ) {

				_gl.renderbufferStorageMultisample( _gl.RENDERBUFFER, samples, glInternalFormat, renderTarget.width, renderTarget.height );

			} else {

				_gl.renderbufferStorage( _gl.RENDERBUFFER, glInternalFormat, renderTarget.width, renderTarget.height );

			}

			_gl.framebufferRenderbuffer( _gl.FRAMEBUFFER, glAttachmentType, _gl.RENDERBUFFER, renderbuffer );

		} else {

			const textures = renderTarget.textures;

			for ( let i = 0; i < textures.length; i ++ ) {

				const texture = textures[ i ];

				const glFormat = utils.convert( texture.format, texture.colorSpace );
				const glType = utils.convert( texture.type );
				const glInternalFormat = getInternalFormat( texture.internalFormat, glFormat, glType, texture.colorSpace );
				const samples = getRenderTargetSamples( renderTarget );

				if ( isMultisample && useMultisampledRTT( renderTarget ) === false ) {

					_gl.renderbufferStorageMultisample( _gl.RENDERBUFFER, samples, glInternalFormat, renderTarget.width, renderTarget.height );

				} else if ( useMultisampledRTT( renderTarget ) ) {

					multisampledRTTExt.renderbufferStorageMultisampleEXT( _gl.RENDERBUFFER, samples, glInternalFormat, renderTarget.width, renderTarget.height );

				} else {

					_gl.renderbufferStorage( _gl.RENDERBUFFER, glInternalFormat, renderTarget.width, renderTarget.height );

				}

			}

		}

		_gl.bindRenderbuffer( _gl.RENDERBUFFER, null );

	}

	// Setup resources for a Depth Texture for a FBO (needs an extension)
	function setupDepthTexture( framebuffer, renderTarget ) {

		const isCube = ( renderTarget && renderTarget.isWebGLCubeRenderTarget );
		if ( isCube ) throw new Error( 'Depth Texture with cube render targets is not supported' );

		state.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer );

		if ( ! ( renderTarget.depthTexture && renderTarget.depthTexture.isDepthTexture ) ) {

			throw new Error( 'renderTarget.depthTexture must be an instance of THREE.DepthTexture' );

		}

		const textureProperties = properties.get( renderTarget.depthTexture );
		textureProperties.__renderTarget = renderTarget;

		// upload an empty depth texture with framebuffer size
		if ( ! textureProperties.__webglTexture ||
				renderTarget.depthTexture.image.width !== renderTarget.width ||
				renderTarget.depthTexture.image.height !== renderTarget.height ) {

			renderTarget.depthTexture.image.width = renderTarget.width;
			renderTarget.depthTexture.image.height = renderTarget.height;
			renderTarget.depthTexture.needsUpdate = true;

		}

		setTexture2D( renderTarget.depthTexture, 0 );

		const webglDepthTexture = textureProperties.__webglTexture;
		const samples = getRenderTargetSamples( renderTarget );

		if ( renderTarget.depthTexture.format === DepthFormat ) {

			if ( useMultisampledRTT( renderTarget ) ) {

				multisampledRTTExt.framebufferTexture2DMultisampleEXT( _gl.FRAMEBUFFER, _gl.DEPTH_ATTACHMENT, _gl.TEXTURE_2D, webglDepthTexture, 0, samples );

			} else {

				_gl.framebufferTexture2D( _gl.FRAMEBUFFER, _gl.DEPTH_ATTACHMENT, _gl.TEXTURE_2D, webglDepthTexture, 0 );

			}

		} else if ( renderTarget.depthTexture.format === DepthStencilFormat ) {

			if ( useMultisampledRTT( renderTarget ) ) {

				multisampledRTTExt.framebufferTexture2DMultisampleEXT( _gl.FRAMEBUFFER, _gl.DEPTH_STENCIL_ATTACHMENT, _gl.TEXTURE_2D, webglDepthTexture, 0, samples );

			} else {

				_gl.framebufferTexture2D( _gl.FRAMEBUFFER, _gl.DEPTH_STENCIL_ATTACHMENT, _gl.TEXTURE_2D, webglDepthTexture, 0 );

			}

		} else {

			throw new Error( 'Unknown depthTexture format' );

		}

	}

	// Setup GL resources for a non-texture depth buffer
	function setupDepthRenderbuffer( renderTarget ) {

		const renderTargetProperties = properties.get( renderTarget );
		const isCube = ( renderTarget.isWebGLCubeRenderTarget === true );

		// if the bound depth texture has changed
		if ( renderTargetProperties.__boundDepthTexture !== renderTarget.depthTexture ) {

			// fire the dispose event to get rid of stored state associated with the previously bound depth buffer
			const depthTexture = renderTarget.depthTexture;
			if ( renderTargetProperties.__depthDisposeCallback ) {

				renderTargetProperties.__depthDisposeCallback();

			}

			// set up dispose listeners to track when the currently attached buffer is implicitly unbound
			if ( depthTexture ) {

				const disposeEvent = () => {

					delete renderTargetProperties.__boundDepthTexture;
					delete renderTargetProperties.__depthDisposeCallback;
					depthTexture.removeEventListener( 'dispose', disposeEvent );

				};

				depthTexture.addEventListener( 'dispose', disposeEvent );
				renderTargetProperties.__depthDisposeCallback = disposeEvent;

			}

			renderTargetProperties.__boundDepthTexture = depthTexture;

		}

		if ( renderTarget.depthTexture && ! renderTargetProperties.__autoAllocateDepthBuffer ) {

			if ( isCube ) throw new Error( 'target.depthTexture not supported in Cube render targets' );

			const mipmaps = renderTarget.texture.mipmaps;

			if ( mipmaps && mipmaps.length > 0 ) {

				setupDepthTexture( renderTargetProperties.__webglFramebuffer[ 0 ], renderTarget );

			} else {

				setupDepthTexture( renderTargetProperties.__webglFramebuffer, renderTarget );

			}

		} else {

			if ( isCube ) {

				renderTargetProperties.__webglDepthbuffer = [];

				for ( let i = 0; i < 6; i ++ ) {

					state.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglFramebuffer[ i ] );

					if ( renderTargetProperties.__webglDepthbuffer[ i ] === undefined ) {

						renderTargetProperties.__webglDepthbuffer[ i ] = _gl.createRenderbuffer();
						setupRenderBufferStorage( renderTargetProperties.__webglDepthbuffer[ i ], renderTarget, false );

					} else {

						// attach buffer if it's been created already
						const glAttachmentType = renderTarget.stencilBuffer ? _gl.DEPTH_STENCIL_ATTACHMENT : _gl.DEPTH_ATTACHMENT;
						const renderbuffer = renderTargetProperties.__webglDepthbuffer[ i ];
						_gl.bindRenderbuffer( _gl.RENDERBUFFER, renderbuffer );
						_gl.framebufferRenderbuffer( _gl.FRAMEBUFFER, glAttachmentType, _gl.RENDERBUFFER, renderbuffer );

					}

				}

			} else {

				const mipmaps = renderTarget.texture.mipmaps;

				if ( mipmaps && mipmaps.length > 0 ) {

					state.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglFramebuffer[ 0 ] );

				} else {

					state.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglFramebuffer );

				}

				if ( renderTargetProperties.__webglDepthbuffer === undefined ) {

					renderTargetProperties.__webglDepthbuffer = _gl.createRenderbuffer();
					setupRenderBufferStorage( renderTargetProperties.__webglDepthbuffer, renderTarget, false );

				} else {

					// attach buffer if it's been created already
					const glAttachmentType = renderTarget.stencilBuffer ? _gl.DEPTH_STENCIL_ATTACHMENT : _gl.DEPTH_ATTACHMENT;
					const renderbuffer = renderTargetProperties.__webglDepthbuffer;
					_gl.bindRenderbuffer( _gl.RENDERBUFFER, renderbuffer );
					_gl.framebufferRenderbuffer( _gl.FRAMEBUFFER, glAttachmentType, _gl.RENDERBUFFER, renderbuffer );

				}

			}

		}

		state.bindFramebuffer( _gl.FRAMEBUFFER, null );

	}

	// rebind framebuffer with external textures
	function rebindTextures( renderTarget, colorTexture, depthTexture ) {

		const renderTargetProperties = properties.get( renderTarget );

		if ( colorTexture !== undefined ) {

			setupFrameBufferTexture( renderTargetProperties.__webglFramebuffer, renderTarget, renderTarget.texture, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, 0 );

		}

		if ( depthTexture !== undefined ) {

			setupDepthRenderbuffer( renderTarget );

		}

	}

	// Set up GL resources for the render target
	function setupRenderTarget( renderTarget ) {

		const texture = renderTarget.texture;

		const renderTargetProperties = properties.get( renderTarget );
		const textureProperties = properties.get( texture );

		renderTarget.addEventListener( 'dispose', onRenderTargetDispose );

		const textures = renderTarget.textures;

		const isCube = ( renderTarget.isWebGLCubeRenderTarget === true );
		const isMultipleRenderTargets = ( textures.length > 1 );

		if ( ! isMultipleRenderTargets ) {

			if ( textureProperties.__webglTexture === undefined ) {

				textureProperties.__webglTexture = _gl.createTexture();

			}

			textureProperties.__version = texture.version;
			info.memory.textures ++;

		}

		// Setup framebuffer

		if ( isCube ) {

			renderTargetProperties.__webglFramebuffer = [];

			for ( let i = 0; i < 6; i ++ ) {

				if ( texture.mipmaps && texture.mipmaps.length > 0 ) {

					renderTargetProperties.__webglFramebuffer[ i ] = [];

					for ( let level = 0; level < texture.mipmaps.length; level ++ ) {

						renderTargetProperties.__webglFramebuffer[ i ][ level ] = _gl.createFramebuffer();

					}

				} else {

					renderTargetProperties.__webglFramebuffer[ i ] = _gl.createFramebuffer();

				}

			}

		} else {

			if ( texture.mipmaps && texture.mipmaps.length > 0 ) {

				renderTargetProperties.__webglFramebuffer = [];

				for ( let level = 0; level < texture.mipmaps.length; level ++ ) {

					renderTargetProperties.__webglFramebuffer[ level ] = _gl.createFramebuffer();

				}

			} else {

				renderTargetProperties.__webglFramebuffer = _gl.createFramebuffer();

			}

			if ( isMultipleRenderTargets ) {

				for ( let i = 0, il = textures.length; i < il; i ++ ) {

					const attachmentProperties = properties.get( textures[ i ] );

					if ( attachmentProperties.__webglTexture === undefined ) {

						attachmentProperties.__webglTexture = _gl.createTexture();

						info.memory.textures ++;

					}

				}

			}

			if ( ( renderTarget.samples > 0 ) && useMultisampledRTT( renderTarget ) === false ) {

				renderTargetProperties.__webglMultisampledFramebuffer = _gl.createFramebuffer();
				renderTargetProperties.__webglColorRenderbuffer = [];

				state.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglMultisampledFramebuffer );

				for ( let i = 0; i < textures.length; i ++ ) {

					const texture = textures[ i ];
					renderTargetProperties.__webglColorRenderbuffer[ i ] = _gl.createRenderbuffer();

					_gl.bindRenderbuffer( _gl.RENDERBUFFER, renderTargetProperties.__webglColorRenderbuffer[ i ] );

					const glFormat = utils.convert( texture.format, texture.colorSpace );
					const glType = utils.convert( texture.type );
					const glInternalFormat = getInternalFormat( texture.internalFormat, glFormat, glType, texture.colorSpace, renderTarget.isXRRenderTarget === true );
					const samples = getRenderTargetSamples( renderTarget );
					_gl.renderbufferStorageMultisample( _gl.RENDERBUFFER, samples, glInternalFormat, renderTarget.width, renderTarget.height );

					_gl.framebufferRenderbuffer( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0 + i, _gl.RENDERBUFFER, renderTargetProperties.__webglColorRenderbuffer[ i ] );

				}

				_gl.bindRenderbuffer( _gl.RENDERBUFFER, null );

				if ( renderTarget.depthBuffer ) {

					renderTargetProperties.__webglDepthRenderbuffer = _gl.createRenderbuffer();
					setupRenderBufferStorage( renderTargetProperties.__webglDepthRenderbuffer, renderTarget, true );

				}

				state.bindFramebuffer( _gl.FRAMEBUFFER, null );

			}

		}

		// Setup color buffer

		if ( isCube ) {

			state.bindTexture( _gl.TEXTURE_CUBE_MAP, textureProperties.__webglTexture );
			setTextureParameters( _gl.TEXTURE_CUBE_MAP, texture );

			for ( let i = 0; i < 6; i ++ ) {

				if ( texture.mipmaps && texture.mipmaps.length > 0 ) {

					for ( let level = 0; level < texture.mipmaps.length; level ++ ) {

						setupFrameBufferTexture( renderTargetProperties.__webglFramebuffer[ i ][ level ], renderTarget, texture, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, level );

					}

				} else {

					setupFrameBufferTexture( renderTargetProperties.__webglFramebuffer[ i ], renderTarget, texture, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0 );

				}

			}

			if ( textureNeedsGenerateMipmaps( texture ) ) {

				generateMipmap( _gl.TEXTURE_CUBE_MAP );

			}

			state.unbindTexture();

		} else if ( isMultipleRenderTargets ) {

			for ( let i = 0, il = textures.length; i < il; i ++ ) {

				const attachment = textures[ i ];
				const attachmentProperties = properties.get( attachment );

				let glTextureType = _gl.TEXTURE_2D;

				if ( renderTarget.isWebGL3DRenderTarget || renderTarget.isWebGLArrayRenderTarget ) {

					glTextureType = renderTarget.isWebGL3DRenderTarget ? _gl.TEXTURE_3D : _gl.TEXTURE_2D_ARRAY;

				}

				state.bindTexture( glTextureType, attachmentProperties.__webglTexture );
				setTextureParameters( glTextureType, attachment );
				setupFrameBufferTexture( renderTargetProperties.__webglFramebuffer, renderTarget, attachment, _gl.COLOR_ATTACHMENT0 + i, glTextureType, 0 );

				if ( textureNeedsGenerateMipmaps( attachment ) ) {

					generateMipmap( glTextureType );

				}

			}

			state.unbindTexture();

		} else {

			let glTextureType = _gl.TEXTURE_2D;

			if ( renderTarget.isWebGL3DRenderTarget || renderTarget.isWebGLArrayRenderTarget ) {

				glTextureType = renderTarget.isWebGL3DRenderTarget ? _gl.TEXTURE_3D : _gl.TEXTURE_2D_ARRAY;

			}

			state.bindTexture( glTextureType, textureProperties.__webglTexture );
			setTextureParameters( glTextureType, texture );

			if ( texture.mipmaps && texture.mipmaps.length > 0 ) {

				for ( let level = 0; level < texture.mipmaps.length; level ++ ) {

					setupFrameBufferTexture( renderTargetProperties.__webglFramebuffer[ level ], renderTarget, texture, _gl.COLOR_ATTACHMENT0, glTextureType, level );

				}

			} else {

				setupFrameBufferTexture( renderTargetProperties.__webglFramebuffer, renderTarget, texture, _gl.COLOR_ATTACHMENT0, glTextureType, 0 );

			}

			if ( textureNeedsGenerateMipmaps( texture ) ) {

				generateMipmap( glTextureType );

			}

			state.unbindTexture();

		}

		// Setup depth and stencil buffers

		if ( renderTarget.depthBuffer ) {

			setupDepthRenderbuffer( renderTarget );

		}

	}

	function updateRenderTargetMipmap( renderTarget ) {

		const textures = renderTarget.textures;

		for ( let i = 0, il = textures.length; i < il; i ++ ) {

			const texture = textures[ i ];

			if ( textureNeedsGenerateMipmaps( texture ) ) {

				const targetType = getTargetType( renderTarget );
				const webglTexture = properties.get( texture ).__webglTexture;

				state.bindTexture( targetType, webglTexture );
				generateMipmap( targetType );
				state.unbindTexture();

			}

		}

	}

	const invalidationArrayRead = [];
	const invalidationArrayDraw = [];

	function updateMultisampleRenderTarget( renderTarget ) {

		if ( renderTarget.samples > 0 ) {

			if ( useMultisampledRTT( renderTarget ) === false ) {

				const textures = renderTarget.textures;
				const width = renderTarget.width;
				const height = renderTarget.height;
				let mask = _gl.COLOR_BUFFER_BIT;
				const depthStyle = renderTarget.stencilBuffer ? _gl.DEPTH_STENCIL_ATTACHMENT : _gl.DEPTH_ATTACHMENT;
				const renderTargetProperties = properties.get( renderTarget );
				const isMultipleRenderTargets = ( textures.length > 1 );

				// If MRT we need to remove FBO attachments
				if ( isMultipleRenderTargets ) {

					for ( let i = 0; i < textures.length; i ++ ) {

						state.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglMultisampledFramebuffer );
						_gl.framebufferRenderbuffer( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0 + i, _gl.RENDERBUFFER, null );

						state.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglFramebuffer );
						_gl.framebufferTexture2D( _gl.DRAW_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0 + i, _gl.TEXTURE_2D, null, 0 );

					}

				}

				state.bindFramebuffer( _gl.READ_FRAMEBUFFER, renderTargetProperties.__webglMultisampledFramebuffer );

				const mipmaps = renderTarget.texture.mipmaps;

				if ( mipmaps && mipmaps.length > 0 ) {

					state.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, renderTargetProperties.__webglFramebuffer[ 0 ] );

				} else {

					state.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, renderTargetProperties.__webglFramebuffer );

				}

				for ( let i = 0; i < textures.length; i ++ ) {

					if ( renderTarget.resolveDepthBuffer ) {

						if ( renderTarget.depthBuffer ) mask |= _gl.DEPTH_BUFFER_BIT;

						// resolving stencil is slow with a D3D backend. disable it for all transmission render targets (see #27799)

						if ( renderTarget.stencilBuffer && renderTarget.resolveStencilBuffer ) mask |= _gl.STENCIL_BUFFER_BIT;

					}

					if ( isMultipleRenderTargets ) {

						_gl.framebufferRenderbuffer( _gl.READ_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.RENDERBUFFER, renderTargetProperties.__webglColorRenderbuffer[ i ] );

						const webglTexture = properties.get( textures[ i ] ).__webglTexture;
						_gl.framebufferTexture2D( _gl.DRAW_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, webglTexture, 0 );

					}

					_gl.blitFramebuffer( 0, 0, width, height, 0, 0, width, height, mask, _gl.NEAREST );

					if ( supportsInvalidateFramebuffer === true ) {

						invalidationArrayRead.length = 0;
						invalidationArrayDraw.length = 0;

						invalidationArrayRead.push( _gl.COLOR_ATTACHMENT0 + i );

						if ( renderTarget.depthBuffer && renderTarget.resolveDepthBuffer === false ) {

							invalidationArrayRead.push( depthStyle );
							invalidationArrayDraw.push( depthStyle );

							_gl.invalidateFramebuffer( _gl.DRAW_FRAMEBUFFER, invalidationArrayDraw );

						}

						_gl.invalidateFramebuffer( _gl.READ_FRAMEBUFFER, invalidationArrayRead );

					}

				}

				state.bindFramebuffer( _gl.READ_FRAMEBUFFER, null );
				state.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, null );

				// If MRT since pre-blit we removed the FBO we need to reconstruct the attachments
				if ( isMultipleRenderTargets ) {

					for ( let i = 0; i < textures.length; i ++ ) {

						state.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglMultisampledFramebuffer );
						_gl.framebufferRenderbuffer( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0 + i, _gl.RENDERBUFFER, renderTargetProperties.__webglColorRenderbuffer[ i ] );

						const webglTexture = properties.get( textures[ i ] ).__webglTexture;

						state.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglFramebuffer );
						_gl.framebufferTexture2D( _gl.DRAW_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0 + i, _gl.TEXTURE_2D, webglTexture, 0 );

					}

				}

				state.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, renderTargetProperties.__webglMultisampledFramebuffer );

			} else {

				if ( renderTarget.depthBuffer && renderTarget.resolveDepthBuffer === false && supportsInvalidateFramebuffer ) {

					const depthStyle = renderTarget.stencilBuffer ? _gl.DEPTH_STENCIL_ATTACHMENT : _gl.DEPTH_ATTACHMENT;

					_gl.invalidateFramebuffer( _gl.DRAW_FRAMEBUFFER, [ depthStyle ] );

				}

			}

		}

	}

	function getRenderTargetSamples( renderTarget ) {

		return Math.min( capabilities.maxSamples, renderTarget.samples );

	}

	function useMultisampledRTT( renderTarget ) {

		const renderTargetProperties = properties.get( renderTarget );

		return renderTarget.samples > 0 && extensions.has( 'WEBGL_multisampled_render_to_texture' ) === true && renderTargetProperties.__useRenderToTexture !== false;

	}

	function updateVideoTexture( texture ) {

		const frame = info.render.frame;

		// Check the last frame we updated the VideoTexture

		if ( _videoTextures.get( texture ) !== frame ) {

			_videoTextures.set( texture, frame );
			texture.update();

		}

	}

	function verifyColorSpace( texture, image ) {

		const colorSpace = texture.colorSpace;
		const format = texture.format;
		const type = texture.type;

		if ( texture.isCompressedTexture === true || texture.isVideoTexture === true ) return image;

		if ( colorSpace !== LinearSRGBColorSpace && colorSpace !== NoColorSpace ) {

			// sRGB

			if ( ColorManagement.getTransfer( colorSpace ) === SRGBTransfer ) {

				// in WebGL 2 uncompressed textures can only be sRGB encoded if they have the RGBA8 format

				if ( format !== RGBAFormat || type !== UnsignedByteType ) {

					console.warn( 'THREE.WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType.' );

				}

			} else {

				console.error( 'THREE.WebGLTextures: Unsupported texture color space:', colorSpace );

			}

		}

		return image;

	}

	function getDimensions( image ) {

		if ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) {

			// if intrinsic data are not available, fallback to width/height

			_imageDimensions.width = image.naturalWidth || image.width;
			_imageDimensions.height = image.naturalHeight || image.height;

		} else if ( typeof VideoFrame !== 'undefined' && image instanceof VideoFrame ) {

			_imageDimensions.width = image.displayWidth;
			_imageDimensions.height = image.displayHeight;

		} else {

			_imageDimensions.width = image.width;
			_imageDimensions.height = image.height;

		}

		return _imageDimensions;

	}

	//

	this.allocateTextureUnit = allocateTextureUnit;
	this.resetTextureUnits = resetTextureUnits;

	this.setTexture2D = setTexture2D;
	this.setTexture2DArray = setTexture2DArray;
	this.setTexture3D = setTexture3D;
	this.setTextureCube = setTextureCube;
	this.rebindTextures = rebindTextures;
	this.setupRenderTarget = setupRenderTarget;
	this.updateRenderTargetMipmap = updateRenderTargetMipmap;
	this.updateMultisampleRenderTarget = updateMultisampleRenderTarget;
	this.setupDepthRenderbuffer = setupDepthRenderbuffer;
	this.setupFrameBufferTexture = setupFrameBufferTexture;
	this.useMultisampledRTT = useMultisampledRTT;

}

function WebGLUtils( gl, extensions ) {

	function convert( p, colorSpace = NoColorSpace ) {

		let extension;

		const transfer = ColorManagement.getTransfer( colorSpace );

		if ( p === UnsignedByteType ) return gl.UNSIGNED_BYTE;
		if ( p === UnsignedShort4444Type ) return gl.UNSIGNED_SHORT_4_4_4_4;
		if ( p === UnsignedShort5551Type ) return gl.UNSIGNED_SHORT_5_5_5_1;
		if ( p === UnsignedInt5999Type ) return gl.UNSIGNED_INT_5_9_9_9_REV;
		if ( p === UnsignedInt101111Type ) return gl.UNSIGNED_INT_10F_11F_11F_REV;

		if ( p === ByteType ) return gl.BYTE;
		if ( p === ShortType ) return gl.SHORT;
		if ( p === UnsignedShortType ) return gl.UNSIGNED_SHORT;
		if ( p === IntType ) return gl.INT;
		if ( p === UnsignedIntType ) return gl.UNSIGNED_INT;
		if ( p === FloatType ) return gl.FLOAT;
		if ( p === HalfFloatType ) return gl.HALF_FLOAT;

		if ( p === AlphaFormat ) return gl.ALPHA;
		if ( p === RGBFormat ) return gl.RGB;
		if ( p === RGBAFormat ) return gl.RGBA;
		if ( p === DepthFormat ) return gl.DEPTH_COMPONENT;
		if ( p === DepthStencilFormat ) return gl.DEPTH_STENCIL;

		// WebGL2 formats.

		if ( p === RedFormat ) return gl.RED;
		if ( p === RedIntegerFormat ) return gl.RED_INTEGER;
		if ( p === RGFormat ) return gl.RG;
		if ( p === RGIntegerFormat ) return gl.RG_INTEGER;
		if ( p === RGBAIntegerFormat ) return gl.RGBA_INTEGER;

		// S3TC

		if ( p === RGB_S3TC_DXT1_Format || p === RGBA_S3TC_DXT1_Format || p === RGBA_S3TC_DXT3_Format || p === RGBA_S3TC_DXT5_Format ) {

			if ( transfer === SRGBTransfer ) {

				extension = extensions.get( 'WEBGL_compressed_texture_s3tc_srgb' );

				if ( extension !== null ) {

					if ( p === RGB_S3TC_DXT1_Format ) return extension.COMPRESSED_SRGB_S3TC_DXT1_EXT;
					if ( p === RGBA_S3TC_DXT1_Format ) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
					if ( p === RGBA_S3TC_DXT3_Format ) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;
					if ( p === RGBA_S3TC_DXT5_Format ) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;

				} else {

					return null;

				}

			} else {

				extension = extensions.get( 'WEBGL_compressed_texture_s3tc' );

				if ( extension !== null ) {

					if ( p === RGB_S3TC_DXT1_Format ) return extension.COMPRESSED_RGB_S3TC_DXT1_EXT;
					if ( p === RGBA_S3TC_DXT1_Format ) return extension.COMPRESSED_RGBA_S3TC_DXT1_EXT;
					if ( p === RGBA_S3TC_DXT3_Format ) return extension.COMPRESSED_RGBA_S3TC_DXT3_EXT;
					if ( p === RGBA_S3TC_DXT5_Format ) return extension.COMPRESSED_RGBA_S3TC_DXT5_EXT;

				} else {

					return null;

				}

			}

		}

		// PVRTC

		if ( p === RGB_PVRTC_4BPPV1_Format || p === RGB_PVRTC_2BPPV1_Format || p === RGBA_PVRTC_4BPPV1_Format || p === RGBA_PVRTC_2BPPV1_Format ) {

			extension = extensions.get( 'WEBGL_compressed_texture_pvrtc' );

			if ( extension !== null ) {

				if ( p === RGB_PVRTC_4BPPV1_Format ) return extension.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
				if ( p === RGB_PVRTC_2BPPV1_Format ) return extension.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
				if ( p === RGBA_PVRTC_4BPPV1_Format ) return extension.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
				if ( p === RGBA_PVRTC_2BPPV1_Format ) return extension.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;

			} else {

				return null;

			}

		}

		// ETC

		if ( p === RGB_ETC1_Format || p === RGB_ETC2_Format || p === RGBA_ETC2_EAC_Format ) {

			extension = extensions.get( 'WEBGL_compressed_texture_etc' );

			if ( extension !== null ) {

				if ( p === RGB_ETC1_Format || p === RGB_ETC2_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ETC2 : extension.COMPRESSED_RGB8_ETC2;
				if ( p === RGBA_ETC2_EAC_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC : extension.COMPRESSED_RGBA8_ETC2_EAC;

			} else {

				return null;

			}

		}

		// ASTC

		if ( p === RGBA_ASTC_4x4_Format || p === RGBA_ASTC_5x4_Format || p === RGBA_ASTC_5x5_Format ||
			p === RGBA_ASTC_6x5_Format || p === RGBA_ASTC_6x6_Format || p === RGBA_ASTC_8x5_Format ||
			p === RGBA_ASTC_8x6_Format || p === RGBA_ASTC_8x8_Format || p === RGBA_ASTC_10x5_Format ||
			p === RGBA_ASTC_10x6_Format || p === RGBA_ASTC_10x8_Format || p === RGBA_ASTC_10x10_Format ||
			p === RGBA_ASTC_12x10_Format || p === RGBA_ASTC_12x12_Format ) {

			extension = extensions.get( 'WEBGL_compressed_texture_astc' );

			if ( extension !== null ) {

				if ( p === RGBA_ASTC_4x4_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR : extension.COMPRESSED_RGBA_ASTC_4x4_KHR;
				if ( p === RGBA_ASTC_5x4_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR : extension.COMPRESSED_RGBA_ASTC_5x4_KHR;
				if ( p === RGBA_ASTC_5x5_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR : extension.COMPRESSED_RGBA_ASTC_5x5_KHR;
				if ( p === RGBA_ASTC_6x5_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR : extension.COMPRESSED_RGBA_ASTC_6x5_KHR;
				if ( p === RGBA_ASTC_6x6_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR : extension.COMPRESSED_RGBA_ASTC_6x6_KHR;
				if ( p === RGBA_ASTC_8x5_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR : extension.COMPRESSED_RGBA_ASTC_8x5_KHR;
				if ( p === RGBA_ASTC_8x6_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR : extension.COMPRESSED_RGBA_ASTC_8x6_KHR;
				if ( p === RGBA_ASTC_8x8_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR : extension.COMPRESSED_RGBA_ASTC_8x8_KHR;
				if ( p === RGBA_ASTC_10x5_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR : extension.COMPRESSED_RGBA_ASTC_10x5_KHR;
				if ( p === RGBA_ASTC_10x6_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR : extension.COMPRESSED_RGBA_ASTC_10x6_KHR;
				if ( p === RGBA_ASTC_10x8_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR : extension.COMPRESSED_RGBA_ASTC_10x8_KHR;
				if ( p === RGBA_ASTC_10x10_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR : extension.COMPRESSED_RGBA_ASTC_10x10_KHR;
				if ( p === RGBA_ASTC_12x10_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR : extension.COMPRESSED_RGBA_ASTC_12x10_KHR;
				if ( p === RGBA_ASTC_12x12_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR : extension.COMPRESSED_RGBA_ASTC_12x12_KHR;

			} else {

				return null;

			}

		}

		// BPTC

		if ( p === RGBA_BPTC_Format || p === RGB_BPTC_SIGNED_Format || p === RGB_BPTC_UNSIGNED_Format ) {

			extension = extensions.get( 'EXT_texture_compression_bptc' );

			if ( extension !== null ) {

				if ( p === RGBA_BPTC_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT : extension.COMPRESSED_RGBA_BPTC_UNORM_EXT;
				if ( p === RGB_BPTC_SIGNED_Format ) return extension.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT;
				if ( p === RGB_BPTC_UNSIGNED_Format ) return extension.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT;

			} else {

				return null;

			}

		}

		// RGTC

		if ( p === RED_RGTC1_Format || p === SIGNED_RED_RGTC1_Format || p === RED_GREEN_RGTC2_Format || p === SIGNED_RED_GREEN_RGTC2_Format ) {

			extension = extensions.get( 'EXT_texture_compression_rgtc' );

			if ( extension !== null ) {

				if ( p === RED_RGTC1_Format ) return extension.COMPRESSED_RED_RGTC1_EXT;
				if ( p === SIGNED_RED_RGTC1_Format ) return extension.COMPRESSED_SIGNED_RED_RGTC1_EXT;
				if ( p === RED_GREEN_RGTC2_Format ) return extension.COMPRESSED_RED_GREEN_RGTC2_EXT;
				if ( p === SIGNED_RED_GREEN_RGTC2_Format ) return extension.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT;

			} else {

				return null;

			}

		}

		//

		if ( p === UnsignedInt248Type ) return gl.UNSIGNED_INT_24_8;

		// if "p" can't be resolved, assume the user defines a WebGL constant as a string (fallback/workaround for packed RGB formats)

		return ( gl[ p ] !== undefined ) ? gl[ p ] : null;

	}

	return { convert: convert };

}

/**
 * This type of camera can be used in order to efficiently render a scene with a
 * predefined set of cameras. This is an important performance aspect for
 * rendering VR scenes.
 *
 * An instance of `ArrayCamera` always has an array of sub cameras. It's mandatory
 * to define for each sub camera the `viewport` property which determines the
 * part of the viewport that is rendered with this camera.
 *
 * @augments PerspectiveCamera
 */
class ArrayCamera extends PerspectiveCamera {

	/**
	 * Constructs a new array camera.
	 *
	 * @param {Array<PerspectiveCamera>} [array=[]] - An array of perspective sub cameras.
	 */
	constructor( array = [] ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isArrayCamera = true;

		/**
		 * Whether this camera is used with multiview rendering or not.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default false
		 */
		this.isMultiViewCamera = false;

		/**
		 * An array of perspective sub cameras.
		 *
		 * @type {Array<PerspectiveCamera>}
		 */
		this.cameras = array;

	}

}

/**
 * This is almost identical to an {@link Object3D}. Its purpose is to
 * make working with groups of objects syntactically clearer.
 *
 * ```js
 * // Create a group and add the two cubes.
 * // These cubes can now be rotated / scaled etc as a group.
 * const group = new THREE.Group();
 *
 * group.add( meshA );
 * group.add( meshB );
 *
 * scene.add( group );
 * ```
 *
 * @augments Object3D
 */
let Group$1 = class Group extends Object3D {

	constructor() {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isGroup = true;

		this.type = 'Group';

	}

};

const _moveEvent = { type: 'move' };

/**
 * Class for representing a XR controller with its
 * different coordinate systems.
 *
 * @private
 */
class WebXRController {

	/**
	 * Constructs a new XR controller.
	 */
	constructor() {

		/**
		 * A group representing the target ray space
		 * of the XR controller.
		 *
		 * @private
		 * @type {?Group}
		 * @default null
		 */
		this._targetRay = null;

		/**
		 * A group representing the grip space
		 * of the XR controller.
		 *
		 * @private
		 * @type {?Group}
		 * @default null
		 */
		this._grip = null;

		/**
		 * A group representing the hand space
		 * of the XR controller.
		 *
		 * @private
		 * @type {?Group}
		 * @default null
		 */
		this._hand = null;

	}

	/**
	 * Returns a group representing the hand space of the XR controller.
	 *
	 * @return {Group} A group representing the hand space of the XR controller.
	 */
	getHandSpace() {

		if ( this._hand === null ) {

			this._hand = new Group$1();
			this._hand.matrixAutoUpdate = false;
			this._hand.visible = false;

			this._hand.joints = {};
			this._hand.inputState = { pinching: false };

		}

		return this._hand;

	}

	/**
	 * Returns a group representing the target ray space of the XR controller.
	 *
	 * @return {Group} A group representing the target ray space of the XR controller.
	 */
	getTargetRaySpace() {

		if ( this._targetRay === null ) {

			this._targetRay = new Group$1();
			this._targetRay.matrixAutoUpdate = false;
			this._targetRay.visible = false;
			this._targetRay.hasLinearVelocity = false;
			this._targetRay.linearVelocity = new Vector3();
			this._targetRay.hasAngularVelocity = false;
			this._targetRay.angularVelocity = new Vector3();

		}

		return this._targetRay;

	}

	/**
	 * Returns a group representing the grip space of the XR controller.
	 *
	 * @return {Group} A group representing the grip space of the XR controller.
	 */
	getGripSpace() {

		if ( this._grip === null ) {

			this._grip = new Group$1();
			this._grip.matrixAutoUpdate = false;
			this._grip.visible = false;
			this._grip.hasLinearVelocity = false;
			this._grip.linearVelocity = new Vector3();
			this._grip.hasAngularVelocity = false;
			this._grip.angularVelocity = new Vector3();

		}

		return this._grip;

	}

	/**
	 * Dispatches the given event to the groups representing
	 * the different coordinate spaces of the XR controller.
	 *
	 * @param {Object} event - The event to dispatch.
	 * @return {WebXRController} A reference to this instance.
	 */
	dispatchEvent( event ) {

		if ( this._targetRay !== null ) {

			this._targetRay.dispatchEvent( event );

		}

		if ( this._grip !== null ) {

			this._grip.dispatchEvent( event );

		}

		if ( this._hand !== null ) {

			this._hand.dispatchEvent( event );

		}

		return this;

	}

	/**
	 * Connects the controller with the given XR input source.
	 *
	 * @param {XRInputSource} inputSource - The input source.
	 * @return {WebXRController} A reference to this instance.
	 */
	connect( inputSource ) {

		if ( inputSource && inputSource.hand ) {

			const hand = this._hand;

			if ( hand ) {

				for ( const inputjoint of inputSource.hand.values() ) {

					// Initialize hand with joints when connected
					this._getHandJoint( hand, inputjoint );

				}

			}

		}

		this.dispatchEvent( { type: 'connected', data: inputSource } );

		return this;

	}

	/**
	 * Disconnects the controller from the given XR input source.
	 *
	 * @param {XRInputSource} inputSource - The input source.
	 * @return {WebXRController} A reference to this instance.
	 */
	disconnect( inputSource ) {

		this.dispatchEvent( { type: 'disconnected', data: inputSource } );

		if ( this._targetRay !== null ) {

			this._targetRay.visible = false;

		}

		if ( this._grip !== null ) {

			this._grip.visible = false;

		}

		if ( this._hand !== null ) {

			this._hand.visible = false;

		}

		return this;

	}

	/**
	 * Updates the controller with the given input source, XR frame and reference space.
	 * This updates the transformations of the groups that represent the different
	 * coordinate systems of the controller.
	 *
	 * @param {XRInputSource} inputSource - The input source.
	 * @param {XRFrame} frame - The XR frame.
	 * @param {XRReferenceSpace} referenceSpace - The reference space.
	 * @return {WebXRController} A reference to this instance.
	 */
	update( inputSource, frame, referenceSpace ) {

		let inputPose = null;
		let gripPose = null;
		let handPose = null;

		const targetRay = this._targetRay;
		const grip = this._grip;
		const hand = this._hand;

		if ( inputSource && frame.session.visibilityState !== 'visible-blurred' ) {

			if ( hand && inputSource.hand ) {

				handPose = true;

				for ( const inputjoint of inputSource.hand.values() ) {

					// Update the joints groups with the XRJoint poses
					const jointPose = frame.getJointPose( inputjoint, referenceSpace );

					// The transform of this joint will be updated with the joint pose on each frame
					const joint = this._getHandJoint( hand, inputjoint );

					if ( jointPose !== null ) {

						joint.matrix.fromArray( jointPose.transform.matrix );
						joint.matrix.decompose( joint.position, joint.rotation, joint.scale );
						joint.matrixWorldNeedsUpdate = true;
						joint.jointRadius = jointPose.radius;

					}

					joint.visible = jointPose !== null;

				}

				// Custom events

				// Check pinchz
				const indexTip = hand.joints[ 'index-finger-tip' ];
				const thumbTip = hand.joints[ 'thumb-tip' ];
				const distance = indexTip.position.distanceTo( thumbTip.position );

				const distanceToPinch = 0.02;
				const threshold = 0.005;

				if ( hand.inputState.pinching && distance > distanceToPinch + threshold ) {

					hand.inputState.pinching = false;
					this.dispatchEvent( {
						type: 'pinchend',
						handedness: inputSource.handedness,
						target: this
					} );

				} else if ( ! hand.inputState.pinching && distance <= distanceToPinch - threshold ) {

					hand.inputState.pinching = true;
					this.dispatchEvent( {
						type: 'pinchstart',
						handedness: inputSource.handedness,
						target: this
					} );

				}

			} else {

				if ( grip !== null && inputSource.gripSpace ) {

					gripPose = frame.getPose( inputSource.gripSpace, referenceSpace );

					if ( gripPose !== null ) {

						grip.matrix.fromArray( gripPose.transform.matrix );
						grip.matrix.decompose( grip.position, grip.rotation, grip.scale );
						grip.matrixWorldNeedsUpdate = true;

						if ( gripPose.linearVelocity ) {

							grip.hasLinearVelocity = true;
							grip.linearVelocity.copy( gripPose.linearVelocity );

						} else {

							grip.hasLinearVelocity = false;

						}

						if ( gripPose.angularVelocity ) {

							grip.hasAngularVelocity = true;
							grip.angularVelocity.copy( gripPose.angularVelocity );

						} else {

							grip.hasAngularVelocity = false;

						}

					}

				}

			}

			if ( targetRay !== null ) {

				inputPose = frame.getPose( inputSource.targetRaySpace, referenceSpace );

				// Some runtimes (namely Vive Cosmos with Vive OpenXR Runtime) have only grip space and ray space is equal to it
				if ( inputPose === null && gripPose !== null ) {

					inputPose = gripPose;

				}

				if ( inputPose !== null ) {

					targetRay.matrix.fromArray( inputPose.transform.matrix );
					targetRay.matrix.decompose( targetRay.position, targetRay.rotation, targetRay.scale );
					targetRay.matrixWorldNeedsUpdate = true;

					if ( inputPose.linearVelocity ) {

						targetRay.hasLinearVelocity = true;
						targetRay.linearVelocity.copy( inputPose.linearVelocity );

					} else {

						targetRay.hasLinearVelocity = false;

					}

					if ( inputPose.angularVelocity ) {

						targetRay.hasAngularVelocity = true;
						targetRay.angularVelocity.copy( inputPose.angularVelocity );

					} else {

						targetRay.hasAngularVelocity = false;

					}

					this.dispatchEvent( _moveEvent );

				}

			}


		}

		if ( targetRay !== null ) {

			targetRay.visible = ( inputPose !== null );

		}

		if ( grip !== null ) {

			grip.visible = ( gripPose !== null );

		}

		if ( hand !== null ) {

			hand.visible = ( handPose !== null );

		}

		return this;

	}

	/**
	 * Returns a group representing the hand joint for the given input joint.
	 *
	 * @private
	 * @param {Group} hand - The group representing the hand space.
	 * @param {XRJointSpace} inputjoint - The hand joint data.
	 * @return {Group} A group representing the hand joint for the given input joint.
	 */
	_getHandJoint( hand, inputjoint ) {

		if ( hand.joints[ inputjoint.jointName ] === undefined ) {

			const joint = new Group$1();
			joint.matrixAutoUpdate = false;
			joint.visible = false;
			hand.joints[ inputjoint.jointName ] = joint;

			hand.add( joint );

		}

		return hand.joints[ inputjoint.jointName ];

	}

}

/**
 * Represents a texture created externally with the same renderer context.
 *
 * This may be a texture from a protected media stream, device camera feed,
 * or other data feeds like a depth sensor.
 *
 * Note that this class is only supported in {@link WebGLRenderer}, and in
 * the {@link WebGPURenderer} WebGPU backend.
 *
 * @augments Texture
 */
class ExternalTexture extends Texture {

	/**
	 * Creates a new raw texture.
	 *
	 * @param {?(WebGLTexture|GPUTexture)} [sourceTexture=null] - The external texture.
	 */
	constructor( sourceTexture = null ) {

		super();

		/**
		 * The external source texture.
		 *
		 * @type {?(WebGLTexture|GPUTexture)}
		 * @default null
		 */
		this.sourceTexture = sourceTexture;

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isExternalTexture = true;

	}

	copy( source ) {

		super.copy( source );

		this.sourceTexture = source.sourceTexture;

		return this;

	}

}

const _occlusion_vertex = `
void main() {

	gl_Position = vec4( position, 1.0 );

}`;

const _occlusion_fragment = `
uniform sampler2DArray depthColor;
uniform float depthWidth;
uniform float depthHeight;

void main() {

	vec2 coord = vec2( gl_FragCoord.x / depthWidth, gl_FragCoord.y / depthHeight );

	if ( coord.x >= 1.0 ) {

		gl_FragDepth = texture( depthColor, vec3( coord.x - 1.0, coord.y, 1 ) ).r;

	} else {

		gl_FragDepth = texture( depthColor, vec3( coord.x, coord.y, 0 ) ).r;

	}

}`;

/**
 * A XR module that manages the access to the Depth Sensing API.
 */
class WebXRDepthSensing {

	/**
	 * Constructs a new depth sensing module.
	 */
	constructor() {

		/**
		 * An opaque texture representing the depth of the user's environment.
		 *
		 * @type {?ExternalTexture}
		 */
		this.texture = null;

		/**
		 * A plane mesh for visualizing the depth texture.
		 *
		 * @type {?Mesh}
		 */
		this.mesh = null;

		/**
		 * The depth near value.
		 *
		 * @type {number}
		 */
		this.depthNear = 0;

		/**
		 * The depth near far.
		 *
		 * @type {number}
		 */
		this.depthFar = 0;

	}

	/**
	 * Inits the depth sensing module
	 *
	 * @param {XRWebGLDepthInformation} depthData - The XR depth data.
	 * @param {XRRenderState} renderState - The XR render state.
	 */
	init( depthData, renderState ) {

		if ( this.texture === null ) {

			const texture = new ExternalTexture( depthData.texture );

			if ( ( depthData.depthNear !== renderState.depthNear ) || ( depthData.depthFar !== renderState.depthFar ) ) {

				this.depthNear = depthData.depthNear;
				this.depthFar = depthData.depthFar;

			}

			this.texture = texture;

		}

	}

	/**
	 * Returns a plane mesh that visualizes the depth texture.
	 *
	 * @param {ArrayCamera} cameraXR - The XR camera.
	 * @return {?Mesh} The plane mesh.
	 */
	getMesh( cameraXR ) {

		if ( this.texture !== null ) {

			if ( this.mesh === null ) {

				const viewport = cameraXR.cameras[ 0 ].viewport;
				const material = new ShaderMaterial( {
					vertexShader: _occlusion_vertex,
					fragmentShader: _occlusion_fragment,
					uniforms: {
						depthColor: { value: this.texture },
						depthWidth: { value: viewport.z },
						depthHeight: { value: viewport.w }
					}
				} );

				this.mesh = new Mesh( new PlaneGeometry( 20, 20 ), material );

			}

		}

		return this.mesh;

	}

	/**
	 * Resets the module
	 */
	reset() {

		this.texture = null;
		this.mesh = null;

	}

	/**
	 * Returns a texture representing the depth of the user's environment.
	 *
	 * @return {?ExternalTexture} The depth texture.
	 */
	getDepthTexture() {

		return this.texture;

	}

}

/**
 * This class represents an abstraction of the WebXR Device API and is
 * internally used by {@link WebGLRenderer}. `WebXRManager` also provides a public
 * interface that allows users to enable/disable XR and perform XR related
 * tasks like for instance retrieving controllers.
 *
 * @augments EventDispatcher
 * @hideconstructor
 */
class WebXRManager extends EventDispatcher {

	/**
	 * Constructs a new WebGL renderer.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {WebGL2RenderingContext} gl - The rendering context.
	 */
	constructor( renderer, gl ) {

		super();

		const scope = this;

		let session = null;

		let framebufferScaleFactor = 1.0;

		let referenceSpace = null;
		let referenceSpaceType = 'local-floor';
		// Set default foveation to maximum.
		let foveation = 1.0;
		let customReferenceSpace = null;

		let pose = null;
		let glBinding = null;
		let glProjLayer = null;
		let glBaseLayer = null;
		let xrFrame = null;

		const supportsGlBinding = typeof XRWebGLBinding !== 'undefined';

		const depthSensing = new WebXRDepthSensing();
		const cameraAccessTextures = {};
		const attributes = gl.getContextAttributes();

		let initialRenderTarget = null;
		let newRenderTarget = null;

		const controllers = [];
		const controllerInputSources = [];

		const currentSize = new Vector2();
		let currentPixelRatio = null;

		//

		const cameraL = new PerspectiveCamera();
		cameraL.viewport = new Vector4();

		const cameraR = new PerspectiveCamera();
		cameraR.viewport = new Vector4();

		const cameras = [ cameraL, cameraR ];

		const cameraXR = new ArrayCamera();

		let _currentDepthNear = null;
		let _currentDepthFar = null;

		//

		/**
		 * Whether the manager's XR camera should be automatically updated or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.cameraAutoUpdate = true;

		/**
		 * This flag notifies the renderer to be ready for XR rendering. Set it to `true`
		 * if you are going to use XR in your app.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.enabled = false;

		/**
		 * Whether XR presentation is active or not.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default false
		 */
		this.isPresenting = false;

		/**
		 * Returns a group representing the `target ray` space of the XR controller.
		 * Use this space for visualizing 3D objects that support the user in pointing
		 * tasks like UI interaction.
		 *
		 * @param {number} index - The index of the controller.
		 * @return {Group} A group representing the `target ray` space.
		 */
		this.getController = function ( index ) {

			let controller = controllers[ index ];

			if ( controller === undefined ) {

				controller = new WebXRController();
				controllers[ index ] = controller;

			}

			return controller.getTargetRaySpace();

		};

		/**
		 * Returns a group representing the `grip` space of the XR controller.
		 * Use this space for visualizing 3D objects that support the user in pointing
		 * tasks like UI interaction.
		 *
		 * Note: If you want to show something in the user's hand AND offer a
		 * pointing ray at the same time, you'll want to attached the handheld object
		 * to the group returned by `getControllerGrip()` and the ray to the
		 * group returned by `getController()`. The idea is to have two
		 * different groups in two different coordinate spaces for the same WebXR
		 * controller.
		 *
		 * @param {number} index - The index of the controller.
		 * @return {Group} A group representing the `grip` space.
		 */
		this.getControllerGrip = function ( index ) {

			let controller = controllers[ index ];

			if ( controller === undefined ) {

				controller = new WebXRController();
				controllers[ index ] = controller;

			}

			return controller.getGripSpace();

		};

		/**
		 * Returns a group representing the `hand` space of the XR controller.
		 * Use this space for visualizing 3D objects that support the user in pointing
		 * tasks like UI interaction.
		 *
		 * @param {number} index - The index of the controller.
		 * @return {Group} A group representing the `hand` space.
		 */
		this.getHand = function ( index ) {

			let controller = controllers[ index ];

			if ( controller === undefined ) {

				controller = new WebXRController();
				controllers[ index ] = controller;

			}

			return controller.getHandSpace();

		};

		//

		function onSessionEvent( event ) {

			const controllerIndex = controllerInputSources.indexOf( event.inputSource );

			if ( controllerIndex === -1 ) {

				return;

			}

			const controller = controllers[ controllerIndex ];

			if ( controller !== undefined ) {

				controller.update( event.inputSource, event.frame, customReferenceSpace || referenceSpace );
				controller.dispatchEvent( { type: event.type, data: event.inputSource } );

			}

		}

		function onSessionEnd() {

			session.removeEventListener( 'select', onSessionEvent );
			session.removeEventListener( 'selectstart', onSessionEvent );
			session.removeEventListener( 'selectend', onSessionEvent );
			session.removeEventListener( 'squeeze', onSessionEvent );
			session.removeEventListener( 'squeezestart', onSessionEvent );
			session.removeEventListener( 'squeezeend', onSessionEvent );
			session.removeEventListener( 'end', onSessionEnd );
			session.removeEventListener( 'inputsourceschange', onInputSourcesChange );

			for ( let i = 0; i < controllers.length; i ++ ) {

				const inputSource = controllerInputSources[ i ];

				if ( inputSource === null ) continue;

				controllerInputSources[ i ] = null;

				controllers[ i ].disconnect( inputSource );

			}

			_currentDepthNear = null;
			_currentDepthFar = null;

			depthSensing.reset();
			for ( const key in cameraAccessTextures ) {

				delete cameraAccessTextures[ key ];

			}

			// restore framebuffer/rendering state

			renderer.setRenderTarget( initialRenderTarget );

			glBaseLayer = null;
			glProjLayer = null;
			glBinding = null;
			session = null;
			newRenderTarget = null;

			//

			animation.stop();

			scope.isPresenting = false;

			renderer.setPixelRatio( currentPixelRatio );
			renderer.setSize( currentSize.width, currentSize.height, false );

			scope.dispatchEvent( { type: 'sessionend' } );

		}

		/**
		 * Sets the framebuffer scale factor.
		 *
		 * This method can not be used during a XR session.
		 *
		 * @param {number} value - The framebuffer scale factor.
		 */
		this.setFramebufferScaleFactor = function ( value ) {

			framebufferScaleFactor = value;

			if ( scope.isPresenting === true ) {

				console.warn( 'THREE.WebXRManager: Cannot change framebuffer scale while presenting.' );

			}

		};

		/**
		 * Sets the reference space type. Can be used to configure a spatial relationship with the user's physical
		 * environment. Depending on how the user moves in 3D space, setting an appropriate reference space can
		 * improve tracking. Default is `local-floor`. Valid values can be found here
		 * https://developer.mozilla.org/en-US/docs/Web/API/XRReferenceSpace#reference_space_types.
		 *
		 * This method can not be used during a XR session.
		 *
		 * @param {string} value - The reference space type.
		 */
		this.setReferenceSpaceType = function ( value ) {

			referenceSpaceType = value;

			if ( scope.isPresenting === true ) {

				console.warn( 'THREE.WebXRManager: Cannot change reference space type while presenting.' );

			}

		};

		/**
		 * Returns the XR reference space.
		 *
		 * @return {XRReferenceSpace} The XR reference space.
		 */
		this.getReferenceSpace = function () {

			return customReferenceSpace || referenceSpace;

		};

		/**
		 * Sets a custom XR reference space.
		 *
		 * @param {XRReferenceSpace} space - The XR reference space.
		 */
		this.setReferenceSpace = function ( space ) {

			customReferenceSpace = space;

		};

		/**
		 * Returns the current base layer.
		 *
		 * This is an `XRProjectionLayer` when the targeted XR device supports the
		 * WebXR Layers API, or an `XRWebGLLayer` otherwise.
		 *
		 * @return {?(XRWebGLLayer|XRProjectionLayer)} The XR base layer.
		 */
		this.getBaseLayer = function () {

			return glProjLayer !== null ? glProjLayer : glBaseLayer;

		};

		/**
		 * Returns the current XR binding.
		 *
		 * Creates a new binding if needed and the browser is
		 * capable of doing so.
		 *
		 * @return {?XRWebGLBinding} The XR binding. Returns `null` if one cannot be created.
		 */
		this.getBinding = function () {

			if ( glBinding === null && supportsGlBinding ) {

				glBinding = new XRWebGLBinding( session, gl );

			}

			return glBinding;

		};

		/**
		 * Returns the current XR frame.
		 *
		 * @return {?XRFrame} The XR frame. Returns `null` when used outside a XR session.
		 */
		this.getFrame = function () {

			return xrFrame;

		};

		/**
		 * Returns the current XR session.
		 *
		 * @return {?XRSession} The XR session. Returns `null` when used outside a XR session.
		 */
		this.getSession = function () {

			return session;

		};

		/**
		 * After a XR session has been requested usually with one of the `*Button` modules, it
		 * is injected into the renderer with this method. This method triggers the start of
		 * the actual XR rendering.
		 *
		 * @async
		 * @param {XRSession} value - The XR session to set.
		 * @return {Promise} A Promise that resolves when the session has been set.
		 */
		this.setSession = async function ( value ) {

			session = value;

			if ( session !== null ) {

				initialRenderTarget = renderer.getRenderTarget();

				session.addEventListener( 'select', onSessionEvent );
				session.addEventListener( 'selectstart', onSessionEvent );
				session.addEventListener( 'selectend', onSessionEvent );
				session.addEventListener( 'squeeze', onSessionEvent );
				session.addEventListener( 'squeezestart', onSessionEvent );
				session.addEventListener( 'squeezeend', onSessionEvent );
				session.addEventListener( 'end', onSessionEnd );
				session.addEventListener( 'inputsourceschange', onInputSourcesChange );

				if ( attributes.xrCompatible !== true ) {

					await gl.makeXRCompatible();

				}

				currentPixelRatio = renderer.getPixelRatio();
				renderer.getSize( currentSize );


				// Check that the browser implements the necessary APIs to use an
				// XRProjectionLayer rather than an XRWebGLLayer
				const supportsLayers = supportsGlBinding && 'createProjectionLayer' in XRWebGLBinding.prototype;

				if ( ! supportsLayers ) {

					const layerInit = {
						antialias: attributes.antialias,
						alpha: true,
						depth: attributes.depth,
						stencil: attributes.stencil,
						framebufferScaleFactor: framebufferScaleFactor
					};

					glBaseLayer = new XRWebGLLayer( session, gl, layerInit );

					session.updateRenderState( { baseLayer: glBaseLayer } );

					renderer.setPixelRatio( 1 );
					renderer.setSize( glBaseLayer.framebufferWidth, glBaseLayer.framebufferHeight, false );

					newRenderTarget = new WebGLRenderTarget(
						glBaseLayer.framebufferWidth,
						glBaseLayer.framebufferHeight,
						{
							format: RGBAFormat,
							type: UnsignedByteType,
							colorSpace: renderer.outputColorSpace,
							stencilBuffer: attributes.stencil,
							resolveDepthBuffer: ( glBaseLayer.ignoreDepthValues === false ),
							resolveStencilBuffer: ( glBaseLayer.ignoreDepthValues === false )

						}
					);

				} else {

					let depthFormat = null;
					let depthType = null;
					let glDepthFormat = null;

					if ( attributes.depth ) {

						glDepthFormat = attributes.stencil ? gl.DEPTH24_STENCIL8 : gl.DEPTH_COMPONENT24;
						depthFormat = attributes.stencil ? DepthStencilFormat : DepthFormat;
						depthType = attributes.stencil ? UnsignedInt248Type : UnsignedIntType;

					}

					const projectionlayerInit = {
						colorFormat: gl.RGBA8,
						depthFormat: glDepthFormat,
						scaleFactor: framebufferScaleFactor
					};

					glBinding = this.getBinding();

					glProjLayer = glBinding.createProjectionLayer( projectionlayerInit );

					session.updateRenderState( { layers: [ glProjLayer ] } );

					renderer.setPixelRatio( 1 );
					renderer.setSize( glProjLayer.textureWidth, glProjLayer.textureHeight, false );

					newRenderTarget = new WebGLRenderTarget(
						glProjLayer.textureWidth,
						glProjLayer.textureHeight,
						{
							format: RGBAFormat,
							type: UnsignedByteType,
							depthTexture: new DepthTexture( glProjLayer.textureWidth, glProjLayer.textureHeight, depthType, undefined, undefined, undefined, undefined, undefined, undefined, depthFormat ),
							stencilBuffer: attributes.stencil,
							colorSpace: renderer.outputColorSpace,
							samples: attributes.antialias ? 4 : 0,
							resolveDepthBuffer: ( glProjLayer.ignoreDepthValues === false ),
							resolveStencilBuffer: ( glProjLayer.ignoreDepthValues === false )
						} );

				}

				newRenderTarget.isXRRenderTarget = true; // TODO Remove this when possible, see #23278

				this.setFoveation( foveation );

				customReferenceSpace = null;
				referenceSpace = await session.requestReferenceSpace( referenceSpaceType );

				animation.setContext( session );
				animation.start();

				scope.isPresenting = true;

				scope.dispatchEvent( { type: 'sessionstart' } );

			}

		};

		/**
		 * Returns the environment blend mode from the current XR session.
		 *
		 * @return {'opaque'|'additive'|'alpha-blend'|undefined} The environment blend mode. Returns `undefined` when used outside of a XR session.
		 */
		this.getEnvironmentBlendMode = function () {

			if ( session !== null ) {

				return session.environmentBlendMode;

			}

		};

		/**
		 * Returns the current depth texture computed via depth sensing.
		 *
		 * See {@link WebXRDepthSensing#getDepthTexture}.
		 *
		 * @return {?Texture} The depth texture.
		 */
		this.getDepthTexture = function () {

			return depthSensing.getDepthTexture();

		};

		function onInputSourcesChange( event ) {

			// Notify disconnected

			for ( let i = 0; i < event.removed.length; i ++ ) {

				const inputSource = event.removed[ i ];
				const index = controllerInputSources.indexOf( inputSource );

				if ( index >= 0 ) {

					controllerInputSources[ index ] = null;
					controllers[ index ].disconnect( inputSource );

				}

			}

			// Notify connected

			for ( let i = 0; i < event.added.length; i ++ ) {

				const inputSource = event.added[ i ];

				let controllerIndex = controllerInputSources.indexOf( inputSource );

				if ( controllerIndex === -1 ) {

					// Assign input source a controller that currently has no input source

					for ( let i = 0; i < controllers.length; i ++ ) {

						if ( i >= controllerInputSources.length ) {

							controllerInputSources.push( inputSource );
							controllerIndex = i;
							break;

						} else if ( controllerInputSources[ i ] === null ) {

							controllerInputSources[ i ] = inputSource;
							controllerIndex = i;
							break;

						}

					}

					// If all controllers do currently receive input we ignore new ones

					if ( controllerIndex === -1 ) break;

				}

				const controller = controllers[ controllerIndex ];

				if ( controller ) {

					controller.connect( inputSource );

				}

			}

		}

		//

		const cameraLPos = new Vector3();
		const cameraRPos = new Vector3();

		/**
		 * Assumes 2 cameras that are parallel and share an X-axis, and that
		 * the cameras' projection and world matrices have already been set.
		 * And that near and far planes are identical for both cameras.
		 * Visualization of this technique: https://computergraphics.stackexchange.com/a/4765
		 *
		 * @param {ArrayCamera} camera - The camera to update.
		 * @param {PerspectiveCamera} cameraL - The left camera.
		 * @param {PerspectiveCamera} cameraR - The right camera.
		 */
		function setProjectionFromUnion( camera, cameraL, cameraR ) {

			cameraLPos.setFromMatrixPosition( cameraL.matrixWorld );
			cameraRPos.setFromMatrixPosition( cameraR.matrixWorld );

			const ipd = cameraLPos.distanceTo( cameraRPos );

			const projL = cameraL.projectionMatrix.elements;
			const projR = cameraR.projectionMatrix.elements;

			// VR systems will have identical far and near planes, and
			// most likely identical top and bottom frustum extents.
			// Use the left camera for these values.
			const near = projL[ 14 ] / ( projL[ 10 ] - 1 );
			const far = projL[ 14 ] / ( projL[ 10 ] + 1 );
			const topFov = ( projL[ 9 ] + 1 ) / projL[ 5 ];
			const bottomFov = ( projL[ 9 ] - 1 ) / projL[ 5 ];

			const leftFov = ( projL[ 8 ] - 1 ) / projL[ 0 ];
			const rightFov = ( projR[ 8 ] + 1 ) / projR[ 0 ];
			const left = near * leftFov;
			const right = near * rightFov;

			// Calculate the new camera's position offset from the
			// left camera. xOffset should be roughly half `ipd`.
			const zOffset = ipd / ( - leftFov + rightFov );
			const xOffset = zOffset * - leftFov;

			// TODO: Better way to apply this offset?
			cameraL.matrixWorld.decompose( camera.position, camera.quaternion, camera.scale );
			camera.translateX( xOffset );
			camera.translateZ( zOffset );
			camera.matrixWorld.compose( camera.position, camera.quaternion, camera.scale );
			camera.matrixWorldInverse.copy( camera.matrixWorld ).invert();

			// Check if the projection uses an infinite far plane.
			if ( projL[ 10 ] === -1 ) {

				// Use the projection matrix from the left eye.
				// The camera offset is sufficient to include the view volumes
				// of both eyes (assuming symmetric projections).
				camera.projectionMatrix.copy( cameraL.projectionMatrix );
				camera.projectionMatrixInverse.copy( cameraL.projectionMatrixInverse );

			} else {

				// Find the union of the frustum values of the cameras and scale
				// the values so that the near plane's position does not change in world space,
				// although must now be relative to the new union camera.
				const near2 = near + zOffset;
				const far2 = far + zOffset;
				const left2 = left - xOffset;
				const right2 = right + ( ipd - xOffset );
				const top2 = topFov * far / far2 * near2;
				const bottom2 = bottomFov * far / far2 * near2;

				camera.projectionMatrix.makePerspective( left2, right2, top2, bottom2, near2, far2 );
				camera.projectionMatrixInverse.copy( camera.projectionMatrix ).invert();

			}

		}

		function updateCamera( camera, parent ) {

			if ( parent === null ) {

				camera.matrixWorld.copy( camera.matrix );

			} else {

				camera.matrixWorld.multiplyMatrices( parent.matrixWorld, camera.matrix );

			}

			camera.matrixWorldInverse.copy( camera.matrixWorld ).invert();

		}

		/**
		 * Updates the state of the XR camera. Use this method on app level if you
		 * set `cameraAutoUpdate` to `false`. The method requires the non-XR
		 * camera of the scene as a parameter. The passed in camera's transformation
		 * is automatically adjusted to the position of the XR camera when calling
		 * this method.
		 *
		 * @param {Camera} camera - The camera.
		 */
		this.updateCamera = function ( camera ) {

			if ( session === null ) return;

			let depthNear = camera.near;
			let depthFar = camera.far;

			if ( depthSensing.texture !== null ) {

				if ( depthSensing.depthNear > 0 ) depthNear = depthSensing.depthNear;
				if ( depthSensing.depthFar > 0 ) depthFar = depthSensing.depthFar;

			}

			cameraXR.near = cameraR.near = cameraL.near = depthNear;
			cameraXR.far = cameraR.far = cameraL.far = depthFar;

			if ( _currentDepthNear !== cameraXR.near || _currentDepthFar !== cameraXR.far ) {

				// Note that the new renderState won't apply until the next frame. See #18320

				session.updateRenderState( {
					depthNear: cameraXR.near,
					depthFar: cameraXR.far
				} );

				_currentDepthNear = cameraXR.near;
				_currentDepthFar = cameraXR.far;

			}

			// inherit camera layers and enable eye layers (1 = left, 2 = right)
			cameraXR.layers.mask = camera.layers.mask | 0b110;
			cameraL.layers.mask = cameraXR.layers.mask & 0b011;
			cameraR.layers.mask = cameraXR.layers.mask & 0b101;

			const parent = camera.parent;
			const cameras = cameraXR.cameras;

			updateCamera( cameraXR, parent );

			for ( let i = 0; i < cameras.length; i ++ ) {

				updateCamera( cameras[ i ], parent );

			}

			// update projection matrix for proper view frustum culling

			if ( cameras.length === 2 ) {

				setProjectionFromUnion( cameraXR, cameraL, cameraR );

			} else {

				// assume single camera setup (AR)

				cameraXR.projectionMatrix.copy( cameraL.projectionMatrix );

			}

			// update user camera and its children

			updateUserCamera( camera, cameraXR, parent );

		};

		function updateUserCamera( camera, cameraXR, parent ) {

			if ( parent === null ) {

				camera.matrix.copy( cameraXR.matrixWorld );

			} else {

				camera.matrix.copy( parent.matrixWorld );
				camera.matrix.invert();
				camera.matrix.multiply( cameraXR.matrixWorld );

			}

			camera.matrix.decompose( camera.position, camera.quaternion, camera.scale );
			camera.updateMatrixWorld( true );

			camera.projectionMatrix.copy( cameraXR.projectionMatrix );
			camera.projectionMatrixInverse.copy( cameraXR.projectionMatrixInverse );

			if ( camera.isPerspectiveCamera ) {

				camera.fov = RAD2DEG * 2 * Math.atan( 1 / camera.projectionMatrix.elements[ 5 ] );
				camera.zoom = 1;

			}

		}

		/**
		 * Returns an instance of {@link ArrayCamera} which represents the XR camera
		 * of the active XR session. For each view it holds a separate camera object.
		 *
		 * The camera's `fov` is currently not used and does not reflect the fov of
		 * the XR camera. If you need the fov on app level, you have to compute in
		 * manually from the XR camera's projection matrices.
		 *
		 * @return {ArrayCamera} The XR camera.
		 */
		this.getCamera = function () {

			return cameraXR;

		};

		/**
		 * Returns the amount of foveation used by the XR compositor for the projection layer.
		 *
		 * @return {number|undefined} The amount of foveation.
		 */
		this.getFoveation = function () {

			if ( glProjLayer === null && glBaseLayer === null ) {

				return undefined;

			}

			return foveation;

		};

		/**
		 * Sets the foveation value.
		 *
		 * @param {number} value - A number in the range `[0,1]` where `0` means no foveation (full resolution)
		 * and `1` means maximum foveation (the edges render at lower resolution).
		 */
		this.setFoveation = function ( value ) {

			// 0 = no foveation = full resolution
			// 1 = maximum foveation = the edges render at lower resolution

			foveation = value;

			if ( glProjLayer !== null ) {

				glProjLayer.fixedFoveation = value;

			}

			if ( glBaseLayer !== null && glBaseLayer.fixedFoveation !== undefined ) {

				glBaseLayer.fixedFoveation = value;

			}

		};

		/**
		 * Returns `true` if depth sensing is supported.
		 *
		 * @return {boolean} Whether depth sensing is supported or not.
		 */
		this.hasDepthSensing = function () {

			return depthSensing.texture !== null;

		};

		/**
		 * Returns the depth sensing mesh.
		 *
		 * See {@link WebXRDepthSensing#getMesh}.
		 *
		 * @return {Mesh} The depth sensing mesh.
		 */
		this.getDepthSensingMesh = function () {

			return depthSensing.getMesh( cameraXR );

		};

		/**
		 * Retrieves an opaque texture from the view-aligned {@link XRCamera}.
		 * Only available during the current animation loop.
		 *
		 * @param {XRCamera} xrCamera - The camera to query.
		 * @return {?Texture} An opaque texture representing the current raw camera frame.
		 */
		this.getCameraTexture = function ( xrCamera ) {

			return cameraAccessTextures[ xrCamera ];

		};

		// Animation Loop

		let onAnimationFrameCallback = null;

		function onAnimationFrame( time, frame ) {

			pose = frame.getViewerPose( customReferenceSpace || referenceSpace );
			xrFrame = frame;

			if ( pose !== null ) {

				const views = pose.views;

				if ( glBaseLayer !== null ) {

					renderer.setRenderTargetFramebuffer( newRenderTarget, glBaseLayer.framebuffer );
					renderer.setRenderTarget( newRenderTarget );

				}

				let cameraXRNeedsUpdate = false;

				// check if it's necessary to rebuild cameraXR's camera list

				if ( views.length !== cameraXR.cameras.length ) {

					cameraXR.cameras.length = 0;
					cameraXRNeedsUpdate = true;

				}

				for ( let i = 0; i < views.length; i ++ ) {

					const view = views[ i ];

					let viewport = null;

					if ( glBaseLayer !== null ) {

						viewport = glBaseLayer.getViewport( view );

					} else {

						const glSubImage = glBinding.getViewSubImage( glProjLayer, view );
						viewport = glSubImage.viewport;

						// For side-by-side projection, we only produce a single texture for both eyes.
						if ( i === 0 ) {

							renderer.setRenderTargetTextures(
								newRenderTarget,
								glSubImage.colorTexture,
								glSubImage.depthStencilTexture );

							renderer.setRenderTarget( newRenderTarget );

						}

					}

					let camera = cameras[ i ];

					if ( camera === undefined ) {

						camera = new PerspectiveCamera();
						camera.layers.enable( i );
						camera.viewport = new Vector4();
						cameras[ i ] = camera;

					}

					camera.matrix.fromArray( view.transform.matrix );
					camera.matrix.decompose( camera.position, camera.quaternion, camera.scale );
					camera.projectionMatrix.fromArray( view.projectionMatrix );
					camera.projectionMatrixInverse.copy( camera.projectionMatrix ).invert();
					camera.viewport.set( viewport.x, viewport.y, viewport.width, viewport.height );

					if ( i === 0 ) {

						cameraXR.matrix.copy( camera.matrix );
						cameraXR.matrix.decompose( cameraXR.position, cameraXR.quaternion, cameraXR.scale );

					}

					if ( cameraXRNeedsUpdate === true ) {

						cameraXR.cameras.push( camera );

					}

				}

				//

				const enabledFeatures = session.enabledFeatures;
				const gpuDepthSensingEnabled = enabledFeatures &&
					enabledFeatures.includes( 'depth-sensing' ) &&
					session.depthUsage == 'gpu-optimized';

				if ( gpuDepthSensingEnabled && supportsGlBinding ) {

					glBinding = scope.getBinding();

					const depthData = glBinding.getDepthInformation( views[ 0 ] );

					if ( depthData && depthData.isValid && depthData.texture ) {

						depthSensing.init( depthData, session.renderState );

					}

				}

				const cameraAccessEnabled = enabledFeatures &&
				    enabledFeatures.includes( 'camera-access' );

				if ( cameraAccessEnabled && supportsGlBinding ) {

					renderer.state.unbindTexture();

					glBinding = scope.getBinding();

					for ( let i = 0; i < views.length; i ++ ) {

						const camera = views[ i ].camera;

						if ( camera ) {

							let cameraTex = cameraAccessTextures[ camera ];

							if ( ! cameraTex ) {

								cameraTex = new ExternalTexture();
								cameraAccessTextures[ camera ] = cameraTex;

							}

							const glTexture = glBinding.getCameraImage( camera );
							cameraTex.sourceTexture = glTexture;

						}

					}

				}

			}

			//

			for ( let i = 0; i < controllers.length; i ++ ) {

				const inputSource = controllerInputSources[ i ];
				const controller = controllers[ i ];

				if ( inputSource !== null && controller !== undefined ) {

					controller.update( inputSource, frame, customReferenceSpace || referenceSpace );

				}

			}

			if ( onAnimationFrameCallback ) onAnimationFrameCallback( time, frame );

			if ( frame.detectedPlanes ) {

				scope.dispatchEvent( { type: 'planesdetected', data: frame } );

			}

			xrFrame = null;

		}

		const animation = new WebGLAnimation();

		animation.setAnimationLoop( onAnimationFrame );

		this.setAnimationLoop = function ( callback ) {

			onAnimationFrameCallback = callback;

		};

		this.dispose = function () {};

	}

}

const _e1 = /*@__PURE__*/ new Euler();
const _m1 = /*@__PURE__*/ new Matrix4();

function WebGLMaterials( renderer, properties ) {

	function refreshTransformUniform( map, uniform ) {

		if ( map.matrixAutoUpdate === true ) {

			map.updateMatrix();

		}

		uniform.value.copy( map.matrix );

	}

	function refreshFogUniforms( uniforms, fog ) {

		fog.color.getRGB( uniforms.fogColor.value, getUnlitUniformColorSpace( renderer ) );

		if ( fog.isFog ) {

			uniforms.fogNear.value = fog.near;
			uniforms.fogFar.value = fog.far;

		} else if ( fog.isFogExp2 ) {

			uniforms.fogDensity.value = fog.density;

		}

	}

	function refreshMaterialUniforms( uniforms, material, pixelRatio, height, transmissionRenderTarget ) {

		if ( material.isMeshBasicMaterial ) {

			refreshUniformsCommon( uniforms, material );

		} else if ( material.isMeshLambertMaterial ) {

			refreshUniformsCommon( uniforms, material );

		} else if ( material.isMeshToonMaterial ) {

			refreshUniformsCommon( uniforms, material );
			refreshUniformsToon( uniforms, material );

		} else if ( material.isMeshPhongMaterial ) {

			refreshUniformsCommon( uniforms, material );
			refreshUniformsPhong( uniforms, material );

		} else if ( material.isMeshStandardMaterial ) {

			refreshUniformsCommon( uniforms, material );
			refreshUniformsStandard( uniforms, material );

			if ( material.isMeshPhysicalMaterial ) {

				refreshUniformsPhysical( uniforms, material, transmissionRenderTarget );

			}

		} else if ( material.isMeshMatcapMaterial ) {

			refreshUniformsCommon( uniforms, material );
			refreshUniformsMatcap( uniforms, material );

		} else if ( material.isMeshDepthMaterial ) {

			refreshUniformsCommon( uniforms, material );

		} else if ( material.isMeshDistanceMaterial ) {

			refreshUniformsCommon( uniforms, material );
			refreshUniformsDistance( uniforms, material );

		} else if ( material.isMeshNormalMaterial ) {

			refreshUniformsCommon( uniforms, material );

		} else if ( material.isLineBasicMaterial ) {

			refreshUniformsLine( uniforms, material );

			if ( material.isLineDashedMaterial ) {

				refreshUniformsDash( uniforms, material );

			}

		} else if ( material.isPointsMaterial ) {

			refreshUniformsPoints( uniforms, material, pixelRatio, height );

		} else if ( material.isSpriteMaterial ) {

			refreshUniformsSprites( uniforms, material );

		} else if ( material.isShadowMaterial ) {

			uniforms.color.value.copy( material.color );
			uniforms.opacity.value = material.opacity;

		} else if ( material.isShaderMaterial ) {

			material.uniformsNeedUpdate = false; // #15581

		}

	}

	function refreshUniformsCommon( uniforms, material ) {

		uniforms.opacity.value = material.opacity;

		if ( material.color ) {

			uniforms.diffuse.value.copy( material.color );

		}

		if ( material.emissive ) {

			uniforms.emissive.value.copy( material.emissive ).multiplyScalar( material.emissiveIntensity );

		}

		if ( material.map ) {

			uniforms.map.value = material.map;

			refreshTransformUniform( material.map, uniforms.mapTransform );

		}

		if ( material.alphaMap ) {

			uniforms.alphaMap.value = material.alphaMap;

			refreshTransformUniform( material.alphaMap, uniforms.alphaMapTransform );

		}

		if ( material.bumpMap ) {

			uniforms.bumpMap.value = material.bumpMap;

			refreshTransformUniform( material.bumpMap, uniforms.bumpMapTransform );

			uniforms.bumpScale.value = material.bumpScale;

			if ( material.side === BackSide ) {

				uniforms.bumpScale.value *= -1;

			}

		}

		if ( material.normalMap ) {

			uniforms.normalMap.value = material.normalMap;

			refreshTransformUniform( material.normalMap, uniforms.normalMapTransform );

			uniforms.normalScale.value.copy( material.normalScale );

			if ( material.side === BackSide ) {

				uniforms.normalScale.value.negate();

			}

		}

		if ( material.displacementMap ) {

			uniforms.displacementMap.value = material.displacementMap;

			refreshTransformUniform( material.displacementMap, uniforms.displacementMapTransform );

			uniforms.displacementScale.value = material.displacementScale;
			uniforms.displacementBias.value = material.displacementBias;

		}

		if ( material.emissiveMap ) {

			uniforms.emissiveMap.value = material.emissiveMap;

			refreshTransformUniform( material.emissiveMap, uniforms.emissiveMapTransform );

		}

		if ( material.specularMap ) {

			uniforms.specularMap.value = material.specularMap;

			refreshTransformUniform( material.specularMap, uniforms.specularMapTransform );

		}

		if ( material.alphaTest > 0 ) {

			uniforms.alphaTest.value = material.alphaTest;

		}

		const materialProperties = properties.get( material );

		const envMap = materialProperties.envMap;
		const envMapRotation = materialProperties.envMapRotation;

		if ( envMap ) {

			uniforms.envMap.value = envMap;

			_e1.copy( envMapRotation );

			// accommodate left-handed frame
			_e1.x *= -1; _e1.y *= -1; _e1.z *= -1;

			if ( envMap.isCubeTexture && envMap.isRenderTargetTexture === false ) {

				// environment maps which are not cube render targets or PMREMs follow a different convention
				_e1.y *= -1;
				_e1.z *= -1;

			}

			uniforms.envMapRotation.value.setFromMatrix4( _m1.makeRotationFromEuler( _e1 ) );

			uniforms.flipEnvMap.value = ( envMap.isCubeTexture && envMap.isRenderTargetTexture === false ) ? -1 : 1;

			uniforms.reflectivity.value = material.reflectivity;
			uniforms.ior.value = material.ior;
			uniforms.refractionRatio.value = material.refractionRatio;

		}

		if ( material.lightMap ) {

			uniforms.lightMap.value = material.lightMap;
			uniforms.lightMapIntensity.value = material.lightMapIntensity;

			refreshTransformUniform( material.lightMap, uniforms.lightMapTransform );

		}

		if ( material.aoMap ) {

			uniforms.aoMap.value = material.aoMap;
			uniforms.aoMapIntensity.value = material.aoMapIntensity;

			refreshTransformUniform( material.aoMap, uniforms.aoMapTransform );

		}

	}

	function refreshUniformsLine( uniforms, material ) {

		uniforms.diffuse.value.copy( material.color );
		uniforms.opacity.value = material.opacity;

		if ( material.map ) {

			uniforms.map.value = material.map;

			refreshTransformUniform( material.map, uniforms.mapTransform );

		}

	}

	function refreshUniformsDash( uniforms, material ) {

		uniforms.dashSize.value = material.dashSize;
		uniforms.totalSize.value = material.dashSize + material.gapSize;
		uniforms.scale.value = material.scale;

	}

	function refreshUniformsPoints( uniforms, material, pixelRatio, height ) {

		uniforms.diffuse.value.copy( material.color );
		uniforms.opacity.value = material.opacity;
		uniforms.size.value = material.size * pixelRatio;
		uniforms.scale.value = height * 0.5;

		if ( material.map ) {

			uniforms.map.value = material.map;

			refreshTransformUniform( material.map, uniforms.uvTransform );

		}

		if ( material.alphaMap ) {

			uniforms.alphaMap.value = material.alphaMap;

			refreshTransformUniform( material.alphaMap, uniforms.alphaMapTransform );

		}

		if ( material.alphaTest > 0 ) {

			uniforms.alphaTest.value = material.alphaTest;

		}

	}

	function refreshUniformsSprites( uniforms, material ) {

		uniforms.diffuse.value.copy( material.color );
		uniforms.opacity.value = material.opacity;
		uniforms.rotation.value = material.rotation;

		if ( material.map ) {

			uniforms.map.value = material.map;

			refreshTransformUniform( material.map, uniforms.mapTransform );

		}

		if ( material.alphaMap ) {

			uniforms.alphaMap.value = material.alphaMap;

			refreshTransformUniform( material.alphaMap, uniforms.alphaMapTransform );

		}

		if ( material.alphaTest > 0 ) {

			uniforms.alphaTest.value = material.alphaTest;

		}

	}

	function refreshUniformsPhong( uniforms, material ) {

		uniforms.specular.value.copy( material.specular );
		uniforms.shininess.value = Math.max( material.shininess, 1e-4 ); // to prevent pow( 0.0, 0.0 )

	}

	function refreshUniformsToon( uniforms, material ) {

		if ( material.gradientMap ) {

			uniforms.gradientMap.value = material.gradientMap;

		}

	}

	function refreshUniformsStandard( uniforms, material ) {

		uniforms.metalness.value = material.metalness;

		if ( material.metalnessMap ) {

			uniforms.metalnessMap.value = material.metalnessMap;

			refreshTransformUniform( material.metalnessMap, uniforms.metalnessMapTransform );

		}

		uniforms.roughness.value = material.roughness;

		if ( material.roughnessMap ) {

			uniforms.roughnessMap.value = material.roughnessMap;

			refreshTransformUniform( material.roughnessMap, uniforms.roughnessMapTransform );

		}

		if ( material.envMap ) {

			//uniforms.envMap.value = material.envMap; // part of uniforms common

			uniforms.envMapIntensity.value = material.envMapIntensity;

		}

	}

	function refreshUniformsPhysical( uniforms, material, transmissionRenderTarget ) {

		uniforms.ior.value = material.ior; // also part of uniforms common

		if ( material.sheen > 0 ) {

			uniforms.sheenColor.value.copy( material.sheenColor ).multiplyScalar( material.sheen );

			uniforms.sheenRoughness.value = material.sheenRoughness;

			if ( material.sheenColorMap ) {

				uniforms.sheenColorMap.value = material.sheenColorMap;

				refreshTransformUniform( material.sheenColorMap, uniforms.sheenColorMapTransform );

			}

			if ( material.sheenRoughnessMap ) {

				uniforms.sheenRoughnessMap.value = material.sheenRoughnessMap;

				refreshTransformUniform( material.sheenRoughnessMap, uniforms.sheenRoughnessMapTransform );

			}

		}

		if ( material.clearcoat > 0 ) {

			uniforms.clearcoat.value = material.clearcoat;
			uniforms.clearcoatRoughness.value = material.clearcoatRoughness;

			if ( material.clearcoatMap ) {

				uniforms.clearcoatMap.value = material.clearcoatMap;

				refreshTransformUniform( material.clearcoatMap, uniforms.clearcoatMapTransform );

			}

			if ( material.clearcoatRoughnessMap ) {

				uniforms.clearcoatRoughnessMap.value = material.clearcoatRoughnessMap;

				refreshTransformUniform( material.clearcoatRoughnessMap, uniforms.clearcoatRoughnessMapTransform );

			}

			if ( material.clearcoatNormalMap ) {

				uniforms.clearcoatNormalMap.value = material.clearcoatNormalMap;

				refreshTransformUniform( material.clearcoatNormalMap, uniforms.clearcoatNormalMapTransform );

				uniforms.clearcoatNormalScale.value.copy( material.clearcoatNormalScale );

				if ( material.side === BackSide ) {

					uniforms.clearcoatNormalScale.value.negate();

				}

			}

		}

		if ( material.dispersion > 0 ) {

			uniforms.dispersion.value = material.dispersion;

		}

		if ( material.iridescence > 0 ) {

			uniforms.iridescence.value = material.iridescence;
			uniforms.iridescenceIOR.value = material.iridescenceIOR;
			uniforms.iridescenceThicknessMinimum.value = material.iridescenceThicknessRange[ 0 ];
			uniforms.iridescenceThicknessMaximum.value = material.iridescenceThicknessRange[ 1 ];

			if ( material.iridescenceMap ) {

				uniforms.iridescenceMap.value = material.iridescenceMap;

				refreshTransformUniform( material.iridescenceMap, uniforms.iridescenceMapTransform );

			}

			if ( material.iridescenceThicknessMap ) {

				uniforms.iridescenceThicknessMap.value = material.iridescenceThicknessMap;

				refreshTransformUniform( material.iridescenceThicknessMap, uniforms.iridescenceThicknessMapTransform );

			}

		}

		if ( material.transmission > 0 ) {

			uniforms.transmission.value = material.transmission;
			uniforms.transmissionSamplerMap.value = transmissionRenderTarget.texture;
			uniforms.transmissionSamplerSize.value.set( transmissionRenderTarget.width, transmissionRenderTarget.height );

			if ( material.transmissionMap ) {

				uniforms.transmissionMap.value = material.transmissionMap;

				refreshTransformUniform( material.transmissionMap, uniforms.transmissionMapTransform );

			}

			uniforms.thickness.value = material.thickness;

			if ( material.thicknessMap ) {

				uniforms.thicknessMap.value = material.thicknessMap;

				refreshTransformUniform( material.thicknessMap, uniforms.thicknessMapTransform );

			}

			uniforms.attenuationDistance.value = material.attenuationDistance;
			uniforms.attenuationColor.value.copy( material.attenuationColor );

		}

		if ( material.anisotropy > 0 ) {

			uniforms.anisotropyVector.value.set( material.anisotropy * Math.cos( material.anisotropyRotation ), material.anisotropy * Math.sin( material.anisotropyRotation ) );

			if ( material.anisotropyMap ) {

				uniforms.anisotropyMap.value = material.anisotropyMap;

				refreshTransformUniform( material.anisotropyMap, uniforms.anisotropyMapTransform );

			}

		}

		uniforms.specularIntensity.value = material.specularIntensity;
		uniforms.specularColor.value.copy( material.specularColor );

		if ( material.specularColorMap ) {

			uniforms.specularColorMap.value = material.specularColorMap;

			refreshTransformUniform( material.specularColorMap, uniforms.specularColorMapTransform );

		}

		if ( material.specularIntensityMap ) {

			uniforms.specularIntensityMap.value = material.specularIntensityMap;

			refreshTransformUniform( material.specularIntensityMap, uniforms.specularIntensityMapTransform );

		}

	}

	function refreshUniformsMatcap( uniforms, material ) {

		if ( material.matcap ) {

			uniforms.matcap.value = material.matcap;

		}

	}

	function refreshUniformsDistance( uniforms, material ) {

		const light = properties.get( material ).light;

		uniforms.referencePosition.value.setFromMatrixPosition( light.matrixWorld );
		uniforms.nearDistance.value = light.shadow.camera.near;
		uniforms.farDistance.value = light.shadow.camera.far;

	}

	return {
		refreshFogUniforms: refreshFogUniforms,
		refreshMaterialUniforms: refreshMaterialUniforms
	};

}

function WebGLUniformsGroups( gl, info, capabilities, state ) {

	let buffers = {};
	let updateList = {};
	let allocatedBindingPoints = [];

	const maxBindingPoints = gl.getParameter( gl.MAX_UNIFORM_BUFFER_BINDINGS ); // binding points are global whereas block indices are per shader program

	function bind( uniformsGroup, program ) {

		const webglProgram = program.program;
		state.uniformBlockBinding( uniformsGroup, webglProgram );

	}

	function update( uniformsGroup, program ) {

		let buffer = buffers[ uniformsGroup.id ];

		if ( buffer === undefined ) {

			prepareUniformsGroup( uniformsGroup );

			buffer = createBuffer( uniformsGroup );
			buffers[ uniformsGroup.id ] = buffer;

			uniformsGroup.addEventListener( 'dispose', onUniformsGroupsDispose );

		}

		// ensure to update the binding points/block indices mapping for this program

		const webglProgram = program.program;
		state.updateUBOMapping( uniformsGroup, webglProgram );

		// update UBO once per frame

		const frame = info.render.frame;

		if ( updateList[ uniformsGroup.id ] !== frame ) {

			updateBufferData( uniformsGroup );

			updateList[ uniformsGroup.id ] = frame;

		}

	}

	function createBuffer( uniformsGroup ) {

		// the setup of an UBO is independent of a particular shader program but global

		const bindingPointIndex = allocateBindingPointIndex();
		uniformsGroup.__bindingPointIndex = bindingPointIndex;

		const buffer = gl.createBuffer();
		const size = uniformsGroup.__size;
		const usage = uniformsGroup.usage;

		gl.bindBuffer( gl.UNIFORM_BUFFER, buffer );
		gl.bufferData( gl.UNIFORM_BUFFER, size, usage );
		gl.bindBuffer( gl.UNIFORM_BUFFER, null );
		gl.bindBufferBase( gl.UNIFORM_BUFFER, bindingPointIndex, buffer );

		return buffer;

	}

	function allocateBindingPointIndex() {

		for ( let i = 0; i < maxBindingPoints; i ++ ) {

			if ( allocatedBindingPoints.indexOf( i ) === -1 ) {

				allocatedBindingPoints.push( i );
				return i;

			}

		}

		console.error( 'THREE.WebGLRenderer: Maximum number of simultaneously usable uniforms groups reached.' );

		return 0;

	}

	function updateBufferData( uniformsGroup ) {

		const buffer = buffers[ uniformsGroup.id ];
		const uniforms = uniformsGroup.uniforms;
		const cache = uniformsGroup.__cache;

		gl.bindBuffer( gl.UNIFORM_BUFFER, buffer );

		for ( let i = 0, il = uniforms.length; i < il; i ++ ) {

			const uniformArray = Array.isArray( uniforms[ i ] ) ? uniforms[ i ] : [ uniforms[ i ] ];

			for ( let j = 0, jl = uniformArray.length; j < jl; j ++ ) {

				const uniform = uniformArray[ j ];

				if ( hasUniformChanged( uniform, i, j, cache ) === true ) {

					const offset = uniform.__offset;

					const values = Array.isArray( uniform.value ) ? uniform.value : [ uniform.value ];

					let arrayOffset = 0;

					for ( let k = 0; k < values.length; k ++ ) {

						const value = values[ k ];

						const info = getUniformSize( value );

						// TODO add integer and struct support
						if ( typeof value === 'number' || typeof value === 'boolean' ) {

							uniform.__data[ 0 ] = value;
							gl.bufferSubData( gl.UNIFORM_BUFFER, offset + arrayOffset, uniform.__data );

						} else if ( value.isMatrix3 ) {

							// manually converting 3x3 to 3x4

							uniform.__data[ 0 ] = value.elements[ 0 ];
							uniform.__data[ 1 ] = value.elements[ 1 ];
							uniform.__data[ 2 ] = value.elements[ 2 ];
							uniform.__data[ 3 ] = 0;
							uniform.__data[ 4 ] = value.elements[ 3 ];
							uniform.__data[ 5 ] = value.elements[ 4 ];
							uniform.__data[ 6 ] = value.elements[ 5 ];
							uniform.__data[ 7 ] = 0;
							uniform.__data[ 8 ] = value.elements[ 6 ];
							uniform.__data[ 9 ] = value.elements[ 7 ];
							uniform.__data[ 10 ] = value.elements[ 8 ];
							uniform.__data[ 11 ] = 0;

						} else {

							value.toArray( uniform.__data, arrayOffset );

							arrayOffset += info.storage / Float32Array.BYTES_PER_ELEMENT;

						}

					}

					gl.bufferSubData( gl.UNIFORM_BUFFER, offset, uniform.__data );

				}

			}

		}

		gl.bindBuffer( gl.UNIFORM_BUFFER, null );

	}

	function hasUniformChanged( uniform, index, indexArray, cache ) {

		const value = uniform.value;
		const indexString = index + '_' + indexArray;

		if ( cache[ indexString ] === undefined ) {

			// cache entry does not exist so far

			if ( typeof value === 'number' || typeof value === 'boolean' ) {

				cache[ indexString ] = value;

			} else {

				cache[ indexString ] = value.clone();

			}

			return true;

		} else {

			const cachedObject = cache[ indexString ];

			// compare current value with cached entry

			if ( typeof value === 'number' || typeof value === 'boolean' ) {

				if ( cachedObject !== value ) {

					cache[ indexString ] = value;
					return true;

				}

			} else {

				if ( cachedObject.equals( value ) === false ) {

					cachedObject.copy( value );
					return true;

				}

			}

		}

		return false;

	}

	function prepareUniformsGroup( uniformsGroup ) {

		// determine total buffer size according to the STD140 layout
		// Hint: STD140 is the only supported layout in WebGL 2

		const uniforms = uniformsGroup.uniforms;

		let offset = 0; // global buffer offset in bytes
		const chunkSize = 16; // size of a chunk in bytes

		for ( let i = 0, l = uniforms.length; i < l; i ++ ) {

			const uniformArray = Array.isArray( uniforms[ i ] ) ? uniforms[ i ] : [ uniforms[ i ] ];

			for ( let j = 0, jl = uniformArray.length; j < jl; j ++ ) {

				const uniform = uniformArray[ j ];

				const values = Array.isArray( uniform.value ) ? uniform.value : [ uniform.value ];

				for ( let k = 0, kl = values.length; k < kl; k ++ ) {

					const value = values[ k ];

					const info = getUniformSize( value );

					const chunkOffset = offset % chunkSize; // offset in the current chunk
					const chunkPadding = chunkOffset % info.boundary; // required padding to match boundary
					const chunkStart = chunkOffset + chunkPadding; // the start position in the current chunk for the data

					offset += chunkPadding;

					// Check for chunk overflow
					if ( chunkStart !== 0 && ( chunkSize - chunkStart ) < info.storage ) {

						// Add padding and adjust offset
						offset += ( chunkSize - chunkStart );

					}

					// the following two properties will be used for partial buffer updates
					uniform.__data = new Float32Array( info.storage / Float32Array.BYTES_PER_ELEMENT );
					uniform.__offset = offset;

					// Update the global offset
					offset += info.storage;

				}

			}

		}

		// ensure correct final padding

		const chunkOffset = offset % chunkSize;

		if ( chunkOffset > 0 ) offset += ( chunkSize - chunkOffset );

		//

		uniformsGroup.__size = offset;
		uniformsGroup.__cache = {};

		return this;

	}

	function getUniformSize( value ) {

		const info = {
			boundary: 0, // bytes
			storage: 0 // bytes
		};

		// determine sizes according to STD140

		if ( typeof value === 'number' || typeof value === 'boolean' ) {

			// float/int/bool

			info.boundary = 4;
			info.storage = 4;

		} else if ( value.isVector2 ) {

			// vec2

			info.boundary = 8;
			info.storage = 8;

		} else if ( value.isVector3 || value.isColor ) {

			// vec3

			info.boundary = 16;
			info.storage = 12; // evil: vec3 must start on a 16-byte boundary but it only consumes 12 bytes

		} else if ( value.isVector4 ) {

			// vec4

			info.boundary = 16;
			info.storage = 16;

		} else if ( value.isMatrix3 ) {

			// mat3 (in STD140 a 3x3 matrix is represented as 3x4)

			info.boundary = 48;
			info.storage = 48;

		} else if ( value.isMatrix4 ) {

			// mat4

			info.boundary = 64;
			info.storage = 64;

		} else if ( value.isTexture ) {

			console.warn( 'THREE.WebGLRenderer: Texture samplers can not be part of an uniforms group.' );

		} else {

			console.warn( 'THREE.WebGLRenderer: Unsupported uniform value type.', value );

		}

		return info;

	}

	function onUniformsGroupsDispose( event ) {

		const uniformsGroup = event.target;

		uniformsGroup.removeEventListener( 'dispose', onUniformsGroupsDispose );

		const index = allocatedBindingPoints.indexOf( uniformsGroup.__bindingPointIndex );
		allocatedBindingPoints.splice( index, 1 );

		gl.deleteBuffer( buffers[ uniformsGroup.id ] );

		delete buffers[ uniformsGroup.id ];
		delete updateList[ uniformsGroup.id ];

	}

	function dispose() {

		for ( const id in buffers ) {

			gl.deleteBuffer( buffers[ id ] );

		}

		allocatedBindingPoints = [];
		buffers = {};
		updateList = {};

	}

	return {

		bind: bind,
		update: update,

		dispose: dispose

	};

}

/**
 * This renderer uses WebGL 2 to display scenes.
 *
 * WebGL 1 is not supported since `r163`.
 */
class WebGLRenderer {

	/**
	 * Constructs a new WebGL renderer.
	 *
	 * @param {WebGLRenderer~Options} [parameters] - The configuration parameter.
	 */
	constructor( parameters = {} ) {

		const {
			canvas = createCanvasElement(),
			context = null,
			depth = true,
			stencil = false,
			alpha = false,
			antialias = false,
			premultipliedAlpha = true,
			preserveDrawingBuffer = false,
			powerPreference = 'default',
			failIfMajorPerformanceCaveat = false,
			reversedDepthBuffer = false,
		} = parameters;

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isWebGLRenderer = true;

		let _alpha;

		if ( context !== null ) {

			if ( typeof WebGLRenderingContext !== 'undefined' && context instanceof WebGLRenderingContext ) {

				throw new Error( 'THREE.WebGLRenderer: WebGL 1 is not supported since r163.' );

			}

			_alpha = context.getContextAttributes().alpha;

		} else {

			_alpha = alpha;

		}

		const uintClearColor = new Uint32Array( 4 );
		const intClearColor = new Int32Array( 4 );

		let currentRenderList = null;
		let currentRenderState = null;

		// render() can be called from within a callback triggered by another render.
		// We track this so that the nested render call gets its list and state isolated from the parent render call.

		const renderListStack = [];
		const renderStateStack = [];

		// public properties

		/**
		 * A canvas where the renderer draws its output.This is automatically created by the renderer
		 * in the constructor (if not provided already); you just need to add it to your page like so:
		 * ```js
		 * document.body.appendChild( renderer.domElement );
		 * ```
		 *
		 * @type {DOMElement}
		 */
		this.domElement = canvas;

		/**
		 * A object with debug configuration settings.
		 *
		 * - `checkShaderErrors`: If it is `true`, defines whether material shader programs are
		 * checked for errors during compilation and linkage process. It may be useful to disable
		 * this check in production for performance gain. It is strongly recommended to keep these
		 * checks enabled during development. If the shader does not compile and link - it will not
		 * work and associated material will not render.
		 * - `onShaderError(gl, program, glVertexShader,glFragmentShader)`: A callback function that
		 * can be used for custom error reporting. The callback receives the WebGL context, an instance
		 * of WebGLProgram as well two instances of WebGLShader representing the vertex and fragment shader.
		 * Assigning a custom function disables the default error reporting.
		 *
		 * @type {Object}
		 */
		this.debug = {

			/**
			 * Enables error checking and reporting when shader programs are being compiled.
			 * @type {boolean}
			 */
			checkShaderErrors: true,
			/**
			 * Callback for custom error reporting.
			 * @type {?Function}
			 */
			onShaderError: null
		};

		// clearing

		/**
		 * Whether the renderer should automatically clear its output before rendering a frame or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.autoClear = true;

		/**
		 * If {@link WebGLRenderer#autoClear} set to `true`, whether the renderer should clear
		 * the color buffer or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.autoClearColor = true;

		/**
		 * If {@link WebGLRenderer#autoClear} set to `true`, whether the renderer should clear
		 * the depth buffer or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.autoClearDepth = true;

		/**
		 * If {@link WebGLRenderer#autoClear} set to `true`, whether the renderer should clear
		 * the stencil buffer or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.autoClearStencil = true;

		// scene graph

		/**
		 * Whether the renderer should sort objects or not.
		 *
		 * Note: Sorting is used to attempt to properly render objects that have some
		 * degree of transparency. By definition, sorting objects may not work in all
		 * cases. Depending on the needs of application, it may be necessary to turn
		 * off sorting and use other methods to deal with transparency rendering e.g.
		 * manually determining each object's rendering order.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.sortObjects = true;

		// user-defined clipping

		/**
		 * User-defined clipping planes specified in world space. These planes apply globally.
		 * Points in space whose dot product with the plane is negative are cut away.
		 *
		 * @type {Array<Plane>}
		 */
		this.clippingPlanes = [];

		/**
		 * Whether the renderer respects object-level clipping planes or not.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.localClippingEnabled = false;

		// tone mapping

		/**
		 * The tone mapping technique of the renderer.
		 *
		 * @type {(NoToneMapping|LinearToneMapping|ReinhardToneMapping|CineonToneMapping|ACESFilmicToneMapping|CustomToneMapping|AgXToneMapping|NeutralToneMapping)}
		 * @default NoToneMapping
		 */
		this.toneMapping = NoToneMapping;

		/**
		 * Exposure level of tone mapping.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.toneMappingExposure = 1.0;

		// transmission

		/**
		 * The normalized resolution scale for the transmission render target, measured in percentage
		 * of viewport dimensions. Lowering this value can result in significant performance improvements
		 * when using {@link MeshPhysicalMaterial#transmission}.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.transmissionResolutionScale = 1.0;

		// internal properties

		const _this = this;

		let _isContextLost = false;

		// internal state cache

		this._outputColorSpace = SRGBColorSpace;

		let _currentActiveCubeFace = 0;
		let _currentActiveMipmapLevel = 0;
		let _currentRenderTarget = null;
		let _currentMaterialId = -1;

		let _currentCamera = null;

		const _currentViewport = new Vector4();
		const _currentScissor = new Vector4();
		let _currentScissorTest = null;

		const _currentClearColor = new Color( 0x000000 );
		let _currentClearAlpha = 0;

		//

		let _width = canvas.width;
		let _height = canvas.height;

		let _pixelRatio = 1;
		let _opaqueSort = null;
		let _transparentSort = null;

		const _viewport = new Vector4( 0, 0, _width, _height );
		const _scissor = new Vector4( 0, 0, _width, _height );
		let _scissorTest = false;

		// frustum

		const _frustum = new Frustum();

		// clipping

		let _clippingEnabled = false;
		let _localClippingEnabled = false;

		// camera matrices cache

		const _projScreenMatrix = new Matrix4();

		const _vector3 = new Vector3();

		const _vector4 = new Vector4();

		const _emptyScene = { background: null, fog: null, environment: null, overrideMaterial: null, isScene: true };

		let _renderBackground = false;

		function getTargetPixelRatio() {

			return _currentRenderTarget === null ? _pixelRatio : 1;

		}

		// initialize

		let _gl = context;

		function getContext( contextName, contextAttributes ) {

			return canvas.getContext( contextName, contextAttributes );

		}

		try {

			const contextAttributes = {
				alpha: true,
				depth,
				stencil,
				antialias,
				premultipliedAlpha,
				preserveDrawingBuffer,
				powerPreference,
				failIfMajorPerformanceCaveat,
			};

			// OffscreenCanvas does not have setAttribute, see #22811
			if ( 'setAttribute' in canvas ) canvas.setAttribute( 'data-engine', `three.js r${REVISION}` );

			// event listeners must be registered before WebGL context is created, see #12753
			canvas.addEventListener( 'webglcontextlost', onContextLost, false );
			canvas.addEventListener( 'webglcontextrestored', onContextRestore, false );
			canvas.addEventListener( 'webglcontextcreationerror', onContextCreationError, false );

			if ( _gl === null ) {

				const contextName = 'webgl2';

				_gl = getContext( contextName, contextAttributes );

				if ( _gl === null ) {

					if ( getContext( contextName ) ) {

						throw new Error( 'Error creating WebGL context with your selected attributes.' );

					} else {

						throw new Error( 'Error creating WebGL context.' );

					}

				}

			}

		} catch ( error ) {

			console.error( 'THREE.WebGLRenderer: ' + error.message );
			throw error;

		}

		let extensions, capabilities, state, info;
		let properties, textures, cubemaps, cubeuvmaps, attributes, geometries, objects;
		let programCache, materials, renderLists, renderStates, clipping, shadowMap;

		let background, morphtargets, bufferRenderer, indexedBufferRenderer;

		let utils, bindingStates, uniformsGroups;

		function initGLContext() {

			extensions = new WebGLExtensions( _gl );
			extensions.init();

			utils = new WebGLUtils( _gl, extensions );

			capabilities = new WebGLCapabilities( _gl, extensions, parameters, utils );

			state = new WebGLState( _gl, extensions );

			if ( capabilities.reversedDepthBuffer && reversedDepthBuffer ) {

				state.buffers.depth.setReversed( true );

			}

			info = new WebGLInfo( _gl );
			properties = new WebGLProperties();
			textures = new WebGLTextures( _gl, extensions, state, properties, capabilities, utils, info );
			cubemaps = new WebGLCubeMaps( _this );
			cubeuvmaps = new WebGLCubeUVMaps( _this );
			attributes = new WebGLAttributes( _gl );
			bindingStates = new WebGLBindingStates( _gl, attributes );
			geometries = new WebGLGeometries( _gl, attributes, info, bindingStates );
			objects = new WebGLObjects( _gl, geometries, attributes, info );
			morphtargets = new WebGLMorphtargets( _gl, capabilities, textures );
			clipping = new WebGLClipping( properties );
			programCache = new WebGLPrograms( _this, cubemaps, cubeuvmaps, extensions, capabilities, bindingStates, clipping );
			materials = new WebGLMaterials( _this, properties );
			renderLists = new WebGLRenderLists();
			renderStates = new WebGLRenderStates( extensions );
			background = new WebGLBackground( _this, cubemaps, cubeuvmaps, state, objects, _alpha, premultipliedAlpha );
			shadowMap = new WebGLShadowMap( _this, objects, capabilities );
			uniformsGroups = new WebGLUniformsGroups( _gl, info, capabilities, state );

			bufferRenderer = new WebGLBufferRenderer( _gl, extensions, info );
			indexedBufferRenderer = new WebGLIndexedBufferRenderer( _gl, extensions, info );

			info.programs = programCache.programs;

			/**
			 * Holds details about the capabilities of the current rendering context.
			 *
			 * @name WebGLRenderer#capabilities
			 * @type {WebGLRenderer~Capabilities}
			 */
			_this.capabilities = capabilities;

			/**
			 * Provides methods for retrieving and testing WebGL extensions.
			 *
			 * - `get(extensionName:string)`: Used to check whether a WebGL extension is supported
			 * and return the extension object if available.
			 * - `has(extensionName:string)`: returns `true` if the extension is supported.
			 *
			 * @name WebGLRenderer#extensions
			 * @type {Object}
			 */
			_this.extensions = extensions;

			/**
			 * Used to track properties of other objects like native WebGL objects.
			 *
			 * @name WebGLRenderer#properties
			 * @type {Object}
			 */
			_this.properties = properties;

			/**
			 * Manages the render lists of the renderer.
			 *
			 * @name WebGLRenderer#renderLists
			 * @type {Object}
			 */
			_this.renderLists = renderLists;



			/**
			 * Interface for managing shadows.
			 *
			 * @name WebGLRenderer#shadowMap
			 * @type {WebGLRenderer~ShadowMap}
			 */
			_this.shadowMap = shadowMap;

			/**
			 * Interface for managing the WebGL state.
			 *
			 * @name WebGLRenderer#state
			 * @type {Object}
			 */
			_this.state = state;

			/**
			 * Holds a series of statistical information about the GPU memory
			 * and the rendering process. Useful for debugging and monitoring.
			 *
			 * By default these data are reset at each render call but when having
			 * multiple render passes per frame (e.g. when using post processing) it can
			 * be preferred to reset with a custom pattern. First, set `autoReset` to
			 * `false`.
			 * ```js
			 * renderer.info.autoReset = false;
			 * ```
			 * Call `reset()` whenever you have finished to render a single frame.
			 * ```js
			 * renderer.info.reset();
			 * ```
			 *
			 * @name WebGLRenderer#info
			 * @type {WebGLRenderer~Info}
			 */
			_this.info = info;

		}

		initGLContext();

		// xr

		const xr = new WebXRManager( _this, _gl );

		/**
		 * A reference to the XR manager.
		 *
		 * @type {WebXRManager}
		 */
		this.xr = xr;

		/**
		 * Returns the rendering context.
		 *
		 * @return {WebGL2RenderingContext} The rendering context.
		 */
		this.getContext = function () {

			return _gl;

		};

		/**
		 * Returns the rendering context attributes.
		 *
		 * @return {WebGLContextAttributes} The rendering context attributes.
		 */
		this.getContextAttributes = function () {

			return _gl.getContextAttributes();

		};

		/**
		 * Simulates a loss of the WebGL context. This requires support for the `WEBGL_lose_context` extension.
		 */
		this.forceContextLoss = function () {

			const extension = extensions.get( 'WEBGL_lose_context' );
			if ( extension ) extension.loseContext();

		};

		/**
		 * Simulates a restore of the WebGL context. This requires support for the `WEBGL_lose_context` extension.
		 */
		this.forceContextRestore = function () {

			const extension = extensions.get( 'WEBGL_lose_context' );
			if ( extension ) extension.restoreContext();

		};

		/**
		 * Returns the pixel ratio.
		 *
		 * @return {number} The pixel ratio.
		 */
		this.getPixelRatio = function () {

			return _pixelRatio;

		};

		/**
		 * Sets the given pixel ratio and resizes the canvas if necessary.
		 *
		 * @param {number} value - The pixel ratio.
		 */
		this.setPixelRatio = function ( value ) {

			if ( value === undefined ) return;

			_pixelRatio = value;

			this.setSize( _width, _height, false );

		};

		/**
		 * Returns the renderer's size in logical pixels. This method does not honor the pixel ratio.
		 *
		 * @param {Vector2} target - The method writes the result in this target object.
		 * @return {Vector2} The renderer's size in logical pixels.
		 */
		this.getSize = function ( target ) {

			return target.set( _width, _height );

		};

		/**
		 * Resizes the output canvas to (width, height) with device pixel ratio taken
		 * into account, and also sets the viewport to fit that size, starting in (0,
		 * 0). Setting `updateStyle` to false prevents any style changes to the output canvas.
		 *
		 * @param {number} width - The width in logical pixels.
		 * @param {number} height - The height in logical pixels.
		 * @param {boolean} [updateStyle=true] - Whether to update the `style` attribute of the canvas or not.
		 */
		this.setSize = function ( width, height, updateStyle = true ) {

			if ( xr.isPresenting ) {

				console.warn( 'THREE.WebGLRenderer: Can\'t change size while VR device is presenting.' );
				return;

			}

			_width = width;
			_height = height;

			canvas.width = Math.floor( width * _pixelRatio );
			canvas.height = Math.floor( height * _pixelRatio );

			if ( updateStyle === true ) {

				canvas.style.width = width + 'px';
				canvas.style.height = height + 'px';

			}

			this.setViewport( 0, 0, width, height );

		};

		/**
		 * Returns the drawing buffer size in physical pixels. This method honors the pixel ratio.
		 *
		 * @param {Vector2} target - The method writes the result in this target object.
		 * @return {Vector2} The drawing buffer size.
		 */
		this.getDrawingBufferSize = function ( target ) {

			return target.set( _width * _pixelRatio, _height * _pixelRatio ).floor();

		};

		/**
		 * This method allows to define the drawing buffer size by specifying
		 * width, height and pixel ratio all at once. The size of the drawing
		 * buffer is computed with this formula:
		 * ```js
		 * size.x = width * pixelRatio;
		 * size.y = height * pixelRatio;
		 * ```
		 *
		 * @param {number} width - The width in logical pixels.
		 * @param {number} height - The height in logical pixels.
		 * @param {number} pixelRatio - The pixel ratio.
		 */
		this.setDrawingBufferSize = function ( width, height, pixelRatio ) {

			_width = width;
			_height = height;

			_pixelRatio = pixelRatio;

			canvas.width = Math.floor( width * pixelRatio );
			canvas.height = Math.floor( height * pixelRatio );

			this.setViewport( 0, 0, width, height );

		};

		/**
		 * Returns the current viewport definition.
		 *
		 * @param {Vector2} target - The method writes the result in this target object.
		 * @return {Vector2} The current viewport definition.
		 */
		this.getCurrentViewport = function ( target ) {

			return target.copy( _currentViewport );

		};

		/**
		 * Returns the viewport definition.
		 *
		 * @param {Vector4} target - The method writes the result in this target object.
		 * @return {Vector4} The viewport definition.
		 */
		this.getViewport = function ( target ) {

			return target.copy( _viewport );

		};

		/**
		 * Sets the viewport to render from `(x, y)` to `(x + width, y + height)`.
		 *
		 * @param {number | Vector4} x - The horizontal coordinate for the lower left corner of the viewport origin in logical pixel unit.
		 * Or alternatively a four-component vector specifying all the parameters of the viewport.
		 * @param {number} y - The vertical coordinate for the lower left corner of the viewport origin  in logical pixel unit.
		 * @param {number} width - The width of the viewport in logical pixel unit.
		 * @param {number} height - The height of the viewport in logical pixel unit.
		 */
		this.setViewport = function ( x, y, width, height ) {

			if ( x.isVector4 ) {

				_viewport.set( x.x, x.y, x.z, x.w );

			} else {

				_viewport.set( x, y, width, height );

			}

			state.viewport( _currentViewport.copy( _viewport ).multiplyScalar( _pixelRatio ).round() );

		};

		/**
		 * Returns the scissor region.
		 *
		 * @param {Vector4} target - The method writes the result in this target object.
		 * @return {Vector4} The scissor region.
		 */
		this.getScissor = function ( target ) {

			return target.copy( _scissor );

		};

		/**
		 * Sets the scissor region to render from `(x, y)` to `(x + width, y + height)`.
		 *
		 * @param {number | Vector4} x - The horizontal coordinate for the lower left corner of the scissor region origin in logical pixel unit.
		 * Or alternatively a four-component vector specifying all the parameters of the scissor region.
		 * @param {number} y - The vertical coordinate for the lower left corner of the scissor region origin  in logical pixel unit.
		 * @param {number} width - The width of the scissor region in logical pixel unit.
		 * @param {number} height - The height of the scissor region in logical pixel unit.
		 */
		this.setScissor = function ( x, y, width, height ) {

			if ( x.isVector4 ) {

				_scissor.set( x.x, x.y, x.z, x.w );

			} else {

				_scissor.set( x, y, width, height );

			}

			state.scissor( _currentScissor.copy( _scissor ).multiplyScalar( _pixelRatio ).round() );

		};

		/**
		 * Returns `true` if the scissor test is enabled.
		 *
		 * @return {boolean} Whether the scissor test is enabled or not.
		 */
		this.getScissorTest = function () {

			return _scissorTest;

		};

		/**
		 * Enable or disable the scissor test. When this is enabled, only the pixels
		 * within the defined scissor area will be affected by further renderer
		 * actions.
		 *
		 * @param {boolean} boolean - Whether the scissor test is enabled or not.
		 */
		this.setScissorTest = function ( boolean ) {

			state.setScissorTest( _scissorTest = boolean );

		};

		/**
		 * Sets a custom opaque sort function for the render lists. Pass `null`
		 * to use the default `painterSortStable` function.
		 *
		 * @param {?Function} method - The opaque sort function.
		 */
		this.setOpaqueSort = function ( method ) {

			_opaqueSort = method;

		};

		/**
		 * Sets a custom transparent sort function for the render lists. Pass `null`
		 * to use the default `reversePainterSortStable` function.
		 *
		 * @param {?Function} method - The opaque sort function.
		 */
		this.setTransparentSort = function ( method ) {

			_transparentSort = method;

		};

		// Clearing

		/**
		 * Returns the clear color.
		 *
		 * @param {Color} target - The method writes the result in this target object.
		 * @return {Color} The clear color.
		 */
		this.getClearColor = function ( target ) {

			return target.copy( background.getClearColor() );

		};

		/**
		 * Sets the clear color and alpha.
		 *
		 * @param {Color} color - The clear color.
		 * @param {number} [alpha=1] - The clear alpha.
		 */
		this.setClearColor = function () {

			background.setClearColor( ...arguments );

		};

		/**
		 * Returns the clear alpha. Ranges within `[0,1]`.
		 *
		 * @return {number} The clear alpha.
		 */
		this.getClearAlpha = function () {

			return background.getClearAlpha();

		};

		/**
		 * Sets the clear alpha.
		 *
		 * @param {number} alpha - The clear alpha.
		 */
		this.setClearAlpha = function () {

			background.setClearAlpha( ...arguments );

		};

		/**
		 * Tells the renderer to clear its color, depth or stencil drawing buffer(s).
		 * This method initializes the buffers to the current clear color values.
		 *
		 * @param {boolean} [color=true] - Whether the color buffer should be cleared or not.
		 * @param {boolean} [depth=true] - Whether the depth buffer should be cleared or not.
		 * @param {boolean} [stencil=true] - Whether the stencil buffer should be cleared or not.
		 */
		this.clear = function ( color = true, depth = true, stencil = true ) {

			let bits = 0;

			if ( color ) {

				// check if we're trying to clear an integer target
				let isIntegerFormat = false;
				if ( _currentRenderTarget !== null ) {

					const targetFormat = _currentRenderTarget.texture.format;
					isIntegerFormat = targetFormat === RGBAIntegerFormat ||
						targetFormat === RGIntegerFormat ||
						targetFormat === RedIntegerFormat;

				}

				// use the appropriate clear functions to clear the target if it's a signed
				// or unsigned integer target
				if ( isIntegerFormat ) {

					const targetType = _currentRenderTarget.texture.type;
					const isUnsignedType = targetType === UnsignedByteType ||
						targetType === UnsignedIntType ||
						targetType === UnsignedShortType ||
						targetType === UnsignedInt248Type ||
						targetType === UnsignedShort4444Type ||
						targetType === UnsignedShort5551Type;

					const clearColor = background.getClearColor();
					const a = background.getClearAlpha();
					const r = clearColor.r;
					const g = clearColor.g;
					const b = clearColor.b;

					if ( isUnsignedType ) {

						uintClearColor[ 0 ] = r;
						uintClearColor[ 1 ] = g;
						uintClearColor[ 2 ] = b;
						uintClearColor[ 3 ] = a;
						_gl.clearBufferuiv( _gl.COLOR, 0, uintClearColor );

					} else {

						intClearColor[ 0 ] = r;
						intClearColor[ 1 ] = g;
						intClearColor[ 2 ] = b;
						intClearColor[ 3 ] = a;
						_gl.clearBufferiv( _gl.COLOR, 0, intClearColor );

					}

				} else {

					bits |= _gl.COLOR_BUFFER_BIT;

				}

			}

			if ( depth ) {

				bits |= _gl.DEPTH_BUFFER_BIT;

			}

			if ( stencil ) {

				bits |= _gl.STENCIL_BUFFER_BIT;
				this.state.buffers.stencil.setMask( 0xffffffff );

			}

			_gl.clear( bits );

		};

		/**
		 * Clears the color buffer. Equivalent to calling `renderer.clear( true, false, false )`.
		 */
		this.clearColor = function () {

			this.clear( true, false, false );

		};

		/**
		 * Clears the depth buffer. Equivalent to calling `renderer.clear( false, true, false )`.
		 */
		this.clearDepth = function () {

			this.clear( false, true, false );

		};

		/**
		 * Clears the stencil buffer. Equivalent to calling `renderer.clear( false, false, true )`.
		 */
		this.clearStencil = function () {

			this.clear( false, false, true );

		};

		/**
		 * Frees the GPU-related resources allocated by this instance. Call this
		 * method whenever this instance is no longer used in your app.
		 */
		this.dispose = function () {

			canvas.removeEventListener( 'webglcontextlost', onContextLost, false );
			canvas.removeEventListener( 'webglcontextrestored', onContextRestore, false );
			canvas.removeEventListener( 'webglcontextcreationerror', onContextCreationError, false );

			background.dispose();
			renderLists.dispose();
			renderStates.dispose();
			properties.dispose();
			cubemaps.dispose();
			cubeuvmaps.dispose();
			objects.dispose();
			bindingStates.dispose();
			uniformsGroups.dispose();
			programCache.dispose();

			xr.dispose();

			xr.removeEventListener( 'sessionstart', onXRSessionStart );
			xr.removeEventListener( 'sessionend', onXRSessionEnd );

			animation.stop();

		};

		// Events

		function onContextLost( event ) {

			event.preventDefault();

			console.log( 'THREE.WebGLRenderer: Context Lost.' );

			_isContextLost = true;

		}

		function onContextRestore( /* event */ ) {

			console.log( 'THREE.WebGLRenderer: Context Restored.' );

			_isContextLost = false;

			const infoAutoReset = info.autoReset;
			const shadowMapEnabled = shadowMap.enabled;
			const shadowMapAutoUpdate = shadowMap.autoUpdate;
			const shadowMapNeedsUpdate = shadowMap.needsUpdate;
			const shadowMapType = shadowMap.type;

			initGLContext();

			info.autoReset = infoAutoReset;
			shadowMap.enabled = shadowMapEnabled;
			shadowMap.autoUpdate = shadowMapAutoUpdate;
			shadowMap.needsUpdate = shadowMapNeedsUpdate;
			shadowMap.type = shadowMapType;

		}

		function onContextCreationError( event ) {

			console.error( 'THREE.WebGLRenderer: A WebGL context could not be created. Reason: ', event.statusMessage );

		}

		function onMaterialDispose( event ) {

			const material = event.target;

			material.removeEventListener( 'dispose', onMaterialDispose );

			deallocateMaterial( material );

		}

		// Buffer deallocation

		function deallocateMaterial( material ) {

			releaseMaterialProgramReferences( material );

			properties.remove( material );

		}


		function releaseMaterialProgramReferences( material ) {

			const programs = properties.get( material ).programs;

			if ( programs !== undefined ) {

				programs.forEach( function ( program ) {

					programCache.releaseProgram( program );

				} );

				if ( material.isShaderMaterial ) {

					programCache.releaseShaderCache( material );

				}

			}

		}

		// Buffer rendering

		this.renderBufferDirect = function ( camera, scene, geometry, material, object, group ) {

			if ( scene === null ) scene = _emptyScene; // renderBufferDirect second parameter used to be fog (could be null)

			const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant() < 0 );

			const program = setProgram( camera, scene, geometry, material, object );

			state.setMaterial( material, frontFaceCW );

			//

			let index = geometry.index;
			let rangeFactor = 1;

			if ( material.wireframe === true ) {

				index = geometries.getWireframeAttribute( geometry );

				if ( index === undefined ) return;

				rangeFactor = 2;

			}

			//

			const drawRange = geometry.drawRange;
			const position = geometry.attributes.position;

			let drawStart = drawRange.start * rangeFactor;
			let drawEnd = ( drawRange.start + drawRange.count ) * rangeFactor;

			if ( group !== null ) {

				drawStart = Math.max( drawStart, group.start * rangeFactor );
				drawEnd = Math.min( drawEnd, ( group.start + group.count ) * rangeFactor );

			}

			if ( index !== null ) {

				drawStart = Math.max( drawStart, 0 );
				drawEnd = Math.min( drawEnd, index.count );

			} else if ( position !== undefined && position !== null ) {

				drawStart = Math.max( drawStart, 0 );
				drawEnd = Math.min( drawEnd, position.count );

			}

			const drawCount = drawEnd - drawStart;

			if ( drawCount < 0 || drawCount === Infinity ) return;

			//

			bindingStates.setup( object, material, program, geometry, index );

			let attribute;
			let renderer = bufferRenderer;

			if ( index !== null ) {

				attribute = attributes.get( index );

				renderer = indexedBufferRenderer;
				renderer.setIndex( attribute );

			}

			//

			if ( object.isMesh ) {

				if ( material.wireframe === true ) {

					state.setLineWidth( material.wireframeLinewidth * getTargetPixelRatio() );
					renderer.setMode( _gl.LINES );

				} else {

					renderer.setMode( _gl.TRIANGLES );

				}

			} else if ( object.isLine ) {

				let lineWidth = material.linewidth;

				if ( lineWidth === undefined ) lineWidth = 1; // Not using Line*Material

				state.setLineWidth( lineWidth * getTargetPixelRatio() );

				if ( object.isLineSegments ) {

					renderer.setMode( _gl.LINES );

				} else if ( object.isLineLoop ) {

					renderer.setMode( _gl.LINE_LOOP );

				} else {

					renderer.setMode( _gl.LINE_STRIP );

				}

			} else if ( object.isPoints ) {

				renderer.setMode( _gl.POINTS );

			} else if ( object.isSprite ) {

				renderer.setMode( _gl.TRIANGLES );

			}

			if ( object.isBatchedMesh ) {

				if ( object._multiDrawInstances !== null ) {

					// @deprecated, r174
					warnOnce( 'THREE.WebGLRenderer: renderMultiDrawInstances has been deprecated and will be removed in r184. Append to renderMultiDraw arguments and use indirection.' );
					renderer.renderMultiDrawInstances( object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount, object._multiDrawInstances );

				} else {

					if ( ! extensions.get( 'WEBGL_multi_draw' ) ) {

						const starts = object._multiDrawStarts;
						const counts = object._multiDrawCounts;
						const drawCount = object._multiDrawCount;
						const bytesPerElement = index ? attributes.get( index ).bytesPerElement : 1;
						const uniforms = properties.get( material ).currentProgram.getUniforms();
						for ( let i = 0; i < drawCount; i ++ ) {

							uniforms.setValue( _gl, '_gl_DrawID', i );
							renderer.render( starts[ i ] / bytesPerElement, counts[ i ] );

						}

					} else {

						renderer.renderMultiDraw( object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount );

					}

				}

			} else if ( object.isInstancedMesh ) {

				renderer.renderInstances( drawStart, drawCount, object.count );

			} else if ( geometry.isInstancedBufferGeometry ) {

				const maxInstanceCount = geometry._maxInstanceCount !== undefined ? geometry._maxInstanceCount : Infinity;
				const instanceCount = Math.min( geometry.instanceCount, maxInstanceCount );

				renderer.renderInstances( drawStart, drawCount, instanceCount );

			} else {

				renderer.render( drawStart, drawCount );

			}

		};

		// Compile

		function prepareMaterial( material, scene, object ) {

			if ( material.transparent === true && material.side === DoubleSide && material.forceSinglePass === false ) {

				material.side = BackSide;
				material.needsUpdate = true;
				getProgram( material, scene, object );

				material.side = FrontSide;
				material.needsUpdate = true;
				getProgram( material, scene, object );

				material.side = DoubleSide;

			} else {

				getProgram( material, scene, object );

			}

		}

		/**
		 * Compiles all materials in the scene with the camera. This is useful to precompile shaders
		 * before the first rendering. If you want to add a 3D object to an existing scene, use the third
		 * optional parameter for applying the target scene.
		 *
		 * Note that the (target) scene's lighting and environment must be configured before calling this method.
		 *
		 * @param {Object3D} scene - The scene or another type of 3D object to precompile.
		 * @param {Camera} camera - The camera.
		 * @param {?Scene} [targetScene=null] - The target scene.
		 * @return {Set<Material>} The precompiled materials.
		 */
		this.compile = function ( scene, camera, targetScene = null ) {

			if ( targetScene === null ) targetScene = scene;

			currentRenderState = renderStates.get( targetScene );
			currentRenderState.init( camera );

			renderStateStack.push( currentRenderState );

			// gather lights from both the target scene and the new object that will be added to the scene.

			targetScene.traverseVisible( function ( object ) {

				if ( object.isLight && object.layers.test( camera.layers ) ) {

					currentRenderState.pushLight( object );

					if ( object.castShadow ) {

						currentRenderState.pushShadow( object );

					}

				}

			} );

			if ( scene !== targetScene ) {

				scene.traverseVisible( function ( object ) {

					if ( object.isLight && object.layers.test( camera.layers ) ) {

						currentRenderState.pushLight( object );

						if ( object.castShadow ) {

							currentRenderState.pushShadow( object );

						}

					}

				} );

			}

			currentRenderState.setupLights();

			// Only initialize materials in the new scene, not the targetScene.

			const materials = new Set();

			scene.traverse( function ( object ) {

				if ( ! ( object.isMesh || object.isPoints || object.isLine || object.isSprite ) ) {

					return;

				}

				const material = object.material;

				if ( material ) {

					if ( Array.isArray( material ) ) {

						for ( let i = 0; i < material.length; i ++ ) {

							const material2 = material[ i ];

							prepareMaterial( material2, targetScene, object );
							materials.add( material2 );

						}

					} else {

						prepareMaterial( material, targetScene, object );
						materials.add( material );

					}

				}

			} );

			currentRenderState = renderStateStack.pop();

			return materials;

		};

		// compileAsync

		/**
		 * Asynchronous version of {@link WebGLRenderer#compile}.
		 *
		 * This method makes use of the `KHR_parallel_shader_compile` WebGL extension. Hence,
		 * it is recommended to use this version of `compile()` whenever possible.
		 *
		 * @async
		 * @param {Object3D} scene - The scene or another type of 3D object to precompile.
		 * @param {Camera} camera - The camera.
		 * @param {?Scene} [targetScene=null] - The target scene.
		 * @return {Promise} A Promise that resolves when the given scene can be rendered without unnecessary stalling due to shader compilation.
		 */
		this.compileAsync = function ( scene, camera, targetScene = null ) {

			const materials = this.compile( scene, camera, targetScene );

			// Wait for all the materials in the new object to indicate that they're
			// ready to be used before resolving the promise.

			return new Promise( ( resolve ) => {

				function checkMaterialsReady() {

					materials.forEach( function ( material ) {

						const materialProperties = properties.get( material );
						const program = materialProperties.currentProgram;

						if ( program.isReady() ) {

							// remove any programs that report they're ready to use from the list
							materials.delete( material );

						}

					} );

					// once the list of compiling materials is empty, call the callback

					if ( materials.size === 0 ) {

						resolve( scene );
						return;

					}

					// if some materials are still not ready, wait a bit and check again

					setTimeout( checkMaterialsReady, 10 );

				}

				if ( extensions.get( 'KHR_parallel_shader_compile' ) !== null ) {

					// If we can check the compilation status of the materials without
					// blocking then do so right away.

					checkMaterialsReady();

				} else {

					// Otherwise start by waiting a bit to give the materials we just
					// initialized a chance to finish.

					setTimeout( checkMaterialsReady, 10 );

				}

			} );

		};

		// Animation Loop

		let onAnimationFrameCallback = null;

		function onAnimationFrame( time ) {

			if ( onAnimationFrameCallback ) onAnimationFrameCallback( time );

		}

		function onXRSessionStart() {

			animation.stop();

		}

		function onXRSessionEnd() {

			animation.start();

		}

		const animation = new WebGLAnimation();
		animation.setAnimationLoop( onAnimationFrame );

		if ( typeof self !== 'undefined' ) animation.setContext( self );

		this.setAnimationLoop = function ( callback ) {

			onAnimationFrameCallback = callback;
			xr.setAnimationLoop( callback );

			( callback === null ) ? animation.stop() : animation.start();

		};

		xr.addEventListener( 'sessionstart', onXRSessionStart );
		xr.addEventListener( 'sessionend', onXRSessionEnd );

		// Rendering

		/**
		 * Renders the given scene (or other type of 3D object) using the given camera.
		 *
		 * The render is done to a previously specified render target set by calling {@link WebGLRenderer#setRenderTarget}
		 * or to the canvas as usual.
		 *
		 * By default render buffers are cleared before rendering but you can prevent
		 * this by setting the property `autoClear` to `false`. If you want to prevent
		 * only certain buffers being cleared you can `autoClearColor`, `autoClearDepth`
		 * or `autoClearStencil` to `false`. To force a clear, use {@link WebGLRenderer#clear}.
		 *
		 * @param {Object3D} scene - The scene to render.
		 * @param {Camera} camera - The camera.
		 */
		this.render = function ( scene, camera ) {

			if ( camera !== undefined && camera.isCamera !== true ) {

				console.error( 'THREE.WebGLRenderer.render: camera is not an instance of THREE.Camera.' );
				return;

			}

			if ( _isContextLost === true ) return;

			// update scene graph

			if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();

			// update camera matrices and frustum

			if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();

			if ( xr.enabled === true && xr.isPresenting === true ) {

				if ( xr.cameraAutoUpdate === true ) xr.updateCamera( camera );

				camera = xr.getCamera(); // use XR camera for rendering

			}

			//
			if ( scene.isScene === true ) scene.onBeforeRender( _this, scene, camera, _currentRenderTarget );

			currentRenderState = renderStates.get( scene, renderStateStack.length );
			currentRenderState.init( camera );

			renderStateStack.push( currentRenderState );

			_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
			_frustum.setFromProjectionMatrix( _projScreenMatrix, WebGLCoordinateSystem, camera.reversedDepth );

			_localClippingEnabled = this.localClippingEnabled;
			_clippingEnabled = clipping.init( this.clippingPlanes, _localClippingEnabled );

			currentRenderList = renderLists.get( scene, renderListStack.length );
			currentRenderList.init();

			renderListStack.push( currentRenderList );

			if ( xr.enabled === true && xr.isPresenting === true ) {

				const depthSensingMesh = _this.xr.getDepthSensingMesh();

				if ( depthSensingMesh !== null ) {

					projectObject( depthSensingMesh, camera, - Infinity, _this.sortObjects );

				}

			}

			projectObject( scene, camera, 0, _this.sortObjects );

			currentRenderList.finish();

			if ( _this.sortObjects === true ) {

				currentRenderList.sort( _opaqueSort, _transparentSort );

			}

			_renderBackground = xr.enabled === false || xr.isPresenting === false || xr.hasDepthSensing() === false;
			if ( _renderBackground ) {

				background.addToRenderList( currentRenderList, scene );

			}

			//

			this.info.render.frame ++;

			if ( _clippingEnabled === true ) clipping.beginShadows();

			const shadowsArray = currentRenderState.state.shadowsArray;

			shadowMap.render( shadowsArray, scene, camera );

			if ( _clippingEnabled === true ) clipping.endShadows();

			//

			if ( this.info.autoReset === true ) this.info.reset();

			// render scene

			const opaqueObjects = currentRenderList.opaque;
			const transmissiveObjects = currentRenderList.transmissive;

			currentRenderState.setupLights();

			if ( camera.isArrayCamera ) {

				const cameras = camera.cameras;

				if ( transmissiveObjects.length > 0 ) {

					for ( let i = 0, l = cameras.length; i < l; i ++ ) {

						const camera2 = cameras[ i ];

						renderTransmissionPass( opaqueObjects, transmissiveObjects, scene, camera2 );

					}

				}

				if ( _renderBackground ) background.render( scene );

				for ( let i = 0, l = cameras.length; i < l; i ++ ) {

					const camera2 = cameras[ i ];

					renderScene( currentRenderList, scene, camera2, camera2.viewport );

				}

			} else {

				if ( transmissiveObjects.length > 0 ) renderTransmissionPass( opaqueObjects, transmissiveObjects, scene, camera );

				if ( _renderBackground ) background.render( scene );

				renderScene( currentRenderList, scene, camera );

			}

			//

			if ( _currentRenderTarget !== null && _currentActiveMipmapLevel === 0 ) {

				// resolve multisample renderbuffers to a single-sample texture if necessary

				textures.updateMultisampleRenderTarget( _currentRenderTarget );

				// Generate mipmap if we're using any kind of mipmap filtering

				textures.updateRenderTargetMipmap( _currentRenderTarget );

			}

			//

			if ( scene.isScene === true ) scene.onAfterRender( _this, scene, camera );

			// _gl.finish();

			bindingStates.resetDefaultState();
			_currentMaterialId = -1;
			_currentCamera = null;

			renderStateStack.pop();

			if ( renderStateStack.length > 0 ) {

				currentRenderState = renderStateStack[ renderStateStack.length - 1 ];

				if ( _clippingEnabled === true ) clipping.setGlobalState( _this.clippingPlanes, currentRenderState.state.camera );

			} else {

				currentRenderState = null;

			}

			renderListStack.pop();

			if ( renderListStack.length > 0 ) {

				currentRenderList = renderListStack[ renderListStack.length - 1 ];

			} else {

				currentRenderList = null;

			}

		};

		function projectObject( object, camera, groupOrder, sortObjects ) {

			if ( object.visible === false ) return;

			const visible = object.layers.test( camera.layers );

			if ( visible ) {

				if ( object.isGroup ) {

					groupOrder = object.renderOrder;

				} else if ( object.isLOD ) {

					if ( object.autoUpdate === true ) object.update( camera );

				} else if ( object.isLight ) {

					currentRenderState.pushLight( object );

					if ( object.castShadow ) {

						currentRenderState.pushShadow( object );

					}

				} else if ( object.isSprite ) {

					if ( ! object.frustumCulled || _frustum.intersectsSprite( object ) ) {

						if ( sortObjects ) {

							_vector4.setFromMatrixPosition( object.matrixWorld )
								.applyMatrix4( _projScreenMatrix );

						}

						const geometry = objects.update( object );
						const material = object.material;

						if ( material.visible ) {

							currentRenderList.push( object, geometry, material, groupOrder, _vector4.z, null );

						}

					}

				} else if ( object.isMesh || object.isLine || object.isPoints ) {

					if ( ! object.frustumCulled || _frustum.intersectsObject( object ) ) {

						const geometry = objects.update( object );
						const material = object.material;

						if ( sortObjects ) {

							if ( object.boundingSphere !== undefined ) {

								if ( object.boundingSphere === null ) object.computeBoundingSphere();
								_vector4.copy( object.boundingSphere.center );

							} else {

								if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();
								_vector4.copy( geometry.boundingSphere.center );

							}

							_vector4
								.applyMatrix4( object.matrixWorld )
								.applyMatrix4( _projScreenMatrix );

						}

						if ( Array.isArray( material ) ) {

							const groups = geometry.groups;

							for ( let i = 0, l = groups.length; i < l; i ++ ) {

								const group = groups[ i ];
								const groupMaterial = material[ group.materialIndex ];

								if ( groupMaterial && groupMaterial.visible ) {

									currentRenderList.push( object, geometry, groupMaterial, groupOrder, _vector4.z, group );

								}

							}

						} else if ( material.visible ) {

							currentRenderList.push( object, geometry, material, groupOrder, _vector4.z, null );

						}

					}

				}

			}

			const children = object.children;

			for ( let i = 0, l = children.length; i < l; i ++ ) {

				projectObject( children[ i ], camera, groupOrder, sortObjects );

			}

		}

		function renderScene( currentRenderList, scene, camera, viewport ) {

			const opaqueObjects = currentRenderList.opaque;
			const transmissiveObjects = currentRenderList.transmissive;
			const transparentObjects = currentRenderList.transparent;

			currentRenderState.setupLightsView( camera );

			if ( _clippingEnabled === true ) clipping.setGlobalState( _this.clippingPlanes, camera );

			if ( viewport ) state.viewport( _currentViewport.copy( viewport ) );

			if ( opaqueObjects.length > 0 ) renderObjects( opaqueObjects, scene, camera );
			if ( transmissiveObjects.length > 0 ) renderObjects( transmissiveObjects, scene, camera );
			if ( transparentObjects.length > 0 ) renderObjects( transparentObjects, scene, camera );

			// Ensure depth buffer writing is enabled so it can be cleared on next render

			state.buffers.depth.setTest( true );
			state.buffers.depth.setMask( true );
			state.buffers.color.setMask( true );

			state.setPolygonOffset( false );

		}

		function renderTransmissionPass( opaqueObjects, transmissiveObjects, scene, camera ) {

			const overrideMaterial = scene.isScene === true ? scene.overrideMaterial : null;

			if ( overrideMaterial !== null ) {

				return;

			}

			if ( currentRenderState.state.transmissionRenderTarget[ camera.id ] === undefined ) {

				currentRenderState.state.transmissionRenderTarget[ camera.id ] = new WebGLRenderTarget( 1, 1, {
					generateMipmaps: true,
					type: ( extensions.has( 'EXT_color_buffer_half_float' ) || extensions.has( 'EXT_color_buffer_float' ) ) ? HalfFloatType : UnsignedByteType,
					minFilter: LinearMipmapLinearFilter,
					samples: 4,
					stencilBuffer: stencil,
					resolveDepthBuffer: false,
					resolveStencilBuffer: false,
					colorSpace: ColorManagement.workingColorSpace,
				} );

				// debug

				/*
				const geometry = new PlaneGeometry();
				const material = new MeshBasicMaterial( { map: _transmissionRenderTarget.texture } );

				const mesh = new Mesh( geometry, material );
				scene.add( mesh );
				*/

			}

			const transmissionRenderTarget = currentRenderState.state.transmissionRenderTarget[ camera.id ];

			const activeViewport = camera.viewport || _currentViewport;
			transmissionRenderTarget.setSize( activeViewport.z * _this.transmissionResolutionScale, activeViewport.w * _this.transmissionResolutionScale );

			//

			const currentRenderTarget = _this.getRenderTarget();
			const currentActiveCubeFace = _this.getActiveCubeFace();
			const currentActiveMipmapLevel = _this.getActiveMipmapLevel();

			_this.setRenderTarget( transmissionRenderTarget );

			_this.getClearColor( _currentClearColor );
			_currentClearAlpha = _this.getClearAlpha();
			if ( _currentClearAlpha < 1 ) _this.setClearColor( 0xffffff, 0.5 );

			_this.clear();

			if ( _renderBackground ) background.render( scene );

			// Turn off the features which can affect the frag color for opaque objects pass.
			// Otherwise they are applied twice in opaque objects pass and transmission objects pass.
			const currentToneMapping = _this.toneMapping;
			_this.toneMapping = NoToneMapping;

			// Remove viewport from camera to avoid nested render calls resetting viewport to it (e.g Reflector).
			// Transmission render pass requires viewport to match the transmissionRenderTarget.
			const currentCameraViewport = camera.viewport;
			if ( camera.viewport !== undefined ) camera.viewport = undefined;

			currentRenderState.setupLightsView( camera );

			if ( _clippingEnabled === true ) clipping.setGlobalState( _this.clippingPlanes, camera );

			renderObjects( opaqueObjects, scene, camera );

			textures.updateMultisampleRenderTarget( transmissionRenderTarget );
			textures.updateRenderTargetMipmap( transmissionRenderTarget );

			if ( extensions.has( 'WEBGL_multisampled_render_to_texture' ) === false ) { // see #28131

				let renderTargetNeedsUpdate = false;

				for ( let i = 0, l = transmissiveObjects.length; i < l; i ++ ) {

					const renderItem = transmissiveObjects[ i ];

					const object = renderItem.object;
					const geometry = renderItem.geometry;
					const material = renderItem.material;
					const group = renderItem.group;

					if ( material.side === DoubleSide && object.layers.test( camera.layers ) ) {

						const currentSide = material.side;

						material.side = BackSide;
						material.needsUpdate = true;

						renderObject( object, scene, camera, geometry, material, group );

						material.side = currentSide;
						material.needsUpdate = true;

						renderTargetNeedsUpdate = true;

					}

				}

				if ( renderTargetNeedsUpdate === true ) {

					textures.updateMultisampleRenderTarget( transmissionRenderTarget );
					textures.updateRenderTargetMipmap( transmissionRenderTarget );

				}

			}

			_this.setRenderTarget( currentRenderTarget, currentActiveCubeFace, currentActiveMipmapLevel );

			_this.setClearColor( _currentClearColor, _currentClearAlpha );

			if ( currentCameraViewport !== undefined ) camera.viewport = currentCameraViewport;

			_this.toneMapping = currentToneMapping;

		}

		function renderObjects( renderList, scene, camera ) {

			const overrideMaterial = scene.isScene === true ? scene.overrideMaterial : null;

			for ( let i = 0, l = renderList.length; i < l; i ++ ) {

				const renderItem = renderList[ i ];

				const object = renderItem.object;
				const geometry = renderItem.geometry;
				const group = renderItem.group;
				let material = renderItem.material;

				if ( material.allowOverride === true && overrideMaterial !== null ) {

					material = overrideMaterial;

				}

				if ( object.layers.test( camera.layers ) ) {

					renderObject( object, scene, camera, geometry, material, group );

				}

			}

		}

		function renderObject( object, scene, camera, geometry, material, group ) {

			object.onBeforeRender( _this, scene, camera, geometry, material, group );

			object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );
			object.normalMatrix.getNormalMatrix( object.modelViewMatrix );

			material.onBeforeRender( _this, scene, camera, geometry, object, group );

			if ( material.transparent === true && material.side === DoubleSide && material.forceSinglePass === false ) {

				material.side = BackSide;
				material.needsUpdate = true;
				_this.renderBufferDirect( camera, scene, geometry, material, object, group );

				material.side = FrontSide;
				material.needsUpdate = true;
				_this.renderBufferDirect( camera, scene, geometry, material, object, group );

				material.side = DoubleSide;

			} else {

				_this.renderBufferDirect( camera, scene, geometry, material, object, group );

			}

			object.onAfterRender( _this, scene, camera, geometry, material, group );

		}

		function getProgram( material, scene, object ) {

			if ( scene.isScene !== true ) scene = _emptyScene; // scene could be a Mesh, Line, Points, ...

			const materialProperties = properties.get( material );

			const lights = currentRenderState.state.lights;
			const shadowsArray = currentRenderState.state.shadowsArray;

			const lightsStateVersion = lights.state.version;

			const parameters = programCache.getParameters( material, lights.state, shadowsArray, scene, object );
			const programCacheKey = programCache.getProgramCacheKey( parameters );

			let programs = materialProperties.programs;

			// always update environment and fog - changing these trigger an getProgram call, but it's possible that the program doesn't change

			materialProperties.environment = material.isMeshStandardMaterial ? scene.environment : null;
			materialProperties.fog = scene.fog;
			materialProperties.envMap = ( material.isMeshStandardMaterial ? cubeuvmaps : cubemaps ).get( material.envMap || materialProperties.environment );
			materialProperties.envMapRotation = ( materialProperties.environment !== null && material.envMap === null ) ? scene.environmentRotation : material.envMapRotation;

			if ( programs === undefined ) {

				// new material

				material.addEventListener( 'dispose', onMaterialDispose );

				programs = new Map();
				materialProperties.programs = programs;

			}

			let program = programs.get( programCacheKey );

			if ( program !== undefined ) {

				// early out if program and light state is identical

				if ( materialProperties.currentProgram === program && materialProperties.lightsStateVersion === lightsStateVersion ) {

					updateCommonMaterialProperties( material, parameters );

					return program;

				}

			} else {

				parameters.uniforms = programCache.getUniforms( material );

				material.onBeforeCompile( parameters, _this );

				program = programCache.acquireProgram( parameters, programCacheKey );
				programs.set( programCacheKey, program );

				materialProperties.uniforms = parameters.uniforms;

			}

			const uniforms = materialProperties.uniforms;

			if ( ( ! material.isShaderMaterial && ! material.isRawShaderMaterial ) || material.clipping === true ) {

				uniforms.clippingPlanes = clipping.uniform;

			}

			updateCommonMaterialProperties( material, parameters );

			// store the light setup it was created for

			materialProperties.needsLights = materialNeedsLights( material );
			materialProperties.lightsStateVersion = lightsStateVersion;

			if ( materialProperties.needsLights ) {

				// wire up the material to this renderer's lighting state

				uniforms.ambientLightColor.value = lights.state.ambient;
				uniforms.lightProbe.value = lights.state.probe;
				uniforms.directionalLights.value = lights.state.directional;
				uniforms.directionalLightShadows.value = lights.state.directionalShadow;
				uniforms.spotLights.value = lights.state.spot;
				uniforms.spotLightShadows.value = lights.state.spotShadow;
				uniforms.rectAreaLights.value = lights.state.rectArea;
				uniforms.ltc_1.value = lights.state.rectAreaLTC1;
				uniforms.ltc_2.value = lights.state.rectAreaLTC2;
				uniforms.pointLights.value = lights.state.point;
				uniforms.pointLightShadows.value = lights.state.pointShadow;
				uniforms.hemisphereLights.value = lights.state.hemi;

				uniforms.directionalShadowMap.value = lights.state.directionalShadowMap;
				uniforms.directionalShadowMatrix.value = lights.state.directionalShadowMatrix;
				uniforms.spotShadowMap.value = lights.state.spotShadowMap;
				uniforms.spotLightMatrix.value = lights.state.spotLightMatrix;
				uniforms.spotLightMap.value = lights.state.spotLightMap;
				uniforms.pointShadowMap.value = lights.state.pointShadowMap;
				uniforms.pointShadowMatrix.value = lights.state.pointShadowMatrix;
				// TODO (abelnation): add area lights shadow info to uniforms

			}

			materialProperties.currentProgram = program;
			materialProperties.uniformsList = null;

			return program;

		}

		function getUniformList( materialProperties ) {

			if ( materialProperties.uniformsList === null ) {

				const progUniforms = materialProperties.currentProgram.getUniforms();
				materialProperties.uniformsList = WebGLUniforms.seqWithValue( progUniforms.seq, materialProperties.uniforms );

			}

			return materialProperties.uniformsList;

		}

		function updateCommonMaterialProperties( material, parameters ) {

			const materialProperties = properties.get( material );

			materialProperties.outputColorSpace = parameters.outputColorSpace;
			materialProperties.batching = parameters.batching;
			materialProperties.batchingColor = parameters.batchingColor;
			materialProperties.instancing = parameters.instancing;
			materialProperties.instancingColor = parameters.instancingColor;
			materialProperties.instancingMorph = parameters.instancingMorph;
			materialProperties.skinning = parameters.skinning;
			materialProperties.morphTargets = parameters.morphTargets;
			materialProperties.morphNormals = parameters.morphNormals;
			materialProperties.morphColors = parameters.morphColors;
			materialProperties.morphTargetsCount = parameters.morphTargetsCount;
			materialProperties.numClippingPlanes = parameters.numClippingPlanes;
			materialProperties.numIntersection = parameters.numClipIntersection;
			materialProperties.vertexAlphas = parameters.vertexAlphas;
			materialProperties.vertexTangents = parameters.vertexTangents;
			materialProperties.toneMapping = parameters.toneMapping;

		}

		function setProgram( camera, scene, geometry, material, object ) {

			if ( scene.isScene !== true ) scene = _emptyScene; // scene could be a Mesh, Line, Points, ...

			textures.resetTextureUnits();

			const fog = scene.fog;
			const environment = material.isMeshStandardMaterial ? scene.environment : null;
			const colorSpace = ( _currentRenderTarget === null ) ? _this.outputColorSpace : ( _currentRenderTarget.isXRRenderTarget === true ? _currentRenderTarget.texture.colorSpace : LinearSRGBColorSpace );
			const envMap = ( material.isMeshStandardMaterial ? cubeuvmaps : cubemaps ).get( material.envMap || environment );
			const vertexAlphas = material.vertexColors === true && !! geometry.attributes.color && geometry.attributes.color.itemSize === 4;
			const vertexTangents = !! geometry.attributes.tangent && ( !! material.normalMap || material.anisotropy > 0 );
			const morphTargets = !! geometry.morphAttributes.position;
			const morphNormals = !! geometry.morphAttributes.normal;
			const morphColors = !! geometry.morphAttributes.color;

			let toneMapping = NoToneMapping;

			if ( material.toneMapped ) {

				if ( _currentRenderTarget === null || _currentRenderTarget.isXRRenderTarget === true ) {

					toneMapping = _this.toneMapping;

				}

			}

			const morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;
			const morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;

			const materialProperties = properties.get( material );
			const lights = currentRenderState.state.lights;

			if ( _clippingEnabled === true ) {

				if ( _localClippingEnabled === true || camera !== _currentCamera ) {

					const useCache =
						camera === _currentCamera &&
						material.id === _currentMaterialId;

					// we might want to call this function with some ClippingGroup
					// object instead of the material, once it becomes feasible
					// (#8465, #8379)
					clipping.setState( material, camera, useCache );

				}

			}

			//

			let needsProgramChange = false;

			if ( material.version === materialProperties.__version ) {

				if ( materialProperties.needsLights && ( materialProperties.lightsStateVersion !== lights.state.version ) ) {

					needsProgramChange = true;

				} else if ( materialProperties.outputColorSpace !== colorSpace ) {

					needsProgramChange = true;

				} else if ( object.isBatchedMesh && materialProperties.batching === false ) {

					needsProgramChange = true;

				} else if ( ! object.isBatchedMesh && materialProperties.batching === true ) {

					needsProgramChange = true;

				} else if ( object.isBatchedMesh && materialProperties.batchingColor === true && object.colorTexture === null ) {

					needsProgramChange = true;

				} else if ( object.isBatchedMesh && materialProperties.batchingColor === false && object.colorTexture !== null ) {

					needsProgramChange = true;

				} else if ( object.isInstancedMesh && materialProperties.instancing === false ) {

					needsProgramChange = true;

				} else if ( ! object.isInstancedMesh && materialProperties.instancing === true ) {

					needsProgramChange = true;

				} else if ( object.isSkinnedMesh && materialProperties.skinning === false ) {

					needsProgramChange = true;

				} else if ( ! object.isSkinnedMesh && materialProperties.skinning === true ) {

					needsProgramChange = true;

				} else if ( object.isInstancedMesh && materialProperties.instancingColor === true && object.instanceColor === null ) {

					needsProgramChange = true;

				} else if ( object.isInstancedMesh && materialProperties.instancingColor === false && object.instanceColor !== null ) {

					needsProgramChange = true;

				} else if ( object.isInstancedMesh && materialProperties.instancingMorph === true && object.morphTexture === null ) {

					needsProgramChange = true;

				} else if ( object.isInstancedMesh && materialProperties.instancingMorph === false && object.morphTexture !== null ) {

					needsProgramChange = true;

				} else if ( materialProperties.envMap !== envMap ) {

					needsProgramChange = true;

				} else if ( material.fog === true && materialProperties.fog !== fog ) {

					needsProgramChange = true;

				} else if ( materialProperties.numClippingPlanes !== undefined &&
					( materialProperties.numClippingPlanes !== clipping.numPlanes ||
					materialProperties.numIntersection !== clipping.numIntersection ) ) {

					needsProgramChange = true;

				} else if ( materialProperties.vertexAlphas !== vertexAlphas ) {

					needsProgramChange = true;

				} else if ( materialProperties.vertexTangents !== vertexTangents ) {

					needsProgramChange = true;

				} else if ( materialProperties.morphTargets !== morphTargets ) {

					needsProgramChange = true;

				} else if ( materialProperties.morphNormals !== morphNormals ) {

					needsProgramChange = true;

				} else if ( materialProperties.morphColors !== morphColors ) {

					needsProgramChange = true;

				} else if ( materialProperties.toneMapping !== toneMapping ) {

					needsProgramChange = true;

				} else if ( materialProperties.morphTargetsCount !== morphTargetsCount ) {

					needsProgramChange = true;

				}

			} else {

				needsProgramChange = true;
				materialProperties.__version = material.version;

			}

			//

			let program = materialProperties.currentProgram;

			if ( needsProgramChange === true ) {

				program = getProgram( material, scene, object );

			}

			let refreshProgram = false;
			let refreshMaterial = false;
			let refreshLights = false;

			const p_uniforms = program.getUniforms(),
				m_uniforms = materialProperties.uniforms;

			if ( state.useProgram( program.program ) ) {

				refreshProgram = true;
				refreshMaterial = true;
				refreshLights = true;

			}

			if ( material.id !== _currentMaterialId ) {

				_currentMaterialId = material.id;

				refreshMaterial = true;

			}

			if ( refreshProgram || _currentCamera !== camera ) {

				// common camera uniforms

				const reversedDepthBuffer = state.buffers.depth.getReversed();

				if ( reversedDepthBuffer && camera.reversedDepth !== true ) {

					camera._reversedDepth = true;
					camera.updateProjectionMatrix();

				}

				p_uniforms.setValue( _gl, 'projectionMatrix', camera.projectionMatrix );

				p_uniforms.setValue( _gl, 'viewMatrix', camera.matrixWorldInverse );

				const uCamPos = p_uniforms.map.cameraPosition;

				if ( uCamPos !== undefined ) {

					uCamPos.setValue( _gl, _vector3.setFromMatrixPosition( camera.matrixWorld ) );

				}

				if ( capabilities.logarithmicDepthBuffer ) {

					p_uniforms.setValue( _gl, 'logDepthBufFC',
						2.0 / ( Math.log( camera.far + 1.0 ) / Math.LN2 ) );

				}

				// consider moving isOrthographic to UniformLib and WebGLMaterials, see https://github.com/mrdoob/three.js/pull/26467#issuecomment-1645185067

				if ( material.isMeshPhongMaterial ||
					material.isMeshToonMaterial ||
					material.isMeshLambertMaterial ||
					material.isMeshBasicMaterial ||
					material.isMeshStandardMaterial ||
					material.isShaderMaterial ) {

					p_uniforms.setValue( _gl, 'isOrthographic', camera.isOrthographicCamera === true );

				}

				if ( _currentCamera !== camera ) {

					_currentCamera = camera;

					// lighting uniforms depend on the camera so enforce an update
					// now, in case this material supports lights - or later, when
					// the next material that does gets activated:

					refreshMaterial = true;		// set to true on material change
					refreshLights = true;		// remains set until update done

				}

			}

			// skinning and morph target uniforms must be set even if material didn't change
			// auto-setting of texture unit for bone and morph texture must go before other textures
			// otherwise textures used for skinning and morphing can take over texture units reserved for other material textures

			if ( object.isSkinnedMesh ) {

				p_uniforms.setOptional( _gl, object, 'bindMatrix' );
				p_uniforms.setOptional( _gl, object, 'bindMatrixInverse' );

				const skeleton = object.skeleton;

				if ( skeleton ) {

					if ( skeleton.boneTexture === null ) skeleton.computeBoneTexture();

					p_uniforms.setValue( _gl, 'boneTexture', skeleton.boneTexture, textures );

				}

			}

			if ( object.isBatchedMesh ) {

				p_uniforms.setOptional( _gl, object, 'batchingTexture' );
				p_uniforms.setValue( _gl, 'batchingTexture', object._matricesTexture, textures );

				p_uniforms.setOptional( _gl, object, 'batchingIdTexture' );
				p_uniforms.setValue( _gl, 'batchingIdTexture', object._indirectTexture, textures );

				p_uniforms.setOptional( _gl, object, 'batchingColorTexture' );
				if ( object._colorsTexture !== null ) {

					p_uniforms.setValue( _gl, 'batchingColorTexture', object._colorsTexture, textures );

				}

			}

			const morphAttributes = geometry.morphAttributes;

			if ( morphAttributes.position !== undefined || morphAttributes.normal !== undefined || ( morphAttributes.color !== undefined ) ) {

				morphtargets.update( object, geometry, program );

			}

			if ( refreshMaterial || materialProperties.receiveShadow !== object.receiveShadow ) {

				materialProperties.receiveShadow = object.receiveShadow;
				p_uniforms.setValue( _gl, 'receiveShadow', object.receiveShadow );

			}

			// https://github.com/mrdoob/three.js/pull/24467#issuecomment-1209031512

			if ( material.isMeshGouraudMaterial && material.envMap !== null ) {

				m_uniforms.envMap.value = envMap;

				m_uniforms.flipEnvMap.value = ( envMap.isCubeTexture && envMap.isRenderTargetTexture === false ) ? -1 : 1;

			}

			if ( material.isMeshStandardMaterial && material.envMap === null && scene.environment !== null ) {

				m_uniforms.envMapIntensity.value = scene.environmentIntensity;

			}

			if ( refreshMaterial ) {

				p_uniforms.setValue( _gl, 'toneMappingExposure', _this.toneMappingExposure );

				if ( materialProperties.needsLights ) {

					// the current material requires lighting info

					// note: all lighting uniforms are always set correctly
					// they simply reference the renderer's state for their
					// values
					//
					// use the current material's .needsUpdate flags to set
					// the GL state when required

					markUniformsLightsNeedsUpdate( m_uniforms, refreshLights );

				}

				// refresh uniforms common to several materials

				if ( fog && material.fog === true ) {

					materials.refreshFogUniforms( m_uniforms, fog );

				}

				materials.refreshMaterialUniforms( m_uniforms, material, _pixelRatio, _height, currentRenderState.state.transmissionRenderTarget[ camera.id ] );

				WebGLUniforms.upload( _gl, getUniformList( materialProperties ), m_uniforms, textures );

			}

			if ( material.isShaderMaterial && material.uniformsNeedUpdate === true ) {

				WebGLUniforms.upload( _gl, getUniformList( materialProperties ), m_uniforms, textures );
				material.uniformsNeedUpdate = false;

			}

			if ( material.isSpriteMaterial ) {

				p_uniforms.setValue( _gl, 'center', object.center );

			}

			// common matrices

			p_uniforms.setValue( _gl, 'modelViewMatrix', object.modelViewMatrix );
			p_uniforms.setValue( _gl, 'normalMatrix', object.normalMatrix );
			p_uniforms.setValue( _gl, 'modelMatrix', object.matrixWorld );

			// UBOs

			if ( material.isShaderMaterial || material.isRawShaderMaterial ) {

				const groups = material.uniformsGroups;

				for ( let i = 0, l = groups.length; i < l; i ++ ) {

					const group = groups[ i ];

					uniformsGroups.update( group, program );
					uniformsGroups.bind( group, program );

				}

			}

			return program;

		}

		// If uniforms are marked as clean, they don't need to be loaded to the GPU.

		function markUniformsLightsNeedsUpdate( uniforms, value ) {

			uniforms.ambientLightColor.needsUpdate = value;
			uniforms.lightProbe.needsUpdate = value;

			uniforms.directionalLights.needsUpdate = value;
			uniforms.directionalLightShadows.needsUpdate = value;
			uniforms.pointLights.needsUpdate = value;
			uniforms.pointLightShadows.needsUpdate = value;
			uniforms.spotLights.needsUpdate = value;
			uniforms.spotLightShadows.needsUpdate = value;
			uniforms.rectAreaLights.needsUpdate = value;
			uniforms.hemisphereLights.needsUpdate = value;

		}

		function materialNeedsLights( material ) {

			return material.isMeshLambertMaterial || material.isMeshToonMaterial || material.isMeshPhongMaterial ||
				material.isMeshStandardMaterial || material.isShadowMaterial ||
				( material.isShaderMaterial && material.lights === true );

		}

		/**
		 * Returns the active cube face.
		 *
		 * @return {number} The active cube face.
		 */
		this.getActiveCubeFace = function () {

			return _currentActiveCubeFace;

		};

		/**
		 * Returns the active mipmap level.
		 *
		 * @return {number} The active mipmap level.
		 */
		this.getActiveMipmapLevel = function () {

			return _currentActiveMipmapLevel;

		};

		/**
		 * Returns the active render target.
		 *
		 * @return {?WebGLRenderTarget} The active render target. Returns `null` if no render target
		 * is currently set.
		 */
		this.getRenderTarget = function () {

			return _currentRenderTarget;

		};

		this.setRenderTargetTextures = function ( renderTarget, colorTexture, depthTexture ) {

			const renderTargetProperties = properties.get( renderTarget );

			renderTargetProperties.__autoAllocateDepthBuffer = renderTarget.resolveDepthBuffer === false;
			if ( renderTargetProperties.__autoAllocateDepthBuffer === false ) {

				// The multisample_render_to_texture extension doesn't work properly if there
				// are midframe flushes and an external depth buffer. Disable use of the extension.
				renderTargetProperties.__useRenderToTexture = false;

			}

			properties.get( renderTarget.texture ).__webglTexture = colorTexture;
			properties.get( renderTarget.depthTexture ).__webglTexture = renderTargetProperties.__autoAllocateDepthBuffer ? undefined : depthTexture;

			renderTargetProperties.__hasExternalTextures = true;

		};

		this.setRenderTargetFramebuffer = function ( renderTarget, defaultFramebuffer ) {

			const renderTargetProperties = properties.get( renderTarget );
			renderTargetProperties.__webglFramebuffer = defaultFramebuffer;
			renderTargetProperties.__useDefaultFramebuffer = defaultFramebuffer === undefined;

		};

		const _scratchFrameBuffer = _gl.createFramebuffer();

		/**
		 * Sets the active rendertarget.
		 *
		 * @param {?WebGLRenderTarget} renderTarget - The render target to set. When `null` is given,
		 * the canvas is set as the active render target instead.
		 * @param {number} [activeCubeFace=0] - The active cube face when using a cube render target.
		 * Indicates the z layer to render in to when using 3D or array render targets.
		 * @param {number} [activeMipmapLevel=0] - The active mipmap level.
		 */
		this.setRenderTarget = function ( renderTarget, activeCubeFace = 0, activeMipmapLevel = 0 ) {

			_currentRenderTarget = renderTarget;
			_currentActiveCubeFace = activeCubeFace;
			_currentActiveMipmapLevel = activeMipmapLevel;

			let useDefaultFramebuffer = true;
			let framebuffer = null;
			let isCube = false;
			let isRenderTarget3D = false;

			if ( renderTarget ) {

				const renderTargetProperties = properties.get( renderTarget );

				if ( renderTargetProperties.__useDefaultFramebuffer !== undefined ) {

					// We need to make sure to rebind the framebuffer.
					state.bindFramebuffer( _gl.FRAMEBUFFER, null );
					useDefaultFramebuffer = false;

				} else if ( renderTargetProperties.__webglFramebuffer === undefined ) {

					textures.setupRenderTarget( renderTarget );

				} else if ( renderTargetProperties.__hasExternalTextures ) {

					// Color and depth texture must be rebound in order for the swapchain to update.
					textures.rebindTextures( renderTarget, properties.get( renderTarget.texture ).__webglTexture, properties.get( renderTarget.depthTexture ).__webglTexture );

				} else if ( renderTarget.depthBuffer ) {

					// check if the depth texture is already bound to the frame buffer and that it's been initialized
					const depthTexture = renderTarget.depthTexture;
					if ( renderTargetProperties.__boundDepthTexture !== depthTexture ) {

						// check if the depth texture is compatible
						if (
							depthTexture !== null &&
							properties.has( depthTexture ) &&
							( renderTarget.width !== depthTexture.image.width || renderTarget.height !== depthTexture.image.height )
						) {

							throw new Error( 'WebGLRenderTarget: Attached DepthTexture is initialized to the incorrect size.' );

						}

						// Swap the depth buffer to the currently attached one
						textures.setupDepthRenderbuffer( renderTarget );

					}

				}

				const texture = renderTarget.texture;

				if ( texture.isData3DTexture || texture.isDataArrayTexture || texture.isCompressedArrayTexture ) {

					isRenderTarget3D = true;

				}

				const __webglFramebuffer = properties.get( renderTarget ).__webglFramebuffer;

				if ( renderTarget.isWebGLCubeRenderTarget ) {

					if ( Array.isArray( __webglFramebuffer[ activeCubeFace ] ) ) {

						framebuffer = __webglFramebuffer[ activeCubeFace ][ activeMipmapLevel ];

					} else {

						framebuffer = __webglFramebuffer[ activeCubeFace ];

					}

					isCube = true;

				} else if ( ( renderTarget.samples > 0 ) && textures.useMultisampledRTT( renderTarget ) === false ) {

					framebuffer = properties.get( renderTarget ).__webglMultisampledFramebuffer;

				} else {

					if ( Array.isArray( __webglFramebuffer ) ) {

						framebuffer = __webglFramebuffer[ activeMipmapLevel ];

					} else {

						framebuffer = __webglFramebuffer;

					}

				}

				_currentViewport.copy( renderTarget.viewport );
				_currentScissor.copy( renderTarget.scissor );
				_currentScissorTest = renderTarget.scissorTest;

			} else {

				_currentViewport.copy( _viewport ).multiplyScalar( _pixelRatio ).floor();
				_currentScissor.copy( _scissor ).multiplyScalar( _pixelRatio ).floor();
				_currentScissorTest = _scissorTest;

			}

			// Use a scratch frame buffer if rendering to a mip level to avoid depth buffers
			// being bound that are different sizes.
			if ( activeMipmapLevel !== 0 ) {

				framebuffer = _scratchFrameBuffer;

			}

			const framebufferBound = state.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer );

			if ( framebufferBound && useDefaultFramebuffer ) {

				state.drawBuffers( renderTarget, framebuffer );

			}

			state.viewport( _currentViewport );
			state.scissor( _currentScissor );
			state.setScissorTest( _currentScissorTest );

			if ( isCube ) {

				const textureProperties = properties.get( renderTarget.texture );
				_gl.framebufferTexture2D( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_CUBE_MAP_POSITIVE_X + activeCubeFace, textureProperties.__webglTexture, activeMipmapLevel );

			} else if ( isRenderTarget3D ) {

				const layer = activeCubeFace;

				for ( let i = 0; i < renderTarget.textures.length; i ++ ) {

					const textureProperties = properties.get( renderTarget.textures[ i ] );

					_gl.framebufferTextureLayer( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0 + i, textureProperties.__webglTexture, activeMipmapLevel, layer );

				}

			} else if ( renderTarget !== null && activeMipmapLevel !== 0 ) {

				// Only bind the frame buffer if we are using a scratch frame buffer to render to a mipmap.
				// If we rebind the texture when using a multi sample buffer then an error about inconsistent samples will be thrown.
				const textureProperties = properties.get( renderTarget.texture );
				_gl.framebufferTexture2D( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, textureProperties.__webglTexture, activeMipmapLevel );

			}

			_currentMaterialId = -1; // reset current material to ensure correct uniform bindings

		};

		/**
		 * Reads the pixel data from the given render target into the given buffer.
		 *
		 * @param {WebGLRenderTarget} renderTarget - The render target to read from.
		 * @param {number} x - The `x` coordinate of the copy region's origin.
		 * @param {number} y - The `y` coordinate of the copy region's origin.
		 * @param {number} width - The width of the copy region.
		 * @param {number} height - The height of the copy region.
		 * @param {TypedArray} buffer - The result buffer.
		 * @param {number} [activeCubeFaceIndex] - The active cube face index.
		 * @param {number} [textureIndex=0] - The texture index of an MRT render target.
		 */
		this.readRenderTargetPixels = function ( renderTarget, x, y, width, height, buffer, activeCubeFaceIndex, textureIndex = 0 ) {

			if ( ! ( renderTarget && renderTarget.isWebGLRenderTarget ) ) {

				console.error( 'THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.' );
				return;

			}

			let framebuffer = properties.get( renderTarget ).__webglFramebuffer;

			if ( renderTarget.isWebGLCubeRenderTarget && activeCubeFaceIndex !== undefined ) {

				framebuffer = framebuffer[ activeCubeFaceIndex ];

			}

			if ( framebuffer ) {

				state.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer );

				try {

					const texture = renderTarget.textures[ textureIndex ];
					const textureFormat = texture.format;
					const textureType = texture.type;

					if ( ! capabilities.textureFormatReadable( textureFormat ) ) {

						console.error( 'THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.' );
						return;

					}

					if ( ! capabilities.textureTypeReadable( textureType ) ) {

						console.error( 'THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.' );
						return;

					}

					// the following if statement ensures valid read requests (no out-of-bounds pixels, see #8604)

					if ( ( x >= 0 && x <= ( renderTarget.width - width ) ) && ( y >= 0 && y <= ( renderTarget.height - height ) ) ) {

						// when using MRT, select the correct color buffer for the subsequent read command

						if ( renderTarget.textures.length > 1 ) _gl.readBuffer( _gl.COLOR_ATTACHMENT0 + textureIndex );

						_gl.readPixels( x, y, width, height, utils.convert( textureFormat ), utils.convert( textureType ), buffer );

					}

				} finally {

					// restore framebuffer of current render target if necessary

					const framebuffer = ( _currentRenderTarget !== null ) ? properties.get( _currentRenderTarget ).__webglFramebuffer : null;
					state.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer );

				}

			}

		};

		/**
		 * Asynchronous, non-blocking version of {@link WebGLRenderer#readRenderTargetPixels}.
		 *
		 * It is recommended to use this version of `readRenderTargetPixels()` whenever possible.
		 *
		 * @async
		 * @param {WebGLRenderTarget} renderTarget - The render target to read from.
		 * @param {number} x - The `x` coordinate of the copy region's origin.
		 * @param {number} y - The `y` coordinate of the copy region's origin.
		 * @param {number} width - The width of the copy region.
		 * @param {number} height - The height of the copy region.
		 * @param {TypedArray} buffer - The result buffer.
		 * @param {number} [activeCubeFaceIndex] - The active cube face index.
		 * @param {number} [textureIndex=0] - The texture index of an MRT render target.
		 * @return {Promise<TypedArray>} A Promise that resolves when the read has been finished. The resolve provides the read data as a typed array.
		 */
		this.readRenderTargetPixelsAsync = async function ( renderTarget, x, y, width, height, buffer, activeCubeFaceIndex, textureIndex = 0 ) {

			if ( ! ( renderTarget && renderTarget.isWebGLRenderTarget ) ) {

				throw new Error( 'THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.' );

			}

			let framebuffer = properties.get( renderTarget ).__webglFramebuffer;
			if ( renderTarget.isWebGLCubeRenderTarget && activeCubeFaceIndex !== undefined ) {

				framebuffer = framebuffer[ activeCubeFaceIndex ];

			}

			if ( framebuffer ) {

				// the following if statement ensures valid read requests (no out-of-bounds pixels, see #8604)
				if ( ( x >= 0 && x <= ( renderTarget.width - width ) ) && ( y >= 0 && y <= ( renderTarget.height - height ) ) ) {

					// set the active frame buffer to the one we want to read
					state.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer );

					const texture = renderTarget.textures[ textureIndex ];
					const textureFormat = texture.format;
					const textureType = texture.type;

					if ( ! capabilities.textureFormatReadable( textureFormat ) ) {

						throw new Error( 'THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.' );

					}

					if ( ! capabilities.textureTypeReadable( textureType ) ) {

						throw new Error( 'THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.' );

					}

					const glBuffer = _gl.createBuffer();
					_gl.bindBuffer( _gl.PIXEL_PACK_BUFFER, glBuffer );
					_gl.bufferData( _gl.PIXEL_PACK_BUFFER, buffer.byteLength, _gl.STREAM_READ );

					// when using MRT, select the correct color buffer for the subsequent read command

					if ( renderTarget.textures.length > 1 ) _gl.readBuffer( _gl.COLOR_ATTACHMENT0 + textureIndex );

					_gl.readPixels( x, y, width, height, utils.convert( textureFormat ), utils.convert( textureType ), 0 );

					// reset the frame buffer to the currently set buffer before waiting
					const currFramebuffer = _currentRenderTarget !== null ? properties.get( _currentRenderTarget ).__webglFramebuffer : null;
					state.bindFramebuffer( _gl.FRAMEBUFFER, currFramebuffer );

					// check if the commands have finished every 8 ms
					const sync = _gl.fenceSync( _gl.SYNC_GPU_COMMANDS_COMPLETE, 0 );

					_gl.flush();

					await probeAsync( _gl, sync, 4 );

					// read the data and delete the buffer
					_gl.bindBuffer( _gl.PIXEL_PACK_BUFFER, glBuffer );
					_gl.getBufferSubData( _gl.PIXEL_PACK_BUFFER, 0, buffer );
					_gl.deleteBuffer( glBuffer );
					_gl.deleteSync( sync );

					return buffer;

				} else {

					throw new Error( 'THREE.WebGLRenderer.readRenderTargetPixelsAsync: requested read bounds are out of range.' );

				}

			}

		};

		/**
		 * Copies pixels from the current bound framebuffer into the given texture.
		 *
		 * @param {FramebufferTexture} texture - The texture.
		 * @param {?Vector2} [position=null] - The start position of the copy operation.
		 * @param {number} [level=0] - The mip level. The default represents the base mip.
		 */
		this.copyFramebufferToTexture = function ( texture, position = null, level = 0 ) {

			const levelScale = Math.pow( 2, - level );
			const width = Math.floor( texture.image.width * levelScale );
			const height = Math.floor( texture.image.height * levelScale );

			const x = position !== null ? position.x : 0;
			const y = position !== null ? position.y : 0;

			textures.setTexture2D( texture, 0 );

			_gl.copyTexSubImage2D( _gl.TEXTURE_2D, level, 0, 0, x, y, width, height );

			state.unbindTexture();

		};

		const _srcFramebuffer = _gl.createFramebuffer();
		const _dstFramebuffer = _gl.createFramebuffer();

		/**
		 * Copies data of the given source texture into a destination texture.
		 *
		 * When using render target textures as `srcTexture` and `dstTexture`, you must make sure both render targets are initialized
		 * {@link WebGLRenderer#initRenderTarget}.
		 *
		 * @param {Texture} srcTexture - The source texture.
		 * @param {Texture} dstTexture - The destination texture.
		 * @param {?(Box2|Box3)} [srcRegion=null] - A bounding box which describes the source region. Can be two or three-dimensional.
		 * @param {?(Vector2|Vector3)} [dstPosition=null] - A vector that represents the origin of the destination region. Can be two or three-dimensional.
		 * @param {number} [srcLevel=0] - The source mipmap level to copy.
		 * @param {?number} [dstLevel=null] - The destination mipmap level.
		 */
		this.copyTextureToTexture = function ( srcTexture, dstTexture, srcRegion = null, dstPosition = null, srcLevel = 0, dstLevel = null ) {

			// support the previous signature with just a single dst mipmap level
			if ( dstLevel === null ) {

				if ( srcLevel !== 0 ) {

					// @deprecated, r171
					warnOnce( 'WebGLRenderer: copyTextureToTexture function signature has changed to support src and dst mipmap levels.' );
					dstLevel = srcLevel;
					srcLevel = 0;

				} else {

					dstLevel = 0;

				}

			}

			// gather the necessary dimensions to copy
			let width, height, depth, minX, minY, minZ;
			let dstX, dstY, dstZ;
			const image = srcTexture.isCompressedTexture ? srcTexture.mipmaps[ dstLevel ] : srcTexture.image;
			if ( srcRegion !== null ) {

				width = srcRegion.max.x - srcRegion.min.x;
				height = srcRegion.max.y - srcRegion.min.y;
				depth = srcRegion.isBox3 ? srcRegion.max.z - srcRegion.min.z : 1;
				minX = srcRegion.min.x;
				minY = srcRegion.min.y;
				minZ = srcRegion.isBox3 ? srcRegion.min.z : 0;

			} else {

				const levelScale = Math.pow( 2, - srcLevel );
				width = Math.floor( image.width * levelScale );
				height = Math.floor( image.height * levelScale );
				if ( srcTexture.isDataArrayTexture ) {

					depth = image.depth;

				} else if ( srcTexture.isData3DTexture ) {

					depth = Math.floor( image.depth * levelScale );

				} else {

					depth = 1;

				}

				minX = 0;
				minY = 0;
				minZ = 0;

			}

			if ( dstPosition !== null ) {

				dstX = dstPosition.x;
				dstY = dstPosition.y;
				dstZ = dstPosition.z;

			} else {

				dstX = 0;
				dstY = 0;
				dstZ = 0;

			}

			// Set up the destination target
			const glFormat = utils.convert( dstTexture.format );
			const glType = utils.convert( dstTexture.type );
			let glTarget;

			if ( dstTexture.isData3DTexture ) {

				textures.setTexture3D( dstTexture, 0 );
				glTarget = _gl.TEXTURE_3D;

			} else if ( dstTexture.isDataArrayTexture || dstTexture.isCompressedArrayTexture ) {

				textures.setTexture2DArray( dstTexture, 0 );
				glTarget = _gl.TEXTURE_2D_ARRAY;

			} else {

				textures.setTexture2D( dstTexture, 0 );
				glTarget = _gl.TEXTURE_2D;

			}

			_gl.pixelStorei( _gl.UNPACK_FLIP_Y_WEBGL, dstTexture.flipY );
			_gl.pixelStorei( _gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, dstTexture.premultiplyAlpha );
			_gl.pixelStorei( _gl.UNPACK_ALIGNMENT, dstTexture.unpackAlignment );

			// used for copying data from cpu
			const currentUnpackRowLen = _gl.getParameter( _gl.UNPACK_ROW_LENGTH );
			const currentUnpackImageHeight = _gl.getParameter( _gl.UNPACK_IMAGE_HEIGHT );
			const currentUnpackSkipPixels = _gl.getParameter( _gl.UNPACK_SKIP_PIXELS );
			const currentUnpackSkipRows = _gl.getParameter( _gl.UNPACK_SKIP_ROWS );
			const currentUnpackSkipImages = _gl.getParameter( _gl.UNPACK_SKIP_IMAGES );

			_gl.pixelStorei( _gl.UNPACK_ROW_LENGTH, image.width );
			_gl.pixelStorei( _gl.UNPACK_IMAGE_HEIGHT, image.height );
			_gl.pixelStorei( _gl.UNPACK_SKIP_PIXELS, minX );
			_gl.pixelStorei( _gl.UNPACK_SKIP_ROWS, minY );
			_gl.pixelStorei( _gl.UNPACK_SKIP_IMAGES, minZ );

			// set up the src texture
			const isSrc3D = srcTexture.isDataArrayTexture || srcTexture.isData3DTexture;
			const isDst3D = dstTexture.isDataArrayTexture || dstTexture.isData3DTexture;
			if ( srcTexture.isDepthTexture ) {

				const srcTextureProperties = properties.get( srcTexture );
				const dstTextureProperties = properties.get( dstTexture );
				const srcRenderTargetProperties = properties.get( srcTextureProperties.__renderTarget );
				const dstRenderTargetProperties = properties.get( dstTextureProperties.__renderTarget );
				state.bindFramebuffer( _gl.READ_FRAMEBUFFER, srcRenderTargetProperties.__webglFramebuffer );
				state.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, dstRenderTargetProperties.__webglFramebuffer );

				for ( let i = 0; i < depth; i ++ ) {

					// if the source or destination are a 3d target then a layer needs to be bound
					if ( isSrc3D ) {

						_gl.framebufferTextureLayer( _gl.READ_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, properties.get( srcTexture ).__webglTexture, srcLevel, minZ + i );
						_gl.framebufferTextureLayer( _gl.DRAW_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, properties.get( dstTexture ).__webglTexture, dstLevel, dstZ + i );

					}

					_gl.blitFramebuffer( minX, minY, width, height, dstX, dstY, width, height, _gl.DEPTH_BUFFER_BIT, _gl.NEAREST );

				}

				state.bindFramebuffer( _gl.READ_FRAMEBUFFER, null );
				state.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, null );

			} else if ( srcLevel !== 0 || srcTexture.isRenderTargetTexture || properties.has( srcTexture ) ) {

				// get the appropriate frame buffers
				const srcTextureProperties = properties.get( srcTexture );
				const dstTextureProperties = properties.get( dstTexture );

				// bind the frame buffer targets
				state.bindFramebuffer( _gl.READ_FRAMEBUFFER, _srcFramebuffer );
				state.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, _dstFramebuffer );

				for ( let i = 0; i < depth; i ++ ) {

					// assign the correct layers and mip maps to the frame buffers
					if ( isSrc3D ) {

						_gl.framebufferTextureLayer( _gl.READ_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, srcTextureProperties.__webglTexture, srcLevel, minZ + i );

					} else {

						_gl.framebufferTexture2D( _gl.READ_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, srcTextureProperties.__webglTexture, srcLevel );

					}

					if ( isDst3D ) {

						_gl.framebufferTextureLayer( _gl.DRAW_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, dstTextureProperties.__webglTexture, dstLevel, dstZ + i );

					} else {

						_gl.framebufferTexture2D( _gl.DRAW_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, dstTextureProperties.__webglTexture, dstLevel );

					}

					// copy the data using the fastest function that can achieve the copy
					if ( srcLevel !== 0 ) {

						_gl.blitFramebuffer( minX, minY, width, height, dstX, dstY, width, height, _gl.COLOR_BUFFER_BIT, _gl.NEAREST );

					} else if ( isDst3D ) {

						_gl.copyTexSubImage3D( glTarget, dstLevel, dstX, dstY, dstZ + i, minX, minY, width, height );

					} else {

						_gl.copyTexSubImage2D( glTarget, dstLevel, dstX, dstY, minX, minY, width, height );

					}

				}

				// unbind read, draw buffers
				state.bindFramebuffer( _gl.READ_FRAMEBUFFER, null );
				state.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, null );

			} else {

				if ( isDst3D ) {

					// copy data into the 3d texture
					if ( srcTexture.isDataTexture || srcTexture.isData3DTexture ) {

						_gl.texSubImage3D( glTarget, dstLevel, dstX, dstY, dstZ, width, height, depth, glFormat, glType, image.data );

					} else if ( dstTexture.isCompressedArrayTexture ) {

						_gl.compressedTexSubImage3D( glTarget, dstLevel, dstX, dstY, dstZ, width, height, depth, glFormat, image.data );

					} else {

						_gl.texSubImage3D( glTarget, dstLevel, dstX, dstY, dstZ, width, height, depth, glFormat, glType, image );

					}

				} else {

					// copy data into the 2d texture
					if ( srcTexture.isDataTexture ) {

						_gl.texSubImage2D( _gl.TEXTURE_2D, dstLevel, dstX, dstY, width, height, glFormat, glType, image.data );

					} else if ( srcTexture.isCompressedTexture ) {

						_gl.compressedTexSubImage2D( _gl.TEXTURE_2D, dstLevel, dstX, dstY, image.width, image.height, glFormat, image.data );

					} else {

						_gl.texSubImage2D( _gl.TEXTURE_2D, dstLevel, dstX, dstY, width, height, glFormat, glType, image );

					}

				}

			}

			// reset values
			_gl.pixelStorei( _gl.UNPACK_ROW_LENGTH, currentUnpackRowLen );
			_gl.pixelStorei( _gl.UNPACK_IMAGE_HEIGHT, currentUnpackImageHeight );
			_gl.pixelStorei( _gl.UNPACK_SKIP_PIXELS, currentUnpackSkipPixels );
			_gl.pixelStorei( _gl.UNPACK_SKIP_ROWS, currentUnpackSkipRows );
			_gl.pixelStorei( _gl.UNPACK_SKIP_IMAGES, currentUnpackSkipImages );

			// Generate mipmaps only when copying level 0
			if ( dstLevel === 0 && dstTexture.generateMipmaps ) {

				_gl.generateMipmap( glTarget );

			}

			state.unbindTexture();

		};

		/**
		 * Initializes the given WebGLRenderTarget memory. Useful for initializing a render target so data
		 * can be copied into it using {@link WebGLRenderer#copyTextureToTexture} before it has been
		 * rendered to.
		 *
		 * @param {WebGLRenderTarget} target - The render target.
		 */
		this.initRenderTarget = function ( target ) {

			if ( properties.get( target ).__webglFramebuffer === undefined ) {

				textures.setupRenderTarget( target );

			}

		};

		/**
		 * Initializes the given texture. Useful for preloading a texture rather than waiting until first
		 * render (which can cause noticeable lags due to decode and GPU upload overhead).
		 *
		 * @param {Texture} texture - The texture.
		 */
		this.initTexture = function ( texture ) {

			if ( texture.isCubeTexture ) {

				textures.setTextureCube( texture, 0 );

			} else if ( texture.isData3DTexture ) {

				textures.setTexture3D( texture, 0 );

			} else if ( texture.isDataArrayTexture || texture.isCompressedArrayTexture ) {

				textures.setTexture2DArray( texture, 0 );

			} else {

				textures.setTexture2D( texture, 0 );

			}

			state.unbindTexture();

		};

		/**
		 * Can be used to reset the internal WebGL state. This method is mostly
		 * relevant for applications which share a single WebGL context across
		 * multiple WebGL libraries.
		 */
		this.resetState = function () {

			_currentActiveCubeFace = 0;
			_currentActiveMipmapLevel = 0;
			_currentRenderTarget = null;

			state.reset();
			bindingStates.reset();

		};

		if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {

			__THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'observe', { detail: this } ) );

		}

	}

	/**
	 * Defines the coordinate system of the renderer.
	 *
	 * In `WebGLRenderer`, the value is always `WebGLCoordinateSystem`.
	 *
	 * @type {WebGLCoordinateSystem|WebGPUCoordinateSystem}
	 * @default WebGLCoordinateSystem
	 * @readonly
	 */
	get coordinateSystem() {

		return WebGLCoordinateSystem;

	}

	/**
	 * Defines the output color space of the renderer.
	 *
	 * @type {SRGBColorSpace|LinearSRGBColorSpace}
	 * @default SRGBColorSpace
	 */
	get outputColorSpace() {

		return this._outputColorSpace;

	}

	set outputColorSpace( colorSpace ) {

		this._outputColorSpace = colorSpace;

		const gl = this.getContext();
		gl.drawingBufferColorSpace = ColorManagement._getDrawingBufferColorSpace( colorSpace );
		gl.unpackColorSpace = ColorManagement._getUnpackColorSpace();

	}

}

/**
 * This class can be used to define a linear fog that grows linearly denser
 * with the distance.
 *
 * ```js
 * const scene = new THREE.Scene();
 * scene.fog = new THREE.Fog( 0xcccccc, 10, 15 );
 * ```
 */
class Fog {

	/**
	 * Constructs a new fog.
	 *
	 * @param {number|Color} color - The fog's color.
	 * @param {number} [near=1] - The minimum distance to start applying fog.
	 * @param {number} [far=1000] - The maximum distance at which fog stops being calculated and applied.
	 */
	constructor( color, near = 1, far = 1000 ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isFog = true;

		/**
		 * The name of the fog.
		 *
		 * @type {string}
		 */
		this.name = '';

		/**
		 * The fog's color.
		 *
		 * @type {Color}
		 */
		this.color = new Color( color );

		/**
		 * The minimum distance to start applying fog. Objects that are less than
		 * `near` units from the active camera won't be affected by fog.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.near = near;

		/**
		 * The maximum distance at which fog stops being calculated and applied.
		 * Objects that are more than `far` units away from the active camera won't
		 * be affected by fog.
		 *
		 * @type {number}
		 * @default 1000
		 */
		this.far = far;

	}

	/**
	 * Returns a new fog with copied values from this instance.
	 *
	 * @return {Fog} A clone of this instance.
	 */
	clone() {

		return new Fog( this.color, this.near, this.far );

	}

	/**
	 * Serializes the fog into JSON.
	 *
	 * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.
	 * @return {Object} A JSON object representing the serialized fog
	 */
	toJSON( /* meta */ ) {

		return {
			type: 'Fog',
			name: this.name,
			color: this.color.getHex(),
			near: this.near,
			far: this.far
		};

	}

}

/**
 * Scenes allow you to set up what is to be rendered and where by three.js.
 * This is where you place 3D objects like meshes, lines or lights.
 *
 * @augments Object3D
 */
class Scene extends Object3D {

	/**
	 * Constructs a new scene.
	 */
	constructor() {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isScene = true;

		this.type = 'Scene';

		/**
		 * Defines the background of the scene. Valid inputs are:
		 *
		 * - A color for defining a uniform colored background.
		 * - A texture for defining a (flat) textured background.
		 * - Cube textures or equirectangular textures for defining a skybox.
		 *
		 * @type {?(Color|Texture)}
		 * @default null
		 */
		this.background = null;

		/**
		 * Sets the environment map for all physical materials in the scene. However,
		 * it's not possible to overwrite an existing texture assigned to the `envMap`
		 * material property.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.environment = null;

		/**
		 * A fog instance defining the type of fog that affects everything
		 * rendered in the scene.
		 *
		 * @type {?(Fog|FogExp2)}
		 * @default null
		 */
		this.fog = null;

		/**
		 * Sets the blurriness of the background. Only influences environment maps
		 * assigned to {@link Scene#background}. Valid input is a float between `0`
		 * and `1`.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.backgroundBlurriness = 0;

		/**
		 * Attenuates the color of the background. Only applies to background textures.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.backgroundIntensity = 1;

		/**
		 * The rotation of the background in radians. Only influences environment maps
		 * assigned to {@link Scene#background}.
		 *
		 * @type {Euler}
		 * @default (0,0,0)
		 */
		this.backgroundRotation = new Euler();

		/**
		 * Attenuates the color of the environment. Only influences environment maps
		 * assigned to {@link Scene#environment}.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.environmentIntensity = 1;

		/**
		 * The rotation of the environment map in radians. Only influences physical materials
		 * in the scene when {@link Scene#environment} is used.
		 *
		 * @type {Euler}
		 * @default (0,0,0)
		 */
		this.environmentRotation = new Euler();

		/**
		 * Forces everything in the scene to be rendered with the defined material. It is possible
		 * to exclude materials from override by setting {@link Material#allowOverride} to `false`.
		 *
		 * @type {?Material}
		 * @default null
		 */
		this.overrideMaterial = null;

		if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {

			__THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'observe', { detail: this } ) );

		}

	}

	copy( source, recursive ) {

		super.copy( source, recursive );

		if ( source.background !== null ) this.background = source.background.clone();
		if ( source.environment !== null ) this.environment = source.environment.clone();
		if ( source.fog !== null ) this.fog = source.fog.clone();

		this.backgroundBlurriness = source.backgroundBlurriness;
		this.backgroundIntensity = source.backgroundIntensity;
		this.backgroundRotation.copy( source.backgroundRotation );

		this.environmentIntensity = source.environmentIntensity;
		this.environmentRotation.copy( source.environmentRotation );

		if ( source.overrideMaterial !== null ) this.overrideMaterial = source.overrideMaterial.clone();

		this.matrixAutoUpdate = source.matrixAutoUpdate;

		return this;

	}

	toJSON( meta ) {

		const data = super.toJSON( meta );

		if ( this.fog !== null ) data.object.fog = this.fog.toJSON();

		if ( this.backgroundBlurriness > 0 ) data.object.backgroundBlurriness = this.backgroundBlurriness;
		if ( this.backgroundIntensity !== 1 ) data.object.backgroundIntensity = this.backgroundIntensity;
		data.object.backgroundRotation = this.backgroundRotation.toArray();

		if ( this.environmentIntensity !== 1 ) data.object.environmentIntensity = this.environmentIntensity;
		data.object.environmentRotation = this.environmentRotation.toArray();

		return data;

	}

}

/**
 * An instanced version of a buffer attribute.
 *
 * @augments BufferAttribute
 */
class InstancedBufferAttribute extends BufferAttribute {

	/**
	 * Constructs a new instanced buffer attribute.
	 *
	 * @param {TypedArray} array - The array holding the attribute data.
	 * @param {number} itemSize - The item size.
	 * @param {boolean} [normalized=false] - Whether the data are normalized or not.
	 * @param {number} [meshPerAttribute=1] - How often a value of this buffer attribute should be repeated.
	 */
	constructor( array, itemSize, normalized, meshPerAttribute = 1 ) {

		super( array, itemSize, normalized );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isInstancedBufferAttribute = true;

		/**
		 * Defines how often a value of this buffer attribute should be repeated. A
		 * value of one means that each value of the instanced attribute is used for
		 * a single instance. A value of two means that each value is used for two
		 * consecutive instances (and so on).
		 *
		 * @type {number}
		 * @default 1
		 */
		this.meshPerAttribute = meshPerAttribute;

	}

	copy( source ) {

		super.copy( source );

		this.meshPerAttribute = source.meshPerAttribute;

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		data.meshPerAttribute = this.meshPerAttribute;

		data.isInstancedBufferAttribute = true;

		return data;

	}

}

/**
 * Creates a texture directly from raw buffer data.
 *
 * The interpretation of the data depends on type and format: If the type is
 * `UnsignedByteType`, a `Uint8Array` will be useful for addressing the
 * texel data. If the format is `RGBAFormat`, data needs four values for
 * one texel; Red, Green, Blue and Alpha (typically the opacity).
 *
 * @augments Texture
 */
class DataTexture extends Texture {

	/**
	 * Constructs a new data texture.
	 *
	 * @param {?TypedArray} [data=null] - The buffer data.
	 * @param {number} [width=1] - The width of the texture.
	 * @param {number} [height=1] - The height of the texture.
	 * @param {number} [format=RGBAFormat] - The texture format.
	 * @param {number} [type=UnsignedByteType] - The texture type.
	 * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.
	 * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
	 * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
	 * @param {number} [magFilter=NearestFilter] - The mag filter value.
	 * @param {number} [minFilter=NearestFilter] - The min filter value.
	 * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
	 * @param {string} [colorSpace=NoColorSpace] - The color space.
	 */
	constructor( data = null, width = 1, height = 1, format, type, mapping, wrapS, wrapT, magFilter = NearestFilter, minFilter = NearestFilter, anisotropy, colorSpace ) {

		super( null, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, colorSpace );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isDataTexture = true;

		/**
		 * The image definition of a data texture.
		 *
		 * @type {{data:TypedArray,width:number,height:number}}
		 */
		this.image = { data: data, width: width, height: height };

		/**
		 * Whether to generate mipmaps (if possible) for a texture.
		 *
		 * Overwritten and set to `false` by default.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.generateMipmaps = false;

		/**
		 * If set to `true`, the texture is flipped along the vertical axis when
		 * uploaded to the GPU.
		 *
		 * Overwritten and set to `false` by default.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.flipY = false;

		/**
		 * Specifies the alignment requirements for the start of each pixel row in memory.
		 *
		 * Overwritten and set to `1` by default.
		 *
		 * @type {boolean}
		 * @default 1
		 */
		this.unpackAlignment = 1;

	}

}

const _instanceLocalMatrix = /*@__PURE__*/ new Matrix4();
const _instanceWorldMatrix = /*@__PURE__*/ new Matrix4();

const _instanceIntersects = [];

const _box3 = /*@__PURE__*/ new Box3();
const _identity = /*@__PURE__*/ new Matrix4();
const _mesh$1 = /*@__PURE__*/ new Mesh();
const _sphere$4 = /*@__PURE__*/ new Sphere();

/**
 * A special version of a mesh with instanced rendering support. Use
 * this class if you have to render a large number of objects with the same
 * geometry and material(s) but with different world transformations. The usage
 * of 'InstancedMesh' will help you to reduce the number of draw calls and thus
 * improve the overall rendering performance in your application.
 *
 * @augments Mesh
 */
class InstancedMesh extends Mesh {

	/**
	 * Constructs a new instanced mesh.
	 *
	 * @param {BufferGeometry} [geometry] - The mesh geometry.
	 * @param {Material|Array<Material>} [material] - The mesh material.
	 * @param {number} count - The number of instances.
	 */
	constructor( geometry, material, count ) {

		super( geometry, material );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isInstancedMesh = true;

		/**
		 * Represents the local transformation of all instances. You have to set its
		 * {@link BufferAttribute#needsUpdate} flag to true if you modify instanced data
		 * via {@link InstancedMesh#setMatrixAt}.
		 *
		 * @type {InstancedBufferAttribute}
		 */
		this.instanceMatrix = new InstancedBufferAttribute( new Float32Array( count * 16 ), 16 );

		/**
		 * Represents the color of all instances. You have to set its
		 * {@link BufferAttribute#needsUpdate} flag to true if you modify instanced data
		 * via {@link InstancedMesh#setColorAt}.
		 *
		 * @type {?InstancedBufferAttribute}
		 * @default null
		 */
		this.instanceColor = null;

		/**
		 * Represents the morph target weights of all instances. You have to set its
		 * {@link Texture#needsUpdate} flag to true if you modify instanced data
		 * via {@link InstancedMesh#setMorphAt}.
		 *
		 * @type {?DataTexture}
		 * @default null
		 */
		this.morphTexture = null;

		/**
		 * The number of instances.
		 *
		 * @type {number}
		 */
		this.count = count;

		/**
		 * The bounding box of the instanced mesh. Can be computed via {@link InstancedMesh#computeBoundingBox}.
		 *
		 * @type {?Box3}
		 * @default null
		 */
		this.boundingBox = null;

		/**
		 * The bounding sphere of the instanced mesh. Can be computed via {@link InstancedMesh#computeBoundingSphere}.
		 *
		 * @type {?Sphere}
		 * @default null
		 */
		this.boundingSphere = null;

		for ( let i = 0; i < count; i ++ ) {

			this.setMatrixAt( i, _identity );

		}

	}

	/**
	 * Computes the bounding box of the instanced mesh, and updates {@link InstancedMesh#boundingBox}.
	 * The bounding box is not automatically computed by the engine; this method must be called by your app.
	 * You may need to recompute the bounding box if an instance is transformed via {@link InstancedMesh#setMatrixAt}.
	 */
	computeBoundingBox() {

		const geometry = this.geometry;
		const count = this.count;

		if ( this.boundingBox === null ) {

			this.boundingBox = new Box3();

		}

		if ( geometry.boundingBox === null ) {

			geometry.computeBoundingBox();

		}

		this.boundingBox.makeEmpty();

		for ( let i = 0; i < count; i ++ ) {

			this.getMatrixAt( i, _instanceLocalMatrix );

			_box3.copy( geometry.boundingBox ).applyMatrix4( _instanceLocalMatrix );

			this.boundingBox.union( _box3 );

		}

	}

	/**
	 * Computes the bounding sphere of the instanced mesh, and updates {@link InstancedMesh#boundingSphere}
	 * The engine automatically computes the bounding sphere when it is needed, e.g., for ray casting or view frustum culling.
	 * You may need to recompute the bounding sphere if an instance is transformed via {@link InstancedMesh#setMatrixAt}.
	 */
	computeBoundingSphere() {

		const geometry = this.geometry;
		const count = this.count;

		if ( this.boundingSphere === null ) {

			this.boundingSphere = new Sphere();

		}

		if ( geometry.boundingSphere === null ) {

			geometry.computeBoundingSphere();

		}

		this.boundingSphere.makeEmpty();

		for ( let i = 0; i < count; i ++ ) {

			this.getMatrixAt( i, _instanceLocalMatrix );

			_sphere$4.copy( geometry.boundingSphere ).applyMatrix4( _instanceLocalMatrix );

			this.boundingSphere.union( _sphere$4 );

		}

	}

	copy( source, recursive ) {

		super.copy( source, recursive );

		this.instanceMatrix.copy( source.instanceMatrix );

		if ( source.morphTexture !== null ) this.morphTexture = source.morphTexture.clone();
		if ( source.instanceColor !== null ) this.instanceColor = source.instanceColor.clone();

		this.count = source.count;

		if ( source.boundingBox !== null ) this.boundingBox = source.boundingBox.clone();
		if ( source.boundingSphere !== null ) this.boundingSphere = source.boundingSphere.clone();

		return this;

	}

	/**
	 * Gets the color of the defined instance.
	 *
	 * @param {number} index - The instance index.
	 * @param {Color} color - The target object that is used to store the method's result.
	 */
	getColorAt( index, color ) {

		color.fromArray( this.instanceColor.array, index * 3 );

	}

	/**
	 * Gets the local transformation matrix of the defined instance.
	 *
	 * @param {number} index - The instance index.
	 * @param {Matrix4} matrix - The target object that is used to store the method's result.
	 */
	getMatrixAt( index, matrix ) {

		matrix.fromArray( this.instanceMatrix.array, index * 16 );

	}

	/**
	 * Gets the morph target weights of the defined instance.
	 *
	 * @param {number} index - The instance index.
	 * @param {Mesh} object - The target object that is used to store the method's result.
	 */
	getMorphAt( index, object ) {

		const objectInfluences = object.morphTargetInfluences;

		const array = this.morphTexture.source.data.data;

		const len = objectInfluences.length + 1; // All influences + the baseInfluenceSum

		const dataIndex = index * len + 1; // Skip the baseInfluenceSum at the beginning

		for ( let i = 0; i < objectInfluences.length; i ++ ) {

			objectInfluences[ i ] = array[ dataIndex + i ];

		}

	}

	raycast( raycaster, intersects ) {

		const matrixWorld = this.matrixWorld;
		const raycastTimes = this.count;

		_mesh$1.geometry = this.geometry;
		_mesh$1.material = this.material;

		if ( _mesh$1.material === undefined ) return;

		// test with bounding sphere first

		if ( this.boundingSphere === null ) this.computeBoundingSphere();

		_sphere$4.copy( this.boundingSphere );
		_sphere$4.applyMatrix4( matrixWorld );

		if ( raycaster.ray.intersectsSphere( _sphere$4 ) === false ) return;

		// now test each instance

		for ( let instanceId = 0; instanceId < raycastTimes; instanceId ++ ) {

			// calculate the world matrix for each instance

			this.getMatrixAt( instanceId, _instanceLocalMatrix );

			_instanceWorldMatrix.multiplyMatrices( matrixWorld, _instanceLocalMatrix );

			// the mesh represents this single instance

			_mesh$1.matrixWorld = _instanceWorldMatrix;

			_mesh$1.raycast( raycaster, _instanceIntersects );

			// process the result of raycast

			for ( let i = 0, l = _instanceIntersects.length; i < l; i ++ ) {

				const intersect = _instanceIntersects[ i ];
				intersect.instanceId = instanceId;
				intersect.object = this;
				intersects.push( intersect );

			}

			_instanceIntersects.length = 0;

		}

	}

	/**
	 * Sets the given color to the defined instance. Make sure you set the `needsUpdate` flag of
	 * {@link InstancedMesh#instanceColor} to `true` after updating all the colors.
	 *
	 * @param {number} index - The instance index.
	 * @param {Color} color - The instance color.
	 */
	setColorAt( index, color ) {

		if ( this.instanceColor === null ) {

			this.instanceColor = new InstancedBufferAttribute( new Float32Array( this.instanceMatrix.count * 3 ).fill( 1 ), 3 );

		}

		color.toArray( this.instanceColor.array, index * 3 );

	}

	/**
	 * Sets the given local transformation matrix to the defined instance. Make sure you set the `needsUpdate` flag of
	 * {@link InstancedMesh#instanceMatrix} to `true` after updating all the colors.
	 *
	 * @param {number} index - The instance index.
	 * @param {Matrix4} matrix - The local transformation.
	 */
	setMatrixAt( index, matrix ) {

		matrix.toArray( this.instanceMatrix.array, index * 16 );

	}

	/**
	 * Sets the morph target weights to the defined instance. Make sure you set the `needsUpdate` flag of
	 * {@link InstancedMesh#morphTexture} to `true` after updating all the influences.
	 *
	 * @param {number} index - The instance index.
	 * @param {Mesh} object -  A mesh which `morphTargetInfluences` property containing the morph target weights
	 * of a single instance.
	 */
	setMorphAt( index, object ) {

		const objectInfluences = object.morphTargetInfluences;

		const len = objectInfluences.length + 1; // morphBaseInfluence + all influences

		if ( this.morphTexture === null ) {

			this.morphTexture = new DataTexture( new Float32Array( len * this.count ), len, this.count, RedFormat, FloatType );

		}

		const array = this.morphTexture.source.data.data;

		let morphInfluencesSum = 0;

		for ( let i = 0; i < objectInfluences.length; i ++ ) {

			morphInfluencesSum += objectInfluences[ i ];

		}

		const morphBaseInfluence = this.geometry.morphTargetsRelative ? 1 : 1 - morphInfluencesSum;

		const dataIndex = len * index;

		array[ dataIndex ] = morphBaseInfluence;

		array.set( objectInfluences, dataIndex + 1 );

	}

	updateMorphTargets() {

	}

	/**
	 * Frees the GPU-related resources allocated by this instance. Call this
	 * method whenever this instance is no longer used in your app.
	 */
	dispose() {

		this.dispatchEvent( { type: 'dispose' } );

		if ( this.morphTexture !== null ) {

			this.morphTexture.dispose();
			this.morphTexture = null;

		}

	}

}

/**
 * A material for rendering line primitives.
 *
 * Materials define the appearance of renderable 3D objects.
 *
 * ```js
 * const material = new THREE.LineBasicMaterial( { color: 0xffffff } );
 * ```
 *
 * @augments Material
 */
class LineBasicMaterial extends Material {

	/**
	 * Constructs a new line basic material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isLineBasicMaterial = true;

		this.type = 'LineBasicMaterial';

		/**
		 * Color of the material.
		 *
		 * @type {Color}
		 * @default (1,1,1)
		 */
		this.color = new Color( 0xffffff );

		/**
		 * Sets the color of the lines using data from a texture. The texture map
		 * color is modulated by the diffuse `color`.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.map = null;

		/**
		 * Controls line thickness or lines.
		 *
		 * Can only be used with {@link SVGRenderer}. WebGL and WebGPU
		 * ignore this setting and always render line primitives with a
		 * width of one pixel.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.linewidth = 1;

		/**
		 * Defines appearance of line ends.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {('butt'|'round'|'square')}
		 * @default 'round'
		 */
		this.linecap = 'round';

		/**
		 * Defines appearance of line joints.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {('round'|'bevel'|'miter')}
		 * @default 'round'
		 */
		this.linejoin = 'round';

		/**
		 * Whether the material is affected by fog or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.fog = true;

		this.setValues( parameters );

	}

	copy( source ) {

		super.copy( source );

		this.color.copy( source.color );

		this.map = source.map;

		this.linewidth = source.linewidth;
		this.linecap = source.linecap;
		this.linejoin = source.linejoin;

		this.fog = source.fog;

		return this;

	}

}

const _vStart = /*@__PURE__*/ new Vector3();
const _vEnd = /*@__PURE__*/ new Vector3();

const _inverseMatrix$2 = /*@__PURE__*/ new Matrix4();
const _ray$2 = /*@__PURE__*/ new Ray();
const _sphere$3 = /*@__PURE__*/ new Sphere();

const _intersectPointOnRay = /*@__PURE__*/ new Vector3();
const _intersectPointOnSegment = /*@__PURE__*/ new Vector3();

/**
 * A continuous line. The line are rendered by connecting consecutive
 * vertices with straight lines.
 *
 * ```js
 * const material = new THREE.LineBasicMaterial( { color: 0x0000ff } );
 *
 * const points = [];
 * points.push( new THREE.Vector3( - 10, 0, 0 ) );
 * points.push( new THREE.Vector3( 0, 10, 0 ) );
 * points.push( new THREE.Vector3( 10, 0, 0 ) );
 *
 * const geometry = new THREE.BufferGeometry().setFromPoints( points );
 *
 * const line = new THREE.Line( geometry, material );
 * scene.add( line );
 * ```
 *
 * @augments Object3D
 */
let Line$1 = class Line extends Object3D {

	/**
	 * Constructs a new line.
	 *
	 * @param {BufferGeometry} [geometry] - The line geometry.
	 * @param {Material|Array<Material>} [material] - The line material.
	 */
	constructor( geometry = new BufferGeometry(), material = new LineBasicMaterial() ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isLine = true;

		this.type = 'Line';

		/**
		 * The line geometry.
		 *
		 * @type {BufferGeometry}
		 */
		this.geometry = geometry;

		/**
		 * The line material.
		 *
		 * @type {Material|Array<Material>}
		 * @default LineBasicMaterial
		 */
		this.material = material;

		/**
		 * A dictionary representing the morph targets in the geometry. The key is the
		 * morph targets name, the value its attribute index. This member is `undefined`
		 * by default and only set when morph targets are detected in the geometry.
		 *
		 * @type {Object<String,number>|undefined}
		 * @default undefined
		 */
		this.morphTargetDictionary = undefined;

		/**
		 * An array of weights typically in the range `[0,1]` that specify how much of the morph
		 * is applied. This member is `undefined` by default and only set when morph targets are
		 * detected in the geometry.
		 *
		 * @type {Array<number>|undefined}
		 * @default undefined
		 */
		this.morphTargetInfluences = undefined;

		this.updateMorphTargets();

	}

	copy( source, recursive ) {

		super.copy( source, recursive );

		this.material = Array.isArray( source.material ) ? source.material.slice() : source.material;
		this.geometry = source.geometry;

		return this;

	}

	/**
	 * Computes an array of distance values which are necessary for rendering dashed lines.
	 * For each vertex in the geometry, the method calculates the cumulative length from the
	 * current point to the very beginning of the line.
	 *
	 * @return {Line} A reference to this line.
	 */
	computeLineDistances() {

		const geometry = this.geometry;

		// we assume non-indexed geometry

		if ( geometry.index === null ) {

			const positionAttribute = geometry.attributes.position;
			const lineDistances = [ 0 ];

			for ( let i = 1, l = positionAttribute.count; i < l; i ++ ) {

				_vStart.fromBufferAttribute( positionAttribute, i - 1 );
				_vEnd.fromBufferAttribute( positionAttribute, i );

				lineDistances[ i ] = lineDistances[ i - 1 ];
				lineDistances[ i ] += _vStart.distanceTo( _vEnd );

			}

			geometry.setAttribute( 'lineDistance', new Float32BufferAttribute( lineDistances, 1 ) );

		} else {

			console.warn( 'THREE.Line.computeLineDistances(): Computation only possible with non-indexed BufferGeometry.' );

		}

		return this;

	}

	/**
	 * Computes intersection points between a casted ray and this line.
	 *
	 * @param {Raycaster} raycaster - The raycaster.
	 * @param {Array<Object>} intersects - The target array that holds the intersection points.
	 */
	raycast( raycaster, intersects ) {

		const geometry = this.geometry;
		const matrixWorld = this.matrixWorld;
		const threshold = raycaster.params.Line.threshold;
		const drawRange = geometry.drawRange;

		// Checking boundingSphere distance to ray

		if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();

		_sphere$3.copy( geometry.boundingSphere );
		_sphere$3.applyMatrix4( matrixWorld );
		_sphere$3.radius += threshold;

		if ( raycaster.ray.intersectsSphere( _sphere$3 ) === false ) return;

		//

		_inverseMatrix$2.copy( matrixWorld ).invert();
		_ray$2.copy( raycaster.ray ).applyMatrix4( _inverseMatrix$2 );

		const localThreshold = threshold / ( ( this.scale.x + this.scale.y + this.scale.z ) / 3 );
		const localThresholdSq = localThreshold * localThreshold;

		const step = this.isLineSegments ? 2 : 1;

		const index = geometry.index;
		const attributes = geometry.attributes;
		const positionAttribute = attributes.position;

		if ( index !== null ) {

			const start = Math.max( 0, drawRange.start );
			const end = Math.min( index.count, ( drawRange.start + drawRange.count ) );

			for ( let i = start, l = end - 1; i < l; i += step ) {

				const a = index.getX( i );
				const b = index.getX( i + 1 );

				const intersect = checkIntersection( this, raycaster, _ray$2, localThresholdSq, a, b, i );

				if ( intersect ) {

					intersects.push( intersect );

				}

			}

			if ( this.isLineLoop ) {

				const a = index.getX( end - 1 );
				const b = index.getX( start );

				const intersect = checkIntersection( this, raycaster, _ray$2, localThresholdSq, a, b, end - 1 );

				if ( intersect ) {

					intersects.push( intersect );

				}

			}

		} else {

			const start = Math.max( 0, drawRange.start );
			const end = Math.min( positionAttribute.count, ( drawRange.start + drawRange.count ) );

			for ( let i = start, l = end - 1; i < l; i += step ) {

				const intersect = checkIntersection( this, raycaster, _ray$2, localThresholdSq, i, i + 1, i );

				if ( intersect ) {

					intersects.push( intersect );

				}

			}

			if ( this.isLineLoop ) {

				const intersect = checkIntersection( this, raycaster, _ray$2, localThresholdSq, end - 1, start, end - 1 );

				if ( intersect ) {

					intersects.push( intersect );

				}

			}

		}

	}

	/**
	 * Sets the values of {@link Line#morphTargetDictionary} and {@link Line#morphTargetInfluences}
	 * to make sure existing morph targets can influence this 3D object.
	 */
	updateMorphTargets() {

		const geometry = this.geometry;

		const morphAttributes = geometry.morphAttributes;
		const keys = Object.keys( morphAttributes );

		if ( keys.length > 0 ) {

			const morphAttribute = morphAttributes[ keys[ 0 ] ];

			if ( morphAttribute !== undefined ) {

				this.morphTargetInfluences = [];
				this.morphTargetDictionary = {};

				for ( let m = 0, ml = morphAttribute.length; m < ml; m ++ ) {

					const name = morphAttribute[ m ].name || String( m );

					this.morphTargetInfluences.push( 0 );
					this.morphTargetDictionary[ name ] = m;

				}

			}

		}

	}

};

function checkIntersection( object, raycaster, ray, thresholdSq, a, b, i ) {

	const positionAttribute = object.geometry.attributes.position;

	_vStart.fromBufferAttribute( positionAttribute, a );
	_vEnd.fromBufferAttribute( positionAttribute, b );

	const distSq = ray.distanceSqToSegment( _vStart, _vEnd, _intersectPointOnRay, _intersectPointOnSegment );

	if ( distSq > thresholdSq ) return;

	_intersectPointOnRay.applyMatrix4( object.matrixWorld ); // Move back to world space for distance calculation

	const distance = raycaster.ray.origin.distanceTo( _intersectPointOnRay );

	if ( distance < raycaster.near || distance > raycaster.far ) return;

	return {

		distance: distance,
		// What do we want? intersection point on the ray or on the segment??
		// point: raycaster.ray.at( distance ),
		point: _intersectPointOnSegment.clone().applyMatrix4( object.matrixWorld ),
		index: i,
		face: null,
		faceIndex: null,
		barycoord: null,
		object: object

	};

}

const _start = /*@__PURE__*/ new Vector3();
const _end = /*@__PURE__*/ new Vector3();

/**
 * A series of lines drawn between pairs of vertices.
 *
 * @augments Line
 */
class LineSegments extends Line$1 {

	/**
	 * Constructs a new line segments.
	 *
	 * @param {BufferGeometry} [geometry] - The line geometry.
	 * @param {Material|Array<Material>} [material] - The line material.
	 */
	constructor( geometry, material ) {

		super( geometry, material );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isLineSegments = true;

		this.type = 'LineSegments';

	}

	computeLineDistances() {

		const geometry = this.geometry;

		// we assume non-indexed geometry

		if ( geometry.index === null ) {

			const positionAttribute = geometry.attributes.position;
			const lineDistances = [];

			for ( let i = 0, l = positionAttribute.count; i < l; i += 2 ) {

				_start.fromBufferAttribute( positionAttribute, i );
				_end.fromBufferAttribute( positionAttribute, i + 1 );

				lineDistances[ i ] = ( i === 0 ) ? 0 : lineDistances[ i - 1 ];
				lineDistances[ i + 1 ] = lineDistances[ i ] + _start.distanceTo( _end );

			}

			geometry.setAttribute( 'lineDistance', new Float32BufferAttribute( lineDistances, 1 ) );

		} else {

			console.warn( 'THREE.LineSegments.computeLineDistances(): Computation only possible with non-indexed BufferGeometry.' );

		}

		return this;

	}

}

/**
 * A material for rendering point primitives.
 *
 * Materials define the appearance of renderable 3D objects.
 *
 * ```js
 * const vertices = [];
 *
 * for ( let i = 0; i < 10000; i ++ ) {
 * 	const x = THREE.MathUtils.randFloatSpread( 2000 );
 * 	const y = THREE.MathUtils.randFloatSpread( 2000 );
 * 	const z = THREE.MathUtils.randFloatSpread( 2000 );
 *
 * 	vertices.push( x, y, z );
 * }
 *
 * const geometry = new THREE.BufferGeometry();
 * geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
 * const material = new THREE.PointsMaterial( { color: 0x888888 } );
 * const points = new THREE.Points( geometry, material );
 * scene.add( points );
 * ```
 *
 * @augments Material
 */
class PointsMaterial extends Material {

	/**
	 * Constructs a new points material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isPointsMaterial = true;

		this.type = 'PointsMaterial';

		/**
		 * Color of the material.
		 *
		 * @type {Color}
		 * @default (1,1,1)
		 */
		this.color = new Color( 0xffffff );

		/**
		 * The color map. May optionally include an alpha channel, typically combined
		 * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
		 * color is modulated by the diffuse `color`.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.map = null;

		/**
		 * The alpha map is a grayscale texture that controls the opacity across the
		 * surface (black: fully transparent; white: fully opaque).
		 *
		 * Only the color of the texture is used, ignoring the alpha channel if one
		 * exists. For RGB and RGBA textures, the renderer will use the green channel
		 * when sampling this texture due to the extra bit of precision provided for
		 * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
		 * luminance/alpha textures will also still work as expected.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.alphaMap = null;

		/**
		 * Defines the size of the points in pixels.
		 *
		 * Might be capped if the value exceeds hardware dependent parameters like [gl.ALIASED_POINT_SIZE_RANGE]{@link https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/getParamete}.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.size = 1;

		/**
		 * Specifies whether size of individual points is attenuated by the camera depth (perspective camera only).
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.sizeAttenuation = true;

		/**
		 * Whether the material is affected by fog or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.fog = true;

		this.setValues( parameters );

	}

	copy( source ) {

		super.copy( source );

		this.color.copy( source.color );

		this.map = source.map;

		this.alphaMap = source.alphaMap;

		this.size = source.size;
		this.sizeAttenuation = source.sizeAttenuation;

		this.fog = source.fog;

		return this;

	}

}

const _inverseMatrix$1 = /*@__PURE__*/ new Matrix4();
const _ray$1 = /*@__PURE__*/ new Ray();
const _sphere$2 = /*@__PURE__*/ new Sphere();
const _position = /*@__PURE__*/ new Vector3();

/**
 * A class for displaying points or point clouds.
 *
 * @augments Object3D
 */
class Points extends Object3D {

	/**
	 * Constructs a new point cloud.
	 *
	 * @param {BufferGeometry} [geometry] - The points geometry.
	 * @param {Material|Array<Material>} [material] - The points material.
	 */
	constructor( geometry = new BufferGeometry(), material = new PointsMaterial() ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isPoints = true;

		this.type = 'Points';

		/**
		 * The points geometry.
		 *
		 * @type {BufferGeometry}
		 */
		this.geometry = geometry;

		/**
		 * The line material.
		 *
		 * @type {Material|Array<Material>}
		 * @default PointsMaterial
		 */
		this.material = material;

		/**
		 * A dictionary representing the morph targets in the geometry. The key is the
		 * morph targets name, the value its attribute index. This member is `undefined`
		 * by default and only set when morph targets are detected in the geometry.
		 *
		 * @type {Object<String,number>|undefined}
		 * @default undefined
		 */
		this.morphTargetDictionary = undefined;

		/**
		 * An array of weights typically in the range `[0,1]` that specify how much of the morph
		 * is applied. This member is `undefined` by default and only set when morph targets are
		 * detected in the geometry.
		 *
		 * @type {Array<number>|undefined}
		 * @default undefined
		 */
		this.morphTargetInfluences = undefined;

		this.updateMorphTargets();

	}

	copy( source, recursive ) {

		super.copy( source, recursive );

		this.material = Array.isArray( source.material ) ? source.material.slice() : source.material;
		this.geometry = source.geometry;

		return this;

	}

	/**
	 * Computes intersection points between a casted ray and this point cloud.
	 *
	 * @param {Raycaster} raycaster - The raycaster.
	 * @param {Array<Object>} intersects - The target array that holds the intersection points.
	 */
	raycast( raycaster, intersects ) {

		const geometry = this.geometry;
		const matrixWorld = this.matrixWorld;
		const threshold = raycaster.params.Points.threshold;
		const drawRange = geometry.drawRange;

		// Checking boundingSphere distance to ray

		if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();

		_sphere$2.copy( geometry.boundingSphere );
		_sphere$2.applyMatrix4( matrixWorld );
		_sphere$2.radius += threshold;

		if ( raycaster.ray.intersectsSphere( _sphere$2 ) === false ) return;

		//

		_inverseMatrix$1.copy( matrixWorld ).invert();
		_ray$1.copy( raycaster.ray ).applyMatrix4( _inverseMatrix$1 );

		const localThreshold = threshold / ( ( this.scale.x + this.scale.y + this.scale.z ) / 3 );
		const localThresholdSq = localThreshold * localThreshold;

		const index = geometry.index;
		const attributes = geometry.attributes;
		const positionAttribute = attributes.position;

		if ( index !== null ) {

			const start = Math.max( 0, drawRange.start );
			const end = Math.min( index.count, ( drawRange.start + drawRange.count ) );

			for ( let i = start, il = end; i < il; i ++ ) {

				const a = index.getX( i );

				_position.fromBufferAttribute( positionAttribute, a );

				testPoint( _position, a, localThresholdSq, matrixWorld, raycaster, intersects, this );

			}

		} else {

			const start = Math.max( 0, drawRange.start );
			const end = Math.min( positionAttribute.count, ( drawRange.start + drawRange.count ) );

			for ( let i = start, l = end; i < l; i ++ ) {

				_position.fromBufferAttribute( positionAttribute, i );

				testPoint( _position, i, localThresholdSq, matrixWorld, raycaster, intersects, this );

			}

		}

	}

	/**
	 * Sets the values of {@link Points#morphTargetDictionary} and {@link Points#morphTargetInfluences}
	 * to make sure existing morph targets can influence this 3D object.
	 */
	updateMorphTargets() {

		const geometry = this.geometry;

		const morphAttributes = geometry.morphAttributes;
		const keys = Object.keys( morphAttributes );

		if ( keys.length > 0 ) {

			const morphAttribute = morphAttributes[ keys[ 0 ] ];

			if ( morphAttribute !== undefined ) {

				this.morphTargetInfluences = [];
				this.morphTargetDictionary = {};

				for ( let m = 0, ml = morphAttribute.length; m < ml; m ++ ) {

					const name = morphAttribute[ m ].name || String( m );

					this.morphTargetInfluences.push( 0 );
					this.morphTargetDictionary[ name ] = m;

				}

			}

		}

	}

}

function testPoint( point, index, localThresholdSq, matrixWorld, raycaster, intersects, object ) {

	const rayPointDistanceSq = _ray$1.distanceSqToPoint( point );

	if ( rayPointDistanceSq < localThresholdSq ) {

		const intersectPoint = new Vector3();

		_ray$1.closestPointToPoint( point, intersectPoint );
		intersectPoint.applyMatrix4( matrixWorld );

		const distance = raycaster.ray.origin.distanceTo( intersectPoint );

		if ( distance < raycaster.near || distance > raycaster.far ) return;

		intersects.push( {

			distance: distance,
			distanceToRay: Math.sqrt( rayPointDistanceSq ),
			point: intersectPoint,
			index: index,
			face: null,
			faceIndex: null,
			barycoord: null,
			object: object

		} );

	}

}

/**
 * Creates a texture from a canvas element.
 *
 * This is almost the same as the base texture class, except that it sets {@link Texture#needsUpdate}
 * to `true` immediately since a canvas can directly be used for rendering.
 *
 * @augments Texture
 */
class CanvasTexture extends Texture {

	/**
	 * Constructs a new texture.
	 *
	 * @param {HTMLCanvasElement} [canvas] - The HTML canvas element.
	 * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.
	 * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
	 * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
	 * @param {number} [magFilter=LinearFilter] - The mag filter value.
	 * @param {number} [minFilter=LinearMipmapLinearFilter] - The min filter value.
	 * @param {number} [format=RGBAFormat] - The texture format.
	 * @param {number} [type=UnsignedByteType] - The texture type.
	 * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
	 */
	constructor( canvas, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) {

		super( canvas, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isCanvasTexture = true;

		this.needsUpdate = true;

	}

}

/**
 * A simple shape of Euclidean geometry. It is constructed from a
 * number of triangular segments that are oriented around a central point and
 * extend as far out as a given radius. It is built counter-clockwise from a
 * start angle and a given central angle. It can also be used to create
 * regular polygons, where the number of segments determines the number of
 * sides.
 *
 * ```js
 * const geometry = new THREE.CircleGeometry( 5, 32 );
 * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
 * const circle = new THREE.Mesh( geometry, material );
 * scene.add( circle )
 * ```
 *
 * @augments BufferGeometry
 */
class CircleGeometry extends BufferGeometry {

	/**
	 * Constructs a new circle geometry.
	 *
	 * @param {number} [radius=1] - Radius of the circle.
	 * @param {number} [segments=32] - Number of segments (triangles), minimum = `3`.
	 * @param {number} [thetaStart=0] - Start angle for first segment in radians.
	 * @param {number} [thetaLength=Math.PI*2] - The central angle, often called theta,
	 * of the circular sector in radians. The default value results in a complete circle.
	 */
	constructor( radius = 1, segments = 32, thetaStart = 0, thetaLength = Math.PI * 2 ) {

		super();

		this.type = 'CircleGeometry';

		/**
		 * Holds the constructor parameters that have been
		 * used to generate the geometry. Any modification
		 * after instantiation does not change the geometry.
		 *
		 * @type {Object}
		 */
		this.parameters = {
			radius: radius,
			segments: segments,
			thetaStart: thetaStart,
			thetaLength: thetaLength
		};

		segments = Math.max( 3, segments );

		// buffers

		const indices = [];
		const vertices = [];
		const normals = [];
		const uvs = [];

		// helper variables

		const vertex = new Vector3();
		const uv = new Vector2();

		// center point

		vertices.push( 0, 0, 0 );
		normals.push( 0, 0, 1 );
		uvs.push( 0.5, 0.5 );

		for ( let s = 0, i = 3; s <= segments; s ++, i += 3 ) {

			const segment = thetaStart + s / segments * thetaLength;

			// vertex

			vertex.x = radius * Math.cos( segment );
			vertex.y = radius * Math.sin( segment );

			vertices.push( vertex.x, vertex.y, vertex.z );

			// normal

			normals.push( 0, 0, 1 );

			// uvs

			uv.x = ( vertices[ i ] / radius + 1 ) / 2;
			uv.y = ( vertices[ i + 1 ] / radius + 1 ) / 2;

			uvs.push( uv.x, uv.y );

		}

		// indices

		for ( let i = 1; i <= segments; i ++ ) {

			indices.push( i, i + 1, 0 );

		}

		// build geometry

		this.setIndex( indices );
		this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
		this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
		this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

	}

	copy( source ) {

		super.copy( source );

		this.parameters = Object.assign( {}, source.parameters );

		return this;

	}

	/**
	 * Factory method for creating an instance of this class from the given
	 * JSON object.
	 *
	 * @param {Object} data - A JSON object representing the serialized geometry.
	 * @return {CircleGeometry} A new instance.
	 */
	static fromJSON( data ) {

		return new CircleGeometry( data.radius, data.segments, data.thetaStart, data.thetaLength );

	}

}

/**
 * An abstract base class for creating an analytic curve object that contains methods
 * for interpolation.
 *
 * @abstract
 */
class Curve {

	/**
	 * Constructs a new curve.
	 */
	constructor() {

		/**
		 * The type property is used for detecting the object type
		 * in context of serialization/deserialization.
		 *
		 * @type {string}
		 * @readonly
		 */
		this.type = 'Curve';

		/**
		 * This value determines the amount of divisions when calculating the
		 * cumulative segment lengths of a curve via {@link Curve#getLengths}. To ensure
		 * precision when using methods like {@link Curve#getSpacedPoints}, it is
		 * recommended to increase the value of this property if the curve is very large.
		 *
		 * @type {number}
		 * @default 200
		 */
		this.arcLengthDivisions = 200;

		/**
		 * Must be set to `true` if the curve parameters have changed.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.needsUpdate = false;

		/**
		 * An internal cache that holds precomputed curve length values.
		 *
		 * @private
		 * @type {?Array<number>}
		 * @default null
		 */
		this.cacheArcLengths = null;

	}

	/**
	 * This method returns a vector in 2D or 3D space (depending on the curve definition)
	 * for the given interpolation factor.
	 *
	 * @abstract
	 * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
	 * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.
	 * @return {(Vector2|Vector3)} The position on the curve. It can be a 2D or 3D vector depending on the curve definition.
	 */
	getPoint( /* t, optionalTarget */ ) {

		console.warn( 'THREE.Curve: .getPoint() not implemented.' );

	}

	/**
	 * This method returns a vector in 2D or 3D space (depending on the curve definition)
	 * for the given interpolation factor. Unlike {@link Curve#getPoint}, this method honors the length
	 * of the curve which equidistant samples.
	 *
	 * @param {number} u - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
	 * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.
	 * @return {(Vector2|Vector3)} The position on the curve. It can be a 2D or 3D vector depending on the curve definition.
	 */
	getPointAt( u, optionalTarget ) {

		const t = this.getUtoTmapping( u );
		return this.getPoint( t, optionalTarget );

	}

	/**
	 * This method samples the curve via {@link Curve#getPoint} and returns an array of points representing
	 * the curve shape.
	 *
	 * @param {number} [divisions=5] - The number of divisions.
	 * @return {Array<(Vector2|Vector3)>} An array holding the sampled curve values. The number of points is `divisions + 1`.
	 */
	getPoints( divisions = 5 ) {

		const points = [];

		for ( let d = 0; d <= divisions; d ++ ) {

			points.push( this.getPoint( d / divisions ) );

		}

		return points;

	}

	// Get sequence of points using getPointAt( u )

	/**
	 * This method samples the curve via {@link Curve#getPointAt} and returns an array of points representing
	 * the curve shape. Unlike {@link Curve#getPoints}, this method returns equi-spaced points across the entire
	 * curve.
	 *
	 * @param {number} [divisions=5] - The number of divisions.
	 * @return {Array<(Vector2|Vector3)>} An array holding the sampled curve values. The number of points is `divisions + 1`.
	 */
	getSpacedPoints( divisions = 5 ) {

		const points = [];

		for ( let d = 0; d <= divisions; d ++ ) {

			points.push( this.getPointAt( d / divisions ) );

		}

		return points;

	}

	/**
	 * Returns the total arc length of the curve.
	 *
	 * @return {number} The length of the curve.
	 */
	getLength() {

		const lengths = this.getLengths();
		return lengths[ lengths.length - 1 ];

	}

	/**
	 * Returns an array of cumulative segment lengths of the curve.
	 *
	 * @param {number} [divisions=this.arcLengthDivisions] - The number of divisions.
	 * @return {Array<number>} An array holding the cumulative segment lengths.
	 */
	getLengths( divisions = this.arcLengthDivisions ) {

		if ( this.cacheArcLengths &&
			( this.cacheArcLengths.length === divisions + 1 ) &&
			! this.needsUpdate ) {

			return this.cacheArcLengths;

		}

		this.needsUpdate = false;

		const cache = [];
		let current, last = this.getPoint( 0 );
		let sum = 0;

		cache.push( 0 );

		for ( let p = 1; p <= divisions; p ++ ) {

			current = this.getPoint( p / divisions );
			sum += current.distanceTo( last );
			cache.push( sum );
			last = current;

		}

		this.cacheArcLengths = cache;

		return cache; // { sums: cache, sum: sum }; Sum is in the last element.

	}

	/**
	 * Update the cumulative segment distance cache. The method must be called
	 * every time curve parameters are changed. If an updated curve is part of a
	 * composed curve like {@link CurvePath}, this method must be called on the
	 * composed curve, too.
	 */
	updateArcLengths() {

		this.needsUpdate = true;
		this.getLengths();

	}

	/**
	 * Given an interpolation factor in the range `[0,1]`, this method returns an updated
	 * interpolation factor in the same range that can be ued to sample equidistant points
	 * from a curve.
	 *
	 * @param {number} u - The interpolation factor.
	 * @param {?number} distance - An optional distance on the curve.
	 * @return {number} The updated interpolation factor.
	 */
	getUtoTmapping( u, distance = null ) {

		const arcLengths = this.getLengths();

		let i = 0;
		const il = arcLengths.length;

		let targetArcLength; // The targeted u distance value to get

		if ( distance ) {

			targetArcLength = distance;

		} else {

			targetArcLength = u * arcLengths[ il - 1 ];

		}

		// binary search for the index with largest value smaller than target u distance

		let low = 0, high = il - 1, comparison;

		while ( low <= high ) {

			i = Math.floor( low + ( high - low ) / 2 ); // less likely to overflow, though probably not issue here, JS doesn't really have integers, all numbers are floats

			comparison = arcLengths[ i ] - targetArcLength;

			if ( comparison < 0 ) {

				low = i + 1;

			} else if ( comparison > 0 ) {

				high = i - 1;

			} else {

				high = i;
				break;

				// DONE

			}

		}

		i = high;

		if ( arcLengths[ i ] === targetArcLength ) {

			return i / ( il - 1 );

		}

		// we could get finer grain at lengths, or use simple interpolation between two points

		const lengthBefore = arcLengths[ i ];
		const lengthAfter = arcLengths[ i + 1 ];

		const segmentLength = lengthAfter - lengthBefore;

		// determine where we are between the 'before' and 'after' points

		const segmentFraction = ( targetArcLength - lengthBefore ) / segmentLength;

		// add that fractional amount to t

		const t = ( i + segmentFraction ) / ( il - 1 );

		return t;

	}

	/**
	 * Returns a unit vector tangent for the given interpolation factor.
	 * If the derived curve does not implement its tangent derivation,
	 * two points a small delta apart will be used to find its gradient
	 * which seems to give a reasonable approximation.
	 *
	 * @param {number} t - The interpolation factor.
	 * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.
	 * @return {(Vector2|Vector3)} The tangent vector.
	 */
	getTangent( t, optionalTarget ) {

		const delta = 0.0001;
		let t1 = t - delta;
		let t2 = t + delta;

		// Capping in case of danger

		if ( t1 < 0 ) t1 = 0;
		if ( t2 > 1 ) t2 = 1;

		const pt1 = this.getPoint( t1 );
		const pt2 = this.getPoint( t2 );

		const tangent = optionalTarget || ( ( pt1.isVector2 ) ? new Vector2() : new Vector3() );

		tangent.copy( pt2 ).sub( pt1 ).normalize();

		return tangent;

	}

	/**
	 * Same as {@link Curve#getTangent} but with equidistant samples.
	 *
	 * @param {number} u - The interpolation factor.
	 * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.
	 * @return {(Vector2|Vector3)} The tangent vector.
	 * @see {@link Curve#getPointAt}
	 */
	getTangentAt( u, optionalTarget ) {

		const t = this.getUtoTmapping( u );
		return this.getTangent( t, optionalTarget );

	}

	/**
	 * Generates the Frenet Frames. Requires a curve definition in 3D space. Used
	 * in geometries like {@link TubeGeometry} or {@link ExtrudeGeometry}.
	 *
	 * @param {number} segments - The number of segments.
	 * @param {boolean} [closed=false] - Whether the curve is closed or not.
	 * @return {{tangents: Array<Vector3>, normals: Array<Vector3>, binormals: Array<Vector3>}} The Frenet Frames.
	 */
	computeFrenetFrames( segments, closed = false ) {

		// see http://www.cs.indiana.edu/pub/techreports/TR425.pdf

		const normal = new Vector3();

		const tangents = [];
		const normals = [];
		const binormals = [];

		const vec = new Vector3();
		const mat = new Matrix4();

		// compute the tangent vectors for each segment on the curve

		for ( let i = 0; i <= segments; i ++ ) {

			const u = i / segments;

			tangents[ i ] = this.getTangentAt( u, new Vector3() );

		}

		// select an initial normal vector perpendicular to the first tangent vector,
		// and in the direction of the minimum tangent xyz component

		normals[ 0 ] = new Vector3();
		binormals[ 0 ] = new Vector3();
		let min = Number.MAX_VALUE;
		const tx = Math.abs( tangents[ 0 ].x );
		const ty = Math.abs( tangents[ 0 ].y );
		const tz = Math.abs( tangents[ 0 ].z );

		if ( tx <= min ) {

			min = tx;
			normal.set( 1, 0, 0 );

		}

		if ( ty <= min ) {

			min = ty;
			normal.set( 0, 1, 0 );

		}

		if ( tz <= min ) {

			normal.set( 0, 0, 1 );

		}

		vec.crossVectors( tangents[ 0 ], normal ).normalize();

		normals[ 0 ].crossVectors( tangents[ 0 ], vec );
		binormals[ 0 ].crossVectors( tangents[ 0 ], normals[ 0 ] );


		// compute the slowly-varying normal and binormal vectors for each segment on the curve

		for ( let i = 1; i <= segments; i ++ ) {

			normals[ i ] = normals[ i - 1 ].clone();

			binormals[ i ] = binormals[ i - 1 ].clone();

			vec.crossVectors( tangents[ i - 1 ], tangents[ i ] );

			if ( vec.length() > Number.EPSILON ) {

				vec.normalize();

				const theta = Math.acos( clamp( tangents[ i - 1 ].dot( tangents[ i ] ), -1, 1 ) ); // clamp for floating pt errors

				normals[ i ].applyMatrix4( mat.makeRotationAxis( vec, theta ) );

			}

			binormals[ i ].crossVectors( tangents[ i ], normals[ i ] );

		}

		// if the curve is closed, postprocess the vectors so the first and last normal vectors are the same

		if ( closed === true ) {

			let theta = Math.acos( clamp( normals[ 0 ].dot( normals[ segments ] ), -1, 1 ) );
			theta /= segments;

			if ( tangents[ 0 ].dot( vec.crossVectors( normals[ 0 ], normals[ segments ] ) ) > 0 ) {

				theta = - theta;

			}

			for ( let i = 1; i <= segments; i ++ ) {

				// twist a little...
				normals[ i ].applyMatrix4( mat.makeRotationAxis( tangents[ i ], theta * i ) );
				binormals[ i ].crossVectors( tangents[ i ], normals[ i ] );

			}

		}

		return {
			tangents: tangents,
			normals: normals,
			binormals: binormals
		};

	}

	/**
	 * Returns a new curve with copied values from this instance.
	 *
	 * @return {Curve} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

	/**
	 * Copies the values of the given curve to this instance.
	 *
	 * @param {Curve} source - The curve to copy.
	 * @return {Curve} A reference to this curve.
	 */
	copy( source ) {

		this.arcLengthDivisions = source.arcLengthDivisions;

		return this;

	}

	/**
	 * Serializes the curve into JSON.
	 *
	 * @return {Object} A JSON object representing the serialized curve.
	 * @see {@link ObjectLoader#parse}
	 */
	toJSON() {

		const data = {
			metadata: {
				version: 4.7,
				type: 'Curve',
				generator: 'Curve.toJSON'
			}
		};

		data.arcLengthDivisions = this.arcLengthDivisions;
		data.type = this.type;

		return data;

	}

	/**
	 * Deserializes the curve from the given JSON.
	 *
	 * @param {Object} json - The JSON holding the serialized curve.
	 * @return {Curve} A reference to this curve.
	 */
	fromJSON( json ) {

		this.arcLengthDivisions = json.arcLengthDivisions;

		return this;

	}

}

/**
 * A curve representing an ellipse.
 *
 * ```js
 * const curve = new THREE.EllipseCurve(
 * 	0, 0,
 * 	10, 10,
 * 	0, 2 * Math.PI,
 * 	false,
 * 	0
 * );
 *
 * const points = curve.getPoints( 50 );
 * const geometry = new THREE.BufferGeometry().setFromPoints( points );
 *
 * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
 *
 * // Create the final object to add to the scene
 * const ellipse = new THREE.Line( geometry, material );
 * ```
 *
 * @augments Curve
 */
class EllipseCurve extends Curve {

	/**
	 * Constructs a new ellipse curve.
	 *
	 * @param {number} [aX=0] - The X center of the ellipse.
	 * @param {number} [aY=0] - The Y center of the ellipse.
	 * @param {number} [xRadius=1] - The radius of the ellipse in the x direction.
	 * @param {number} [yRadius=1] - The radius of the ellipse in the y direction.
	 * @param {number} [aStartAngle=0] - The start angle of the curve in radians starting from the positive X axis.
	 * @param {number} [aEndAngle=Math.PI*2] - The end angle of the curve in radians starting from the positive X axis.
	 * @param {boolean} [aClockwise=false] - Whether the ellipse is drawn clockwise or not.
	 * @param {number} [aRotation=0] - The rotation angle of the ellipse in radians, counterclockwise from the positive X axis.
	 */
	constructor( aX = 0, aY = 0, xRadius = 1, yRadius = 1, aStartAngle = 0, aEndAngle = Math.PI * 2, aClockwise = false, aRotation = 0 ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isEllipseCurve = true;

		this.type = 'EllipseCurve';

		/**
		 * The X center of the ellipse.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.aX = aX;

		/**
		 * The Y center of the ellipse.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.aY = aY;

		/**
		 * The radius of the ellipse in the x direction.
		 * Setting the this value equal to the {@link EllipseCurve#yRadius} will result in a circle.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.xRadius = xRadius;

		/**
		 * The radius of the ellipse in the y direction.
		 * Setting the this value equal to the {@link EllipseCurve#xRadius} will result in a circle.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.yRadius = yRadius;

		/**
		 * The start angle of the curve in radians starting from the positive X axis.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.aStartAngle = aStartAngle;

		/**
		 * The end angle of the curve in radians starting from the positive X axis.
		 *
		 * @type {number}
		 * @default Math.PI*2
		 */
		this.aEndAngle = aEndAngle;

		/**
		 * Whether the ellipse is drawn clockwise or not.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.aClockwise = aClockwise;

		/**
		 * The rotation angle of the ellipse in radians, counterclockwise from the positive X axis.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.aRotation = aRotation;

	}

	/**
	 * Returns a point on the curve.
	 *
	 * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
	 * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.
	 * @return {Vector2} The position on the curve.
	 */
	getPoint( t, optionalTarget = new Vector2() ) {

		const point = optionalTarget;

		const twoPi = Math.PI * 2;
		let deltaAngle = this.aEndAngle - this.aStartAngle;
		const samePoints = Math.abs( deltaAngle ) < Number.EPSILON;

		// ensures that deltaAngle is 0 .. 2 PI
		while ( deltaAngle < 0 ) deltaAngle += twoPi;
		while ( deltaAngle > twoPi ) deltaAngle -= twoPi;

		if ( deltaAngle < Number.EPSILON ) {

			if ( samePoints ) {

				deltaAngle = 0;

			} else {

				deltaAngle = twoPi;

			}

		}

		if ( this.aClockwise === true && ! samePoints ) {

			if ( deltaAngle === twoPi ) {

				deltaAngle = - twoPi;

			} else {

				deltaAngle = deltaAngle - twoPi;

			}

		}

		const angle = this.aStartAngle + t * deltaAngle;
		let x = this.aX + this.xRadius * Math.cos( angle );
		let y = this.aY + this.yRadius * Math.sin( angle );

		if ( this.aRotation !== 0 ) {

			const cos = Math.cos( this.aRotation );
			const sin = Math.sin( this.aRotation );

			const tx = x - this.aX;
			const ty = y - this.aY;

			// Rotate the point about the center of the ellipse.
			x = tx * cos - ty * sin + this.aX;
			y = tx * sin + ty * cos + this.aY;

		}

		return point.set( x, y );

	}

	copy( source ) {

		super.copy( source );

		this.aX = source.aX;
		this.aY = source.aY;

		this.xRadius = source.xRadius;
		this.yRadius = source.yRadius;

		this.aStartAngle = source.aStartAngle;
		this.aEndAngle = source.aEndAngle;

		this.aClockwise = source.aClockwise;

		this.aRotation = source.aRotation;

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		data.aX = this.aX;
		data.aY = this.aY;

		data.xRadius = this.xRadius;
		data.yRadius = this.yRadius;

		data.aStartAngle = this.aStartAngle;
		data.aEndAngle = this.aEndAngle;

		data.aClockwise = this.aClockwise;

		data.aRotation = this.aRotation;

		return data;

	}

	fromJSON( json ) {

		super.fromJSON( json );

		this.aX = json.aX;
		this.aY = json.aY;

		this.xRadius = json.xRadius;
		this.yRadius = json.yRadius;

		this.aStartAngle = json.aStartAngle;
		this.aEndAngle = json.aEndAngle;

		this.aClockwise = json.aClockwise;

		this.aRotation = json.aRotation;

		return this;

	}

}

/**
 * A curve representing an arc.
 *
 * @augments EllipseCurve
 */
class ArcCurve extends EllipseCurve {

	/**
	 * Constructs a new arc curve.
	 *
	 * @param {number} [aX=0] - The X center of the ellipse.
	 * @param {number} [aY=0] - The Y center of the ellipse.
	 * @param {number} [aRadius=1] - The radius of the ellipse in the x direction.
	 * @param {number} [aStartAngle=0] - The start angle of the curve in radians starting from the positive X axis.
	 * @param {number} [aEndAngle=Math.PI*2] - The end angle of the curve in radians starting from the positive X axis.
	 * @param {boolean} [aClockwise=false] - Whether the ellipse is drawn clockwise or not.
	 */
	constructor( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) {

		super( aX, aY, aRadius, aRadius, aStartAngle, aEndAngle, aClockwise );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isArcCurve = true;

		this.type = 'ArcCurve';

	}

}

function CubicPoly() {

	/**
	 * Centripetal CatmullRom Curve - which is useful for avoiding
	* cusps and self-intersections in non-uniform catmull rom curves.
	* http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf
	*
	* curve.type accepts centripetal(default), chordal and catmullrom
	* curve.tension is used for catmullrom which defaults to 0.5
	*/

	/*
	Based on an optimized c++ solution in
	- http://stackoverflow.com/questions/9489736/catmull-rom-curve-with-no-cusps-and-no-self-intersections/
	- http://ideone.com/NoEbVM

	This CubicPoly class could be used for reusing some variables and calculations,
	but for three.js curve use, it could be possible inlined and flatten into a single function call
	which can be placed in CurveUtils.
	*/

	let c0 = 0, c1 = 0, c2 = 0, c3 = 0;

	/*
	 * Compute coefficients for a cubic polynomial
	 *   p(s) = c0 + c1*s + c2*s^2 + c3*s^3
	 * such that
	 *   p(0) = x0, p(1) = x1
	 *  and
	 *   p'(0) = t0, p'(1) = t1.
	 */
	function init( x0, x1, t0, t1 ) {

		c0 = x0;
		c1 = t0;
		c2 = -3 * x0 + 3 * x1 - 2 * t0 - t1;
		c3 = 2 * x0 - 2 * x1 + t0 + t1;

	}

	return {

		initCatmullRom: function ( x0, x1, x2, x3, tension ) {

			init( x1, x2, tension * ( x2 - x0 ), tension * ( x3 - x1 ) );

		},

		initNonuniformCatmullRom: function ( x0, x1, x2, x3, dt0, dt1, dt2 ) {

			// compute tangents when parameterized in [t1,t2]
			let t1 = ( x1 - x0 ) / dt0 - ( x2 - x0 ) / ( dt0 + dt1 ) + ( x2 - x1 ) / dt1;
			let t2 = ( x2 - x1 ) / dt1 - ( x3 - x1 ) / ( dt1 + dt2 ) + ( x3 - x2 ) / dt2;

			// rescale tangents for parametrization in [0,1]
			t1 *= dt1;
			t2 *= dt1;

			init( x1, x2, t1, t2 );

		},

		calc: function ( t ) {

			const t2 = t * t;
			const t3 = t2 * t;
			return c0 + c1 * t + c2 * t2 + c3 * t3;

		}

	};

}

//

const tmp = /*@__PURE__*/ new Vector3();
const px = /*@__PURE__*/ new CubicPoly();
const py = /*@__PURE__*/ new CubicPoly();
const pz = /*@__PURE__*/ new CubicPoly();

/**
 * A curve representing a Catmull-Rom spline.
 *
 * ```js
 * //Create a closed wavey loop
 * const curve = new THREE.CatmullRomCurve3( [
 * 	new THREE.Vector3( -10, 0, 10 ),
 * 	new THREE.Vector3( -5, 5, 5 ),
 * 	new THREE.Vector3( 0, 0, 0 ),
 * 	new THREE.Vector3( 5, -5, 5 ),
 * 	new THREE.Vector3( 10, 0, 10 )
 * ] );
 *
 * const points = curve.getPoints( 50 );
 * const geometry = new THREE.BufferGeometry().setFromPoints( points );
 *
 * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
 *
 * // Create the final object to add to the scene
 * const curveObject = new THREE.Line( geometry, material );
 * ```
 *
 * @augments Curve
 */
class CatmullRomCurve3 extends Curve {

	/**
	 * Constructs a new Catmull-Rom curve.
	 *
	 * @param {Array<Vector3>} [points] - An array of 3D points defining the curve.
	 * @param {boolean} [closed=false] - Whether the curve is closed or not.
	 * @param {('centripetal'|'chordal'|'catmullrom')} [curveType='centripetal'] - The curve type.
	 * @param {number} [tension=0.5] - Tension of the curve.
	 */
	constructor( points = [], closed = false, curveType = 'centripetal', tension = 0.5 ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isCatmullRomCurve3 = true;

		this.type = 'CatmullRomCurve3';

		/**
		 * An array of 3D points defining the curve.
		 *
		 * @type {Array<Vector3>}
		 */
		this.points = points;

		/**
		 * Whether the curve is closed or not.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.closed = closed;

		/**
		 * The curve type.
		 *
		 * @type {('centripetal'|'chordal'|'catmullrom')}
		 * @default 'centripetal'
		 */
		this.curveType = curveType;

		/**
		 * Tension of the curve.
		 *
		 * @type {number}
		 * @default 0.5
		 */
		this.tension = tension;

	}

	/**
	 * Returns a point on the curve.
	 *
	 * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
	 * @param {Vector3} [optionalTarget] - The optional target vector the result is written to.
	 * @return {Vector3} The position on the curve.
	 */
	getPoint( t, optionalTarget = new Vector3() ) {

		const point = optionalTarget;

		const points = this.points;
		const l = points.length;

		const p = ( l - ( this.closed ? 0 : 1 ) ) * t;
		let intPoint = Math.floor( p );
		let weight = p - intPoint;

		if ( this.closed ) {

			intPoint += intPoint > 0 ? 0 : ( Math.floor( Math.abs( intPoint ) / l ) + 1 ) * l;

		} else if ( weight === 0 && intPoint === l - 1 ) {

			intPoint = l - 2;
			weight = 1;

		}

		let p0, p3; // 4 points (p1 & p2 defined below)

		if ( this.closed || intPoint > 0 ) {

			p0 = points[ ( intPoint - 1 ) % l ];

		} else {

			// extrapolate first point
			tmp.subVectors( points[ 0 ], points[ 1 ] ).add( points[ 0 ] );
			p0 = tmp;

		}

		const p1 = points[ intPoint % l ];
		const p2 = points[ ( intPoint + 1 ) % l ];

		if ( this.closed || intPoint + 2 < l ) {

			p3 = points[ ( intPoint + 2 ) % l ];

		} else {

			// extrapolate last point
			tmp.subVectors( points[ l - 1 ], points[ l - 2 ] ).add( points[ l - 1 ] );
			p3 = tmp;

		}

		if ( this.curveType === 'centripetal' || this.curveType === 'chordal' ) {

			// init Centripetal / Chordal Catmull-Rom
			const pow = this.curveType === 'chordal' ? 0.5 : 0.25;
			let dt0 = Math.pow( p0.distanceToSquared( p1 ), pow );
			let dt1 = Math.pow( p1.distanceToSquared( p2 ), pow );
			let dt2 = Math.pow( p2.distanceToSquared( p3 ), pow );

			// safety check for repeated points
			if ( dt1 < 1e-4 ) dt1 = 1.0;
			if ( dt0 < 1e-4 ) dt0 = dt1;
			if ( dt2 < 1e-4 ) dt2 = dt1;

			px.initNonuniformCatmullRom( p0.x, p1.x, p2.x, p3.x, dt0, dt1, dt2 );
			py.initNonuniformCatmullRom( p0.y, p1.y, p2.y, p3.y, dt0, dt1, dt2 );
			pz.initNonuniformCatmullRom( p0.z, p1.z, p2.z, p3.z, dt0, dt1, dt2 );

		} else if ( this.curveType === 'catmullrom' ) {

			px.initCatmullRom( p0.x, p1.x, p2.x, p3.x, this.tension );
			py.initCatmullRom( p0.y, p1.y, p2.y, p3.y, this.tension );
			pz.initCatmullRom( p0.z, p1.z, p2.z, p3.z, this.tension );

		}

		point.set(
			px.calc( weight ),
			py.calc( weight ),
			pz.calc( weight )
		);

		return point;

	}

	copy( source ) {

		super.copy( source );

		this.points = [];

		for ( let i = 0, l = source.points.length; i < l; i ++ ) {

			const point = source.points[ i ];

			this.points.push( point.clone() );

		}

		this.closed = source.closed;
		this.curveType = source.curveType;
		this.tension = source.tension;

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		data.points = [];

		for ( let i = 0, l = this.points.length; i < l; i ++ ) {

			const point = this.points[ i ];
			data.points.push( point.toArray() );

		}

		data.closed = this.closed;
		data.curveType = this.curveType;
		data.tension = this.tension;

		return data;

	}

	fromJSON( json ) {

		super.fromJSON( json );

		this.points = [];

		for ( let i = 0, l = json.points.length; i < l; i ++ ) {

			const point = json.points[ i ];
			this.points.push( new Vector3().fromArray( point ) );

		}

		this.closed = json.closed;
		this.curveType = json.curveType;
		this.tension = json.tension;

		return this;

	}

}

// Bezier Curves formulas obtained from: https://en.wikipedia.org/wiki/B%C3%A9zier_curve

/**
 * Computes a point on a Catmull-Rom spline.
 *
 * @param {number} t - The interpolation factor.
 * @param {number} p0 - The first control point.
 * @param {number} p1 - The second control point.
 * @param {number} p2 - The third control point.
 * @param {number} p3 - The fourth control point.
 * @return {number} The calculated point on a Catmull-Rom spline.
 */
function CatmullRom( t, p0, p1, p2, p3 ) {

	const v0 = ( p2 - p0 ) * 0.5;
	const v1 = ( p3 - p1 ) * 0.5;
	const t2 = t * t;
	const t3 = t * t2;
	return ( 2 * p1 - 2 * p2 + v0 + v1 ) * t3 + ( -3 * p1 + 3 * p2 - 2 * v0 - v1 ) * t2 + v0 * t + p1;

}

//

function QuadraticBezierP0( t, p ) {

	const k = 1 - t;
	return k * k * p;

}

function QuadraticBezierP1( t, p ) {

	return 2 * ( 1 - t ) * t * p;

}

function QuadraticBezierP2( t, p ) {

	return t * t * p;

}

/**
 * Computes a point on a Quadratic Bezier curve.
 *
 * @param {number} t - The interpolation factor.
 * @param {number} p0 - The first control point.
 * @param {number} p1 - The second control point.
 * @param {number} p2 - The third control point.
 * @return {number} The calculated point on a Quadratic Bezier curve.
 */
function QuadraticBezier( t, p0, p1, p2 ) {

	return QuadraticBezierP0( t, p0 ) + QuadraticBezierP1( t, p1 ) +
		QuadraticBezierP2( t, p2 );

}

//

function CubicBezierP0( t, p ) {

	const k = 1 - t;
	return k * k * k * p;

}

function CubicBezierP1( t, p ) {

	const k = 1 - t;
	return 3 * k * k * t * p;

}

function CubicBezierP2( t, p ) {

	return 3 * ( 1 - t ) * t * t * p;

}

function CubicBezierP3( t, p ) {

	return t * t * t * p;

}

/**
 * Computes a point on a Cubic Bezier curve.
 *
 * @param {number} t - The interpolation factor.
 * @param {number} p0 - The first control point.
 * @param {number} p1 - The second control point.
 * @param {number} p2 - The third control point.
 * @param {number} p3 - The fourth control point.
 * @return {number} The calculated point on a Cubic Bezier curve.
 */
function CubicBezier( t, p0, p1, p2, p3 ) {

	return CubicBezierP0( t, p0 ) + CubicBezierP1( t, p1 ) + CubicBezierP2( t, p2 ) +
		CubicBezierP3( t, p3 );

}

/**
 * A curve representing a 2D Cubic Bezier curve.
 *
 * ```js
 * const curve = new THREE.CubicBezierCurve(
 * 	new THREE.Vector2( - 0, 0 ),
 * 	new THREE.Vector2( - 5, 15 ),
 * 	new THREE.Vector2( 20, 15 ),
 * 	new THREE.Vector2( 10, 0 )
 * );
 *
 * const points = curve.getPoints( 50 );
 * const geometry = new THREE.BufferGeometry().setFromPoints( points );
 *
 * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
 *
 * // Create the final object to add to the scene
 * const curveObject = new THREE.Line( geometry, material );
 * ```
 *
 * @augments Curve
 */
class CubicBezierCurve extends Curve {

	/**
	 * Constructs a new Cubic Bezier curve.
	 *
	 * @param {Vector2} [v0] - The start point.
	 * @param {Vector2} [v1] - The first control point.
	 * @param {Vector2} [v2] - The second control point.
	 * @param {Vector2} [v3] - The end point.
	 */
	constructor( v0 = new Vector2(), v1 = new Vector2(), v2 = new Vector2(), v3 = new Vector2() ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isCubicBezierCurve = true;

		this.type = 'CubicBezierCurve';

		/**
		 * The start point.
		 *
		 * @type {Vector2}
		 */
		this.v0 = v0;

		/**
		 * The first control point.
		 *
		 * @type {Vector2}
		 */
		this.v1 = v1;

		/**
		 * The second control point.
		 *
		 * @type {Vector2}
		 */
		this.v2 = v2;

		/**
		 * The end point.
		 *
		 * @type {Vector2}
		 */
		this.v3 = v3;

	}

	/**
	 * Returns a point on the curve.
	 *
	 * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
	 * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.
	 * @return {Vector2} The position on the curve.
	 */
	getPoint( t, optionalTarget = new Vector2() ) {

		const point = optionalTarget;

		const v0 = this.v0, v1 = this.v1, v2 = this.v2, v3 = this.v3;

		point.set(
			CubicBezier( t, v0.x, v1.x, v2.x, v3.x ),
			CubicBezier( t, v0.y, v1.y, v2.y, v3.y )
		);

		return point;

	}

	copy( source ) {

		super.copy( source );

		this.v0.copy( source.v0 );
		this.v1.copy( source.v1 );
		this.v2.copy( source.v2 );
		this.v3.copy( source.v3 );

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		data.v0 = this.v0.toArray();
		data.v1 = this.v1.toArray();
		data.v2 = this.v2.toArray();
		data.v3 = this.v3.toArray();

		return data;

	}

	fromJSON( json ) {

		super.fromJSON( json );

		this.v0.fromArray( json.v0 );
		this.v1.fromArray( json.v1 );
		this.v2.fromArray( json.v2 );
		this.v3.fromArray( json.v3 );

		return this;

	}

}

/**
 * A curve representing a 3D Cubic Bezier curve.
 *
 * @augments Curve
 */
class CubicBezierCurve3 extends Curve {

	/**
	 * Constructs a new Cubic Bezier curve.
	 *
	 * @param {Vector3} [v0] - The start point.
	 * @param {Vector3} [v1] - The first control point.
	 * @param {Vector3} [v2] - The second control point.
	 * @param {Vector3} [v3] - The end point.
	 */
	constructor( v0 = new Vector3(), v1 = new Vector3(), v2 = new Vector3(), v3 = new Vector3() ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isCubicBezierCurve3 = true;

		this.type = 'CubicBezierCurve3';

		/**
		 * The start point.
		 *
		 * @type {Vector3}
		 */
		this.v0 = v0;

		/**
		 * The first control point.
		 *
		 * @type {Vector3}
		 */
		this.v1 = v1;

		/**
		 * The second control point.
		 *
		 * @type {Vector3}
		 */
		this.v2 = v2;

		/**
		 * The end point.
		 *
		 * @type {Vector3}
		 */
		this.v3 = v3;

	}

	/**
	 * Returns a point on the curve.
	 *
	 * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
	 * @param {Vector3} [optionalTarget] - The optional target vector the result is written to.
	 * @return {Vector3} The position on the curve.
	 */
	getPoint( t, optionalTarget = new Vector3() ) {

		const point = optionalTarget;

		const v0 = this.v0, v1 = this.v1, v2 = this.v2, v3 = this.v3;

		point.set(
			CubicBezier( t, v0.x, v1.x, v2.x, v3.x ),
			CubicBezier( t, v0.y, v1.y, v2.y, v3.y ),
			CubicBezier( t, v0.z, v1.z, v2.z, v3.z )
		);

		return point;

	}

	copy( source ) {

		super.copy( source );

		this.v0.copy( source.v0 );
		this.v1.copy( source.v1 );
		this.v2.copy( source.v2 );
		this.v3.copy( source.v3 );

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		data.v0 = this.v0.toArray();
		data.v1 = this.v1.toArray();
		data.v2 = this.v2.toArray();
		data.v3 = this.v3.toArray();

		return data;

	}

	fromJSON( json ) {

		super.fromJSON( json );

		this.v0.fromArray( json.v0 );
		this.v1.fromArray( json.v1 );
		this.v2.fromArray( json.v2 );
		this.v3.fromArray( json.v3 );

		return this;

	}

}

/**
 * A curve representing a 2D line segment.
 *
 * @augments Curve
 */
class LineCurve extends Curve {

	/**
	 * Constructs a new line curve.
	 *
	 * @param {Vector2} [v1] - The start point.
	 * @param {Vector2} [v2] - The end point.
	 */
	constructor( v1 = new Vector2(), v2 = new Vector2() ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isLineCurve = true;

		this.type = 'LineCurve';

		/**
		 * The start point.
		 *
		 * @type {Vector2}
		 */
		this.v1 = v1;

		/**
		 * The end point.
		 *
		 * @type {Vector2}
		 */
		this.v2 = v2;

	}

	/**
	 * Returns a point on the line.
	 *
	 * @param {number} t - A interpolation factor representing a position on the line. Must be in the range `[0,1]`.
	 * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.
	 * @return {Vector2} The position on the line.
	 */
	getPoint( t, optionalTarget = new Vector2() ) {

		const point = optionalTarget;

		if ( t === 1 ) {

			point.copy( this.v2 );

		} else {

			point.copy( this.v2 ).sub( this.v1 );
			point.multiplyScalar( t ).add( this.v1 );

		}

		return point;

	}

	// Line curve is linear, so we can overwrite default getPointAt
	getPointAt( u, optionalTarget ) {

		return this.getPoint( u, optionalTarget );

	}

	getTangent( t, optionalTarget = new Vector2() ) {

		return optionalTarget.subVectors( this.v2, this.v1 ).normalize();

	}

	getTangentAt( u, optionalTarget ) {

		return this.getTangent( u, optionalTarget );

	}

	copy( source ) {

		super.copy( source );

		this.v1.copy( source.v1 );
		this.v2.copy( source.v2 );

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		data.v1 = this.v1.toArray();
		data.v2 = this.v2.toArray();

		return data;

	}

	fromJSON( json ) {

		super.fromJSON( json );

		this.v1.fromArray( json.v1 );
		this.v2.fromArray( json.v2 );

		return this;

	}

}

/**
 * A curve representing a 3D line segment.
 *
 * @augments Curve
 */
class LineCurve3 extends Curve {

	/**
	 * Constructs a new line curve.
	 *
	 * @param {Vector3} [v1] - The start point.
	 * @param {Vector3} [v2] - The end point.
	 */
	constructor( v1 = new Vector3(), v2 = new Vector3() ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isLineCurve3 = true;

		this.type = 'LineCurve3';

		/**
		 * The start point.
		 *
		 * @type {Vector3}
		 */
		this.v1 = v1;

		/**
		 * The end point.
		 *
		 * @type {Vector2}
		 */
		this.v2 = v2;

	}

	/**
	 * Returns a point on the line.
	 *
	 * @param {number} t - A interpolation factor representing a position on the line. Must be in the range `[0,1]`.
	 * @param {Vector3} [optionalTarget] - The optional target vector the result is written to.
	 * @return {Vector3} The position on the line.
	 */
	getPoint( t, optionalTarget = new Vector3() ) {

		const point = optionalTarget;

		if ( t === 1 ) {

			point.copy( this.v2 );

		} else {

			point.copy( this.v2 ).sub( this.v1 );
			point.multiplyScalar( t ).add( this.v1 );

		}

		return point;

	}

	// Line curve is linear, so we can overwrite default getPointAt
	getPointAt( u, optionalTarget ) {

		return this.getPoint( u, optionalTarget );

	}

	getTangent( t, optionalTarget = new Vector3() ) {

		return optionalTarget.subVectors( this.v2, this.v1 ).normalize();

	}

	getTangentAt( u, optionalTarget ) {

		return this.getTangent( u, optionalTarget );

	}

	copy( source ) {

		super.copy( source );

		this.v1.copy( source.v1 );
		this.v2.copy( source.v2 );

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		data.v1 = this.v1.toArray();
		data.v2 = this.v2.toArray();

		return data;

	}

	fromJSON( json ) {

		super.fromJSON( json );

		this.v1.fromArray( json.v1 );
		this.v2.fromArray( json.v2 );

		return this;

	}

}

/**
 * A curve representing a 2D Quadratic Bezier curve.
 *
 * ```js
 * const curve = new THREE.QuadraticBezierCurve(
 * 	new THREE.Vector2( - 10, 0 ),
 * 	new THREE.Vector2( 20, 15 ),
 * 	new THREE.Vector2( 10, 0 )
 * )
 *
 * const points = curve.getPoints( 50 );
 * const geometry = new THREE.BufferGeometry().setFromPoints( points );
 *
 * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
 *
 * // Create the final object to add to the scene
 * const curveObject = new THREE.Line( geometry, material );
 * ```
 *
 * @augments Curve
 */
class QuadraticBezierCurve extends Curve {

	/**
	 * Constructs a new Quadratic Bezier curve.
	 *
	 * @param {Vector2} [v0] - The start point.
	 * @param {Vector2} [v1] - The control point.
	 * @param {Vector2} [v2] - The end point.
	 */
	constructor( v0 = new Vector2(), v1 = new Vector2(), v2 = new Vector2() ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isQuadraticBezierCurve = true;

		this.type = 'QuadraticBezierCurve';

		/**
		 * The start point.
		 *
		 * @type {Vector2}
		 */
		this.v0 = v0;

		/**
		 * The control point.
		 *
		 * @type {Vector2}
		 */
		this.v1 = v1;

		/**
		 * The end point.
		 *
		 * @type {Vector2}
		 */
		this.v2 = v2;

	}

	/**
	 * Returns a point on the curve.
	 *
	 * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
	 * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.
	 * @return {Vector2} The position on the curve.
	 */
	getPoint( t, optionalTarget = new Vector2() ) {

		const point = optionalTarget;

		const v0 = this.v0, v1 = this.v1, v2 = this.v2;

		point.set(
			QuadraticBezier( t, v0.x, v1.x, v2.x ),
			QuadraticBezier( t, v0.y, v1.y, v2.y )
		);

		return point;

	}

	copy( source ) {

		super.copy( source );

		this.v0.copy( source.v0 );
		this.v1.copy( source.v1 );
		this.v2.copy( source.v2 );

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		data.v0 = this.v0.toArray();
		data.v1 = this.v1.toArray();
		data.v2 = this.v2.toArray();

		return data;

	}

	fromJSON( json ) {

		super.fromJSON( json );

		this.v0.fromArray( json.v0 );
		this.v1.fromArray( json.v1 );
		this.v2.fromArray( json.v2 );

		return this;

	}

}

/**
 * A curve representing a 3D Quadratic Bezier curve.
 *
 * @augments Curve
 */
class QuadraticBezierCurve3 extends Curve {

	/**
	 * Constructs a new Quadratic Bezier curve.
	 *
	 * @param {Vector3} [v0] - The start point.
	 * @param {Vector3} [v1] - The control point.
	 * @param {Vector3} [v2] - The end point.
	 */
	constructor( v0 = new Vector3(), v1 = new Vector3(), v2 = new Vector3() ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isQuadraticBezierCurve3 = true;

		this.type = 'QuadraticBezierCurve3';

		/**
		 * The start point.
		 *
		 * @type {Vector3}
		 */
		this.v0 = v0;

		/**
		 * The control point.
		 *
		 * @type {Vector3}
		 */
		this.v1 = v1;

		/**
		 * The end point.
		 *
		 * @type {Vector3}
		 */
		this.v2 = v2;

	}

	/**
	 * Returns a point on the curve.
	 *
	 * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
	 * @param {Vector3} [optionalTarget] - The optional target vector the result is written to.
	 * @return {Vector3} The position on the curve.
	 */
	getPoint( t, optionalTarget = new Vector3() ) {

		const point = optionalTarget;

		const v0 = this.v0, v1 = this.v1, v2 = this.v2;

		point.set(
			QuadraticBezier( t, v0.x, v1.x, v2.x ),
			QuadraticBezier( t, v0.y, v1.y, v2.y ),
			QuadraticBezier( t, v0.z, v1.z, v2.z )
		);

		return point;

	}

	copy( source ) {

		super.copy( source );

		this.v0.copy( source.v0 );
		this.v1.copy( source.v1 );
		this.v2.copy( source.v2 );

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		data.v0 = this.v0.toArray();
		data.v1 = this.v1.toArray();
		data.v2 = this.v2.toArray();

		return data;

	}

	fromJSON( json ) {

		super.fromJSON( json );

		this.v0.fromArray( json.v0 );
		this.v1.fromArray( json.v1 );
		this.v2.fromArray( json.v2 );

		return this;

	}

}

/**
 * A curve representing a 2D spline curve.
 *
 * ```js
 * // Create a sine-like wave
 * const curve = new THREE.SplineCurve( [
 * 	new THREE.Vector2( -10, 0 ),
 * 	new THREE.Vector2( -5, 5 ),
 * 	new THREE.Vector2( 0, 0 ),
 * 	new THREE.Vector2( 5, -5 ),
 * 	new THREE.Vector2( 10, 0 )
 * ] );
 *
 * const points = curve.getPoints( 50 );
 * const geometry = new THREE.BufferGeometry().setFromPoints( points );
 *
 * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
 *
 * // Create the final object to add to the scene
 * const splineObject = new THREE.Line( geometry, material );
 * ```
 *
 * @augments Curve
 */
class SplineCurve extends Curve {

	/**
	 * Constructs a new 2D spline curve.
	 *
	 * @param {Array<Vector2>} [points] -  An array of 2D points defining the curve.
	 */
	constructor( points = [] ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isSplineCurve = true;

		this.type = 'SplineCurve';

		/**
		 * An array of 2D points defining the curve.
		 *
		 * @type {Array<Vector2>}
		 */
		this.points = points;

	}

	/**
	 * Returns a point on the curve.
	 *
	 * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
	 * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.
	 * @return {Vector2} The position on the curve.
	 */
	getPoint( t, optionalTarget = new Vector2() ) {

		const point = optionalTarget;

		const points = this.points;
		const p = ( points.length - 1 ) * t;

		const intPoint = Math.floor( p );
		const weight = p - intPoint;

		const p0 = points[ intPoint === 0 ? intPoint : intPoint - 1 ];
		const p1 = points[ intPoint ];
		const p2 = points[ intPoint > points.length - 2 ? points.length - 1 : intPoint + 1 ];
		const p3 = points[ intPoint > points.length - 3 ? points.length - 1 : intPoint + 2 ];

		point.set(
			CatmullRom( weight, p0.x, p1.x, p2.x, p3.x ),
			CatmullRom( weight, p0.y, p1.y, p2.y, p3.y )
		);

		return point;

	}

	copy( source ) {

		super.copy( source );

		this.points = [];

		for ( let i = 0, l = source.points.length; i < l; i ++ ) {

			const point = source.points[ i ];

			this.points.push( point.clone() );

		}

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		data.points = [];

		for ( let i = 0, l = this.points.length; i < l; i ++ ) {

			const point = this.points[ i ];
			data.points.push( point.toArray() );

		}

		return data;

	}

	fromJSON( json ) {

		super.fromJSON( json );

		this.points = [];

		for ( let i = 0, l = json.points.length; i < l; i ++ ) {

			const point = json.points[ i ];
			this.points.push( new Vector2().fromArray( point ) );

		}

		return this;

	}

}

var Curves = /*#__PURE__*/Object.freeze({
	__proto__: null,
	ArcCurve: ArcCurve,
	CatmullRomCurve3: CatmullRomCurve3,
	CubicBezierCurve: CubicBezierCurve,
	CubicBezierCurve3: CubicBezierCurve3,
	EllipseCurve: EllipseCurve,
	LineCurve: LineCurve,
	LineCurve3: LineCurve3,
	QuadraticBezierCurve: QuadraticBezierCurve,
	QuadraticBezierCurve3: QuadraticBezierCurve3,
	SplineCurve: SplineCurve
});

/**
 * A base class extending {@link Curve}. `CurvePath` is simply an
 * array of connected curves, but retains the API of a curve.
 *
 * @augments Curve
 */
class CurvePath extends Curve {

	/**
	 * Constructs a new curve path.
	 */
	constructor() {

		super();

		this.type = 'CurvePath';

		/**
		 * An array of curves defining the
		 * path.
		 *
		 * @type {Array<Curve>}
		 */
		this.curves = [];

		/**
		 * Whether the path should automatically be closed
		 * by a line curve.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.autoClose = false;

	}

	/**
	 * Adds a curve to this curve path.
	 *
	 * @param {Curve} curve - The curve to add.
	 */
	add( curve ) {

		this.curves.push( curve );

	}

	/**
	 * Adds a line curve to close the path.
	 *
	 * @return {CurvePath} A reference to this curve path.
	 */
	closePath() {

		// Add a line curve if start and end of lines are not connected
		const startPoint = this.curves[ 0 ].getPoint( 0 );
		const endPoint = this.curves[ this.curves.length - 1 ].getPoint( 1 );

		if ( ! startPoint.equals( endPoint ) ) {

			const lineType = ( startPoint.isVector2 === true ) ? 'LineCurve' : 'LineCurve3';
			this.curves.push( new Curves[ lineType ]( endPoint, startPoint ) );

		}

		return this;

	}

	/**
	 * This method returns a vector in 2D or 3D space (depending on the curve definitions)
	 * for the given interpolation factor.
	 *
	 * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
	 * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.
	 * @return {?(Vector2|Vector3)} The position on the curve. It can be a 2D or 3D vector depending on the curve definition.
	 */
	getPoint( t, optionalTarget ) {

		// To get accurate point with reference to
		// entire path distance at time t,
		// following has to be done:

		// 1. Length of each sub path have to be known
		// 2. Locate and identify type of curve
		// 3. Get t for the curve
		// 4. Return curve.getPointAt(t')

		const d = t * this.getLength();
		const curveLengths = this.getCurveLengths();
		let i = 0;

		// To think about boundaries points.

		while ( i < curveLengths.length ) {

			if ( curveLengths[ i ] >= d ) {

				const diff = curveLengths[ i ] - d;
				const curve = this.curves[ i ];

				const segmentLength = curve.getLength();
				const u = segmentLength === 0 ? 0 : 1 - diff / segmentLength;

				return curve.getPointAt( u, optionalTarget );

			}

			i ++;

		}

		return null;

		// loop where sum != 0, sum > d , sum+1 <d

	}

	getLength() {

		// We cannot use the default THREE.Curve getPoint() with getLength() because in
		// THREE.Curve, getLength() depends on getPoint() but in THREE.CurvePath
		// getPoint() depends on getLength

		const lens = this.getCurveLengths();
		return lens[ lens.length - 1 ];

	}

	updateArcLengths() {

		// cacheLengths must be recalculated.

		this.needsUpdate = true;
		this.cacheLengths = null;
		this.getCurveLengths();

	}

	/**
	 * Returns list of cumulative curve lengths of the defined curves.
	 *
	 * @return {Array<number>} The curve lengths.
	 */
	getCurveLengths() {

		// Compute lengths and cache them
		// We cannot overwrite getLengths() because UtoT mapping uses it.
		// We use cache values if curves and cache array are same length

		if ( this.cacheLengths && this.cacheLengths.length === this.curves.length ) {

			return this.cacheLengths;

		}

		// Get length of sub-curve
		// Push sums into cached array

		const lengths = [];
		let sums = 0;

		for ( let i = 0, l = this.curves.length; i < l; i ++ ) {

			sums += this.curves[ i ].getLength();
			lengths.push( sums );

		}

		this.cacheLengths = lengths;

		return lengths;

	}

	getSpacedPoints( divisions = 40 ) {

		const points = [];

		for ( let i = 0; i <= divisions; i ++ ) {

			points.push( this.getPoint( i / divisions ) );

		}

		if ( this.autoClose ) {

			points.push( points[ 0 ] );

		}

		return points;

	}

	getPoints( divisions = 12 ) {

		const points = [];
		let last;

		for ( let i = 0, curves = this.curves; i < curves.length; i ++ ) {

			const curve = curves[ i ];
			const resolution = curve.isEllipseCurve ? divisions * 2
				: ( curve.isLineCurve || curve.isLineCurve3 ) ? 1
					: curve.isSplineCurve ? divisions * curve.points.length
						: divisions;

			const pts = curve.getPoints( resolution );

			for ( let j = 0; j < pts.length; j ++ ) {

				const point = pts[ j ];

				if ( last && last.equals( point ) ) continue; // ensures no consecutive points are duplicates

				points.push( point );
				last = point;

			}

		}

		if ( this.autoClose && points.length > 1 && ! points[ points.length - 1 ].equals( points[ 0 ] ) ) {

			points.push( points[ 0 ] );

		}

		return points;

	}

	copy( source ) {

		super.copy( source );

		this.curves = [];

		for ( let i = 0, l = source.curves.length; i < l; i ++ ) {

			const curve = source.curves[ i ];

			this.curves.push( curve.clone() );

		}

		this.autoClose = source.autoClose;

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		data.autoClose = this.autoClose;
		data.curves = [];

		for ( let i = 0, l = this.curves.length; i < l; i ++ ) {

			const curve = this.curves[ i ];
			data.curves.push( curve.toJSON() );

		}

		return data;

	}

	fromJSON( json ) {

		super.fromJSON( json );

		this.autoClose = json.autoClose;
		this.curves = [];

		for ( let i = 0, l = json.curves.length; i < l; i ++ ) {

			const curve = json.curves[ i ];
			this.curves.push( new Curves[ curve.type ]().fromJSON( curve ) );

		}

		return this;

	}

}

/**
 * A 2D path representation. The class provides methods for creating paths
 * and contours of 2D shapes similar to the 2D Canvas API.
 *
 * ```js
 * const path = new THREE.Path();
 *
 * path.lineTo( 0, 0.8 );
 * path.quadraticCurveTo( 0, 1, 0.2, 1 );
 * path.lineTo( 1, 1 );
 *
 * const points = path.getPoints();
 *
 * const geometry = new THREE.BufferGeometry().setFromPoints( points );
 * const material = new THREE.LineBasicMaterial( { color: 0xffffff } );
 *
 * const line = new THREE.Line( geometry, material );
 * scene.add( line );
 * ```
 *
 * @augments CurvePath
 */
let Path$1 = class Path extends CurvePath {

	/**
	 * Constructs a new path.
	 *
	 * @param {Array<Vector2>} [points] - An array of 2D points defining the path.
	 */
	constructor( points ) {

		super();

		this.type = 'Path';

		/**
		 * The current offset of the path. Any new curve added will start here.
		 *
		 * @type {Vector2}
		 */
		this.currentPoint = new Vector2();

		if ( points ) {

			this.setFromPoints( points );

		}

	}

	/**
	 * Creates a path from the given list of points. The points are added
	 * to the path as instances of {@link LineCurve}.
	 *
	 * @param {Array<Vector2>} points - An array of 2D points.
	 * @return {Path} A reference to this path.
	 */
	setFromPoints( points ) {

		this.moveTo( points[ 0 ].x, points[ 0 ].y );

		for ( let i = 1, l = points.length; i < l; i ++ ) {

			this.lineTo( points[ i ].x, points[ i ].y );

		}

		return this;

	}

	/**
	 * Moves {@link Path#currentPoint} to the given point.
	 *
	 * @param {number} x - The x coordinate.
	 * @param {number} y - The y coordinate.
	 * @return {Path} A reference to this path.
	 */
	moveTo( x, y ) {

		this.currentPoint.set( x, y ); // TODO consider referencing vectors instead of copying?

		return this;

	}

	/**
	 * Adds an instance of {@link LineCurve} to the path by connecting
	 * the current point with the given one.
	 *
	 * @param {number} x - The x coordinate of the end point.
	 * @param {number} y - The y coordinate of the end point.
	 * @return {Path} A reference to this path.
	 */
	lineTo( x, y ) {

		const curve = new LineCurve( this.currentPoint.clone(), new Vector2( x, y ) );
		this.curves.push( curve );

		this.currentPoint.set( x, y );

		return this;

	}

	/**
	 * Adds an instance of {@link QuadraticBezierCurve} to the path by connecting
	 * the current point with the given one.
	 *
	 * @param {number} aCPx - The x coordinate of the control point.
	 * @param {number} aCPy - The y coordinate of the control point.
	 * @param {number} aX - The x coordinate of the end point.
	 * @param {number} aY - The y coordinate of the end point.
	 * @return {Path} A reference to this path.
	 */
	quadraticCurveTo( aCPx, aCPy, aX, aY ) {

		const curve = new QuadraticBezierCurve(
			this.currentPoint.clone(),
			new Vector2( aCPx, aCPy ),
			new Vector2( aX, aY )
		);

		this.curves.push( curve );

		this.currentPoint.set( aX, aY );

		return this;

	}

	/**
	 * Adds an instance of {@link CubicBezierCurve} to the path by connecting
	 * the current point with the given one.
	 *
	 * @param {number} aCP1x - The x coordinate of the first control point.
	 * @param {number} aCP1y - The y coordinate of the first control point.
	 * @param {number} aCP2x - The x coordinate of the second control point.
	 * @param {number} aCP2y - The y coordinate of the second control point.
	 * @param {number} aX - The x coordinate of the end point.
	 * @param {number} aY - The y coordinate of the end point.
	 * @return {Path} A reference to this path.
	 */
	bezierCurveTo( aCP1x, aCP1y, aCP2x, aCP2y, aX, aY ) {

		const curve = new CubicBezierCurve(
			this.currentPoint.clone(),
			new Vector2( aCP1x, aCP1y ),
			new Vector2( aCP2x, aCP2y ),
			new Vector2( aX, aY )
		);

		this.curves.push( curve );

		this.currentPoint.set( aX, aY );

		return this;

	}

	/**
	 * Adds an instance of {@link SplineCurve} to the path by connecting
	 * the current point with the given list of points.
	 *
	 * @param {Array<Vector2>} pts - An array of points in 2D space.
	 * @return {Path} A reference to this path.
	 */
	splineThru( pts ) {

		const npts = [ this.currentPoint.clone() ].concat( pts );

		const curve = new SplineCurve( npts );
		this.curves.push( curve );

		this.currentPoint.copy( pts[ pts.length - 1 ] );

		return this;

	}

	/**
	 * Adds an arc as an instance of {@link EllipseCurve} to the path, positioned relative
	 * to the current point.
	 *
	 * @param {number} [aX=0] - The x coordinate of the center of the arc offsetted from the previous curve.
	 * @param {number} [aY=0] - The y coordinate of the center of the arc offsetted from the previous curve.
	 * @param {number} [aRadius=1] - The radius of the arc.
	 * @param {number} [aStartAngle=0] - The start angle in radians.
	 * @param {number} [aEndAngle=Math.PI*2] - The end angle in radians.
	 * @param {boolean} [aClockwise=false] - Whether to sweep the arc clockwise or not.
	 * @return {Path} A reference to this path.
	 */
	arc( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) {

		const x0 = this.currentPoint.x;
		const y0 = this.currentPoint.y;

		this.absarc( aX + x0, aY + y0, aRadius,
			aStartAngle, aEndAngle, aClockwise );

		return this;

	}

	/**
	 * Adds an absolutely positioned arc as an instance of {@link EllipseCurve} to the path.
	 *
	 * @param {number} [aX=0] - The x coordinate of the center of the arc.
	 * @param {number} [aY=0] - The y coordinate of the center of the arc.
	 * @param {number} [aRadius=1] - The radius of the arc.
	 * @param {number} [aStartAngle=0] - The start angle in radians.
	 * @param {number} [aEndAngle=Math.PI*2] - The end angle in radians.
	 * @param {boolean} [aClockwise=false] - Whether to sweep the arc clockwise or not.
	 * @return {Path} A reference to this path.
	 */
	absarc( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) {

		this.absellipse( aX, aY, aRadius, aRadius, aStartAngle, aEndAngle, aClockwise );

		return this;

	}

	/**
	 * Adds an ellipse as an instance of {@link EllipseCurve} to the path, positioned relative
	 * to the current point
	 *
	 * @param {number} [aX=0] - The x coordinate of the center of the ellipse offsetted from the previous curve.
	 * @param {number} [aY=0] - The y coordinate of the center of the ellipse offsetted from the previous curve.
	 * @param {number} [xRadius=1] - The radius of the ellipse in the x axis.
	 * @param {number} [yRadius=1] - The radius of the ellipse in the y axis.
	 * @param {number} [aStartAngle=0] - The start angle in radians.
	 * @param {number} [aEndAngle=Math.PI*2] - The end angle in radians.
	 * @param {boolean} [aClockwise=false] - Whether to sweep the ellipse clockwise or not.
	 * @param {number} [aRotation=0] - The rotation angle of the ellipse in radians, counterclockwise from the positive X axis.
	 * @return {Path} A reference to this path.
	 */
	ellipse( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation ) {

		const x0 = this.currentPoint.x;
		const y0 = this.currentPoint.y;

		this.absellipse( aX + x0, aY + y0, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation );

		return this;

	}

	/**
	 * Adds an absolutely positioned ellipse as an instance of {@link EllipseCurve} to the path.
	 *
	 * @param {number} [aX=0] - The x coordinate of the absolute center of the ellipse.
	 * @param {number} [aY=0] - The y coordinate of the absolute center of the ellipse.
	 * @param {number} [xRadius=1] - The radius of the ellipse in the x axis.
	 * @param {number} [yRadius=1] - The radius of the ellipse in the y axis.
	 * @param {number} [aStartAngle=0] - The start angle in radians.
	 * @param {number} [aEndAngle=Math.PI*2] - The end angle in radians.
	 * @param {boolean} [aClockwise=false] - Whether to sweep the ellipse clockwise or not.
	 * @param {number} [aRotation=0] - The rotation angle of the ellipse in radians, counterclockwise from the positive X axis.
	 * @return {Path} A reference to this path.
	 */
	absellipse( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation ) {

		const curve = new EllipseCurve( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation );

		if ( this.curves.length > 0 ) {

			// if a previous curve is present, attempt to join
			const firstPoint = curve.getPoint( 0 );

			if ( ! firstPoint.equals( this.currentPoint ) ) {

				this.lineTo( firstPoint.x, firstPoint.y );

			}

		}

		this.curves.push( curve );

		const lastPoint = curve.getPoint( 1 );
		this.currentPoint.copy( lastPoint );

		return this;

	}

	copy( source ) {

		super.copy( source );

		this.currentPoint.copy( source.currentPoint );

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		data.currentPoint = this.currentPoint.toArray();

		return data;

	}

	fromJSON( json ) {

		super.fromJSON( json );

		this.currentPoint.fromArray( json.currentPoint );

		return this;

	}

};

/**
 * Defines an arbitrary 2d shape plane using paths with optional holes. It
 * can be used with {@link ExtrudeGeometry}, {@link ShapeGeometry}, to get
 * points, or to get triangulated faces.
 *
 * ```js
 * const heartShape = new THREE.Shape();
 *
 * heartShape.moveTo( 25, 25 );
 * heartShape.bezierCurveTo( 25, 25, 20, 0, 0, 0 );
 * heartShape.bezierCurveTo( - 30, 0, - 30, 35, - 30, 35 );
 * heartShape.bezierCurveTo( - 30, 55, - 10, 77, 25, 95 );
 * heartShape.bezierCurveTo( 60, 77, 80, 55, 80, 35 );
 * heartShape.bezierCurveTo( 80, 35, 80, 0, 50, 0 );
 * heartShape.bezierCurveTo( 35, 0, 25, 25, 25, 25 );
 *
 * const extrudeSettings = {
 * 	depth: 8,
 * 	bevelEnabled: true,
 * 	bevelSegments: 2,
 * 	steps: 2,
 * 	bevelSize: 1,
 * 	bevelThickness: 1
 * };
 *
 * const geometry = new THREE.ExtrudeGeometry( heartShape, extrudeSettings );
 * const mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial() );
 * ```
 *
 * @augments Path
 */
class Shape extends Path$1 {

	/**
	 * Constructs a new shape.
	 *
	 * @param {Array<Vector2>} [points] - An array of 2D points defining the shape.
	 */
	constructor( points ) {

		super( points );

		/**
		 * The UUID of the shape.
		 *
		 * @type {string}
		 * @readonly
		 */
		this.uuid = generateUUID();

		this.type = 'Shape';

		/**
		 * Defines the holes in the shape. Hole definitions must use the
		 * opposite winding order (CW/CCW) than the outer shape.
		 *
		 * @type {Array<Path>}
		 * @readonly
		 */
		this.holes = [];

	}

	/**
	 * Returns an array representing each contour of the holes
	 * as a list of 2D points.
	 *
	 * @param {number} divisions - The fineness of the result.
	 * @return {Array<Array<Vector2>>} The holes as a series of 2D points.
	 */
	getPointsHoles( divisions ) {

		const holesPts = [];

		for ( let i = 0, l = this.holes.length; i < l; i ++ ) {

			holesPts[ i ] = this.holes[ i ].getPoints( divisions );

		}

		return holesPts;

	}

	// get points of shape and holes (keypoints based on segments parameter)

	/**
	 * Returns an object that holds contour data for the shape and its holes as
	 * arrays of 2D points.
	 *
	 * @param {number} divisions - The fineness of the result.
	 * @return {{shape:Array<Vector2>,holes:Array<Array<Vector2>>}} An object with contour data.
	 */
	extractPoints( divisions ) {

		return {

			shape: this.getPoints( divisions ),
			holes: this.getPointsHoles( divisions )

		};

	}

	copy( source ) {

		super.copy( source );

		this.holes = [];

		for ( let i = 0, l = source.holes.length; i < l; i ++ ) {

			const hole = source.holes[ i ];

			this.holes.push( hole.clone() );

		}

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		data.uuid = this.uuid;
		data.holes = [];

		for ( let i = 0, l = this.holes.length; i < l; i ++ ) {

			const hole = this.holes[ i ];
			data.holes.push( hole.toJSON() );

		}

		return data;

	}

	fromJSON( json ) {

		super.fromJSON( json );

		this.uuid = json.uuid;
		this.holes = [];

		for ( let i = 0, l = json.holes.length; i < l; i ++ ) {

			const hole = json.holes[ i ];
			this.holes.push( new Path$1().fromJSON( hole ) );

		}

		return this;

	}

}

/* eslint-disable */
// copy of mapbox/earcut version 3.0.1
// https://github.com/mapbox/earcut/tree/v3.0.1

function earcut(data, holeIndices, dim = 2) {

    const hasHoles = holeIndices && holeIndices.length;
    const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
    let outerNode = linkedList(data, 0, outerLen, dim, true);
    const triangles = [];

    if (!outerNode || outerNode.next === outerNode.prev) return triangles;

    let minX, minY, invSize;

    if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);

    // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
    if (data.length > 80 * dim) {
        minX = Infinity;
        minY = Infinity;
        let maxX = -Infinity;
        let maxY = -Infinity;

        for (let i = dim; i < outerLen; i += dim) {
            const x = data[i];
            const y = data[i + 1];
            if (x < minX) minX = x;
            if (y < minY) minY = y;
            if (x > maxX) maxX = x;
            if (y > maxY) maxY = y;
        }

        // minX, minY and invSize are later used to transform coords into integers for z-order calculation
        invSize = Math.max(maxX - minX, maxY - minY);
        invSize = invSize !== 0 ? 32767 / invSize : 0;
    }

    earcutLinked(outerNode, triangles, dim, minX, minY, invSize, 0);

    return triangles;
}

// create a circular doubly linked list from polygon points in the specified winding order
function linkedList(data, start, end, dim, clockwise) {
    let last;

    if (clockwise === (signedArea(data, start, end, dim) > 0)) {
        for (let i = start; i < end; i += dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
    } else {
        for (let i = end - dim; i >= start; i -= dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
    }

    if (last && equals(last, last.next)) {
        removeNode(last);
        last = last.next;
    }

    return last;
}

// eliminate colinear or duplicate points
function filterPoints(start, end) {
    if (!start) return start;
    if (!end) end = start;

    let p = start,
        again;
    do {
        again = false;

        if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
            removeNode(p);
            p = end = p.prev;
            if (p === p.next) break;
            again = true;

        } else {
            p = p.next;
        }
    } while (again || p !== end);

    return end;
}

// main ear slicing loop which triangulates a polygon (given as a linked list)
function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
    if (!ear) return;

    // interlink polygon nodes in z-order
    if (!pass && invSize) indexCurve(ear, minX, minY, invSize);

    let stop = ear;

    // iterate through ears, slicing them one by one
    while (ear.prev !== ear.next) {
        const prev = ear.prev;
        const next = ear.next;

        if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
            triangles.push(prev.i, ear.i, next.i); // cut off the triangle

            removeNode(ear);

            // skipping the next vertex leads to less sliver triangles
            ear = next.next;
            stop = next.next;

            continue;
        }

        ear = next;

        // if we looped through the whole remaining polygon and can't find any more ears
        if (ear === stop) {
            // try filtering points and slicing again
            if (!pass) {
                earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1);

            // if this didn't work, try curing all small self-intersections locally
            } else if (pass === 1) {
                ear = cureLocalIntersections(filterPoints(ear), triangles);
                earcutLinked(ear, triangles, dim, minX, minY, invSize, 2);

            // as a last resort, try splitting the remaining polygon into two
            } else if (pass === 2) {
                splitEarcut(ear, triangles, dim, minX, minY, invSize);
            }

            break;
        }
    }
}

// check whether a polygon node forms a valid ear with adjacent nodes
function isEar(ear) {
    const a = ear.prev,
        b = ear,
        c = ear.next;

    if (area(a, b, c) >= 0) return false; // reflex, can't be an ear

    // now make sure we don't have other points inside the potential ear
    const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;

    // triangle bbox
    const x0 = Math.min(ax, bx, cx),
        y0 = Math.min(ay, by, cy),
        x1 = Math.max(ax, bx, cx),
        y1 = Math.max(ay, by, cy);

    let p = c.next;
    while (p !== a) {
        if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 &&
            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&
            area(p.prev, p, p.next) >= 0) return false;
        p = p.next;
    }

    return true;
}

function isEarHashed(ear, minX, minY, invSize) {
    const a = ear.prev,
        b = ear,
        c = ear.next;

    if (area(a, b, c) >= 0) return false; // reflex, can't be an ear

    const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;

    // triangle bbox
    const x0 = Math.min(ax, bx, cx),
        y0 = Math.min(ay, by, cy),
        x1 = Math.max(ax, bx, cx),
        y1 = Math.max(ay, by, cy);

    // z-order range for the current triangle bbox;
    const minZ = zOrder(x0, y0, minX, minY, invSize),
        maxZ = zOrder(x1, y1, minX, minY, invSize);

    let p = ear.prevZ,
        n = ear.nextZ;

    // look for points inside the triangle in both directions
    while (p && p.z >= minZ && n && n.z <= maxZ) {
        if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
        p = p.prevZ;

        if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
        n = n.nextZ;
    }

    // look for remaining points in decreasing z-order
    while (p && p.z >= minZ) {
        if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
        p = p.prevZ;
    }

    // look for remaining points in increasing z-order
    while (n && n.z <= maxZ) {
        if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
        n = n.nextZ;
    }

    return true;
}

// go through all polygon nodes and cure small local self-intersections
function cureLocalIntersections(start, triangles) {
    let p = start;
    do {
        const a = p.prev,
            b = p.next.next;

        if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {

            triangles.push(a.i, p.i, b.i);

            // remove two nodes involved
            removeNode(p);
            removeNode(p.next);

            p = start = b;
        }
        p = p.next;
    } while (p !== start);

    return filterPoints(p);
}

// try splitting polygon into two and triangulate them independently
function splitEarcut(start, triangles, dim, minX, minY, invSize) {
    // look for a valid diagonal that divides the polygon into two
    let a = start;
    do {
        let b = a.next.next;
        while (b !== a.prev) {
            if (a.i !== b.i && isValidDiagonal(a, b)) {
                // split the polygon in two by the diagonal
                let c = splitPolygon(a, b);

                // filter colinear points around the cuts
                a = filterPoints(a, a.next);
                c = filterPoints(c, c.next);

                // run earcut on each half
                earcutLinked(a, triangles, dim, minX, minY, invSize, 0);
                earcutLinked(c, triangles, dim, minX, minY, invSize, 0);
                return;
            }
            b = b.next;
        }
        a = a.next;
    } while (a !== start);
}

// link every hole into the outer loop, producing a single-ring polygon without holes
function eliminateHoles(data, holeIndices, outerNode, dim) {
    const queue = [];

    for (let i = 0, len = holeIndices.length; i < len; i++) {
        const start = holeIndices[i] * dim;
        const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
        const list = linkedList(data, start, end, dim, false);
        if (list === list.next) list.steiner = true;
        queue.push(getLeftmost(list));
    }

    queue.sort(compareXYSlope);

    // process holes from left to right
    for (let i = 0; i < queue.length; i++) {
        outerNode = eliminateHole(queue[i], outerNode);
    }

    return outerNode;
}

function compareXYSlope(a, b) {
    let result = a.x - b.x;
    // when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find
    // the bridge to the outer shell is always the point that they meet at.
    if (result === 0) {
        result = a.y - b.y;
        if (result === 0) {
            const aSlope = (a.next.y - a.y) / (a.next.x - a.x);
            const bSlope = (b.next.y - b.y) / (b.next.x - b.x);
            result = aSlope - bSlope;
        }
    }
    return result;
}

// find a bridge between vertices that connects hole with an outer ring and and link it
function eliminateHole(hole, outerNode) {
    const bridge = findHoleBridge(hole, outerNode);
    if (!bridge) {
        return outerNode;
    }

    const bridgeReverse = splitPolygon(bridge, hole);

    // filter collinear points around the cuts
    filterPoints(bridgeReverse, bridgeReverse.next);
    return filterPoints(bridge, bridge.next);
}

// David Eberly's algorithm for finding a bridge between hole and outer polygon
function findHoleBridge(hole, outerNode) {
    let p = outerNode;
    const hx = hole.x;
    const hy = hole.y;
    let qx = -Infinity;
    let m;

    // find a segment intersected by a ray from the hole's leftmost point to the left;
    // segment's endpoint with lesser x will be potential connection point
    // unless they intersect at a vertex, then choose the vertex
    if (equals(hole, p)) return p;
    do {
        if (equals(hole, p.next)) return p.next;
        else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
            const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
            if (x <= hx && x > qx) {
                qx = x;
                m = p.x < p.next.x ? p : p.next;
                if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint
            }
        }
        p = p.next;
    } while (p !== outerNode);

    if (!m) return null;

    // look for points inside the triangle of hole point, segment intersection and endpoint;
    // if there are no points found, we have a valid connection;
    // otherwise choose the point of the minimum angle with the ray as connection point

    const stop = m;
    const mx = m.x;
    const my = m.y;
    let tanMin = Infinity;

    p = m;

    do {
        if (hx >= p.x && p.x >= mx && hx !== p.x &&
                pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {

            const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential

            if (locallyInside(p, hole) &&
                (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
                m = p;
                tanMin = tan;
            }
        }

        p = p.next;
    } while (p !== stop);

    return m;
}

// whether sector in vertex m contains sector in vertex p in the same coordinates
function sectorContainsSector(m, p) {
    return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;
}

// interlink polygon nodes in z-order
function indexCurve(start, minX, minY, invSize) {
    let p = start;
    do {
        if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize);
        p.prevZ = p.prev;
        p.nextZ = p.next;
        p = p.next;
    } while (p !== start);

    p.prevZ.nextZ = null;
    p.prevZ = null;

    sortLinked(p);
}

// Simon Tatham's linked list merge sort algorithm
// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
function sortLinked(list) {
    let numMerges;
    let inSize = 1;

    do {
        let p = list;
        let e;
        list = null;
        let tail = null;
        numMerges = 0;

        while (p) {
            numMerges++;
            let q = p;
            let pSize = 0;
            for (let i = 0; i < inSize; i++) {
                pSize++;
                q = q.nextZ;
                if (!q) break;
            }
            let qSize = inSize;

            while (pSize > 0 || (qSize > 0 && q)) {

                if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
                    e = p;
                    p = p.nextZ;
                    pSize--;
                } else {
                    e = q;
                    q = q.nextZ;
                    qSize--;
                }

                if (tail) tail.nextZ = e;
                else list = e;

                e.prevZ = tail;
                tail = e;
            }

            p = q;
        }

        tail.nextZ = null;
        inSize *= 2;

    } while (numMerges > 1);

    return list;
}

// z-order of a point given coords and inverse of the longer side of data bbox
function zOrder(x, y, minX, minY, invSize) {
    // coords are transformed into non-negative 15-bit integer range
    x = (x - minX) * invSize | 0;
    y = (y - minY) * invSize | 0;

    x = (x | (x << 8)) & 0x00FF00FF;
    x = (x | (x << 4)) & 0x0F0F0F0F;
    x = (x | (x << 2)) & 0x33333333;
    x = (x | (x << 1)) & 0x55555555;

    y = (y | (y << 8)) & 0x00FF00FF;
    y = (y | (y << 4)) & 0x0F0F0F0F;
    y = (y | (y << 2)) & 0x33333333;
    y = (y | (y << 1)) & 0x55555555;

    return x | (y << 1);
}

// find the leftmost node of a polygon ring
function getLeftmost(start) {
    let p = start,
        leftmost = start;
    do {
        if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p;
        p = p.next;
    } while (p !== start);

    return leftmost;
}

// check if a point lies within a convex triangle
function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
    return (cx - px) * (ay - py) >= (ax - px) * (cy - py) &&
           (ax - px) * (by - py) >= (bx - px) * (ay - py) &&
           (bx - px) * (cy - py) >= (cx - px) * (by - py);
}

// check if a point lies within a convex triangle but false if its equal to the first point of the triangle
function pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) {
    return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py);
}

// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
function isValidDiagonal(a, b) {
    return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // doesn't intersect other edges
           (locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
            (area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors
            equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case
}

// signed area of a triangle
function area(p, q, r) {
    return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
}

// check if two points are equal
function equals(p1, p2) {
    return p1.x === p2.x && p1.y === p2.y;
}

// check if two segments intersect
function intersects(p1, q1, p2, q2) {
    const o1 = sign(area(p1, q1, p2));
    const o2 = sign(area(p1, q1, q2));
    const o3 = sign(area(p2, q2, p1));
    const o4 = sign(area(p2, q2, q1));

    if (o1 !== o2 && o3 !== o4) return true; // general case

    if (o1 === 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1
    if (o2 === 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1
    if (o3 === 0 && onSegment(p2, p1, q2)) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2
    if (o4 === 0 && onSegment(p2, q1, q2)) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2

    return false;
}

// for collinear points p, q, r, check if point q lies on segment pr
function onSegment(p, q, r) {
    return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y);
}

function sign(num) {
    return num > 0 ? 1 : num < 0 ? -1 : 0;
}

// check if a polygon diagonal intersects any polygon segments
function intersectsPolygon(a, b) {
    let p = a;
    do {
        if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
                intersects(p, p.next, a, b)) return true;
        p = p.next;
    } while (p !== a);

    return false;
}

// check if a polygon diagonal is locally inside the polygon
function locallyInside(a, b) {
    return area(a.prev, a, a.next) < 0 ?
        area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 :
        area(a, b, a.prev) < 0 || area(a, a.next, b) < 0;
}

// check if the middle point of a polygon diagonal is inside the polygon
function middleInside(a, b) {
    let p = a;
    let inside = false;
    const px = (a.x + b.x) / 2;
    const py = (a.y + b.y) / 2;
    do {
        if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
                (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
            inside = !inside;
        p = p.next;
    } while (p !== a);

    return inside;
}

// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
// if one belongs to the outer ring and another to a hole, it merges it into a single ring
function splitPolygon(a, b) {
    const a2 = createNode(a.i, a.x, a.y),
        b2 = createNode(b.i, b.x, b.y),
        an = a.next,
        bp = b.prev;

    a.next = b;
    b.prev = a;

    a2.next = an;
    an.prev = a2;

    b2.next = a2;
    a2.prev = b2;

    bp.next = b2;
    b2.prev = bp;

    return b2;
}

// create a node and optionally link it with previous one (in a circular doubly linked list)
function insertNode(i, x, y, last) {
    const p = createNode(i, x, y);

    if (!last) {
        p.prev = p;
        p.next = p;

    } else {
        p.next = last.next;
        p.prev = last;
        last.next.prev = p;
        last.next = p;
    }
    return p;
}

function removeNode(p) {
    p.next.prev = p.prev;
    p.prev.next = p.next;

    if (p.prevZ) p.prevZ.nextZ = p.nextZ;
    if (p.nextZ) p.nextZ.prevZ = p.prevZ;
}

function createNode(i, x, y) {
    return {
        i, // vertex index in coordinates array
        x, y, // vertex coordinates
        prev: null, // previous and next vertex nodes in a polygon ring
        next: null,
        z: 0, // z-order curve value
        prevZ: null, // previous and next nodes in z-order
        nextZ: null,
        steiner: false // indicates whether this is a steiner point
    };
}

function signedArea(data, start, end, dim) {
    let sum = 0;
    for (let i = start, j = end - dim; i < end; i += dim) {
        sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);
        j = i;
    }
    return sum;
}

class Earcut {

	/**
	 * Triangulates the given shape definition by returning an array of triangles.
	 *
	 * @param {Array<number>} data - An array with 2D points.
	 * @param {Array<number>} holeIndices - An array with indices defining holes.
	 * @param {number} [dim=2] - The number of coordinates per vertex in the input array.
	 * @return {Array<number>} An array representing the triangulated faces. Each face is defined by three consecutive numbers
	 * representing vertex indices.
	 */
	static triangulate( data, holeIndices, dim = 2 ) {

		return earcut( data, holeIndices, dim );

	}

}

/**
 * A class containing utility functions for shapes.
 *
 * @hideconstructor
 */
class ShapeUtils {

	/**
	 * Calculate area of a ( 2D ) contour polygon.
	 *
	 * @param {Array<Vector2>} contour - An array of 2D points.
	 * @return {number} The area.
	 */
	static area( contour ) {

		const n = contour.length;
		let a = 0.0;

		for ( let p = n - 1, q = 0; q < n; p = q ++ ) {

			a += contour[ p ].x * contour[ q ].y - contour[ q ].x * contour[ p ].y;

		}

		return a * 0.5;

	}

	/**
	 * Returns `true` if the given contour uses a clockwise winding order.
	 *
	 * @param {Array<Vector2>} pts - An array of 2D points defining a polygon.
	 * @return {boolean} Whether the given contour uses a clockwise winding order or not.
	 */
	static isClockWise( pts ) {

		return ShapeUtils.area( pts ) < 0;

	}

	/**
	 * Triangulates the given shape definition.
	 *
	 * @param {Array<Vector2>} contour - An array of 2D points defining the contour.
	 * @param {Array<Array<Vector2>>} holes - An array that holds arrays of 2D points defining the holes.
	 * @return {Array<Array<number>>} An array that holds for each face definition an array with three indices.
	 */
	static triangulateShape( contour, holes ) {

		const vertices = []; // flat array of vertices like [ x0,y0, x1,y1, x2,y2, ... ]
		const holeIndices = []; // array of hole indices
		const faces = []; // final array of vertex indices like [ [ a,b,d ], [ b,c,d ] ]

		removeDupEndPts( contour );
		addContour( vertices, contour );

		//

		let holeIndex = contour.length;

		holes.forEach( removeDupEndPts );

		for ( let i = 0; i < holes.length; i ++ ) {

			holeIndices.push( holeIndex );
			holeIndex += holes[ i ].length;
			addContour( vertices, holes[ i ] );

		}

		//

		const triangles = Earcut.triangulate( vertices, holeIndices );

		//

		for ( let i = 0; i < triangles.length; i += 3 ) {

			faces.push( triangles.slice( i, i + 3 ) );

		}

		return faces;

	}

}

function removeDupEndPts( points ) {

	const l = points.length;

	if ( l > 2 && points[ l - 1 ].equals( points[ 0 ] ) ) {

		points.pop();

	}

}

function addContour( vertices, contour ) {

	for ( let i = 0; i < contour.length; i ++ ) {

		vertices.push( contour[ i ].x );
		vertices.push( contour[ i ].y );

	}

}

/**
 * Creates extruded geometry from a path shape.
 *
 * ```js
 * const length = 12, width = 8;
 *
 * const shape = new THREE.Shape();
 * shape.moveTo( 0,0 );
 * shape.lineTo( 0, width );
 * shape.lineTo( length, width );
 * shape.lineTo( length, 0 );
 * shape.lineTo( 0, 0 );
 *
 * const geometry = new THREE.ExtrudeGeometry( shape );
 * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
 * const mesh = new THREE.Mesh( geometry, material ) ;
 * scene.add( mesh );
 * ```
 *
 * @augments BufferGeometry
 */
class ExtrudeGeometry extends BufferGeometry {

	/**
	 * Constructs a new extrude geometry.
	 *
	 * @param {Shape|Array<Shape>} [shapes] - A shape or an array of shapes.
	 * @param {ExtrudeGeometry~Options} [options] - The extrude settings.
	 */
	constructor( shapes = new Shape( [ new Vector2( 0.5, 0.5 ), new Vector2( -0.5, 0.5 ), new Vector2( -0.5, -0.5 ), new Vector2( 0.5, -0.5 ) ] ), options = {} ) {

		super();

		this.type = 'ExtrudeGeometry';

		/**
		 * Holds the constructor parameters that have been
		 * used to generate the geometry. Any modification
		 * after instantiation does not change the geometry.
		 *
		 * @type {Object}
		 */
		this.parameters = {
			shapes: shapes,
			options: options
		};

		shapes = Array.isArray( shapes ) ? shapes : [ shapes ];

		const scope = this;

		const verticesArray = [];
		const uvArray = [];

		for ( let i = 0, l = shapes.length; i < l; i ++ ) {

			const shape = shapes[ i ];
			addShape( shape );

		}

		// build geometry

		this.setAttribute( 'position', new Float32BufferAttribute( verticesArray, 3 ) );
		this.setAttribute( 'uv', new Float32BufferAttribute( uvArray, 2 ) );

		this.computeVertexNormals();

		// functions

		function addShape( shape ) {

			const placeholder = [];

			// options

			const curveSegments = options.curveSegments !== undefined ? options.curveSegments : 12;
			const steps = options.steps !== undefined ? options.steps : 1;
			const depth = options.depth !== undefined ? options.depth : 1;

			let bevelEnabled = options.bevelEnabled !== undefined ? options.bevelEnabled : true;
			let bevelThickness = options.bevelThickness !== undefined ? options.bevelThickness : 0.2;
			let bevelSize = options.bevelSize !== undefined ? options.bevelSize : bevelThickness - 0.1;
			let bevelOffset = options.bevelOffset !== undefined ? options.bevelOffset : 0;
			let bevelSegments = options.bevelSegments !== undefined ? options.bevelSegments : 3;

			const extrudePath = options.extrudePath;

			const uvgen = options.UVGenerator !== undefined ? options.UVGenerator : WorldUVGenerator;

			//

			let extrudePts, extrudeByPath = false;
			let splineTube, binormal, normal, position2;

			if ( extrudePath ) {

				extrudePts = extrudePath.getSpacedPoints( steps );

				extrudeByPath = true;
				bevelEnabled = false; // bevels not supported for path extrusion

				// SETUP TNB variables

				// TODO1 - have a .isClosed in spline?

				splineTube = extrudePath.computeFrenetFrames( steps, false );

				// console.log(splineTube, 'splineTube', splineTube.normals.length, 'steps', steps, 'extrudePts', extrudePts.length);

				binormal = new Vector3();
				normal = new Vector3();
				position2 = new Vector3();

			}

			// Safeguards if bevels are not enabled

			if ( ! bevelEnabled ) {

				bevelSegments = 0;
				bevelThickness = 0;
				bevelSize = 0;
				bevelOffset = 0;

			}

			// Variables initialization

			const shapePoints = shape.extractPoints( curveSegments );

			let vertices = shapePoints.shape;
			const holes = shapePoints.holes;

			const reverse = ! ShapeUtils.isClockWise( vertices );

			if ( reverse ) {

				vertices = vertices.reverse();

				// Maybe we should also check if holes are in the opposite direction, just to be safe ...

				for ( let h = 0, hl = holes.length; h < hl; h ++ ) {

					const ahole = holes[ h ];

					if ( ShapeUtils.isClockWise( ahole ) ) {

						holes[ h ] = ahole.reverse();

					}

				}

			}

			/**Merges index-adjacent points that are within a threshold distance of each other. Array is modified in-place. Threshold distance is empirical, and scaled based on the magnitude of point coordinates.
			 * @param {Array<Vector2>} points
			*/
			function mergeOverlappingPoints( points ) {

				const THRESHOLD = 1e-10;
				const THRESHOLD_SQ = THRESHOLD * THRESHOLD;
				let prevPos = points[ 0 ];
				for ( let i = 1; i <= points.length; i ++ ) {

					const currentIndex = i % points.length;
					const currentPos = points[ currentIndex ];
					const dx = currentPos.x - prevPos.x;
					const dy = currentPos.y - prevPos.y;
					const distSq = dx * dx + dy * dy;

					const scalingFactorSqrt = Math.max(
						Math.abs( currentPos.x ),
						Math.abs( currentPos.y ),
						Math.abs( prevPos.x ),
						Math.abs( prevPos.y )
					);
					const thresholdSqScaled = THRESHOLD_SQ * scalingFactorSqrt * scalingFactorSqrt;
					if ( distSq <= thresholdSqScaled ) {

						points.splice( currentIndex, 1 );
						i --;
						continue;

					}

					prevPos = currentPos;

				}

			}

			mergeOverlappingPoints( vertices );
			holes.forEach( mergeOverlappingPoints );

			const numHoles = holes.length;

			/* Vertices */

			const contour = vertices; // vertices has all points but contour has only points of circumference

			for ( let h = 0; h < numHoles; h ++ ) {

				const ahole = holes[ h ];

				vertices = vertices.concat( ahole );

			}


			function scalePt2( pt, vec, size ) {

				if ( ! vec ) console.error( 'THREE.ExtrudeGeometry: vec does not exist' );

				return pt.clone().addScaledVector( vec, size );

			}

			const vlen = vertices.length;


			// Find directions for point movement


			function getBevelVec( inPt, inPrev, inNext ) {

				// computes for inPt the corresponding point inPt' on a new contour
				//   shifted by 1 unit (length of normalized vector) to the left
				// if we walk along contour clockwise, this new contour is outside the old one
				//
				// inPt' is the intersection of the two lines parallel to the two
				//  adjacent edges of inPt at a distance of 1 unit on the left side.

				let v_trans_x, v_trans_y, shrink_by; // resulting translation vector for inPt

				// good reading for geometry algorithms (here: line-line intersection)
				// http://geomalgorithms.com/a05-_intersect-1.html

				const v_prev_x = inPt.x - inPrev.x,
					v_prev_y = inPt.y - inPrev.y;
				const v_next_x = inNext.x - inPt.x,
					v_next_y = inNext.y - inPt.y;

				const v_prev_lensq = ( v_prev_x * v_prev_x + v_prev_y * v_prev_y );

				// check for collinear edges
				const collinear0 = ( v_prev_x * v_next_y - v_prev_y * v_next_x );

				if ( Math.abs( collinear0 ) > Number.EPSILON ) {

					// not collinear

					// length of vectors for normalizing

					const v_prev_len = Math.sqrt( v_prev_lensq );
					const v_next_len = Math.sqrt( v_next_x * v_next_x + v_next_y * v_next_y );

					// shift adjacent points by unit vectors to the left

					const ptPrevShift_x = ( inPrev.x - v_prev_y / v_prev_len );
					const ptPrevShift_y = ( inPrev.y + v_prev_x / v_prev_len );

					const ptNextShift_x = ( inNext.x - v_next_y / v_next_len );
					const ptNextShift_y = ( inNext.y + v_next_x / v_next_len );

					// scaling factor for v_prev to intersection point

					const sf = ( ( ptNextShift_x - ptPrevShift_x ) * v_next_y -
							( ptNextShift_y - ptPrevShift_y ) * v_next_x ) /
						( v_prev_x * v_next_y - v_prev_y * v_next_x );

					// vector from inPt to intersection point

					v_trans_x = ( ptPrevShift_x + v_prev_x * sf - inPt.x );
					v_trans_y = ( ptPrevShift_y + v_prev_y * sf - inPt.y );

					// Don't normalize!, otherwise sharp corners become ugly
					//  but prevent crazy spikes
					const v_trans_lensq = ( v_trans_x * v_trans_x + v_trans_y * v_trans_y );
					if ( v_trans_lensq <= 2 ) {

						return new Vector2( v_trans_x, v_trans_y );

					} else {

						shrink_by = Math.sqrt( v_trans_lensq / 2 );

					}

				} else {

					// handle special case of collinear edges

					let direction_eq = false; // assumes: opposite

					if ( v_prev_x > Number.EPSILON ) {

						if ( v_next_x > Number.EPSILON ) {

							direction_eq = true;

						}

					} else {

						if ( v_prev_x < - Number.EPSILON ) {

							if ( v_next_x < - Number.EPSILON ) {

								direction_eq = true;

							}

						} else {

							if ( Math.sign( v_prev_y ) === Math.sign( v_next_y ) ) {

								direction_eq = true;

							}

						}

					}

					if ( direction_eq ) {

						// console.log("Warning: lines are a straight sequence");
						v_trans_x = - v_prev_y;
						v_trans_y = v_prev_x;
						shrink_by = Math.sqrt( v_prev_lensq );

					} else {

						// console.log("Warning: lines are a straight spike");
						v_trans_x = v_prev_x;
						v_trans_y = v_prev_y;
						shrink_by = Math.sqrt( v_prev_lensq / 2 );

					}

				}

				return new Vector2( v_trans_x / shrink_by, v_trans_y / shrink_by );

			}


			const contourMovements = [];

			for ( let i = 0, il = contour.length, j = il - 1, k = i + 1; i < il; i ++, j ++, k ++ ) {

				if ( j === il ) j = 0;
				if ( k === il ) k = 0;

				//  (j)---(i)---(k)
				// console.log('i,j,k', i, j , k)

				contourMovements[ i ] = getBevelVec( contour[ i ], contour[ j ], contour[ k ] );

			}

			const holesMovements = [];
			let oneHoleMovements, verticesMovements = contourMovements.concat();

			for ( let h = 0, hl = numHoles; h < hl; h ++ ) {

				const ahole = holes[ h ];

				oneHoleMovements = [];

				for ( let i = 0, il = ahole.length, j = il - 1, k = i + 1; i < il; i ++, j ++, k ++ ) {

					if ( j === il ) j = 0;
					if ( k === il ) k = 0;

					//  (j)---(i)---(k)
					oneHoleMovements[ i ] = getBevelVec( ahole[ i ], ahole[ j ], ahole[ k ] );

				}

				holesMovements.push( oneHoleMovements );
				verticesMovements = verticesMovements.concat( oneHoleMovements );

			}

			let faces;

			if ( bevelSegments === 0 ) {

				faces = ShapeUtils.triangulateShape( contour, holes );

			} else {

				const contractedContourVertices = [];
				const expandedHoleVertices = [];

				// Loop bevelSegments, 1 for the front, 1 for the back

				for ( let b = 0; b < bevelSegments; b ++ ) {

					//for ( b = bevelSegments; b > 0; b -- ) {

					const t = b / bevelSegments;
					const z = bevelThickness * Math.cos( t * Math.PI / 2 );
					const bs = bevelSize * Math.sin( t * Math.PI / 2 ) + bevelOffset;

					// contract shape

					for ( let i = 0, il = contour.length; i < il; i ++ ) {

						const vert = scalePt2( contour[ i ], contourMovements[ i ], bs );

						v( vert.x, vert.y, - z );
						if ( t === 0 ) contractedContourVertices.push( vert );

					}

					// expand holes

					for ( let h = 0, hl = numHoles; h < hl; h ++ ) {

						const ahole = holes[ h ];
						oneHoleMovements = holesMovements[ h ];
						const oneHoleVertices = [];
						for ( let i = 0, il = ahole.length; i < il; i ++ ) {

							const vert = scalePt2( ahole[ i ], oneHoleMovements[ i ], bs );

							v( vert.x, vert.y, - z );
							if ( t === 0 ) oneHoleVertices.push( vert );

						}

						if ( t === 0 ) expandedHoleVertices.push( oneHoleVertices );

					}

				}

				faces = ShapeUtils.triangulateShape( contractedContourVertices, expandedHoleVertices );

			}

			const flen = faces.length;

			const bs = bevelSize + bevelOffset;

			// Back facing vertices

			for ( let i = 0; i < vlen; i ++ ) {

				const vert = bevelEnabled ? scalePt2( vertices[ i ], verticesMovements[ i ], bs ) : vertices[ i ];

				if ( ! extrudeByPath ) {

					v( vert.x, vert.y, 0 );

				} else {

					// v( vert.x, vert.y + extrudePts[ 0 ].y, extrudePts[ 0 ].x );

					normal.copy( splineTube.normals[ 0 ] ).multiplyScalar( vert.x );
					binormal.copy( splineTube.binormals[ 0 ] ).multiplyScalar( vert.y );

					position2.copy( extrudePts[ 0 ] ).add( normal ).add( binormal );

					v( position2.x, position2.y, position2.z );

				}

			}

			// Add stepped vertices...
			// Including front facing vertices

			for ( let s = 1; s <= steps; s ++ ) {

				for ( let i = 0; i < vlen; i ++ ) {

					const vert = bevelEnabled ? scalePt2( vertices[ i ], verticesMovements[ i ], bs ) : vertices[ i ];

					if ( ! extrudeByPath ) {

						v( vert.x, vert.y, depth / steps * s );

					} else {

						// v( vert.x, vert.y + extrudePts[ s - 1 ].y, extrudePts[ s - 1 ].x );

						normal.copy( splineTube.normals[ s ] ).multiplyScalar( vert.x );
						binormal.copy( splineTube.binormals[ s ] ).multiplyScalar( vert.y );

						position2.copy( extrudePts[ s ] ).add( normal ).add( binormal );

						v( position2.x, position2.y, position2.z );

					}

				}

			}


			// Add bevel segments planes

			//for ( b = 1; b <= bevelSegments; b ++ ) {
			for ( let b = bevelSegments - 1; b >= 0; b -- ) {

				const t = b / bevelSegments;
				const z = bevelThickness * Math.cos( t * Math.PI / 2 );
				const bs = bevelSize * Math.sin( t * Math.PI / 2 ) + bevelOffset;

				// contract shape

				for ( let i = 0, il = contour.length; i < il; i ++ ) {

					const vert = scalePt2( contour[ i ], contourMovements[ i ], bs );
					v( vert.x, vert.y, depth + z );

				}

				// expand holes

				for ( let h = 0, hl = holes.length; h < hl; h ++ ) {

					const ahole = holes[ h ];
					oneHoleMovements = holesMovements[ h ];

					for ( let i = 0, il = ahole.length; i < il; i ++ ) {

						const vert = scalePt2( ahole[ i ], oneHoleMovements[ i ], bs );

						if ( ! extrudeByPath ) {

							v( vert.x, vert.y, depth + z );

						} else {

							v( vert.x, vert.y + extrudePts[ steps - 1 ].y, extrudePts[ steps - 1 ].x + z );

						}

					}

				}

			}

			/* Faces */

			// Top and bottom faces

			buildLidFaces();

			// Sides faces

			buildSideFaces();


			/////  Internal functions

			function buildLidFaces() {

				const start = verticesArray.length / 3;

				if ( bevelEnabled ) {

					let layer = 0; // steps + 1
					let offset = vlen * layer;

					// Bottom faces

					for ( let i = 0; i < flen; i ++ ) {

						const face = faces[ i ];
						f3( face[ 2 ] + offset, face[ 1 ] + offset, face[ 0 ] + offset );

					}

					layer = steps + bevelSegments * 2;
					offset = vlen * layer;

					// Top faces

					for ( let i = 0; i < flen; i ++ ) {

						const face = faces[ i ];
						f3( face[ 0 ] + offset, face[ 1 ] + offset, face[ 2 ] + offset );

					}

				} else {

					// Bottom faces

					for ( let i = 0; i < flen; i ++ ) {

						const face = faces[ i ];
						f3( face[ 2 ], face[ 1 ], face[ 0 ] );

					}

					// Top faces

					for ( let i = 0; i < flen; i ++ ) {

						const face = faces[ i ];
						f3( face[ 0 ] + vlen * steps, face[ 1 ] + vlen * steps, face[ 2 ] + vlen * steps );

					}

				}

				scope.addGroup( start, verticesArray.length / 3 - start, 0 );

			}

			// Create faces for the z-sides of the shape

			function buildSideFaces() {

				const start = verticesArray.length / 3;
				let layeroffset = 0;
				sidewalls( contour, layeroffset );
				layeroffset += contour.length;

				for ( let h = 0, hl = holes.length; h < hl; h ++ ) {

					const ahole = holes[ h ];
					sidewalls( ahole, layeroffset );

					//, true
					layeroffset += ahole.length;

				}


				scope.addGroup( start, verticesArray.length / 3 - start, 1 );


			}

			function sidewalls( contour, layeroffset ) {

				let i = contour.length;

				while ( -- i >= 0 ) {

					const j = i;
					let k = i - 1;
					if ( k < 0 ) k = contour.length - 1;

					//console.log('b', i,j, i-1, k,vertices.length);

					for ( let s = 0, sl = ( steps + bevelSegments * 2 ); s < sl; s ++ ) {

						const slen1 = vlen * s;
						const slen2 = vlen * ( s + 1 );

						const a = layeroffset + j + slen1,
							b = layeroffset + k + slen1,
							c = layeroffset + k + slen2,
							d = layeroffset + j + slen2;

						f4( a, b, c, d );

					}

				}

			}

			function v( x, y, z ) {

				placeholder.push( x );
				placeholder.push( y );
				placeholder.push( z );

			}


			function f3( a, b, c ) {

				addVertex( a );
				addVertex( b );
				addVertex( c );

				const nextIndex = verticesArray.length / 3;
				const uvs = uvgen.generateTopUV( scope, verticesArray, nextIndex - 3, nextIndex - 2, nextIndex - 1 );

				addUV( uvs[ 0 ] );
				addUV( uvs[ 1 ] );
				addUV( uvs[ 2 ] );

			}

			function f4( a, b, c, d ) {

				addVertex( a );
				addVertex( b );
				addVertex( d );

				addVertex( b );
				addVertex( c );
				addVertex( d );


				const nextIndex = verticesArray.length / 3;
				const uvs = uvgen.generateSideWallUV( scope, verticesArray, nextIndex - 6, nextIndex - 3, nextIndex - 2, nextIndex - 1 );

				addUV( uvs[ 0 ] );
				addUV( uvs[ 1 ] );
				addUV( uvs[ 3 ] );

				addUV( uvs[ 1 ] );
				addUV( uvs[ 2 ] );
				addUV( uvs[ 3 ] );

			}

			function addVertex( index ) {

				verticesArray.push( placeholder[ index * 3 + 0 ] );
				verticesArray.push( placeholder[ index * 3 + 1 ] );
				verticesArray.push( placeholder[ index * 3 + 2 ] );

			}


			function addUV( vector2 ) {

				uvArray.push( vector2.x );
				uvArray.push( vector2.y );

			}

		}

	}

	copy( source ) {

		super.copy( source );

		this.parameters = Object.assign( {}, source.parameters );

		return this;

	}

	toJSON() {

		const data = super.toJSON();

		const shapes = this.parameters.shapes;
		const options = this.parameters.options;

		return toJSON$1( shapes, options, data );

	}

	/**
	 * Factory method for creating an instance of this class from the given
	 * JSON object.
	 *
	 * @param {Object} data - A JSON object representing the serialized geometry.
	 * @param {Array<Shape>} shapes - An array of shapes.
	 * @return {ExtrudeGeometry} A new instance.
	 */
	static fromJSON( data, shapes ) {

		const geometryShapes = [];

		for ( let j = 0, jl = data.shapes.length; j < jl; j ++ ) {

			const shape = shapes[ data.shapes[ j ] ];

			geometryShapes.push( shape );

		}

		const extrudePath = data.options.extrudePath;

		if ( extrudePath !== undefined ) {

			data.options.extrudePath = new Curves[ extrudePath.type ]().fromJSON( extrudePath );

		}

		return new ExtrudeGeometry( geometryShapes, data.options );

	}

}

const WorldUVGenerator = {

	generateTopUV: function ( geometry, vertices, indexA, indexB, indexC ) {

		const a_x = vertices[ indexA * 3 ];
		const a_y = vertices[ indexA * 3 + 1 ];
		const b_x = vertices[ indexB * 3 ];
		const b_y = vertices[ indexB * 3 + 1 ];
		const c_x = vertices[ indexC * 3 ];
		const c_y = vertices[ indexC * 3 + 1 ];

		return [
			new Vector2( a_x, a_y ),
			new Vector2( b_x, b_y ),
			new Vector2( c_x, c_y )
		];

	},

	generateSideWallUV: function ( geometry, vertices, indexA, indexB, indexC, indexD ) {

		const a_x = vertices[ indexA * 3 ];
		const a_y = vertices[ indexA * 3 + 1 ];
		const a_z = vertices[ indexA * 3 + 2 ];
		const b_x = vertices[ indexB * 3 ];
		const b_y = vertices[ indexB * 3 + 1 ];
		const b_z = vertices[ indexB * 3 + 2 ];
		const c_x = vertices[ indexC * 3 ];
		const c_y = vertices[ indexC * 3 + 1 ];
		const c_z = vertices[ indexC * 3 + 2 ];
		const d_x = vertices[ indexD * 3 ];
		const d_y = vertices[ indexD * 3 + 1 ];
		const d_z = vertices[ indexD * 3 + 2 ];

		if ( Math.abs( a_y - b_y ) < Math.abs( a_x - b_x ) ) {

			return [
				new Vector2( a_x, 1 - a_z ),
				new Vector2( b_x, 1 - b_z ),
				new Vector2( c_x, 1 - c_z ),
				new Vector2( d_x, 1 - d_z )
			];

		} else {

			return [
				new Vector2( a_y, 1 - a_z ),
				new Vector2( b_y, 1 - b_z ),
				new Vector2( c_y, 1 - c_z ),
				new Vector2( d_y, 1 - d_z )
			];

		}

	}

};

function toJSON$1( shapes, options, data ) {

	data.shapes = [];

	if ( Array.isArray( shapes ) ) {

		for ( let i = 0, l = shapes.length; i < l; i ++ ) {

			const shape = shapes[ i ];

			data.shapes.push( shape.uuid );

		}

	} else {

		data.shapes.push( shapes.uuid );

	}

	data.options = Object.assign( {}, options );

	if ( options.extrudePath !== undefined ) data.options.extrudePath = options.extrudePath.toJSON();

	return data;

}

/**
 * A class for generating a sphere geometry.
 *
 * ```js
 * const geometry = new THREE.SphereGeometry( 15, 32, 16 );
 * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
 * const sphere = new THREE.Mesh( geometry, material );
 * scene.add( sphere );
 * ```
 *
 * @augments BufferGeometry
 */
class SphereGeometry extends BufferGeometry {

	/**
	 * Constructs a new sphere geometry.
	 *
	 * @param {number} [radius=1] - The sphere radius.
	 * @param {number} [widthSegments=32] - The number of horizontal segments. Minimum value is `3`.
	 * @param {number} [heightSegments=16] - The number of vertical segments. Minimum value is `2`.
	 * @param {number} [phiStart=0] - The horizontal starting angle in radians.
	 * @param {number} [phiLength=Math.PI*2] - The horizontal sweep angle size.
	 * @param {number} [thetaStart=0] - The vertical starting angle in radians.
	 * @param {number} [thetaLength=Math.PI] - The vertical sweep angle size.
	 */
	constructor( radius = 1, widthSegments = 32, heightSegments = 16, phiStart = 0, phiLength = Math.PI * 2, thetaStart = 0, thetaLength = Math.PI ) {

		super();

		this.type = 'SphereGeometry';

		/**
		 * Holds the constructor parameters that have been
		 * used to generate the geometry. Any modification
		 * after instantiation does not change the geometry.
		 *
		 * @type {Object}
		 */
		this.parameters = {
			radius: radius,
			widthSegments: widthSegments,
			heightSegments: heightSegments,
			phiStart: phiStart,
			phiLength: phiLength,
			thetaStart: thetaStart,
			thetaLength: thetaLength
		};

		widthSegments = Math.max( 3, Math.floor( widthSegments ) );
		heightSegments = Math.max( 2, Math.floor( heightSegments ) );

		const thetaEnd = Math.min( thetaStart + thetaLength, Math.PI );

		let index = 0;
		const grid = [];

		const vertex = new Vector3();
		const normal = new Vector3();

		// buffers

		const indices = [];
		const vertices = [];
		const normals = [];
		const uvs = [];

		// generate vertices, normals and uvs

		for ( let iy = 0; iy <= heightSegments; iy ++ ) {

			const verticesRow = [];

			const v = iy / heightSegments;

			// special case for the poles

			let uOffset = 0;

			if ( iy === 0 && thetaStart === 0 ) {

				uOffset = 0.5 / widthSegments;

			} else if ( iy === heightSegments && thetaEnd === Math.PI ) {

				uOffset = -0.5 / widthSegments;

			}

			for ( let ix = 0; ix <= widthSegments; ix ++ ) {

				const u = ix / widthSegments;

				// vertex

				vertex.x = - radius * Math.cos( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );
				vertex.y = radius * Math.cos( thetaStart + v * thetaLength );
				vertex.z = radius * Math.sin( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );

				vertices.push( vertex.x, vertex.y, vertex.z );

				// normal

				normal.copy( vertex ).normalize();
				normals.push( normal.x, normal.y, normal.z );

				// uv

				uvs.push( u + uOffset, 1 - v );

				verticesRow.push( index ++ );

			}

			grid.push( verticesRow );

		}

		// indices

		for ( let iy = 0; iy < heightSegments; iy ++ ) {

			for ( let ix = 0; ix < widthSegments; ix ++ ) {

				const a = grid[ iy ][ ix + 1 ];
				const b = grid[ iy ][ ix ];
				const c = grid[ iy + 1 ][ ix ];
				const d = grid[ iy + 1 ][ ix + 1 ];

				if ( iy !== 0 || thetaStart > 0 ) indices.push( a, b, d );
				if ( iy !== heightSegments - 1 || thetaEnd < Math.PI ) indices.push( b, c, d );

			}

		}

		// build geometry

		this.setIndex( indices );
		this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
		this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
		this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

	}

	copy( source ) {

		super.copy( source );

		this.parameters = Object.assign( {}, source.parameters );

		return this;

	}

	/**
	 * Factory method for creating an instance of this class from the given
	 * JSON object.
	 *
	 * @param {Object} data - A JSON object representing the serialized geometry.
	 * @return {SphereGeometry} A new instance.
	 */
	static fromJSON( data ) {

		return new SphereGeometry( data.radius, data.widthSegments, data.heightSegments, data.phiStart, data.phiLength, data.thetaStart, data.thetaLength );

	}

}

/**
 * A standard physically based material, using Metallic-Roughness workflow.
 *
 * Physically based rendering (PBR) has recently become the standard in many
 * 3D applications, such as [Unity]{@link https://blogs.unity3d.com/2014/10/29/physically-based-shading-in-unity-5-a-primer/},
 * [Unreal]{@link https://docs.unrealengine.com/latest/INT/Engine/Rendering/Materials/PhysicallyBased/} and
 * [3D Studio Max]{@link http://area.autodesk.com/blogs/the-3ds-max-blog/what039s-new-for-rendering-in-3ds-max-2017}.
 *
 * This approach differs from older approaches in that instead of using
 * approximations for the way in which light interacts with a surface, a
 * physically correct model is used. The idea is that, instead of tweaking
 * materials to look good under specific lighting, a material can be created
 * that will react 'correctly' under all lighting scenarios.
 *
 * In practice this gives a more accurate and realistic looking result than
 * the {@link MeshLambertMaterial} or {@link MeshPhongMaterial}, at the cost of
 * being somewhat more computationally expensive. `MeshStandardMaterial` uses per-fragment
 * shading.
 *
 * Note that for best results you should always specify an environment map when using this material.
 *
 * For a non-technical introduction to the concept of PBR and how to set up a
 * PBR material, check out these articles by the people at [marmoset]{@link https://www.marmoset.co}:
 *
 * - [Basic Theory of Physically Based Rendering]{@link https://www.marmoset.co/posts/basic-theory-of-physically-based-rendering/}
 * - [Physically Based Rendering and You Can Too]{@link https://www.marmoset.co/posts/physically-based-rendering-and-you-can-too/}
 *
 * Technical details of the approach used in three.js (and most other PBR systems) can be found is this
 * [paper from Disney]{@link https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf}
 * (pdf), by Brent Burley.
 *
 * @augments Material
 */
class MeshStandardMaterial extends Material {

	/**
	 * Constructs a new mesh standard material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isMeshStandardMaterial = true;

		this.type = 'MeshStandardMaterial';

		this.defines = { 'STANDARD': '' };

		/**
		 * Color of the material.
		 *
		 * @type {Color}
		 * @default (1,1,1)
		 */
		this.color = new Color( 0xffffff ); // diffuse

		/**
		 * How rough the material appears. `0.0` means a smooth mirror reflection, `1.0`
		 * means fully diffuse. If `roughnessMap` is also provided,
		 * both values are multiplied.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.roughness = 1.0;

		/**
		 * How much the material is like a metal. Non-metallic materials such as wood
		 * or stone use `0.0`, metallic use `1.0`, with nothing (usually) in between.
		 * A value between `0.0` and `1.0` could be used for a rusty metal look.
		 * If `metalnessMap` is also provided, both values are multiplied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.metalness = 0.0;

		/**
		 * The color map. May optionally include an alpha channel, typically combined
		 * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
		 * color is modulated by the diffuse `color`.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.map = null;

		/**
		 * The light map. Requires a second set of UVs.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.lightMap = null;

		/**
		 * Intensity of the baked light.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.lightMapIntensity = 1.0;

		/**
		 * The red channel of this texture is used as the ambient occlusion map.
		 * Requires a second set of UVs.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.aoMap = null;

		/**
		 * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`
		 * disables ambient occlusion. Where intensity is `1` and the AO map's
		 * red channel is also `1`, ambient light is fully occluded on a surface.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.aoMapIntensity = 1.0;

		/**
		 * Emissive (light) color of the material, essentially a solid color
		 * unaffected by other lighting.
		 *
		 * @type {Color}
		 * @default (0,0,0)
		 */
		this.emissive = new Color( 0x000000 );

		/**
		 * Intensity of the emissive light. Modulates the emissive color.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.emissiveIntensity = 1.0;

		/**
		 * Set emissive (glow) map. The emissive map color is modulated by the
		 * emissive color and the emissive intensity. If you have an emissive map,
		 * be sure to set the emissive color to something other than black.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.emissiveMap = null;

		/**
		 * The texture to create a bump map. The black and white values map to the
		 * perceived depth in relation to the lights. Bump doesn't actually affect
		 * the geometry of the object, only the lighting. If a normal map is defined
		 * this will be ignored.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.bumpMap = null;

		/**
		 * How much the bump map affects the material. Typical range is `[0,1]`.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.bumpScale = 1;

		/**
		 * The texture to create a normal map. The RGB values affect the surface
		 * normal for each pixel fragment and change the way the color is lit. Normal
		 * maps do not change the actual shape of the surface, only the lighting. In
		 * case the material has a normal map authored using the left handed
		 * convention, the `y` component of `normalScale` should be negated to compensate
		 * for the different handedness.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.normalMap = null;

		/**
		 * The type of normal map.
		 *
		 * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}
		 * @default TangentSpaceNormalMap
		 */
		this.normalMapType = TangentSpaceNormalMap;

		/**
		 * How much the normal map affects the material. Typical value range is `[0,1]`.
		 *
		 * @type {Vector2}
		 * @default (1,1)
		 */
		this.normalScale = new Vector2( 1, 1 );

		/**
		 * The displacement map affects the position of the mesh's vertices. Unlike
		 * other maps which only affect the light and shade of the material the
		 * displaced vertices can cast shadows, block other objects, and otherwise
		 * act as real geometry. The displacement texture is an image where the value
		 * of each pixel (white being the highest) is mapped against, and
		 * repositions, the vertices of the mesh.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.displacementMap = null;

		/**
		 * How much the displacement map affects the mesh (where black is no
		 * displacement, and white is maximum displacement). Without a displacement
		 * map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementScale = 1;

		/**
		 * The offset of the displacement map's values on the mesh's vertices.
		 * The bias is added to the scaled sample of the displacement map.
		 * Without a displacement map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementBias = 0;

		/**
		 * The green channel of this texture is used to alter the roughness of the
		 * material.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.roughnessMap = null;

		/**
		 * The blue channel of this texture is used to alter the metalness of the
		 * material.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.metalnessMap = null;

		/**
		 * The alpha map is a grayscale texture that controls the opacity across the
		 * surface (black: fully transparent; white: fully opaque).
		 *
		 * Only the color of the texture is used, ignoring the alpha channel if one
		 * exists. For RGB and RGBA textures, the renderer will use the green channel
		 * when sampling this texture due to the extra bit of precision provided for
		 * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
		 * luminance/alpha textures will also still work as expected.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.alphaMap = null;

		/**
		 * The environment map. To ensure a physically correct rendering, environment maps
		 * are internally pre-processed with {@link PMREMGenerator}.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.envMap = null;

		/**
		 * The rotation of the environment map in radians.
		 *
		 * @type {Euler}
		 * @default (0,0,0)
		 */
		this.envMapRotation = new Euler();

		/**
		 * Scales the effect of the environment map by multiplying its color.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.envMapIntensity = 1.0;

		/**
		 * Renders the geometry as a wireframe.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.wireframe = false;

		/**
		 * Controls the thickness of the wireframe.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.wireframeLinewidth = 1;

		/**
		 * Defines appearance of wireframe ends.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {('round'|'bevel'|'miter')}
		 * @default 'round'
		 */
		this.wireframeLinecap = 'round';

		/**
		 * Defines appearance of wireframe joints.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {('round'|'bevel'|'miter')}
		 * @default 'round'
		 */
		this.wireframeLinejoin = 'round';

		/**
		 * Whether the material is rendered with flat shading or not.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.flatShading = false;

		/**
		 * Whether the material is affected by fog or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.fog = true;

		this.setValues( parameters );

	}

	copy( source ) {

		super.copy( source );

		this.defines = { 'STANDARD': '' };

		this.color.copy( source.color );
		this.roughness = source.roughness;
		this.metalness = source.metalness;

		this.map = source.map;

		this.lightMap = source.lightMap;
		this.lightMapIntensity = source.lightMapIntensity;

		this.aoMap = source.aoMap;
		this.aoMapIntensity = source.aoMapIntensity;

		this.emissive.copy( source.emissive );
		this.emissiveMap = source.emissiveMap;
		this.emissiveIntensity = source.emissiveIntensity;

		this.bumpMap = source.bumpMap;
		this.bumpScale = source.bumpScale;

		this.normalMap = source.normalMap;
		this.normalMapType = source.normalMapType;
		this.normalScale.copy( source.normalScale );

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		this.roughnessMap = source.roughnessMap;

		this.metalnessMap = source.metalnessMap;

		this.alphaMap = source.alphaMap;

		this.envMap = source.envMap;
		this.envMapRotation.copy( source.envMapRotation );
		this.envMapIntensity = source.envMapIntensity;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;
		this.wireframeLinecap = source.wireframeLinecap;
		this.wireframeLinejoin = source.wireframeLinejoin;

		this.flatShading = source.flatShading;

		this.fog = source.fog;

		return this;

	}

}

/**
 * An extension of the {@link MeshStandardMaterial}, providing more advanced
 * physically-based rendering properties:
 *
 * - Anisotropy: Ability to represent the anisotropic property of materials
 * as observable with brushed metals.
 * - Clearcoat: Some materials — like car paints, carbon fiber, and wet surfaces — require
 * a clear, reflective layer on top of another layer that may be irregular or rough.
 * Clearcoat approximates this effect, without the need for a separate transparent surface.
 * - Iridescence: Allows to render the effect where hue varies  depending on the viewing
 * angle and illumination angle. This can be seen on soap bubbles, oil films, or on the
 * wings of many insects.
 * - Physically-based transparency: One limitation of {@link Material#opacity} is that highly
 * transparent materials are less reflective. Physically-based transmission provides a more
 * realistic option for thin, transparent surfaces like glass.
 * - Advanced reflectivity: More flexible reflectivity for non-metallic materials.
 * - Sheen: Can be used for representing cloth and fabric materials.
 *
 * As a result of these complex shading features, `MeshPhysicalMaterial` has a
 * higher performance cost, per pixel, than other three.js materials. Most
 * effects are disabled by default, and add cost as they are enabled. For
 * best results, always specify an environment map when using this material.
 *
 * @augments MeshStandardMaterial
 */
class MeshPhysicalMaterial extends MeshStandardMaterial {

	/**
	 * Constructs a new mesh physical material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isMeshPhysicalMaterial = true;

		this.defines = {

			'STANDARD': '',
			'PHYSICAL': ''

		};

		this.type = 'MeshPhysicalMaterial';

		/**
		 * The rotation of the anisotropy in tangent, bitangent space, measured in radians
		 * counter-clockwise from the tangent. When `anisotropyMap` is present, this
		 * property provides additional rotation to the vectors in the texture.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.anisotropyRotation = 0;

		/**
		 * Red and green channels represent the anisotropy direction in `[-1, 1]` tangent,
		 * bitangent space, to be rotated by `anisotropyRotation`. The blue channel
		 * contains strength as `[0, 1]` to be multiplied by `anisotropy`.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.anisotropyMap = null;

		/**
		 * The red channel of this texture is multiplied against `clearcoat`,
		 * for per-pixel control over a coating's intensity.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.clearcoatMap = null;

		/**
		 * Roughness of the clear coat layer, from `0.0` to `1.0`.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.clearcoatRoughness = 0.0;

		/**
		 * The green channel of this texture is multiplied against
		 * `clearcoatRoughness`, for per-pixel control over a coating's roughness.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.clearcoatRoughnessMap = null;

		/**
		 * How much `clearcoatNormalMap` affects the clear coat layer, from
		 * `(0,0)` to `(1,1)`.
		 *
		 * @type {Vector2}
		 * @default (1,1)
		 */
		this.clearcoatNormalScale = new Vector2( 1, 1 );

		/**
		 * Can be used to enable independent normals for the clear coat layer.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.clearcoatNormalMap = null;

		/**
		 * Index-of-refraction for non-metallic materials, from `1.0` to `2.333`.
		 *
		 * @type {number}
		 * @default 1.5
		 */
		this.ior = 1.5;

		/**
		 * Degree of reflectivity, from `0.0` to `1.0`. Default is `0.5`, which
		 * corresponds to an index-of-refraction of `1.5`.
		 *
		 * This models the reflectivity of non-metallic materials. It has no effect
		 * when `metalness` is `1.0`
		 *
		 * @name MeshPhysicalMaterial#reflectivity
		 * @type {number}
		 * @default 0.5
		 */
		Object.defineProperty( this, 'reflectivity', {
			get: function () {

				return ( clamp( 2.5 * ( this.ior - 1 ) / ( this.ior + 1 ), 0, 1 ) );

			},
			set: function ( reflectivity ) {

				this.ior = ( 1 + 0.4 * reflectivity ) / ( 1 - 0.4 * reflectivity );

			}
		} );

		/**
		 * The red channel of this texture is multiplied against `iridescence`, for per-pixel
		 * control over iridescence.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.iridescenceMap = null;

		/**
		 * Strength of the iridescence RGB color shift effect, represented by an index-of-refraction.
		 * Between `1.0` to `2.333`.
		 *
		 * @type {number}
		 * @default 1.3
		 */
		this.iridescenceIOR = 1.3;

		/**
		 *Array of exactly 2 elements, specifying minimum and maximum thickness of the iridescence layer.
		 Thickness of iridescence layer has an equivalent effect of the one `thickness` has on `ior`.
		 *
		 * @type {Array<number,number>}
		 * @default [100,400]
		 */
		this.iridescenceThicknessRange = [ 100, 400 ];

		/**
		 * A texture that defines the thickness of the iridescence layer, stored in the green channel.
		 * Minimum and maximum values of thickness are defined by `iridescenceThicknessRange` array:
		 * - `0.0` in the green channel will result in thickness equal to first element of the array.
		 * - `1.0` in the green channel will result in thickness equal to second element of the array.
		 * - Values in-between will linearly interpolate between the elements of the array.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.iridescenceThicknessMap = null;

		/**
		 * The sheen tint.
		 *
		 * @type {Color}
		 * @default (0,0,0)
		 */
		this.sheenColor = new Color( 0x000000 );

		/**
		 * The RGB channels of this texture are multiplied against  `sheenColor`, for per-pixel control
		 * over sheen tint.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.sheenColorMap = null;

		/**
		 * Roughness of the sheen layer, from `0.0` to `1.0`.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.sheenRoughness = 1.0;

		/**
		 * The alpha channel of this texture is multiplied against `sheenRoughness`, for per-pixel control
		 * over sheen roughness.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.sheenRoughnessMap = null;

		/**
		 * The red channel of this texture is multiplied against `transmission`, for per-pixel control over
		 * optical transparency.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.transmissionMap = null;

		/**
		 * The thickness of the volume beneath the surface. The value is given in the
		 * coordinate space of the mesh. If the value is `0` the material is
		 * thin-walled. Otherwise the material is a volume boundary.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.thickness = 0;

		/**
		 * A texture that defines the thickness, stored in the green channel. This will
		 * be multiplied by `thickness`.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.thicknessMap = null;

		/**
		 * Density of the medium given as the average distance that light travels in
		 * the medium before interacting with a particle. The value is given in world
		 * space units, and must be greater than zero.
		 *
		 * @type {number}
		 * @default Infinity
		 */
		this.attenuationDistance = Infinity;

		/**
		 * The color that white light turns into due to absorption when reaching the
		 * attenuation distance.
		 *
		 * @type {Color}
		 * @default (1,1,1)
		 */
		this.attenuationColor = new Color( 1, 1, 1 );

		/**
		 * A float that scales the amount of specular reflection for non-metals only.
		 * When set to zero, the model is effectively Lambertian. From `0.0` to `1.0`.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.specularIntensity = 1.0;

		/**
		 * The alpha channel of this texture is multiplied against `specularIntensity`,
		 * for per-pixel control over specular intensity.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.specularIntensityMap = null;

		/**
		 * Tints the specular reflection at normal incidence for non-metals only.
		 *
		 * @type {Color}
		 * @default (1,1,1)
		 */
		this.specularColor = new Color( 1, 1, 1 );

		/**
		 * The RGB channels of this texture are multiplied against `specularColor`,
		 * for per-pixel control over specular color.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.specularColorMap = null;

		this._anisotropy = 0;
		this._clearcoat = 0;
		this._dispersion = 0;
		this._iridescence = 0;
		this._sheen = 0.0;
		this._transmission = 0;

		this.setValues( parameters );

	}

	/**
	 * The anisotropy strength.
	 *
	 * @type {number}
	 * @default 0
	 */
	get anisotropy() {

		return this._anisotropy;

	}

	set anisotropy( value ) {

		if ( this._anisotropy > 0 !== value > 0 ) {

			this.version ++;

		}

		this._anisotropy = value;

	}

	/**
	 * Represents the intensity of the clear coat layer, from `0.0` to `1.0`. Use
	 * clear coat related properties to enable multilayer materials that have a
	 * thin translucent layer over the base layer.
	 *
	 * @type {number}
	 * @default 0
	 */
	get clearcoat() {

		return this._clearcoat;

	}

	set clearcoat( value ) {

		if ( this._clearcoat > 0 !== value > 0 ) {

			this.version ++;

		}

		this._clearcoat = value;

	}
	/**
	 * The intensity of the iridescence layer, simulating RGB color shift based on the angle between
	 * the surface and the viewer, from `0.0` to `1.0`.
	 *
	 * @type {number}
	 * @default 0
	 */
	get iridescence() {

		return this._iridescence;

	}

	set iridescence( value ) {

		if ( this._iridescence > 0 !== value > 0 ) {

			this.version ++;

		}

		this._iridescence = value;

	}

	/**
	 * Defines the strength of the angular separation of colors (chromatic aberration) transmitting
	 * through a relatively clear volume. Any value zero or larger is valid, the typical range of
	 * realistic values is `[0, 1]`. This property can be only be used with transmissive objects.
	 *
	 * @type {number}
	 * @default 0
	 */
	get dispersion() {

		return this._dispersion;

	}

	set dispersion( value ) {

		if ( this._dispersion > 0 !== value > 0 ) {

			this.version ++;

		}

		this._dispersion = value;

	}

	/**
	 * The intensity of the sheen layer, from `0.0` to `1.0`.
	 *
	 * @type {number}
	 * @default 0
	 */
	get sheen() {

		return this._sheen;

	}

	set sheen( value ) {

		if ( this._sheen > 0 !== value > 0 ) {

			this.version ++;

		}

		this._sheen = value;

	}

	/**
	 * Degree of transmission (or optical transparency), from `0.0` to `1.0`.
	 *
	 * Thin, transparent or semitransparent, plastic or glass materials remain
	 * largely reflective even if they are fully transmissive. The transmission
	 * property can be used to model these materials.
	 *
	 * When transmission is non-zero, `opacity` should be  set to `1`.
	 *
	 * @type {number}
	 * @default 0
	 */
	get transmission() {

		return this._transmission;

	}

	set transmission( value ) {

		if ( this._transmission > 0 !== value > 0 ) {

			this.version ++;

		}

		this._transmission = value;

	}

	copy( source ) {

		super.copy( source );

		this.defines = {

			'STANDARD': '',
			'PHYSICAL': ''

		};

		this.anisotropy = source.anisotropy;
		this.anisotropyRotation = source.anisotropyRotation;
		this.anisotropyMap = source.anisotropyMap;

		this.clearcoat = source.clearcoat;
		this.clearcoatMap = source.clearcoatMap;
		this.clearcoatRoughness = source.clearcoatRoughness;
		this.clearcoatRoughnessMap = source.clearcoatRoughnessMap;
		this.clearcoatNormalMap = source.clearcoatNormalMap;
		this.clearcoatNormalScale.copy( source.clearcoatNormalScale );

		this.dispersion = source.dispersion;
		this.ior = source.ior;

		this.iridescence = source.iridescence;
		this.iridescenceMap = source.iridescenceMap;
		this.iridescenceIOR = source.iridescenceIOR;
		this.iridescenceThicknessRange = [ ...source.iridescenceThicknessRange ];
		this.iridescenceThicknessMap = source.iridescenceThicknessMap;

		this.sheen = source.sheen;
		this.sheenColor.copy( source.sheenColor );
		this.sheenColorMap = source.sheenColorMap;
		this.sheenRoughness = source.sheenRoughness;
		this.sheenRoughnessMap = source.sheenRoughnessMap;

		this.transmission = source.transmission;
		this.transmissionMap = source.transmissionMap;

		this.thickness = source.thickness;
		this.thicknessMap = source.thicknessMap;
		this.attenuationDistance = source.attenuationDistance;
		this.attenuationColor.copy( source.attenuationColor );

		this.specularIntensity = source.specularIntensity;
		this.specularIntensityMap = source.specularIntensityMap;
		this.specularColor.copy( source.specularColor );
		this.specularColorMap = source.specularColorMap;

		return this;

	}

}

/**
 * A material for shiny surfaces with specular highlights.
 *
 * The material uses a non-physically based [Blinn-Phong]{@link https://en.wikipedia.org/wiki/Blinn-Phong_shading_model}
 * model for calculating reflectance. Unlike the Lambertian model used in the
 * {@link MeshLambertMaterial} this can simulate shiny surfaces with specular
 * highlights (such as varnished wood). `MeshPhongMaterial` uses per-fragment shading.
 *
 * Performance will generally be greater when using this material over the
 * {@link MeshStandardMaterial} or {@link MeshPhysicalMaterial}, at the cost of
 * some graphical accuracy.
 *
 * @augments Material
 */
class MeshPhongMaterial extends Material {

	/**
	 * Constructs a new mesh phong material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isMeshPhongMaterial = true;

		this.type = 'MeshPhongMaterial';

		/**
		 * Color of the material.
		 *
		 * @type {Color}
		 * @default (1,1,1)
		 */
		this.color = new Color( 0xffffff ); // diffuse

		/**
		 * Specular color of the material. The default color is set to `0x111111` (very dark grey)
		 *
		 * This defines how shiny the material is and the color of its shine.
		 *
		 * @type {Color}
		 */
		this.specular = new Color( 0x111111 );

		/**
		 * How shiny the specular highlight is; a higher value gives a sharper highlight.
		 *
		 * @type {number}
		 * @default 30
		 */
		this.shininess = 30;

		/**
		 * The color map. May optionally include an alpha channel, typically combined
		 * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
		 * color is modulated by the diffuse `color`.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.map = null;

		/**
		 * The light map. Requires a second set of UVs.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.lightMap = null;

		/**
		 * Intensity of the baked light.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.lightMapIntensity = 1.0;

		/**
		 * The red channel of this texture is used as the ambient occlusion map.
		 * Requires a second set of UVs.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.aoMap = null;

		/**
		 * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`
		 * disables ambient occlusion. Where intensity is `1` and the AO map's
		 * red channel is also `1`, ambient light is fully occluded on a surface.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.aoMapIntensity = 1.0;

		/**
		 * Emissive (light) color of the material, essentially a solid color
		 * unaffected by other lighting.
		 *
		 * @type {Color}
		 * @default (0,0,0)
		 */
		this.emissive = new Color( 0x000000 );

		/**
		 * Intensity of the emissive light. Modulates the emissive color.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.emissiveIntensity = 1.0;

		/**
		 * Set emissive (glow) map. The emissive map color is modulated by the
		 * emissive color and the emissive intensity. If you have an emissive map,
		 * be sure to set the emissive color to something other than black.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.emissiveMap = null;

		/**
		 * The texture to create a bump map. The black and white values map to the
		 * perceived depth in relation to the lights. Bump doesn't actually affect
		 * the geometry of the object, only the lighting. If a normal map is defined
		 * this will be ignored.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.bumpMap = null;

		/**
		 * How much the bump map affects the material. Typical range is `[0,1]`.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.bumpScale = 1;

		/**
		 * The texture to create a normal map. The RGB values affect the surface
		 * normal for each pixel fragment and change the way the color is lit. Normal
		 * maps do not change the actual shape of the surface, only the lighting. In
		 * case the material has a normal map authored using the left handed
		 * convention, the `y` component of `normalScale` should be negated to compensate
		 * for the different handedness.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.normalMap = null;

		/**
		 * The type of normal map.
		 *
		 * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}
		 * @default TangentSpaceNormalMap
		 */
		this.normalMapType = TangentSpaceNormalMap;

		/**
		 * How much the normal map affects the material. Typical value range is `[0,1]`.
		 *
		 * @type {Vector2}
		 * @default (1,1)
		 */
		this.normalScale = new Vector2( 1, 1 );

		/**
		 * The displacement map affects the position of the mesh's vertices. Unlike
		 * other maps which only affect the light and shade of the material the
		 * displaced vertices can cast shadows, block other objects, and otherwise
		 * act as real geometry. The displacement texture is an image where the value
		 * of each pixel (white being the highest) is mapped against, and
		 * repositions, the vertices of the mesh.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.displacementMap = null;

		/**
		 * How much the displacement map affects the mesh (where black is no
		 * displacement, and white is maximum displacement). Without a displacement
		 * map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementScale = 1;

		/**
		 * The offset of the displacement map's values on the mesh's vertices.
		 * The bias is added to the scaled sample of the displacement map.
		 * Without a displacement map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementBias = 0;

		/**
		 * The specular map value affects both how much the specular surface
		 * highlight contributes and how much of the environment map affects the
		 * surface.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.specularMap = null;

		/**
		 * The alpha map is a grayscale texture that controls the opacity across the
		 * surface (black: fully transparent; white: fully opaque).
		 *
		 * Only the color of the texture is used, ignoring the alpha channel if one
		 * exists. For RGB and RGBA textures, the renderer will use the green channel
		 * when sampling this texture due to the extra bit of precision provided for
		 * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
		 * luminance/alpha textures will also still work as expected.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.alphaMap = null;

		/**
		 * The environment map.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.envMap = null;

		/**
		 * The rotation of the environment map in radians.
		 *
		 * @type {Euler}
		 * @default (0,0,0)
		 */
		this.envMapRotation = new Euler();

		/**
		 * How to combine the result of the surface's color with the environment map, if any.
		 *
		 * When set to `MixOperation`, the {@link MeshBasicMaterial#reflectivity} is used to
		 * blend between the two colors.
		 *
		 * @type {(MultiplyOperation|MixOperation|AddOperation)}
		 * @default MultiplyOperation
		 */
		this.combine = MultiplyOperation;

		/**
		 * How much the environment map affects the surface.
		 * The valid range is between `0` (no reflections) and `1` (full reflections).
		 *
		 * @type {number}
		 * @default 1
		 */
		this.reflectivity = 1;

		/**
		 * The index of refraction (IOR) of air (approximately 1) divided by the
		 * index of refraction of the material. It is used with environment mapping
		 * modes {@link CubeRefractionMapping} and {@link EquirectangularRefractionMapping}.
		 * The refraction ratio should not exceed `1`.
		 *
		 * @type {number}
		 * @default 0.98
		 */
		this.refractionRatio = 0.98;

		/**
		 * Renders the geometry as a wireframe.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.wireframe = false;

		/**
		 * Controls the thickness of the wireframe.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.wireframeLinewidth = 1;

		/**
		 * Defines appearance of wireframe ends.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {('round'|'bevel'|'miter')}
		 * @default 'round'
		 */
		this.wireframeLinecap = 'round';

		/**
		 * Defines appearance of wireframe joints.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {('round'|'bevel'|'miter')}
		 * @default 'round'
		 */
		this.wireframeLinejoin = 'round';

		/**
		 * Whether the material is rendered with flat shading or not.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.flatShading = false;

		/**
		 * Whether the material is affected by fog or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.fog = true;

		this.setValues( parameters );

	}

	copy( source ) {

		super.copy( source );

		this.color.copy( source.color );
		this.specular.copy( source.specular );
		this.shininess = source.shininess;

		this.map = source.map;

		this.lightMap = source.lightMap;
		this.lightMapIntensity = source.lightMapIntensity;

		this.aoMap = source.aoMap;
		this.aoMapIntensity = source.aoMapIntensity;

		this.emissive.copy( source.emissive );
		this.emissiveMap = source.emissiveMap;
		this.emissiveIntensity = source.emissiveIntensity;

		this.bumpMap = source.bumpMap;
		this.bumpScale = source.bumpScale;

		this.normalMap = source.normalMap;
		this.normalMapType = source.normalMapType;
		this.normalScale.copy( source.normalScale );

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		this.specularMap = source.specularMap;

		this.alphaMap = source.alphaMap;

		this.envMap = source.envMap;
		this.envMapRotation.copy( source.envMapRotation );
		this.combine = source.combine;
		this.reflectivity = source.reflectivity;
		this.refractionRatio = source.refractionRatio;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;
		this.wireframeLinecap = source.wireframeLinecap;
		this.wireframeLinejoin = source.wireframeLinejoin;

		this.flatShading = source.flatShading;

		this.fog = source.fog;

		return this;

	}

}

/**
 * A material implementing toon shading.
 *
 * @augments Material
 */
class MeshToonMaterial extends Material {

	/**
	 * Constructs a new mesh toon material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isMeshToonMaterial = true;

		this.defines = { 'TOON': '' };

		this.type = 'MeshToonMaterial';

		/**
		 * Color of the material.
		 *
		 * @type {Color}
		 * @default (1,1,1)
		 */
		this.color = new Color( 0xffffff );

		/**
		 * The color map. May optionally include an alpha channel, typically combined
		 * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
		 * color is modulated by the diffuse `color`.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.map = null;

		/**
		 * Gradient map for toon shading. It's required to set
		 * {@link Texture#minFilter} and {@link Texture#magFilter} to {@linkNearestFilter}
		 * when using this type of texture.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.gradientMap = null;

		/**
		 * The light map. Requires a second set of UVs.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.lightMap = null;

		/**
		 * Intensity of the baked light.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.lightMapIntensity = 1.0;

		/**
		 * The red channel of this texture is used as the ambient occlusion map.
		 * Requires a second set of UVs.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.aoMap = null;

		/**
		 * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`
		 * disables ambient occlusion. Where intensity is `1` and the AO map's
		 * red channel is also `1`, ambient light is fully occluded on a surface.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.aoMapIntensity = 1.0;

		/**
		 * Emissive (light) color of the material, essentially a solid color
		 * unaffected by other lighting.
		 *
		 * @type {Color}
		 * @default (0,0,0)
		 */
		this.emissive = new Color( 0x000000 );

		/**
		 * Intensity of the emissive light. Modulates the emissive color.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.emissiveIntensity = 1.0;

		/**
		 * Set emissive (glow) map. The emissive map color is modulated by the
		 * emissive color and the emissive intensity. If you have an emissive map,
		 * be sure to set the emissive color to something other than black.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.emissiveMap = null;

		/**
		 * The texture to create a bump map. The black and white values map to the
		 * perceived depth in relation to the lights. Bump doesn't actually affect
		 * the geometry of the object, only the lighting. If a normal map is defined
		 * this will be ignored.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.bumpMap = null;

		/**
		 * How much the bump map affects the material. Typical range is `[0,1]`.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.bumpScale = 1;

		/**
		 * The texture to create a normal map. The RGB values affect the surface
		 * normal for each pixel fragment and change the way the color is lit. Normal
		 * maps do not change the actual shape of the surface, only the lighting. In
		 * case the material has a normal map authored using the left handed
		 * convention, the `y` component of `normalScale` should be negated to compensate
		 * for the different handedness.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.normalMap = null;

		/**
		 * The type of normal map.
		 *
		 * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}
		 * @default TangentSpaceNormalMap
		 */
		this.normalMapType = TangentSpaceNormalMap;

		/**
		 * How much the normal map affects the material. Typical value range is `[0,1]`.
		 *
		 * @type {Vector2}
		 * @default (1,1)
		 */
		this.normalScale = new Vector2( 1, 1 );

		/**
		 * The displacement map affects the position of the mesh's vertices. Unlike
		 * other maps which only affect the light and shade of the material the
		 * displaced vertices can cast shadows, block other objects, and otherwise
		 * act as real geometry. The displacement texture is an image where the value
		 * of each pixel (white being the highest) is mapped against, and
		 * repositions, the vertices of the mesh.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.displacementMap = null;

		/**
		 * How much the displacement map affects the mesh (where black is no
		 * displacement, and white is maximum displacement). Without a displacement
		 * map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementScale = 1;

		/**
		 * The offset of the displacement map's values on the mesh's vertices.
		 * The bias is added to the scaled sample of the displacement map.
		 * Without a displacement map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementBias = 0;

		/**
		 * The alpha map is a grayscale texture that controls the opacity across the
		 * surface (black: fully transparent; white: fully opaque).
		 *
		 * Only the color of the texture is used, ignoring the alpha channel if one
		 * exists. For RGB and RGBA textures, the renderer will use the green channel
		 * when sampling this texture due to the extra bit of precision provided for
		 * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
		 * luminance/alpha textures will also still work as expected.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.alphaMap = null;

		/**
		 * Renders the geometry as a wireframe.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.wireframe = false;

		/**
		 * Controls the thickness of the wireframe.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.wireframeLinewidth = 1;

		/**
		 * Defines appearance of wireframe ends.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {('round'|'bevel'|'miter')}
		 * @default 'round'
		 */
		this.wireframeLinecap = 'round';

		/**
		 * Defines appearance of wireframe joints.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {('round'|'bevel'|'miter')}
		 * @default 'round'
		 */
		this.wireframeLinejoin = 'round';

		/**
		 * Whether the material is affected by fog or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.fog = true;

		this.setValues( parameters );

	}

	copy( source ) {

		super.copy( source );

		this.color.copy( source.color );

		this.map = source.map;
		this.gradientMap = source.gradientMap;

		this.lightMap = source.lightMap;
		this.lightMapIntensity = source.lightMapIntensity;

		this.aoMap = source.aoMap;
		this.aoMapIntensity = source.aoMapIntensity;

		this.emissive.copy( source.emissive );
		this.emissiveMap = source.emissiveMap;
		this.emissiveIntensity = source.emissiveIntensity;

		this.bumpMap = source.bumpMap;
		this.bumpScale = source.bumpScale;

		this.normalMap = source.normalMap;
		this.normalMapType = source.normalMapType;
		this.normalScale.copy( source.normalScale );

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		this.alphaMap = source.alphaMap;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;
		this.wireframeLinecap = source.wireframeLinecap;
		this.wireframeLinejoin = source.wireframeLinejoin;

		this.fog = source.fog;

		return this;

	}

}

/**
 * A material that maps the normal vectors to RGB colors.
 *
 * @augments Material
 */
class MeshNormalMaterial extends Material {

	/**
	 * Constructs a new mesh normal material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isMeshNormalMaterial = true;

		this.type = 'MeshNormalMaterial';

		/**
		 * The texture to create a bump map. The black and white values map to the
		 * perceived depth in relation to the lights. Bump doesn't actually affect
		 * the geometry of the object, only the lighting. If a normal map is defined
		 * this will be ignored.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.bumpMap = null;

		/**
		 * How much the bump map affects the material. Typical range is `[0,1]`.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.bumpScale = 1;

		/**
		 * The texture to create a normal map. The RGB values affect the surface
		 * normal for each pixel fragment and change the way the color is lit. Normal
		 * maps do not change the actual shape of the surface, only the lighting. In
		 * case the material has a normal map authored using the left handed
		 * convention, the `y` component of `normalScale` should be negated to compensate
		 * for the different handedness.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.normalMap = null;

		/**
		 * The type of normal map.
		 *
		 * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}
		 * @default TangentSpaceNormalMap
		 */
		this.normalMapType = TangentSpaceNormalMap;

		/**
		 * How much the normal map affects the material. Typical value range is `[0,1]`.
		 *
		 * @type {Vector2}
		 * @default (1,1)
		 */
		this.normalScale = new Vector2( 1, 1 );

		/**
		 * The displacement map affects the position of the mesh's vertices. Unlike
		 * other maps which only affect the light and shade of the material the
		 * displaced vertices can cast shadows, block other objects, and otherwise
		 * act as real geometry. The displacement texture is an image where the value
		 * of each pixel (white being the highest) is mapped against, and
		 * repositions, the vertices of the mesh.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.displacementMap = null;

		/**
		 * How much the displacement map affects the mesh (where black is no
		 * displacement, and white is maximum displacement). Without a displacement
		 * map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementScale = 1;

		/**
		 * The offset of the displacement map's values on the mesh's vertices.
		 * The bias is added to the scaled sample of the displacement map.
		 * Without a displacement map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementBias = 0;

		/**
		 * Renders the geometry as a wireframe.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.wireframe = false;

		/**
		 * Controls the thickness of the wireframe.
		 *
		 * WebGL and WebGPU ignore this property and always render
		 * 1 pixel wide lines.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.wireframeLinewidth = 1;

		/**
		 * Whether the material is rendered with flat shading or not.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.flatShading = false;

		this.setValues( parameters );

	}

	copy( source ) {

		super.copy( source );

		this.bumpMap = source.bumpMap;
		this.bumpScale = source.bumpScale;

		this.normalMap = source.normalMap;
		this.normalMapType = source.normalMapType;
		this.normalScale.copy( source.normalScale );

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;

		this.flatShading = source.flatShading;

		return this;

	}

}

/**
 * A material for non-shiny surfaces, without specular highlights.
 *
 * The material uses a non-physically based [Lambertian]{@link https://en.wikipedia.org/wiki/Lambertian_reflectance}
 * model for calculating reflectance. This can simulate some surfaces (such
 * as untreated wood or stone) well, but cannot simulate shiny surfaces with
 * specular highlights (such as varnished wood). `MeshLambertMaterial` uses per-fragment
 * shading.
 *
 * Due to the simplicity of the reflectance and illumination models,
 * performance will be greater when using this material over the
 * {@link MeshPhongMaterial}, {@link MeshStandardMaterial} or
 * {@link MeshPhysicalMaterial}, at the cost of some graphical accuracy.
 *
 * @augments Material
 */
class MeshLambertMaterial extends Material {

	/**
	 * Constructs a new mesh lambert material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isMeshLambertMaterial = true;

		this.type = 'MeshLambertMaterial';

		/**
		 * Color of the material.
		 *
		 * @type {Color}
		 * @default (1,1,1)
		 */
		this.color = new Color( 0xffffff ); // diffuse

		/**
		 * The color map. May optionally include an alpha channel, typically combined
		 * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
		 * color is modulated by the diffuse `color`.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.map = null;

		/**
		 * The light map. Requires a second set of UVs.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.lightMap = null;

		/**
		 * Intensity of the baked light.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.lightMapIntensity = 1.0;

		/**
		 * The red channel of this texture is used as the ambient occlusion map.
		 * Requires a second set of UVs.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.aoMap = null;

		/**
		 * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`
		 * disables ambient occlusion. Where intensity is `1` and the AO map's
		 * red channel is also `1`, ambient light is fully occluded on a surface.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.aoMapIntensity = 1.0;

		/**
		 * Emissive (light) color of the material, essentially a solid color
		 * unaffected by other lighting.
		 *
		 * @type {Color}
		 * @default (0,0,0)
		 */
		this.emissive = new Color( 0x000000 );

		/**
		 * Intensity of the emissive light. Modulates the emissive color.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.emissiveIntensity = 1.0;

		/**
		 * Set emissive (glow) map. The emissive map color is modulated by the
		 * emissive color and the emissive intensity. If you have an emissive map,
		 * be sure to set the emissive color to something other than black.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.emissiveMap = null;

		/**
		 * The texture to create a bump map. The black and white values map to the
		 * perceived depth in relation to the lights. Bump doesn't actually affect
		 * the geometry of the object, only the lighting. If a normal map is defined
		 * this will be ignored.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.bumpMap = null;

		/**
		 * How much the bump map affects the material. Typical range is `[0,1]`.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.bumpScale = 1;

		/**
		 * The texture to create a normal map. The RGB values affect the surface
		 * normal for each pixel fragment and change the way the color is lit. Normal
		 * maps do not change the actual shape of the surface, only the lighting. In
		 * case the material has a normal map authored using the left handed
		 * convention, the `y` component of `normalScale` should be negated to compensate
		 * for the different handedness.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.normalMap = null;

		/**
		 * The type of normal map.
		 *
		 * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}
		 * @default TangentSpaceNormalMap
		 */
		this.normalMapType = TangentSpaceNormalMap;

		/**
		 * How much the normal map affects the material. Typical value range is `[0,1]`.
		 *
		 * @type {Vector2}
		 * @default (1,1)
		 */
		this.normalScale = new Vector2( 1, 1 );

		/**
		 * The displacement map affects the position of the mesh's vertices. Unlike
		 * other maps which only affect the light and shade of the material the
		 * displaced vertices can cast shadows, block other objects, and otherwise
		 * act as real geometry. The displacement texture is an image where the value
		 * of each pixel (white being the highest) is mapped against, and
		 * repositions, the vertices of the mesh.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.displacementMap = null;

		/**
		 * How much the displacement map affects the mesh (where black is no
		 * displacement, and white is maximum displacement). Without a displacement
		 * map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementScale = 1;

		/**
		 * The offset of the displacement map's values on the mesh's vertices.
		 * The bias is added to the scaled sample of the displacement map.
		 * Without a displacement map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementBias = 0;

		/**
		 * Specular map used by the material.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.specularMap = null;

		/**
		 * The alpha map is a grayscale texture that controls the opacity across the
		 * surface (black: fully transparent; white: fully opaque).
		 *
		 * Only the color of the texture is used, ignoring the alpha channel if one
		 * exists. For RGB and RGBA textures, the renderer will use the green channel
		 * when sampling this texture due to the extra bit of precision provided for
		 * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
		 * luminance/alpha textures will also still work as expected.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.alphaMap = null;

		/**
		 * The environment map.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.envMap = null;

		/**
		 * The rotation of the environment map in radians.
		 *
		 * @type {Euler}
		 * @default (0,0,0)
		 */
		this.envMapRotation = new Euler();

		/**
		 * How to combine the result of the surface's color with the environment map, if any.
		 *
		 * When set to `MixOperation`, the {@link MeshBasicMaterial#reflectivity} is used to
		 * blend between the two colors.
		 *
		 * @type {(MultiplyOperation|MixOperation|AddOperation)}
		 * @default MultiplyOperation
		 */
		this.combine = MultiplyOperation;

		/**
		 * How much the environment map affects the surface.
		 * The valid range is between `0` (no reflections) and `1` (full reflections).
		 *
		 * @type {number}
		 * @default 1
		 */
		this.reflectivity = 1;

		/**
		 * The index of refraction (IOR) of air (approximately 1) divided by the
		 * index of refraction of the material. It is used with environment mapping
		 * modes {@link CubeRefractionMapping} and {@link EquirectangularRefractionMapping}.
		 * The refraction ratio should not exceed `1`.
		 *
		 * @type {number}
		 * @default 0.98
		 */
		this.refractionRatio = 0.98;

		/**
		 * Renders the geometry as a wireframe.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.wireframe = false;

		/**
		 * Controls the thickness of the wireframe.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.wireframeLinewidth = 1;

		/**
		 * Defines appearance of wireframe ends.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {('round'|'bevel'|'miter')}
		 * @default 'round'
		 */
		this.wireframeLinecap = 'round';

		/**
		 * Defines appearance of wireframe joints.
		 *
		 * Can only be used with {@link SVGRenderer}.
		 *
		 * @type {('round'|'bevel'|'miter')}
		 * @default 'round'
		 */
		this.wireframeLinejoin = 'round';

		/**
		 * Whether the material is rendered with flat shading or not.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.flatShading = false;

		/**
		 * Whether the material is affected by fog or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.fog = true;

		this.setValues( parameters );

	}

	copy( source ) {

		super.copy( source );

		this.color.copy( source.color );

		this.map = source.map;

		this.lightMap = source.lightMap;
		this.lightMapIntensity = source.lightMapIntensity;

		this.aoMap = source.aoMap;
		this.aoMapIntensity = source.aoMapIntensity;

		this.emissive.copy( source.emissive );
		this.emissiveMap = source.emissiveMap;
		this.emissiveIntensity = source.emissiveIntensity;

		this.bumpMap = source.bumpMap;
		this.bumpScale = source.bumpScale;

		this.normalMap = source.normalMap;
		this.normalMapType = source.normalMapType;
		this.normalScale.copy( source.normalScale );

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		this.specularMap = source.specularMap;

		this.alphaMap = source.alphaMap;

		this.envMap = source.envMap;
		this.envMapRotation.copy( source.envMapRotation );
		this.combine = source.combine;
		this.reflectivity = source.reflectivity;
		this.refractionRatio = source.refractionRatio;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;
		this.wireframeLinecap = source.wireframeLinecap;
		this.wireframeLinejoin = source.wireframeLinejoin;

		this.flatShading = source.flatShading;

		this.fog = source.fog;

		return this;

	}

}

/**
 * This material is defined by a MatCap (or Lit Sphere) texture, which encodes the
 * material color and shading.
 *
 * `MeshMatcapMaterial` does not respond to lights since the matcap image file encodes
 * baked lighting. It will cast a shadow onto an object that receives shadows
 * (and shadow clipping works), but it will not self-shadow or receive
 * shadows.
 *
 * @augments Material
 */
class MeshMatcapMaterial extends Material {

	/**
	 * Constructs a new mesh matcap material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isMeshMatcapMaterial = true;

		this.defines = { 'MATCAP': '' };

		this.type = 'MeshMatcapMaterial';

		/**
		 * Color of the material.
		 *
		 * @type {Color}
		 * @default (1,1,1)
		 */
		this.color = new Color( 0xffffff ); // diffuse

		/**
		 * The matcap map.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.matcap = null;

		/**
		 * The color map. May optionally include an alpha channel, typically combined
		 * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
		 * color is modulated by the diffuse `color`.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.map = null;

		/**
		 * The texture to create a bump map. The black and white values map to the
		 * perceived depth in relation to the lights. Bump doesn't actually affect
		 * the geometry of the object, only the lighting. If a normal map is defined
		 * this will be ignored.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.bumpMap = null;

		/**
		 * How much the bump map affects the material. Typical range is `[0,1]`.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.bumpScale = 1;

		/**
		 * The texture to create a normal map. The RGB values affect the surface
		 * normal for each pixel fragment and change the way the color is lit. Normal
		 * maps do not change the actual shape of the surface, only the lighting. In
		 * case the material has a normal map authored using the left handed
		 * convention, the `y` component of `normalScale` should be negated to compensate
		 * for the different handedness.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.normalMap = null;

		/**
		 * The type of normal map.
		 *
		 * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}
		 * @default TangentSpaceNormalMap
		 */
		this.normalMapType = TangentSpaceNormalMap;

		/**
		 * How much the normal map affects the material. Typical value range is `[0,1]`.
		 *
		 * @type {Vector2}
		 * @default (1,1)
		 */
		this.normalScale = new Vector2( 1, 1 );

		/**
		 * The displacement map affects the position of the mesh's vertices. Unlike
		 * other maps which only affect the light and shade of the material the
		 * displaced vertices can cast shadows, block other objects, and otherwise
		 * act as real geometry. The displacement texture is an image where the value
		 * of each pixel (white being the highest) is mapped against, and
		 * repositions, the vertices of the mesh.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.displacementMap = null;

		/**
		 * How much the displacement map affects the mesh (where black is no
		 * displacement, and white is maximum displacement). Without a displacement
		 * map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementScale = 1;

		/**
		 * The offset of the displacement map's values on the mesh's vertices.
		 * The bias is added to the scaled sample of the displacement map.
		 * Without a displacement map set, this value is not applied.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.displacementBias = 0;

		/**
		 * The alpha map is a grayscale texture that controls the opacity across the
		 * surface (black: fully transparent; white: fully opaque).
		 *
		 * Only the color of the texture is used, ignoring the alpha channel if one
		 * exists. For RGB and RGBA textures, the renderer will use the green channel
		 * when sampling this texture due to the extra bit of precision provided for
		 * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
		 * luminance/alpha textures will also still work as expected.
		 *
		 * @type {?Texture}
		 * @default null
		 */
		this.alphaMap = null;

		/**
		 * Whether the material is rendered with flat shading or not.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.flatShading = false;

		/**
		 * Whether the material is affected by fog or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.fog = true;

		this.setValues( parameters );

	}


	copy( source ) {

		super.copy( source );

		this.defines = { 'MATCAP': '' };

		this.color.copy( source.color );

		this.matcap = source.matcap;

		this.map = source.map;

		this.bumpMap = source.bumpMap;
		this.bumpScale = source.bumpScale;

		this.normalMap = source.normalMap;
		this.normalMapType = source.normalMapType;
		this.normalScale.copy( source.normalScale );

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		this.alphaMap = source.alphaMap;

		this.flatShading = source.flatShading;

		this.fog = source.fog;

		return this;

	}

}

/**
 * A material for rendering line primitives.
 *
 * Materials define the appearance of renderable 3D objects.
 *
 * ```js
 * const material = new THREE.LineDashedMaterial( {
 * 	color: 0xffffff,
 * 	scale: 1,
 * 	dashSize: 3,
 * 	gapSize: 1,
 * } );
 * ```
 *
 * @augments LineBasicMaterial
 */
class LineDashedMaterial extends LineBasicMaterial {

	/**
	 * Constructs a new line dashed material.
	 *
	 * @param {Object} [parameters] - An object with one or more properties
	 * defining the material's appearance. Any property of the material
	 * (including any property from inherited materials) can be passed
	 * in here. Color values can be passed any type of value accepted
	 * by {@link Color#set}.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isLineDashedMaterial = true;
		this.type = 'LineDashedMaterial';

		/**
		 * The scale of the dashed part of a line.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.scale = 1;

		/**
		 * The size of the dash. This is both the gap with the stroke.
		 *
		 * @type {number}
		 * @default 3
		 */
		this.dashSize = 3;

		/**
		 * The size of the gap.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.gapSize = 1;

		this.setValues( parameters );

	}

	copy( source ) {

		super.copy( source );

		this.scale = source.scale;
		this.dashSize = source.dashSize;
		this.gapSize = source.gapSize;

		return this;

	}

}

/**
 * @class
 * @classdesc A simple caching system, used internally by {@link FileLoader}.
 * To enable caching across all loaders that use {@link FileLoader}, add `THREE.Cache.enabled = true.` once in your app.
 * @hideconstructor
 */
const Cache = {

	/**
	 * Whether caching is enabled or not.
	 *
	 * @static
	 * @type {boolean}
	 * @default false
	 */
	enabled: false,

	/**
	 * A dictionary that holds cached files.
	 *
	 * @static
	 * @type {Object<string,Object>}
	 */
	files: {},

	/**
	 * Adds a cache entry with a key to reference the file. If this key already
	 * holds a file, it is overwritten.
	 *
	 * @static
	 * @param {string} key - The key to reference the cached file.
	 * @param {Object} file -  The file to be cached.
	 */
	add: function ( key, file ) {

		if ( this.enabled === false ) return;

		// console.log( 'THREE.Cache', 'Adding key:', key );

		this.files[ key ] = file;

	},

	/**
	 * Gets the cached value for the given key.
	 *
	 * @static
	 * @param {string} key - The key to reference the cached file.
	 * @return {Object|undefined} The cached file. If the key does not exist `undefined` is returned.
	 */
	get: function ( key ) {

		if ( this.enabled === false ) return;

		// console.log( 'THREE.Cache', 'Checking key:', key );

		return this.files[ key ];

	},

	/**
	 * Removes the cached file associated with the given key.
	 *
	 * @static
	 * @param {string} key - The key to reference the cached file.
	 */
	remove: function ( key ) {

		delete this.files[ key ];

	},

	/**
	 * Remove all values from the cache.
	 *
	 * @static
	 */
	clear: function () {

		this.files = {};

	}

};

/**
 * Handles and keeps track of loaded and pending data. A default global
 * instance of this class is created and used by loaders if not supplied
 * manually.
 *
 * In general that should be sufficient, however there are times when it can
 * be useful to have separate loaders - for example if you want to show
 * separate loading bars for objects and textures.
 *
 * ```js
 * const manager = new THREE.LoadingManager();
 * manager.onLoad = () => console.log( 'Loading complete!' );
 *
 * const loader1 = new OBJLoader( manager );
 * const loader2 = new ColladaLoader( manager );
 * ```
 */
class LoadingManager {

	/**
	 * Constructs a new loading manager.
	 *
	 * @param {Function} [onLoad] - Executes when all items have been loaded.
	 * @param {Function} [onProgress] - Executes when single items have been loaded.
	 * @param {Function} [onError] - Executes when an error occurs.
	 */
	constructor( onLoad, onProgress, onError ) {

		const scope = this;

		let isLoading = false;
		let itemsLoaded = 0;
		let itemsTotal = 0;
		let urlModifier = undefined;
		const handlers = [];

		// Refer to #5689 for the reason why we don't set .onStart
		// in the constructor

		/**
		 * Executes when an item starts loading.
		 *
		 * @type {Function|undefined}
		 * @default undefined
		 */
		this.onStart = undefined;

		/**
		 * Executes when all items have been loaded.
		 *
		 * @type {Function|undefined}
		 * @default undefined
		 */
		this.onLoad = onLoad;

		/**
		 * Executes when single items have been loaded.
		 *
		 * @type {Function|undefined}
		 * @default undefined
		 */
		this.onProgress = onProgress;

		/**
		 * Executes when an error occurs.
		 *
		 * @type {Function|undefined}
		 * @default undefined
		 */
		this.onError = onError;

		/**
		 * Used for aborting ongoing requests in loaders using this manager.
		 *
		 * @type {AbortController}
		 */
		this.abortController = new AbortController();

		/**
		 * This should be called by any loader using the manager when the loader
		 * starts loading an item.
		 *
		 * @param {string} url - The URL to load.
		 */
		this.itemStart = function ( url ) {

			itemsTotal ++;

			if ( isLoading === false ) {

				if ( scope.onStart !== undefined ) {

					scope.onStart( url, itemsLoaded, itemsTotal );

				}

			}

			isLoading = true;

		};

		/**
		 * This should be called by any loader using the manager when the loader
		 * ended loading an item.
		 *
		 * @param {string} url - The URL of the loaded item.
		 */
		this.itemEnd = function ( url ) {

			itemsLoaded ++;

			if ( scope.onProgress !== undefined ) {

				scope.onProgress( url, itemsLoaded, itemsTotal );

			}

			if ( itemsLoaded === itemsTotal ) {

				isLoading = false;

				if ( scope.onLoad !== undefined ) {

					scope.onLoad();

				}

			}

		};

		/**
		 * This should be called by any loader using the manager when the loader
		 * encounters an error when loading an item.
		 *
		 * @param {string} url - The URL of the item that produces an error.
		 */
		this.itemError = function ( url ) {

			if ( scope.onError !== undefined ) {

				scope.onError( url );

			}

		};

		/**
		 * Given a URL, uses the URL modifier callback (if any) and returns a
		 * resolved URL. If no URL modifier is set, returns the original URL.
		 *
		 * @param {string} url - The URL to load.
		 * @return {string} The resolved URL.
		 */
		this.resolveURL = function ( url ) {

			if ( urlModifier ) {

				return urlModifier( url );

			}

			return url;

		};

		/**
		 * If provided, the callback will be passed each resource URL before a
		 * request is sent. The callback may return the original URL, or a new URL to
		 * override loading behavior. This behavior can be used to load assets from
		 * .ZIP files, drag-and-drop APIs, and Data URIs.
		 *
		 * ```js
		 * const blobs = {'fish.gltf': blob1, 'diffuse.png': blob2, 'normal.png': blob3};
		 *
		 * const manager = new THREE.LoadingManager();
		 *
		 * // Initialize loading manager with URL callback.
		 * const objectURLs = [];
		 * manager.setURLModifier( ( url ) => {
		 *
		 * 	url = URL.createObjectURL( blobs[ url ] );
		 * 	objectURLs.push( url );
		 * 	return url;
		 *
		 * } );
		 *
		 * // Load as usual, then revoke the blob URLs.
		 * const loader = new GLTFLoader( manager );
		 * loader.load( 'fish.gltf', (gltf) => {
		 *
		 * 	scene.add( gltf.scene );
		 * 	objectURLs.forEach( ( url ) => URL.revokeObjectURL( url ) );
		 *
		 * } );
		 * ```
		 *
		 * @param {function(string):string} transform - URL modifier callback. Called with an URL and must return a resolved URL.
		 * @return {LoadingManager} A reference to this loading manager.
		 */
		this.setURLModifier = function ( transform ) {

			urlModifier = transform;

			return this;

		};

		/**
		 * Registers a loader with the given regular expression. Can be used to
		 * define what loader should be used in order to load specific files. A
		 * typical use case is to overwrite the default loader for textures.
		 *
		 * ```js
		 * // add handler for TGA textures
		 * manager.addHandler( /\.tga$/i, new TGALoader() );
		 * ```
		 *
		 * @param {string} regex - A regular expression.
		 * @param {Loader} loader - A loader that should handle matched cases.
		 * @return {LoadingManager} A reference to this loading manager.
		 */
		this.addHandler = function ( regex, loader ) {

			handlers.push( regex, loader );

			return this;

		};

		/**
		 * Removes the loader for the given regular expression.
		 *
		 * @param {string} regex - A regular expression.
		 * @return {LoadingManager} A reference to this loading manager.
		 */
		this.removeHandler = function ( regex ) {

			const index = handlers.indexOf( regex );

			if ( index !== -1 ) {

				handlers.splice( index, 2 );

			}

			return this;

		};

		/**
		 * Can be used to retrieve the registered loader for the given file path.
		 *
		 * @param {string} file - The file path.
		 * @return {?Loader} The registered loader. Returns `null` if no loader was found.
		 */
		this.getHandler = function ( file ) {

			for ( let i = 0, l = handlers.length; i < l; i += 2 ) {

				const regex = handlers[ i ];
				const loader = handlers[ i + 1 ];

				if ( regex.global ) regex.lastIndex = 0; // see #17920

				if ( regex.test( file ) ) {

					return loader;

				}

			}

			return null;

		};

		/**
		 * Can be used to abort ongoing loading requests in loaders using this manager.
		 * The abort only works if the loaders implement {@link Loader#abort} and `AbortSignal.any()`
		 * is supported in the browser.
		 *
		 * @return {LoadingManager} A reference to this loading manager.
		 */
		this.abort = function () {

			this.abortController.abort();
			this.abortController = new AbortController();

			return this;

		};

	}

}

/**
 * The global default loading manager.
 *
 * @constant
 * @type {LoadingManager}
 */
const DefaultLoadingManager = /*@__PURE__*/ new LoadingManager();

/**
 * Abstract base class for loaders.
 *
 * @abstract
 */
class Loader {

	/**
	 * Constructs a new loader.
	 *
	 * @param {LoadingManager} [manager] - The loading manager.
	 */
	constructor( manager ) {

		/**
		 * The loading manager.
		 *
		 * @type {LoadingManager}
		 * @default DefaultLoadingManager
		 */
		this.manager = ( manager !== undefined ) ? manager : DefaultLoadingManager;

		/**
		 * The crossOrigin string to implement CORS for loading the url from a
		 * different domain that allows CORS.
		 *
		 * @type {string}
		 * @default 'anonymous'
		 */
		this.crossOrigin = 'anonymous';

		/**
		 * Whether the XMLHttpRequest uses credentials.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.withCredentials = false;

		/**
		 * The base path from which the asset will be loaded.
		 *
		 * @type {string}
		 */
		this.path = '';

		/**
		 * The base path from which additional resources like textures will be loaded.
		 *
		 * @type {string}
		 */
		this.resourcePath = '';

		/**
		 * The [request header]{@link https://developer.mozilla.org/en-US/docs/Glossary/Request_header}
		 * used in HTTP request.
		 *
		 * @type {Object<string, any>}
		 */
		this.requestHeader = {};

	}

	/**
	 * This method needs to be implemented by all concrete loaders. It holds the
	 * logic for loading assets from the backend.
	 *
	 * @abstract
	 * @param {string} url - The path/URL of the file to be loaded.
	 * @param {Function} onLoad - Executed when the loading process has been finished.
	 * @param {onProgressCallback} [onProgress] - Executed while the loading is in progress.
	 * @param {onErrorCallback} [onError] - Executed when errors occur.
	 */
	load( /* url, onLoad, onProgress, onError */ ) {}

	/**
	 * A async version of {@link Loader#load}.
	 *
	 * @param {string} url - The path/URL of the file to be loaded.
	 * @param {onProgressCallback} [onProgress] - Executed while the loading is in progress.
	 * @return {Promise} A Promise that resolves when the asset has been loaded.
	 */
	loadAsync( url, onProgress ) {

		const scope = this;

		return new Promise( function ( resolve, reject ) {

			scope.load( url, resolve, onProgress, reject );

		} );

	}

	/**
	 * This method needs to be implemented by all concrete loaders. It holds the
	 * logic for parsing the asset into three.js entities.
	 *
	 * @abstract
	 * @param {any} data - The data to parse.
	 */
	parse( /* data */ ) {}

	/**
	 * Sets the `crossOrigin` String to implement CORS for loading the URL
	 * from a different domain that allows CORS.
	 *
	 * @param {string} crossOrigin - The `crossOrigin` value.
	 * @return {Loader} A reference to this instance.
	 */
	setCrossOrigin( crossOrigin ) {

		this.crossOrigin = crossOrigin;
		return this;

	}

	/**
	 * Whether the XMLHttpRequest uses credentials such as cookies, authorization
	 * headers or TLS client certificates, see [XMLHttpRequest.withCredentials]{@link https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials}.
	 *
	 * Note: This setting has no effect if you are loading files locally or from the same domain.
	 *
	 * @param {boolean} value - The `withCredentials` value.
	 * @return {Loader} A reference to this instance.
	 */
	setWithCredentials( value ) {

		this.withCredentials = value;
		return this;

	}

	/**
	 * Sets the base path for the asset.
	 *
	 * @param {string} path - The base path.
	 * @return {Loader} A reference to this instance.
	 */
	setPath( path ) {

		this.path = path;
		return this;

	}

	/**
	 * Sets the base path for dependent resources like textures.
	 *
	 * @param {string} resourcePath - The resource path.
	 * @return {Loader} A reference to this instance.
	 */
	setResourcePath( resourcePath ) {

		this.resourcePath = resourcePath;
		return this;

	}

	/**
	 * Sets the given request header.
	 *
	 * @param {Object} requestHeader - A [request header]{@link https://developer.mozilla.org/en-US/docs/Glossary/Request_header}
	 * for configuring the HTTP request.
	 * @return {Loader} A reference to this instance.
	 */
	setRequestHeader( requestHeader ) {

		this.requestHeader = requestHeader;
		return this;

	}

	/**
	 * This method can be implemented in loaders for aborting ongoing requests.
	 *
	 * @abstract
	 * @return {Loader} A reference to this instance.
	 */
	abort() {

		return this;

	}

}

/**
 * Callback for onProgress in loaders.
 *
 * @callback onProgressCallback
 * @param {ProgressEvent} event - An instance of `ProgressEvent` that represents the current loading status.
 */

/**
 * Callback for onError in loaders.
 *
 * @callback onErrorCallback
 * @param {Error} error - The error which occurred during the loading process.
 */

/**
 * The default material name that is used by loaders
 * when creating materials for loaded 3D objects.
 *
 * Note: Not all loaders might honor this setting.
 *
 * @static
 * @type {string}
 * @default '__DEFAULT'
 */
Loader.DEFAULT_MATERIAL_NAME = '__DEFAULT';

const _loading = new WeakMap();

/**
 * A loader for loading images. The class loads images with the HTML `Image` API.
 *
 * ```js
 * const loader = new THREE.ImageLoader();
 * const image = await loader.loadAsync( 'image.png' );
 * ```
 * Please note that `ImageLoader` has dropped support for progress
 * events in `r84`. For an `ImageLoader` that supports progress events, see
 * [this thread]{@link https://github.com/mrdoob/three.js/issues/10439#issuecomment-275785639}.
 *
 * @augments Loader
 */
class ImageLoader extends Loader {

	/**
	 * Constructs a new image loader.
	 *
	 * @param {LoadingManager} [manager] - The loading manager.
	 */
	constructor( manager ) {

		super( manager );

	}

	/**
	 * Starts loading from the given URL and passes the loaded image
	 * to the `onLoad()` callback. The method also returns a new `Image` object which can
	 * directly be used for texture creation. If you do it this way, the texture
	 * may pop up in your scene once the respective loading process is finished.
	 *
	 * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
	 * @param {function(Image)} onLoad - Executed when the loading process has been finished.
	 * @param {onProgressCallback} onProgress - Unsupported in this loader.
	 * @param {onErrorCallback} onError - Executed when errors occur.
	 * @return {Image} The image.
	 */
	load( url, onLoad, onProgress, onError ) {

		if ( this.path !== undefined ) url = this.path + url;

		url = this.manager.resolveURL( url );

		const scope = this;

		const cached = Cache.get( `image:${url}` );

		if ( cached !== undefined ) {

			if ( cached.complete === true ) {

				scope.manager.itemStart( url );

				setTimeout( function () {

					if ( onLoad ) onLoad( cached );

					scope.manager.itemEnd( url );

				}, 0 );

			} else {

				let arr = _loading.get( cached );

				if ( arr === undefined ) {

					arr = [];
					_loading.set( cached, arr );

				}

				arr.push( { onLoad, onError } );

			}

			return cached;

		}

		const image = createElementNS( 'img' );

		function onImageLoad() {

			removeEventListeners();

			if ( onLoad ) onLoad( this );

			//

			const callbacks = _loading.get( this ) || [];

			for ( let i = 0; i < callbacks.length; i ++ ) {

				const callback = callbacks[ i ];
				if ( callback.onLoad ) callback.onLoad( this );

			}

			_loading.delete( this );

			scope.manager.itemEnd( url );

		}

		function onImageError( event ) {

			removeEventListeners();

			if ( onError ) onError( event );

			Cache.remove( `image:${url}` );

			//

			const callbacks = _loading.get( this ) || [];

			for ( let i = 0; i < callbacks.length; i ++ ) {

				const callback = callbacks[ i ];
				if ( callback.onError ) callback.onError( event );

			}

			_loading.delete( this );


			scope.manager.itemError( url );
			scope.manager.itemEnd( url );

		}

		function removeEventListeners() {

			image.removeEventListener( 'load', onImageLoad, false );
			image.removeEventListener( 'error', onImageError, false );

		}

		image.addEventListener( 'load', onImageLoad, false );
		image.addEventListener( 'error', onImageError, false );

		if ( url.slice( 0, 5 ) !== 'data:' ) {

			if ( this.crossOrigin !== undefined ) image.crossOrigin = this.crossOrigin;

		}

		Cache.add( `image:${url}`, image );
		scope.manager.itemStart( url );

		image.src = url;

		return image;

	}

}

/**
 * Class for loading textures. Images are internally
 * loaded via {@link ImageLoader}.
 *
 * ```js
 * const loader = new THREE.TextureLoader();
 * const texture = await loader.loadAsync( 'textures/land_ocean_ice_cloud_2048.jpg' );
 *
 * const material = new THREE.MeshBasicMaterial( { map:texture } );
 * ```
 * Please note that `TextureLoader` has dropped support for progress
 * events in `r84`. For a `TextureLoader` that supports progress events, see
 * [this thread]{@link https://github.com/mrdoob/three.js/issues/10439#issuecomment-293260145}.
 *
 * @augments Loader
 */
class TextureLoader extends Loader {

	/**
	 * Constructs a new texture loader.
	 *
	 * @param {LoadingManager} [manager] - The loading manager.
	 */
	constructor( manager ) {

		super( manager );

	}

	/**
	 * Starts loading from the given URL and pass the fully loaded texture
	 * to the `onLoad()` callback. The method also returns a new texture object which can
	 * directly be used for material creation. If you do it this way, the texture
	 * may pop up in your scene once the respective loading process is finished.
	 *
	 * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
	 * @param {function(Texture)} onLoad - Executed when the loading process has been finished.
	 * @param {onProgressCallback} onProgress - Unsupported in this loader.
	 * @param {onErrorCallback} onError - Executed when errors occur.
	 * @return {Texture} The texture.
	 */
	load( url, onLoad, onProgress, onError ) {

		const texture = new Texture();

		const loader = new ImageLoader( this.manager );
		loader.setCrossOrigin( this.crossOrigin );
		loader.setPath( this.path );

		loader.load( url, function ( image ) {

			texture.image = image;
			texture.needsUpdate = true;

			if ( onLoad !== undefined ) {

				onLoad( texture );

			}

		}, onProgress, onError );

		return texture;

	}

}

/**
 * Abstract base class for lights - all other light types inherit the
 * properties and methods described here.
 *
 * @abstract
 * @augments Object3D
 */
class Light extends Object3D {

	/**
	 * Constructs a new light.
	 *
	 * @param {(number|Color|string)} [color=0xffffff] - The light's color.
	 * @param {number} [intensity=1] - The light's strength/intensity.
	 */
	constructor( color, intensity = 1 ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isLight = true;

		this.type = 'Light';

		/**
		 * The light's color.
		 *
		 * @type {Color}
		 */
		this.color = new Color( color );

		/**
		 * The light's intensity.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.intensity = intensity;

	}

	/**
	 * Frees the GPU-related resources allocated by this instance. Call this
	 * method whenever this instance is no longer used in your app.
	 */
	dispose() {

		// Empty here in base class; some subclasses override.

	}

	copy( source, recursive ) {

		super.copy( source, recursive );

		this.color.copy( source.color );
		this.intensity = source.intensity;

		return this;

	}

	toJSON( meta ) {

		const data = super.toJSON( meta );

		data.object.color = this.color.getHex();
		data.object.intensity = this.intensity;

		if ( this.groundColor !== undefined ) data.object.groundColor = this.groundColor.getHex();

		if ( this.distance !== undefined ) data.object.distance = this.distance;
		if ( this.angle !== undefined ) data.object.angle = this.angle;
		if ( this.decay !== undefined ) data.object.decay = this.decay;
		if ( this.penumbra !== undefined ) data.object.penumbra = this.penumbra;

		if ( this.shadow !== undefined ) data.object.shadow = this.shadow.toJSON();
		if ( this.target !== undefined ) data.object.target = this.target.uuid;

		return data;

	}

}

/**
 * A light source positioned directly above the scene, with color fading from
 * the sky color to the ground color.
 *
 * This light cannot be used to cast shadows.
 *
 * ```js
 * const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );
 * scene.add( light );
 * ```
 *
 * @augments Light
 */
class HemisphereLight extends Light {

	/**
	 * Constructs a new hemisphere light.
	 *
	 * @param {(number|Color|string)} [skyColor=0xffffff] - The light's sky color.
	 * @param {(number|Color|string)} [groundColor=0xffffff] - The light's ground color.
	 * @param {number} [intensity=1] - The light's strength/intensity.
	 */
	constructor( skyColor, groundColor, intensity ) {

		super( skyColor, intensity );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isHemisphereLight = true;

		this.type = 'HemisphereLight';

		this.position.copy( Object3D.DEFAULT_UP );
		this.updateMatrix();

		/**
		 * The light's ground color.
		 *
		 * @type {Color}
		 */
		this.groundColor = new Color( groundColor );

	}

	copy( source, recursive ) {

		super.copy( source, recursive );

		this.groundColor.copy( source.groundColor );

		return this;

	}

}

const _projScreenMatrix$1 = /*@__PURE__*/ new Matrix4();
const _lightPositionWorld$1 = /*@__PURE__*/ new Vector3();
const _lookTarget$1 = /*@__PURE__*/ new Vector3();

/**
 * Abstract base class for light shadow classes. These classes
 * represent the shadow configuration for different light types.
 *
 * @abstract
 */
class LightShadow {

	/**
	 * Constructs a new light shadow.
	 *
	 * @param {Camera} camera - The light's view of the world.
	 */
	constructor( camera ) {

		/**
		 * The light's view of the world.
		 *
		 * @type {Camera}
		 */
		this.camera = camera;

		/**
		 * The intensity of the shadow. The default is `1`.
		 * Valid values are in the range `[0, 1]`.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.intensity = 1;

		/**
		 * Shadow map bias, how much to add or subtract from the normalized depth
		 * when deciding whether a surface is in shadow.
		 *
		 * The default is `0`. Very tiny adjustments here (in the order of `0.0001`)
		 * may help reduce artifacts in shadows.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.bias = 0;

		/**
		 * Defines how much the position used to query the shadow map is offset along
		 * the object normal. The default is `0`. Increasing this value can be used to
		 * reduce shadow acne especially in large scenes where light shines onto
		 * geometry at a shallow angle. The cost is that shadows may appear distorted.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.normalBias = 0;

		/**
		 * Setting this to values greater than 1 will blur the edges of the shadow.
		 * High values will cause unwanted banding effects in the shadows - a greater
		 * map size will allow for a higher value to be used here before these effects
		 * become visible.
		 *
		 * The property has no effect when the shadow map type is `PCFSoftShadowMap` and
		 * and it is recommended to increase softness by decreasing the shadow map size instead.
		 *
		 * The property has no effect when the shadow map type is `BasicShadowMap`.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.radius = 1;

		/**
		 * The amount of samples to use when blurring a VSM shadow map.
		 *
		 * @type {number}
		 * @default 8
		 */
		this.blurSamples = 8;

		/**
		 * Defines the width and height of the shadow map. Higher values give better quality
		 * shadows at the cost of computation time. Values must be powers of two.
		 *
		 * @type {Vector2}
		 * @default (512,512)
		 */
		this.mapSize = new Vector2( 512, 512 );

		/**
		 * The type of shadow texture. The default is `UnsignedByteType`.
		 *
		 * @type {number}
		 * @default UnsignedByteType
		 */
		this.mapType = UnsignedByteType;

		/**
		 * The depth map generated using the internal camera; a location beyond a
		 * pixel's depth is in shadow. Computed internally during rendering.
		 *
		 * @type {?RenderTarget}
		 * @default null
		 */
		this.map = null;

		/**
		 * The distribution map generated using the internal camera; an occlusion is
		 * calculated based on the distribution of depths. Computed internally during
		 * rendering.
		 *
		 * @type {?RenderTarget}
		 * @default null
		 */
		this.mapPass = null;

		/**
		 * Model to shadow camera space, to compute location and depth in shadow map.
		 * This is computed internally during rendering.
		 *
		 * @type {Matrix4}
		 */
		this.matrix = new Matrix4();

		/**
		 * Enables automatic updates of the light's shadow. If you do not require dynamic
		 * lighting / shadows, you may set this to `false`.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.autoUpdate = true;

		/**
		 * When set to `true`, shadow maps will be updated in the next `render` call.
		 * If you have set {@link LightShadow#autoUpdate} to `false`, you will need to
		 * set this property to `true` and then make a render call to update the light's shadow.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.needsUpdate = false;

		this._frustum = new Frustum();
		this._frameExtents = new Vector2( 1, 1 );

		this._viewportCount = 1;

		this._viewports = [

			new Vector4( 0, 0, 1, 1 )

		];

	}

	/**
	 * Used internally by the renderer to get the number of viewports that need
	 * to be rendered for this shadow.
	 *
	 * @return {number} The viewport count.
	 */
	getViewportCount() {

		return this._viewportCount;

	}

	/**
	 * Gets the shadow cameras frustum. Used internally by the renderer to cull objects.
	 *
	 * @return {Frustum} The shadow camera frustum.
	 */
	getFrustum() {

		return this._frustum;

	}

	/**
	 * Update the matrices for the camera and shadow, used internally by the renderer.
	 *
	 * @param {Light} light - The light for which the shadow is being rendered.
	 */
	updateMatrices( light ) {

		const shadowCamera = this.camera;
		const shadowMatrix = this.matrix;

		_lightPositionWorld$1.setFromMatrixPosition( light.matrixWorld );
		shadowCamera.position.copy( _lightPositionWorld$1 );

		_lookTarget$1.setFromMatrixPosition( light.target.matrixWorld );
		shadowCamera.lookAt( _lookTarget$1 );
		shadowCamera.updateMatrixWorld();

		_projScreenMatrix$1.multiplyMatrices( shadowCamera.projectionMatrix, shadowCamera.matrixWorldInverse );
		this._frustum.setFromProjectionMatrix( _projScreenMatrix$1, shadowCamera.coordinateSystem, shadowCamera.reversedDepth );

		if ( shadowCamera.reversedDepth ) {

			shadowMatrix.set(
				0.5, 0.0, 0.0, 0.5,
				0.0, 0.5, 0.0, 0.5,
				0.0, 0.0, 1.0, 0.0,
				0.0, 0.0, 0.0, 1.0
			);

		} else {

			shadowMatrix.set(
				0.5, 0.0, 0.0, 0.5,
				0.0, 0.5, 0.0, 0.5,
				0.0, 0.0, 0.5, 0.5,
				0.0, 0.0, 0.0, 1.0
			);

		}

		shadowMatrix.multiply( _projScreenMatrix$1 );

	}

	/**
	 * Returns a viewport definition for the given viewport index.
	 *
	 * @param {number} viewportIndex - The viewport index.
	 * @return {Vector4} The viewport.
	 */
	getViewport( viewportIndex ) {

		return this._viewports[ viewportIndex ];

	}

	/**
	 * Returns the frame extends.
	 *
	 * @return {Vector2} The frame extends.
	 */
	getFrameExtents() {

		return this._frameExtents;

	}

	/**
	 * Frees the GPU-related resources allocated by this instance. Call this
	 * method whenever this instance is no longer used in your app.
	 */
	dispose() {

		if ( this.map ) {

			this.map.dispose();

		}

		if ( this.mapPass ) {

			this.mapPass.dispose();

		}

	}

	/**
	 * Copies the values of the given light shadow instance to this instance.
	 *
	 * @param {LightShadow} source - The light shadow to copy.
	 * @return {LightShadow} A reference to this light shadow instance.
	 */
	copy( source ) {

		this.camera = source.camera.clone();

		this.intensity = source.intensity;

		this.bias = source.bias;
		this.radius = source.radius;

		this.autoUpdate = source.autoUpdate;
		this.needsUpdate = source.needsUpdate;
		this.normalBias = source.normalBias;
		this.blurSamples = source.blurSamples;

		this.mapSize.copy( source.mapSize );

		return this;

	}

	/**
	 * Returns a new light shadow instance with copied values from this instance.
	 *
	 * @return {LightShadow} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

	/**
	 * Serializes the light shadow into JSON.
	 *
	 * @return {Object} A JSON object representing the serialized light shadow.
	 * @see {@link ObjectLoader#parse}
	 */
	toJSON() {

		const object = {};

		if ( this.intensity !== 1 ) object.intensity = this.intensity;
		if ( this.bias !== 0 ) object.bias = this.bias;
		if ( this.normalBias !== 0 ) object.normalBias = this.normalBias;
		if ( this.radius !== 1 ) object.radius = this.radius;
		if ( this.mapSize.x !== 512 || this.mapSize.y !== 512 ) object.mapSize = this.mapSize.toArray();

		object.camera = this.camera.toJSON( false ).object;
		delete object.camera.matrix;

		return object;

	}

}

/**
 * Represents the shadow configuration of directional lights.
 *
 * @augments LightShadow
 */
class DirectionalLightShadow extends LightShadow {

	/**
	 * Constructs a new directional light shadow.
	 */
	constructor() {

		super( new OrthographicCamera( -5, 5, 5, -5, 0.5, 500 ) );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isDirectionalLightShadow = true;

	}

}

/**
 * A light that gets emitted in a specific direction. This light will behave
 * as though it is infinitely far away and the rays produced from it are all
 * parallel. The common use case for this is to simulate daylight; the sun is
 * far enough away that its position can be considered to be infinite, and
 * all light rays coming from it are parallel.
 *
 * A common point of confusion for directional lights is that setting the
 * rotation has no effect. This is because three.js's DirectionalLight is the
 * equivalent to what is often called a 'Target Direct Light' in other
 * applications.
 *
 * This means that its direction is calculated as pointing from the light's
 * {@link Object3D#position} to the {@link DirectionalLight#target} position
 * (as opposed to a 'Free Direct Light' that just has a rotation
 * component).
 *
 * This light can cast shadows - see the {@link DirectionalLightShadow} for details.
 *
 * ```js
 * // White directional light at half intensity shining from the top.
 * const directionalLight = new THREE.DirectionalLight( 0xffffff, 0.5 );
 * scene.add( directionalLight );
 * ```
 *
 * @augments Light
 */
class DirectionalLight extends Light {

	/**
	 * Constructs a new directional light.
	 *
	 * @param {(number|Color|string)} [color=0xffffff] - The light's color.
	 * @param {number} [intensity=1] - The light's strength/intensity.
	 */
	constructor( color, intensity ) {

		super( color, intensity );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isDirectionalLight = true;

		this.type = 'DirectionalLight';

		this.position.copy( Object3D.DEFAULT_UP );
		this.updateMatrix();

		/**
		 * The directional light points from its position to the
		 * target's position.
		 *
		 * For the target's position to be changed to anything other
		 * than the default, it must be added to the scene.
		 *
		 * It is also possible to set the target to be another 3D object
		 * in the scene. The light will now track the target object.
		 *
		 * @type {Object3D}
		 */
		this.target = new Object3D();

		/**
		 * This property holds the light's shadow configuration.
		 *
		 * @type {DirectionalLightShadow}
		 */
		this.shadow = new DirectionalLightShadow();

	}

	dispose() {

		this.shadow.dispose();

	}

	copy( source ) {

		super.copy( source );

		this.target = source.target.clone();
		this.shadow = source.shadow.clone();

		return this;

	}

}

/**
 * This light globally illuminates all objects in the scene equally.
 *
 * It cannot be used to cast shadows as it does not have a direction.
 *
 * ```js
 * const light = new THREE.AmbientLight( 0x404040 ); // soft white light
 * scene.add( light );
 * ```
 *
 * @augments Light
 */
class AmbientLight extends Light {

	/**
	 * Constructs a new ambient light.
	 *
	 * @param {(number|Color|string)} [color=0xffffff] - The light's color.
	 * @param {number} [intensity=1] - The light's strength/intensity.
	 */
	constructor( color, intensity ) {

		super( color, intensity );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isAmbientLight = true;

		this.type = 'AmbientLight';

	}

}

const _matrix = /*@__PURE__*/ new Matrix4();

/**
 * This class is designed to assist with raycasting. Raycasting is used for
 * mouse picking (working out what objects in the 3d space the mouse is over)
 * amongst other things.
 */
class Raycaster {

	/**
	 * Constructs a new raycaster.
	 *
	 * @param {Vector3} origin - The origin vector where the ray casts from.
	 * @param {Vector3} direction - The (normalized) direction vector that gives direction to the ray.
	 * @param {number} [near=0] - All results returned are further away than near. Near can't be negative.
	 * @param {number} [far=Infinity] - All results returned are closer than far. Far can't be lower than near.
	 */
	constructor( origin, direction, near = 0, far = Infinity ) {

		/**
		 * The ray used for raycasting.
		 *
		 * @type {Ray}
		 */
		this.ray = new Ray( origin, direction );

		/**
		 * All results returned are further away than near. Near can't be negative.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.near = near;

		/**
		 * All results returned are further away than near. Near can't be negative.
		 *
		 * @type {number}
		 * @default Infinity
		 */
		this.far = far;

		/**
		 * The camera to use when raycasting against view-dependent objects such as
		 * billboarded objects like sprites. This field can be set manually or
		 * is set when calling `setFromCamera()`.
		 *
		 * @type {?Camera}
		 * @default null
		 */
		this.camera = null;

		/**
		 * Allows to selectively ignore 3D objects when performing intersection tests.
		 * The following code example ensures that only 3D objects on layer `1` will be
		 * honored by raycaster.
		 * ```js
		 * raycaster.layers.set( 1 );
		 * object.layers.enable( 1 );
		 * ```
		 *
		 * @type {Layers}
		 */
		this.layers = new Layers();


		/**
		 * A parameter object that configures the raycasting. It has the structure:
		 *
		 * ```
		 * {
		 * 	Mesh: {},
		 * 	Line: { threshold: 1 },
		 * 	LOD: {},
		 * 	Points: { threshold: 1 },
		 * 	Sprite: {}
		 * }
		 * ```
		 * Where `threshold` is the precision of the raycaster when intersecting objects, in world units.
		 *
		 * @type {Object}
		 */
		this.params = {
			Mesh: {},
			Line: { threshold: 1 },
			LOD: {},
			Points: { threshold: 1 },
			Sprite: {}
		};

	}

	/**
	 * Updates the ray with a new origin and direction by copying the values from the arguments.
	 *
	 * @param {Vector3} origin - The origin vector where the ray casts from.
	 * @param {Vector3} direction - The (normalized) direction vector that gives direction to the ray.
	 */
	set( origin, direction ) {

		// direction is assumed to be normalized (for accurate distance calculations)

		this.ray.set( origin, direction );

	}

	/**
	 * Uses the given coordinates and camera to compute a new origin and direction for the internal ray.
	 *
	 * @param {Vector2} coords - 2D coordinates of the mouse, in normalized device coordinates (NDC).
	 * X and Y components should be between `-1` and `1`.
	 * @param {Camera} camera - The camera from which the ray should originate.
	 */
	setFromCamera( coords, camera ) {

		if ( camera.isPerspectiveCamera ) {

			this.ray.origin.setFromMatrixPosition( camera.matrixWorld );
			this.ray.direction.set( coords.x, coords.y, 0.5 ).unproject( camera ).sub( this.ray.origin ).normalize();
			this.camera = camera;

		} else if ( camera.isOrthographicCamera ) {

			this.ray.origin.set( coords.x, coords.y, ( camera.near + camera.far ) / ( camera.near - camera.far ) ).unproject( camera ); // set origin in plane of camera
			this.ray.direction.set( 0, 0, -1 ).transformDirection( camera.matrixWorld );
			this.camera = camera;

		} else {

			console.error( 'THREE.Raycaster: Unsupported camera type: ' + camera.type );

		}

	}

	/**
	 * Uses the given WebXR controller to compute a new origin and direction for the internal ray.
	 *
	 * @param {WebXRController} controller - The controller to copy the position and direction from.
	 * @return {Raycaster} A reference to this raycaster.
	 */
	setFromXRController( controller ) {

		_matrix.identity().extractRotation( controller.matrixWorld );

		this.ray.origin.setFromMatrixPosition( controller.matrixWorld );
		this.ray.direction.set( 0, 0, -1 ).applyMatrix4( _matrix );

		return this;

	}

	/**
	 * The intersection point of a raycaster intersection test.
	 * @typedef {Object} Raycaster~Intersection
	 * @property {number} distance - The distance from the ray's origin to the intersection point.
	 * @property {number} distanceToRay -  Some 3D objects e.g. {@link Points} provide the distance of the
	 * intersection to the nearest point on the ray. For other objects it will be `undefined`.
	 * @property {Vector3} point - The intersection point, in world coordinates.
	 * @property {Object} face - The face that has been intersected.
	 * @property {number} faceIndex - The face index.
	 * @property {Object3D} object - The 3D object that has been intersected.
	 * @property {Vector2} uv - U,V coordinates at point of intersection.
	 * @property {Vector2} uv1 - Second set of U,V coordinates at point of intersection.
	 * @property {Vector3} uv1 - Interpolated normal vector at point of intersection.
	 * @property {number} instanceId - The index number of the instance where the ray
	 * intersects the {@link InstancedMesh}.
	 */

	/**
	 * Checks all intersection between the ray and the object with or without the
	 * descendants. Intersections are returned sorted by distance, closest first.
	 *
	 * `Raycaster` delegates to the `raycast()` method of the passed 3D object, when
	 * evaluating whether the ray intersects the object or not. This allows meshes to respond
	 * differently to ray casting than lines or points.
	 *
	 * Note that for meshes, faces must be pointed towards the origin of the ray in order
	 * to be detected; intersections of the ray passing through the back of a face will not
	 * be detected. To raycast against both faces of an object, you'll want to set  {@link Material#side}
	 * to `THREE.DoubleSide`.
	 *
	 * @param {Object3D} object - The 3D object to check for intersection with the ray.
	 * @param {boolean} [recursive=true] - If set to `true`, it also checks all descendants.
	 * Otherwise it only checks intersection with the object.
	 * @param {Array<Raycaster~Intersection>} [intersects=[]] The target array that holds the result of the method.
	 * @return {Array<Raycaster~Intersection>} An array holding the intersection points.
	 */
	intersectObject( object, recursive = true, intersects = [] ) {

		intersect( object, this, intersects, recursive );

		intersects.sort( ascSort );

		return intersects;

	}

	/**
	 * Checks all intersection between the ray and the objects with or without
	 * the descendants. Intersections are returned sorted by distance, closest first.
	 *
	 * @param {Array<Object3D>} objects - The 3D objects to check for intersection with the ray.
	 * @param {boolean} [recursive=true] - If set to `true`, it also checks all descendants.
	 * Otherwise it only checks intersection with the object.
	 * @param {Array<Raycaster~Intersection>} [intersects=[]] The target array that holds the result of the method.
	 * @return {Array<Raycaster~Intersection>} An array holding the intersection points.
	 */
	intersectObjects( objects, recursive = true, intersects = [] ) {

		for ( let i = 0, l = objects.length; i < l; i ++ ) {

			intersect( objects[ i ], this, intersects, recursive );

		}

		intersects.sort( ascSort );

		return intersects;

	}

}

function ascSort( a, b ) {

	return a.distance - b.distance;

}

function intersect( object, raycaster, intersects, recursive ) {

	let propagate = true;

	if ( object.layers.test( raycaster.layers ) ) {

		const result = object.raycast( raycaster, intersects );

		if ( result === false ) propagate = false;

	}

	if ( propagate === true && recursive === true ) {

		const children = object.children;

		for ( let i = 0, l = children.length; i < l; i ++ ) {

			intersect( children[ i ], raycaster, intersects, true );

		}

	}

}

/**
 * Class for keeping track of time.
 */
class Clock {

	/**
	 * Constructs a new clock.
	 *
	 * @param {boolean} [autoStart=true] - Whether to automatically start the clock when
	 * `getDelta()` is called for the first time.
	 */
	constructor( autoStart = true ) {

		/**
		 * If set to `true`, the clock starts automatically when `getDelta()` is called
		 * for the first time.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.autoStart = autoStart;

		/**
		 * Holds the time at which the clock's `start()` method was last called.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.startTime = 0;

		/**
		 * Holds the time at which the clock's `start()`, `getElapsedTime()` or
		 * `getDelta()` methods were last called.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.oldTime = 0;

		/**
		 * Keeps track of the total time that the clock has been running.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.elapsedTime = 0;

		/**
		 * Whether the clock is running or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.running = false;

	}

	/**
	 * Starts the clock. When `autoStart` is set to `true`, the method is automatically
	 * called by the class.
	 */
	start() {

		this.startTime = performance.now();

		this.oldTime = this.startTime;
		this.elapsedTime = 0;
		this.running = true;

	}

	/**
	 * Stops the clock.
	 */
	stop() {

		this.getElapsedTime();
		this.running = false;
		this.autoStart = false;

	}

	/**
	 * Returns the elapsed time in seconds.
	 *
	 * @return {number} The elapsed time.
	 */
	getElapsedTime() {

		this.getDelta();
		return this.elapsedTime;

	}

	/**
	 * Returns the delta time in seconds.
	 *
	 * @return {number} The delta time.
	 */
	getDelta() {

		let diff = 0;

		if ( this.autoStart && ! this.running ) {

			this.start();
			return 0;

		}

		if ( this.running ) {

			const newTime = performance.now();

			diff = ( newTime - this.oldTime ) / 1000;
			this.oldTime = newTime;

			this.elapsedTime += diff;

		}

		return diff;

	}

}

/**
 * This class can be used to represent points in 3D space as
 * [Spherical coordinates]{@link https://en.wikipedia.org/wiki/Spherical_coordinate_system}.
 */
class Spherical {

	/**
	 * Constructs a new spherical.
	 *
	 * @param {number} [radius=1] - The radius, or the Euclidean distance (straight-line distance) from the point to the origin.
	 * @param {number} [phi=0] - The polar angle in radians from the y (up) axis.
	 * @param {number} [theta=0] - The equator/azimuthal angle in radians around the y (up) axis.
	 */
	constructor( radius = 1, phi = 0, theta = 0 ) {

		/**
		 * The radius, or the Euclidean distance (straight-line distance) from the point to the origin.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.radius = radius;

		/**
		 * The polar angle in radians from the y (up) axis.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.phi = phi;

		/**
		 * The equator/azimuthal angle in radians around the y (up) axis.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.theta = theta;

	}

	/**
	 * Sets the spherical components by copying the given values.
	 *
	 * @param {number} radius - The radius.
	 * @param {number} phi - The polar angle.
	 * @param {number} theta - The azimuthal angle.
	 * @return {Spherical} A reference to this spherical.
	 */
	set( radius, phi, theta ) {

		this.radius = radius;
		this.phi = phi;
		this.theta = theta;

		return this;

	}

	/**
	 * Copies the values of the given spherical to this instance.
	 *
	 * @param {Spherical} other - The spherical to copy.
	 * @return {Spherical} A reference to this spherical.
	 */
	copy( other ) {

		this.radius = other.radius;
		this.phi = other.phi;
		this.theta = other.theta;

		return this;

	}

	/**
	 * Restricts the polar angle [page:.phi phi] to be between `0.000001` and pi -
	 * `0.000001`.
	 *
	 * @return {Spherical} A reference to this spherical.
	 */
	makeSafe() {

		const EPS = 0.000001;
		this.phi = clamp( this.phi, EPS, Math.PI - EPS );

		return this;

	}

	/**
	 * Sets the spherical components from the given vector which is assumed to hold
	 * Cartesian coordinates.
	 *
	 * @param {Vector3} v - The vector to set.
	 * @return {Spherical} A reference to this spherical.
	 */
	setFromVector3( v ) {

		return this.setFromCartesianCoords( v.x, v.y, v.z );

	}

	/**
	 * Sets the spherical components from the given Cartesian coordinates.
	 *
	 * @param {number} x - The x value.
	 * @param {number} y - The y value.
	 * @param {number} z - The z value.
	 * @return {Spherical} A reference to this spherical.
	 */
	setFromCartesianCoords( x, y, z ) {

		this.radius = Math.sqrt( x * x + y * y + z * z );

		if ( this.radius === 0 ) {

			this.theta = 0;
			this.phi = 0;

		} else {

			this.theta = Math.atan2( x, z );
			this.phi = Math.acos( clamp( y / this.radius, -1, 1 ) );

		}

		return this;

	}

	/**
	 * Returns a new spherical with copied values from this instance.
	 *
	 * @return {Spherical} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

}

const _vector = /*@__PURE__*/ new Vector2();

/**
 * Represents an axis-aligned bounding box (AABB) in 2D space.
 */
class Box2 {

	/**
	 * Constructs a new bounding box.
	 *
	 * @param {Vector2} [min=(Infinity,Infinity)] - A vector representing the lower boundary of the box.
	 * @param {Vector2} [max=(-Infinity,-Infinity)] - A vector representing the upper boundary of the box.
	 */
	constructor( min = new Vector2( + Infinity, + Infinity ), max = new Vector2( - Infinity, - Infinity ) ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isBox2 = true;

		/**
		 * The lower boundary of the box.
		 *
		 * @type {Vector2}
		 */
		this.min = min;

		/**
		 * The upper boundary of the box.
		 *
		 * @type {Vector2}
		 */
		this.max = max;

	}

	/**
	 * Sets the lower and upper boundaries of this box.
	 * Please note that this method only copies the values from the given objects.
	 *
	 * @param {Vector2} min - The lower boundary of the box.
	 * @param {Vector2} max - The upper boundary of the box.
	 * @return {Box2} A reference to this bounding box.
	 */
	set( min, max ) {

		this.min.copy( min );
		this.max.copy( max );

		return this;

	}

	/**
	 * Sets the upper and lower bounds of this box so it encloses the position data
	 * in the given array.
	 *
	 * @param {Array<Vector2>} points - An array holding 2D position data as instances of {@link Vector2}.
	 * @return {Box2} A reference to this bounding box.
	 */
	setFromPoints( points ) {

		this.makeEmpty();

		for ( let i = 0, il = points.length; i < il; i ++ ) {

			this.expandByPoint( points[ i ] );

		}

		return this;

	}

	/**
	 * Centers this box on the given center vector and sets this box's width, height and
	 * depth to the given size values.
	 *
	 * @param {Vector2} center - The center of the box.
	 * @param {Vector2} size - The x and y dimensions of the box.
	 * @return {Box2} A reference to this bounding box.
	 */
	setFromCenterAndSize( center, size ) {

		const halfSize = _vector.copy( size ).multiplyScalar( 0.5 );
		this.min.copy( center ).sub( halfSize );
		this.max.copy( center ).add( halfSize );

		return this;

	}

	/**
	 * Returns a new box with copied values from this instance.
	 *
	 * @return {Box2} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

	/**
	 * Copies the values of the given box to this instance.
	 *
	 * @param {Box2} box - The box to copy.
	 * @return {Box2} A reference to this bounding box.
	 */
	copy( box ) {

		this.min.copy( box.min );
		this.max.copy( box.max );

		return this;

	}

	/**
	 * Makes this box empty which means in encloses a zero space in 2D.
	 *
	 * @return {Box2} A reference to this bounding box.
	 */
	makeEmpty() {

		this.min.x = this.min.y = + Infinity;
		this.max.x = this.max.y = - Infinity;

		return this;

	}

	/**
	 * Returns true if this box includes zero points within its bounds.
	 * Note that a box with equal lower and upper bounds still includes one
	 * point, the one both bounds share.
	 *
	 * @return {boolean} Whether this box is empty or not.
	 */
	isEmpty() {

		// this is a more robust check for empty than ( volume <= 0 ) because volume can get positive with two negative axes

		return ( this.max.x < this.min.x ) || ( this.max.y < this.min.y );

	}

	/**
	 * Returns the center point of this box.
	 *
	 * @param {Vector2} target - The target vector that is used to store the method's result.
	 * @return {Vector2} The center point.
	 */
	getCenter( target ) {

		return this.isEmpty() ? target.set( 0, 0 ) : target.addVectors( this.min, this.max ).multiplyScalar( 0.5 );

	}

	/**
	 * Returns the dimensions of this box.
	 *
	 * @param {Vector2} target - The target vector that is used to store the method's result.
	 * @return {Vector2} The size.
	 */
	getSize( target ) {

		return this.isEmpty() ? target.set( 0, 0 ) : target.subVectors( this.max, this.min );

	}

	/**
	 * Expands the boundaries of this box to include the given point.
	 *
	 * @param {Vector2} point - The point that should be included by the bounding box.
	 * @return {Box2} A reference to this bounding box.
	 */
	expandByPoint( point ) {

		this.min.min( point );
		this.max.max( point );

		return this;

	}

	/**
	 * Expands this box equilaterally by the given vector. The width of this
	 * box will be expanded by the x component of the vector in both
	 * directions. The height of this box will be expanded by the y component of
	 * the vector in both directions.
	 *
	 * @param {Vector2} vector - The vector that should expand the bounding box.
	 * @return {Box2} A reference to this bounding box.
	 */
	expandByVector( vector ) {

		this.min.sub( vector );
		this.max.add( vector );

		return this;

	}

	/**
	 * Expands each dimension of the box by the given scalar. If negative, the
	 * dimensions of the box will be contracted.
	 *
	 * @param {number} scalar - The scalar value that should expand the bounding box.
	 * @return {Box2} A reference to this bounding box.
	 */
	expandByScalar( scalar ) {

		this.min.addScalar( - scalar );
		this.max.addScalar( scalar );

		return this;

	}

	/**
	 * Returns `true` if the given point lies within or on the boundaries of this box.
	 *
	 * @param {Vector2} point - The point to test.
	 * @return {boolean} Whether the bounding box contains the given point or not.
	 */
	containsPoint( point ) {

		return point.x >= this.min.x && point.x <= this.max.x &&
			point.y >= this.min.y && point.y <= this.max.y;

	}

	/**
	 * Returns `true` if this bounding box includes the entirety of the given bounding box.
	 * If this box and the given one are identical, this function also returns `true`.
	 *
	 * @param {Box2} box - The bounding box to test.
	 * @return {boolean} Whether the bounding box contains the given bounding box or not.
	 */
	containsBox( box ) {

		return this.min.x <= box.min.x && box.max.x <= this.max.x &&
			this.min.y <= box.min.y && box.max.y <= this.max.y;

	}

	/**
	 * Returns a point as a proportion of this box's width and height.
	 *
	 * @param {Vector2} point - A point in 2D space.
	 * @param {Vector2} target - The target vector that is used to store the method's result.
	 * @return {Vector2} A point as a proportion of this box's width and height.
	 */
	getParameter( point, target ) {

		// This can potentially have a divide by zero if the box
		// has a size dimension of 0.

		return target.set(
			( point.x - this.min.x ) / ( this.max.x - this.min.x ),
			( point.y - this.min.y ) / ( this.max.y - this.min.y )
		);

	}

	/**
	 * Returns `true` if the given bounding box intersects with this bounding box.
	 *
	 * @param {Box2} box - The bounding box to test.
	 * @return {boolean} Whether the given bounding box intersects with this bounding box.
	 */
	intersectsBox( box ) {

		// using 4 splitting planes to rule out intersections

		return box.max.x >= this.min.x && box.min.x <= this.max.x &&
			box.max.y >= this.min.y && box.min.y <= this.max.y;

	}

	/**
	 * Clamps the given point within the bounds of this box.
	 *
	 * @param {Vector2} point - The point to clamp.
	 * @param {Vector2} target - The target vector that is used to store the method's result.
	 * @return {Vector2} The clamped point.
	 */
	clampPoint( point, target ) {

		return target.copy( point ).clamp( this.min, this.max );

	}

	/**
	 * Returns the euclidean distance from any edge of this box to the specified point. If
	 * the given point lies inside of this box, the distance will be `0`.
	 *
	 * @param {Vector2} point - The point to compute the distance to.
	 * @return {number} The euclidean distance.
	 */
	distanceToPoint( point ) {

		return this.clampPoint( point, _vector ).distanceTo( point );

	}

	/**
	 * Computes the intersection of this bounding box and the given one, setting the upper
	 * bound of this box to the lesser of the two boxes' upper bounds and the
	 * lower bound of this box to the greater of the two boxes' lower bounds. If
	 * there's no overlap, makes this box empty.
	 *
	 * @param {Box2} box - The bounding box to intersect with.
	 * @return {Box2} A reference to this bounding box.
	 */
	intersect( box ) {

		this.min.max( box.min );
		this.max.min( box.max );

		if ( this.isEmpty() ) this.makeEmpty();

		return this;

	}

	/**
	 * Computes the union of this box and another and the given one, setting the upper
	 * bound of this box to the greater of the two boxes' upper bounds and the
	 * lower bound of this box to the lesser of the two boxes' lower bounds.
	 *
	 * @param {Box2} box - The bounding box that will be unioned with this instance.
	 * @return {Box2} A reference to this bounding box.
	 */
	union( box ) {

		this.min.min( box.min );
		this.max.max( box.max );

		return this;

	}

	/**
	 * Adds the given offset to both the upper and lower bounds of this bounding box,
	 * effectively moving it in 2D space.
	 *
	 * @param {Vector2} offset - The offset that should be used to translate the bounding box.
	 * @return {Box2} A reference to this bounding box.
	 */
	translate( offset ) {

		this.min.add( offset );
		this.max.add( offset );

		return this;

	}

	/**
	 * Returns `true` if this bounding box is equal with the given one.
	 *
	 * @param {Box2} box - The box to test for equality.
	 * @return {boolean} Whether this bounding box is equal with the given one.
	 */
	equals( box ) {

		return box.min.equals( this.min ) && box.max.equals( this.max );

	}

}

const _startP = /*@__PURE__*/ new Vector3();
const _startEnd = /*@__PURE__*/ new Vector3();

const _d1 = /*@__PURE__*/ new Vector3();
const _d2 = /*@__PURE__*/ new Vector3();
const _r = /*@__PURE__*/ new Vector3();
const _c1 = /*@__PURE__*/ new Vector3();
const _c2 = /*@__PURE__*/ new Vector3();

/**
 * An analytical line segment in 3D space represented by a start and end point.
 */
class Line3 {

	/**
	 * Constructs a new line segment.
	 *
	 * @param {Vector3} [start=(0,0,0)] - Start of the line segment.
	 * @param {Vector3} [end=(0,0,0)] - End of the line segment.
	 */
	constructor( start = new Vector3(), end = new Vector3() ) {

		/**
		 * Start of the line segment.
		 *
		 * @type {Vector3}
		 */
		this.start = start;

		/**
		 * End of the line segment.
		 *
		 * @type {Vector3}
		 */
		this.end = end;

	}

	/**
	 * Sets the start and end values by copying the given vectors.
	 *
	 * @param {Vector3} start - The start point.
	 * @param {Vector3} end - The end point.
	 * @return {Line3} A reference to this line segment.
	 */
	set( start, end ) {

		this.start.copy( start );
		this.end.copy( end );

		return this;

	}

	/**
	 * Copies the values of the given line segment to this instance.
	 *
	 * @param {Line3} line - The line segment to copy.
	 * @return {Line3} A reference to this line segment.
	 */
	copy( line ) {

		this.start.copy( line.start );
		this.end.copy( line.end );

		return this;

	}

	/**
	 * Returns the center of the line segment.
	 *
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The center point.
	 */
	getCenter( target ) {

		return target.addVectors( this.start, this.end ).multiplyScalar( 0.5 );

	}

	/**
	 * Returns the delta vector of the line segment's start and end point.
	 *
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The delta vector.
	 */
	delta( target ) {

		return target.subVectors( this.end, this.start );

	}

	/**
	 * Returns the squared Euclidean distance between the line' start and end point.
	 *
	 * @return {number} The squared Euclidean distance.
	 */
	distanceSq() {

		return this.start.distanceToSquared( this.end );

	}

	/**
	 * Returns the Euclidean distance between the line' start and end point.
	 *
	 * @return {number} The Euclidean distance.
	 */
	distance() {

		return this.start.distanceTo( this.end );

	}

	/**
	 * Returns a vector at a certain position along the line segment.
	 *
	 * @param {number} t - A value between `[0,1]` to represent a position along the line segment.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The delta vector.
	 */
	at( t, target ) {

		return this.delta( target ).multiplyScalar( t ).add( this.start );

	}

	/**
	 * Returns a point parameter based on the closest point as projected on the line segment.
	 *
	 * @param {Vector3} point - The point for which to return a point parameter.
	 * @param {boolean} clampToLine - Whether to clamp the result to the range `[0,1]` or not.
	 * @return {number} The point parameter.
	 */
	closestPointToPointParameter( point, clampToLine ) {

		_startP.subVectors( point, this.start );
		_startEnd.subVectors( this.end, this.start );

		const startEnd2 = _startEnd.dot( _startEnd );
		const startEnd_startP = _startEnd.dot( _startP );

		let t = startEnd_startP / startEnd2;

		if ( clampToLine ) {

			t = clamp( t, 0, 1 );

		}

		return t;

	}

	/**
	 * Returns the closest point on the line for a given point.
	 *
	 * @param {Vector3} point - The point to compute the closest point on the line for.
	 * @param {boolean} clampToLine - Whether to clamp the result to the range `[0,1]` or not.
	 * @param {Vector3} target - The target vector that is used to store the method's result.
	 * @return {Vector3} The closest point on the line.
	 */
	closestPointToPoint( point, clampToLine, target ) {

		const t = this.closestPointToPointParameter( point, clampToLine );

		return this.delta( target ).multiplyScalar( t ).add( this.start );

	}

	/**
	 * Returns the closest squared distance between this line segment and the given one.
	 *
	 * @param {Line3} line - The line segment to compute the closest squared distance to.
	 * @param {Vector3} [c1] - The closest point on this line segment.
	 * @param {Vector3} [c2] - The closest point on the given line segment.
	 * @return {number} The squared distance between this line segment and the given one.
	 */
	distanceSqToLine3( line, c1 = _c1, c2 = _c2 ) {

		// from Real-Time Collision Detection by Christer Ericson, chapter 5.1.9

		// Computes closest points C1 and C2 of S1(s)=P1+s*(Q1-P1) and
		// S2(t)=P2+t*(Q2-P2), returning s and t. Function result is squared
		// distance between between S1(s) and S2(t)

		const EPSILON = 1e-8 * 1e-8; // must be squared since we compare squared length
		let s, t;

		const p1 = this.start;
		const p2 = line.start;
		const q1 = this.end;
		const q2 = line.end;

		_d1.subVectors( q1, p1 ); // Direction vector of segment S1
		_d2.subVectors( q2, p2 ); // Direction vector of segment S2
		_r.subVectors( p1, p2 );

		const a = _d1.dot( _d1 ); // Squared length of segment S1, always nonnegative
		const e = _d2.dot( _d2 ); // Squared length of segment S2, always nonnegative
		const f = _d2.dot( _r );

		// Check if either or both segments degenerate into points

		if ( a <= EPSILON && e <= EPSILON ) {

			// Both segments degenerate into points

			c1.copy( p1 );
			c2.copy( p2 );

			c1.sub( c2 );

			return c1.dot( c1 );

		}

		if ( a <= EPSILON ) {

			// First segment degenerates into a point

			s = 0;
			t = f / e; // s = 0 => t = (b*s + f) / e = f / e
			t = clamp( t, 0, 1 );


		} else {

			const c = _d1.dot( _r );

			if ( e <= EPSILON ) {

				// Second segment degenerates into a point

				t = 0;
				s = clamp( - c / a, 0, 1 ); // t = 0 => s = (b*t - c) / a = -c / a

			} else {

				// The general nondegenerate case starts here

				const b = _d1.dot( _d2 );
				const denom = a * e - b * b; // Always nonnegative

				// If segments not parallel, compute closest point on L1 to L2 and
				// clamp to segment S1. Else pick arbitrary s (here 0)

				if ( denom !== 0 ) {

					s = clamp( ( b * f - c * e ) / denom, 0, 1 );

				} else {

					s = 0;

				}

				// Compute point on L2 closest to S1(s) using
				// t = Dot((P1 + D1*s) - P2,D2) / Dot(D2,D2) = (b*s + f) / e

				t = ( b * s + f ) / e;

				// If t in [0,1] done. Else clamp t, recompute s for the new value
				// of t using s = Dot((P2 + D2*t) - P1,D1) / Dot(D1,D1)= (t*b - c) / a
				// and clamp s to [0, 1]

				if ( t < 0 ) {

					t = 0.;
					s = clamp( - c / a, 0, 1 );

				} else if ( t > 1 ) {

					t = 1;
					s = clamp( ( b - c ) / a, 0, 1 );

				}

			}

		}

		c1.copy( p1 ).add( _d1.multiplyScalar( s ) );
		c2.copy( p2 ).add( _d2.multiplyScalar( t ) );

		c1.sub( c2 );

		return c1.dot( c1 );

	}

	/**
	 * Applies a 4x4 transformation matrix to this line segment.
	 *
	 * @param {Matrix4} matrix - The transformation matrix.
	 * @return {Line3} A reference to this line segment.
	 */
	applyMatrix4( matrix ) {

		this.start.applyMatrix4( matrix );
		this.end.applyMatrix4( matrix );

		return this;

	}

	/**
	 * Returns `true` if this line segment is equal with the given one.
	 *
	 * @param {Line3} line - The line segment to test for equality.
	 * @return {boolean} Whether this line segment is equal with the given one.
	 */
	equals( line ) {

		return line.start.equals( this.start ) && line.end.equals( this.end );

	}

	/**
	 * Returns a new line segment with copied values from this instance.
	 *
	 * @return {Line3} A clone of this instance.
	 */
	clone() {

		return new this.constructor().copy( this );

	}

}

/**
 * A helper object to visualize an instance of {@link Plane}.
 *
 * ```js
 * const plane = new THREE.Plane( new THREE.Vector3( 1, 1, 0.2 ), 3 );
 * const helper = new THREE.PlaneHelper( plane, 1, 0xffff00 );
 * scene.add( helper );
 * ```
 *
 * @augments Line
 */
class PlaneHelper extends Line$1 {

	/**
	 * Constructs a new plane helper.
	 *
	 * @param {Plane} plane - The plane to be visualized.
	 * @param {number} [size=1] - The side length of plane helper.
	 * @param {number|Color|string} [hex=0xffff00] - The helper's color.
	 */
	constructor( plane, size = 1, hex = 0xffff00 ) {

		const color = hex;

		const positions = [ 1, -1, 0, -1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0, -1, -1, 0, 1, -1, 0, 1, 1, 0 ];

		const geometry = new BufferGeometry();
		geometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
		geometry.computeBoundingSphere();

		super( geometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );

		this.type = 'PlaneHelper';

		/**
		 * The plane being visualized.
		 *
		 * @type {Plane}
		 */
		this.plane = plane;

		/**
		 * The side length of plane helper.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.size = size;

		const positions2 = [ 1, 1, 0, -1, 1, 0, -1, -1, 0, 1, 1, 0, -1, -1, 0, 1, -1, 0 ];

		const geometry2 = new BufferGeometry();
		geometry2.setAttribute( 'position', new Float32BufferAttribute( positions2, 3 ) );
		geometry2.computeBoundingSphere();

		this.add( new Mesh( geometry2, new MeshBasicMaterial( { color: color, opacity: 0.2, transparent: true, depthWrite: false, toneMapped: false } ) ) );

	}

	updateMatrixWorld( force ) {

		this.position.set( 0, 0, 0 );

		this.scale.set( 0.5 * this.size, 0.5 * this.size, 1 );

		this.lookAt( this.plane.normal );

		this.translateZ( - this.plane.constant );

		super.updateMatrixWorld( force );

	}

	/**
	 * Updates the helper to match the position and direction of the
	 * light being visualized.
	 */
	dispose() {

		this.geometry.dispose();
		this.material.dispose();
		this.children[ 0 ].geometry.dispose();
		this.children[ 0 ].material.dispose();

	}

}

/**
 * This class is used to convert a series of paths to an array of
 * shapes. It is specifically used in context of fonts and SVG.
 */
class ShapePath {

	/**
	 * Constructs a new shape path.
	 */
	constructor() {

		this.type = 'ShapePath';

		/**
		 * The color of the shape.
		 *
		 * @type {Color}
		 */
		this.color = new Color();

		/**
		 * The paths that have been generated for this shape.
		 *
		 * @type {Array<Path>}
		 * @default null
		 */
		this.subPaths = [];

		/**
		 * The current path that is being generated.
		 *
		 * @type {?Path}
		 * @default null
		 */
		this.currentPath = null;

	}

	/**
	 * Creates a new path and moves it current point to the given one.
	 *
	 * @param {number} x - The x coordinate.
	 * @param {number} y - The y coordinate.
	 * @return {ShapePath} A reference to this shape path.
	 */
	moveTo( x, y ) {

		this.currentPath = new Path$1();
		this.subPaths.push( this.currentPath );
		this.currentPath.moveTo( x, y );

		return this;

	}

	/**
	 * Adds an instance of {@link LineCurve} to the path by connecting
	 * the current point with the given one.
	 *
	 * @param {number} x - The x coordinate of the end point.
	 * @param {number} y - The y coordinate of the end point.
	 * @return {ShapePath} A reference to this shape path.
	 */
	lineTo( x, y ) {

		this.currentPath.lineTo( x, y );

		return this;

	}

	/**
	 * Adds an instance of {@link QuadraticBezierCurve} to the path by connecting
	 * the current point with the given one.
	 *
	 * @param {number} aCPx - The x coordinate of the control point.
	 * @param {number} aCPy - The y coordinate of the control point.
	 * @param {number} aX - The x coordinate of the end point.
	 * @param {number} aY - The y coordinate of the end point.
	 * @return {ShapePath} A reference to this shape path.
	 */
	quadraticCurveTo( aCPx, aCPy, aX, aY ) {

		this.currentPath.quadraticCurveTo( aCPx, aCPy, aX, aY );

		return this;

	}

	/**
	 * Adds an instance of {@link CubicBezierCurve} to the path by connecting
	 * the current point with the given one.
	 *
	 * @param {number} aCP1x - The x coordinate of the first control point.
	 * @param {number} aCP1y - The y coordinate of the first control point.
	 * @param {number} aCP2x - The x coordinate of the second control point.
	 * @param {number} aCP2y - The y coordinate of the second control point.
	 * @param {number} aX - The x coordinate of the end point.
	 * @param {number} aY - The y coordinate of the end point.
	 * @return {ShapePath} A reference to this shape path.
	 */
	bezierCurveTo( aCP1x, aCP1y, aCP2x, aCP2y, aX, aY ) {

		this.currentPath.bezierCurveTo( aCP1x, aCP1y, aCP2x, aCP2y, aX, aY );

		return this;

	}

	/**
	 * Adds an instance of {@link SplineCurve} to the path by connecting
	 * the current point with the given list of points.
	 *
	 * @param {Array<Vector2>} pts - An array of points in 2D space.
	 * @return {ShapePath} A reference to this shape path.
	 */
	splineThru( pts ) {

		this.currentPath.splineThru( pts );

		return this;

	}

	/**
	 * Converts the paths into an array of shapes.
	 *
	 * @param {boolean} isCCW - By default solid shapes are  defined clockwise (CW) and holes are defined counterclockwise (CCW).
	 * If this flag is set to `true`, then those are flipped.
	 * @return {Array<Shape>} An array of shapes.
	 */
	toShapes( isCCW ) {

		function toShapesNoHoles( inSubpaths ) {

			const shapes = [];

			for ( let i = 0, l = inSubpaths.length; i < l; i ++ ) {

				const tmpPath = inSubpaths[ i ];

				const tmpShape = new Shape();
				tmpShape.curves = tmpPath.curves;

				shapes.push( tmpShape );

			}

			return shapes;

		}

		function isPointInsidePolygon( inPt, inPolygon ) {

			const polyLen = inPolygon.length;

			// inPt on polygon contour => immediate success    or
			// toggling of inside/outside at every single! intersection point of an edge
			//  with the horizontal line through inPt, left of inPt
			//  not counting lowerY endpoints of edges and whole edges on that line
			let inside = false;
			for ( let p = polyLen - 1, q = 0; q < polyLen; p = q ++ ) {

				let edgeLowPt = inPolygon[ p ];
				let edgeHighPt = inPolygon[ q ];

				let edgeDx = edgeHighPt.x - edgeLowPt.x;
				let edgeDy = edgeHighPt.y - edgeLowPt.y;

				if ( Math.abs( edgeDy ) > Number.EPSILON ) {

					// not parallel
					if ( edgeDy < 0 ) {

						edgeLowPt = inPolygon[ q ]; edgeDx = - edgeDx;
						edgeHighPt = inPolygon[ p ]; edgeDy = - edgeDy;

					}

					if ( ( inPt.y < edgeLowPt.y ) || ( inPt.y > edgeHighPt.y ) ) 		continue;

					if ( inPt.y === edgeLowPt.y ) {

						if ( inPt.x === edgeLowPt.x )		return	true;		// inPt is on contour ?
						// continue;				// no intersection or edgeLowPt => doesn't count !!!

					} else {

						const perpEdge = edgeDy * ( inPt.x - edgeLowPt.x ) - edgeDx * ( inPt.y - edgeLowPt.y );
						if ( perpEdge === 0 )				return	true;		// inPt is on contour ?
						if ( perpEdge < 0 ) 				continue;
						inside = ! inside;		// true intersection left of inPt

					}

				} else {

					// parallel or collinear
					if ( inPt.y !== edgeLowPt.y ) 		continue;			// parallel
					// edge lies on the same horizontal line as inPt
					if ( ( ( edgeHighPt.x <= inPt.x ) && ( inPt.x <= edgeLowPt.x ) ) ||
						 ( ( edgeLowPt.x <= inPt.x ) && ( inPt.x <= edgeHighPt.x ) ) )		return	true;	// inPt: Point on contour !
					// continue;

				}

			}

			return	inside;

		}

		const isClockWise = ShapeUtils.isClockWise;

		const subPaths = this.subPaths;
		if ( subPaths.length === 0 ) return [];

		let solid, tmpPath, tmpShape;
		const shapes = [];

		if ( subPaths.length === 1 ) {

			tmpPath = subPaths[ 0 ];
			tmpShape = new Shape();
			tmpShape.curves = tmpPath.curves;
			shapes.push( tmpShape );
			return shapes;

		}

		let holesFirst = ! isClockWise( subPaths[ 0 ].getPoints() );
		holesFirst = isCCW ? ! holesFirst : holesFirst;

		// console.log("Holes first", holesFirst);

		const betterShapeHoles = [];
		const newShapes = [];
		let newShapeHoles = [];
		let mainIdx = 0;
		let tmpPoints;

		newShapes[ mainIdx ] = undefined;
		newShapeHoles[ mainIdx ] = [];

		for ( let i = 0, l = subPaths.length; i < l; i ++ ) {

			tmpPath = subPaths[ i ];
			tmpPoints = tmpPath.getPoints();
			solid = isClockWise( tmpPoints );
			solid = isCCW ? ! solid : solid;

			if ( solid ) {

				if ( ( ! holesFirst ) && ( newShapes[ mainIdx ] ) )	mainIdx ++;

				newShapes[ mainIdx ] = { s: new Shape(), p: tmpPoints };
				newShapes[ mainIdx ].s.curves = tmpPath.curves;

				if ( holesFirst )	mainIdx ++;
				newShapeHoles[ mainIdx ] = [];

				//console.log('cw', i);

			} else {

				newShapeHoles[ mainIdx ].push( { h: tmpPath, p: tmpPoints[ 0 ] } );

				//console.log('ccw', i);

			}

		}

		// only Holes? -> probably all Shapes with wrong orientation
		if ( ! newShapes[ 0 ] )	return	toShapesNoHoles( subPaths );


		if ( newShapes.length > 1 ) {

			let ambiguous = false;
			let toChange = 0;

			for ( let sIdx = 0, sLen = newShapes.length; sIdx < sLen; sIdx ++ ) {

				betterShapeHoles[ sIdx ] = [];

			}

			for ( let sIdx = 0, sLen = newShapes.length; sIdx < sLen; sIdx ++ ) {

				const sho = newShapeHoles[ sIdx ];

				for ( let hIdx = 0; hIdx < sho.length; hIdx ++ ) {

					const ho = sho[ hIdx ];
					let hole_unassigned = true;

					for ( let s2Idx = 0; s2Idx < newShapes.length; s2Idx ++ ) {

						if ( isPointInsidePolygon( ho.p, newShapes[ s2Idx ].p ) ) {

							if ( sIdx !== s2Idx )	toChange ++;

							if ( hole_unassigned ) {

								hole_unassigned = false;
								betterShapeHoles[ s2Idx ].push( ho );

							} else {

								ambiguous = true;

							}

						}

					}

					if ( hole_unassigned ) {

						betterShapeHoles[ sIdx ].push( ho );

					}

				}

			}

			if ( toChange > 0 && ambiguous === false ) {

				newShapeHoles = betterShapeHoles;

			}

		}

		let tmpHoles;

		for ( let i = 0, il = newShapes.length; i < il; i ++ ) {

			tmpShape = newShapes[ i ].s;
			shapes.push( tmpShape );
			tmpHoles = newShapeHoles[ i ];

			for ( let j = 0, jl = tmpHoles.length; j < jl; j ++ ) {

				tmpShape.holes.push( tmpHoles[ j ].h );

			}

		}

		//console.log("shape", shapes);

		return shapes;

	}

}

/**
 * Abstract base class for controls.
 *
 * @abstract
 * @augments EventDispatcher
 */
class Controls extends EventDispatcher {

	/**
	 * Constructs a new controls instance.
	 *
	 * @param {Object3D} object - The object that is managed by the controls.
	 * @param {?HTMLDOMElement} domElement - The HTML element used for event listeners.
	 */
	constructor( object, domElement = null ) {

		super();

		/**
		 * The object that is managed by the controls.
		 *
		 * @type {Object3D}
		 */
		this.object = object;

		/**
		 * The HTML element used for event listeners.
		 *
		 * @type {?HTMLDOMElement}
		 * @default null
		 */
		this.domElement = domElement;

		/**
		 * Whether the controls responds to user input or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.enabled = true;

		/**
		 * The internal state of the controls.
		 *
		 * @type {number}
		 * @default -1
		 */
		this.state = -1;

		/**
		 * This object defines the keyboard input of the controls.
		 *
		 * @type {Object}
		 */
		this.keys = {};

		/**
		 * This object defines what type of actions are assigned to the available mouse buttons.
		 * It depends on the control implementation what kind of mouse buttons and actions are supported.
		 *
		 * @type {{LEFT: ?number, MIDDLE: ?number, RIGHT: ?number}}
		 */
		this.mouseButtons = { LEFT: null, MIDDLE: null, RIGHT: null };

		/**
		 * This object defines what type of actions are assigned to what kind of touch interaction.
		 * It depends on the control implementation what kind of touch interaction and actions are supported.
		 *
		 * @type {{ONE: ?number, TWO: ?number}}
		 */
		this.touches = { ONE: null, TWO: null };

	}

	/**
	 * Connects the controls to the DOM. This method has so called "side effects" since
	 * it adds the module's event listeners to the DOM.
	 *
	 * @param {HTMLDOMElement} element - The DOM element to connect to.
	 */
	connect( element ) {

		if ( element === undefined ) {

			console.warn( 'THREE.Controls: connect() now requires an element.' ); // @deprecated, the warning can be removed with r185
			return;

		}

		if ( this.domElement !== null ) this.disconnect();

		this.domElement = element;

	}

	/**
	 * Disconnects the controls from the DOM.
	 */
	disconnect() {}

	/**
	 * Call this method if you no longer want use to the controls. It frees all internal
	 * resources and removes all event listeners.
	 */
	dispose() {}

	/**
	 * Controls should implement this method if they have to update their internal state
	 * per simulation step.
	 *
	 * @param {number} [delta] - The time delta in seconds.
	 */
	update( /* delta */ ) {}

}

// export * from './Three.Legacy.js';

if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {

	/* eslint-disable no-undef */
	__THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'register', { detail: {
		revision: REVISION,
	} } ) );
	/* eslint-enable no-undef */

}

if ( typeof window !== 'undefined' ) {

	if ( window.__THREE__ ) {

		console.warn( 'WARNING: Multiple instances of Three.js being imported.' );

	} else {

		window.__THREE__ = REVISION;

	}

}

/**
 * @license
 * Copyright 2010-2025 Three.js Authors
 * SPDX-License-Identifier: MIT
 */

/**
 * A class for generating text as a single geometry. It is constructed by providing a string of text, and a set of
 * parameters consisting of a loaded font and extrude settings.
 *
 * See the {@link FontLoader} page for additional details.
 *
 * `TextGeometry` uses [typeface.json]{@link http://gero3.github.io/facetype.js/} generated fonts.
 * Some existing fonts can be found located in `/examples/fonts`.
 *
 * ```js
 * const loader = new FontLoader();
 * const font = await loader.loadAsync( 'fonts/helvetiker_regular.typeface.json' );
 * const geometry = new TextGeometry( 'Hello three.js!', {
 * 	font: font,
 * 	size: 80,
 * 	depth: 5,
 * 	curveSegments: 12
 * } );
 * ```
 *
 * @augments ExtrudeGeometry
 * @three_import import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
 */
class TextGeometry extends ExtrudeGeometry {

	/**
	 * Constructs a new text geometry.
	 *
	 * @param {string} text - The text that should be transformed into a geometry.
	 * @param {TextGeometry~Options} [parameters] - The text settings.
	 */
	constructor( text, parameters = {} ) {

		const font = parameters.font;

		if ( font === undefined ) {

			super(); // generate default extrude geometry

		} else {

			const shapes = font.generateShapes( text, parameters.size );

			// defaults

			if ( parameters.depth === undefined ) parameters.depth = 50;
			if ( parameters.bevelThickness === undefined ) parameters.bevelThickness = 10;
			if ( parameters.bevelSize === undefined ) parameters.bevelSize = 8;
			if ( parameters.bevelEnabled === undefined ) parameters.bevelEnabled = false;

			super( shapes, parameters );

		}

		this.type = 'TextGeometry';

	}

}

/**
 * Class representing a font.
 */
class Font {

	/**
	 * Constructs a new font.
	 *
	 * @param {Object} data - The font data as JSON.
	 */
	constructor( data ) {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isFont = true;

		this.type = 'Font';

		/**
		 * The font data as JSON.
		 *
		 * @type {Object}
		 */
		this.data = data;

	}

	/**
	 * Generates geometry shapes from the given text and size. The result of this method
	 * should be used with {@link ShapeGeometry} to generate the actual geometry data.
	 *
	 * @param {string} text - The text.
	 * @param {number} [size=100] - The text size.
	 * @return {Array<Shape>} An array of shapes representing the text.
	 */
	generateShapes( text, size = 100 ) {

		const shapes = [];
		const paths = createPaths( text, size, this.data );

		for ( let p = 0, pl = paths.length; p < pl; p ++ ) {

			shapes.push( ...paths[ p ].toShapes() );

		}

		return shapes;

	}

}

function createPaths( text, size, data ) {

	const chars = Array.from( text );
	const scale = size / data.resolution;
	const line_height = ( data.boundingBox.yMax - data.boundingBox.yMin + data.underlineThickness ) * scale;

	const paths = [];

	let offsetX = 0, offsetY = 0;

	for ( let i = 0; i < chars.length; i ++ ) {

		const char = chars[ i ];

		if ( char === '\n' ) {

			offsetX = 0;
			offsetY -= line_height;

		} else {

			const ret = createPath( char, scale, offsetX, offsetY, data );
			offsetX += ret.offsetX;
			paths.push( ret.path );

		}

	}

	return paths;

}

function createPath( char, scale, offsetX, offsetY, data ) {

	const glyph = data.glyphs[ char ] || data.glyphs[ '?' ];

	if ( ! glyph ) {

		console.error( 'THREE.Font: character "' + char + '" does not exists in font family ' + data.familyName + '.' );

		return;

	}

	const path = new ShapePath();

	let x, y, cpx, cpy, cpx1, cpy1, cpx2, cpy2;

	if ( glyph.o ) {

		const outline = glyph._cachedOutline || ( glyph._cachedOutline = glyph.o.split( ' ' ) );

		for ( let i = 0, l = outline.length; i < l; ) {

			const action = outline[ i ++ ];

			switch ( action ) {

				case 'm': // moveTo

					x = outline[ i ++ ] * scale + offsetX;
					y = outline[ i ++ ] * scale + offsetY;

					path.moveTo( x, y );

					break;

				case 'l': // lineTo

					x = outline[ i ++ ] * scale + offsetX;
					y = outline[ i ++ ] * scale + offsetY;

					path.lineTo( x, y );

					break;

				case 'q': // quadraticCurveTo

					cpx = outline[ i ++ ] * scale + offsetX;
					cpy = outline[ i ++ ] * scale + offsetY;
					cpx1 = outline[ i ++ ] * scale + offsetX;
					cpy1 = outline[ i ++ ] * scale + offsetY;

					path.quadraticCurveTo( cpx1, cpy1, cpx, cpy );

					break;

				case 'b': // bezierCurveTo

					cpx = outline[ i ++ ] * scale + offsetX;
					cpy = outline[ i ++ ] * scale + offsetY;
					cpx1 = outline[ i ++ ] * scale + offsetX;
					cpy1 = outline[ i ++ ] * scale + offsetY;
					cpx2 = outline[ i ++ ] * scale + offsetX;
					cpy2 = outline[ i ++ ] * scale + offsetY;

					path.bezierCurveTo( cpx1, cpy1, cpx2, cpy2, cpx, cpy );

					break;

			}

		}

	}

	return { offsetX: glyph.ha * scale, path: path };

}

/**
 * Fires when the camera has been transformed by the controls.
 *
 * @event OrbitControls#change
 * @type {Object}
 */
const _changeEvent = { type: 'change' };

/**
 * Fires when an interaction was initiated.
 *
 * @event OrbitControls#start
 * @type {Object}
 */
const _startEvent = { type: 'start' };

/**
 * Fires when an interaction has finished.
 *
 * @event OrbitControls#end
 * @type {Object}
 */
const _endEvent = { type: 'end' };

const _ray = new Ray();
const _plane = new Plane();
const _TILT_LIMIT = Math.cos( 70 * MathUtils$1.DEG2RAD );

const _v = new Vector3();
const _twoPI = 2 * Math.PI;

const _STATE = {
	NONE: -1,
	ROTATE: 0,
	DOLLY: 1,
	PAN: 2,
	TOUCH_ROTATE: 3,
	TOUCH_PAN: 4,
	TOUCH_DOLLY_PAN: 5,
	TOUCH_DOLLY_ROTATE: 6
};
const _EPS = 0.000001;


/**
 * Orbit controls allow the camera to orbit around a target.
 *
 * OrbitControls performs orbiting, dollying (zooming), and panning. Unlike {@link TrackballControls},
 * it maintains the "up" direction `object.up` (+Y by default).
 *
 * - Orbit: Left mouse / touch: one-finger move.
 * - Zoom: Middle mouse, or mousewheel / touch: two-finger spread or squish.
 * - Pan: Right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move.
 *
 * ```js
 * const controls = new OrbitControls( camera, renderer.domElement );
 *
 * // controls.update() must be called after any manual changes to the camera's transform
 * camera.position.set( 0, 20, 100 );
 * controls.update();
 *
 * function animate() {
 *
 * 	// required if controls.enableDamping or controls.autoRotate are set to true
 * 	controls.update();
 *
 * 	renderer.render( scene, camera );
 *
 * }
 * ```
 *
 * @augments Controls
 * @three_import import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 */
class OrbitControls extends Controls {

	/**
	 * Constructs a new controls instance.
	 *
	 * @param {Object3D} object - The object that is managed by the controls.
	 * @param {?HTMLDOMElement} domElement - The HTML element used for event listeners.
	 */
	constructor( object, domElement = null ) {

		super( object, domElement );

		this.state = _STATE.NONE;

		/**
		 * The focus point of the controls, the `object` orbits around this.
		 * It can be updated manually at any point to change the focus of the controls.
		 *
		 * @type {Vector3}
		 */
		this.target = new Vector3();

		/**
		 * The focus point of the `minTargetRadius` and `maxTargetRadius` limits.
		 * It can be updated manually at any point to change the center of interest
		 * for the `target`.
		 *
		 * @type {Vector3}
		 */
		this.cursor = new Vector3();

		/**
		 * How far you can dolly in (perspective camera only).
		 *
		 * @type {number}
		 * @default 0
		 */
		this.minDistance = 0;

		/**
		 * How far you can dolly out (perspective camera only).
		 *
		 * @type {number}
		 * @default Infinity
		 */
		this.maxDistance = Infinity;

		/**
		 * How far you can zoom in (orthographic camera only).
		 *
		 * @type {number}
		 * @default 0
		 */
		this.minZoom = 0;

		/**
		 * How far you can zoom out (orthographic camera only).
		 *
		 * @type {number}
		 * @default Infinity
		 */
		this.maxZoom = Infinity;

		/**
		 * How close you can get the target to the 3D `cursor`.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.minTargetRadius = 0;

		/**
		 * How far you can move the target from the 3D `cursor`.
		 *
		 * @type {number}
		 * @default Infinity
		 */
		this.maxTargetRadius = Infinity;

		/**
		 * How far you can orbit vertically, lower limit. Range is `[0, Math.PI]` radians.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.minPolarAngle = 0;

		/**
		 * How far you can orbit vertically, upper limit. Range is `[0, Math.PI]` radians.
		 *
		 * @type {number}
		 * @default Math.PI
		 */
		this.maxPolarAngle = Math.PI;

		/**
		 * How far you can orbit horizontally, lower limit. If set, the interval `[ min, max ]`
		 * must be a sub-interval of `[ - 2 PI, 2 PI ]`, with `( max - min < 2 PI )`.
		 *
		 * @type {number}
		 * @default -Infinity
		 */
		this.minAzimuthAngle = - Infinity;

		/**
		 * How far you can orbit horizontally, upper limit. If set, the interval `[ min, max ]`
		 * must be a sub-interval of `[ - 2 PI, 2 PI ]`, with `( max - min < 2 PI )`.
		 *
		 * @type {number}
		 * @default -Infinity
		 */
		this.maxAzimuthAngle = Infinity;

		/**
		 * Set to `true` to enable damping (inertia), which can be used to give a sense of weight
		 * to the controls. Note that if this is enabled, you must call `update()` in your animation
		 * loop.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.enableDamping = false;

		/**
		 * The damping inertia used if `enableDamping` is set to `true`.
		 *
		 * Note that for this to work, you must call `update()` in your animation loop.
		 *
		 * @type {number}
		 * @default 0.05
		 */
		this.dampingFactor = 0.05;

		/**
		 * Enable or disable zooming (dollying) of the camera.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.enableZoom = true;

		/**
		 * Speed of zooming / dollying.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.zoomSpeed = 1.0;

		/**
		 * Enable or disable horizontal and vertical rotation of the camera.
		 *
		 * Note that it is possible to disable a single axis by setting the min and max of the
		 * `minPolarAngle` or `minAzimuthAngle` to the same value, which will cause the vertical
		 * or horizontal rotation to be fixed at that value.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.enableRotate = true;

		/**
		 * Speed of rotation.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.rotateSpeed = 1.0;

		/**
		 * How fast to rotate the camera when the keyboard is used.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.keyRotateSpeed = 1.0;

		/**
		 * Enable or disable camera panning.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.enablePan = true;

		/**
		 * Speed of panning.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.panSpeed = 1.0;

		/**
		 * Defines how the camera's position is translated when panning. If `true`, the camera pans
		 * in screen space. Otherwise, the camera pans in the plane orthogonal to the camera's up
		 * direction.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.screenSpacePanning = true;

		/**
		 * How fast to pan the camera when the keyboard is used in
		 * pixels per keypress.
		 *
		 * @type {number}
		 * @default 7
		 */
		this.keyPanSpeed = 7.0;

		/**
		 * Setting this property to `true` allows to zoom to the cursor's position.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.zoomToCursor = false;

		/**
		 * Set to true to automatically rotate around the target
		 *
		 * Note that if this is enabled, you must call `update()` in your animation loop.
		 * If you want the auto-rotate speed to be independent of the frame rate (the refresh
		 * rate of the display), you must pass the time `deltaTime`, in seconds, to `update()`.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.autoRotate = false;

		/**
		 * How fast to rotate around the target if `autoRotate` is `true`. The default  equates to 30 seconds
		 * per orbit at 60fps.
		 *
		 * Note that if `autoRotate` is enabled, you must call `update()` in your animation loop.
		 *
		 * @type {number}
		 * @default 2
		 */
		this.autoRotateSpeed = 2.0;

		/**
		 * This object contains references to the keycodes for controlling camera panning.
		 *
		 * ```js
		 * controls.keys = {
		 * 	LEFT: 'ArrowLeft', //left arrow
		 * 	UP: 'ArrowUp', // up arrow
		 * 	RIGHT: 'ArrowRight', // right arrow
		 * 	BOTTOM: 'ArrowDown' // down arrow
		 * }
		 * ```
		 * @type {Object}
		 */
		this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' };

		/**
		 * This object contains references to the mouse actions used by the controls.
		 *
		 * ```js
		 * controls.mouseButtons = {
		 * 	LEFT: THREE.MOUSE.ROTATE,
		 * 	MIDDLE: THREE.MOUSE.DOLLY,
		 * 	RIGHT: THREE.MOUSE.PAN
		 * }
		 * ```
		 * @type {Object}
		 */
		this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };

		/**
		 * This object contains references to the touch actions used by the controls.
		 *
		 * ```js
		 * controls.mouseButtons = {
		 * 	ONE: THREE.TOUCH.ROTATE,
		 * 	TWO: THREE.TOUCH.DOLLY_PAN
		 * }
		 * ```
		 * @type {Object}
		 */
		this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };

		/**
		 * Used internally by `saveState()` and `reset()`.
		 *
		 * @type {Vector3}
		 */
		this.target0 = this.target.clone();

		/**
		 * Used internally by `saveState()` and `reset()`.
		 *
		 * @type {Vector3}
		 */
		this.position0 = this.object.position.clone();

		/**
		 * Used internally by `saveState()` and `reset()`.
		 *
		 * @type {number}
		 */
		this.zoom0 = this.object.zoom;

		// the target DOM element for key events
		this._domElementKeyEvents = null;

		// internals

		this._lastPosition = new Vector3();
		this._lastQuaternion = new Quaternion();
		this._lastTargetPosition = new Vector3();

		// so camera.up is the orbit axis
		this._quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );
		this._quatInverse = this._quat.clone().invert();

		// current position in spherical coordinates
		this._spherical = new Spherical();
		this._sphericalDelta = new Spherical();

		this._scale = 1;
		this._panOffset = new Vector3();

		this._rotateStart = new Vector2();
		this._rotateEnd = new Vector2();
		this._rotateDelta = new Vector2();

		this._panStart = new Vector2();
		this._panEnd = new Vector2();
		this._panDelta = new Vector2();

		this._dollyStart = new Vector2();
		this._dollyEnd = new Vector2();
		this._dollyDelta = new Vector2();

		this._dollyDirection = new Vector3();
		this._mouse = new Vector2();
		this._performCursorZoom = false;

		this._pointers = [];
		this._pointerPositions = {};

		this._controlActive = false;

		// event listeners

		this._onPointerMove = onPointerMove.bind( this );
		this._onPointerDown = onPointerDown.bind( this );
		this._onPointerUp = onPointerUp.bind( this );
		this._onContextMenu = onContextMenu.bind( this );
		this._onMouseWheel = onMouseWheel.bind( this );
		this._onKeyDown = onKeyDown.bind( this );

		this._onTouchStart = onTouchStart.bind( this );
		this._onTouchMove = onTouchMove.bind( this );

		this._onMouseDown = onMouseDown.bind( this );
		this._onMouseMove = onMouseMove.bind( this );

		this._interceptControlDown = interceptControlDown.bind( this );
		this._interceptControlUp = interceptControlUp.bind( this );

		//

		if ( this.domElement !== null ) {

			this.connect( this.domElement );

		}

		this.update();

	}

	connect( element ) {

		super.connect( element );

		this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
		this.domElement.addEventListener( 'pointercancel', this._onPointerUp );

		this.domElement.addEventListener( 'contextmenu', this._onContextMenu );
		this.domElement.addEventListener( 'wheel', this._onMouseWheel, { passive: false } );

		const document = this.domElement.getRootNode(); // offscreen canvas compatibility
		document.addEventListener( 'keydown', this._interceptControlDown, { passive: true, capture: true } );

		this.domElement.style.touchAction = 'none'; // disable touch scroll

	}

	disconnect() {

		this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
		this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
		this.domElement.removeEventListener( 'pointerup', this._onPointerUp );
		this.domElement.removeEventListener( 'pointercancel', this._onPointerUp );

		this.domElement.removeEventListener( 'wheel', this._onMouseWheel );
		this.domElement.removeEventListener( 'contextmenu', this._onContextMenu );

		this.stopListenToKeyEvents();

		const document = this.domElement.getRootNode(); // offscreen canvas compatibility
		document.removeEventListener( 'keydown', this._interceptControlDown, { capture: true } );

		this.domElement.style.touchAction = 'auto';

	}

	dispose() {

		this.disconnect();

	}

	/**
	 * Get the current vertical rotation, in radians.
	 *
	 * @return {number} The current vertical rotation, in radians.
	 */
	getPolarAngle() {

		return this._spherical.phi;

	}

	/**
	 * Get the current horizontal rotation, in radians.
	 *
	 * @return {number} The current horizontal rotation, in radians.
	 */
	getAzimuthalAngle() {

		return this._spherical.theta;

	}

	/**
	 * Returns the distance from the camera to the target.
	 *
	 * @return {number} The distance from the camera to the target.
	 */
	getDistance() {

		return this.object.position.distanceTo( this.target );

	}

	/**
	 * Adds key event listeners to the given DOM element.
	 * `window` is a recommended argument for using this method.
	 *
	 * @param {HTMLDOMElement} domElement - The DOM element
	 */
	listenToKeyEvents( domElement ) {

		domElement.addEventListener( 'keydown', this._onKeyDown );
		this._domElementKeyEvents = domElement;

	}

	/**
	 * Removes the key event listener previously defined with `listenToKeyEvents()`.
	 */
	stopListenToKeyEvents() {

		if ( this._domElementKeyEvents !== null ) {

			this._domElementKeyEvents.removeEventListener( 'keydown', this._onKeyDown );
			this._domElementKeyEvents = null;

		}

	}

	/**
	 * Save the current state of the controls. This can later be recovered with `reset()`.
	 */
	saveState() {

		this.target0.copy( this.target );
		this.position0.copy( this.object.position );
		this.zoom0 = this.object.zoom;

	}

	/**
	 * Reset the controls to their state from either the last time the `saveState()`
	 * was called, or the initial state.
	 */
	reset() {

		this.target.copy( this.target0 );
		this.object.position.copy( this.position0 );
		this.object.zoom = this.zoom0;

		this.object.updateProjectionMatrix();
		this.dispatchEvent( _changeEvent );

		this.update();

		this.state = _STATE.NONE;

	}

	update( deltaTime = null ) {

		const position = this.object.position;

		_v.copy( position ).sub( this.target );

		// rotate offset to "y-axis-is-up" space
		_v.applyQuaternion( this._quat );

		// angle from z-axis around y-axis
		this._spherical.setFromVector3( _v );

		if ( this.autoRotate && this.state === _STATE.NONE ) {

			this._rotateLeft( this._getAutoRotationAngle( deltaTime ) );

		}

		if ( this.enableDamping ) {

			this._spherical.theta += this._sphericalDelta.theta * this.dampingFactor;
			this._spherical.phi += this._sphericalDelta.phi * this.dampingFactor;

		} else {

			this._spherical.theta += this._sphericalDelta.theta;
			this._spherical.phi += this._sphericalDelta.phi;

		}

		// restrict theta to be between desired limits

		let min = this.minAzimuthAngle;
		let max = this.maxAzimuthAngle;

		if ( isFinite( min ) && isFinite( max ) ) {

			if ( min < - Math.PI ) min += _twoPI; else if ( min > Math.PI ) min -= _twoPI;

			if ( max < - Math.PI ) max += _twoPI; else if ( max > Math.PI ) max -= _twoPI;

			if ( min <= max ) {

				this._spherical.theta = Math.max( min, Math.min( max, this._spherical.theta ) );

			} else {

				this._spherical.theta = ( this._spherical.theta > ( min + max ) / 2 ) ?
					Math.max( min, this._spherical.theta ) :
					Math.min( max, this._spherical.theta );

			}

		}

		// restrict phi to be between desired limits
		this._spherical.phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, this._spherical.phi ) );

		this._spherical.makeSafe();


		// move target to panned location

		if ( this.enableDamping === true ) {

			this.target.addScaledVector( this._panOffset, this.dampingFactor );

		} else {

			this.target.add( this._panOffset );

		}

		// Limit the target distance from the cursor to create a sphere around the center of interest
		this.target.sub( this.cursor );
		this.target.clampLength( this.minTargetRadius, this.maxTargetRadius );
		this.target.add( this.cursor );

		let zoomChanged = false;
		// adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera
		// we adjust zoom later in these cases
		if ( this.zoomToCursor && this._performCursorZoom || this.object.isOrthographicCamera ) {

			this._spherical.radius = this._clampDistance( this._spherical.radius );

		} else {

			const prevRadius = this._spherical.radius;
			this._spherical.radius = this._clampDistance( this._spherical.radius * this._scale );
			zoomChanged = prevRadius != this._spherical.radius;

		}

		_v.setFromSpherical( this._spherical );

		// rotate offset back to "camera-up-vector-is-up" space
		_v.applyQuaternion( this._quatInverse );

		position.copy( this.target ).add( _v );

		this.object.lookAt( this.target );

		if ( this.enableDamping === true ) {

			this._sphericalDelta.theta *= ( 1 - this.dampingFactor );
			this._sphericalDelta.phi *= ( 1 - this.dampingFactor );

			this._panOffset.multiplyScalar( 1 - this.dampingFactor );

		} else {

			this._sphericalDelta.set( 0, 0, 0 );

			this._panOffset.set( 0, 0, 0 );

		}

		// adjust camera position
		if ( this.zoomToCursor && this._performCursorZoom ) {

			let newRadius = null;
			if ( this.object.isPerspectiveCamera ) {

				// move the camera down the pointer ray
				// this method avoids floating point error
				const prevRadius = _v.length();
				newRadius = this._clampDistance( prevRadius * this._scale );

				const radiusDelta = prevRadius - newRadius;
				this.object.position.addScaledVector( this._dollyDirection, radiusDelta );
				this.object.updateMatrixWorld();

				zoomChanged = !! radiusDelta;

			} else if ( this.object.isOrthographicCamera ) {

				// adjust the ortho camera position based on zoom changes
				const mouseBefore = new Vector3( this._mouse.x, this._mouse.y, 0 );
				mouseBefore.unproject( this.object );

				const prevZoom = this.object.zoom;
				this.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / this._scale ) );
				this.object.updateProjectionMatrix();

				zoomChanged = prevZoom !== this.object.zoom;

				const mouseAfter = new Vector3( this._mouse.x, this._mouse.y, 0 );
				mouseAfter.unproject( this.object );

				this.object.position.sub( mouseAfter ).add( mouseBefore );
				this.object.updateMatrixWorld();

				newRadius = _v.length();

			} else {

				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' );
				this.zoomToCursor = false;

			}

			// handle the placement of the target
			if ( newRadius !== null ) {

				if ( this.screenSpacePanning ) {

					// position the orbit target in front of the new camera position
					this.target.set( 0, 0, -1 )
						.transformDirection( this.object.matrix )
						.multiplyScalar( newRadius )
						.add( this.object.position );

				} else {

					// get the ray and translation plane to compute target
					_ray.origin.copy( this.object.position );
					_ray.direction.set( 0, 0, -1 ).transformDirection( this.object.matrix );

					// if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid
					// extremely large values
					if ( Math.abs( this.object.up.dot( _ray.direction ) ) < _TILT_LIMIT ) {

						this.object.lookAt( this.target );

					} else {

						_plane.setFromNormalAndCoplanarPoint( this.object.up, this.target );
						_ray.intersectPlane( _plane, this.target );

					}

				}

			}

		} else if ( this.object.isOrthographicCamera ) {

			const prevZoom = this.object.zoom;
			this.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / this._scale ) );

			if ( prevZoom !== this.object.zoom ) {

				this.object.updateProjectionMatrix();
				zoomChanged = true;

			}

		}

		this._scale = 1;
		this._performCursorZoom = false;

		// update condition is:
		// min(camera displacement, camera rotation in radians)^2 > EPS
		// using small-angle approximation cos(x/2) = 1 - x^2 / 8

		if ( zoomChanged ||
			this._lastPosition.distanceToSquared( this.object.position ) > _EPS ||
			8 * ( 1 - this._lastQuaternion.dot( this.object.quaternion ) ) > _EPS ||
			this._lastTargetPosition.distanceToSquared( this.target ) > _EPS ) {

			this.dispatchEvent( _changeEvent );

			this._lastPosition.copy( this.object.position );
			this._lastQuaternion.copy( this.object.quaternion );
			this._lastTargetPosition.copy( this.target );

			return true;

		}

		return false;

	}

	_getAutoRotationAngle( deltaTime ) {

		if ( deltaTime !== null ) {

			return ( _twoPI / 60 * this.autoRotateSpeed ) * deltaTime;

		} else {

			return _twoPI / 60 / 60 * this.autoRotateSpeed;

		}

	}

	_getZoomScale( delta ) {

		const normalizedDelta = Math.abs( delta * 0.01 );
		return Math.pow( 0.95, this.zoomSpeed * normalizedDelta );

	}

	_rotateLeft( angle ) {

		this._sphericalDelta.theta -= angle;

	}

	_rotateUp( angle ) {

		this._sphericalDelta.phi -= angle;

	}

	_panLeft( distance, objectMatrix ) {

		_v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
		_v.multiplyScalar( - distance );

		this._panOffset.add( _v );

	}

	_panUp( distance, objectMatrix ) {

		if ( this.screenSpacePanning === true ) {

			_v.setFromMatrixColumn( objectMatrix, 1 );

		} else {

			_v.setFromMatrixColumn( objectMatrix, 0 );
			_v.crossVectors( this.object.up, _v );

		}

		_v.multiplyScalar( distance );

		this._panOffset.add( _v );

	}

	// deltaX and deltaY are in pixels; right and down are positive
	_pan( deltaX, deltaY ) {

		const element = this.domElement;

		if ( this.object.isPerspectiveCamera ) {

			// perspective
			const position = this.object.position;
			_v.copy( position ).sub( this.target );
			let targetDistance = _v.length();

			// half of the fov is center to top of screen
			targetDistance *= Math.tan( ( this.object.fov / 2 ) * Math.PI / 180.0 );

			// we use only clientHeight here so aspect ratio does not distort speed
			this._panLeft( 2 * deltaX * targetDistance / element.clientHeight, this.object.matrix );
			this._panUp( 2 * deltaY * targetDistance / element.clientHeight, this.object.matrix );

		} else if ( this.object.isOrthographicCamera ) {

			// orthographic
			this._panLeft( deltaX * ( this.object.right - this.object.left ) / this.object.zoom / element.clientWidth, this.object.matrix );
			this._panUp( deltaY * ( this.object.top - this.object.bottom ) / this.object.zoom / element.clientHeight, this.object.matrix );

		} else {

			// camera neither orthographic nor perspective
			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
			this.enablePan = false;

		}

	}

	_dollyOut( dollyScale ) {

		if ( this.object.isPerspectiveCamera || this.object.isOrthographicCamera ) {

			this._scale /= dollyScale;

		} else {

			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
			this.enableZoom = false;

		}

	}

	_dollyIn( dollyScale ) {

		if ( this.object.isPerspectiveCamera || this.object.isOrthographicCamera ) {

			this._scale *= dollyScale;

		} else {

			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
			this.enableZoom = false;

		}

	}

	_updateZoomParameters( x, y ) {

		if ( ! this.zoomToCursor ) {

			return;

		}

		this._performCursorZoom = true;

		const rect = this.domElement.getBoundingClientRect();
		const dx = x - rect.left;
		const dy = y - rect.top;
		const w = rect.width;
		const h = rect.height;

		this._mouse.x = ( dx / w ) * 2 - 1;
		this._mouse.y = - ( dy / h ) * 2 + 1;

		this._dollyDirection.set( this._mouse.x, this._mouse.y, 1 ).unproject( this.object ).sub( this.object.position ).normalize();

	}

	_clampDistance( dist ) {

		return Math.max( this.minDistance, Math.min( this.maxDistance, dist ) );

	}

	//
	// event callbacks - update the object state
	//

	_handleMouseDownRotate( event ) {

		this._rotateStart.set( event.clientX, event.clientY );

	}

	_handleMouseDownDolly( event ) {

		this._updateZoomParameters( event.clientX, event.clientX );
		this._dollyStart.set( event.clientX, event.clientY );

	}

	_handleMouseDownPan( event ) {

		this._panStart.set( event.clientX, event.clientY );

	}

	_handleMouseMoveRotate( event ) {

		this._rotateEnd.set( event.clientX, event.clientY );

		this._rotateDelta.subVectors( this._rotateEnd, this._rotateStart ).multiplyScalar( this.rotateSpeed );

		const element = this.domElement;

		this._rotateLeft( _twoPI * this._rotateDelta.x / element.clientHeight ); // yes, height

		this._rotateUp( _twoPI * this._rotateDelta.y / element.clientHeight );

		this._rotateStart.copy( this._rotateEnd );

		this.update();

	}

	_handleMouseMoveDolly( event ) {

		this._dollyEnd.set( event.clientX, event.clientY );

		this._dollyDelta.subVectors( this._dollyEnd, this._dollyStart );

		if ( this._dollyDelta.y > 0 ) {

			this._dollyOut( this._getZoomScale( this._dollyDelta.y ) );

		} else if ( this._dollyDelta.y < 0 ) {

			this._dollyIn( this._getZoomScale( this._dollyDelta.y ) );

		}

		this._dollyStart.copy( this._dollyEnd );

		this.update();

	}

	_handleMouseMovePan( event ) {

		this._panEnd.set( event.clientX, event.clientY );

		this._panDelta.subVectors( this._panEnd, this._panStart ).multiplyScalar( this.panSpeed );

		this._pan( this._panDelta.x, this._panDelta.y );

		this._panStart.copy( this._panEnd );

		this.update();

	}

	_handleMouseWheel( event ) {

		this._updateZoomParameters( event.clientX, event.clientY );

		if ( event.deltaY < 0 ) {

			this._dollyIn( this._getZoomScale( event.deltaY ) );

		} else if ( event.deltaY > 0 ) {

			this._dollyOut( this._getZoomScale( event.deltaY ) );

		}

		this.update();

	}

	_handleKeyDown( event ) {

		let needsUpdate = false;

		switch ( event.code ) {

			case this.keys.UP:

				if ( event.ctrlKey || event.metaKey || event.shiftKey ) {

					if ( this.enableRotate ) {

						this._rotateUp( _twoPI * this.keyRotateSpeed / this.domElement.clientHeight );

					}

				} else {

					if ( this.enablePan ) {

						this._pan( 0, this.keyPanSpeed );

					}

				}

				needsUpdate = true;
				break;

			case this.keys.BOTTOM:

				if ( event.ctrlKey || event.metaKey || event.shiftKey ) {

					if ( this.enableRotate ) {

						this._rotateUp( - _twoPI * this.keyRotateSpeed / this.domElement.clientHeight );

					}

				} else {

					if ( this.enablePan ) {

						this._pan( 0, - this.keyPanSpeed );

					}

				}

				needsUpdate = true;
				break;

			case this.keys.LEFT:

				if ( event.ctrlKey || event.metaKey || event.shiftKey ) {

					if ( this.enableRotate ) {

						this._rotateLeft( _twoPI * this.keyRotateSpeed / this.domElement.clientHeight );

					}

				} else {

					if ( this.enablePan ) {

						this._pan( this.keyPanSpeed, 0 );

					}

				}

				needsUpdate = true;
				break;

			case this.keys.RIGHT:

				if ( event.ctrlKey || event.metaKey || event.shiftKey ) {

					if ( this.enableRotate ) {

						this._rotateLeft( - _twoPI * this.keyRotateSpeed / this.domElement.clientHeight );

					}

				} else {

					if ( this.enablePan ) {

						this._pan( - this.keyPanSpeed, 0 );

					}

				}

				needsUpdate = true;
				break;

		}

		if ( needsUpdate ) {

			// prevent the browser from scrolling on cursor keys
			event.preventDefault();

			this.update();

		}


	}

	_handleTouchStartRotate( event ) {

		if ( this._pointers.length === 1 ) {

			this._rotateStart.set( event.pageX, event.pageY );

		} else {

			const position = this._getSecondPointerPosition( event );

			const x = 0.5 * ( event.pageX + position.x );
			const y = 0.5 * ( event.pageY + position.y );

			this._rotateStart.set( x, y );

		}

	}

	_handleTouchStartPan( event ) {

		if ( this._pointers.length === 1 ) {

			this._panStart.set( event.pageX, event.pageY );

		} else {

			const position = this._getSecondPointerPosition( event );

			const x = 0.5 * ( event.pageX + position.x );
			const y = 0.5 * ( event.pageY + position.y );

			this._panStart.set( x, y );

		}

	}

	_handleTouchStartDolly( event ) {

		const position = this._getSecondPointerPosition( event );

		const dx = event.pageX - position.x;
		const dy = event.pageY - position.y;

		const distance = Math.sqrt( dx * dx + dy * dy );

		this._dollyStart.set( 0, distance );

	}

	_handleTouchStartDollyPan( event ) {

		if ( this.enableZoom ) this._handleTouchStartDolly( event );

		if ( this.enablePan ) this._handleTouchStartPan( event );

	}

	_handleTouchStartDollyRotate( event ) {

		if ( this.enableZoom ) this._handleTouchStartDolly( event );

		if ( this.enableRotate ) this._handleTouchStartRotate( event );

	}

	_handleTouchMoveRotate( event ) {

		if ( this._pointers.length == 1 ) {

			this._rotateEnd.set( event.pageX, event.pageY );

		} else {

			const position = this._getSecondPointerPosition( event );

			const x = 0.5 * ( event.pageX + position.x );
			const y = 0.5 * ( event.pageY + position.y );

			this._rotateEnd.set( x, y );

		}

		this._rotateDelta.subVectors( this._rotateEnd, this._rotateStart ).multiplyScalar( this.rotateSpeed );

		const element = this.domElement;

		this._rotateLeft( _twoPI * this._rotateDelta.x / element.clientHeight ); // yes, height

		this._rotateUp( _twoPI * this._rotateDelta.y / element.clientHeight );

		this._rotateStart.copy( this._rotateEnd );

	}

	_handleTouchMovePan( event ) {

		if ( this._pointers.length === 1 ) {

			this._panEnd.set( event.pageX, event.pageY );

		} else {

			const position = this._getSecondPointerPosition( event );

			const x = 0.5 * ( event.pageX + position.x );
			const y = 0.5 * ( event.pageY + position.y );

			this._panEnd.set( x, y );

		}

		this._panDelta.subVectors( this._panEnd, this._panStart ).multiplyScalar( this.panSpeed );

		this._pan( this._panDelta.x, this._panDelta.y );

		this._panStart.copy( this._panEnd );

	}

	_handleTouchMoveDolly( event ) {

		const position = this._getSecondPointerPosition( event );

		const dx = event.pageX - position.x;
		const dy = event.pageY - position.y;

		const distance = Math.sqrt( dx * dx + dy * dy );

		this._dollyEnd.set( 0, distance );

		this._dollyDelta.set( 0, Math.pow( this._dollyEnd.y / this._dollyStart.y, this.zoomSpeed ) );

		this._dollyOut( this._dollyDelta.y );

		this._dollyStart.copy( this._dollyEnd );

		const centerX = ( event.pageX + position.x ) * 0.5;
		const centerY = ( event.pageY + position.y ) * 0.5;

		this._updateZoomParameters( centerX, centerY );

	}

	_handleTouchMoveDollyPan( event ) {

		if ( this.enableZoom ) this._handleTouchMoveDolly( event );

		if ( this.enablePan ) this._handleTouchMovePan( event );

	}

	_handleTouchMoveDollyRotate( event ) {

		if ( this.enableZoom ) this._handleTouchMoveDolly( event );

		if ( this.enableRotate ) this._handleTouchMoveRotate( event );

	}

	// pointers

	_addPointer( event ) {

		this._pointers.push( event.pointerId );

	}

	_removePointer( event ) {

		delete this._pointerPositions[ event.pointerId ];

		for ( let i = 0; i < this._pointers.length; i ++ ) {

			if ( this._pointers[ i ] == event.pointerId ) {

				this._pointers.splice( i, 1 );
				return;

			}

		}

	}

	_isTrackingPointer( event ) {

		for ( let i = 0; i < this._pointers.length; i ++ ) {

			if ( this._pointers[ i ] == event.pointerId ) return true;

		}

		return false;

	}

	_trackPointer( event ) {

		let position = this._pointerPositions[ event.pointerId ];

		if ( position === undefined ) {

			position = new Vector2();
			this._pointerPositions[ event.pointerId ] = position;

		}

		position.set( event.pageX, event.pageY );

	}

	_getSecondPointerPosition( event ) {

		const pointerId = ( event.pointerId === this._pointers[ 0 ] ) ? this._pointers[ 1 ] : this._pointers[ 0 ];

		return this._pointerPositions[ pointerId ];

	}

	//

	_customWheelEvent( event ) {

		const mode = event.deltaMode;

		// minimal wheel event altered to meet delta-zoom demand
		const newEvent = {
			clientX: event.clientX,
			clientY: event.clientY,
			deltaY: event.deltaY,
		};

		switch ( mode ) {

			case 1: // LINE_MODE
				newEvent.deltaY *= 16;
				break;

			case 2: // PAGE_MODE
				newEvent.deltaY *= 100;
				break;

		}

		// detect if event was triggered by pinching
		if ( event.ctrlKey && ! this._controlActive ) {

			newEvent.deltaY *= 10;

		}

		return newEvent;

	}

}

function onPointerDown( event ) {

	if ( this.enabled === false ) return;

	if ( this._pointers.length === 0 ) {

		this.domElement.setPointerCapture( event.pointerId );

		this.domElement.addEventListener( 'pointermove', this._onPointerMove );
		this.domElement.addEventListener( 'pointerup', this._onPointerUp );

	}

	//

	if ( this._isTrackingPointer( event ) ) return;

	//

	this._addPointer( event );

	if ( event.pointerType === 'touch' ) {

		this._onTouchStart( event );

	} else {

		this._onMouseDown( event );

	}

}

function onPointerMove( event ) {

	if ( this.enabled === false ) return;

	if ( event.pointerType === 'touch' ) {

		this._onTouchMove( event );

	} else {

		this._onMouseMove( event );

	}

}

function onPointerUp( event ) {

	this._removePointer( event );

	switch ( this._pointers.length ) {

		case 0:

			this.domElement.releasePointerCapture( event.pointerId );

			this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
			this.domElement.removeEventListener( 'pointerup', this._onPointerUp );

			this.dispatchEvent( _endEvent );

			this.state = _STATE.NONE;

			break;

		case 1:

			const pointerId = this._pointers[ 0 ];
			const position = this._pointerPositions[ pointerId ];

			// minimal placeholder event - allows state correction on pointer-up
			this._onTouchStart( { pointerId: pointerId, pageX: position.x, pageY: position.y } );

			break;

	}

}

function onMouseDown( event ) {

	let mouseAction;

	switch ( event.button ) {

		case 0:

			mouseAction = this.mouseButtons.LEFT;
			break;

		case 1:

			mouseAction = this.mouseButtons.MIDDLE;
			break;

		case 2:

			mouseAction = this.mouseButtons.RIGHT;
			break;

		default:

			mouseAction = -1;

	}

	switch ( mouseAction ) {

		case MOUSE.DOLLY:

			if ( this.enableZoom === false ) return;

			this._handleMouseDownDolly( event );

			this.state = _STATE.DOLLY;

			break;

		case MOUSE.ROTATE:

			if ( event.ctrlKey || event.metaKey || event.shiftKey ) {

				if ( this.enablePan === false ) return;

				this._handleMouseDownPan( event );

				this.state = _STATE.PAN;

			} else {

				if ( this.enableRotate === false ) return;

				this._handleMouseDownRotate( event );

				this.state = _STATE.ROTATE;

			}

			break;

		case MOUSE.PAN:

			if ( event.ctrlKey || event.metaKey || event.shiftKey ) {

				if ( this.enableRotate === false ) return;

				this._handleMouseDownRotate( event );

				this.state = _STATE.ROTATE;

			} else {

				if ( this.enablePan === false ) return;

				this._handleMouseDownPan( event );

				this.state = _STATE.PAN;

			}

			break;

		default:

			this.state = _STATE.NONE;

	}

	if ( this.state !== _STATE.NONE ) {

		this.dispatchEvent( _startEvent );

	}

}

function onMouseMove( event ) {

	switch ( this.state ) {

		case _STATE.ROTATE:

			if ( this.enableRotate === false ) return;

			this._handleMouseMoveRotate( event );

			break;

		case _STATE.DOLLY:

			if ( this.enableZoom === false ) return;

			this._handleMouseMoveDolly( event );

			break;

		case _STATE.PAN:

			if ( this.enablePan === false ) return;

			this._handleMouseMovePan( event );

			break;

	}

}

function onMouseWheel( event ) {

	if ( this.enabled === false || this.enableZoom === false || this.state !== _STATE.NONE ) return;

	event.preventDefault();

	this.dispatchEvent( _startEvent );

	this._handleMouseWheel( this._customWheelEvent( event ) );

	this.dispatchEvent( _endEvent );

}

function onKeyDown( event ) {

	if ( this.enabled === false ) return;

	this._handleKeyDown( event );

}

function onTouchStart( event ) {

	this._trackPointer( event );

	switch ( this._pointers.length ) {

		case 1:

			switch ( this.touches.ONE ) {

				case TOUCH.ROTATE:

					if ( this.enableRotate === false ) return;

					this._handleTouchStartRotate( event );

					this.state = _STATE.TOUCH_ROTATE;

					break;

				case TOUCH.PAN:

					if ( this.enablePan === false ) return;

					this._handleTouchStartPan( event );

					this.state = _STATE.TOUCH_PAN;

					break;

				default:

					this.state = _STATE.NONE;

			}

			break;

		case 2:

			switch ( this.touches.TWO ) {

				case TOUCH.DOLLY_PAN:

					if ( this.enableZoom === false && this.enablePan === false ) return;

					this._handleTouchStartDollyPan( event );

					this.state = _STATE.TOUCH_DOLLY_PAN;

					break;

				case TOUCH.DOLLY_ROTATE:

					if ( this.enableZoom === false && this.enableRotate === false ) return;

					this._handleTouchStartDollyRotate( event );

					this.state = _STATE.TOUCH_DOLLY_ROTATE;

					break;

				default:

					this.state = _STATE.NONE;

			}

			break;

		default:

			this.state = _STATE.NONE;

	}

	if ( this.state !== _STATE.NONE ) {

		this.dispatchEvent( _startEvent );

	}

}

function onTouchMove( event ) {

	this._trackPointer( event );

	switch ( this.state ) {

		case _STATE.TOUCH_ROTATE:

			if ( this.enableRotate === false ) return;

			this._handleTouchMoveRotate( event );

			this.update();

			break;

		case _STATE.TOUCH_PAN:

			if ( this.enablePan === false ) return;

			this._handleTouchMovePan( event );

			this.update();

			break;

		case _STATE.TOUCH_DOLLY_PAN:

			if ( this.enableZoom === false && this.enablePan === false ) return;

			this._handleTouchMoveDollyPan( event );

			this.update();

			break;

		case _STATE.TOUCH_DOLLY_ROTATE:

			if ( this.enableZoom === false && this.enableRotate === false ) return;

			this._handleTouchMoveDollyRotate( event );

			this.update();

			break;

		default:

			this.state = _STATE.NONE;

	}

}

function onContextMenu( event ) {

	if ( this.enabled === false ) return;

	event.preventDefault();

}

function interceptControlDown( event ) {

	if ( event.key === 'Control' ) {

		this._controlActive = true;

		const document = this.domElement.getRootNode(); // offscreen canvas compatibility

		document.addEventListener( 'keyup', this._interceptControlUp, { passive: true, capture: true } );

	}

}

function interceptControlUp( event ) {

	if ( event.key === 'Control' ) {

		this._controlActive = false;

		const document = this.domElement.getRootNode(); // offscreen canvas compatibility

		document.removeEventListener( 'keyup', this._interceptControlUp, { passive: true, capture: true } );

	}

}

/**
 * @module CopyShader
 * @three_import import { CopyShader } from 'three/addons/shaders/CopyShader.js';
 */

/**
 * Full-screen copy shader pass.
 *
 * @constant
 * @type {ShaderMaterial~Shader}
 */
const CopyShader = {

	name: 'CopyShader',

	uniforms: {

		'tDiffuse': { value: null },
		'opacity': { value: 1.0 }

	},

	vertexShader: /* glsl */`

		varying vec2 vUv;

		void main() {

			vUv = uv;
			gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

		}`,

	fragmentShader: /* glsl */`

		uniform float opacity;

		uniform sampler2D tDiffuse;

		varying vec2 vUv;

		void main() {

			vec4 texel = texture2D( tDiffuse, vUv );
			gl_FragColor = opacity * texel;


		}`

};

/**
 * Abstract base class for all post processing passes.
 *
 * This module is only relevant for post processing with {@link WebGLRenderer}.
 *
 * @abstract
 * @three_import import { Pass } from 'three/addons/postprocessing/Pass.js';
 */
class Pass {

	/**
	 * Constructs a new pass.
	 */
	constructor() {

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isPass = true;

		/**
		 * If set to `true`, the pass is processed by the composer.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.enabled = true;

		/**
		 * If set to `true`, the pass indicates to swap read and write buffer after rendering.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.needsSwap = true;

		/**
		 * If set to `true`, the pass clears its buffer before rendering
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.clear = false;

		/**
		 * If set to `true`, the result of the pass is rendered to screen. The last pass in the composers
		 * pass chain gets automatically rendered to screen, no matter how this property is configured.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.renderToScreen = false;

	}

	/**
	 * Sets the size of the pass.
	 *
	 * @abstract
	 * @param {number} width - The width to set.
	 * @param {number} height - The height to set.
	 */
	setSize( /* width, height */ ) {}

	/**
	 * This method holds the render logic of a pass. It must be implemented in all derived classes.
	 *
	 * @abstract
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
	 * destination for the pass.
	 * @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
	 * previous pass from this buffer.
	 * @param {number} deltaTime - The delta time in seconds.
	 * @param {boolean} maskActive - Whether masking is active or not.
	 */
	render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {

		console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );

	}

	/**
	 * Frees the GPU-related resources allocated by this instance. Call this
	 * method whenever the pass is no longer used in your app.
	 *
	 * @abstract
	 */
	dispose() {}

}

// Helper for passes that need to fill the viewport with a single quad.

const _camera = new OrthographicCamera( -1, 1, 1, -1, 0, 1 );

// https://github.com/mrdoob/three.js/pull/21358

class FullscreenTriangleGeometry extends BufferGeometry {

	constructor() {

		super();

		this.setAttribute( 'position', new Float32BufferAttribute( [ -1, 3, 0, -1, -1, 0, 3, -1, 0 ], 3 ) );
		this.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );

	}

}

const _geometry = new FullscreenTriangleGeometry();


/**
 * This module is a helper for passes which need to render a full
 * screen effect which is quite common in context of post processing.
 *
 * The intended usage is to reuse a single full screen quad for rendering
 * subsequent passes by just reassigning the `material` reference.
 *
 * This module can only be used with {@link WebGLRenderer}.
 *
 * @augments Mesh
 * @three_import import { FullScreenQuad } from 'three/addons/postprocessing/Pass.js';
 */
class FullScreenQuad {

	/**
	 * Constructs a new full screen quad.
	 *
	 * @param {?Material} material - The material to render te full screen quad with.
	 */
	constructor( material ) {

		this._mesh = new Mesh( _geometry, material );

	}

	/**
	 * Frees the GPU-related resources allocated by this instance. Call this
	 * method whenever the instance is no longer used in your app.
	 */
	dispose() {

		this._mesh.geometry.dispose();

	}

	/**
	 * Renders the full screen quad.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 */
	render( renderer ) {

		renderer.render( this._mesh, _camera );

	}

	/**
	 * The quad's material.
	 *
	 * @type {?Material}
	 */
	get material() {

		return this._mesh.material;

	}

	set material( value ) {

		this._mesh.material = value;

	}

}

/**
 * This pass can be used to create a post processing effect
 * with a raw GLSL shader object. Useful for implementing custom
 * effects.
 *
 * ```js
 * const fxaaPass = new ShaderPass( FXAAShader );
 * composer.addPass( fxaaPass );
 * ```
 *
 * @augments Pass
 * @three_import import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
 */
class ShaderPass extends Pass {

	/**
	 * Constructs a new shader pass.
	 *
	 * @param {Object|ShaderMaterial} [shader] - A shader object holding vertex and fragment shader as well as
	 * defines and uniforms. It's also valid to pass a custom shader material.
	 * @param {string} [textureID='tDiffuse'] - The name of the texture uniform that should sample
	 * the read buffer.
	 */
	constructor( shader, textureID = 'tDiffuse' ) {

		super();

		/**
		 * The name of the texture uniform that should sample the read buffer.
		 *
		 * @type {string}
		 * @default 'tDiffuse'
		 */
		this.textureID = textureID;

		/**
		 * The pass uniforms.
		 *
		 * @type {?Object}
		 */
		this.uniforms = null;

		/**
		 * The pass material.
		 *
		 * @type {?ShaderMaterial}
		 */
		this.material = null;

		if ( shader instanceof ShaderMaterial ) {

			this.uniforms = shader.uniforms;

			this.material = shader;

		} else if ( shader ) {

			this.uniforms = UniformsUtils.clone( shader.uniforms );

			this.material = new ShaderMaterial( {

				name: ( shader.name !== undefined ) ? shader.name : 'unspecified',
				defines: Object.assign( {}, shader.defines ),
				uniforms: this.uniforms,
				vertexShader: shader.vertexShader,
				fragmentShader: shader.fragmentShader

			} );

		}

		// internals

		this._fsQuad = new FullScreenQuad( this.material );

	}

	/**
	 * Performs the shader pass.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
	 * destination for the pass.
	 * @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
	 * previous pass from this buffer.
	 * @param {number} deltaTime - The delta time in seconds.
	 * @param {boolean} maskActive - Whether masking is active or not.
	 */
	render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {

		if ( this.uniforms[ this.textureID ] ) {

			this.uniforms[ this.textureID ].value = readBuffer.texture;

		}

		this._fsQuad.material = this.material;

		if ( this.renderToScreen ) {

			renderer.setRenderTarget( null );
			this._fsQuad.render( renderer );

		} else {

			renderer.setRenderTarget( writeBuffer );
			// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
			if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
			this._fsQuad.render( renderer );

		}

	}

	/**
	 * Frees the GPU-related resources allocated by this instance. Call this
	 * method whenever the pass is no longer used in your app.
	 */
	dispose() {

		this.material.dispose();

		this._fsQuad.dispose();

	}

}

/**
 * This pass can be used to define a mask during post processing.
 * Meaning only areas of subsequent post processing are affected
 * which lie in the masking area of this pass. Internally, the masking
 * is implemented with the stencil buffer.
 *
 * ```js
 * const maskPass = new MaskPass( scene, camera );
 * composer.addPass( maskPass );
 * ```
 *
 * @augments Pass
 * @three_import import { MaskPass } from 'three/addons/postprocessing/MaskPass.js';
 */
class MaskPass extends Pass {

	/**
	 * Constructs a new mask pass.
	 *
	 * @param {Scene} scene - The 3D objects in this scene will define the mask.
	 * @param {Camera} camera - The camera.
	 */
	constructor( scene, camera ) {

		super();

		/**
		 * The scene that defines the mask.
		 *
		 * @type {Scene}
		 */
		this.scene = scene;

		/**
		 * The camera.
		 *
		 * @type {Camera}
		 */
		this.camera = camera;

		/**
		 * Overwritten to perform a clear operation by default.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.clear = true;

		/**
		 * Overwritten to disable the swap.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.needsSwap = false;

		/**
		 * Whether to inverse the mask or not.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.inverse = false;

	}

	/**
	 * Performs a mask pass with the configured scene and camera.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
	 * destination for the pass.
	 * @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
	 * previous pass from this buffer.
	 * @param {number} deltaTime - The delta time in seconds.
	 * @param {boolean} maskActive - Whether masking is active or not.
	 */
	render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {

		const context = renderer.getContext();
		const state = renderer.state;

		// don't update color or depth

		state.buffers.color.setMask( false );
		state.buffers.depth.setMask( false );

		// lock buffers

		state.buffers.color.setLocked( true );
		state.buffers.depth.setLocked( true );

		// set up stencil

		let writeValue, clearValue;

		if ( this.inverse ) {

			writeValue = 0;
			clearValue = 1;

		} else {

			writeValue = 1;
			clearValue = 0;

		}

		state.buffers.stencil.setTest( true );
		state.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE );
		state.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff );
		state.buffers.stencil.setClear( clearValue );
		state.buffers.stencil.setLocked( true );

		// draw into the stencil buffer

		renderer.setRenderTarget( readBuffer );
		if ( this.clear ) renderer.clear();
		renderer.render( this.scene, this.camera );

		renderer.setRenderTarget( writeBuffer );
		if ( this.clear ) renderer.clear();
		renderer.render( this.scene, this.camera );

		// unlock color and depth buffer and make them writable for subsequent rendering/clearing

		state.buffers.color.setLocked( false );
		state.buffers.depth.setLocked( false );

		state.buffers.color.setMask( true );
		state.buffers.depth.setMask( true );

		// only render where stencil is set to 1

		state.buffers.stencil.setLocked( false );
		state.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1
		state.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP );
		state.buffers.stencil.setLocked( true );

	}

}

/**
 * This pass can be used to clear a mask previously defined with {@link MaskPass}.
 *
 * ```js
 * const clearPass = new ClearMaskPass();
 * composer.addPass( clearPass );
 * ```
 *
 * @augments Pass
 */
class ClearMaskPass extends Pass {

	/**
	 * Constructs a new clear mask pass.
	 */
	constructor() {

		super();

		/**
		 * Overwritten to disable the swap.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.needsSwap = false;

	}

	/**
	 * Performs the clear of the currently defined mask.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
	 * destination for the pass.
	 * @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
	 * previous pass from this buffer.
	 * @param {number} deltaTime - The delta time in seconds.
	 * @param {boolean} maskActive - Whether masking is active or not.
	 */
	render( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) {

		renderer.state.buffers.stencil.setLocked( false );
		renderer.state.buffers.stencil.setTest( false );

	}

}

/**
 * Used to implement post-processing effects in three.js.
 * The class manages a chain of post-processing passes to produce the final visual result.
 * Post-processing passes are executed in order of their addition/insertion.
 * The last pass is automatically rendered to screen.
 *
 * This module can only be used with {@link WebGLRenderer}.
 *
 * ```js
 * const composer = new EffectComposer( renderer );
 *
 * // adding some passes
 * const renderPass = new RenderPass( scene, camera );
 * composer.addPass( renderPass );
 *
 * const glitchPass = new GlitchPass();
 * composer.addPass( glitchPass );
 *
 * const outputPass = new OutputPass()
 * composer.addPass( outputPass );
 *
 * function animate() {
 *
 * 	composer.render(); // instead of renderer.render()
 *
 * }
 * ```
 *
 * @three_import import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
 */
class EffectComposer {

	/**
	 * Constructs a new effect composer.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {WebGLRenderTarget} [renderTarget] - This render target and a clone will
	 * be used as the internal read and write buffers. If not given, the composer creates
	 * the buffers automatically.
	 */
	constructor( renderer, renderTarget ) {

		/**
		 * The renderer.
		 *
		 * @type {WebGLRenderer}
		 */
		this.renderer = renderer;

		this._pixelRatio = renderer.getPixelRatio();

		if ( renderTarget === undefined ) {

			const size = renderer.getSize( new Vector2() );
			this._width = size.width;
			this._height = size.height;

			renderTarget = new WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType } );
			renderTarget.texture.name = 'EffectComposer.rt1';

		} else {

			this._width = renderTarget.width;
			this._height = renderTarget.height;

		}

		this.renderTarget1 = renderTarget;
		this.renderTarget2 = renderTarget.clone();
		this.renderTarget2.texture.name = 'EffectComposer.rt2';

		/**
		 * A reference to the internal write buffer. Passes usually write
		 * their result into this buffer.
		 *
		 * @type {WebGLRenderTarget}
		 */
		this.writeBuffer = this.renderTarget1;

		/**
		 * A reference to the internal read buffer. Passes usually read
		 * the previous render result from this buffer.
		 *
		 * @type {WebGLRenderTarget}
		 */
		this.readBuffer = this.renderTarget2;

		/**
		 * Whether the final pass is rendered to the screen (default framebuffer) or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.renderToScreen = true;

		/**
		 * An array representing the (ordered) chain of post-processing passes.
		 *
		 * @type {Array<Pass>}
		 */
		this.passes = [];

		/**
		 * A copy pass used for internal swap operations.
		 *
		 * @private
		 * @type {ShaderPass}
		 */
		this.copyPass = new ShaderPass( CopyShader );
		this.copyPass.material.blending = NoBlending;

		/**
		 * The internal clock for managing time data.
		 *
		 * @private
		 * @type {Clock}
		 */
		this.clock = new Clock();

	}

	/**
	 * Swaps the internal read/write buffers.
	 */
	swapBuffers() {

		const tmp = this.readBuffer;
		this.readBuffer = this.writeBuffer;
		this.writeBuffer = tmp;

	}

	/**
	 * Adds the given pass to the pass chain.
	 *
	 * @param {Pass} pass - The pass to add.
	 */
	addPass( pass ) {

		this.passes.push( pass );
		pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );

	}

	/**
	 * Inserts the given pass at a given index.
	 *
	 * @param {Pass} pass - The pass to insert.
	 * @param {number} index - The index into the pass chain.
	 */
	insertPass( pass, index ) {

		this.passes.splice( index, 0, pass );
		pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );

	}

	/**
	 * Removes the given pass from the pass chain.
	 *
	 * @param {Pass} pass - The pass to remove.
	 */
	removePass( pass ) {

		const index = this.passes.indexOf( pass );

		if ( index !== -1 ) {

			this.passes.splice( index, 1 );

		}

	}

	/**
	 * Returns `true` if the pass for the given index is the last enabled pass in the pass chain.
	 *
	 * @param {number} passIndex - The pass index.
	 * @return {boolean} Whether the pass for the given index is the last pass in the pass chain.
	 */
	isLastEnabledPass( passIndex ) {

		for ( let i = passIndex + 1; i < this.passes.length; i ++ ) {

			if ( this.passes[ i ].enabled ) {

				return false;

			}

		}

		return true;

	}

	/**
	 * Executes all enabled post-processing passes in order to produce the final frame.
	 *
	 * @param {number} deltaTime - The delta time in seconds. If not given, the composer computes
	 * its own time delta value.
	 */
	render( deltaTime ) {

		// deltaTime value is in seconds

		if ( deltaTime === undefined ) {

			deltaTime = this.clock.getDelta();

		}

		const currentRenderTarget = this.renderer.getRenderTarget();

		let maskActive = false;

		for ( let i = 0, il = this.passes.length; i < il; i ++ ) {

			const pass = this.passes[ i ];

			if ( pass.enabled === false ) continue;

			pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) );
			pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );

			if ( pass.needsSwap ) {

				if ( maskActive ) {

					const context = this.renderer.getContext();
					const stencil = this.renderer.state.buffers.stencil;

					//context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff );
					stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff );

					this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime );

					//context.stencilFunc( context.EQUAL, 1, 0xffffffff );
					stencil.setFunc( context.EQUAL, 1, 0xffffffff );

				}

				this.swapBuffers();

			}

			if ( MaskPass !== undefined ) {

				if ( pass instanceof MaskPass ) {

					maskActive = true;

				} else if ( pass instanceof ClearMaskPass ) {

					maskActive = false;

				}

			}

		}

		this.renderer.setRenderTarget( currentRenderTarget );

	}

	/**
	 * Resets the internal state of the EffectComposer.
	 *
	 * @param {WebGLRenderTarget} [renderTarget] - This render target has the same purpose like
	 * the one from the constructor. If set, it is used to setup the read and write buffers.
	 */
	reset( renderTarget ) {

		if ( renderTarget === undefined ) {

			const size = this.renderer.getSize( new Vector2() );
			this._pixelRatio = this.renderer.getPixelRatio();
			this._width = size.width;
			this._height = size.height;

			renderTarget = this.renderTarget1.clone();
			renderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );

		}

		this.renderTarget1.dispose();
		this.renderTarget2.dispose();
		this.renderTarget1 = renderTarget;
		this.renderTarget2 = renderTarget.clone();

		this.writeBuffer = this.renderTarget1;
		this.readBuffer = this.renderTarget2;

	}

	/**
	 * Resizes the internal read and write buffers as well as all passes. Similar to {@link WebGLRenderer#setSize},
	 * this method honors the current pixel ration.
	 *
	 * @param {number} width - The width in logical pixels.
	 * @param {number} height - The height in logical pixels.
	 */
	setSize( width, height ) {

		this._width = width;
		this._height = height;

		const effectiveWidth = this._width * this._pixelRatio;
		const effectiveHeight = this._height * this._pixelRatio;

		this.renderTarget1.setSize( effectiveWidth, effectiveHeight );
		this.renderTarget2.setSize( effectiveWidth, effectiveHeight );

		for ( let i = 0; i < this.passes.length; i ++ ) {

			this.passes[ i ].setSize( effectiveWidth, effectiveHeight );

		}

	}

	/**
	 * Sets device pixel ratio. This is usually used for HiDPI device to prevent blurring output.
	 * Setting the pixel ratio will automatically resize the composer.
	 *
	 * @param {number} pixelRatio - The pixel ratio to set.
	 */
	setPixelRatio( pixelRatio ) {

		this._pixelRatio = pixelRatio;

		this.setSize( this._width, this._height );

	}

	/**
	 * Frees the GPU-related resources allocated by this instance. Call this
	 * method whenever the composer is no longer used in your app.
	 */
	dispose() {

		this.renderTarget1.dispose();
		this.renderTarget2.dispose();

		this.copyPass.dispose();

	}

}

/**
 * This class represents a render pass. It takes a camera and a scene and produces
 * a beauty pass for subsequent post processing effects.
 *
 * ```js
 * const renderPass = new RenderPass( scene, camera );
 * composer.addPass( renderPass );
 * ```
 *
 * @augments Pass
 * @three_import import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
 */
class RenderPass extends Pass {

	/**
	 * Constructs a new render pass.
	 *
	 * @param {Scene} scene - The scene to render.
	 * @param {Camera} camera - The camera.
	 * @param {?Material} [overrideMaterial=null] - The override material. If set, this material is used
	 * for all objects in the scene.
	 * @param {?(number|Color|string)} [clearColor=null] - The clear color of the render pass.
	 * @param {?number} [clearAlpha=null] - The clear alpha of the render pass.
	 */
	constructor( scene, camera, overrideMaterial = null, clearColor = null, clearAlpha = null ) {

		super();

		/**
		 * The scene to render.
		 *
		 * @type {Scene}
		 */
		this.scene = scene;

		/**
		 * The camera.
		 *
		 * @type {Camera}
		 */
		this.camera = camera;

		/**
		 * The override material. If set, this material is used
		 * for all objects in the scene.
		 *
		 * @type {?Material}
		 * @default null
		 */
		this.overrideMaterial = overrideMaterial;

		/**
		 * The clear color of the render pass.
		 *
		 * @type {?(number|Color|string)}
		 * @default null
		 */
		this.clearColor = clearColor;

		/**
		 * The clear alpha of the render pass.
		 *
		 * @type {?number}
		 * @default null
		 */
		this.clearAlpha = clearAlpha;

		/**
		 * Overwritten to perform a clear operation by default.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.clear = true;

		/**
		 * If set to `true`, only the depth can be cleared when `clear` is to `false`.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.clearDepth = false;

		/**
		 * Overwritten to disable the swap.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.needsSwap = false;
		this._oldClearColor = new Color();

	}

	/**
	 * Performs a beauty pass with the configured scene and camera.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
	 * destination for the pass.
	 * @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
	 * previous pass from this buffer.
	 * @param {number} deltaTime - The delta time in seconds.
	 * @param {boolean} maskActive - Whether masking is active or not.
	 */
	render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {

		const oldAutoClear = renderer.autoClear;
		renderer.autoClear = false;

		let oldClearAlpha, oldOverrideMaterial;

		if ( this.overrideMaterial !== null ) {

			oldOverrideMaterial = this.scene.overrideMaterial;

			this.scene.overrideMaterial = this.overrideMaterial;

		}

		if ( this.clearColor !== null ) {

			renderer.getClearColor( this._oldClearColor );
			renderer.setClearColor( this.clearColor, renderer.getClearAlpha() );

		}

		if ( this.clearAlpha !== null ) {

			oldClearAlpha = renderer.getClearAlpha();
			renderer.setClearAlpha( this.clearAlpha );

		}

		if ( this.clearDepth == true ) {

			renderer.clearDepth();

		}

		renderer.setRenderTarget( this.renderToScreen ? null : readBuffer );

		if ( this.clear === true ) {

			// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
			renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );

		}

		renderer.render( this.scene, this.camera );

		// restore

		if ( this.clearColor !== null ) {

			renderer.setClearColor( this._oldClearColor );

		}

		if ( this.clearAlpha !== null ) {

			renderer.setClearAlpha( oldClearAlpha );

		}

		if ( this.overrideMaterial !== null ) {

			this.scene.overrideMaterial = oldOverrideMaterial;

		}

		renderer.autoClear = oldAutoClear;

	}

}

/**
 * @module LuminosityHighPassShader
 * @three_import import { LuminosityHighPassShader } from 'three/addons/shaders/LuminosityHighPassShader.js';
 */

/**
 * Luminosity high pass shader.
 *
 * @constant
 * @type {ShaderMaterial~Shader}
 */
const LuminosityHighPassShader = {

	uniforms: {

		'tDiffuse': { value: null },
		'luminosityThreshold': { value: 1.0 },
		'smoothWidth': { value: 1.0 },
		'defaultColor': { value: new Color( 0x000000 ) },
		'defaultOpacity': { value: 0.0 }

	},

	vertexShader: /* glsl */`

		varying vec2 vUv;

		void main() {

			vUv = uv;

			gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

		}`,

	fragmentShader: /* glsl */`

		uniform sampler2D tDiffuse;
		uniform vec3 defaultColor;
		uniform float defaultOpacity;
		uniform float luminosityThreshold;
		uniform float smoothWidth;

		varying vec2 vUv;

		void main() {

			vec4 texel = texture2D( tDiffuse, vUv );

			float v = luminance( texel.xyz );

			vec4 outputColor = vec4( defaultColor.rgb, defaultOpacity );

			float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v );

			gl_FragColor = mix( outputColor, texel, alpha );

		}`

};

/**
 * This pass is inspired by the bloom pass of Unreal Engine. It creates a
 * mip map chain of bloom textures and blurs them with different radii. Because
 * of the weighted combination of mips, and because larger blurs are done on
 * higher mips, this effect provides good quality and performance.
 *
 * When using this pass, tone mapping must be enabled in the renderer settings.
 *
 * Reference:
 * - [Bloom in Unreal Engine]{@link https://docs.unrealengine.com/latest/INT/Engine/Rendering/PostProcessEffects/Bloom/}
 *
 * ```js
 * const resolution = new THREE.Vector2( window.innerWidth, window.innerHeight );
 * const bloomPass = new UnrealBloomPass( resolution, 1.5, 0.4, 0.85 );
 * composer.addPass( bloomPass );
 * ```
 *
 * @augments Pass
 * @three_import import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
 */
class UnrealBloomPass extends Pass {

	/**
	 * Constructs a new Unreal Bloom pass.
	 *
	 * @param {Vector2} [resolution] - The effect's resolution.
	 * @param {number} [strength=1] - The Bloom strength.
	 * @param {number} radius - The Bloom radius.
	 * @param {number} threshold - The luminance threshold limits which bright areas contribute to the Bloom effect.
	 */
	constructor( resolution, strength = 1, radius, threshold ) {

		super();

		/**
		 * The Bloom strength.
		 *
		 * @type {number}
		 * @default 1
		 */
		this.strength = strength;

		/**
		 * The Bloom radius.
		 *
		 * @type {number}
		 */
		this.radius = radius;

		/**
		 * The luminance threshold limits which bright areas contribute to the Bloom effect.
		 *
		 * @type {number}
		 */
		this.threshold = threshold;

		/**
		 * The effect's resolution.
		 *
		 * @type {Vector2}
		 * @default (256,256)
		 */
		this.resolution = ( resolution !== undefined ) ? new Vector2( resolution.x, resolution.y ) : new Vector2( 256, 256 );

		/**
		 * The effect's clear color
		 *
		 * @type {Color}
		 * @default (0,0,0)
		 */
		this.clearColor = new Color( 0, 0, 0 );

		/**
		 * Overwritten to disable the swap.
		 *
		 * @type {boolean}
		 * @default false
		 */
		this.needsSwap = false;

		// internals

		// render targets
		this.renderTargetsHorizontal = [];
		this.renderTargetsVertical = [];
		this.nMips = 5;
		let resx = Math.round( this.resolution.x / 2 );
		let resy = Math.round( this.resolution.y / 2 );

		this.renderTargetBright = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
		this.renderTargetBright.texture.name = 'UnrealBloomPass.bright';
		this.renderTargetBright.texture.generateMipmaps = false;

		for ( let i = 0; i < this.nMips; i ++ ) {

			const renderTargetHorizontal = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );

			renderTargetHorizontal.texture.name = 'UnrealBloomPass.h' + i;
			renderTargetHorizontal.texture.generateMipmaps = false;

			this.renderTargetsHorizontal.push( renderTargetHorizontal );

			const renderTargetVertical = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );

			renderTargetVertical.texture.name = 'UnrealBloomPass.v' + i;
			renderTargetVertical.texture.generateMipmaps = false;

			this.renderTargetsVertical.push( renderTargetVertical );

			resx = Math.round( resx / 2 );

			resy = Math.round( resy / 2 );

		}

		// luminosity high pass material

		const highPassShader = LuminosityHighPassShader;
		this.highPassUniforms = UniformsUtils.clone( highPassShader.uniforms );

		this.highPassUniforms[ 'luminosityThreshold' ].value = threshold;
		this.highPassUniforms[ 'smoothWidth' ].value = 0.01;

		this.materialHighPassFilter = new ShaderMaterial( {
			uniforms: this.highPassUniforms,
			vertexShader: highPassShader.vertexShader,
			fragmentShader: highPassShader.fragmentShader
		} );

		// gaussian blur materials

		this.separableBlurMaterials = [];
		const kernelSizeArray = [ 3, 5, 7, 9, 11 ];
		resx = Math.round( this.resolution.x / 2 );
		resy = Math.round( this.resolution.y / 2 );

		for ( let i = 0; i < this.nMips; i ++ ) {

			this.separableBlurMaterials.push( this._getSeparableBlurMaterial( kernelSizeArray[ i ] ) );

			this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy );

			resx = Math.round( resx / 2 );

			resy = Math.round( resy / 2 );

		}

		// composite material

		this.compositeMaterial = this._getCompositeMaterial( this.nMips );
		this.compositeMaterial.uniforms[ 'blurTexture1' ].value = this.renderTargetsVertical[ 0 ].texture;
		this.compositeMaterial.uniforms[ 'blurTexture2' ].value = this.renderTargetsVertical[ 1 ].texture;
		this.compositeMaterial.uniforms[ 'blurTexture3' ].value = this.renderTargetsVertical[ 2 ].texture;
		this.compositeMaterial.uniforms[ 'blurTexture4' ].value = this.renderTargetsVertical[ 3 ].texture;
		this.compositeMaterial.uniforms[ 'blurTexture5' ].value = this.renderTargetsVertical[ 4 ].texture;
		this.compositeMaterial.uniforms[ 'bloomStrength' ].value = strength;
		this.compositeMaterial.uniforms[ 'bloomRadius' ].value = 0.1;

		const bloomFactors = [ 1.0, 0.8, 0.6, 0.4, 0.2 ];
		this.compositeMaterial.uniforms[ 'bloomFactors' ].value = bloomFactors;
		this.bloomTintColors = [ new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ) ];
		this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors;

		// blend material

		this.copyUniforms = UniformsUtils.clone( CopyShader.uniforms );

		this.blendMaterial = new ShaderMaterial( {
			uniforms: this.copyUniforms,
			vertexShader: CopyShader.vertexShader,
			fragmentShader: CopyShader.fragmentShader,
			blending: AdditiveBlending,
			depthTest: false,
			depthWrite: false,
			transparent: true
		} );

		this._oldClearColor = new Color();
		this._oldClearAlpha = 1;

		this._basic = new MeshBasicMaterial();

		this._fsQuad = new FullScreenQuad( null );

	}

	/**
	 * Frees the GPU-related resources allocated by this instance. Call this
	 * method whenever the pass is no longer used in your app.
	 */
	dispose() {

		for ( let i = 0; i < this.renderTargetsHorizontal.length; i ++ ) {

			this.renderTargetsHorizontal[ i ].dispose();

		}

		for ( let i = 0; i < this.renderTargetsVertical.length; i ++ ) {

			this.renderTargetsVertical[ i ].dispose();

		}

		this.renderTargetBright.dispose();

		//

		for ( let i = 0; i < this.separableBlurMaterials.length; i ++ ) {

			this.separableBlurMaterials[ i ].dispose();

		}

		this.compositeMaterial.dispose();
		this.blendMaterial.dispose();
		this._basic.dispose();

		//

		this._fsQuad.dispose();

	}

	/**
	 * Sets the size of the pass.
	 *
	 * @param {number} width - The width to set.
	 * @param {number} height - The height to set.
	 */
	setSize( width, height ) {

		let resx = Math.round( width / 2 );
		let resy = Math.round( height / 2 );

		this.renderTargetBright.setSize( resx, resy );

		for ( let i = 0; i < this.nMips; i ++ ) {

			this.renderTargetsHorizontal[ i ].setSize( resx, resy );
			this.renderTargetsVertical[ i ].setSize( resx, resy );

			this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy );

			resx = Math.round( resx / 2 );
			resy = Math.round( resy / 2 );

		}

	}

	/**
	 * Performs the Bloom pass.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
	 * destination for the pass.
	 * @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
	 * previous pass from this buffer.
	 * @param {number} deltaTime - The delta time in seconds.
	 * @param {boolean} maskActive - Whether masking is active or not.
	 */
	render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {

		renderer.getClearColor( this._oldClearColor );
		this._oldClearAlpha = renderer.getClearAlpha();
		const oldAutoClear = renderer.autoClear;
		renderer.autoClear = false;

		renderer.setClearColor( this.clearColor, 0 );

		if ( maskActive ) renderer.state.buffers.stencil.setTest( false );

		// Render input to screen

		if ( this.renderToScreen ) {

			this._fsQuad.material = this._basic;
			this._basic.map = readBuffer.texture;

			renderer.setRenderTarget( null );
			renderer.clear();
			this._fsQuad.render( renderer );

		}

		// 1. Extract Bright Areas

		this.highPassUniforms[ 'tDiffuse' ].value = readBuffer.texture;
		this.highPassUniforms[ 'luminosityThreshold' ].value = this.threshold;
		this._fsQuad.material = this.materialHighPassFilter;

		renderer.setRenderTarget( this.renderTargetBright );
		renderer.clear();
		this._fsQuad.render( renderer );

		// 2. Blur All the mips progressively

		let inputRenderTarget = this.renderTargetBright;

		for ( let i = 0; i < this.nMips; i ++ ) {

			this._fsQuad.material = this.separableBlurMaterials[ i ];

			this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = inputRenderTarget.texture;
			this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionX;
			renderer.setRenderTarget( this.renderTargetsHorizontal[ i ] );
			renderer.clear();
			this._fsQuad.render( renderer );

			this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = this.renderTargetsHorizontal[ i ].texture;
			this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionY;
			renderer.setRenderTarget( this.renderTargetsVertical[ i ] );
			renderer.clear();
			this._fsQuad.render( renderer );

			inputRenderTarget = this.renderTargetsVertical[ i ];

		}

		// Composite All the mips

		this._fsQuad.material = this.compositeMaterial;
		this.compositeMaterial.uniforms[ 'bloomStrength' ].value = this.strength;
		this.compositeMaterial.uniforms[ 'bloomRadius' ].value = this.radius;
		this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors;

		renderer.setRenderTarget( this.renderTargetsHorizontal[ 0 ] );
		renderer.clear();
		this._fsQuad.render( renderer );

		// Blend it additively over the input texture

		this._fsQuad.material = this.blendMaterial;
		this.copyUniforms[ 'tDiffuse' ].value = this.renderTargetsHorizontal[ 0 ].texture;

		if ( maskActive ) renderer.state.buffers.stencil.setTest( true );

		if ( this.renderToScreen ) {

			renderer.setRenderTarget( null );
			this._fsQuad.render( renderer );

		} else {

			renderer.setRenderTarget( readBuffer );
			this._fsQuad.render( renderer );

		}

		// Restore renderer settings

		renderer.setClearColor( this._oldClearColor, this._oldClearAlpha );
		renderer.autoClear = oldAutoClear;

	}

	// internals

	_getSeparableBlurMaterial( kernelRadius ) {

		const coefficients = [];

		for ( let i = 0; i < kernelRadius; i ++ ) {

			coefficients.push( 0.39894 * Math.exp( -0.5 * i * i / ( kernelRadius * kernelRadius ) ) / kernelRadius );

		}

		return new ShaderMaterial( {

			defines: {
				'KERNEL_RADIUS': kernelRadius
			},

			uniforms: {
				'colorTexture': { value: null },
				'invSize': { value: new Vector2( 0.5, 0.5 ) }, // inverse texture size
				'direction': { value: new Vector2( 0.5, 0.5 ) },
				'gaussianCoefficients': { value: coefficients } // precomputed Gaussian coefficients
			},

			vertexShader:
				`varying vec2 vUv;
				void main() {
					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
				}`,

			fragmentShader:
				`#include <common>
				varying vec2 vUv;
				uniform sampler2D colorTexture;
				uniform vec2 invSize;
				uniform vec2 direction;
				uniform float gaussianCoefficients[KERNEL_RADIUS];

				void main() {
					float weightSum = gaussianCoefficients[0];
					vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum;
					for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
						float x = float(i);
						float w = gaussianCoefficients[i];
						vec2 uvOffset = direction * invSize * x;
						vec3 sample1 = texture2D( colorTexture, vUv + uvOffset ).rgb;
						vec3 sample2 = texture2D( colorTexture, vUv - uvOffset ).rgb;
						diffuseSum += (sample1 + sample2) * w;
						weightSum += 2.0 * w;
					}
					gl_FragColor = vec4(diffuseSum/weightSum, 1.0);
				}`
		} );

	}

	_getCompositeMaterial( nMips ) {

		return new ShaderMaterial( {

			defines: {
				'NUM_MIPS': nMips
			},

			uniforms: {
				'blurTexture1': { value: null },
				'blurTexture2': { value: null },
				'blurTexture3': { value: null },
				'blurTexture4': { value: null },
				'blurTexture5': { value: null },
				'bloomStrength': { value: 1.0 },
				'bloomFactors': { value: null },
				'bloomTintColors': { value: null },
				'bloomRadius': { value: 0.0 }
			},

			vertexShader:
				`varying vec2 vUv;
				void main() {
					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
				}`,

			fragmentShader:
				`varying vec2 vUv;
				uniform sampler2D blurTexture1;
				uniform sampler2D blurTexture2;
				uniform sampler2D blurTexture3;
				uniform sampler2D blurTexture4;
				uniform sampler2D blurTexture5;
				uniform float bloomStrength;
				uniform float bloomRadius;
				uniform float bloomFactors[NUM_MIPS];
				uniform vec3 bloomTintColors[NUM_MIPS];

				float lerpBloomFactor(const in float factor) {
					float mirrorFactor = 1.2 - factor;
					return mix(factor, mirrorFactor, bloomRadius);
				}

				void main() {
					gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) +
						lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) +
						lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) +
						lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) +
						lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) );
				}`
		} );

	}

}

UnrealBloomPass.BlurDirectionX = new Vector2( 1.0, 0.0 );
UnrealBloomPass.BlurDirectionY = new Vector2( 0.0, 1.0 );

class RenderableObject {

	constructor() {

		this.id = 0;

		this.object = null;
		this.z = 0;
		this.renderOrder = 0;

	}

}

//

class RenderableFace {

	constructor() {

		this.id = 0;

		this.v1 = new RenderableVertex();
		this.v2 = new RenderableVertex();
		this.v3 = new RenderableVertex();

		this.normalModel = new Vector3();

		this.vertexNormalsModel = [ new Vector3(), new Vector3(), new Vector3() ];
		this.vertexNormalsLength = 0;

		this.color = new Color();
		this.material = null;
		this.uvs = [ new Vector2(), new Vector2(), new Vector2() ];

		this.z = 0;
		this.renderOrder = 0;

	}

}

//

class RenderableVertex {

	constructor() {

		this.position = new Vector3();
		this.positionWorld = new Vector3();
		this.positionScreen = new Vector4();

		this.visible = true;

	}

	copy( vertex ) {

		this.positionWorld.copy( vertex.positionWorld );
		this.positionScreen.copy( vertex.positionScreen );

	}

}

//

class RenderableLine {

	constructor() {

		this.id = 0;

		this.v1 = new RenderableVertex();
		this.v2 = new RenderableVertex();

		this.vertexColors = [ new Color(), new Color() ];
		this.material = null;

		this.z = 0;
		this.renderOrder = 0;

	}

}

//

class RenderableSprite {

	constructor() {

		this.id = 0;

		this.object = null;

		this.x = 0;
		this.y = 0;
		this.z = 0;

		this.rotation = 0;
		this.scale = new Vector2();

		this.material = null;
		this.renderOrder = 0;

	}

}

/**
 * This class can project a given scene in 3D space into a 2D representation
 * used for rendering with a 2D API. `Projector` is currently used by {@link SVGRenderer}
 * and was previously used by the legacy `CanvasRenderer`.
 *
 * @three_import import { Projector } from 'three/addons/renderers/Projector.js';
 */
class Projector {

	/**
	 * Constructs a new projector.
	 */
	constructor() {

		let _object, _objectCount, _objectPoolLength = 0,
			_vertex, _vertexCount, _vertexPoolLength = 0,
			_face, _faceCount, _facePoolLength = 0,
			_line, _lineCount, _linePoolLength = 0,
			_sprite, _spriteCount, _spritePoolLength = 0,
			_modelMatrix;

		const

			_renderData = { objects: [], lights: [], elements: [] },

			_vector3 = new Vector3(),
			_vector4 = new Vector4(),

			_clipBox = new Box3( new Vector3( -1, -1, -1 ), new Vector3( 1, 1, 1 ) ),
			_boundingBox = new Box3(),
			_points3 = new Array( 3 ),

			_viewMatrix = new Matrix4(),
			_viewProjectionMatrix = new Matrix4(),

			_modelViewProjectionMatrix = new Matrix4(),

			_frustum = new Frustum(),

			_objectPool = [], _vertexPool = [], _facePool = [], _linePool = [], _spritePool = [];

		//

		function RenderList() {

			const normals = [];
			const colors = [];
			const uvs = [];

			let object = null;

			const normalMatrix = new Matrix3();

			function setObject( value ) {

				object = value;

				normalMatrix.getNormalMatrix( object.matrixWorld );

				normals.length = 0;
				colors.length = 0;
				uvs.length = 0;

			}

			function projectVertex( vertex ) {

				const position = vertex.position;
				const positionWorld = vertex.positionWorld;
				const positionScreen = vertex.positionScreen;

				positionWorld.copy( position ).applyMatrix4( _modelMatrix );
				positionScreen.copy( positionWorld ).applyMatrix4( _viewProjectionMatrix );

				const invW = 1 / positionScreen.w;

				positionScreen.x *= invW;
				positionScreen.y *= invW;
				positionScreen.z *= invW;

				vertex.visible = positionScreen.x >= -1 && positionScreen.x <= 1 &&
						 positionScreen.y >= -1 && positionScreen.y <= 1 &&
						 positionScreen.z >= -1 && positionScreen.z <= 1;

			}

			function pushVertex( x, y, z ) {

				_vertex = getNextVertexInPool();
				_vertex.position.set( x, y, z );

				projectVertex( _vertex );

			}

			function pushNormal( x, y, z ) {

				normals.push( x, y, z );

			}

			function pushColor( r, g, b ) {

				colors.push( r, g, b );

			}

			function pushUv( x, y ) {

				uvs.push( x, y );

			}

			function checkTriangleVisibility( v1, v2, v3 ) {

				if ( v1.visible === true || v2.visible === true || v3.visible === true ) return true;

				_points3[ 0 ] = v1.positionScreen;
				_points3[ 1 ] = v2.positionScreen;
				_points3[ 2 ] = v3.positionScreen;

				return _clipBox.intersectsBox( _boundingBox.setFromPoints( _points3 ) );

			}

			function checkBackfaceCulling( v1, v2, v3 ) {

				return ( ( v3.positionScreen.x - v1.positionScreen.x ) *
					    ( v2.positionScreen.y - v1.positionScreen.y ) -
					    ( v3.positionScreen.y - v1.positionScreen.y ) *
					    ( v2.positionScreen.x - v1.positionScreen.x ) ) < 0;

			}

			function pushLine( a, b ) {

				const v1 = _vertexPool[ a ];
				const v2 = _vertexPool[ b ];

				// Clip

				v1.positionScreen.copy( v1.position ).applyMatrix4( _modelViewProjectionMatrix );
				v2.positionScreen.copy( v2.position ).applyMatrix4( _modelViewProjectionMatrix );

				if ( clipLine( v1.positionScreen, v2.positionScreen ) === true ) {

					// Perform the perspective divide
					v1.positionScreen.multiplyScalar( 1 / v1.positionScreen.w );
					v2.positionScreen.multiplyScalar( 1 / v2.positionScreen.w );

					_line = getNextLineInPool();
					_line.id = object.id;
					_line.v1.copy( v1 );
					_line.v2.copy( v2 );
					_line.z = Math.max( v1.positionScreen.z, v2.positionScreen.z );
					_line.renderOrder = object.renderOrder;

					_line.material = object.material;

					if ( object.material.vertexColors ) {

						_line.vertexColors[ 0 ].fromArray( colors, a * 3 );
						_line.vertexColors[ 1 ].fromArray( colors, b * 3 );

					}

					_renderData.elements.push( _line );

				}

			}

			function pushTriangle( a, b, c, material ) {

				const v1 = _vertexPool[ a ];
				const v2 = _vertexPool[ b ];
				const v3 = _vertexPool[ c ];

				if ( checkTriangleVisibility( v1, v2, v3 ) === false ) return;

				if ( material.side === DoubleSide || checkBackfaceCulling( v1, v2, v3 ) === true ) {

					_face = getNextFaceInPool();

					_face.id = object.id;
					_face.v1.copy( v1 );
					_face.v2.copy( v2 );
					_face.v3.copy( v3 );
					_face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3;
					_face.renderOrder = object.renderOrder;

					// face normal
					_vector3.subVectors( v3.position, v2.position );
					_vector4.subVectors( v1.position, v2.position );
					_vector3.cross( _vector4 );
					_face.normalModel.copy( _vector3 );
					_face.normalModel.applyMatrix3( normalMatrix ).normalize();

					for ( let i = 0; i < 3; i ++ ) {

						const normal = _face.vertexNormalsModel[ i ];
						normal.fromArray( normals, arguments[ i ] * 3 );
						normal.applyMatrix3( normalMatrix ).normalize();

						const uv = _face.uvs[ i ];
						uv.fromArray( uvs, arguments[ i ] * 2 );

					}

					_face.vertexNormalsLength = 3;

					_face.material = material;

					if ( material.vertexColors ) {

						_face.color.fromArray( colors, a * 3 );

					}

					_renderData.elements.push( _face );

				}

			}

			return {
				setObject: setObject,
				projectVertex: projectVertex,
				checkTriangleVisibility: checkTriangleVisibility,
				checkBackfaceCulling: checkBackfaceCulling,
				pushVertex: pushVertex,
				pushNormal: pushNormal,
				pushColor: pushColor,
				pushUv: pushUv,
				pushLine: pushLine,
				pushTriangle: pushTriangle
			};

		}

		const renderList = new RenderList();

		function projectObject( object ) {

			if ( object.visible === false ) return;

			if ( object.isLight ) {

				_renderData.lights.push( object );

			} else if ( object.isMesh || object.isLine || object.isPoints ) {

				if ( object.material.visible === false ) return;
				if ( object.frustumCulled === true && _frustum.intersectsObject( object ) === false ) return;

				addObject( object );

			} else if ( object.isSprite ) {

				if ( object.material.visible === false ) return;
				if ( object.frustumCulled === true && _frustum.intersectsSprite( object ) === false ) return;

				addObject( object );

			}

			const children = object.children;

			for ( let i = 0, l = children.length; i < l; i ++ ) {

				projectObject( children[ i ] );

			}

		}

		function addObject( object ) {

			_object = getNextObjectInPool();
			_object.id = object.id;
			_object.object = object;

			_vector3.setFromMatrixPosition( object.matrixWorld );
			_vector3.applyMatrix4( _viewProjectionMatrix );
			_object.z = _vector3.z;
			_object.renderOrder = object.renderOrder;

			_renderData.objects.push( _object );

		}

		/**
		 * Projects the given scene in 3D space into a 2D representation. The result
		 * is an object with renderable items.
		 *
		 * @param {Object3D} scene - A scene or any other type of 3D object.
		 * @param {Camera} camera - The camera.
		 * @param {boolean} sortObjects - Whether to sort objects or not.
		 * @param {boolean} sortElements - Whether to sort elements (faces, lines and sprites) or not.
		 * @return {{objects:Array<Objects>,lights:Array<Objects>,elements:Array<Objects>}} The projected scene as renderable objects.
		 */
		this.projectScene = function ( scene, camera, sortObjects, sortElements ) {

			_faceCount = 0;
			_lineCount = 0;
			_spriteCount = 0;

			_renderData.elements.length = 0;

			if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();
			if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();

			_viewMatrix.copy( camera.matrixWorldInverse );
			_viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, _viewMatrix );

			_frustum.setFromProjectionMatrix( _viewProjectionMatrix );

			//

			_objectCount = 0;

			_renderData.objects.length = 0;
			_renderData.lights.length = 0;

			projectObject( scene );

			if ( sortObjects === true ) {

				_renderData.objects.sort( painterSort );

			}

			//

			const objects = _renderData.objects;

			for ( let o = 0, ol = objects.length; o < ol; o ++ ) {

				const object = objects[ o ].object;
				const geometry = object.geometry;

				renderList.setObject( object );

				_modelMatrix = object.matrixWorld;

				_vertexCount = 0;

				if ( object.isMesh ) {

					let material = object.material;

					const isMultiMaterial = Array.isArray( material );

					const attributes = geometry.attributes;
					const groups = geometry.groups;

					if ( attributes.position === undefined ) continue;

					const positions = attributes.position.array;

					for ( let i = 0, l = positions.length; i < l; i += 3 ) {

						let x = positions[ i ];
						let y = positions[ i + 1 ];
						let z = positions[ i + 2 ];

						const morphTargets = geometry.morphAttributes.position;

						if ( morphTargets !== undefined ) {

							const morphTargetsRelative = geometry.morphTargetsRelative;
							const morphInfluences = object.morphTargetInfluences;

							for ( let t = 0, tl = morphTargets.length; t < tl; t ++ ) {

								const influence = morphInfluences[ t ];

								if ( influence === 0 ) continue;

								const target = morphTargets[ t ];

								if ( morphTargetsRelative ) {

									x += target.getX( i / 3 ) * influence;
									y += target.getY( i / 3 ) * influence;
									z += target.getZ( i / 3 ) * influence;

								} else {

									x += ( target.getX( i / 3 ) - positions[ i ] ) * influence;
									y += ( target.getY( i / 3 ) - positions[ i + 1 ] ) * influence;
									z += ( target.getZ( i / 3 ) - positions[ i + 2 ] ) * influence;

								}

							}

						}

						renderList.pushVertex( x, y, z );

					}

					if ( attributes.normal !== undefined ) {

						const normals = attributes.normal.array;

						for ( let i = 0, l = normals.length; i < l; i += 3 ) {

							renderList.pushNormal( normals[ i ], normals[ i + 1 ], normals[ i + 2 ] );

						}

					}

					if ( attributes.color !== undefined ) {

						const colors = attributes.color.array;

						for ( let i = 0, l = colors.length; i < l; i += 3 ) {

							renderList.pushColor( colors[ i ], colors[ i + 1 ], colors[ i + 2 ] );

						}

					}

					if ( attributes.uv !== undefined ) {

						const uvs = attributes.uv.array;

						for ( let i = 0, l = uvs.length; i < l; i += 2 ) {

							renderList.pushUv( uvs[ i ], uvs[ i + 1 ] );

						}

					}

					if ( geometry.index !== null ) {

						const indices = geometry.index.array;

						if ( groups.length > 0 ) {

							for ( let g = 0; g < groups.length; g ++ ) {

								const group = groups[ g ];

								material = isMultiMaterial === true
									 ? object.material[ group.materialIndex ]
									 : object.material;

								if ( material === undefined ) continue;

								for ( let i = group.start, l = group.start + group.count; i < l; i += 3 ) {

									renderList.pushTriangle( indices[ i ], indices[ i + 1 ], indices[ i + 2 ], material );

								}

							}

						} else {

							for ( let i = 0, l = indices.length; i < l; i += 3 ) {

								renderList.pushTriangle( indices[ i ], indices[ i + 1 ], indices[ i + 2 ], material );

							}

						}

					} else {

						if ( groups.length > 0 ) {

							for ( let g = 0; g < groups.length; g ++ ) {

								const group = groups[ g ];

								material = isMultiMaterial === true
									 ? object.material[ group.materialIndex ]
									 : object.material;

								if ( material === undefined ) continue;

								for ( let i = group.start, l = group.start + group.count; i < l; i += 3 ) {

									renderList.pushTriangle( i, i + 1, i + 2, material );

								}

							}

						} else {

							for ( let i = 0, l = positions.length / 3; i < l; i += 3 ) {

								renderList.pushTriangle( i, i + 1, i + 2, material );

							}

						}

					}

				} else if ( object.isLine ) {

					_modelViewProjectionMatrix.multiplyMatrices( _viewProjectionMatrix, _modelMatrix );

					const attributes = geometry.attributes;

					if ( attributes.position !== undefined ) {

						const positions = attributes.position.array;

						for ( let i = 0, l = positions.length; i < l; i += 3 ) {

							renderList.pushVertex( positions[ i ], positions[ i + 1 ], positions[ i + 2 ] );

						}

						if ( attributes.color !== undefined ) {

							const colors = attributes.color.array;

							for ( let i = 0, l = colors.length; i < l; i += 3 ) {

								renderList.pushColor( colors[ i ], colors[ i + 1 ], colors[ i + 2 ] );

							}

						}

						if ( geometry.index !== null ) {

							const indices = geometry.index.array;

							for ( let i = 0, l = indices.length; i < l; i += 2 ) {

								renderList.pushLine( indices[ i ], indices[ i + 1 ] );

							}

						} else {

							const step = object.isLineSegments ? 2 : 1;

							for ( let i = 0, l = ( positions.length / 3 ) - 1; i < l; i += step ) {

								renderList.pushLine( i, i + 1 );

							}

						}

					}

				} else if ( object.isPoints ) {

					_modelViewProjectionMatrix.multiplyMatrices( _viewProjectionMatrix, _modelMatrix );

					const attributes = geometry.attributes;

					if ( attributes.position !== undefined ) {

						const positions = attributes.position.array;

						for ( let i = 0, l = positions.length; i < l; i += 3 ) {

							_vector4.set( positions[ i ], positions[ i + 1 ], positions[ i + 2 ], 1 );
							_vector4.applyMatrix4( _modelViewProjectionMatrix );

							pushPoint( _vector4, object, camera );

						}

					}

				} else if ( object.isSprite ) {

					object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );
					_vector4.set( _modelMatrix.elements[ 12 ], _modelMatrix.elements[ 13 ], _modelMatrix.elements[ 14 ], 1 );
					_vector4.applyMatrix4( _viewProjectionMatrix );

					pushPoint( _vector4, object, camera );

				}

			}

			if ( sortElements === true ) {

				_renderData.elements.sort( painterSort );

			}

			return _renderData;

		};

		function pushPoint( _vector4, object, camera ) {

			const invW = 1 / _vector4.w;

			_vector4.z *= invW;

			if ( _vector4.z >= -1 && _vector4.z <= 1 ) {

				_sprite = getNextSpriteInPool();
				_sprite.id = object.id;
				_sprite.x = _vector4.x * invW;
				_sprite.y = _vector4.y * invW;
				_sprite.z = _vector4.z;
				_sprite.renderOrder = object.renderOrder;
				_sprite.object = object;

				_sprite.rotation = object.rotation;

				_sprite.scale.x = object.scale.x * Math.abs( _sprite.x - ( _vector4.x + camera.projectionMatrix.elements[ 0 ] ) / ( _vector4.w + camera.projectionMatrix.elements[ 12 ] ) );
				_sprite.scale.y = object.scale.y * Math.abs( _sprite.y - ( _vector4.y + camera.projectionMatrix.elements[ 5 ] ) / ( _vector4.w + camera.projectionMatrix.elements[ 13 ] ) );

				_sprite.material = object.material;

				_renderData.elements.push( _sprite );

			}

		}

		// Pools

		function getNextObjectInPool() {

			if ( _objectCount === _objectPoolLength ) {

				const object = new RenderableObject();
				_objectPool.push( object );
				_objectPoolLength ++;
				_objectCount ++;
				return object;

			}

			return _objectPool[ _objectCount ++ ];

		}

		function getNextVertexInPool() {

			if ( _vertexCount === _vertexPoolLength ) {

				const vertex = new RenderableVertex();
				_vertexPool.push( vertex );
				_vertexPoolLength ++;
				_vertexCount ++;
				return vertex;

			}

			return _vertexPool[ _vertexCount ++ ];

		}

		function getNextFaceInPool() {

			if ( _faceCount === _facePoolLength ) {

				const face = new RenderableFace();
				_facePool.push( face );
				_facePoolLength ++;
				_faceCount ++;
				return face;

			}

			return _facePool[ _faceCount ++ ];


		}

		function getNextLineInPool() {

			if ( _lineCount === _linePoolLength ) {

				const line = new RenderableLine();
				_linePool.push( line );
				_linePoolLength ++;
				_lineCount ++;
				return line;

			}

			return _linePool[ _lineCount ++ ];

		}

		function getNextSpriteInPool() {

			if ( _spriteCount === _spritePoolLength ) {

				const sprite = new RenderableSprite();
				_spritePool.push( sprite );
				_spritePoolLength ++;
				_spriteCount ++;
				return sprite;

			}

			return _spritePool[ _spriteCount ++ ];

		}

		//

		function painterSort( a, b ) {

			if ( a.renderOrder !== b.renderOrder ) {

				return a.renderOrder - b.renderOrder;

			} else if ( a.z !== b.z ) {

				return b.z - a.z;

			} else if ( a.id !== b.id ) {

				return a.id - b.id;

			} else {

				return 0;

			}

		}

		function clipLine( s1, s2 ) {

			let alpha1 = 0, alpha2 = 1;

			// Calculate the boundary coordinate of each vertex for the near and far clip planes,
			// Z = -1 and Z = +1, respectively.

			const bc1near = s1.z + s1.w,
				bc2near = s2.z + s2.w,
				bc1far = - s1.z + s1.w,
				bc2far = - s2.z + s2.w;

			if ( bc1near >= 0 && bc2near >= 0 && bc1far >= 0 && bc2far >= 0 ) {

				// Both vertices lie entirely within all clip planes.
				return true;

			} else if ( ( bc1near < 0 && bc2near < 0 ) || ( bc1far < 0 && bc2far < 0 ) ) {

				// Both vertices lie entirely outside one of the clip planes.
				return false;

			} else {

				// The line segment spans at least one clip plane.

				if ( bc1near < 0 ) {

					// v1 lies outside the near plane, v2 inside
					alpha1 = Math.max( alpha1, bc1near / ( bc1near - bc2near ) );

				} else if ( bc2near < 0 ) {

					// v2 lies outside the near plane, v1 inside
					alpha2 = Math.min( alpha2, bc1near / ( bc1near - bc2near ) );

				}

				if ( bc1far < 0 ) {

					// v1 lies outside the far plane, v2 inside
					alpha1 = Math.max( alpha1, bc1far / ( bc1far - bc2far ) );

				} else if ( bc2far < 0 ) {

					// v2 lies outside the far plane, v2 inside
					alpha2 = Math.min( alpha2, bc1far / ( bc1far - bc2far ) );

				}

				if ( alpha2 < alpha1 ) {

					// The line segment spans two boundaries, but is outside both of them.
					// (This can't happen when we're only clipping against just near/far but good
					//  to leave the check here for future usage if other clip planes are added.)
					return false;

				} else {

					// Update the s1 and s2 vertices to match the clipped line segment.
					s1.lerp( s2, alpha1 );
					s2.lerp( s1, 1 - alpha2 );

					return true;

				}

			}

		}

	}

}

/**
 * This renderer an be used to render geometric data using SVG. The produced vector
 * graphics are particular useful in the following use cases:
 *
 * - Animated logos or icons.
 * - Interactive 2D/3D diagrams or graphs.
 * - Interactive maps.
 * - Complex or animated user interfaces.
 *
 * `SVGRenderer` has various advantages. It produces crystal-clear and sharp output which
 * is independent of the actual viewport resolution.SVG elements can be styled via CSS.
 * And they have good accessibility since it's possible to add metadata like title or description
 * (useful for search engines or screen readers).
 *
 * There are, however, some important limitations:
 * - No advanced shading.
 * - No texture support.
 * - No shadow support.
 *
 * @three_import import { SVGRenderer } from 'three/addons/renderers/SVGRenderer.js';
 */
class SVGRenderer {

	/**
	 * Constructs a new SVG renderer.
	 */
	constructor() {

		let _renderData, _elements, _lights,
			_svgWidth, _svgHeight, _svgWidthHalf, _svgHeightHalf,

			_v1, _v2, _v3,

			_svgNode,
			_pathCount = 0,

			_precision = null,
			_quality = 1,

			_currentPath, _currentStyle;

		const _this = this,
			_clipBox = new Box2(),
			_elemBox = new Box2(),

			_color = new Color(),
			_diffuseColor = new Color(),
			_ambientLight = new Color(),
			_directionalLights = new Color(),
			_pointLights = new Color(),
			_clearColor = new Color(),

			_vector3 = new Vector3(), // Needed for PointLight
			_centroid = new Vector3(),
			_normal = new Vector3(),
			_normalViewMatrix = new Matrix3(),

			_viewMatrix = new Matrix4(),
			_viewProjectionMatrix = new Matrix4(),

			_svgPathPool = [],

			_projector = new Projector(),
			_svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );

		/**
		 * The DOM where the renderer appends its child-elements.
		 *
		 * @type {DOMElement}
		 */
		this.domElement = _svg;

		/**
		 * Whether to automatically perform a clear before a render call or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.autoClear = true;

		/**
		 * Whether to sort 3D objects or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.sortObjects = true;

		/**
		 * Whether to sort elements or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.sortElements = true;

		/**
		 * Number of fractional pixels to enlarge polygons in order to
		 * prevent anti-aliasing gaps. Range is `[0,1]`.
		 *
		 * @type {number}
		 * @default 0.5
		 */
		this.overdraw = 0.5;

		/**
		 * The output color space.
		 *
		 * @type {(SRGBColorSpace|LinearSRGBColorSpace)}
		 * @default SRGBColorSpace
		 */
		this.outputColorSpace = SRGBColorSpace;

		/**
		 * Provides information about the number of
		 * rendered vertices and faces.
		 *
		 * @type {Object}
		 */
		this.info = {

			render: {

				vertices: 0,
				faces: 0

			}

		};

		/**
		 * Sets the render quality. Setting to `high` means This value indicates that the browser
		 * tries to improve the SVG quality over rendering speed and geometric precision.
		 *
		 * @param {('low'|'high')} quality - The quality.
		 */
		this.setQuality = function ( quality ) {

			switch ( quality ) {

				case 'high': _quality = 1; break;
				case 'low': _quality = 0; break;

			}

		};

		/**
		 * Sets the clear color.
		 *
		 * @param {(number|Color|string)} color - The clear color to set.
		 */
		this.setClearColor = function ( color ) {

			_clearColor.set( color );

		};

		this.setPixelRatio = function () {};

		/**
		 * Resizes the renderer to the given width and height.
		 *
		 * @param {number} width - The width of the renderer.
		 * @param {number} height - The height of the renderer.
		 */
		this.setSize = function ( width, height ) {

			_svgWidth = width; _svgHeight = height;
			_svgWidthHalf = _svgWidth / 2; _svgHeightHalf = _svgHeight / 2;

			_svg.setAttribute( 'viewBox', ( - _svgWidthHalf ) + ' ' + ( - _svgHeightHalf ) + ' ' + _svgWidth + ' ' + _svgHeight );
			_svg.setAttribute( 'width', _svgWidth );
			_svg.setAttribute( 'height', _svgHeight );

			_clipBox.min.set( - _svgWidthHalf, - _svgHeightHalf );
			_clipBox.max.set( _svgWidthHalf, _svgHeightHalf );

		};

		/**
		 * Returns an object containing the width and height of the renderer.
		 *
		 * @return {{width:number,height:number}} The size of the renderer.
		 */
		this.getSize = function () {

			return {
				width: _svgWidth,
				height: _svgHeight
			};

		};

		/**
		 * Sets the precision of the data used to create a paths.
		 *
		 * @param {number} precision - The precision to set.
		 */
		this.setPrecision = function ( precision ) {

			_precision = precision;

		};

		function removeChildNodes() {

			_pathCount = 0;

			while ( _svg.childNodes.length > 0 ) {

				_svg.removeChild( _svg.childNodes[ 0 ] );

			}

		}

		function convert( c ) {

			return _precision !== null ? c.toFixed( _precision ) : c;

		}

		/**
		 * Performs a manual clear with the defined clear color.
		 */
		this.clear = function () {

			removeChildNodes();
			_svg.style.backgroundColor = _clearColor.getStyle( _this.outputColorSpace );

		};

		/**
		 * Renders the given scene using the given camera.
		 *
		 * @param {Object3D} scene - A scene or any other type of 3D object.
		 * @param {Camera} camera - The camera.
		 */
		this.render = function ( scene, camera ) {

			if ( camera instanceof Camera === false ) {

				console.error( 'THREE.SVGRenderer.render: camera is not an instance of Camera.' );
				return;

			}

			const background = scene.background;

			if ( background && background.isColor ) {

				removeChildNodes();
				_svg.style.backgroundColor = background.getStyle( _this.outputColorSpace );

			} else if ( this.autoClear === true ) {

				this.clear();

			}

			_this.info.render.vertices = 0;
			_this.info.render.faces = 0;

			_viewMatrix.copy( camera.matrixWorldInverse );
			_viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, _viewMatrix );

			_renderData = _projector.projectScene( scene, camera, this.sortObjects, this.sortElements );
			_elements = _renderData.elements;
			_lights = _renderData.lights;

			_normalViewMatrix.getNormalMatrix( camera.matrixWorldInverse );

			calculateLights( _lights );

			 // reset accumulated path

			_currentPath = '';
			_currentStyle = '';

			for ( let e = 0, el = _elements.length; e < el; e ++ ) {

				const element = _elements[ e ];
				const material = element.material;

				if ( material === undefined || material.opacity === 0 ) continue;

				_elemBox.makeEmpty();

				if ( element instanceof RenderableSprite ) {

					_v1 = element;
					_v1.x *= _svgWidthHalf; _v1.y *= - _svgHeightHalf;

					renderSprite( _v1, element, material );

				} else if ( element instanceof RenderableLine ) {

					_v1 = element.v1; _v2 = element.v2;

					_v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf;
					_v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf;

					_elemBox.setFromPoints( [ _v1.positionScreen, _v2.positionScreen ] );

					if ( _clipBox.intersectsBox( _elemBox ) === true ) {

						renderLine( _v1, _v2, material );

					}

				} else if ( element instanceof RenderableFace ) {

					_v1 = element.v1; _v2 = element.v2; _v3 = element.v3;

					if ( _v1.positionScreen.z < -1 || _v1.positionScreen.z > 1 ) continue;
					if ( _v2.positionScreen.z < -1 || _v2.positionScreen.z > 1 ) continue;
					if ( _v3.positionScreen.z < -1 || _v3.positionScreen.z > 1 ) continue;

					_v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf;
					_v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf;
					_v3.positionScreen.x *= _svgWidthHalf; _v3.positionScreen.y *= - _svgHeightHalf;

					if ( this.overdraw > 0 ) {

						expand( _v1.positionScreen, _v2.positionScreen, this.overdraw );
						expand( _v2.positionScreen, _v3.positionScreen, this.overdraw );
						expand( _v3.positionScreen, _v1.positionScreen, this.overdraw );

					}

					_elemBox.setFromPoints( [
						_v1.positionScreen,
						_v2.positionScreen,
						_v3.positionScreen
					] );

					if ( _clipBox.intersectsBox( _elemBox ) === true ) {

						renderFace3( _v1, _v2, _v3, element, material );

					}

				}

			}

			flushPath(); // just to flush last svg:path

			scene.traverseVisible( function ( object ) {

				 if ( object.isSVGObject ) {

					_vector3.setFromMatrixPosition( object.matrixWorld );
					_vector3.applyMatrix4( _viewProjectionMatrix );

					if ( _vector3.z < -1 || _vector3.z > 1 ) return;

					const x = _vector3.x * _svgWidthHalf;
					const y = - _vector3.y * _svgHeightHalf;

					const node = object.node;
					node.setAttribute( 'transform', 'translate(' + x + ',' + y + ')' );

					_svg.appendChild( node );

				}

			} );

		};

		function calculateLights( lights ) {

			_ambientLight.setRGB( 0, 0, 0 );
			_directionalLights.setRGB( 0, 0, 0 );
			_pointLights.setRGB( 0, 0, 0 );

			for ( let l = 0, ll = lights.length; l < ll; l ++ ) {

				const light = lights[ l ];
				const lightColor = light.color;

				if ( light.isAmbientLight ) {

					_ambientLight.r += lightColor.r;
					_ambientLight.g += lightColor.g;
					_ambientLight.b += lightColor.b;

				} else if ( light.isDirectionalLight ) {

					_directionalLights.r += lightColor.r;
					_directionalLights.g += lightColor.g;
					_directionalLights.b += lightColor.b;

				} else if ( light.isPointLight ) {

					_pointLights.r += lightColor.r;
					_pointLights.g += lightColor.g;
					_pointLights.b += lightColor.b;

				}

			}

		}

		function calculateLight( lights, position, normal, color ) {

			for ( let l = 0, ll = lights.length; l < ll; l ++ ) {

				const light = lights[ l ];
				const lightColor = light.color;

				if ( light.isDirectionalLight ) {

					const lightPosition = _vector3.setFromMatrixPosition( light.matrixWorld ).normalize();

					let amount = normal.dot( lightPosition );

					if ( amount <= 0 ) continue;

					amount *= light.intensity;

					color.r += lightColor.r * amount;
					color.g += lightColor.g * amount;
					color.b += lightColor.b * amount;

				} else if ( light.isPointLight ) {

					const lightPosition = _vector3.setFromMatrixPosition( light.matrixWorld );

					let amount = normal.dot( _vector3.subVectors( lightPosition, position ).normalize() );

					if ( amount <= 0 ) continue;

					amount *= light.distance == 0 ? 1 : 1 - Math.min( position.distanceTo( lightPosition ) / light.distance, 1 );

					if ( amount == 0 ) continue;

					amount *= light.intensity;

					color.r += lightColor.r * amount;
					color.g += lightColor.g * amount;
					color.b += lightColor.b * amount;

				}

			}

		}

		function renderSprite( v1, element, material ) {

			let scaleX = element.scale.x * _svgWidthHalf;
			let scaleY = element.scale.y * _svgHeightHalf;

			if ( material.isPointsMaterial ) {

				scaleX *= material.size;
				scaleY *= material.size;

			}

			const path = 'M' + convert( v1.x - scaleX * 0.5 ) + ',' + convert( v1.y - scaleY * 0.5 ) + 'h' + convert( scaleX ) + 'v' + convert( scaleY ) + 'h' + convert( - scaleX ) + 'z';
			let style = '';

			if ( material.isSpriteMaterial || material.isPointsMaterial ) {

				style = 'fill:' + material.color.getStyle( _this.outputColorSpace ) + ';fill-opacity:' + material.opacity;

			}

			addPath( style, path );

		}

		function renderLine( v1, v2, material ) {

			const path = 'M' + convert( v1.positionScreen.x ) + ',' + convert( v1.positionScreen.y ) + 'L' + convert( v2.positionScreen.x ) + ',' + convert( v2.positionScreen.y );

			if ( material.isLineBasicMaterial ) {

				let style = 'fill:none;stroke:' + material.color.getStyle( _this.outputColorSpace ) + ';stroke-opacity:' + material.opacity + ';stroke-width:' + material.linewidth + ';stroke-linecap:' + material.linecap;

				if ( material.isLineDashedMaterial ) {

					style = style + ';stroke-dasharray:' + material.dashSize + ',' + material.gapSize;

				}

				addPath( style, path );

			}

		}

		function renderFace3( v1, v2, v3, element, material ) {

			_this.info.render.vertices += 3;
			_this.info.render.faces ++;

			const path = 'M' + convert( v1.positionScreen.x ) + ',' + convert( v1.positionScreen.y ) + 'L' + convert( v2.positionScreen.x ) + ',' + convert( v2.positionScreen.y ) + 'L' + convert( v3.positionScreen.x ) + ',' + convert( v3.positionScreen.y ) + 'z';
			let style = '';

			if ( material.isMeshBasicMaterial ) {

				_color.copy( material.color );

				if ( material.vertexColors ) {

					_color.multiply( element.color );

				}

			} else if ( material.isMeshLambertMaterial || material.isMeshPhongMaterial || material.isMeshStandardMaterial ) {

				_diffuseColor.copy( material.color );

				if ( material.vertexColors ) {

					_diffuseColor.multiply( element.color );

				}

				_color.copy( _ambientLight );

				_centroid.copy( v1.positionWorld ).add( v2.positionWorld ).add( v3.positionWorld ).divideScalar( 3 );

				calculateLight( _lights, _centroid, element.normalModel, _color );

				_color.multiply( _diffuseColor ).add( material.emissive );

			} else if ( material.isMeshNormalMaterial ) {

				_normal.copy( element.normalModel ).applyMatrix3( _normalViewMatrix ).normalize();

				_color.setRGB( _normal.x, _normal.y, _normal.z ).multiplyScalar( 0.5 ).addScalar( 0.5 );

			}

			if ( material.wireframe ) {

				style = 'fill:none;stroke:' + _color.getStyle( _this.outputColorSpace ) + ';stroke-opacity:' + material.opacity + ';stroke-width:' + material.wireframeLinewidth + ';stroke-linecap:' + material.wireframeLinecap + ';stroke-linejoin:' + material.wireframeLinejoin;

			} else {

				style = 'fill:' + _color.getStyle( _this.outputColorSpace ) + ';fill-opacity:' + material.opacity;

			}

			addPath( style, path );

		}

		// Hide anti-alias gaps

		function expand( v1, v2, pixels ) {

			let x = v2.x - v1.x, y = v2.y - v1.y;
			const det = x * x + y * y;

			if ( det === 0 ) return;

			const idet = pixels / Math.sqrt( det );

			x *= idet; y *= idet;

			v2.x += x; v2.y += y;
			v1.x -= x; v1.y -= y;

		}

		function addPath( style, path ) {

			if ( _currentStyle === style ) {

				_currentPath += path;

			} else {

				flushPath();

				_currentStyle = style;
				_currentPath = path;

			}

		}

		function flushPath() {

			if ( _currentPath ) {

				_svgNode = getPathNode( _pathCount ++ );
				_svgNode.setAttribute( 'd', _currentPath );
				_svgNode.setAttribute( 'style', _currentStyle );
				_svg.appendChild( _svgNode );

			}

			_currentPath = '';
			_currentStyle = '';

		}

		function getPathNode( id ) {

			if ( _svgPathPool[ id ] == null ) {

				_svgPathPool[ id ] = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );

				if ( _quality == 0 ) {

					_svgPathPool[ id ].setAttribute( 'shape-rendering', 'crispEdges' ); //optimizeSpeed

				}

				return _svgPathPool[ id ];

			}

			return _svgPathPool[ id ];

		}

	}

}

const originalTHREE = {
   REVISION, DoubleSide, FrontSide, Object3D, Color, Vector2, Vector3, Matrix4, Line3, Raycaster,
   WebGLRenderer, WebGLRenderTarget,
   BufferGeometry, BufferAttribute, Float32BufferAttribute, Mesh, MeshBasicMaterial, MeshLambertMaterial,
   LineSegments, LineDashedMaterial, LineBasicMaterial, Points, PointsMaterial,
   Plane, Scene, PerspectiveCamera, OrthographicCamera, ShapeUtils,
   Box3, InstancedMesh, MeshStandardMaterial, MeshNormalMaterial,
   MeshPhysicalMaterial, MeshPhongMaterial, MeshDepthMaterial, MeshMatcapMaterial, MeshToonMaterial,
   Group: Group$1, PlaneHelper, Euler, Quaternion, BoxGeometry, CircleGeometry, SphereGeometry, Fog,
   AmbientLight, HemisphereLight, DirectionalLight,
   CanvasTexture, TextureLoader,

   Font, OrbitControls, SVGRenderer, TextGeometry, EffectComposer, RenderPass, UnrealBloomPass
}, THREE = Object.assign({}, originalTHREE);


/** @summary Import proper three.js version
  * @desc in node.js only r162 supports WebGL1 which can be emulated with "gl" package.
  * Therefore only this version can be used for working in node.js
  * @private */
async function importThreeJs(original) {

   if (!isNodeJs() || (THREE.REVISION <= 162))
      return THREE;
   return Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(h1 => {
      Object.assign(THREE, h1);
      return Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; });
   }).then(h2 => {
      Object.assign(THREE, h2);
      return THREE;
   });
}

let _hfont;

/** @summary Create three.js Helvetica Regular Font instance
  * @private */
function getHelveticaFont() {
   if (_hfont && _hfont instanceof THREE.Font)
      return _hfont;

   // eslint-disable-next-line
   const glyphs={"0":{x_min:73,x_max:715,ha:792,o:"m 394 -29 q 153 129 242 -29 q 73 479 73 272 q 152 829 73 687 q 394 989 241 989 q 634 829 545 989 q 715 479 715 684 q 635 129 715 270 q 394 -29 546 -29 m 394 89 q 546 211 489 89 q 598 479 598 322 q 548 748 598 640 q 394 871 491 871 q 241 748 298 871 q 190 479 190 637 q 239 211 190 319 q 394 89 296 89 "},"1":{x_min:215.671875,x_max:574,ha:792,o:"m 574 0 l 442 0 l 442 697 l 215 697 l 215 796 q 386 833 330 796 q 475 986 447 875 l 574 986 l 574 0 "},"2":{x_min:59,x_max:731,ha:792,o:"m 731 0 l 59 0 q 197 314 59 188 q 457 487 199 315 q 598 691 598 580 q 543 819 598 772 q 411 867 488 867 q 272 811 328 867 q 209 630 209 747 l 81 630 q 182 901 81 805 q 408 986 271 986 q 629 909 536 986 q 731 694 731 826 q 613 449 731 541 q 378 316 495 383 q 201 122 235 234 l 731 122 l 731 0 "},"3":{x_min:54,x_max:737,ha:792,o:"m 737 284 q 635 55 737 141 q 399 -25 541 -25 q 156 52 248 -25 q 54 308 54 140 l 185 308 q 245 147 185 202 q 395 96 302 96 q 539 140 484 96 q 602 280 602 190 q 510 429 602 390 q 324 454 451 454 l 324 565 q 487 584 441 565 q 565 719 565 617 q 515 835 565 791 q 395 879 466 879 q 255 824 307 879 q 203 661 203 769 l 78 661 q 166 909 78 822 q 387 992 250 992 q 603 921 513 992 q 701 723 701 844 q 669 607 701 656 q 578 524 637 558 q 696 434 655 499 q 737 284 737 369 "},"4":{x_min:48,x_max:742.453125,ha:792,o:"m 742 243 l 602 243 l 602 0 l 476 0 l 476 243 l 48 243 l 48 368 l 476 958 l 602 958 l 602 354 l 742 354 l 742 243 m 476 354 l 476 792 l 162 354 l 476 354 "},"5":{x_min:54.171875,x_max:738,ha:792,o:"m 738 314 q 626 60 738 153 q 382 -23 526 -23 q 155 47 248 -23 q 54 256 54 125 l 183 256 q 259 132 204 174 q 382 91 314 91 q 533 149 471 91 q 602 314 602 213 q 538 469 602 411 q 386 528 475 528 q 284 506 332 528 q 197 439 237 484 l 81 439 l 159 958 l 684 958 l 684 840 l 254 840 l 214 579 q 306 627 258 612 q 407 643 354 643 q 636 552 540 643 q 738 314 738 457 "},"6":{x_min:53,x_max:739,ha:792,o:"m 739 312 q 633 62 739 162 q 400 -31 534 -31 q 162 78 257 -31 q 53 439 53 206 q 178 859 53 712 q 441 986 284 986 q 643 912 559 986 q 732 713 732 833 l 601 713 q 544 830 594 786 q 426 875 494 875 q 268 793 331 875 q 193 517 193 697 q 301 597 240 570 q 427 624 362 624 q 643 540 552 624 q 739 312 739 451 m 603 298 q 540 461 603 400 q 404 516 484 516 q 268 461 323 516 q 207 300 207 401 q 269 137 207 198 q 405 83 325 83 q 541 137 486 83 q 603 298 603 197 "},"7":{x_min:58.71875,x_max:730.953125,ha:792,o:"m 730 839 q 469 448 560 641 q 335 0 378 255 l 192 0 q 328 441 235 252 q 593 830 421 630 l 58 830 l 58 958 l 730 958 l 730 839 "},"8":{x_min:55,x_max:736,ha:792,o:"m 571 527 q 694 424 652 491 q 736 280 736 358 q 648 71 736 158 q 395 -26 551 -26 q 142 69 238 -26 q 55 279 55 157 q 96 425 55 359 q 220 527 138 491 q 120 615 153 562 q 88 726 88 668 q 171 904 88 827 q 395 986 261 986 q 618 905 529 986 q 702 727 702 830 q 670 616 702 667 q 571 527 638 565 m 394 565 q 519 610 475 565 q 563 717 563 655 q 521 823 563 781 q 392 872 474 872 q 265 824 312 872 q 224 720 224 783 q 265 613 224 656 q 394 565 312 565 m 395 91 q 545 150 488 91 q 597 280 597 204 q 546 408 597 355 q 395 465 492 465 q 244 408 299 465 q 194 280 194 356 q 244 150 194 203 q 395 91 299 91 "},"9":{x_min:53,x_max:739,ha:792,o:"m 739 524 q 619 94 739 241 q 362 -32 516 -32 q 150 47 242 -32 q 59 244 59 126 l 191 244 q 246 129 191 176 q 373 82 301 82 q 526 161 466 82 q 597 440 597 255 q 363 334 501 334 q 130 432 216 334 q 53 650 53 521 q 134 880 53 786 q 383 986 226 986 q 659 841 566 986 q 739 524 739 719 m 388 449 q 535 514 480 449 q 585 658 585 573 q 535 805 585 744 q 388 873 480 873 q 242 809 294 873 q 191 658 191 745 q 239 514 191 572 q 388 449 292 449 "},"ο":{x_min:0,x_max:712,ha:815,o:"m 356 -25 q 96 88 192 -25 q 0 368 0 201 q 92 642 0 533 q 356 761 192 761 q 617 644 517 761 q 712 368 712 533 q 619 91 712 201 q 356 -25 520 -25 m 356 85 q 527 175 465 85 q 583 369 583 255 q 528 562 583 484 q 356 651 466 651 q 189 560 250 651 q 135 369 135 481 q 187 177 135 257 q 356 85 250 85 "},S:{x_min:0,x_max:788,ha:890,o:"m 788 291 q 662 54 788 144 q 397 -26 550 -26 q 116 68 226 -26 q 0 337 0 168 l 131 337 q 200 152 131 220 q 384 85 269 85 q 557 129 479 85 q 650 270 650 183 q 490 429 650 379 q 194 513 341 470 q 33 739 33 584 q 142 964 33 881 q 388 1041 242 1041 q 644 957 543 1041 q 756 716 756 867 l 625 716 q 561 874 625 816 q 395 933 497 933 q 243 891 309 933 q 164 759 164 841 q 325 609 164 656 q 625 526 475 568 q 788 291 788 454 "},"¦":{x_min:343,x_max:449,ha:792,o:"m 449 462 l 343 462 l 343 986 l 449 986 l 449 462 m 449 -242 l 343 -242 l 343 280 l 449 280 l 449 -242 "},"/":{x_min:183.25,x_max:608.328125,ha:792,o:"m 608 1041 l 266 -129 l 183 -129 l 520 1041 l 608 1041 "},"Τ":{x_min:-0.4375,x_max:777.453125,ha:839,o:"m 777 893 l 458 893 l 458 0 l 319 0 l 319 892 l 0 892 l 0 1013 l 777 1013 l 777 893 "},y:{x_min:0,x_max:684.78125,ha:771,o:"m 684 738 l 388 -83 q 311 -216 356 -167 q 173 -279 252 -279 q 97 -266 133 -279 l 97 -149 q 132 -155 109 -151 q 168 -160 155 -160 q 240 -114 213 -160 q 274 -26 248 -98 l 0 738 l 137 737 l 341 139 l 548 737 l 684 738 "},"Π":{x_min:0,x_max:803,ha:917,o:"m 803 0 l 667 0 l 667 886 l 140 886 l 140 0 l 0 0 l 0 1012 l 803 1012 l 803 0 "},"ΐ":{x_min:-111,x_max:339,ha:361,o:"m 339 800 l 229 800 l 229 925 l 339 925 l 339 800 m -1 800 l -111 800 l -111 925 l -1 925 l -1 800 m 284 3 q 233 -10 258 -5 q 182 -15 207 -15 q 85 26 119 -15 q 42 200 42 79 l 42 737 l 167 737 l 168 215 q 172 141 168 157 q 226 101 183 101 q 248 103 239 101 q 284 112 257 104 l 284 3 m 302 1040 l 113 819 l 30 819 l 165 1040 l 302 1040 "},g:{x_min:0,x_max:686,ha:838,o:"m 686 34 q 586 -213 686 -121 q 331 -306 487 -306 q 131 -252 216 -306 q 31 -84 31 -190 l 155 -84 q 228 -174 166 -138 q 345 -207 284 -207 q 514 -109 454 -207 q 564 89 564 -27 q 461 6 521 36 q 335 -23 401 -23 q 88 100 184 -23 q 0 370 0 215 q 87 634 0 522 q 330 758 183 758 q 457 728 398 758 q 564 644 515 699 l 564 737 l 686 737 l 686 34 m 582 367 q 529 560 582 481 q 358 652 468 652 q 189 561 250 652 q 135 369 135 482 q 189 176 135 255 q 361 85 251 85 q 529 176 468 85 q 582 367 582 255 "},"²":{x_min:0,x_max:442,ha:539,o:"m 442 383 l 0 383 q 91 566 0 492 q 260 668 176 617 q 354 798 354 727 q 315 875 354 845 q 227 905 277 905 q 136 869 173 905 q 99 761 99 833 l 14 761 q 82 922 14 864 q 232 974 141 974 q 379 926 316 974 q 442 797 442 878 q 351 635 442 704 q 183 539 321 611 q 92 455 92 491 l 442 455 l 442 383 "},"–":{x_min:0,x_max:705.5625,ha:803,o:"m 705 334 l 0 334 l 0 410 l 705 410 l 705 334 "},"Κ":{x_min:0,x_max:819.5625,ha:893,o:"m 819 0 l 650 0 l 294 509 l 139 356 l 139 0 l 0 0 l 0 1013 l 139 1013 l 139 526 l 626 1013 l 809 1013 l 395 600 l 819 0 "},"ƒ":{x_min:-46.265625,x_max:392,ha:513,o:"m 392 651 l 259 651 l 79 -279 l -46 -278 l 134 651 l 14 651 l 14 751 l 135 751 q 151 948 135 900 q 304 1041 185 1041 q 334 1040 319 1041 q 392 1034 348 1039 l 392 922 q 337 931 360 931 q 271 883 287 931 q 260 793 260 853 l 260 751 l 392 751 l 392 651 "},e:{x_min:0,x_max:714,ha:813,o:"m 714 326 l 140 326 q 200 157 140 227 q 359 87 260 87 q 488 130 431 87 q 561 245 545 174 l 697 245 q 577 48 670 123 q 358 -26 484 -26 q 97 85 195 -26 q 0 363 0 197 q 94 642 0 529 q 358 765 195 765 q 626 627 529 765 q 714 326 714 503 m 576 429 q 507 583 564 522 q 355 650 445 650 q 206 583 266 650 q 140 429 152 522 l 576 429 "},"ό":{x_min:0,x_max:712,ha:815,o:"m 356 -25 q 94 91 194 -25 q 0 368 0 202 q 92 642 0 533 q 356 761 192 761 q 617 644 517 761 q 712 368 712 533 q 619 91 712 201 q 356 -25 520 -25 m 356 85 q 527 175 465 85 q 583 369 583 255 q 528 562 583 484 q 356 651 466 651 q 189 560 250 651 q 135 369 135 481 q 187 177 135 257 q 356 85 250 85 m 576 1040 l 387 819 l 303 819 l 438 1040 l 576 1040 "},J:{x_min:0,x_max:588,ha:699,o:"m 588 279 q 287 -26 588 -26 q 58 73 126 -26 q 0 327 0 158 l 133 327 q 160 172 133 227 q 288 96 198 96 q 426 171 391 96 q 449 336 449 219 l 449 1013 l 588 1013 l 588 279 "},"»":{x_min:-1,x_max:503,ha:601,o:"m 503 302 l 280 136 l 281 256 l 429 373 l 281 486 l 280 608 l 503 440 l 503 302 m 221 302 l 0 136 l 0 255 l 145 372 l 0 486 l -1 608 l 221 440 l 221 302 "},"©":{x_min:-3,x_max:1008,ha:1106,o:"m 502 -7 q 123 151 263 -7 q -3 501 -3 294 q 123 851 -3 706 q 502 1011 263 1011 q 881 851 739 1011 q 1008 501 1008 708 q 883 151 1008 292 q 502 -7 744 -7 m 502 60 q 830 197 709 60 q 940 501 940 322 q 831 805 940 681 q 502 944 709 944 q 174 805 296 944 q 65 501 65 680 q 173 197 65 320 q 502 60 294 60 m 741 394 q 661 246 731 302 q 496 190 591 190 q 294 285 369 190 q 228 497 228 370 q 295 714 228 625 q 499 813 370 813 q 656 762 588 813 q 733 625 724 711 l 634 625 q 589 704 629 673 q 498 735 550 735 q 377 666 421 735 q 334 504 334 597 q 374 340 334 408 q 490 272 415 272 q 589 304 549 272 q 638 394 628 337 l 741 394 "},"ώ":{x_min:0,x_max:922,ha:1030,o:"m 687 1040 l 498 819 l 415 819 l 549 1040 l 687 1040 m 922 339 q 856 97 922 203 q 650 -26 780 -26 q 538 9 587 -26 q 461 103 489 44 q 387 12 436 46 q 277 -22 339 -22 q 69 97 147 -22 q 0 338 0 202 q 45 551 0 444 q 161 737 84 643 l 302 737 q 175 552 219 647 q 124 336 124 446 q 155 179 124 248 q 275 88 197 88 q 375 163 341 88 q 400 294 400 219 l 400 572 l 524 572 l 524 294 q 561 135 524 192 q 643 88 591 88 q 762 182 719 88 q 797 341 797 257 q 745 555 797 450 q 619 737 705 637 l 760 737 q 874 551 835 640 q 922 339 922 444 "},"^":{x_min:193.0625,x_max:598.609375,ha:792,o:"m 598 772 l 515 772 l 395 931 l 277 772 l 193 772 l 326 1013 l 462 1013 l 598 772 "},"«":{x_min:0,x_max:507.203125,ha:604,o:"m 506 136 l 284 302 l 284 440 l 506 608 l 507 485 l 360 371 l 506 255 l 506 136 m 222 136 l 0 302 l 0 440 l 222 608 l 221 486 l 73 373 l 222 256 l 222 136 "},D:{x_min:0,x_max:828,ha:935,o:"m 389 1013 q 714 867 593 1013 q 828 521 828 729 q 712 161 828 309 q 382 0 587 0 l 0 0 l 0 1013 l 389 1013 m 376 124 q 607 247 523 124 q 681 510 681 355 q 607 771 681 662 q 376 896 522 896 l 139 896 l 139 124 l 376 124 "},"∙":{x_min:0,x_max:142,ha:239,o:"m 142 585 l 0 585 l 0 738 l 142 738 l 142 585 "},"ÿ":{x_min:0,x_max:47,ha:125,o:"m 47 3 q 37 -7 47 -7 q 28 0 30 -7 q 39 -4 32 -4 q 45 3 45 -1 l 37 0 q 28 9 28 0 q 39 19 28 19 l 47 16 l 47 19 l 47 3 m 37 1 q 44 8 44 1 q 37 16 44 16 q 30 8 30 16 q 37 1 30 1 m 26 1 l 23 22 l 14 0 l 3 22 l 3 3 l 0 25 l 13 1 l 22 25 l 26 1 "},w:{x_min:0,x_max:1009.71875,ha:1100,o:"m 1009 738 l 783 0 l 658 0 l 501 567 l 345 0 l 222 0 l 0 738 l 130 738 l 284 174 l 432 737 l 576 738 l 721 173 l 881 737 l 1009 738 "},$:{x_min:0,x_max:700,ha:793,o:"m 664 717 l 542 717 q 490 825 531 785 q 381 872 450 865 l 381 551 q 620 446 540 522 q 700 241 700 370 q 618 45 700 116 q 381 -25 536 -25 l 381 -152 l 307 -152 l 307 -25 q 81 62 162 -25 q 0 297 0 149 l 124 297 q 169 146 124 204 q 307 81 215 89 l 307 441 q 80 536 148 469 q 13 725 13 603 q 96 910 13 839 q 307 982 180 982 l 307 1077 l 381 1077 l 381 982 q 574 917 494 982 q 664 717 664 845 m 307 565 l 307 872 q 187 831 233 872 q 142 724 142 791 q 180 618 142 656 q 307 565 218 580 m 381 76 q 562 237 562 96 q 517 361 562 313 q 381 423 472 409 l 381 76 "},"\\":{x_min:-0.015625,x_max:425.0625,ha:522,o:"m 425 -129 l 337 -129 l 0 1041 l 83 1041 l 425 -129 "},"µ":{x_min:0,x_max:697.21875,ha:747,o:"m 697 -4 q 629 -14 658 -14 q 498 97 513 -14 q 422 9 470 41 q 313 -23 374 -23 q 207 4 258 -23 q 119 81 156 32 l 119 -278 l 0 -278 l 0 738 l 124 738 l 124 343 q 165 173 124 246 q 308 83 216 83 q 452 178 402 83 q 493 359 493 255 l 493 738 l 617 738 l 617 214 q 623 136 617 160 q 673 92 637 92 q 697 96 684 92 l 697 -4 "},"Ι":{x_min:42,x_max:181,ha:297,o:"m 181 0 l 42 0 l 42 1013 l 181 1013 l 181 0 "},"Ύ":{x_min:0,x_max:1144.5,ha:1214,o:"m 1144 1012 l 807 416 l 807 0 l 667 0 l 667 416 l 325 1012 l 465 1012 l 736 533 l 1004 1012 l 1144 1012 m 277 1040 l 83 799 l 0 799 l 140 1040 l 277 1040 "},"’":{x_min:0,x_max:139,ha:236,o:"m 139 851 q 102 737 139 784 q 0 669 65 690 l 0 734 q 59 787 42 741 q 72 873 72 821 l 0 873 l 0 1013 l 139 1013 l 139 851 "},"Ν":{x_min:0,x_max:801,ha:915,o:"m 801 0 l 651 0 l 131 822 l 131 0 l 0 0 l 0 1013 l 151 1013 l 670 191 l 670 1013 l 801 1013 l 801 0 "},"-":{x_min:8.71875,x_max:350.390625,ha:478,o:"m 350 317 l 8 317 l 8 428 l 350 428 l 350 317 "},Q:{x_min:0,x_max:968,ha:1072,o:"m 954 5 l 887 -79 l 744 35 q 622 -11 687 2 q 483 -26 556 -26 q 127 130 262 -26 q 0 504 0 279 q 127 880 0 728 q 484 1041 262 1041 q 841 884 708 1041 q 968 507 968 735 q 933 293 968 398 q 832 104 899 188 l 954 5 m 723 191 q 802 330 777 248 q 828 499 828 412 q 744 790 828 673 q 483 922 650 922 q 228 791 322 922 q 142 505 142 673 q 227 221 142 337 q 487 91 323 91 q 632 123 566 91 l 520 215 l 587 301 l 723 191 "},"ς":{x_min:1,x_max:676.28125,ha:740,o:"m 676 460 l 551 460 q 498 595 542 546 q 365 651 448 651 q 199 578 263 651 q 136 401 136 505 q 266 178 136 241 q 508 106 387 142 q 640 -50 640 62 q 625 -158 640 -105 q 583 -278 611 -211 l 465 -278 q 498 -182 490 -211 q 515 -80 515 -126 q 381 12 515 -15 q 134 91 197 51 q 1 388 1 179 q 100 651 1 542 q 354 761 199 761 q 587 680 498 761 q 676 460 676 599 "},M:{x_min:0,x_max:954,ha:1067,o:"m 954 0 l 819 0 l 819 869 l 537 0 l 405 0 l 128 866 l 128 0 l 0 0 l 0 1013 l 200 1013 l 472 160 l 757 1013 l 954 1013 l 954 0 "},"Ψ":{x_min:0,x_max:1006,ha:1094,o:"m 1006 678 q 914 319 1006 429 q 571 200 814 200 l 571 0 l 433 0 l 433 200 q 92 319 194 200 q 0 678 0 429 l 0 1013 l 139 1013 l 139 679 q 191 417 139 492 q 433 326 255 326 l 433 1013 l 571 1013 l 571 326 l 580 326 q 813 423 747 326 q 868 679 868 502 l 868 1013 l 1006 1013 l 1006 678 "},C:{x_min:0,x_max:886,ha:944,o:"m 886 379 q 760 87 886 201 q 455 -26 634 -26 q 112 136 236 -26 q 0 509 0 283 q 118 882 0 737 q 469 1041 245 1041 q 748 955 630 1041 q 879 708 879 859 l 745 708 q 649 862 724 805 q 473 920 573 920 q 219 791 312 920 q 136 509 136 675 q 217 229 136 344 q 470 99 311 99 q 672 179 591 99 q 753 379 753 259 l 886 379 "},"!":{x_min:0,x_max:138,ha:236,o:"m 138 684 q 116 409 138 629 q 105 244 105 299 l 33 244 q 16 465 33 313 q 0 684 0 616 l 0 1013 l 138 1013 l 138 684 m 138 0 l 0 0 l 0 151 l 138 151 l 138 0 "},"{":{x_min:0,x_max:480.5625,ha:578,o:"m 480 -286 q 237 -213 303 -286 q 187 -45 187 -159 q 194 48 187 -15 q 201 141 201 112 q 164 264 201 225 q 0 314 118 314 l 0 417 q 164 471 119 417 q 201 605 201 514 q 199 665 201 644 q 193 772 193 769 q 241 941 193 887 q 480 1015 308 1015 l 480 915 q 336 866 375 915 q 306 742 306 828 q 310 662 306 717 q 314 577 314 606 q 288 452 314 500 q 176 365 256 391 q 289 275 257 337 q 314 143 314 226 q 313 84 314 107 q 310 -11 310 -5 q 339 -131 310 -94 q 480 -182 377 -182 l 480 -286 "},X:{x_min:-0.015625,x_max:854.15625,ha:940,o:"m 854 0 l 683 0 l 423 409 l 166 0 l 0 0 l 347 519 l 18 1013 l 186 1013 l 428 637 l 675 1013 l 836 1013 l 504 520 l 854 0 "},"#":{x_min:0,x_max:963.890625,ha:1061,o:"m 963 690 l 927 590 l 719 590 l 655 410 l 876 410 l 840 310 l 618 310 l 508 -3 l 393 -2 l 506 309 l 329 310 l 215 -2 l 102 -3 l 212 310 l 0 310 l 36 410 l 248 409 l 312 590 l 86 590 l 120 690 l 347 690 l 459 1006 l 573 1006 l 462 690 l 640 690 l 751 1006 l 865 1006 l 754 690 l 963 690 m 606 590 l 425 590 l 362 410 l 543 410 l 606 590 "},"ι":{x_min:42,x_max:284,ha:361,o:"m 284 3 q 233 -10 258 -5 q 182 -15 207 -15 q 85 26 119 -15 q 42 200 42 79 l 42 738 l 167 738 l 168 215 q 172 141 168 157 q 226 101 183 101 q 248 103 239 101 q 284 112 257 104 l 284 3 "},"Ά":{x_min:0,x_max:906.953125,ha:982,o:"m 283 1040 l 88 799 l 5 799 l 145 1040 l 283 1040 m 906 0 l 756 0 l 650 303 l 251 303 l 143 0 l 0 0 l 376 1012 l 529 1012 l 906 0 m 609 421 l 452 866 l 293 421 l 609 421 "},")":{x_min:0,x_max:318,ha:415,o:"m 318 365 q 257 25 318 191 q 87 -290 197 -141 l 0 -290 q 140 21 93 -128 q 193 360 193 189 q 141 704 193 537 q 0 1024 97 850 l 87 1024 q 257 706 197 871 q 318 365 318 542 "},"ε":{x_min:0,x_max:634.71875,ha:714,o:"m 634 234 q 527 38 634 110 q 300 -25 433 -25 q 98 29 183 -25 q 0 204 0 93 q 37 314 0 265 q 128 390 67 353 q 56 460 82 419 q 26 555 26 505 q 114 712 26 654 q 295 763 191 763 q 499 700 416 763 q 589 515 589 631 l 478 515 q 419 618 464 580 q 307 657 374 657 q 207 630 253 657 q 151 547 151 598 q 238 445 151 469 q 389 434 280 434 l 389 331 l 349 331 q 206 315 255 331 q 125 210 125 287 q 183 107 125 145 q 302 76 233 76 q 436 117 379 76 q 509 234 493 159 l 634 234 "},"Δ":{x_min:0,x_max:952.78125,ha:1028,o:"m 952 0 l 0 0 l 400 1013 l 551 1013 l 952 0 m 762 124 l 476 867 l 187 124 l 762 124 "},"}":{x_min:0,x_max:481,ha:578,o:"m 481 314 q 318 262 364 314 q 282 136 282 222 q 284 65 282 97 q 293 -58 293 -48 q 241 -217 293 -166 q 0 -286 174 -286 l 0 -182 q 143 -130 105 -182 q 171 -2 171 -93 q 168 81 171 22 q 165 144 165 140 q 188 275 165 229 q 306 365 220 339 q 191 455 224 391 q 165 588 165 505 q 168 681 165 624 q 171 742 171 737 q 141 865 171 827 q 0 915 102 915 l 0 1015 q 243 942 176 1015 q 293 773 293 888 q 287 675 293 741 q 282 590 282 608 q 318 466 282 505 q 481 417 364 417 l 481 314 "},"‰":{x_min:-3,x_max:1672,ha:1821,o:"m 846 0 q 664 76 732 0 q 603 244 603 145 q 662 412 603 344 q 846 489 729 489 q 1027 412 959 489 q 1089 244 1089 343 q 1029 76 1089 144 q 846 0 962 0 m 845 103 q 945 143 910 103 q 981 243 981 184 q 947 340 981 301 q 845 385 910 385 q 745 342 782 385 q 709 243 709 300 q 742 147 709 186 q 845 103 781 103 m 888 986 l 284 -25 l 199 -25 l 803 986 l 888 986 m 241 468 q 58 545 126 468 q -3 715 -3 615 q 56 881 -3 813 q 238 958 124 958 q 421 881 353 958 q 483 712 483 813 q 423 544 483 612 q 241 468 356 468 m 241 855 q 137 811 175 855 q 100 710 100 768 q 136 612 100 653 q 240 572 172 572 q 344 614 306 572 q 382 713 382 656 q 347 810 382 771 q 241 855 308 855 m 1428 0 q 1246 76 1314 0 q 1185 244 1185 145 q 1244 412 1185 344 q 1428 489 1311 489 q 1610 412 1542 489 q 1672 244 1672 343 q 1612 76 1672 144 q 1428 0 1545 0 m 1427 103 q 1528 143 1492 103 q 1564 243 1564 184 q 1530 340 1564 301 q 1427 385 1492 385 q 1327 342 1364 385 q 1291 243 1291 300 q 1324 147 1291 186 q 1427 103 1363 103 "},a:{x_min:0,x_max:698.609375,ha:794,o:"m 698 0 q 661 -12 679 -7 q 615 -17 643 -17 q 536 12 564 -17 q 500 96 508 41 q 384 6 456 37 q 236 -25 312 -25 q 65 31 130 -25 q 0 194 0 88 q 118 390 0 334 q 328 435 180 420 q 488 483 476 451 q 495 523 495 504 q 442 619 495 584 q 325 654 389 654 q 209 617 257 654 q 152 513 161 580 l 33 513 q 123 705 33 633 q 332 772 207 772 q 528 712 448 772 q 617 531 617 645 l 617 163 q 624 108 617 126 q 664 90 632 90 l 698 94 l 698 0 m 491 262 l 491 372 q 272 329 350 347 q 128 201 128 294 q 166 113 128 144 q 264 83 205 83 q 414 130 346 83 q 491 262 491 183 "},"—":{x_min:0,x_max:941.671875,ha:1039,o:"m 941 334 l 0 334 l 0 410 l 941 410 l 941 334 "},"=":{x_min:8.71875,x_max:780.953125,ha:792,o:"m 780 510 l 8 510 l 8 606 l 780 606 l 780 510 m 780 235 l 8 235 l 8 332 l 780 332 l 780 235 "},N:{x_min:0,x_max:801,ha:914,o:"m 801 0 l 651 0 l 131 823 l 131 0 l 0 0 l 0 1013 l 151 1013 l 670 193 l 670 1013 l 801 1013 l 801 0 "},"ρ":{x_min:0,x_max:712,ha:797,o:"m 712 369 q 620 94 712 207 q 362 -26 521 -26 q 230 2 292 -26 q 119 83 167 30 l 119 -278 l 0 -278 l 0 362 q 91 643 0 531 q 355 764 190 764 q 617 647 517 764 q 712 369 712 536 m 583 366 q 530 559 583 480 q 359 651 469 651 q 190 562 252 651 q 135 370 135 483 q 189 176 135 257 q 359 85 250 85 q 528 175 466 85 q 583 366 583 254 "},"¯":{x_min:0,x_max:941.671875,ha:938,o:"m 941 1033 l 0 1033 l 0 1109 l 941 1109 l 941 1033 "},Z:{x_min:0,x_max:779,ha:849,o:"m 779 0 l 0 0 l 0 113 l 621 896 l 40 896 l 40 1013 l 779 1013 l 778 887 l 171 124 l 779 124 l 779 0 "},u:{x_min:0,x_max:617,ha:729,o:"m 617 0 l 499 0 l 499 110 q 391 10 460 45 q 246 -25 322 -25 q 61 58 127 -25 q 0 258 0 136 l 0 738 l 125 738 l 125 284 q 156 148 125 202 q 273 82 197 82 q 433 165 369 82 q 493 340 493 243 l 493 738 l 617 738 l 617 0 "},k:{x_min:0,x_max:612.484375,ha:697,o:"m 612 738 l 338 465 l 608 0 l 469 0 l 251 382 l 121 251 l 121 0 l 0 0 l 0 1013 l 121 1013 l 121 402 l 456 738 l 612 738 "},"Η":{x_min:0,x_max:803,ha:917,o:"m 803 0 l 667 0 l 667 475 l 140 475 l 140 0 l 0 0 l 0 1013 l 140 1013 l 140 599 l 667 599 l 667 1013 l 803 1013 l 803 0 "},"Α":{x_min:0,x_max:906.953125,ha:985,o:"m 906 0 l 756 0 l 650 303 l 251 303 l 143 0 l 0 0 l 376 1013 l 529 1013 l 906 0 m 609 421 l 452 866 l 293 421 l 609 421 "},s:{x_min:0,x_max:604,ha:697,o:"m 604 217 q 501 36 604 104 q 292 -23 411 -23 q 86 43 166 -23 q 0 238 0 114 l 121 237 q 175 122 121 164 q 300 85 223 85 q 415 112 363 85 q 479 207 479 147 q 361 309 479 276 q 140 372 141 370 q 21 544 21 426 q 111 708 21 647 q 298 761 190 761 q 492 705 413 761 q 583 531 583 643 l 462 531 q 412 625 462 594 q 298 657 363 657 q 199 636 242 657 q 143 558 143 608 q 262 454 143 486 q 484 394 479 397 q 604 217 604 341 "},B:{x_min:0,x_max:778,ha:876,o:"m 580 546 q 724 469 670 535 q 778 311 778 403 q 673 83 778 171 q 432 0 575 0 l 0 0 l 0 1013 l 411 1013 q 629 957 541 1013 q 732 768 732 892 q 691 633 732 693 q 580 546 650 572 m 393 899 l 139 899 l 139 588 l 379 588 q 521 624 462 588 q 592 744 592 667 q 531 859 592 819 q 393 899 471 899 m 419 124 q 566 169 504 124 q 635 303 635 219 q 559 436 635 389 q 402 477 494 477 l 139 477 l 139 124 l 419 124 "},"…":{x_min:0,x_max:614,ha:708,o:"m 142 0 l 0 0 l 0 151 l 142 151 l 142 0 m 378 0 l 236 0 l 236 151 l 378 151 l 378 0 m 614 0 l 472 0 l 472 151 l 614 151 l 614 0 "},"?":{x_min:0,x_max:607,ha:704,o:"m 607 777 q 543 599 607 674 q 422 474 482 537 q 357 272 357 391 l 236 272 q 297 487 236 395 q 411 619 298 490 q 474 762 474 691 q 422 885 474 838 q 301 933 371 933 q 179 880 228 933 q 124 706 124 819 l 0 706 q 94 963 0 872 q 302 1044 177 1044 q 511 973 423 1044 q 607 777 607 895 m 370 0 l 230 0 l 230 151 l 370 151 l 370 0 "},H:{x_min:0,x_max:803,ha:915,o:"m 803 0 l 667 0 l 667 475 l 140 475 l 140 0 l 0 0 l 0 1013 l 140 1013 l 140 599 l 667 599 l 667 1013 l 803 1013 l 803 0 "},"ν":{x_min:0,x_max:675,ha:761,o:"m 675 738 l 404 0 l 272 0 l 0 738 l 133 738 l 340 147 l 541 738 l 675 738 "},c:{x_min:1,x_max:701.390625,ha:775,o:"m 701 264 q 584 53 681 133 q 353 -26 487 -26 q 91 91 188 -26 q 1 370 1 201 q 92 645 1 537 q 353 761 190 761 q 572 688 479 761 q 690 493 666 615 l 556 493 q 487 606 545 562 q 356 650 428 650 q 186 563 246 650 q 134 372 134 487 q 188 179 134 258 q 359 88 250 88 q 492 136 437 88 q 566 264 548 185 l 701 264 "},"¶":{x_min:0,x_max:566.671875,ha:678,o:"m 21 892 l 52 892 l 98 761 l 145 892 l 176 892 l 178 741 l 157 741 l 157 867 l 108 741 l 88 741 l 40 871 l 40 741 l 21 741 l 21 892 m 308 854 l 308 731 q 252 691 308 691 q 227 691 240 691 q 207 696 213 695 l 207 712 l 253 706 q 288 733 288 706 l 288 763 q 244 741 279 741 q 193 797 193 741 q 261 860 193 860 q 287 860 273 860 q 308 854 302 855 m 288 842 l 263 843 q 213 796 213 843 q 248 756 213 756 q 288 796 288 756 l 288 842 m 566 988 l 502 988 l 502 -1 l 439 -1 l 439 988 l 317 988 l 317 -1 l 252 -1 l 252 602 q 81 653 155 602 q 0 805 0 711 q 101 989 0 918 q 309 1053 194 1053 l 566 1053 l 566 988 "},"β":{x_min:0,x_max:660,ha:745,o:"m 471 550 q 610 450 561 522 q 660 280 660 378 q 578 64 660 151 q 367 -22 497 -22 q 239 5 299 -22 q 126 82 178 32 l 126 -278 l 0 -278 l 0 593 q 54 903 0 801 q 318 1042 127 1042 q 519 964 436 1042 q 603 771 603 887 q 567 644 603 701 q 471 550 532 586 m 337 79 q 476 138 418 79 q 535 279 535 198 q 427 437 535 386 q 226 477 344 477 l 226 583 q 398 620 329 583 q 486 762 486 668 q 435 884 486 833 q 312 935 384 935 q 169 861 219 935 q 126 698 126 797 l 126 362 q 170 169 126 242 q 337 79 224 79 "},"Μ":{x_min:0,x_max:954,ha:1068,o:"m 954 0 l 819 0 l 819 868 l 537 0 l 405 0 l 128 865 l 128 0 l 0 0 l 0 1013 l 199 1013 l 472 158 l 758 1013 l 954 1013 l 954 0 "},"Ό":{x_min:0.109375,x_max:1120,ha:1217,o:"m 1120 505 q 994 132 1120 282 q 642 -29 861 -29 q 290 130 422 -29 q 167 505 167 280 q 294 883 167 730 q 650 1046 430 1046 q 999 882 868 1046 q 1120 505 1120 730 m 977 504 q 896 784 977 669 q 644 915 804 915 q 391 785 484 915 q 307 504 307 669 q 391 224 307 339 q 644 95 486 95 q 894 224 803 95 q 977 504 977 339 m 277 1040 l 83 799 l 0 799 l 140 1040 l 277 1040 "},"Ή":{x_min:0,x_max:1158,ha:1275,o:"m 1158 0 l 1022 0 l 1022 475 l 496 475 l 496 0 l 356 0 l 356 1012 l 496 1012 l 496 599 l 1022 599 l 1022 1012 l 1158 1012 l 1158 0 m 277 1040 l 83 799 l 0 799 l 140 1040 l 277 1040 "},"•":{x_min:0,x_max:663.890625,ha:775,o:"m 663 529 q 566 293 663 391 q 331 196 469 196 q 97 294 194 196 q 0 529 0 393 q 96 763 0 665 q 331 861 193 861 q 566 763 469 861 q 663 529 663 665 "},"¥":{x_min:0.1875,x_max:819.546875,ha:886,o:"m 563 561 l 697 561 l 696 487 l 520 487 l 482 416 l 482 380 l 697 380 l 695 308 l 482 308 l 482 0 l 342 0 l 342 308 l 125 308 l 125 380 l 342 380 l 342 417 l 303 487 l 125 487 l 125 561 l 258 561 l 0 1013 l 140 1013 l 411 533 l 679 1013 l 819 1013 l 563 561 "},"(":{x_min:0,x_max:318.0625,ha:415,o:"m 318 -290 l 230 -290 q 61 23 122 -142 q 0 365 0 190 q 62 712 0 540 q 230 1024 119 869 l 318 1024 q 175 705 219 853 q 125 360 125 542 q 176 22 125 187 q 318 -290 223 -127 "},U:{x_min:0,x_max:796,ha:904,o:"m 796 393 q 681 93 796 212 q 386 -25 566 -25 q 101 95 208 -25 q 0 393 0 211 l 0 1013 l 138 1013 l 138 391 q 204 191 138 270 q 394 107 276 107 q 586 191 512 107 q 656 391 656 270 l 656 1013 l 796 1013 l 796 393 "},"γ":{x_min:0.5,x_max:744.953125,ha:822,o:"m 744 737 l 463 54 l 463 -278 l 338 -278 l 338 54 l 154 495 q 104 597 124 569 q 13 651 67 651 l 0 651 l 0 751 l 39 753 q 168 711 121 753 q 242 594 207 676 l 403 208 l 617 737 l 744 737 "},"α":{x_min:0,x_max:765.5625,ha:809,o:"m 765 -4 q 698 -14 726 -14 q 564 97 586 -14 q 466 7 525 40 q 337 -26 407 -26 q 88 98 186 -26 q 0 369 0 212 q 88 637 0 525 q 337 760 184 760 q 465 728 407 760 q 563 637 524 696 l 563 739 l 685 739 l 685 222 q 693 141 685 168 q 748 94 708 94 q 765 96 760 94 l 765 -4 m 584 371 q 531 562 584 485 q 360 653 470 653 q 192 566 254 653 q 135 379 135 489 q 186 181 135 261 q 358 84 247 84 q 528 176 465 84 q 584 371 584 260 "},F:{x_min:0,x_max:683.328125,ha:717,o:"m 683 888 l 140 888 l 140 583 l 613 583 l 613 458 l 140 458 l 140 0 l 0 0 l 0 1013 l 683 1013 l 683 888 "},"­":{x_min:0,x_max:705.5625,ha:803,o:"m 705 334 l 0 334 l 0 410 l 705 410 l 705 334 "},":":{x_min:0,x_max:142,ha:239,o:"m 142 585 l 0 585 l 0 738 l 142 738 l 142 585 m 142 0 l 0 0 l 0 151 l 142 151 l 142 0 "},"Χ":{x_min:0,x_max:854.171875,ha:935,o:"m 854 0 l 683 0 l 423 409 l 166 0 l 0 0 l 347 519 l 18 1013 l 186 1013 l 427 637 l 675 1013 l 836 1013 l 504 521 l 854 0 "},"*":{x_min:116,x_max:674,ha:792,o:"m 674 768 l 475 713 l 610 544 l 517 477 l 394 652 l 272 478 l 178 544 l 314 713 l 116 766 l 153 876 l 341 812 l 342 1013 l 446 1013 l 446 811 l 635 874 l 674 768 "},"†":{x_min:0,x_max:777,ha:835,o:"m 458 804 l 777 804 l 777 683 l 458 683 l 458 0 l 319 0 l 319 681 l 0 683 l 0 804 l 319 804 l 319 1015 l 458 1013 l 458 804 "},"°":{x_min:0,x_max:347,ha:444,o:"m 173 802 q 43 856 91 802 q 0 977 0 905 q 45 1101 0 1049 q 173 1153 90 1153 q 303 1098 255 1153 q 347 977 347 1049 q 303 856 347 905 q 173 802 256 802 m 173 884 q 238 910 214 884 q 262 973 262 937 q 239 1038 262 1012 q 173 1064 217 1064 q 108 1037 132 1064 q 85 973 85 1010 q 108 910 85 937 q 173 884 132 884 "},V:{x_min:0,x_max:862.71875,ha:940,o:"m 862 1013 l 505 0 l 361 0 l 0 1013 l 143 1013 l 434 165 l 718 1012 l 862 1013 "},"Ξ":{x_min:0,x_max:734.71875,ha:763,o:"m 723 889 l 9 889 l 9 1013 l 723 1013 l 723 889 m 673 463 l 61 463 l 61 589 l 673 589 l 673 463 m 734 0 l 0 0 l 0 124 l 734 124 l 734 0 "}," ":{x_min:0,x_max:0,ha:853},"Ϋ":{x_min:0.328125,x_max:819.515625,ha:889,o:"m 588 1046 l 460 1046 l 460 1189 l 588 1189 l 588 1046 m 360 1046 l 232 1046 l 232 1189 l 360 1189 l 360 1046 m 819 1012 l 482 416 l 482 0 l 342 0 l 342 416 l 0 1012 l 140 1012 l 411 533 l 679 1012 l 819 1012 "},"”":{x_min:0,x_max:347,ha:454,o:"m 139 851 q 102 737 139 784 q 0 669 65 690 l 0 734 q 59 787 42 741 q 72 873 72 821 l 0 873 l 0 1013 l 139 1013 l 139 851 m 347 851 q 310 737 347 784 q 208 669 273 690 l 208 734 q 267 787 250 741 q 280 873 280 821 l 208 873 l 208 1013 l 347 1013 l 347 851 "},"@":{x_min:0,x_max:1260,ha:1357,o:"m 1098 -45 q 877 -160 1001 -117 q 633 -203 752 -203 q 155 -29 327 -203 q 0 360 0 127 q 176 802 0 616 q 687 1008 372 1008 q 1123 854 969 1008 q 1260 517 1260 718 q 1155 216 1260 341 q 868 82 1044 82 q 772 106 801 82 q 737 202 737 135 q 647 113 700 144 q 527 82 594 82 q 367 147 420 82 q 314 312 314 212 q 401 565 314 452 q 639 690 498 690 q 810 588 760 690 l 849 668 l 938 668 q 877 441 900 532 q 833 226 833 268 q 853 182 833 198 q 902 167 873 167 q 1088 272 1012 167 q 1159 512 1159 372 q 1051 793 1159 681 q 687 925 925 925 q 248 747 415 925 q 97 361 97 586 q 226 26 97 159 q 627 -122 370 -122 q 856 -87 737 -122 q 1061 8 976 -53 l 1098 -45 m 786 488 q 738 580 777 545 q 643 615 700 615 q 483 517 548 615 q 425 322 425 430 q 457 203 425 250 q 552 156 490 156 q 722 273 665 156 q 786 488 738 309 "},"Ί":{x_min:0,x_max:499,ha:613,o:"m 277 1040 l 83 799 l 0 799 l 140 1040 l 277 1040 m 499 0 l 360 0 l 360 1012 l 499 1012 l 499 0 "},i:{x_min:14,x_max:136,ha:275,o:"m 136 873 l 14 873 l 14 1013 l 136 1013 l 136 873 m 136 0 l 14 0 l 14 737 l 136 737 l 136 0 "},"Β":{x_min:0,x_max:778,ha:877,o:"m 580 545 q 724 468 671 534 q 778 310 778 402 q 673 83 778 170 q 432 0 575 0 l 0 0 l 0 1013 l 411 1013 q 629 957 541 1013 q 732 768 732 891 q 691 632 732 692 q 580 545 650 571 m 393 899 l 139 899 l 139 587 l 379 587 q 521 623 462 587 q 592 744 592 666 q 531 859 592 819 q 393 899 471 899 m 419 124 q 566 169 504 124 q 635 302 635 219 q 559 435 635 388 q 402 476 494 476 l 139 476 l 139 124 l 419 124 "},"υ":{x_min:0,x_max:617,ha:725,o:"m 617 352 q 540 94 617 199 q 308 -24 455 -24 q 76 94 161 -24 q 0 352 0 199 l 0 739 l 126 739 l 126 355 q 169 185 126 257 q 312 98 220 98 q 451 185 402 98 q 492 355 492 257 l 492 739 l 617 739 l 617 352 "},"]":{x_min:0,x_max:275,ha:372,o:"m 275 -281 l 0 -281 l 0 -187 l 151 -187 l 151 920 l 0 920 l 0 1013 l 275 1013 l 275 -281 "},m:{x_min:0,x_max:1019,ha:1128,o:"m 1019 0 l 897 0 l 897 454 q 860 591 897 536 q 739 660 816 660 q 613 586 659 660 q 573 436 573 522 l 573 0 l 447 0 l 447 455 q 412 591 447 535 q 294 657 372 657 q 165 586 213 657 q 122 437 122 521 l 122 0 l 0 0 l 0 738 l 117 738 l 117 640 q 202 730 150 697 q 316 763 254 763 q 437 730 381 763 q 525 642 494 697 q 621 731 559 700 q 753 763 682 763 q 943 694 867 763 q 1019 512 1019 625 l 1019 0 "},"χ":{x_min:8.328125,x_max:780.5625,ha:815,o:"m 780 -278 q 715 -294 747 -294 q 616 -257 663 -294 q 548 -175 576 -227 l 379 133 l 143 -277 l 9 -277 l 313 254 l 163 522 q 127 586 131 580 q 36 640 91 640 q 8 637 27 640 l 8 752 l 52 757 q 162 719 113 757 q 236 627 200 690 l 383 372 l 594 737 l 726 737 l 448 250 l 625 -69 q 670 -153 647 -110 q 743 -188 695 -188 q 780 -184 759 -188 l 780 -278 "},"ί":{x_min:42,x_max:326.71875,ha:361,o:"m 284 3 q 233 -10 258 -5 q 182 -15 207 -15 q 85 26 119 -15 q 42 200 42 79 l 42 737 l 167 737 l 168 215 q 172 141 168 157 q 226 101 183 101 q 248 102 239 101 q 284 112 257 104 l 284 3 m 326 1040 l 137 819 l 54 819 l 189 1040 l 326 1040 "},"Ζ":{x_min:0,x_max:779.171875,ha:850,o:"m 779 0 l 0 0 l 0 113 l 620 896 l 40 896 l 40 1013 l 779 1013 l 779 887 l 170 124 l 779 124 l 779 0 "},R:{x_min:0,x_max:781.953125,ha:907,o:"m 781 0 l 623 0 q 587 242 590 52 q 407 433 585 433 l 138 433 l 138 0 l 0 0 l 0 1013 l 396 1013 q 636 946 539 1013 q 749 731 749 868 q 711 597 749 659 q 608 502 674 534 q 718 370 696 474 q 729 207 722 352 q 781 26 736 62 l 781 0 m 373 551 q 533 594 465 551 q 614 731 614 645 q 532 859 614 815 q 373 896 465 896 l 138 896 l 138 551 l 373 551 "},o:{x_min:0,x_max:713,ha:821,o:"m 357 -25 q 94 91 194 -25 q 0 368 0 202 q 93 642 0 533 q 357 761 193 761 q 618 644 518 761 q 713 368 713 533 q 619 91 713 201 q 357 -25 521 -25 m 357 85 q 528 175 465 85 q 584 369 584 255 q 529 562 584 484 q 357 651 467 651 q 189 560 250 651 q 135 369 135 481 q 187 177 135 257 q 357 85 250 85 "},K:{x_min:0,x_max:819.46875,ha:906,o:"m 819 0 l 649 0 l 294 509 l 139 355 l 139 0 l 0 0 l 0 1013 l 139 1013 l 139 526 l 626 1013 l 809 1013 l 395 600 l 819 0 "},",":{x_min:0,x_max:142,ha:239,o:"m 142 -12 q 105 -132 142 -82 q 0 -205 68 -182 l 0 -138 q 57 -82 40 -124 q 70 0 70 -51 l 0 0 l 0 151 l 142 151 l 142 -12 "},d:{x_min:0,x_max:683,ha:796,o:"m 683 0 l 564 0 l 564 93 q 456 6 516 38 q 327 -25 395 -25 q 87 100 181 -25 q 0 365 0 215 q 90 639 0 525 q 343 763 187 763 q 564 647 486 763 l 564 1013 l 683 1013 l 683 0 m 582 373 q 529 562 582 484 q 361 653 468 653 q 190 561 253 653 q 135 365 135 479 q 189 175 135 254 q 358 85 251 85 q 529 178 468 85 q 582 373 582 258 "},"¨":{x_min:-109,x_max:247,ha:232,o:"m 247 1046 l 119 1046 l 119 1189 l 247 1189 l 247 1046 m 19 1046 l -109 1046 l -109 1189 l 19 1189 l 19 1046 "},E:{x_min:0,x_max:736.109375,ha:789,o:"m 736 0 l 0 0 l 0 1013 l 725 1013 l 725 889 l 139 889 l 139 585 l 677 585 l 677 467 l 139 467 l 139 125 l 736 125 l 736 0 "},Y:{x_min:0,x_max:820,ha:886,o:"m 820 1013 l 482 416 l 482 0 l 342 0 l 342 416 l 0 1013 l 140 1013 l 411 534 l 679 1012 l 820 1013 "},"\"":{x_min:0,x_max:299,ha:396,o:"m 299 606 l 203 606 l 203 988 l 299 988 l 299 606 m 96 606 l 0 606 l 0 988 l 96 988 l 96 606 "},"‹":{x_min:17.984375,x_max:773.609375,ha:792,o:"m 773 40 l 18 376 l 17 465 l 773 799 l 773 692 l 159 420 l 773 149 l 773 40 "},"„":{x_min:0,x_max:364,ha:467,o:"m 141 -12 q 104 -132 141 -82 q 0 -205 67 -182 l 0 -138 q 56 -82 40 -124 q 69 0 69 -51 l 0 0 l 0 151 l 141 151 l 141 -12 m 364 -12 q 327 -132 364 -82 q 222 -205 290 -182 l 222 -138 q 279 -82 262 -124 q 292 0 292 -51 l 222 0 l 222 151 l 364 151 l 364 -12 "},"δ":{x_min:1,x_max:710,ha:810,o:"m 710 360 q 616 87 710 196 q 356 -28 518 -28 q 99 82 197 -28 q 1 356 1 192 q 100 606 1 509 q 355 703 199 703 q 180 829 288 754 q 70 903 124 866 l 70 1012 l 643 1012 l 643 901 l 258 901 q 462 763 422 794 q 636 592 577 677 q 710 360 710 485 m 584 365 q 552 501 584 447 q 451 602 521 555 q 372 611 411 611 q 197 541 258 611 q 136 355 136 472 q 190 171 136 245 q 358 85 252 85 q 528 173 465 85 q 584 365 584 252 "},"έ":{x_min:0,x_max:634.71875,ha:714,o:"m 634 234 q 527 38 634 110 q 300 -25 433 -25 q 98 29 183 -25 q 0 204 0 93 q 37 313 0 265 q 128 390 67 352 q 56 459 82 419 q 26 555 26 505 q 114 712 26 654 q 295 763 191 763 q 499 700 416 763 q 589 515 589 631 l 478 515 q 419 618 464 580 q 307 657 374 657 q 207 630 253 657 q 151 547 151 598 q 238 445 151 469 q 389 434 280 434 l 389 331 l 349 331 q 206 315 255 331 q 125 210 125 287 q 183 107 125 145 q 302 76 233 76 q 436 117 379 76 q 509 234 493 159 l 634 234 m 520 1040 l 331 819 l 248 819 l 383 1040 l 520 1040 "},"ω":{x_min:0,x_max:922,ha:1031,o:"m 922 339 q 856 97 922 203 q 650 -26 780 -26 q 538 9 587 -26 q 461 103 489 44 q 387 12 436 46 q 277 -22 339 -22 q 69 97 147 -22 q 0 339 0 203 q 45 551 0 444 q 161 738 84 643 l 302 738 q 175 553 219 647 q 124 336 124 446 q 155 179 124 249 q 275 88 197 88 q 375 163 341 88 q 400 294 400 219 l 400 572 l 524 572 l 524 294 q 561 135 524 192 q 643 88 591 88 q 762 182 719 88 q 797 342 797 257 q 745 556 797 450 q 619 738 705 638 l 760 738 q 874 551 835 640 q 922 339 922 444 "},"´":{x_min:0,x_max:96,ha:251,o:"m 96 606 l 0 606 l 0 988 l 96 988 l 96 606 "},"±":{x_min:11,x_max:781,ha:792,o:"m 781 490 l 446 490 l 446 255 l 349 255 l 349 490 l 11 490 l 11 586 l 349 586 l 349 819 l 446 819 l 446 586 l 781 586 l 781 490 m 781 21 l 11 21 l 11 115 l 781 115 l 781 21 "},"|":{x_min:343,x_max:449,ha:792,o:"m 449 462 l 343 462 l 343 986 l 449 986 l 449 462 m 449 -242 l 343 -242 l 343 280 l 449 280 l 449 -242 "},"ϋ":{x_min:0,x_max:617,ha:725,o:"m 482 800 l 372 800 l 372 925 l 482 925 l 482 800 m 239 800 l 129 800 l 129 925 l 239 925 l 239 800 m 617 352 q 540 93 617 199 q 308 -24 455 -24 q 76 93 161 -24 q 0 352 0 199 l 0 738 l 126 738 l 126 354 q 169 185 126 257 q 312 98 220 98 q 451 185 402 98 q 492 354 492 257 l 492 738 l 617 738 l 617 352 "},"§":{x_min:0,x_max:593,ha:690,o:"m 593 425 q 554 312 593 369 q 467 233 516 254 q 537 83 537 172 q 459 -74 537 -12 q 288 -133 387 -133 q 115 -69 184 -133 q 47 96 47 -6 l 166 96 q 199 7 166 40 q 288 -26 232 -26 q 371 -5 332 -26 q 420 60 420 21 q 311 201 420 139 q 108 309 210 255 q 0 490 0 383 q 33 602 0 551 q 124 687 66 654 q 75 743 93 712 q 58 812 58 773 q 133 984 58 920 q 300 1043 201 1043 q 458 987 394 1043 q 529 814 529 925 l 411 814 q 370 908 404 877 q 289 939 336 939 q 213 911 246 939 q 180 841 180 883 q 286 720 180 779 q 484 612 480 615 q 593 425 593 534 m 467 409 q 355 544 467 473 q 196 630 228 612 q 146 587 162 609 q 124 525 124 558 q 239 387 124 462 q 398 298 369 315 q 448 345 429 316 q 467 409 467 375 "},b:{x_min:0,x_max:685,ha:783,o:"m 685 372 q 597 99 685 213 q 347 -25 501 -25 q 219 5 277 -25 q 121 93 161 36 l 121 0 l 0 0 l 0 1013 l 121 1013 l 121 634 q 214 723 157 692 q 341 754 272 754 q 591 637 493 754 q 685 372 685 526 m 554 356 q 499 550 554 470 q 328 644 437 644 q 162 556 223 644 q 108 369 108 478 q 160 176 108 256 q 330 83 221 83 q 498 169 435 83 q 554 356 554 245 "},q:{x_min:0,x_max:683,ha:876,o:"m 683 -278 l 564 -278 l 564 97 q 474 8 533 39 q 345 -23 415 -23 q 91 93 188 -23 q 0 364 0 203 q 87 635 0 522 q 337 760 184 760 q 466 727 408 760 q 564 637 523 695 l 564 737 l 683 737 l 683 -278 m 582 375 q 527 564 582 488 q 358 652 466 652 q 190 565 253 652 q 135 377 135 488 q 189 179 135 261 q 361 84 251 84 q 530 179 469 84 q 582 375 582 260 "},"Ω":{x_min:-0.171875,x_max:969.5625,ha:1068,o:"m 969 0 l 555 0 l 555 123 q 744 308 675 194 q 814 558 814 423 q 726 812 814 709 q 484 922 633 922 q 244 820 334 922 q 154 567 154 719 q 223 316 154 433 q 412 123 292 199 l 412 0 l 0 0 l 0 124 l 217 124 q 68 327 122 210 q 15 572 15 444 q 144 911 15 781 q 484 1041 274 1041 q 822 909 691 1041 q 953 569 953 777 q 899 326 953 443 q 750 124 846 210 l 969 124 l 969 0 "},"ύ":{x_min:0,x_max:617,ha:725,o:"m 617 352 q 540 93 617 199 q 308 -24 455 -24 q 76 93 161 -24 q 0 352 0 199 l 0 738 l 126 738 l 126 354 q 169 185 126 257 q 312 98 220 98 q 451 185 402 98 q 492 354 492 257 l 492 738 l 617 738 l 617 352 m 535 1040 l 346 819 l 262 819 l 397 1040 l 535 1040 "},z:{x_min:-0.015625,x_max:613.890625,ha:697,o:"m 613 0 l 0 0 l 0 100 l 433 630 l 20 630 l 20 738 l 594 738 l 593 636 l 163 110 l 613 110 l 613 0 "},"™":{x_min:0,x_max:894,ha:1000,o:"m 389 951 l 229 951 l 229 503 l 160 503 l 160 951 l 0 951 l 0 1011 l 389 1011 l 389 951 m 894 503 l 827 503 l 827 939 l 685 503 l 620 503 l 481 937 l 481 503 l 417 503 l 417 1011 l 517 1011 l 653 580 l 796 1010 l 894 1011 l 894 503 "},"ή":{x_min:0.78125,x_max:697,ha:810,o:"m 697 -278 l 572 -278 l 572 454 q 540 587 572 536 q 425 650 501 650 q 271 579 337 650 q 206 420 206 509 l 206 0 l 81 0 l 81 489 q 73 588 81 562 q 0 644 56 644 l 0 741 q 68 755 38 755 q 158 721 124 755 q 200 630 193 687 q 297 726 234 692 q 434 761 359 761 q 620 692 544 761 q 697 516 697 624 l 697 -278 m 479 1040 l 290 819 l 207 819 l 341 1040 l 479 1040 "},"Θ":{x_min:0,x_max:960,ha:1056,o:"m 960 507 q 833 129 960 280 q 476 -32 698 -32 q 123 129 255 -32 q 0 507 0 280 q 123 883 0 732 q 476 1045 255 1045 q 832 883 696 1045 q 960 507 960 732 m 817 500 q 733 789 817 669 q 476 924 639 924 q 223 792 317 924 q 142 507 142 675 q 222 222 142 339 q 476 89 315 89 q 730 218 636 89 q 817 500 817 334 m 716 449 l 243 449 l 243 571 l 716 571 l 716 449 "},"®":{x_min:-3,x_max:1008,ha:1106,o:"m 503 532 q 614 562 566 532 q 672 658 672 598 q 614 747 672 716 q 503 772 569 772 l 338 772 l 338 532 l 503 532 m 502 -7 q 123 151 263 -7 q -3 501 -3 294 q 123 851 -3 706 q 502 1011 263 1011 q 881 851 739 1011 q 1008 501 1008 708 q 883 151 1008 292 q 502 -7 744 -7 m 502 60 q 830 197 709 60 q 940 501 940 322 q 831 805 940 681 q 502 944 709 944 q 174 805 296 944 q 65 501 65 680 q 173 197 65 320 q 502 60 294 60 m 788 146 l 678 146 q 653 316 655 183 q 527 449 652 449 l 338 449 l 338 146 l 241 146 l 241 854 l 518 854 q 688 808 621 854 q 766 658 766 755 q 739 563 766 607 q 668 497 713 519 q 751 331 747 472 q 788 164 756 190 l 788 146 "},"~":{x_min:0,x_max:833,ha:931,o:"m 833 958 q 778 753 833 831 q 594 665 716 665 q 402 761 502 665 q 240 857 302 857 q 131 795 166 857 q 104 665 104 745 l 0 665 q 54 867 0 789 q 237 958 116 958 q 429 861 331 958 q 594 765 527 765 q 704 827 670 765 q 729 958 729 874 l 833 958 "},"Ε":{x_min:0,x_max:736.21875,ha:778,o:"m 736 0 l 0 0 l 0 1013 l 725 1013 l 725 889 l 139 889 l 139 585 l 677 585 l 677 467 l 139 467 l 139 125 l 736 125 l 736 0 "},"³":{x_min:0,x_max:450,ha:547,o:"m 450 552 q 379 413 450 464 q 220 366 313 366 q 69 414 130 366 q 0 567 0 470 l 85 567 q 126 470 85 504 q 225 437 168 437 q 320 467 280 437 q 360 552 360 498 q 318 632 360 608 q 213 657 276 657 q 195 657 203 657 q 176 657 181 657 l 176 722 q 279 733 249 722 q 334 815 334 752 q 300 881 334 856 q 220 907 267 907 q 133 875 169 907 q 97 781 97 844 l 15 781 q 78 926 15 875 q 220 972 135 972 q 364 930 303 972 q 426 817 426 888 q 344 697 426 733 q 421 642 392 681 q 450 552 450 603 "},"[":{x_min:0,x_max:273.609375,ha:371,o:"m 273 -281 l 0 -281 l 0 1013 l 273 1013 l 273 920 l 124 920 l 124 -187 l 273 -187 l 273 -281 "},L:{x_min:0,x_max:645.828125,ha:696,o:"m 645 0 l 0 0 l 0 1013 l 140 1013 l 140 126 l 645 126 l 645 0 "},"σ":{x_min:0,x_max:803.390625,ha:894,o:"m 803 628 l 633 628 q 713 368 713 512 q 618 93 713 204 q 357 -25 518 -25 q 94 91 194 -25 q 0 368 0 201 q 94 644 0 533 q 356 761 194 761 q 481 750 398 761 q 608 739 564 739 l 803 739 l 803 628 m 360 85 q 529 180 467 85 q 584 374 584 262 q 527 566 584 490 q 352 651 463 651 q 187 559 247 651 q 135 368 135 478 q 189 175 135 254 q 360 85 251 85 "},"ζ":{x_min:0,x_max:573,ha:642,o:"m 573 -40 q 553 -162 573 -97 q 510 -278 543 -193 l 400 -278 q 441 -187 428 -219 q 462 -90 462 -132 q 378 -14 462 -14 q 108 45 197 -14 q 0 290 0 117 q 108 631 0 462 q 353 901 194 767 l 55 901 l 55 1012 l 561 1012 l 561 924 q 261 669 382 831 q 128 301 128 489 q 243 117 128 149 q 458 98 350 108 q 573 -40 573 80 "},"θ":{x_min:0,x_max:674,ha:778,o:"m 674 496 q 601 160 674 304 q 336 -26 508 -26 q 73 153 165 -26 q 0 485 0 296 q 72 840 0 683 q 343 1045 166 1045 q 605 844 516 1045 q 674 496 674 692 m 546 579 q 498 798 546 691 q 336 935 437 935 q 178 798 237 935 q 126 579 137 701 l 546 579 m 546 475 l 126 475 q 170 233 126 348 q 338 80 230 80 q 504 233 447 80 q 546 475 546 346 "},"Ο":{x_min:0,x_max:958,ha:1054,o:"m 485 1042 q 834 883 703 1042 q 958 511 958 735 q 834 136 958 287 q 481 -26 701 -26 q 126 130 261 -26 q 0 504 0 279 q 127 880 0 729 q 485 1042 263 1042 m 480 98 q 731 225 638 98 q 815 504 815 340 q 733 783 815 670 q 480 913 640 913 q 226 785 321 913 q 142 504 142 671 q 226 224 142 339 q 480 98 319 98 "},"Γ":{x_min:0,x_max:705.28125,ha:749,o:"m 705 886 l 140 886 l 140 0 l 0 0 l 0 1012 l 705 1012 l 705 886 "}," ":{x_min:0,x_max:0,ha:375},"%":{x_min:-3,x_max:1089,ha:1186,o:"m 845 0 q 663 76 731 0 q 602 244 602 145 q 661 412 602 344 q 845 489 728 489 q 1027 412 959 489 q 1089 244 1089 343 q 1029 76 1089 144 q 845 0 962 0 m 844 103 q 945 143 909 103 q 981 243 981 184 q 947 340 981 301 q 844 385 909 385 q 744 342 781 385 q 708 243 708 300 q 741 147 708 186 q 844 103 780 103 m 888 986 l 284 -25 l 199 -25 l 803 986 l 888 986 m 241 468 q 58 545 126 468 q -3 715 -3 615 q 56 881 -3 813 q 238 958 124 958 q 421 881 353 958 q 483 712 483 813 q 423 544 483 612 q 241 468 356 468 m 241 855 q 137 811 175 855 q 100 710 100 768 q 136 612 100 653 q 240 572 172 572 q 344 614 306 572 q 382 713 382 656 q 347 810 382 771 q 241 855 308 855 "},P:{x_min:0,x_max:726,ha:806,o:"m 424 1013 q 640 931 555 1013 q 726 719 726 850 q 637 506 726 587 q 413 426 548 426 l 140 426 l 140 0 l 0 0 l 0 1013 l 424 1013 m 379 889 l 140 889 l 140 548 l 372 548 q 522 589 459 548 q 593 720 593 637 q 528 845 593 801 q 379 889 463 889 "},"Έ":{x_min:0,x_max:1078.21875,ha:1118,o:"m 1078 0 l 342 0 l 342 1013 l 1067 1013 l 1067 889 l 481 889 l 481 585 l 1019 585 l 1019 467 l 481 467 l 481 125 l 1078 125 l 1078 0 m 277 1040 l 83 799 l 0 799 l 140 1040 l 277 1040 "},"Ώ":{x_min:0.125,x_max:1136.546875,ha:1235,o:"m 1136 0 l 722 0 l 722 123 q 911 309 842 194 q 981 558 981 423 q 893 813 981 710 q 651 923 800 923 q 411 821 501 923 q 321 568 321 720 q 390 316 321 433 q 579 123 459 200 l 579 0 l 166 0 l 166 124 l 384 124 q 235 327 289 210 q 182 572 182 444 q 311 912 182 782 q 651 1042 441 1042 q 989 910 858 1042 q 1120 569 1120 778 q 1066 326 1120 443 q 917 124 1013 210 l 1136 124 l 1136 0 m 277 1040 l 83 800 l 0 800 l 140 1041 l 277 1040 "},_:{x_min:0,x_max:705.5625,ha:803,o:"m 705 -334 l 0 -334 l 0 -234 l 705 -234 l 705 -334 "},"Ϊ":{x_min:-110,x_max:246,ha:275,o:"m 246 1046 l 118 1046 l 118 1189 l 246 1189 l 246 1046 m 18 1046 l -110 1046 l -110 1189 l 18 1189 l 18 1046 m 136 0 l 0 0 l 0 1012 l 136 1012 l 136 0 "},"+":{x_min:23,x_max:768,ha:792,o:"m 768 372 l 444 372 l 444 0 l 347 0 l 347 372 l 23 372 l 23 468 l 347 468 l 347 840 l 444 840 l 444 468 l 768 468 l 768 372 "},"½":{x_min:0,x_max:1050,ha:1149,o:"m 1050 0 l 625 0 q 712 178 625 108 q 878 277 722 187 q 967 385 967 328 q 932 456 967 429 q 850 484 897 484 q 759 450 798 484 q 721 352 721 416 l 640 352 q 706 502 640 448 q 851 551 766 551 q 987 509 931 551 q 1050 385 1050 462 q 976 251 1050 301 q 829 179 902 215 q 717 68 740 133 l 1050 68 l 1050 0 m 834 985 l 215 -28 l 130 -28 l 750 984 l 834 985 m 224 422 l 142 422 l 142 811 l 0 811 l 0 867 q 104 889 62 867 q 164 973 157 916 l 224 973 l 224 422 "},"Ρ":{x_min:0,x_max:720,ha:783,o:"m 424 1013 q 637 933 554 1013 q 720 723 720 853 q 633 508 720 591 q 413 426 546 426 l 140 426 l 140 0 l 0 0 l 0 1013 l 424 1013 m 378 889 l 140 889 l 140 548 l 371 548 q 521 589 458 548 q 592 720 592 637 q 527 845 592 801 q 378 889 463 889 "},"'":{x_min:0,x_max:139,ha:236,o:"m 139 851 q 102 737 139 784 q 0 669 65 690 l 0 734 q 59 787 42 741 q 72 873 72 821 l 0 873 l 0 1013 l 139 1013 l 139 851 "},"ª":{x_min:0,x_max:350,ha:397,o:"m 350 625 q 307 616 328 616 q 266 631 281 616 q 247 673 251 645 q 190 628 225 644 q 116 613 156 613 q 32 641 64 613 q 0 722 0 669 q 72 826 0 800 q 247 866 159 846 l 247 887 q 220 934 247 916 q 162 953 194 953 q 104 934 129 953 q 76 882 80 915 l 16 882 q 60 976 16 941 q 166 1011 104 1011 q 266 979 224 1011 q 308 891 308 948 l 308 706 q 311 679 308 688 q 331 670 315 670 l 350 672 l 350 625 m 247 757 l 247 811 q 136 790 175 798 q 64 726 64 773 q 83 682 64 697 q 132 667 103 667 q 207 690 174 667 q 247 757 247 718 "},"΅":{x_min:0,x_max:450,ha:553,o:"m 450 800 l 340 800 l 340 925 l 450 925 l 450 800 m 406 1040 l 212 800 l 129 800 l 269 1040 l 406 1040 m 110 800 l 0 800 l 0 925 l 110 925 l 110 800 "},T:{x_min:0,x_max:777,ha:835,o:"m 777 894 l 458 894 l 458 0 l 319 0 l 319 894 l 0 894 l 0 1013 l 777 1013 l 777 894 "},"Φ":{x_min:0,x_max:915,ha:997,o:"m 527 0 l 389 0 l 389 122 q 110 231 220 122 q 0 509 0 340 q 110 785 0 677 q 389 893 220 893 l 389 1013 l 527 1013 l 527 893 q 804 786 693 893 q 915 509 915 679 q 805 231 915 341 q 527 122 696 122 l 527 0 m 527 226 q 712 310 641 226 q 779 507 779 389 q 712 705 779 627 q 527 787 641 787 l 527 226 m 389 226 l 389 787 q 205 698 275 775 q 136 505 136 620 q 206 308 136 391 q 389 226 276 226 "},"⁋":{x_min:0,x_max:0,ha:694},j:{x_min:-77.78125,x_max:167,ha:349,o:"m 167 871 l 42 871 l 42 1013 l 167 1013 l 167 871 m 167 -80 q 121 -231 167 -184 q -26 -278 76 -278 l -77 -278 l -77 -164 l -41 -164 q 26 -143 11 -164 q 42 -65 42 -122 l 42 737 l 167 737 l 167 -80 "},"Σ":{x_min:0,x_max:756.953125,ha:819,o:"m 756 0 l 0 0 l 0 107 l 395 523 l 22 904 l 22 1013 l 745 1013 l 745 889 l 209 889 l 566 523 l 187 125 l 756 125 l 756 0 "},"›":{x_min:18.0625,x_max:774,ha:792,o:"m 774 376 l 18 40 l 18 149 l 631 421 l 18 692 l 18 799 l 774 465 l 774 376 "},"<":{x_min:17.984375,x_max:773.609375,ha:792,o:"m 773 40 l 18 376 l 17 465 l 773 799 l 773 692 l 159 420 l 773 149 l 773 40 "},"£":{x_min:0,x_max:704.484375,ha:801,o:"m 704 41 q 623 -10 664 5 q 543 -26 583 -26 q 359 15 501 -26 q 243 36 288 36 q 158 23 197 36 q 73 -21 119 10 l 6 76 q 125 195 90 150 q 175 331 175 262 q 147 443 175 383 l 0 443 l 0 512 l 108 512 q 43 734 43 623 q 120 929 43 854 q 358 1010 204 1010 q 579 936 487 1010 q 678 729 678 857 l 678 684 l 552 684 q 504 838 552 780 q 362 896 457 896 q 216 852 263 896 q 176 747 176 815 q 199 627 176 697 q 248 512 217 574 l 468 512 l 468 443 l 279 443 q 297 356 297 398 q 230 194 297 279 q 153 107 211 170 q 227 133 190 125 q 293 142 264 142 q 410 119 339 142 q 516 96 482 96 q 579 105 550 96 q 648 142 608 115 l 704 41 "},t:{x_min:0,x_max:367,ha:458,o:"m 367 0 q 312 -5 339 -2 q 262 -8 284 -8 q 145 28 183 -8 q 108 143 108 64 l 108 638 l 0 638 l 0 738 l 108 738 l 108 944 l 232 944 l 232 738 l 367 738 l 367 638 l 232 638 l 232 185 q 248 121 232 140 q 307 102 264 102 q 345 104 330 102 q 367 107 360 107 l 367 0 "},"¬":{x_min:0,x_max:706,ha:803,o:"m 706 411 l 706 158 l 630 158 l 630 335 l 0 335 l 0 411 l 706 411 "},"λ":{x_min:0,x_max:750,ha:803,o:"m 750 -7 q 679 -15 716 -15 q 538 59 591 -15 q 466 214 512 97 l 336 551 l 126 0 l 0 0 l 270 705 q 223 837 247 770 q 116 899 190 899 q 90 898 100 899 l 90 1004 q 152 1011 125 1011 q 298 938 244 1011 q 373 783 326 901 l 605 192 q 649 115 629 136 q 716 95 669 95 l 736 95 q 750 97 745 97 l 750 -7 "},W:{x_min:0,x_max:1263.890625,ha:1351,o:"m 1263 1013 l 995 0 l 859 0 l 627 837 l 405 0 l 265 0 l 0 1013 l 136 1013 l 342 202 l 556 1013 l 701 1013 l 921 207 l 1133 1012 l 1263 1013 "},">":{x_min:18.0625,x_max:774,ha:792,o:"m 774 376 l 18 40 l 18 149 l 631 421 l 18 692 l 18 799 l 774 465 l 774 376 "},v:{x_min:0,x_max:675.15625,ha:761,o:"m 675 738 l 404 0 l 272 0 l 0 738 l 133 737 l 340 147 l 541 737 l 675 738 "},"τ":{x_min:0.28125,x_max:644.5,ha:703,o:"m 644 628 l 382 628 l 382 179 q 388 120 382 137 q 436 91 401 91 q 474 94 447 91 q 504 97 501 97 l 504 0 q 454 -9 482 -5 q 401 -14 426 -14 q 278 67 308 -14 q 260 233 260 118 l 260 628 l 0 628 l 0 739 l 644 739 l 644 628 "},"ξ":{x_min:0,x_max:624.9375,ha:699,o:"m 624 -37 q 608 -153 624 -96 q 563 -278 593 -211 l 454 -278 q 491 -183 486 -200 q 511 -83 511 -126 q 484 -23 511 -44 q 370 1 452 1 q 323 0 354 1 q 283 -1 293 -1 q 84 76 169 -1 q 0 266 0 154 q 56 431 0 358 q 197 538 108 498 q 94 613 134 562 q 54 730 54 665 q 77 823 54 780 q 143 901 101 867 l 27 901 l 27 1012 l 576 1012 l 576 901 l 380 901 q 244 863 303 901 q 178 745 178 820 q 312 600 178 636 q 532 582 380 582 l 532 479 q 276 455 361 479 q 118 281 118 410 q 165 173 118 217 q 274 120 208 133 q 494 101 384 110 q 624 -37 624 76 "},"&":{x_min:-3,x_max:894.25,ha:992,o:"m 894 0 l 725 0 l 624 123 q 471 0 553 40 q 306 -41 390 -41 q 168 -7 231 -41 q 62 92 105 26 q 14 187 31 139 q -3 276 -3 235 q 55 433 -3 358 q 248 581 114 508 q 170 689 196 640 q 137 817 137 751 q 214 985 137 922 q 384 1041 284 1041 q 548 988 483 1041 q 622 824 622 928 q 563 666 622 739 q 431 556 516 608 l 621 326 q 649 407 639 361 q 663 493 653 426 l 781 493 q 703 229 781 352 l 894 0 m 504 818 q 468 908 504 877 q 384 940 433 940 q 293 907 331 940 q 255 818 255 875 q 289 714 255 767 q 363 628 313 678 q 477 729 446 682 q 504 818 504 771 m 556 209 l 314 499 q 179 395 223 449 q 135 283 135 341 q 146 222 135 253 q 183 158 158 192 q 333 80 241 80 q 556 209 448 80 "},"Λ":{x_min:0,x_max:862.5,ha:942,o:"m 862 0 l 719 0 l 426 847 l 143 0 l 0 0 l 356 1013 l 501 1013 l 862 0 "},I:{x_min:41,x_max:180,ha:293,o:"m 180 0 l 41 0 l 41 1013 l 180 1013 l 180 0 "},G:{x_min:0,x_max:921,ha:1011,o:"m 921 0 l 832 0 l 801 136 q 655 15 741 58 q 470 -28 568 -28 q 126 133 259 -28 q 0 499 0 284 q 125 881 0 731 q 486 1043 259 1043 q 763 957 647 1043 q 905 709 890 864 l 772 709 q 668 866 747 807 q 486 926 589 926 q 228 795 322 926 q 142 507 142 677 q 228 224 142 342 q 483 94 323 94 q 712 195 625 94 q 796 435 796 291 l 477 435 l 477 549 l 921 549 l 921 0 "},"ΰ":{x_min:0,x_max:617,ha:725,o:"m 524 800 l 414 800 l 414 925 l 524 925 l 524 800 m 183 800 l 73 800 l 73 925 l 183 925 l 183 800 m 617 352 q 540 93 617 199 q 308 -24 455 -24 q 76 93 161 -24 q 0 352 0 199 l 0 738 l 126 738 l 126 354 q 169 185 126 257 q 312 98 220 98 q 451 185 402 98 q 492 354 492 257 l 492 738 l 617 738 l 617 352 m 489 1040 l 300 819 l 216 819 l 351 1040 l 489 1040 "},"`":{x_min:0,x_max:138.890625,ha:236,o:"m 138 699 l 0 699 l 0 861 q 36 974 0 929 q 138 1041 72 1020 l 138 977 q 82 931 95 969 q 69 839 69 893 l 138 839 l 138 699 "},"·":{x_min:0,x_max:142,ha:239,o:"m 142 585 l 0 585 l 0 738 l 142 738 l 142 585 "},"Υ":{x_min:0.328125,x_max:819.515625,ha:889,o:"m 819 1013 l 482 416 l 482 0 l 342 0 l 342 416 l 0 1013 l 140 1013 l 411 533 l 679 1013 l 819 1013 "},r:{x_min:0,x_max:355.5625,ha:432,o:"m 355 621 l 343 621 q 179 569 236 621 q 122 411 122 518 l 122 0 l 0 0 l 0 737 l 117 737 l 117 604 q 204 719 146 686 q 355 753 262 753 l 355 621 "},x:{x_min:0,x_max:675,ha:764,o:"m 675 0 l 525 0 l 331 286 l 144 0 l 0 0 l 256 379 l 12 738 l 157 737 l 336 473 l 516 738 l 661 738 l 412 380 l 675 0 "},"μ":{x_min:0,x_max:696.609375,ha:747,o:"m 696 -4 q 628 -14 657 -14 q 498 97 513 -14 q 422 8 470 41 q 313 -24 374 -24 q 207 3 258 -24 q 120 80 157 31 l 120 -278 l 0 -278 l 0 738 l 124 738 l 124 343 q 165 172 124 246 q 308 82 216 82 q 451 177 402 82 q 492 358 492 254 l 492 738 l 616 738 l 616 214 q 623 136 616 160 q 673 92 636 92 q 696 95 684 92 l 696 -4 "},h:{x_min:0,x_max:615,ha:724,o:"m 615 472 l 615 0 l 490 0 l 490 454 q 456 590 490 535 q 338 654 416 654 q 186 588 251 654 q 122 436 122 522 l 122 0 l 0 0 l 0 1013 l 122 1013 l 122 633 q 218 727 149 694 q 362 760 287 760 q 552 676 484 760 q 615 472 615 600 "},".":{x_min:0,x_max:142,ha:239,o:"m 142 0 l 0 0 l 0 151 l 142 151 l 142 0 "},"φ":{x_min:-2,x_max:878,ha:974,o:"m 496 -279 l 378 -279 l 378 -17 q 101 88 204 -17 q -2 367 -2 194 q 68 626 -2 510 q 283 758 151 758 l 283 646 q 167 537 209 626 q 133 373 133 462 q 192 177 133 254 q 378 93 259 93 l 378 758 q 445 764 426 763 q 476 765 464 765 q 765 659 653 765 q 878 377 878 553 q 771 96 878 209 q 496 -17 665 -17 l 496 -279 m 496 93 l 514 93 q 687 183 623 93 q 746 380 746 265 q 691 569 746 491 q 522 658 629 658 l 496 656 l 496 93 "},";":{x_min:0,x_max:142,ha:239,o:"m 142 585 l 0 585 l 0 738 l 142 738 l 142 585 m 142 -12 q 105 -132 142 -82 q 0 -206 68 -182 l 0 -138 q 58 -82 43 -123 q 68 0 68 -56 l 0 0 l 0 151 l 142 151 l 142 -12 "},f:{x_min:0,x_max:378,ha:472,o:"m 378 638 l 246 638 l 246 0 l 121 0 l 121 638 l 0 638 l 0 738 l 121 738 q 137 935 121 887 q 290 1028 171 1028 q 320 1027 305 1028 q 378 1021 334 1026 l 378 908 q 323 918 346 918 q 257 870 273 918 q 246 780 246 840 l 246 738 l 378 738 l 378 638 "},"“":{x_min:1,x_max:348.21875,ha:454,o:"m 140 670 l 1 670 l 1 830 q 37 943 1 897 q 140 1011 74 990 l 140 947 q 82 900 97 940 q 68 810 68 861 l 140 810 l 140 670 m 348 670 l 209 670 l 209 830 q 245 943 209 897 q 348 1011 282 990 l 348 947 q 290 900 305 940 q 276 810 276 861 l 348 810 l 348 670 "},A:{x_min:0.03125,x_max:906.953125,ha:1008,o:"m 906 0 l 756 0 l 648 303 l 251 303 l 142 0 l 0 0 l 376 1013 l 529 1013 l 906 0 m 610 421 l 452 867 l 293 421 l 610 421 "},"‘":{x_min:1,x_max:139.890625,ha:236,o:"m 139 670 l 1 670 l 1 830 q 37 943 1 897 q 139 1011 74 990 l 139 947 q 82 900 97 940 q 68 810 68 861 l 139 810 l 139 670 "},"ϊ":{x_min:-70,x_max:283,ha:361,o:"m 283 800 l 173 800 l 173 925 l 283 925 l 283 800 m 40 800 l -70 800 l -70 925 l 40 925 l 40 800 m 283 3 q 232 -10 257 -5 q 181 -15 206 -15 q 84 26 118 -15 q 41 200 41 79 l 41 737 l 166 737 l 167 215 q 171 141 167 157 q 225 101 182 101 q 247 103 238 101 q 283 112 256 104 l 283 3 "},"π":{x_min:-0.21875,x_max:773.21875,ha:857,o:"m 773 -7 l 707 -11 q 575 40 607 -11 q 552 174 552 77 l 552 226 l 552 626 l 222 626 l 222 0 l 97 0 l 97 626 l 0 626 l 0 737 l 773 737 l 773 626 l 676 626 l 676 171 q 695 103 676 117 q 773 90 714 90 l 773 -7 "},"ά":{x_min:0,x_max:765.5625,ha:809,o:"m 765 -4 q 698 -14 726 -14 q 564 97 586 -14 q 466 7 525 40 q 337 -26 407 -26 q 88 98 186 -26 q 0 369 0 212 q 88 637 0 525 q 337 760 184 760 q 465 727 407 760 q 563 637 524 695 l 563 738 l 685 738 l 685 222 q 693 141 685 168 q 748 94 708 94 q 765 95 760 94 l 765 -4 m 584 371 q 531 562 584 485 q 360 653 470 653 q 192 566 254 653 q 135 379 135 489 q 186 181 135 261 q 358 84 247 84 q 528 176 465 84 q 584 371 584 260 m 604 1040 l 415 819 l 332 819 l 466 1040 l 604 1040 "},O:{x_min:0,x_max:958,ha:1057,o:"m 485 1041 q 834 882 702 1041 q 958 512 958 734 q 834 136 958 287 q 481 -26 702 -26 q 126 130 261 -26 q 0 504 0 279 q 127 880 0 728 q 485 1041 263 1041 m 480 98 q 731 225 638 98 q 815 504 815 340 q 733 783 815 669 q 480 912 640 912 q 226 784 321 912 q 142 504 142 670 q 226 224 142 339 q 480 98 319 98 "},n:{x_min:0,x_max:615,ha:724,o:"m 615 463 l 615 0 l 490 0 l 490 454 q 453 592 490 537 q 331 656 410 656 q 178 585 240 656 q 117 421 117 514 l 117 0 l 0 0 l 0 738 l 117 738 l 117 630 q 218 728 150 693 q 359 764 286 764 q 552 675 484 764 q 615 463 615 593 "},l:{x_min:41,x_max:166,ha:279,o:"m 166 0 l 41 0 l 41 1013 l 166 1013 l 166 0 "},"¤":{x_min:40.09375,x_max:728.796875,ha:825,o:"m 728 304 l 649 224 l 512 363 q 383 331 458 331 q 256 363 310 331 l 119 224 l 40 304 l 177 441 q 150 553 150 493 q 184 673 150 621 l 40 818 l 119 898 l 267 749 q 321 766 291 759 q 384 773 351 773 q 447 766 417 773 q 501 749 477 759 l 649 898 l 728 818 l 585 675 q 612 618 604 648 q 621 553 621 587 q 591 441 621 491 l 728 304 m 384 682 q 280 643 318 682 q 243 551 243 604 q 279 461 243 499 q 383 423 316 423 q 487 461 449 423 q 525 553 525 500 q 490 641 525 605 q 384 682 451 682 "},"κ":{x_min:0,x_max:632.328125,ha:679,o:"m 632 0 l 482 0 l 225 384 l 124 288 l 124 0 l 0 0 l 0 738 l 124 738 l 124 446 l 433 738 l 596 738 l 312 466 l 632 0 "},p:{x_min:0,x_max:685,ha:786,o:"m 685 364 q 598 96 685 205 q 350 -23 504 -23 q 121 89 205 -23 l 121 -278 l 0 -278 l 0 738 l 121 738 l 121 633 q 220 726 159 691 q 351 761 280 761 q 598 636 504 761 q 685 364 685 522 m 557 371 q 501 560 557 481 q 330 651 437 651 q 162 559 223 651 q 108 366 108 479 q 162 177 108 254 q 333 87 224 87 q 502 178 441 87 q 557 371 557 258 "},"‡":{x_min:0,x_max:777,ha:835,o:"m 458 238 l 458 0 l 319 0 l 319 238 l 0 238 l 0 360 l 319 360 l 319 681 l 0 683 l 0 804 l 319 804 l 319 1015 l 458 1013 l 458 804 l 777 804 l 777 683 l 458 683 l 458 360 l 777 360 l 777 238 l 458 238 "},"ψ":{x_min:0,x_max:808,ha:907,o:"m 465 -278 l 341 -278 l 341 -15 q 87 102 180 -15 q 0 378 0 210 l 0 739 l 133 739 l 133 379 q 182 195 133 275 q 341 98 242 98 l 341 922 l 465 922 l 465 98 q 623 195 563 98 q 675 382 675 278 l 675 742 l 808 742 l 808 381 q 720 104 808 213 q 466 -13 627 -13 l 465 -278 "},"η":{x_min:0.78125,x_max:697,ha:810,o:"m 697 -278 l 572 -278 l 572 454 q 540 587 572 536 q 425 650 501 650 q 271 579 337 650 q 206 420 206 509 l 206 0 l 81 0 l 81 489 q 73 588 81 562 q 0 644 56 644 l 0 741 q 68 755 38 755 q 158 720 124 755 q 200 630 193 686 q 297 726 234 692 q 434 761 359 761 q 620 692 544 761 q 697 516 697 624 l 697 -278 "}};
   // eslint-disable-next-line
   const cssFontWeight='normal', ascender=1189, underlinePosition=-100, cssFontStyle='normal', boundingBox={yMin:-334,xMin:-111,yMax:1189,xMax:1672}, resolution = 1000, original_font_information={postscript_name:"Helvetiker-Regular",version_string:"Version 1.00 2004 initial release",vendor_url:"http://www.magenta.gr/",full_font_name:"Helvetiker",font_family_name:"Helvetiker",copyright:"Copyright (c) Μagenta ltd, 2004",description:"",trademark:"",designer:"",designer_url:"",unique_font_identifier:"Μagenta ltd:Helvetiker:22-10-104",license_url:"http://www.ellak.gr/fonts/MgOpen/license.html",license_description:"Copyright (c) 2004 by MAGENTA Ltd. All Rights Reserved.\r\n\r\nPermission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license (\"Fonts\") and associated documentation files (the \"Font Software\"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: \r\n\r\nThe above copyright and this permission notice shall be included in all copies of one or more of the Font Software typefaces.\r\n\r\nThe Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing the word \"MgOpen\", or if the modifications are accepted for inclusion in the Font Software itself by the each appointed Administrator.\r\n\r\nThis License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the \"MgOpen\" name.\r\n\r\nThe Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. \r\n\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL MAGENTA OR PERSONS OR BODIES IN CHARGE OF ADMINISTRATION AND MAINTENANCE OF THE FONT SOFTWARE BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.",manufacturer_name:"Μagenta ltd",font_sub_family_name:"Regular"}, descender = -334, familyName = 'Helvetiker', lineHeight = 1522, underlineThickness = 50, helvetiker_regular_typeface = {glyphs, cssFontWeight, ascender, underlinePosition, cssFontStyle, boundingBox, resolution,original_font_information, descender, familyName, lineHeight, underlineThickness};

   // segment in clockwise - holes, otherwise - solid
   // provide infinity symbol
   glyphs['\u221E'] = { x_min: 100, x_max: 700, ha: 800, o: 'm 150 200 l 350 300 l 150 400 l 150 200 m 450 300 l 650 200 l 650 400 l 450 300 m 100 100 l 100 500 l 400 350 l 700 500 l 700 100 l 400 250 l 100 100 ' };

   _hfont = new THREE.Font({
      ascender, boundingBox, cssFontStyle, cssFontWeight, default: helvetiker_regular_typeface, descender, familyName, glyphs,
      lineHeight, original_font_information, resolution, underlinePosition, underlineThickness
   });

   return _hfont;
}

/** @summary Create three.js Color instance, handles optional opacity
  * @private */
function getMaterialArgs(color$1, args) {
   if (!args || !isObject(args))
      args = {};

   if (isStr(color$1) && (((color$1[0] === '#') && (color$1.length === 9)) || (color$1.indexOf('rgba') >= 0))) {
      const col = color(color$1);
      args.color = new THREE.Color(col.r, col.g, col.b);
      args.opacity = col.opacity ?? 1;
      args.transparent = args.opacity < 1;
   } else
      args.color = new THREE.Color(color$1);
   return args;
}

function createSVGRenderer(as_is, precision, doc) {

   const excl_style1 = ';stroke-opacity:1;stroke-width:1;stroke-linecap:round',
         excl_style2 = ';fill-opacity:1';
   /* eslint-disable-next-line one-var */
   const doc_wrapper = {
      svg_attr: {},
      svg_style: {},
      path_attr: {},
      accPath: '',
      createElementNS(ns, kind) {
         if (kind === 'path') {
            return {
               _wrapper: this,
               setAttribute(name, value) {
                  // cut useless fill-opacity:1 at the end of many SVG attributes
                  if ((name === 'style') && value) {
                     const pos1 = value.indexOf(excl_style1);
                     if ((pos1 >= 0) && (pos1 === value.length - excl_style1.length))
                        value = value.slice(0, value.length - excl_style1.length);
                     const pos2 = value.indexOf(excl_style2);
                     if ((pos2 >= 0) && (pos2 === value.length - excl_style2.length))
                        value = value.slice(0, value.length - excl_style2.length);
                  }
                  this._wrapper.path_attr[name] = value;
               }
            };
         }

         if (kind !== 'svg') {
            console.error(`not supported element for SVGRenderer ${kind}`);
            return null;
         }

         return {
            _wrapper: this,
            childNodes: [], // may be accessed - make dummy
            style: this.svg_style, // for background color
            setAttribute(name, value) {
               this._wrapper.svg_attr[name] = value;
            },
            appendChild(/* node */) {
               this._wrapper.accPath += `<path style="${this._wrapper.path_attr.style}" d="${this._wrapper.path_attr.d}"/>`;
               this._wrapper.path_attr = {};
            },
            removeChild(/* node */) {
               this.childNodes = [];
            }
         };
      }
   };

   let originalDocument;

   if (isNodeJs()) {
      originalDocument = globalThis.document;
      globalThis.document = doc_wrapper;
   }

   const rndr = new THREE.SVGRenderer();

   if (isNodeJs())
      globalThis.document = originalDocument;

   rndr.doc_wrapper = doc_wrapper; // use it to get final SVG code

   rndr.originalRender = rndr.render;

   rndr.render = function(scene, camera) {
      const _doc = globalThis.document;
      if (isNodeJs())
         globalThis.document = this.doc_wrapper;

      this.originalRender(scene, camera);

      if (isNodeJs())
         globalThis.document = _doc;
   };

   rndr.clearHTML = function() {
      this.doc_wrapper.accPath = '';
   };

   rndr.makeOuterHTML = function() {
      const wrap = this.doc_wrapper,
            _textSizeAttr = `viewBox="${wrap.svg_attr.viewBox}" width="${wrap.svg_attr.width}" height="${wrap.svg_attr.height}"`,
            _textClearAttr = wrap.svg_style.backgroundColor ? ` style="background:${wrap.svg_style.backgroundColor}"` : '';

      return `<svg xmlns="${nsSVG}" ${_textSizeAttr}${_textClearAttr}>${wrap.accPath}</svg>`;
   };

   rndr.fillTargetSVG = function(svg) {
      if (isNodeJs()) {
         const wrap = this.doc_wrapper;

         svg.setAttribute('viewBox', wrap.svg_attr.viewBox);
         svg.setAttribute('width', wrap.svg_attr.width);
         svg.setAttribute('height', wrap.svg_attr.height);
         svg.style.background = wrap.svg_style.backgroundColor || '';

         svg.innerHTML = wrap.accPath;
      } else {
         const src = this.domElement;

         svg.setAttribute('viewBox', src.getAttribute('viewBox'));
         svg.setAttribute('width', src.getAttribute('width'));
         svg.setAttribute('height', src.getAttribute('height'));
         svg.style.background = src.style.backgroundColor;

         while (src.firstChild) {
            const elem = src.firstChild;
            src.removeChild(elem);
            svg.appendChild(elem);
         }
      }
   };

   rndr.setPrecision(precision);

   return rndr;
}


/** @summary Define rendering kind which will be used for rendering of 3D elements
  * @param {value} [render3d] - pre-configured value, will be used if applicable
  * @param {value} [is_batch] - is batch mode is configured
  * @return {value} - rendering kind, see constants.Render3D
  * @private */
function getRender3DKind(render3d, is_batch) {
   if (is_batch === undefined)
      is_batch = isBatchMode();

   if (!render3d)
      render3d = is_batch ? settings.Render3DBatch : settings.Render3D;
   const rc = constants$1.Render3D;

   if (render3d === rc.Default)
      render3d = is_batch ? rc.WebGLImage : rc.WebGL;
   if (is_batch && (render3d === rc.WebGL))
      render3d = rc.WebGLImage;

   return render3d;
}

const Handling3DDrawings = {

   /** @summary Access current 3d mode
     * @param {string} [new_value] - when specified, set new 3d mode
     * @return current value
     * @private */
   access3dKind(new_value) {
      const svg = this.getPadPainter()?.getPadSvg();
      if (!svg || svg.empty())
         return -1;

      // returns kind of currently created 3d canvas
      const kind = svg.property('can3d');
      if (new_value !== undefined)
         svg.property('can3d', new_value);
      return ((kind === null) || (kind === undefined)) ? -1 : kind;
   },

   /** @summary Returns size which available for 3D drawing.
     * @desc One uses frame sizes for the 3D drawing - like TH2/TH3 objects
     * @private */
   getSizeFor3d(can3d /* , render3d */) {
      if (can3d === undefined) {
         // analyze which render/embed mode can be used
         can3d = getRender3DKind();
         // all non-webgl elements can be embedded into SVG as is
         if (can3d !== constants$1.Render3D.WebGL)
            can3d = constants$1.Embed3D.EmbedSVG;
         else if (settings.Embed3D !== constants$1.Embed3D.Default)
            can3d = settings.Embed3D;
         else if (browser.isFirefox)
            can3d = constants$1.Embed3D.Embed;
         else if (browser.chromeVersion > 95)
            // version 96 works partially, 97 works fine
            can3d = constants$1.Embed3D.Embed;
         else
            can3d = constants$1.Embed3D.Overlay;
      }

      const pp = this.getPadPainter(),
            pad = pp?.getPadSvg(),
            clname = 'draw3d_' + (pp?.getPadName() || 'canvas');

      if (!pad || pad.empty()) {
         // this is a case when object drawn without canvas

         const rect = getElementRect(this.selectDom());
         if ((rect.height < 10) && (rect.width > 10)) {
            rect.height = Math.round(0.66 * rect.width);
            this.selectDom().style('height', rect.height + 'px');
         }
         rect.x = rect.y = 0;
         rect.clname = clname;
         rect.can3d = -1;
         return rect;
      }

      const fp = this.getFramePainter();
      let size;

      if (fp?.mode3d && (can3d > 0))
         size = fp.getFrameRect();
      else {
         let elem = (can3d > 0) ? pad : this.getCanvSvg();
         size = { x: 0, y: 0, width: elem.property('draw_width'), height: elem.property('draw_height') };
         if (Number.isNaN(size.width) || Number.isNaN(size.height)) {
            size.width = pp.getPadWidth();
            size.height = pp.getPadHeight();
         } else if (fp && !fp.mode3d) {
            elem = pp.getFrameSvg();
            size.x = elem.property('draw_x');
            size.y = elem.property('draw_y');
         }
      }

      size.clname = clname;
      size.can3d = can3d;

      const rect = pp?.getPadRect();
      if (rect) {
         // while 3D canvas uses area also for the axis labels, extend area relative to normal frame
         const dx = Math.round(size.width * 0.07), dy = Math.round(size.height * 0.05);

         size.x = Math.max(0, size.x - dx);
         size.y = Math.max(0, size.y - dy);
         size.width = Math.min(size.width + 2 * dx, rect.width - size.x);
         size.height = Math.min(size.height + 2 * dy, rect.height - size.y);
      }

      if (can3d === constants$1.Embed3D.Overlay) {
         size = getAbsPosInCanvas(pad, size);
         const scale = this.getCanvPainter().getPadScale();
         if (scale && scale !== 1) {
            size.x /= scale;
            size.y /= scale;
            size.width /= scale;
            size.height /= scale;
         }
      }

      return size;
   },

   /** @summary Clear all 3D drawings
     * @return can3d value - how webgl canvas was placed
     * @private */
   clear3dCanvas() {
      const can3d = this.access3dKind(null);
      if (can3d < 0) {
         // remove first child from main element - if it is canvas
         const main = this.selectDom().node();
         let chld = main?.firstChild;

         while (chld && !chld.$jsroot)
            chld = chld.nextSibling;

         if (chld?.$jsroot === '3d') {
            delete chld.painter;
            main.removeChild(chld);
         }
         return can3d;
      }

      const size = this.getSizeFor3d(can3d);
      if (size.can3d === 0) {
         select(this.getCanvSvg().node().nextSibling).remove(); // remove html5 canvas
         this.getCanvSvg().style('display', null); // show SVG canvas
      } else {
         const pp = this.getPadPainter();
         if (!pp || pp.getPadSvg().empty())
            return;

         this.apply3dSize(size).remove();

         pp.getFrameSvg().style('display', null);  // clear display property
      }
      return can3d;
   },

   /** @summary Add 3D canvas
     * @private */
   add3dCanvas(size, canv, webgl) {
      if (!canv || (size.can3d < -1))
         return;

      if (size.can3d === -1) {
         // case when 3D object drawn without canvas

         const main = this.selectDom().node();
         if (main) {
            main.appendChild(canv);
            canv.painter = this;
            canv.$jsroot = '3d'; // mark canvas as added by jsroot
         }

         return;
      }

      if ((size.can3d > 0) && !webgl)
         size.can3d = constants$1.Embed3D.EmbedSVG;

      this.access3dKind(size.can3d);

      if (size.can3d === 0) {
         this.getCanvSvg().style('display', 'none'); // hide SVG canvas

         this.getCanvSvg().node().parentNode.appendChild(canv); // add directly
      } else {
         const pp = this.getPadPainter();
         if (!pp || pp.getPadSvg().empty())
            return;

         // first hide normal frame
         pp.getFrameSvg().style('display', 'none');

         const elem = this.apply3dSize(size);
         elem.attr('title', '').node().appendChild(canv);
      }
   },

   /** @summary Apply size to 3D elements
     * @private */
   apply3dSize(size, onlyget) {
      if (size.can3d < 0)
         return select(null);

      let elem;

      if (size.can3d > 1) {
         const pp = this.getPadPainter();

         elem = pp.getLayerSvg(size.clname);
         if (onlyget)
            return elem;

         const svg = pp.getPadSvg();

         if (size.can3d === constants$1.Embed3D.EmbedSVG) {
            // this is SVG mode or image mode - just create group to hold element

            if (elem.empty())
               elem = svg.insert('g', '.primitives_layer').attr('class', size.clname);

            makeTranslate(elem, size.x, size.y);
         } else {
            if (elem.empty())
               elem = svg.insert('foreignObject', '.primitives_layer').attr('class', size.clname);

            elem.attr('x', size.x)
                .attr('y', size.y)
                .attr('width', size.width)
                .attr('height', size.height)
                .attr('viewBox', `0 0 ${size.width} ${size.height}`)
                .attr('preserveAspectRatio', 'xMidYMid');
         }
      } else {
         let prnt = this.getCanvSvg().node().parentNode;

         elem = select(prnt).select('.' + size.clname);
         if (onlyget)
            return elem;

         // force redraw by resize
         this.getCanvSvg().property('redraw_by_resize', true);

         if (elem.empty()) {
            elem = select(prnt).append('div').attr('class', size.clname)
                                  .style('user-select', 'none');
         }

         // our position inside canvas, but to set 'absolute' position we should use
         // canvas element offset relative to first parent with non-static position
         // now try to use getBoundingClientRect - it should be more precise

         const pos0 = prnt.getBoundingClientRect(), doc = getDocument();

         while (prnt) {
            if (prnt === doc) {
               prnt = null;
               break;
            }
            try {
               if (getComputedStyle(prnt).position !== 'static')
                  break;
            } catch {
               break;
            }
            prnt = prnt.parentNode;
         }

         const pos1 = prnt?.getBoundingClientRect() ?? { top: 0, left: 0 },
               offx = Math.round(pos0.left - pos1.left),
               offy = Math.round(pos0.top - pos1.top);

         elem.style('position', 'absolute').style('left', (size.x + offx) + 'px').style('top', (size.y + offy) + 'px').style('width', size.width + 'px').style('height', size.height + 'px');
      }

      return elem;
   }

}; // Handling3DDrawings


/** @summary Assigns method to handle 3D drawings inside SVG
  * @private */
function assign3DHandler(painter) {
   Object.assign(painter, Handling3DDrawings);
}


/** @summary Creates renderer for the 3D drawings
  * @param {value} width - rendering width
  * @param {value} height - rendering height
  * @param {value} render3d - render type, see {@link constants.Render3D}
  * @param {object} args - different arguments for creating 3D renderer
  * @return {Promise} with renderer object
  * @private */
async function createRender3D(width, height, render3d, args) {
   const rc = constants$1.Render3D, doc = getDocument();

   render3d = getRender3DKind(render3d);

   if (!args)
      args = { antialias: true, alpha: true };

   let promise;

   if (render3d === rc.None)
      promise = Promise.resolve(null);
   else if (render3d === rc.SVG) {
      // SVG rendering
      const r = createSVGRenderer(false, 0);
      r.jsroot_dom = doc.createElementNS(nsSVG, 'svg');
      promise = Promise.resolve(r);
   } else if (isNodeJs()) {
      // try to use WebGL inside node.js - need to create headless context
      promise = Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(node_canvas => {
         args.canvas = node_canvas.default.createCanvas(width, height);
         args.canvas.addEventListener = () => {}; // dummy
         args.canvas.removeEventListener = () => {}; // dummy
         args.canvas.style = {};
         return internals._node_gl || Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; });
      }).then(node_gl => {
         internals._node_gl = node_gl;
         const gl = node_gl?.default(width, height, { preserveDrawingBuffer: true });
         if (!gl)
            throw Error('Fail to create headless-gl');
         args.context = gl;
         gl.canvas = args.canvas;

         const r = new THREE.WebGLRenderer(args);
         r.jsroot_output = new THREE.WebGLRenderTarget(width, height);
         r.setRenderTarget(r.jsroot_output);
         r.jsroot_dom = doc.createElementNS(nsSVG, 'image');
         return r;
      }).catch(() => {
         console.log('gl module is not available - fallback to SVGRenderer');
         render3d = rc.SVG;
         const r = createSVGRenderer(false, 0);
         r.jsroot_dom = doc.createElementNS(nsSVG, 'svg');
         return r;
      });
   } else if (render3d === rc.WebGL) {
      // interactive WebGL Rendering
      promise = Promise.resolve(new THREE.WebGLRenderer(args));
   } else {
      // rendering with WebGL directly into svg image
      const r = new THREE.WebGLRenderer(args);
      r.jsroot_dom = doc.createElementNS(nsSVG, 'image');
      promise = Promise.resolve(r);
   }

   return promise.then(renderer => {
      if (!renderer)
         return renderer;

      if (!renderer.jsroot_dom)
         renderer.jsroot_dom = renderer.domElement;
      else
         renderer.jsroot_custom_dom = true;

      // res.renderer.setClearColor('#000000', 1);
      // res.renderer.setClearColor(0x0, 0);
      renderer.jsroot_render3d = render3d;

      // which format used to convert into images
      renderer.jsroot_image_format = 'png';

      renderer.originalSetSize = renderer.setSize;

      // apply size to dom element
      renderer.setSize = function(w, h, updateStyle) {
         if (this.jsroot_custom_dom) {
            this.jsroot_dom.setAttribute('width', w);
            this.jsroot_dom.setAttribute('height', h);
         }

         this.originalSetSize(w, h, updateStyle);
      };

      renderer.setSize(width, height);

      return renderer;
   });
}


/** @summary Cleanup created renderer object
  * @private */
function cleanupRender3D(renderer) {
   if (!renderer)
      return;

   if (isNodeJs()) {
      const ctxt = isFunc(renderer.getContext) ? renderer.getContext() : null,
            ext = ctxt?.getExtension('STACKGL_destroy_context');
      if (isFunc(ext?.destroy))
         ext.destroy();
   } else {
      // suppress warnings in Chrome about lost webgl context, not required in firefox
      if (browser.isChrome && isFunc(renderer.forceContextLoss))
         renderer.forceContextLoss();

      if (isFunc(renderer.dispose))
         renderer.dispose();
   }
}

/** @summary Cleanup previous renderings before doing next one
  * @desc used together with SVG
  * @private */
function beforeRender3D(renderer) {
   if (isFunc(renderer.clearHTML))
      renderer.clearHTML();
}

/** @summary Post-process result of rendering
  * @desc used together with SVG or node.js image rendering
  * @private */
function afterRender3D(renderer) {
   const rc = constants$1.Render3D;

   if (renderer.jsroot_render3d === rc.WebGL)
      return;

   if (renderer.jsroot_render3d === rc.SVG) {
      // case of SVGRenderer
      renderer.fillTargetSVG(renderer.jsroot_dom);
   } else if (isNodeJs()) {
      // this is WebGL rendering in node.js
      const canvas = renderer.domElement,
            context = canvas.getContext('2d'),
            pixels = new Uint8Array(4 * canvas.width * canvas.height);

      renderer.readRenderTargetPixels(renderer.jsroot_output, 0, 0, canvas.width, canvas.height, pixels);

      // small code to flip Y scale
      let indx1 = 0, indx2 = (canvas.height - 1) * 4 * canvas.width, k, d;
      while (indx1 < indx2) {
         for (k = 0; k < 4 * canvas.width; ++k) {
            d = pixels[indx1 + k];
            pixels[indx1 + k] = pixels[indx2 + k];
            pixels[indx2 + k] = d;
         }
         indx1 += 4 * canvas.width;
         indx2 -= 4 * canvas.width;
      }

      const imageData = context.createImageData(canvas.width, canvas.height);
      imageData.data.set(pixels);
      context.putImageData(imageData, 0, 0);

      const format = 'image/' + renderer.jsroot_image_format,
            dataUrl = canvas.toDataURL(format);

      renderer.jsroot_dom.setAttribute('href', dataUrl);
   } else {
      const dataUrl = renderer.domElement.toDataURL('image/' + renderer.jsroot_image_format);
      renderer.jsroot_dom.setAttribute('href', dataUrl);
   }
}

// ========================================================================================================

/**
 * @summary Tooltip handler for 3D drawings
 *
 * @private
 */

class TooltipFor3D {

   /** @summary constructor
     * @param {object} dom - DOM element
     * @param {object} canvas - canvas for 3D rendering */
   constructor(prnt, canvas) {
      this.tt = null;
      this.cont = null;
      this.lastlbl = '';
      this.parent = prnt || getDocument().body;
      this.canvas = canvas; // we need canvas to recalculate mouse events
      this.abspos = !prnt;
      this.scale = 1;
   }

   /** @summary check parent */
   checkParent(prnt) {
      if (prnt && (this.parent !== prnt)) {
         this.hide();
         this.parent = prnt;
      }
   }

   /** @summary set scaling factor */
   setScale(v) {
      this.scale = v;
   }

   /** @summary extract position from event
     * @desc can be used to process it later when event is gone */
   extract_pos(e) {
      if (isObject(e) && (e.u !== undefined) && (e.l !== undefined))
         return e;
      const res = { u: 0, l: 0 };
      if (this.abspos) {
         res.l = e.pageX;
         res.u = e.pageY;
      } else {
         res.l = e.offsetX;
         res.u = e.offsetY;
      }
      res.l /= this.scale;
      res.u /= this.scale;
      return res;
   }

   /** @summary Method used to define position of next tooltip
     * @desc event is delivered from canvas,
     * but position should be calculated relative to the element where tooltip is placed */
   pos(e) {
      if (!this.tt)
         return;

      const pos = this.extract_pos(e);
      if (!this.abspos) {
         const rect1 = this.parent.getBoundingClientRect(),
               rect2 = this.canvas.getBoundingClientRect();

         if ((rect1.left !== undefined) && (rect2.left !== undefined))
            pos.l += (rect2.left - rect1.left);

         if ((rect1.top !== undefined) && (rect2.top !== undefined))
            pos.u += rect2.top - rect1.top;

         if (pos.l + this.tt.offsetWidth + 3 >= this.parent.offsetWidth)
            pos.l = this.parent.offsetWidth - this.tt.offsetWidth - 3;

         if (pos.u + this.tt.offsetHeight + 15 >= this.parent.offsetHeight)
            pos.u = this.parent.offsetHeight - this.tt.offsetHeight - 15;

         // one should find parent with non-static position,
         // all absolute coordinates calculated relative to such node
         let abs_parent = this.parent;
         while (abs_parent) {
            const style = getComputedStyle(abs_parent);
            if (!style || (style.position !== 'static'))
               break;
            if (!abs_parent.parentNode || (abs_parent.parentNode.nodeType !== 1))
               break;
            abs_parent = abs_parent.parentNode;
         }

         if (abs_parent && (abs_parent !== this.parent)) {
            const rect0 = abs_parent.getBoundingClientRect();
            pos.l += (rect1.left - rect0.left);
            pos.u += (rect1.top - rect0.top);
         }
      }

      this.tt.style.top = `${pos.u + 15}px`;
      this.tt.style.left = `${pos.l + 3}px`;
   }

   /** @summary Show tooltip */
   show(v /* , mouse_pos, status_func */) {
      let lines;
      if (v && isObject(v) && (v.lines || v.line)) {
         if (!v.only_status)
            lines = v.line ? [v.line] : v.lines;
      } else if (isStr(v))
         lines = [v];

      const doc = this.parent.ownerDocument;

      if (!lines || !doc)
         return this.hide();

      if (!this.tt) {
         this.tt = doc.createElement('div');
         this.tt.setAttribute('style', 'opacity: 1; filter: alpha(opacity=1); position: absolute; display: block; width: auto; overflow: hidden; z-index: 101;');
         this.cont = doc.createElement('div');
         this.cont.setAttribute('style', 'display: block; padding: 5px; margin-left: 5px; font-size: 11px; line-height: 18px; background: #777; color: #fff;');
         this.tt.appendChild(this.cont);
         this.parent.appendChild(this.tt);
      }

      this.cont.innerText = '';
      lines.forEach(lbl => {
         const p = doc.createElement('p');
         p.innerText = lbl;
         p.setAttribute('style', 'padding: 0px; margin: 1px;');
         this.cont.appendChild(p);
      });
   }

   /** @summary Hide tooltip */
   hide() {
      if (this.tt)
         this.parent.removeChild(this.tt);

      this.tt = this.cont = null;
   }

} // class TooltipFor3D

/** @summary Create OrbitControls for painter
  * @private */
function createOrbitControl(painter, camera, scene, renderer, lookat) {
   const enable_zoom = settings.Zooming && settings.ZoomMouse,
         enable_select = isFunc(painter.processMouseClick);

   let control = null;

   function control_mousedown(evnt) {
      if (!control)
         return;

      // function used to hide some events from orbit control and redirect them to zooming rect
      if (control.mouse_zoom_mesh) {
         evnt.stopImmediatePropagation();
         evnt.stopPropagation();
         return;
      }

      // only left-button is considered
      if ((evnt.button !== undefined) && (evnt.button !== 0))
         return;
      if ((evnt.buttons !== undefined) && (evnt.buttons !== 1))
         return;

      if (control.enable_zoom) {
         control.mouse_zoom_mesh = control.detectZoomMesh(evnt);
         if (control.mouse_zoom_mesh) {
            // just block orbit control
            evnt.stopImmediatePropagation();
            evnt.stopPropagation();
            return;
         }
      }

      if (control.enable_select)
         control.mouse_select_pnt = control.getMousePos(evnt, {});
   }

   function control_mouseup(evnt) {
      if (!control)
         return;

      if (control.mouse_zoom_mesh && control.mouse_zoom_mesh.point2 && control.painter.get3dZoomCoord) {
         let kind = control.mouse_zoom_mesh.object.zoom,
             pos1 = control.painter.get3dZoomCoord(control.mouse_zoom_mesh.point, kind),
             pos2 = control.painter.get3dZoomCoord(control.mouse_zoom_mesh.point2, kind);

         if (pos1 > pos2)
            [pos1, pos2] = [pos2, pos1];

         if ((kind === 'z') && control.mouse_zoom_mesh.object.use_y_for_z)
            kind = 'y';

         // try to zoom
         if ((pos1 < pos2) && control.painter.zoom(kind, pos1, pos2))
            control.mouse_zoom_mesh = null;
      }

      // if selection was drawn, it should be removed and picture rendered again
      if (control.enable_zoom)
         control.removeZoomMesh();

      if (control.enable_select && control.mouse_select_pnt) {
         const pnt = control.getMousePos(evnt, {}),
               same_pnt = (pnt.x === control.mouse_select_pnt.x) && (pnt.y === control.mouse_select_pnt.y);
         delete control.mouse_select_pnt;

         if (same_pnt) {
            const intersects = control.getMouseIntersects(pnt);
            control.painter.processMouseClick(pnt, intersects, evnt);
         }
      }
   }

   function render3DFired(_painter) {
      if (_painter?.renderer === undefined)
         return false;
      // when timeout configured, object is prepared for rendering
      return _painter.render_tmout !== undefined;
   }

   function control_mousewheel(evnt) {
      if (!control)
         return;

      // try to handle zoom extra
      if (render3DFired(control.painter) || control.mouse_zoom_mesh) {
         evnt.preventDefault();
         evnt.stopPropagation();
         evnt.stopImmediatePropagation();
         return; // already fired redraw, do not react on the mouse wheel
      }

      const intersect = control.detectZoomMesh(evnt);
      if (!intersect)
         return;

      evnt.preventDefault();
      evnt.stopPropagation();
      evnt.stopImmediatePropagation();

      if (isFunc(control.painter?.analyzeMouseWheelEvent)) {
         let kind = intersect.object.zoom,
             position = intersect.point[kind];
         const item = { name: kind, ignore: false };

         // z changes from 0..2*size_z3d, others -size_x3d..+size_x3d
         switch (kind) {
            case 'x':
               position = (position + control.painter.size_x3d) / 2 / control.painter.size_x3d;
               break;
            case 'y':
               position = (position + control.painter.size_y3d) / 2 / control.painter.size_y3d;
               break;
            case 'z':
               position = position / 2 / control.painter.size_z3d;
               break;
         }

         control.painter.analyzeMouseWheelEvent(evnt, item, position, false);

         if ((kind === 'z') && intersect.object.use_y_for_z)
            kind = 'y';

         control.painter.zoom(kind, item.min, item.max);
      }
   }

   // assign own handler before creating OrbitControl

   if (settings.Zooming && settings.ZoomWheel)
      renderer.domElement.addEventListener('wheel', control_mousewheel);

   if (enable_zoom || enable_select) {
      renderer.domElement.addEventListener('pointerdown', control_mousedown);
      renderer.domElement.addEventListener('pointerup', control_mouseup);
   }

   control = new THREE.OrbitControls(camera, renderer.domElement);

   control.enableDamping = false;
   control.dampingFactor = 1.0;
   control.enableZoom = true;
   control.enableKeys = settings.HandleKeys;

   if (lookat) {
      control.target.copy(lookat);
      control.target0.copy(lookat);
      control.update();
   }

   control.tooltip = new TooltipFor3D(painter.selectDom().node(), renderer.domElement);

   control.painter = painter;
   control.camera = camera;
   control.scene = scene;
   control.renderer = renderer;
   control.raycaster = new THREE.Raycaster();
   control.raycaster.params.Line.threshold = 10;
   control.raycaster.params.Points.threshold = 5;
   control.mouse_zoom_mesh = null; // zoom mesh, currently used in the zooming
   control.block_ctxt = false; // require to block context menu command appearing after control ends, required in chrome which inject contextmenu when key released
   control.block_mousemove = false; // when true, tooltip or cursor will not react on mouse move
   control.cursor_changed = false;
   control.control_changed = false;
   control.control_active = false;
   control.mouse_ctxt = { x: 0, y: 0, on: false };
   control.enable_zoom = enable_zoom;
   control.enable_select = enable_select;

   control.cleanup = function() {
      if (settings.Zooming && settings.ZoomWheel)
         this.domElement.removeEventListener('wheel', control_mousewheel);
      if (this.enable_zoom || this.enable_select) {
         this.domElement.removeEventListener('pointerdown', control_mousedown);
         this.domElement.removeEventListener('pointerup', control_mouseup);
      }

      this.domElement.removeEventListener('click', this.lstn_click);
      this.domElement.removeEventListener('dblclick', this.lstn_dblclick);
      this.domElement.removeEventListener('contextmenu', this.lstn_contextmenu);
      this.domElement.removeEventListener('mousemove', this.lstn_mousemove);
      this.domElement.removeEventListener('mouseleave', this.lstn_mouseleave);

      this.dispose(); // this is from OrbitControl itself

      this.tooltip.hide();
      delete this.tooltip;
      delete this.painter;
      delete this.camera;
      delete this.scene;
      delete this.renderer;
      delete this.raycaster;
      delete this.mouse_zoom_mesh;
   };

   control.hideTooltip = function() {
      this.tooltip.hide();
   };

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

   control.getOriginDirectionIntersects = function(origin, direction) {
      this.raycaster.set(origin, direction);
      let intersects = this.raycaster.intersectObjects(this.scene.children, true);
      // painter may want to filter intersects
      if (isFunc(this.painter.filterIntersects))
         intersects = this.painter.filterIntersects(intersects);
      return intersects;
   };

   control.getMouseIntersects = function(mouse) {
      // domElement gives correct coordinate with canvas render, but isn't always right for webgl renderer
      if (!this.renderer)
         return [];

      const sz = (this.renderer instanceof THREE.SVGRenderer) ? this.renderer.domElement : this.renderer.getSize(new THREE.Vector2()),
            pnt = { x: mouse.x / sz.width * 2 - 1, y: -mouse.y / sz.height * 2 + 1 };

      this.camera.updateMatrix();
      this.camera.updateMatrixWorld();
      this.raycaster.setFromCamera(pnt, this.camera);
      let intersects = this.raycaster.intersectObjects(this.scene.children, true);

      // painter may want to filter intersects
      if (isFunc(this.painter.filterIntersects))
         intersects = this.painter.filterIntersects(intersects);

      return intersects;
   };

   control.detectZoomMesh = function(evnt) {
      const mouse = this.getMousePos(evnt, {}),
            intersects = this.getMouseIntersects(mouse);
      if (intersects) {
         for (let n = 0; n < intersects.length; ++n) {
            if (intersects[n].object.zoom && !intersects[n].object.zoom_disabled)
               return intersects[n];
         }
      }

      return null;
   };

   control.getInfoAtMousePosition = function(mouse_pos) {
      const intersects = this.getMouseIntersects(mouse_pos);
      let tip = null, p = null;

      for (let i = 0; i < intersects.length; ++i) {
         const obj3d = intersects[i].object;
         if (isFunc(obj3d?.tooltip)) {
            tip = obj3d.tooltip(intersects[i]);
            p = obj3d.tip_painter || obj3d.painter || tip?.$painter;
            break;
         }
      }

      if (tip && p) {
         return {
            obj: p.getObject(),
            name: p.getObject().fName,
            bin: tip.bin, cont: tip.value,
            binx: tip.ix, biny: tip.iy, binz: tip.iz,
            grx: (tip.x1 + tip.x2) / 2, gry: (tip.y1 + tip.y2) / 2, grz: (tip.z1 + tip.z2) / 2
         };
      }
   };

   control.processDblClick = function(evnt) {
      // first check if zoom mesh clicked
      const zoom_intersect = this.detectZoomMesh(evnt);
      if (zoom_intersect && this.painter) {
         this.painter.unzoom(zoom_intersect.object.use_y_for_z ? 'y' : zoom_intersect.object.zoom);
         return;
      }

      // then check if double-click handler assigned
      const handler = this.painter?.getFramePainter()?.getDblclickHandler();

      if (isFunc(handler)) {
         const info = this.getInfoAtMousePosition(this.getMousePos(evnt, {}));
         if (info) {
            handler(info);
            return;
         }
      }

      this.reset();
   };

   control.changeEvent = function() {
      this.mouse_ctxt.on = false; // disable context menu if any changes where done by orbit control
      this.painter.render3D(0);
      this.control_changed = true;
   };

   control.startEvent = function() {
      this.control_active = true;
      this.block_ctxt = false;
      this.mouse_ctxt.on = false;

      this.tooltip.hide();

      // do not reset here, problem of events sequence in orbitcontrol
      // it issue change/start/stop event when do zooming
      // control.control_changed = false;
   };

   control.endEvent = function() {
      this.control_active = false;
      if (this.mouse_ctxt.on) {
         this.mouse_ctxt.on = false;
         this.contextMenu(this.mouse_ctxt, this.getMouseIntersects(this.mouse_ctxt));
      } /* else if (this.control_changed) {
         // react on camera change when required
      } */
      this.control_changed = false;
   };

   control.mainProcessContextMenu = function(evnt) {
      evnt.preventDefault();
      this.getMousePos(evnt, this.mouse_ctxt);
      if (this.control_active)
         this.mouse_ctxt.on = true;
      else if (this.block_ctxt)
         this.block_ctxt = false;
      else
         this.contextMenu(this.mouse_ctxt, this.getMouseIntersects(this.mouse_ctxt));
   };

   control.contextMenu = function(/* pos, intersects */) {
      // do nothing, function called when context menu want to be activated
   };

   control.setTooltipEnabled = function(on) {
      this.block_mousemove = !on;
      if (on === false) {
         this.tooltip.hide();
         this.removeZoomMesh();
      }
   };

   control.removeZoomMesh = function() {
      if (this.mouse_zoom_mesh?.object.showSelection())
         this.painter.render3D();
      this.mouse_zoom_mesh = null; // in any case clear mesh, enable orbit control again
   };

   control.mainProcessMouseMove = function(evnt) {
      if (!this.painter)
         return; // protect when cleanup

      if (this.control_active && evnt.buttons && (evnt.buttons & 2))
         this.block_ctxt = true; // if right button in control was active, block next context menu

      if (this.control_active || this.block_mousemove || !isFunc(this.processMouseMove))
         return;

      if (this.mouse_zoom_mesh) {
         // when working with zoom mesh, need special handling

         const zoom2 = this.detectZoomMesh(evnt),
               pnt2 = (zoom2?.object === this.mouse_zoom_mesh.object) ? zoom2.point : this.mouse_zoom_mesh.object.globalIntersect(this.raycaster);

         if (pnt2)
            this.mouse_zoom_mesh.point2 = pnt2;

         if (pnt2 && this.painter.enable_highlight) {
            if (this.mouse_zoom_mesh.object.showSelection(this.mouse_zoom_mesh.point, pnt2))
               this.painter.render3D(0);
         }

         this.tooltip.hide();
         return;
      }

      evnt.preventDefault();

      // extract mouse position
      this.tmout_mouse = this.getMousePos(evnt, {});
      this.tmout_ttpos = this.tooltip?.extract_pos(evnt);

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

      if (!this.mouse_tmout)
         this.delayedProcessMouseMove();
      else
         this.tmout_handle = setTimeout(() => this.delayedProcessMouseMove(), this.mouse_tmout);
   };

   control.delayedProcessMouseMove = function() {
      // remove handle - allow to trigger new timeout
      delete this.tmout_handle;
      if (!this.painter)
         return; // protect when cleanup

      const mouse = this.tmout_mouse,
            intersects = this.getMouseIntersects(mouse),
            tip = this.processMouseMove(intersects);

      if (tip) {
         let name = '', title = '', info = '';
         const coord = mouse ? mouse.x.toFixed(0) + ',' + mouse.y.toFixed(0) : '';
         if (isStr(tip))
            info = tip;
         else {
            name = tip.name;
            title = tip.title;
            if (tip.line)
               info = tip.line;
            else if (tip.lines) {
               info = tip.lines.slice(1).join(' ');
               name = tip.lines[0];
            }
         }
         this.painter.showObjectStatus(name, title, info, coord);
      }

      this.cursor_changed = false;
      if (tip && this.painter?.isTooltipAllowed()) {
         this.tooltip.checkParent(this.painter.selectDom().node());
         this.tooltip.show(tip, mouse);
         this.tooltip.pos(this.tmout_ttpos);
      } else {
         this.tooltip.hide();
         if (intersects) {
            for (let n = 0; n < intersects.length; ++n) {
               if (intersects[n].object.zoom && !intersects[n].object.zoom_disabled)
                  this.cursor_changed = true;
            }
         }
      }

      getDocument().body.style.cursor = this.cursor_changed ? 'pointer' : 'auto';
   };

   control.mainProcessMouseLeave = function() {
      if (!this.painter)
         return; // protect when cleanup

      // do not enter main event at all
      if (this.tmout_handle) {
         clearTimeout(this.tmout_handle);
         delete this.tmout_handle;
      }
      this.tooltip.hide();
      if (isFunc(this.processMouseLeave))
         this.processMouseLeave();
      if (this.cursor_changed) {
         getDocument().body.style.cursor = 'auto';
         this.cursor_changed = false;
      }
   };

   control.mainProcessDblClick = function(evnt) {
      // suppress simple click handler if double click detected
      if (this.single_click_tm) {
         clearTimeout(this.single_click_tm);
         delete this.single_click_tm;
      }
      this.processDblClick(evnt);
   };

   control.processClick = function(mouse_pos, kind) {
      delete this.single_click_tm;

      if (kind === 1) {
         const handler = this.painter?.getFramePainter()?.getClickHandler();
         if (isFunc(handler)) {
            const info = this.getInfoAtMousePosition(mouse_pos);
            if (info) {
               handler(info);
               return;
            }
         }
      }

      // method assigned in the Eve7 and used for object selection
      if ((kind === 2) && isFunc(this.processSingleClick)) {
         const intersects = this.getMouseIntersects(mouse_pos);
         this.processSingleClick(intersects);
      }

      if (kind === 3) {
         const intersects = this.getMouseIntersects(mouse_pos);
         let objpainter = null;
         for (let i = 0; !objpainter && (i < intersects.length); ++i) {
            const obj3d = intersects[i].object;
            objpainter = obj3d.painter || obj3d.parent?.painter; // check one top level
         }
         if (objpainter) {
            // while axis painter not directly appears in the list of primitives, pad and canvas take from frame
            const padp = this.painter?.getPadPainter(),
                  canvp = this.painter?.getCanvPainter();
            canvp?.producePadEvent('select', padp, objpainter);
         }
      }
   };

   control.lstn_click = function(evnt) {
      // ignore right-mouse click
      if (evnt.detail === 2)
         return;

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

      let kind = 0;
      if (this.painter?.getFramePainter()?.getClickHandler())
         kind = 1;  // user click handler
      else if (this.processSingleClick && this.painter?.options?.mouse_click)
         kind = 2;  // eve7 click handler
      else if (this.painter?.getCanvPainter())
         kind = 3;  // select event for GED

      // if normal event, set longer timeout waiting if double click not detected
      if (kind)
         this.single_click_tm = setTimeout(this.processClick.bind(this, this.getMousePos(evnt, {}), kind), 300);
   }.bind(control);

   control.addEventListener('change', () => control.changeEvent());
   control.addEventListener('start', () => control.startEvent());
   control.addEventListener('end', () => control.endEvent());

   control.lstn_contextmenu = evnt => control.mainProcessContextMenu(evnt);
   control.lstn_dblclick = evnt => control.mainProcessDblClick(evnt);
   control.lstn_mousemove = evnt => control.mainProcessMouseMove(evnt);
   control.lstn_mouseleave = () => control.mainProcessMouseLeave();

   renderer.domElement.addEventListener('click', control.lstn_click);
   renderer.domElement.addEventListener('dblclick', control.lstn_dblclick);
   renderer.domElement.addEventListener('contextmenu', control.lstn_contextmenu);
   renderer.domElement.addEventListener('mousemove', control.lstn_mousemove);
   renderer.domElement.addEventListener('mouseleave', control.lstn_mouseleave);

   return control;
}

/** @summary Method cleanup three.js object as much as possible.
  * @desc Simplify JS engine to remove it from memory
  * @private */
function disposeThreejsObject(obj, only_childs) {
   if (!obj)
      return;

   if (obj.children) {
      for (let i = 0; i < obj.children.length; i++)
         disposeThreejsObject(obj.children[i]);
   }

   if (only_childs) {
      obj.children = [];
      return;
   }

   obj.children = undefined;

   if (obj.geometry) {
      obj.geometry.dispose();
      obj.geometry = undefined;
   }
   if (obj.material) {
      if (obj.material.map) {
         obj.material.map.dispose();
         obj.material.map = undefined;
      }
      obj.material.dispose();
      obj.material = undefined;
   }

   // cleanup jsroot fields to simplify browser cleanup job
   delete obj.painter;
   delete obj.bins_index;
   delete obj.tooltip;
   delete obj.stack; // used in geom painter
   delete obj.drawn_highlight; // special highlight object
   // used in lego tooltips
   delete obj.face_to_bins_index;
   delete obj.tip_painter;
   delete obj.handle;
}


/** @summary Create LineSegments mesh (or only geometry)
  * @desc If required, calculates lineDistance attribute for dashed geometries
  * @private */
function createLineSegments(arr, material, index = undefined, only_geometry = false) {
   const geom = new THREE.BufferGeometry();

   geom.setAttribute('position', arr instanceof Float32Array ? new THREE.BufferAttribute(arr, 3) : new THREE.Float32BufferAttribute(arr, 3));
   if (index)
      geom.setIndex(new THREE.BufferAttribute(index, 1));

   if (material.isLineDashedMaterial) {
      const v1 = new THREE.Vector3(),
            v2 = new THREE.Vector3();
      let d = 0, distances;

      if (index) {
         distances = new Float32Array(index.length);
         for (let n = 0; n < index.length; n += 2) {
            const i1 = index[n], i2 = index[n + 1];
            v1.set(arr[i1], arr[i1 + 1], arr[i1 + 2]);
            v2.set(arr[i2], arr[i2 + 1], arr[i2 + 2]);
            distances[n] = d;
            d += v2.distanceTo(v1);
            distances[n + 1] = d;
         }
      } else {
         distances = new Float32Array(arr.length / 3);
         for (let n = 0; n < arr.length; n += 6) {
            v1.set(arr[n], arr[n + 1], arr[n + 2]);
            v2.set(arr[n + 3], arr[n + 4], arr[n + 5]);
            distances[n / 3] = d;
            d += v2.distanceTo(v1);
            distances[n / 3 + 1] = d;
         }
      }
      geom.setAttribute('lineDistance', new THREE.BufferAttribute(distances, 1));
   }

   return only_geometry ? geom : new THREE.LineSegments(geom, material);
}

/** @summary Help structures for calculating Box mesh
  * @private */
const Box3D = {
   Vertices: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 0),
              new THREE.Vector3(1, 0, 1), new THREE.Vector3(1, 0, 0),
              new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 1, 1),
              new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, 1)],
   Indexes: [0, 2, 1, 2, 3, 1, 4, 6, 5, 6, 7, 5, 4, 5, 1, 5, 0, 1,
             7, 6, 2, 6, 3, 2, 5, 7, 0, 7, 2, 0, 1, 3, 4, 3, 6, 4],
   Normals: [1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1],
   Segments: [0, 2, 2, 7, 7, 5, 5, 0, 1, 3, 3, 6, 6, 4, 4, 1, 1, 0, 3, 2, 6, 7, 4, 5],  // segments addresses Vertices
   Crosses: [0, 7, 2, 5, 0, 3, 1, 2, 7, 3, 2, 6, 5, 6, 4, 7, 5, 1, 0, 4, 3, 4, 1, 6], // addresses Vertices
   MeshSegments: undefined
};

// these segments address vertices from the mesh, we can use positions from box mesh
Box3D.MeshSegments = (function() {
   const arr = new Int32Array(Box3D.Segments.length);

   for (let n = 0; n < arr.length; ++n) {
      for (let k = 0; k < Box3D.Indexes.length; ++k) {
         if (Box3D.Segments[n] === Box3D.Indexes[k]) {
            arr[n] = k;
            break;
         }
      }
   }
   return arr;
})();


/**
 * @summary Abstract interactive control interface for 3D objects
 *
 * @abstract
 * @private
 */

class InteractiveControl {

   cleanup() {}
   extractIndex(/* intersect */) {}
   setSelected(/* col, indx */) {}
   setHighlight(/* col, indx */) {}
   checkHighlightIndex(/* indx */) {}

} // class InteractiveControl


/**
 * @summary Class for creation of 3D points
 *
 * @private
 */

class PointsCreator {

   /** @summary constructor
     * @param {number} number - number of points
     * @param {boolean} [iswebgl] - if WebGL is used
     * @param {number} [scale] - scale factor */
   constructor(number, iswebgl = true, scale = 1) {
      this.webgl = iswebgl;
      this.scale = scale || 1;

      this.pos = new Float32Array(number * 3);
      this.geom = new THREE.BufferGeometry();
      this.geom.setAttribute('position', new THREE.BufferAttribute(this.pos, 3));
      this.indx = 0;
   }

   /** @summary Add point */
   addPoint(x, y, z) {
      this.pos[this.indx] = x;
      this.pos[this.indx + 1] = y;
      this.pos[this.indx + 2] = z;
      this.indx += 3;
   }

   /** @summary Create points */
   createPoints(args) {
      if (!isObject(args))
         args = { color: args };
      if (!args.color)
         args.color = 'black';

      let k = 1;

      // special dots
      if (!args.style)
         k = 1.1;
      else if (args.style === 1)
         k = 0.3;
      else if (args.style === 6)
         k = 0.5;
      else if (args.style === 7)
         k = 0.7;

      const makePoints = texture => {
         const material_args = { size: 3 * this.scale * k };
         if (texture) {
            material_args.map = texture;
            material_args.transparent = true;
         } else
            material_args.color = args.color || 'black';

         const pnts = new THREE.Points(this.geom, new THREE.PointsMaterial(material_args));
         pnts.nvertex = 1;
         return pnts;
      };

      // this is plain creation of points, no need for texture loading

      if (k !== 1) {
         const res = makePoints();
         return this.noPromise ? res : Promise.resolve(res);
      }

      const handler = new TAttMarkerHandler({ style: args.style, color: args.color, size: 7 }),
            w = handler.fill ? 1 : 7,
            imgdata = `<svg width="64" height="64" xmlns="${nsSVG}">` +
                      `<path d="${handler.create(32, 32)}" style="stroke: ${handler.getStrokeColor()}; stroke-width: ${w}; fill: ${args.fill ?? handler.getFillColor()}"></path>` +
                      '</svg>',
            dataUrl = prSVG + (isNodeJs() ? imgdata : encodeURIComponent(imgdata));
      let promise;

      if (isNodeJs()) {
         promise = Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(handle => handle.default.loadImage(dataUrl).then(img => {
            const canvas = handle.default.createCanvas(64, 64),
                  ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0, 64, 64);
            return new THREE.CanvasTexture(canvas);
         }));
      } else if (this.noPromise) {
         // only for v6 support
         return makePoints(new THREE.TextureLoader().load(dataUrl));
      } else {
         promise = new Promise((resolveFunc, rejectFunc) => {
            const loader = new THREE.TextureLoader();
            loader.load(dataUrl, res => resolveFunc(res), undefined, () => rejectFunc(Error(`Fail to load ${dataUrl}`)));
         });
      }

      return promise.then(makePoints);
   }

} // class PointsCreator


/** @summary Create material for 3D line
  * @desc Takes into account dashed properties
  * @private */
function create3DLineMaterial(painter, arg, is_v7 = false) {
   if (!painter || !arg)
      return null;

   let color, lstyle, lwidth;
   if (isStr(arg) || is_v7) {
      color = painter.v7EvalColor(arg + 'color', 'black');
      lstyle = parseInt(painter.v7EvalAttr(arg + 'style', 0));
      lwidth = parseInt(painter.v7EvalAttr(arg + 'width', 1));
   } else {
      color = painter.getColor(arg.fLineColor);
      lstyle = arg.fLineStyle;
      lwidth = arg.fLineWidth;
   }

   const style = lstyle ? getSvgLineStyle(lstyle) : '',
         dash = style ? style.split(',') : [],
         material = (dash && dash.length >= 2)
            ? new THREE.LineDashedMaterial({ color, dashSize: parseInt(dash[0]), gapSize: parseInt(dash[1]) })
            : new THREE.LineBasicMaterial({ color });

   if (lwidth && (lwidth > 1))
      material.linewidth = lwidth;

   return material;
}

/** @summary Create plain text geometry
  * @private */
function createTextGeometry(lbl, size) {
   const geom_args = { font: getHelveticaFont(), size, height: 0, curveSegments: 5 };
   if (THREE.REVISION > 162)
      geom_args.depth = 0;
   else
      geom_args.height = 0;

   return new THREE.TextGeometry(lbl, geom_args);
}

class TextParseWrapper {

   constructor(kind, parent, font_size) {
      this.kind = kind ?? 'g';
      this.childs = [];
      this.x = 0;
      this.y = 0;
      this.font_size = parent?.font_size ?? font_size;
      this.stroke_width = parent?.stroke_width ?? 5;
      parent?.childs.push(this);
   }

   append(kind) {
      if (kind === 'svg:g')
         return new TextParseWrapper('g', this);
      if (kind === 'svg:text')
         return new TextParseWrapper('text', this);
      if (kind === 'svg:path')
         return new TextParseWrapper('path', this);
      console.warn('missing handle for svg', kind);
   }

   style(name, value) {
      if ((name === 'stroke-width') && value)
         this.stroke_width = Number.parseInt(value);
      return this;
   }

   property(name, value) {
      if (value === undefined)
         return this[name];
      this[name] = value;
      return this;
   }

   attr(name, value) {
      const get = () => {
         if (!value)
            return '';
         const res = value[0];
         value = value.slice(1);
         return res;
      }, getN = skip => {
         let p = 0;
         while (((value[p] >= '0') && (value[p] <= '9')) || (value[p] === '-'))
            p++;
         const res = Number.parseInt(value.slice(0, p));
         value = value.slice(p);
         if (skip)
            get();
         return res;
      };

      if ((name === 'font-size') && value)
         this.font_size = Number.parseInt(value);
      else if ((name === 'transform') && isStr(value) && (value.indexOf('translate') === 0)) {
         const arr = value.slice(value.indexOf('(') + 1, value.lastIndexOf(')')).split(',');
         this.x += arr[0] ? Number.parseInt(arr[0]) * 0.01 : 0;
         this.y -= arr[1] ? Number.parseInt(arr[1]) * 0.01 : 0;
      } else if ((name === 'x') && (this.kind === 'text'))
         this.x += Number.parseInt(value) * 0.01;
      else if ((name === 'y') && (this.kind === 'text'))
         this.y -= Number.parseInt(value) * 0.01;
      else if ((name === 'fill') && (this.kind === 'text'))
         this.fill = value;
      else if ((name === 'd') && (this.kind === 'path') && (value !== 'M0,0')) {
         if (get() !== 'M')
            return console.error('Not starts with M');
         let x1 = getN(true), y1 = getN(), next;
         const pnts = [], add_line = (x2, y2) => {
            const angle = Math.atan2(y2 - y1, x2 - x1),
                  dx = 0.5 * this.stroke_width * Math.sin(angle),
                  dy = -0.5 * this.stroke_width * Math.cos(angle);
            // front side
            pnts.push(x1 - dx, y1 - dy, 0, x2 - dx, y2 - dy, 0, x2 + dx, y2 + dy, 0, x1 - dx, y1 - dy, 0, x2 + dx, y2 + dy, 0, x1 + dx, y1 + dy, 0);
            // back side
            pnts.push(x1 - dx, y1 - dy, 0, x2 + dx, y2 + dy, 0, x2 - dx, y2 - dy, 0, x1 - dx, y1 - dy, 0, x1 + dx, y1 + dy, 0, x2 + dx, y2 + dy, 0);
            x1 = x2;
            y1 = y2;
         };

         while ((next = get())) {
            switch (next) {
               case 'L':
                  add_line(getN(true), getN());
                  continue;
               case 'l':
                  add_line(x1 + getN(true), y1 + getN());
                  continue;
               case 'H':
                  add_line(getN(), y1);
                  continue;
               case 'h':
                  add_line(x1 + getN(), y1);
                  continue;
               case 'V':
                  add_line(x1, getN());
                  continue;
               case 'v':
                  add_line(x1, y1 + getN());
                  continue;
               case 'a': {
                  const rx = getN(true), ry = getN(true),
                        angle = getN(true) / 180 * Math.PI, flag1 = getN(true);
                  getN(true); // skip unused flag2
                  const x2 = x1 + getN(true),
                        y2 = y1 + getN(),
                        x0 = x1 + rx * Math.cos(angle),
                        y0 = y1 + ry * Math.sin(angle);
                  let angle2 = Math.atan2(y0 - y2, x0 - x2);
                  if (flag1 && (angle2 < angle))
                     angle2 += 2 * Math.PI;
                  else if (!flag1 && (angle2 > angle))
                     angle2 -= 2 * Math.PI;

                  for (let cnt = 0; cnt < 10; ++cnt) {
                     const a = angle + (angle2 - angle) / 10 * (cnt + 1);
                     add_line(x0 - rx * Math.cos(a), y0 - ry * Math.sin(a));
                  }
                  continue;
               }
               default:
                  console.log('not supported path operator', next);
            }
         }

         if (pnts.length) {
            const pos = new Float32Array(pnts);
            this.geom = new THREE.BufferGeometry();
            this.geom.setAttribute('position', new THREE.BufferAttribute(pos, 3));
            this.geom.scale(0.01, -0.01, 0.01);
            this.geom.computeVertexNormals();
         }
      }
      return this;
   }

   text(v) {
      if (this.kind === 'text')
         this._text = v;
   }

   collect(geoms, geom_args, as_array) {
      if (this._text) {
         geom_args.size = Math.round(0.01 * this.font_size);
         const geom = new THREE.TextGeometry(this._text, geom_args);
         if (as_array) {
            // this is latex parsing
            // while three.js uses full height, make it more like normal fonts
            geom.scale(1, 0.9, 1);
            geom.translate(0, 0.0005 * this.font_size, 0);
         }
         geom.translate(this.x, this.y, 0);
         geom._fill = this.fill;
         geoms.push(geom);
      }
      if (this.geom) {
         this.geom.translate(this.x, this.y, 0);
         this.geom._fill = this.fill;
         geoms.push(this.geom);
      }

      this.childs.forEach(chld => {
         chld.x += this.x;
         chld.y += this.y;
         chld.collect(geoms, geom_args, as_array);
      });
   }

} // class TextParseWrapper


function createLatexGeometry(painter, lbl, size, as_array, use_latex = true) {
   const geom_args = { font: getHelveticaFont(), size, height: 0, curveSegments: 5 },
         font_size = size * 100,
         node = new TextParseWrapper('g', null, font_size),
         arg = { font_size, text: lbl, fast: true, font: { size: font_size, isMonospace: () => false, aver_width: 0.9 } },
         geoms = [];

   if (THREE.REVISION > 162)
      geom_args.depth = 0;
   else
      geom_args.height = 0;

   if (!isPlainText(lbl)) {
      produceLatex(painter, node, arg);
      node.collect(geoms, geom_args, as_array);
   }

   if (!geoms.length) {
      geom_args.size = size;
      const res = new THREE.TextGeometry(translateLaTeX(lbl), geom_args);
      return as_array ? [res] : res;
   }

   if (as_array)
      return geoms;

   if (geoms.length === 1)
      return geoms[0];

   let total_size = 0;
   geoms.forEach(geom => { total_size += geom.getAttribute('position').array.length; });

   const pos = new Float32Array(total_size),
         norm = new Float32Array(total_size);
   let indx = 0;

   geoms.forEach(geom => {
      const p1 = geom.getAttribute('position').array,
            n1 = geom.getAttribute('normal').array;
      for (let i = 0; i < p1.length; ++i, ++indx) {
         pos[indx] = p1[i];
         norm[indx] = n1[i];
      }
   });

   const fullgeom = new THREE.BufferGeometry();
   fullgeom.setAttribute('position', new THREE.BufferAttribute(pos, 3));
   fullgeom.setAttribute('normal', new THREE.BufferAttribute(norm, 3));
   return fullgeom;
}


/** @summary Build three.js object for the TLatex
  * @private */
function build3dlatex(obj, opt, painter, fp) {
   if (!painter)
      painter = new ObjectPainter(null, obj, opt);
   const handle = painter.createAttText({ attr: obj }),
         valign = handle.align % 10,
         halign = (handle.align - valign) / 10,
         text_size = handle.size > 1 ? handle.size : 2 * handle.size * (fp?.size_z3d || 100),
         arr3d = createLatexGeometry(painter, obj.fTitle, text_size || 10, true, fp || (obj._typename === clTLatex)),
         bb = new THREE.Box3().makeEmpty();

   arr3d.forEach(geom => {
      geom.computeBoundingBox();
      bb.expandByPoint(geom.boundingBox.max);
      bb.expandByPoint(geom.boundingBox.min);
   });

   let dx = 0, dy = 0;
   if (halign === 2)
      dx = 0.5 * (bb.max.x + bb.min.x);
   else if (halign === 3)
      dx = bb.max.x;

   if (valign === 2)
      dy = 0.5 * (bb.max.y + bb.min.y);
   else if (valign === 3)
      dy = bb.max.y;

   const obj3d = new THREE.Object3D(),
         materials = [],
         getMaterial = color => {
            if (!color)
               color = 'black';
            if (!materials[color])
               materials[color] = new THREE.MeshBasicMaterial(getMaterialArgs(color, { vertexColors: false }));
            return materials[color];
         };

   arr3d.forEach(geom => {
      geom.translate(-dx, -dy, 0);
      obj3d.add(new THREE.Mesh(geom, getMaterial(geom._fill || handle.color)));
   });

   return arr3d.length === 1 ? obj3d.children[0] : obj3d;
}

var latex3d = /*#__PURE__*/Object.freeze({
__proto__: null,
build3dlatex: build3dlatex,
createLatexGeometry: createLatexGeometry
});

/**
 * A math namespace - all functions can be exported from base/math.mjs.
 * Also all these functions can be used with TFormula calculations
 * @namespace Math
 */

/* eslint-disable curly */
/* eslint-disable no-loss-of-precision */
/* eslint-disable no-useless-assignment */
/* eslint-disable no-use-before-define */
/* eslint-disable no-else-return */
/* eslint-disable no-shadow */
/* eslint-disable operator-assignment */
/* eslint-disable @stylistic/js/comma-spacing */
/* eslint-disable @stylistic/js/no-floating-decimal */
/* eslint-disable @stylistic/js/space-in-parens */

// this can be improved later

/* eslint-disable eqeqeq */

const kMACHEP = 1.11022302462515654042363166809e-16,
      kMINLOG = -708.3964185322641,
      kMAXLOG = 709.782712893383973096206318587,
      kMAXSTIR = 108.116855767857671821730036754,
      kBig = 4.503599627370496e15,
      kBiginv = 2.22044604925031308085e-16,
      kSqrt2 = 1.41421356237309515,
      M_PI = 3.14159265358979323846264338328;

/** @summary Polynomialeval function
  * @desc calculates a value of a polynomial of the form:
  * a[0]x^N+a[1]x^(N-1) + ... + a[N]
  * @memberof Math */
function Polynomialeval(x, a, N) {
   if (!N)
      return a[0];

   let pom = a[0];
   for (let i = 1; i <= N; ++i)
      pom = pom * x + a[i];
   return pom;
}

/** @summary Polynomial1eval function
  * @desc calculates a value of a polynomial of the form:
  * x^N+a[0]x^(N-1) + ... + a[N-1]
  * @memberof Math */
function Polynomial1eval(x, a, N) {
   if (!N)
      return a[0];

   let pom = x + a[0];
   for (let i = 1; i < N; ++i)
      pom = pom * x + a[i];
   return pom;
}

/** @summary lgam function, logarithm from gamma
  * @memberof Math */
function lgam(x) {
   let p, q, u, w, z;
   const kMAXLGM = 2.556348e305,
         LS2PI = 0.91893853320467274178,
         A = [
            8.11614167470508450300E-4,
            -5950619042843014e-19,
            7.93650340457716943945E-4,
            -0.002777777777300997,
            8.33333333333331927722E-2
         ], B = [
            -1378.2515256912086,
            -38801.631513463784,
            -331612.9927388712,
            -1162370.974927623,
            -1721737.0082083966,
            -853555.6642457654
         ], C = [
            /* 1.00000000000000000000E0, */
            -351.81570143652345,
            -17064.210665188115,
            -220528.59055385445,
            -1139334.4436798252,
            -2532523.0717758294,
            -2018891.4143353277
         ];

   if ((x >= Number.MAX_VALUE) || (x == Number.POSITIVE_INFINITY))
      return Number.POSITIVE_INFINITY;

   if ( x < -34 ) {
      q = -x;
      w = lgam(q);
      p = Math.floor(q);
      if ( p === q ) // _unur_FP_same(p,q)
         return Number.POSITIVE_INFINITY;
      z = q - p;
      if ( z > 0.5 ) {
         p += 1.0;
         z = p - q;
      }
      z = q * Math.sin( Math.PI * z );
      if ( z < 1e-300 )
         return Number.POSITIVE_INFINITY;
      z = Math.log(Math.PI) - Math.log( z ) - w;
      return z;
   }
   if ( x < 13.0 ) {
      z = 1.0;
      p = 0.0;
      u = x;
      while ( u >= 3.0 ) {
         p -= 1.0;
         u = x + p;
         z *= u;
      }
      while ( u < 2.0 ) {
         if ( u < 1e-300 )
            return Number.POSITIVE_INFINITY;
         z /= u;
         p += 1.0;
         u = x + p;
      }
      if ( z < 0.0 ) {
         z = -z;
      }
      if ( u === 2.0 )
         return Math.log(z);
      p -= 2.0;
      x = x + p;
      p = x * Polynomialeval(x, B, 5 ) / Polynomial1eval( x, C, 6);
      return Math.log(z) + p;
   }
   if ( x > kMAXLGM )
      return Number.POSITIVE_INFINITY;

   q = ( x - 0.5 ) * Math.log(x) - x + LS2PI;
   if ( x > 1.0e8 )
      return q;

   p = 1.0 / (x * x);
   if ( x >= 1000.0 )
      q += ((7.9365079365079365079365e-4 * p
            - 2.7777777777777777777778e-3) * p
            + 0.0833333333333333333333) / x;
   else
      q += Polynomialeval( p, A, 4 ) / x;
   return q;
}

/** @summary Stirling formula for the gamma function
  * @memberof Math */
function stirf(x) {
   let y, w, v;

   const STIR = [
      7.87311395793093628397E-4,
      -22954996161337813e-20,
      -0.0026813261780578124,
      3.47222221605458667310E-3,
      8.33333333333482257126E-2
   ], SQTPI = Math.sqrt(2 * Math.PI);

   w = 1.0 / x;
   w = 1.0 + w * Polynomialeval( w, STIR, 4 );
   y = Math.exp(x);

   /*   #define kMAXSTIR kMAXLOG/log(kMAXLOG)  */

   if ( x > kMAXSTIR )
   { /* Avoid overflow in pow() */
      v = Math.pow( x, 0.5 * x - 0.25 );
      y = v * (v / y);
   }
   else
   {
      y = Math.pow( x, x - 0.5 ) / y;
   }
   y = SQTPI * y * w;
   return y;
}

/** @summary complementary error function
  * @memberof Math */
function erfc(a) {
   const erfP = [
      2.46196981473530512524E-10,
      5.64189564831068821977E-1,
      7.46321056442269912687E0,
      4.86371970985681366614E1,
      1.96520832956077098242E2,
      5.26445194995477358631E2,
      9.34528527171957607540E2,
      1.02755188689515710272E3,
      5.57535335369399327526E2
   ], erfQ = [
      1.32281951154744992508E1,
      8.67072140885989742329E1,
      3.54937778887819891062E2,
      9.75708501743205489753E2,
      1.82390916687909736289E3,
      2.24633760818710981792E3,
      1.65666309194161350182E3,
      5.57535340817727675546E2
   ], erfR = [
      5.64189583547755073984E-1,
      1.27536670759978104416E0,
      5.01905042251180477414E0,
      6.16021097993053585195E0,
      7.40974269950448939160E0,
      2.97886665372100240670E0
   ], erfS = [
      2.26052863220117276590E0,
      9.39603524938001434673E0,
      1.20489539808096656605E1,
      1.70814450747565897222E1,
      9.60896809063285878198E0,
      3.36907645100081516050E0
   ];

   let p,q,x,y,z;

   if ( a < 0.0 )
      x = -a;
   else
      x = a;

   if ( x < 1.0 )
      return 1.0 - erf(a);

   z = -a * a;

   if (z < -kMAXLOG)
      return (a < 0) ? 2.0 : 0.0;

   z = Math.exp(z);

   if ( x < 8.0 ) {
      p = Polynomialeval( x, erfP, 8 );
      q = Polynomial1eval( x, erfQ, 8 );
   } else {
      p = Polynomialeval( x, erfR, 5 );
      q = Polynomial1eval( x, erfS, 6 );
   }
   y = (z * p) / q;

   if (a < 0)
      y = 2.0 - y;

   if (y == 0)
      return (a < 0) ? 2.0 : 0.0;

   return y;
}

/** @summary error function
  * @memberof Math */
function erf(x) {
   if (Math.abs(x) > 1.0)
      return 1.0 - erfc(x);

   const erfT = [
      9.60497373987051638749E0,
      9.00260197203842689217E1,
      2.23200534594684319226E3,
      7.00332514112805075473E3,
      5.55923013010394962768E4
   ], erfU = [
      3.35617141647503099647E1,
      5.21357949780152679795E2,
      4.59432382970980127987E3,
      2.26290000613890934246E4,
      4.92673942608635921086E4
   ], z = x * x;

   return x * Polynomialeval(z, erfT, 4) / Polynomial1eval(z, erfU, 5);
}

/** @summary lognormal_cdf_c function
  * @memberof Math */
function lognormal_cdf_c(x, m, s, x0) {
   if (x0 === undefined)
      x0 = 0;
   const z = (Math.log((x - x0)) - m) / (s * kSqrt2);
   if (z > 1.)
      return 0.5 * erfc(z);
   else
      return 0.5 * (1.0 - erf(z));
}

/** @summary lognormal_cdf_c function
  * @memberof Math */
function lognormal_cdf(x, m, s, x0 = 0) {
   const z = (Math.log((x - x0)) - m) / (s * kSqrt2);
   if (z < -1)
      return 0.5 * erfc(-z);
   else
      return 0.5 * (1.0 + erf(z));
}

/** @summary normal_cdf_c function
  * @memberof Math */
function normal_cdf_c(x, sigma, x0 = 0) {
   const z = (x - x0) / (sigma * kSqrt2);
   if (z > 1.)
      return 0.5 * erfc(z);
   else
      return 0.5 * (1. - erf(z));
}

/** @summary normal_cdf function
  * @memberof Math */
function normal_cdf(x, sigma, x0 = 0) {
   const z = (x - x0) / (sigma * kSqrt2);
   if (z < -1)
      return 0.5 * erfc(-z);
   else
      return 0.5 * (1.0 + erf(z));
}

/** @summary log normal pdf
  * @memberof Math */
function lognormal_pdf(x, m, s, x0 = 0) {
   if ((x - x0) <= 0)
      return 0.0;
   const tmp = (Math.log((x - x0)) - m) / s;
   return 1.0 / ((x - x0) * Math.abs(s) * Math.sqrt(2 * M_PI)) * Math.exp(-(tmp * tmp) / 2);
}

/** @summary normal pdf
  * @memberof Math */
function normal_pdf(x, sigma = 1, x0 = 0) {
   const tmp = (x - x0) / sigma;
   return (1.0 / (Math.sqrt(2 * M_PI) * Math.abs(sigma))) * Math.exp(-tmp * tmp / 2);
}

/** @summary gamma calculation
  * @memberof Math */
function gamma(x) {
   let p, q, z, i, sgngam = 1;

   if (x >= Number.MAX_VALUE)
      return x;

   q = Math.abs(x);

   if ( q > 33.0 )
   {
      if ( x < 0.0 )
      {
         p = Math.floor(q);
         if ( p == q )
            return Number.POSITIVE_INFINITY;
         i = Math.round(p);
         if ( (i & 1) == 0 )
            sgngam = -1;
         z = q - p;
         if ( z > 0.5 )
         {
            p += 1.0;
            z = q - p;
         }
         z = q * Math.sin(Math.PI * z);
         if ( z == 0 )
         {
            return sgngam > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
         }
         z = Math.abs(z);
         z = Math.PI / (z * stirf(q));
      }
      else
      {
         z = stirf(x);
      }
      return sgngam * z;
   }

   z = 1.0;
   while ( x >= 3.0 ) {
      x -= 1.0;
      z *= x;
   }

   let small = false;

   while (( x < 0.0 ) && !small) {
      if ( x > -1e-9 )
         small = true;
      else {
         z /= x;
         x += 1.0;
      }
   }

   while (( x < 2.0 ) && !small) {
      if ( x < 1.e-9 )
         small = true;
      else {
         z /= x;
         x += 1.0;
      }
   }

   if (small) {
      if ( x == 0 )
         return Number.POSITIVE_INFINITY;
      else
         return z / ((1.0 + 0.5772156649015329 * x) * x);
   }

   if ( x == 2.0 )
      return z;

   const P = [
      1.60119522476751861407E-4,
      1.19135147006586384913E-3,
      1.04213797561761569935E-2,
      4.76367800457137231464E-2,
      2.07448227648435975150E-1,
      4.94214826801497100753E-1,
      9.99999999999999996796E-1
   ], Q = [
      -23158187332412014e-21,
      5.39605580493303397842E-4,
      -0.004456419138517973,
      1.18139785222060435552E-2,
      3.58236398605498653373E-2,
      -0.23459179571824335,
      7.14304917030273074085E-2,
      1.00000000000000000320E0];

   x -= 2.0;
   p = Polynomialeval( x, P, 6 );
   q = Polynomialeval( x, Q, 7 );
   return z * p / q;
}

/** @summary ndtri function
  * @memberof Math */
function ndtri(y0) {
   if ( y0 <= 0.0 )
      return Number.NEGATIVE_INFINITY;
   if ( y0 >= 1.0 )
      return Number.POSITIVE_INFINITY;

   const P0 = [
      -59.96335010141079,
      9.80010754185999661536E1,
      -56.67628574690703,
      1.39312609387279679503E1,
      -1.2391658386738125
   ], Q0 = [
      1.95448858338141759834E0,
      4.67627912898881538453E0,
      8.63602421390890590575E1,
      -225.46268785411937,
      2.00260212380060660359E2,
      -82.03722561683334,
      1.59056225126211695515E1,
      -1.1833162112133
   ], P1 = [
      4.05544892305962419923E0,
      3.15251094599893866154E1,
      5.71628192246421288162E1,
      4.40805073893200834700E1,
      1.46849561928858024014E1,
      2.18663306850790267539E0,
      -0.1402560791713545,
      -0.03504246268278482,
      -8574567851546854e-19
   ], Q1 = [
      1.57799883256466749731E1,
      4.53907635128879210584E1,
      4.13172038254672030440E1,
      1.50425385692907503408E1,
      2.50464946208309415979E0,
      -0.14218292285478779,
      -0.03808064076915783,
      -9332594808954574e-19
   ], P2 = [
      3.23774891776946035970E0,
      6.91522889068984211695E0,
      3.93881025292474443415E0,
      1.33303460815807542389E0,
      2.01485389549179081538E-1,
      1.23716634817820021358E-2,
      3.01581553508235416007E-4,
      2.65806974686737550832E-6,
      6.23974539184983293730E-9
   ], Q2 = [
      6.02427039364742014255E0,
      3.67983563856160859403E0,
      1.37702099489081330271E0,
      2.16236993594496635890E-1,
      1.34204006088543189037E-2,
      3.28014464682127739104E-4,
      2.89247864745380683936E-6,
      6.79019408009981274425E-9
   ], s2pi = 2.50662827463100050242e0, dd = 0.13533528323661269189;

   let code = 1, y = y0, x, y2, x1;

   if (y > (1.0 - dd)) {
      y = 1.0 - y;
      code = 0;
   }
   if ( y > dd ) {
      y = y - 0.5;
      y2 = y * y;
      x = y + y * (y2 * Polynomialeval( y2, P0, 4) / Polynomial1eval( y2, Q0, 8 ));
      x = x * s2pi;
      return x;
   }
   x = Math.sqrt(-2 * Math.log(y));
   const x0 = x - Math.log(x) / x,
         z = 1.0 / x;
   if ( x < 8.0 )
      x1 = z * Polynomialeval( z, P1, 8 ) / Polynomial1eval( z, Q1, 8 );
   else
      x1 = z * Polynomialeval( z, P2, 8 ) / Polynomial1eval( z, Q2, 8 );
   x = x0 - x1;
   if ( code != 0 )
      x = -x;
   return x;
}

/** @summary normal_quantile function
  * @memberof Math */
function normal_quantile(z, sigma) {
   return sigma * ndtri(z);
}

/** @summary normal_quantile_c function
  * @memberof Math */
function normal_quantile_c(z, sigma) {
   return -sigma * ndtri(z);
}

/** @summary igamc function
  * @memberof Math */
function igamc(a,x) {
   // LM: for negative values returns 0.0
   // This is correct if a is a negative integer since Gamma(-n) = +/- inf
   if (a <= 0)
      return 0.0;

   if (x <= 0)
      return 1.0;

   if ((x < 1.0) || (x < a))
      return (1.0 - igam(a,x));

   let ax = a * Math.log(x) - x - lgam(a);
   if ( ax < -kMAXLOG )
      return 0.0;

   ax = Math.exp(ax);

   /* continued fraction */
   let y = 1.0 - a,
       z = x + y + 1.0,
       c = 0.0,
       pkm2 = 1.0,
       qkm2 = x,
       pkm1 = x + 1.0,
       qkm1 = z * x,
       ans = pkm1 / qkm1,
       yc, r, t, pk, qk;

   do {
      c += 1.0;
      y += 1.0;
      z += 2.0;
      yc = y * c;
      pk = pkm1 * z - pkm2 * yc;
      qk = qkm1 * z - qkm2 * yc;
      if (qk)
      {
         r = pk / qk;
         t = Math.abs( (ans - r) / r );
         ans = r;
      }
      else
         t = 1.0;
      pkm2 = pkm1;
      pkm1 = pk;
      qkm2 = qkm1;
      qkm1 = qk;
      if ( Math.abs(pk) > kBig )
      {
         pkm2 *= kBiginv;
         pkm1 *= kBiginv;
         qkm2 *= kBiginv;
         qkm1 *= kBiginv;
      }
   } while ( t > kMACHEP );

   return ans * ax;
}

/** @summary igam function
  * @memberof Math */
function igam(a, x) {
   // LM: for negative values returns 1.0 instead of zero
   // This is correct if a is a negative integer since Gamma(-n) = +/- inf
   if (a <= 0)
      return 1.0;

   if (x <= 0)
      return 0.0;

   if ((x > 1.0) && (x > a))
      return 1.0 - igamc(a,x);

   /* Compute  x**a * exp(-x) / gamma(a)  */
   let ax = a * Math.log(x) - x - lgam(a);
   if ( ax < -kMAXLOG )
      return 0.0;

   ax = Math.exp(ax);

   /* power series */
   let r = a, c = 1.0, ans = 1.0;

   do {
      r += 1.0;
      c *= x / r;
      ans += c;
   } while ( c / ans > kMACHEP );

   return ans * ax / a;
}


/** @summary igami function
  * @memberof Math */
function igami(a, y0) {
   // check the domain
   if (a <= 0) {
      console.error(`igami : Wrong domain for parameter a = ${a} (must be > 0)`);
      return 0;
   }
   if (y0 <= 0) {
      return Number.POSITIVE_INFINITY;
   }
   if (y0 >= 1) {
      return 0;
   }
   const kMAXNUM = Number.MAX_VALUE, dithresh = 5.0 * kMACHEP;
   let x0 = kMAXNUM, x1 = 0, x, yl = 0, yh = 1, y, d, lgm, i, dir;

   /* approximation to inverse function */
   d = 1.0 / (9.0 * a);
   y = 1.0 - d - ndtri(y0) * Math.sqrt(d);
   x = a * y * y * y;

   lgm = lgam(a);

   for ( i = 0; i < 10; ++i ) {
      if ( x > x0 || x < x1 )
         break;
      y = igamc(a,x);
      if ( y < yl || y > yh )
         break;
      if ( y < y0 ) {
         x0 = x;
         yl = y;
      }
      else {
         x1 = x;
         yh = y;
      }
      /* compute the derivative of the function at this point */
      d = (a - 1.0) * Math.log(x) - x - lgm;
      if ( d < -kMAXLOG )
         break;
      d = -Math.exp(d);
      /* compute the step to the next approximation of x */
      d = (y - y0) / d;
      if ( Math.abs(d / x) < kMACHEP )
         return x;
      x = x - d;
   }
   /* Resort to interval halving if Newton iteration did not converge. */
   d = 0.0625;
   if ( x0 == kMAXNUM ) {
      if ( x <= 0.0 )
         x = 1.0;
      while ( x0 == kMAXNUM ) {
         x = (1.0 + d) * x;
         y = igamc( a, x );
         if ( y < y0 ) {
            x0 = x;
            yl = y;
            break;
         }
         d = d + d;
      }
   }
   d = 0.5;
   dir = 0;

   for ( i = 0; i < 400; ++i ) {
      x = x1 + d * (x0 - x1);
      y = igamc( a, x );
      lgm = (x0 - x1) / (x1 + x0);
      if ( Math.abs(lgm) < dithresh )
         break;
      lgm = (y - y0) / y0;
      if ( Math.abs(lgm) < dithresh )
         break;
      if ( x <= 0.0 )
         break;
      if ( y >= y0 ) {
         x1 = x;
         yh = y;
         if ( dir < 0 ) {
            dir = 0;
            d = 0.5;
         }
         else if ( dir > 1 )
            d = 0.5 * d + 0.5;
         else
            d = (y0 - yl) / (yh - yl);
         dir += 1;
      }
      else {
         x0 = x;
         yl = y;
         if ( dir > 0 ) {
            dir = 0;
            d = 0.5;
         }
         else if ( dir < -1 )
            d = 0.5 * d;
         else
            d = (y0 - yl) / (yh - yl);
         dir -= 1;
      }
   }
   return x;
}

/** @summary landau_pdf function
  * @desc LANDAU pdf : algorithm from CERNLIB G110 denlan
  *  same algorithm is used in GSL
  * @memberof Math */
function landau_pdf(x, xi, x0 = 0) {
   if (xi <= 0)
      return 0;
   const v = (x - x0) / xi;
   let u, ue, us, denlan;
   const p1 = [0.4259894875,-0.124976255, 0.03984243700, -0.006298287635, 0.001511162253],
         q1 = [1.0 ,-0.3388260629, 0.09594393323, -0.01608042283, 0.003778942063],
         p2 = [0.1788541609, 0.1173957403, 0.01488850518, -0.001394989411, 0.0001283617211],
         q2 = [1.0 , 0.7428795082, 0.3153932961, 0.06694219548, 0.008790609714],
         p3 = [0.1788544503, 0.09359161662,0.006325387654, 0.00006611667319,-2031049101e-15],
         q3 = [1.0 , 0.6097809921, 0.2560616665, 0.04746722384, 0.006957301675],
         p4 = [0.9874054407, 118.6723273, 849.2794360, -743.7792444, 427.0262186],
         q4 = [1.0 , 106.8615961, 337.6496214, 2016.712389, 1597.063511],
         p5 = [1.003675074, 167.5702434, 4789.711289, 21217.86767, -22324.9491],
         q5 = [1.0 , 156.9424537, 3745.310488, 9834.698876, 66924.28357],
         p6 = [1.000827619, 664.9143136, 62972.92665, 475554.6998, -5743609.109],
         q6 = [1.0 , 651.4101098, 56974.73333, 165917.4725, -2815759.939],
         a1 = [0.04166666667,-0.01996527778, 0.02709538966],
         a2 = [-1.84556867,-4.284640743];

   if (v < -5.5) {
      u = Math.exp(v + 1.0);
      if (u < 1e-10)
         return 0.0;
      ue = Math.exp(-1 / u);
      us = Math.sqrt(u);
      denlan = 0.3989422803 * (ue / us) * (1 + (a1[0] + (a1[1] + a1[2] * u) * u) * u);
   } else if (v < -1) {
      u = Math.exp(-v - 1);
      denlan = Math.exp(-u) * Math.sqrt(u) *
         (p1[0] + (p1[1] + (p1[2] + (p1[3] + p1[4] * v) * v) * v) * v) /
         (q1[0] + (q1[1] + (q1[2] + (q1[3] + q1[4] * v) * v) * v) * v);
   } else if (v < 1) {
      denlan = (p2[0] + (p2[1] + (p2[2] + (p2[3] + p2[4] * v) * v) * v) * v) /
         (q2[0] + (q2[1] + (q2[2] + (q2[3] + q2[4] * v) * v) * v) * v);
   } else if (v < 5) {
      denlan = (p3[0] + (p3[1] + (p3[2] + (p3[3] + p3[4] * v) * v) * v) * v) /
         (q3[0] + (q3[1] + (q3[2] + (q3[3] + q3[4] * v) * v) * v) * v);
   } else if (v < 12) {
      u = 1 / v;
      denlan = u * u * (p4[0] + (p4[1] + (p4[2] + (p4[3] + p4[4] * u) * u) * u) * u) /
         (q4[0] + (q4[1] + (q4[2] + (q4[3] + q4[4] * u) * u) * u) * u);
   } else if (v < 50) {
      u = 1 / v;
      denlan = u * u * (p5[0] + (p5[1] + (p5[2] + (p5[3] + p5[4] * u) * u) * u) * u) /
         (q5[0] + (q5[1] + (q5[2] + (q5[3] + q5[4] * u) * u) * u) * u);
   } else if (v < 300) {
      u = 1 / v;
      denlan = u * u * (p6[0] + (p6[1] + (p6[2] + (p6[3] + p6[4] * u) * u) * u) * u) /
         (q6[0] + (q6[1] + (q6[2] + (q6[3] + q6[4] * u) * u) * u) * u);
   } else {
      u = 1 / (v - v * Math.log(v) / (v + 1));
      denlan = u * u * (1 + (a2[0] + a2[1] * u) * u);
   }
   return denlan / xi;
}

/** @summary Landau function
  * @memberof Math */
function Landau(x, mpv, sigma, norm) {
   if (sigma <= 0)
      return 0;
   const den = landau_pdf((x - mpv) / sigma, 1, 0);
   if (!norm)
      return den;
   return den / sigma;
}

/** @summary inc_gamma_c
  * @memberof Math */
function inc_gamma_c(a,x) {
   return igamc(a,x);
}

/** @summary inc_gamma
  * @memberof Math */
function inc_gamma(a,x) {
   return igam(a,x);
}

/** @summary lgamma
  * @memberof Math */
function lgamma(z) {
   return lgam(z);
}

/** @summary Probability density function of the beta distribution.
  * @memberof Math */
function beta_pdf(x, a, b) {
   if (x < 0 || x > 1.0)
      return 0;
   if (x == 0 ) {
      if (a < 1)
         return Number.POSITIVE_INFINITY;
      else if (a > 1)
         return 0;
      else if ( a == 1)
         return b; // to avoid a nan from log(0)*0
   }
   if (x == 1 ) {
      if (b < 1)
         return Number.POSITIVE_INFINITY;
      else if (b > 1)
         return 0;
      else if ( b == 1)
         return a; // to avoid a nan from log(0)*0
   }
   return Math.exp(lgamma(a + b) - lgamma(a) - lgamma(b) +
                    Math.log(x) * (a - 1.) + Math.log1p(-x) * (b - 1.));
}

/** @summary beta
  * @memberof Math */
function beta(x,y) {
   return Math.exp(lgamma(x) + lgamma(y) - lgamma(x + y));
}

/** @summary chisquared_cdf_c
  * @memberof Math */
function chisquared_cdf_c(x,r,x0 = 0) {
   return inc_gamma_c(0.5 * r, 0.5 * (x - x0));
}

/** @summary Continued fraction expansion #1 for incomplete beta integral
  * @memberof Math */
function incbcf(a,b,x) {
   let xk, pk, pkm1, pkm2, qk, qkm1, qkm2,
       k1, k2, k3, k4, k5, k6, k7, k8,
       r, t, ans, n;
   const thresh = 3.0 * kMACHEP;

   k1 = a;
   k2 = a + b;
   k3 = a;
   k4 = a + 1.0;
   k5 = 1.0;
   k6 = b - 1.0;
   k7 = k4;
   k8 = a + 2.0;

   pkm2 = 0.0;
   qkm2 = 1.0;
   pkm1 = 1.0;
   qkm1 = 1.0;
   ans = 1.0;
   r = 1.0;
   n = 0;

   do {
      xk = -( x * k1 * k2 ) / ( k3 * k4 );
      pk = pkm1 + pkm2 * xk;
      qk = qkm1 + qkm2 * xk;
      pkm2 = pkm1;
      pkm1 = pk;
      qkm2 = qkm1;
      qkm1 = qk;

      xk = ( x * k5 * k6 ) / ( k7 * k8 );
      pk = pkm1 + pkm2 * xk;
      qk = qkm1 + qkm2 * xk;
      pkm2 = pkm1;
      pkm1 = pk;
      qkm2 = qkm1;
      qkm1 = qk;

      if ( qk != 0 )
         r = pk / qk;
      if ( r != 0 )
      {
         t = Math.abs( (ans - r) / r );
         ans = r;
      }
      else
         t = 1.0;

      if ( t < thresh )
         break; // goto cdone;

      k1 += 1.0;
      k2 += 1.0;
      k3 += 2.0;
      k4 += 2.0;
      k5 += 1.0;
      k6 -= 1.0;
      k7 += 2.0;
      k8 += 2.0;

      if ((Math.abs(qk) + Math.abs(pk)) > kBig) {
         pkm2 *= kBiginv;
         pkm1 *= kBiginv;
         qkm2 *= kBiginv;
         qkm1 *= kBiginv;
      }
      if ((Math.abs(qk) < kBiginv) || (Math.abs(pk) < kBiginv)) {
         pkm2 *= kBig;
         pkm1 *= kBig;
         qkm2 *= kBig;
         qkm1 *= kBig;
      }
   }
   while ( ++n < 300 );

   // cdone:
   return ans;
}

/** @summary Continued fraction expansion #2 for incomplete beta integral
  * @memberof Math */
function incbd(a,b,x) {
   const z = x / (1.0 - x),
         thresh = 3.0 * kMACHEP;

   let xk, pk, pkm1, pkm2, qk, qkm1, qkm2,
       k1, k2, k3, k4, k5, k6, k7, k8,
       r, t, ans, n;

   k1 = a;
   k2 = b - 1.0;
   k3 = a;
   k4 = a + 1.0;
   k5 = 1.0;
   k6 = a + b;
   k7 = a + 1.0;
   k8 = a + 2.0;

   pkm2 = 0.0;
   qkm2 = 1.0;
   pkm1 = 1.0;
   qkm1 = 1.0;
   ans = 1.0;
   r = 1.0;
   n = 0;
   do {
      xk = -( z * k1 * k2 ) / ( k3 * k4 );
      pk = pkm1 + pkm2 * xk;
      qk = qkm1 + qkm2 * xk;
      pkm2 = pkm1;
      pkm1 = pk;
      qkm2 = qkm1;
      qkm1 = qk;

      xk = ( z * k5 * k6 ) / ( k7 * k8 );
      pk = pkm1 + pkm2 * xk;
      qk = qkm1 + qkm2 * xk;
      pkm2 = pkm1;
      pkm1 = pk;
      qkm2 = qkm1;
      qkm1 = qk;

      if ( qk != 0 )
         r = pk / qk;
      if ( r != 0 )
      {
         t = Math.abs( (ans - r) / r );
         ans = r;
      }
      else
         t = 1.0;

      if ( t < thresh )
         break; // goto cdone;

      k1 += 1.0;
      k2 -= 1.0;
      k3 += 2.0;
      k4 += 2.0;
      k5 += 1.0;
      k6 += 1.0;
      k7 += 2.0;
      k8 += 2.0;

      if ((Math.abs(qk) + Math.abs(pk)) > kBig) {
         pkm2 *= kBiginv;
         pkm1 *= kBiginv;
         qkm2 *= kBiginv;
         qkm1 *= kBiginv;
      }
      if ((Math.abs(qk) < kBiginv) || (Math.abs(pk) < kBiginv)) {
         pkm2 *= kBig;
         pkm1 *= kBig;
         qkm2 *= kBig;
         qkm1 *= kBig;
      }
   }
   while ( ++n < 300 );
   // cdone:
   return ans;
}

/** @summary ROOT::Math::Cephes::pseries
  * @memberof Math */
function pseries(a,b,x) {
   let s, t, u, v, n;

   const ai = 1.0 / a;
   u = (1.0 - b) * x;
   v = u / (a + 1.0);
   const t1 = v;
   t = u;
   n = 2.0;
   s = 0.0;
   const z = kMACHEP * ai;
   while ( Math.abs(v) > z ) {
      u = (n - b) * x / n;
      t *= u;
      v = t / (a + n);
      s += v;
      n += 1.0;
   }
   s += t1;
   s += ai;

   u = a * Math.log(x);
   if ( (a + b) < kMAXSTIR && Math.abs(u) < kMAXLOG )
   {
      t = gamma(a + b) / (gamma(a) * gamma(b));
      s = s * t * Math.pow(x,a);
   }
   else
   {
      t = lgam(a + b) - lgam(a) - lgam(b) + u + Math.log(s);
      if ( t < kMINLOG )
         s = 0.0;
      else
         s = Math.exp(t);
   }
   return s;
}

/** @summary ROOT::Math::Cephes::incbet
  * @memberof Math */
function incbet(aa,bb,xx) {
   let a, b, t, x, xc, w, y, flag;

   if ( aa <= 0.0 || bb <= 0.0 )
      return 0.0;

   // LM: changed: for X > 1 return 1.
   if (xx <= 0.0)
      return 0.0;
   if (xx >= 1.0)
      return 1.0;

   flag = 0;

   /* - to test if that way is better for large b/  (comment out from Cephes version)
   if ( (bb * xx) <= 1.0 && xx <= 0.95)
   {
   t = pseries(aa, bb, xx);
   goto done;
   }

**/
   w = 1.0 - xx;

   /* Reverse a and b if x is greater than the mean. */
   /* aa,bb > 1 -> sharp rise at x=aa/(aa+bb) */
   if (xx > (aa / (aa + bb)))
   {
      flag = 1;
      a = bb;
      b = aa;
      xc = xx;
      x = w;
   }
   else
   {
      a = aa;
      b = bb;
      xc = w;
      x = xx;
   }

   if ( flag == 1 && (b * x) <= 1.0 && x <= 0.95) {
      t = pseries(a, b, x);
      // goto done;
   } else {
      /* Choose expansion for better convergence. */
      y = x * (a + b - 2.0) - (a - 1.0);
      if ( y < 0.0 )
         w = incbcf( a, b, x );
      else
         w = incbd( a, b, x ) / xc;

      /* Multiply w by the factor
      a      b   _             _     _
      x  (1-x)   | (a+b) / (a | (a) | (b)) .   */

      y = a * Math.log(x);
      t = b * Math.log(xc);
      if ( (a + b) < kMAXSTIR && Math.abs(y) < kMAXLOG && Math.abs(t) < kMAXLOG )
      {
         t = Math.pow(xc,b);
         t *= Math.pow(x,a);
         t /= a;
         t *= w;
         t *= gamma(a + b) / (gamma(a) * gamma(b));
         // goto done;
      } else {
      /* Resort to logarithms.  */
         y += t + lgam(a + b) - lgam(a) - lgam(b);
         y += Math.log(w / a);
         if ( y < kMINLOG )
            t = 0.0;
         else
            t = Math.exp(y);
      }
   }

   // done:

   if (flag == 1) {
      if ( t <= kMACHEP )
         t = 1.0 - kMACHEP;
      else
         t = 1.0 - t;
   }
   return t;
}

/** @summary copy of ROOT::Math::Cephes::incbi
  * @memberof Math */
function incbi(aa,bb,yy0) {
   let a, b, y0, d, y, x, x0, x1, lgm, yp, di, dithresh, yl, yh, xt,
       i, rflg, dir, nflg, ihalve = true;

   // check the domain
   if (aa <= 0) {
      // MATH_ERROR_MSG('Cephes::incbi','Wrong domain for parameter a (must be > 0)');
      return 0;
   }
   if (bb <= 0) {
      // MATH_ERROR_MSG('Cephes::incbi','Wrong domain for parameter b (must be > 0)');
      return 0;
   }

   const process_done = () => {
      if ( rflg ) {
         if ( x <= kMACHEP )
            x = 1.0 - kMACHEP;
         else
            x = 1.0 - x;
      }
      return x;
   };

   i = 0;
   if ( yy0 <= 0 )
      return 0.0;
   if ( yy0 >= 1.0 )
      return 1.0;
   x0 = 0.0;
   yl = 0.0;
   x1 = 1.0;
   yh = 1.0;
   nflg = 0;

   if ( aa <= 1.0 || bb <= 1.0 )
   {
      dithresh = 1.0e-6;
      rflg = 0;
      a = aa;
      b = bb;
      y0 = yy0;
      x = a / (a + b);
      y = incbet( a, b, x );
      // goto ihalve; // will start
   }
   else
   {
      dithresh = 1.0e-4;
      /* approximation to inverse function */

      yp = -ndtri(yy0);

      if ( yy0 > 0.5 )
      {
         rflg = 1;
         a = bb;
         b = aa;
         y0 = 1.0 - yy0;
         yp = -yp;
      }
      else
      {
         rflg = 0;
         a = aa;
         b = bb;
         y0 = yy0;
      }

      lgm = (yp * yp - 3.0) / 6.0;
      x = 2.0 / (1.0 / (2.0 * a - 1.0) + 1.0 / (2.0 * b - 1.0));
      d = yp * Math.sqrt( x + lgm ) / x
         - (1.0 / (2.0 * b - 1.0) - 1.0 / (2.0 * a - 1.0))
         * (lgm + 5.0 / 6.0 - 2.0 / (3.0 * x));
      d = 2.0 * d;
      if ( d < kMINLOG ) {
         // x = 1.0;
         // goto under;
         x = 0.0;
         return process_done();
      }
      x = a / (a + b * Math.exp(d));
      y = incbet( a, b, x );
      yp = (y - y0) / y0;
      if ( Math.abs(yp) < 0.2 )
         ihalve = false; // instead goto newt; exclude ihalve for the first time
   }

   let mainloop = 1000;

   // endless loop until coverage
   while (mainloop-- > 0) {
   /* Resort to interval halving if not close enough. */
   // ihalve:
      if (ihalve) {
         dir = 0;
         di = 0.5;
         for ( i = 0; i < 100; i++ )
         {
            if ( i != 0 )
            {
               x = x0 + di * (x1 - x0);
               if ( x == 1.0 )
                  x = 1.0 - kMACHEP;
               if ( x == 0.0 )
               {
                  di = 0.5;
                  x = x0 + di * (x1 - x0);
                  if ( x == 0.0 )
                     return process_done(); // goto under;
               }
               y = incbet( a, b, x );
               yp = (x1 - x0) / (x1 + x0);
               if ( Math.abs(yp) < dithresh )
                  break; // goto newt;
               yp = (y - y0) / y0;
               if ( Math.abs(yp) < dithresh )
                  break; // goto newt;
            }
            if ( y < y0 )
            {
               x0 = x;
               yl = y;
               if ( dir < 0 )
               {
                  dir = 0;
                  di = 0.5;
               }
               else if ( dir > 3 )
                  di = 1.0 - (1.0 - di) * (1.0 - di);
               else if ( dir > 1 )
                  di = 0.5 * di + 0.5;
               else
                  di = (y0 - y) / (yh - yl);
               dir += 1;
               if ( x0 > 0.75 )
               {
                  if ( rflg == 1 )
                  {
                     rflg = 0;
                     a = aa;
                     b = bb;
                     y0 = yy0;
                  }
                  else
                  {
                     rflg = 1;
                     a = bb;
                     b = aa;
                     y0 = 1.0 - yy0;
                  }
                  x = 1.0 - x;
                  y = incbet( a, b, x );
                  x0 = 0.0;
                  yl = 0.0;
                  x1 = 1.0;
                  yh = 1.0;
                  continue; // goto ihalve;
               }
            }
            else
            {
               x1 = x;
               if ( rflg == 1 && x1 < kMACHEP )
               {
                  x = 0.0;
                  return process_done(); // goto done;
               }
               yh = y;
               if ( dir > 0 )
               {
                  dir = 0;
                  di = 0.5;
               }
               else if ( dir < -3 )
                  di = di * di;
               else if ( dir < -1 )
                  di = 0.5 * di;
               else
                  di = (y - y0) / (yh - yl);
               dir -= 1;
            }
         }
         // math_error( 'incbi', PLOSS );
         if ( x0 >= 1.0 ) {
            x = 1.0 - kMACHEP;
            return process_done(); // goto done;
         }
         if ( x <= 0.0 ) {
            // math_error( 'incbi', UNDERFLOW );
            x = 0.0;
            return process_done(); // goto done;
         }
         break; // if here, break ihalve
      } // end of ihalve

      ihalve = true; // enter loop next time

      // newt:

      if ( nflg )
         return process_done(); // goto done;
      nflg = 1;
      lgm = lgam(a + b) - lgam(a) - lgam(b);

      for ( i = 0; i < 8; i++ )
      {
         /* Compute the function at this point. */
         if ( i != 0 )
            y = incbet(a,b,x);
         if ( y < yl )
         {
            x = x0;
            y = yl;
         }
         else if ( y > yh )
         {
            x = x1;
            y = yh;
         }
         else if ( y < y0 )
         {
            x0 = x;
            yl = y;
         }
         else
         {
            x1 = x;
            yh = y;
         }
         if ( x == 1.0 || x == 0.0 )
            break;
         /* Compute the derivative of the function at this point. */
         d = (a - 1.0) * Math.log(x) + (b - 1.0) * Math.log(1.0 - x) + lgm;
         if ( d < kMINLOG )
            return process_done(); // goto done;
         if ( d > kMAXLOG )
            break;
         d = Math.exp(d);
         /* Compute the step to the next approximation of x. */
         d = (y - y0) / d;
         xt = x - d;
         if ( xt <= x0 )
         {
            y = (x - x0) / (x1 - x0);
            xt = x0 + 0.5 * y * (x - x0);
            if ( xt <= 0.0 )
               break;
         }
         if ( xt >= x1 )
         {
            y = (x1 - x) / (x1 - x0);
            xt = x1 - 0.5 * y * (x1 - x);
            if ( xt >= 1.0 )
               break;
         }
         x = xt;
         if ( Math.abs(d / x) < 128.0 * kMACHEP )
            return process_done(); // goto done;
      }
      /* Did not converge.  */
      dithresh = 256.0 * kMACHEP;
   } // endless loop instead of // goto ihalve;

   // done:

   return process_done();
}

/** @summary Calculates the normalized (regularized) incomplete beta function.
  * @memberof Math */
function inc_beta(x,a,b) {
   return incbet(a,b,x);
}

const BetaIncomplete = inc_beta;

/** @summary ROOT::Math::beta_quantile
  * @memberof Math */
function beta_quantile(z,a,b) {
   return incbi(a,b,z);
}

/** @summary Complement of the cumulative distribution function of the beta distribution.
  * @memberof Math */
function beta_cdf_c(x,a,b) {
   return inc_beta(1 - x, b, a);
}

/** @summary chisquared_cdf
  * @memberof Math */
function chisquared_cdf(x,r,x0 = 0) {
   return inc_gamma(0.5 * r, 0.5 * (x - x0));
}

/** @summary gamma_quantile_c function
  * @memberof Math */
function gamma_quantile_c(z, alpha, theta) {
   return theta * igami( alpha, z);
}

/** @summary gamma_quantile function
  * @memberof Math */
function gamma_quantile(z, alpha, theta) {
   return theta * igami( alpha, 1. - z);
}

/** @summary breitwigner_cdf_c function
  * @memberof Math */
function breitwigner_cdf_c(x,gamma, x0 = 0) {
   return 0.5 - Math.atan(2.0 * (x - x0) / gamma) / M_PI;
}

/** @summary breitwigner_cdf function
  * @memberof Math */
function breitwigner_cdf(x, gamma, x0 = 0) {
   return 0.5 + Math.atan(2.0 * (x - x0) / gamma) / M_PI;
}

/** @summary cauchy_cdf_c function
  * @memberof Math */
function cauchy_cdf_c(x, b, x0 = 0) {
   return 0.5 - Math.atan( (x - x0) / b) / M_PI;
}

/** @summary cauchy_cdf function
  * @memberof Math */
function cauchy_cdf(x, b, x0 = 0) {
   return 0.5 + Math.atan( (x - x0) / b) / M_PI;
}

/** @summary cauchy_pdf function
  * @memberof Math */
function cauchy_pdf(x, b = 1, x0 = 0) {
   return b / (M_PI * ((x - x0) * (x - x0) + b * b));
}

/** @summary gaussian_pdf function
  * @memberof Math */
function gaussian_pdf(x, sigma = 1, x0 = 0) {
   const tmp = (x - x0) / sigma;
   return (1.0 / (Math.sqrt(2 * M_PI) * Math.abs(sigma))) * Math.exp(-tmp * tmp / 2);
}

/** @summary gamma_pdf function
  * @memberof Math */
function gamma_pdf(x, alpha, theta, x0 = 0) {
   if ((x - x0) < 0) {
      return 0.0;
   } else if ((x - x0) == 0) {
      return (alpha == 1) ? 1.0 / theta : 0;
   } else if (alpha == 1) {
      return Math.exp(-(x - x0) / theta) / theta;
   }
   return Math.exp((alpha - 1) * Math.log((x - x0) / theta) - (x - x0) / theta - lgamma(alpha)) / theta;
}

/** @summary tdistribution_cdf_c function
  * @memberof Math */
function tdistribution_cdf_c(x, r, x0 = 0) {
   const p = x - x0,
         sign = (p > 0) ? 1. : -1;
   return .5 - .5 * inc_beta(p * p / (r + p * p), .5, .5 * r) * sign;
}

/** @summary tdistribution_cdf function
  * @memberof Math */
function tdistribution_cdf(x, r, x0 = 0) {
   const p = x - x0,
         sign = (p > 0) ? 1. : -1;
   return .5 + .5 * inc_beta(p * p / (r + p * p), .5, .5 * r) * sign;
}

/** @summary tdistribution_pdf function
  * @memberof Math */
function tdistribution_pdf(x, r, x0 = 0) {
   return (Math.exp(lgamma((r + 1.0) / 2.0) - lgamma(r / 2.0)) / Math.sqrt(M_PI * r))
          * Math.pow((1.0 + (x - x0) * (x - x0) / r), -(r + 1.0) / 2.0);
}

/** @summary exponential_cdf_c function
  * @memberof Math */
function exponential_cdf_c(x, lambda, x0 = 0) {
   return ((x - x0) < 0) ? 1.0 : Math.exp(-lambda * (x - x0));
}

/** @summary exponential_cdf function
  * @memberof Math */
function exponential_cdf(x, lambda, x0 = 0) {
   return ((x - x0) < 0) ? 0.0 : -Math.expm1(-lambda * (x - x0));
}

/** @summary chisquared_pdf
  * @memberof Math */
function chisquared_pdf(x, r, x0 = 0) {
   if ((x - x0) < 0)
      return 0.0;
   const a = r / 2 - 1.;
   // let return inf for case x  = x0 and treat special case of r = 2 otherwise will return nan
   if (x == x0 && a == 0)
      return 0.5;

   return Math.exp((r / 2 - 1) * Math.log((x - x0) / 2) - (x - x0) / 2 - lgamma(r / 2)) / 2;
}

/** @summary Probability density function of the F-distribution.
  * @memberof Math */
function fdistribution_pdf(x, n, m, x0 = 0) {
   if (n < 0 || m < 0)
      return Number.NaN;
   if ((x - x0) < 0)
      return 0.0;

   return Math.exp((n / 2) * Math.log(n) + (m / 2) * Math.log(m) + lgamma((n + m) / 2) - lgamma(n / 2) - lgamma(m / 2)
                 + (n / 2 - 1) * Math.log(x - x0) - ((n + m) / 2) * Math.log(m + n * (x - x0)));
}

/** @summary fdistribution_cdf_c function
  * @memberof Math */
function fdistribution_cdf_c(x, n, m, x0 = 0) {
   if (n < 0 || m < 0)
      return Number.NaN;

   const z = m / (m + n * (x - x0));
   // fox z->1 and large a and b IB looses precision use complement function
   if (z > 0.9 && n > 1 && m > 1)
      return 1. - fdistribution_cdf(x, n, m, x0);

   // for the complement use the fact that IB(x,a,b) = 1. - IB(1-x,b,a)
   return inc_beta(m / (m + n * (x - x0)), .5 * m, .5 * n);
}

/** @summary fdistribution_cdf function
  * @memberof Math */
function fdistribution_cdf(x, n, m, x0 = 0) {
   if (n < 0 || m < 0)
      return Number.NaN;

   const z = n * (x - x0) / (m + n * (x - x0));
   // fox z->1 and large a and b IB looses precision use complement function
   if (z > 0.9 && n > 1 && m > 1)
      return 1. - fdistribution_cdf_c(x, n, m, x0);

   return inc_beta(z, .5 * n, .5 * m);
}

/** @summary Prob function
  * @memberof Math */
function Prob(chi2, ndf) {
   if (ndf <= 0)
      return 0; // Set CL to zero in case ndf <= 0

   if (chi2 <= 0) {
      if (chi2 < 0)
         return 0;
      else
         return 1;
   }

   return chisquared_cdf_c(chi2,ndf,0);
}

/** @summary Gaus function
  * @memberof Math */
function Gaus(x, mean, sigma, norm) {
   if (!sigma)
      return 1e30;
   const arg = (x - mean) / sigma;
   if (arg < -39 || arg > 39)
      return 0;
   const res = Math.exp(-0.5 * arg * arg);
   return norm ? res / (2.50662827463100024 * sigma) : res; // sqrt(2*Pi)=2.50662827463100024
}

/** @summary BreitWigner function
  * @memberof Math */
function BreitWigner(x, mean, gamma) {
   return gamma / ((x - mean) * (x - mean) + gamma * gamma / 4) / 2 / Math.PI;
}

/** @summary Calculates Beta-function Gamma(p)*Gamma(q)/Gamma(p+q).
  * @memberof Math */
function Beta(x,y) {
   return Math.exp(lgamma(x) + lgamma(y) - lgamma(x + y));
}

/** @summary GammaDist function
  * @memberof Math */
function GammaDist(x, gamma, mu = 0, beta = 1) {
   if ((x < mu) || (gamma <= 0) || (beta <= 0))
      return 0;
   return gamma_pdf(x, gamma, beta, mu);
}

/** @summary probability density function of Laplace distribution
  * @memberof Math */
function LaplaceDist(x, alpha = 0, beta = 1) {
   return Math.exp(-Math.abs((x - alpha) / beta)) / (2. * beta);
}

/** @summary distribution function of Laplace distribution
  * @memberof Math */
function LaplaceDistI(x, alpha = 0, beta = 1) {
   return (x <= alpha) ? 0.5 * Math.exp(-Math.abs((x - alpha) / beta)) : 1 - 0.5 * Math.exp(-Math.abs((x - alpha) / beta));
}

/** @summary density function for Student's t- distribution
  * @memberof Math */
function Student(T, ndf) {
   if (ndf < 1)
      return 0;

   const r = ndf,
         rh = 0.5 * r,
         rh1 = rh + 0.5,
         denom = Math.sqrt(r * Math.PI) * gamma(rh) * Math.pow(1 + T * T / r, rh1);
   return gamma(rh1) / denom;
}

/** @summary cumulative distribution function of Student's
  * @memberof Math */
function StudentI(T, ndf) {
   const r = ndf;

   return (T > 0)
     ? (1 - 0.5 * BetaIncomplete((r / (r + T * T)), r * 0.5, 0.5))
     : 0.5 * BetaIncomplete((r / (r + T * T)), r * 0.5, 0.5);
}

/** @summary LogNormal function
  * @memberof Math */
function LogNormal(x, sigma, theta = 0, m = 1) {
   if ((x < theta) || (sigma <= 0) || (m <= 0))
      return 0;
   return lognormal_pdf(x, Math.log(m), sigma, theta);
}

/** @summary Computes the probability density function of the Beta distribution
  * @memberof Math */
function BetaDist(x, p, q) {
   if ((x < 0) || (x > 1) || (p <= 0) || (q <= 0))
      return 0;
   const beta = Beta(p, q);
   return Math.pow(x, p - 1) * Math.pow(1 - x, q - 1) / beta;
}

/** @summary Computes the distribution function of the Beta distribution.
  * @memberof Math */
function BetaDistI(x, p, q) {
   if ((x < 0) || (x > 1) || (p <= 0) || (q <= 0))
      return 0;
   return BetaIncomplete(x, p, q);
}

/** @summary gaus function for TFormula
  * @memberof Math */
function gaus(f, x, i) {
   return f.GetParValue(i + 0) * Math.exp(-0.5 * Math.pow((x - f.GetParValue(i + 1)) / f.GetParValue(i + 2), 2));
}

/** @summary gausn function for TFormula
  * @memberof Math */
function gausn(f, x, i) {
   return gaus(f, x, i) / (Math.sqrt(2 * Math.PI) * f.GetParValue(i + 2));
}

/** @summary gausxy function for TFormula
  * @memberof Math */
function gausxy(f, x, y, i) {
   return f.GetParValue(i + 0) * Math.exp(-0.5 * Math.pow((x - f.GetParValue(i + 1)) / f.GetParValue(i + 2), 2))
                             * Math.exp(-0.5 * Math.pow((y - f.GetParValue(i + 3)) / f.GetParValue(i + 4), 2));
}

/** @summary expo function for TFormula
  * @memberof Math */
function expo(f, x, i) {
   return Math.exp(f.GetParValue(i + 0) + f.GetParValue(i + 1) * x);
}

/** @summary landau function for TFormula
  * @memberof Math */
function landau(f, x, i) {
   return Landau(x, f.GetParValue(i + 1),f.GetParValue(i + 2), false);
}

/** @summary landaun function for TFormula
  * @memberof Math */
function landaun(f, x, i) {
   return Landau(x, f.GetParValue(i + 1),f.GetParValue(i + 2), true);
}

/** @summary Crystal ball function
  * @memberof Math */
function crystalball_function(x, alpha, n, sigma, mean = 0) {
   if (sigma < 0.)
      return 0.;
   let z = (x - mean) / sigma;
   if (alpha < 0)
      z = -z;
   const abs_alpha = Math.abs(alpha);
   if (z > -abs_alpha)
      return Math.exp(-0.5 * z * z);
   const nDivAlpha = n / abs_alpha,
         AA = Math.exp(-0.5 * abs_alpha * abs_alpha),
         B = nDivAlpha - abs_alpha,
         arg = nDivAlpha / (B - z);
   return AA * Math.pow(arg,n);
}

/** @summary pdf definition of the crystal_ball which is defined only for n > 1 otherwise integral is diverging
  * @memberof Math */
function crystalball_pdf(x, alpha, n, sigma, mean = 0) {
   if (sigma < 0.)
      return 0.;
   if (n <= 1)
      return Number.NaN;  // pdf is not normalized for n <=1
   const abs_alpha = Math.abs(alpha),
         C = n / abs_alpha * 1. / (n - 1.) * Math.exp(-alpha * alpha / 2.),
         D = Math.sqrt(M_PI / 2.) * (1. + erf(abs_alpha / Math.sqrt(2.))),
         N = 1. / (sigma * (C + D));
   return N * crystalball_function(x,alpha,n,sigma,mean);
}

/** @summary compute the integral of the crystal ball function
  * @memberof Math */
function crystalball_integral(x, alpha, n, sigma, mean = 0) {
   if (sigma == 0)
      return 0;
   if (alpha == 0)
      return 0.;
   const useLog = (n == 1.0),
         abs_alpha = Math.abs(alpha);

   let z = (x - mean) / sigma, intgaus = 0., intpow = 0.;
   if (alpha < 0 )
      z = -z;

   const sqrtpiover2 = Math.sqrt(M_PI / 2.),
         sqrt2pi = Math.sqrt( 2. * M_PI),
         oneoversqrt2 = 1. / Math.sqrt(2.);
   if (z <= -abs_alpha) {
      const A = Math.pow(n / abs_alpha,n) * Math.exp(-0.5 * alpha * alpha),
            B = n / abs_alpha - abs_alpha;

      if (!useLog) {
         const C = (n / abs_alpha) * (1. / (n - 1)) * Math.exp(-alpha * alpha / 2.);
         intpow = C - A / (n - 1.) * Math.pow(B - z,-n + 1);
      }
      else {
         // for n=1 the primitive of 1/x is log(x)
         intpow = -A * Math.log( n / abs_alpha ) + A * Math.log(B - z);
      }
      intgaus = sqrtpiover2 * (1. + erf(abs_alpha * oneoversqrt2));
   } else {
      intgaus = normal_cdf_c(z, 1);
      intgaus *= sqrt2pi;
      intpow = 0;
   }
   return sigma * (intgaus + intpow);
}

/** @summary crystalball_cdf function
  * @memberof Math */
function crystalball_cdf(x, alpha, n, sigma, mean = 0) {
   if (n <= 1.)
      return Number.NaN;

   const abs_alpha = Math.abs(alpha),
         C = n / abs_alpha * 1. / (n - 1.) * Math.exp(-alpha * alpha / 2.),
         D = Math.sqrt(M_PI / 2.) * (1. + erf(abs_alpha / Math.sqrt(2.))),
         totIntegral = sigma * (C + D),
         integral = crystalball_integral(x,alpha,n,sigma,mean);

   return (alpha > 0) ? 1. - integral / totIntegral : integral / totIntegral;
}

/** @summary crystalball_cdf_c function
  * @memberof Math */
function crystalball_cdf_c(x, alpha, n, sigma, mean = 0) {
   if (n <= 1.)
      return Number.NaN;

   const abs_alpha = Math.abs(alpha),
         C = n / abs_alpha * 1. / (n - 1.) * Math.exp(-alpha * alpha / 2.),
         D = Math.sqrt(M_PI / 2.) * (1. + erf(abs_alpha / Math.sqrt(2.))),
         totIntegral = sigma * (C + D),
         integral = crystalball_integral(x,alpha,n,sigma,mean);

   return (alpha > 0) ? integral / totIntegral : 1. - (integral / totIntegral);
}

/** @summary ChebyshevN function
  * @memberof Math */
function ChebyshevN(n, x, c) {
   let d1 = 0.0, d2 = 0.0;
   const y2 = 2.0 * x;

   for (let i = n; i >= 1; i--) {
      const temp = d1;
      d1 = y2 * d1 - d2 + c[i];
      d2 = temp;
   }

   return x * d1 - d2 + c[0];
}

/** @summary Chebyshev0 function
  * @memberof Math */
function Chebyshev0(_x, c0) {
   return c0;
}

/** @summary Chebyshev1 function
  * @memberof Math */
function Chebyshev1(x, c0, c1) {
   return c0 + c1 * x;
}

/** @summary Chebyshev2 function
  * @memberof Math */
function Chebyshev2(x, c0, c1, c2) {
   return c0 + c1 * x + c2 * (2.0 * x * x - 1.0);
}

/** @summary Chebyshev3 function
  * @memberof Math */
function Chebyshev3(x, ...args) {
   return ChebyshevN(3, x, args);
}

/** @summary Chebyshev4 function
  * @memberof Math */
function Chebyshev4(x, ...args) {
   return ChebyshevN(4, x, args);
}

/** @summary Chebyshev5 function
  * @memberof Math */
function Chebyshev5(x, ...args) {
   return ChebyshevN(5, x, args);
}

/** @summary Chebyshev6 function
  * @memberof Math */
function Chebyshev6(x, ...args) {
   return ChebyshevN(6, x, args);
}

/** @summary Chebyshev7 function
  * @memberof Math */
function Chebyshev7(x, ...args) {
   return ChebyshevN(7, x, args);
}

/** @summary Chebyshev8 function
  * @memberof Math */
function Chebyshev8(x, ...args) {
   return ChebyshevN(8, x, args);
}

/** @summary Chebyshev9 function
  * @memberof Math */
function Chebyshev9(x, ...args) {
   return ChebyshevN(9, x, args);
}

/** @summary Chebyshev10 function
  * @memberof Math */
function Chebyshev10(x, ...args) {
   return ChebyshevN(10, x, args);
}

// =========================================================================

/** @summary Calculate ClopperPearson
  * @memberof Math */
function eff_ClopperPearson(total,passed,level,bUpper) {
   const alpha = (1.0 - level) / 2;
   if (bUpper)
      return ((passed == total) ? 1.0 : beta_quantile(1 - alpha,passed + 1,total - passed));

   return ((passed == 0) ? 0.0 : beta_quantile(alpha,passed,total - passed + 1.0));
}

/** @summary Calculate normal
  * @memberof Math */
function eff_Normal(total,passed,level,bUpper) {
   if (total == 0)
      return bUpper ? 1 : 0;

   const alpha = (1.0 - level) / 2,
         average = passed / total,
         sigma = Math.sqrt(average * (1 - average) / total),
         delta = normal_quantile(1 - alpha, sigma);

   if (bUpper)
      return ((average + delta) > 1) ? 1.0 : (average + delta);

   return ((average - delta) < 0) ? 0.0 : (average - delta);
}

/** @summary Calculates the boundaries for the frequentist Wilson interval
  * @memberof Math */
function eff_Wilson(total,passed,level,bUpper) {
   const alpha = (1.0 - level) / 2;
   if (total == 0)
      return bUpper ? 1 : 0;
   const average = passed / total,
         kappa = normal_quantile(1 - alpha,1),
         mode = (passed + 0.5 * kappa * kappa) / (total + kappa * kappa),
         delta = kappa / (total + kappa * kappa) * Math.sqrt(total * average * (1 - average) + kappa * kappa / 4);

   if (bUpper)
      return ((mode + delta) > 1) ? 1.0 : (mode + delta);

   return ((mode - delta) < 0) ? 0.0 : (mode - delta);
}

/** @summary Calculates the boundaries for the frequentist Agresti-Coull interval
  * @memberof Math */
function eff_AgrestiCoull(total,passed,level,bUpper) {
   const alpha = (1.0 - level) / 2,
         kappa = normal_quantile(1 - alpha,1),
         mode = (passed + 0.5 * kappa * kappa) / (total + kappa * kappa),
         delta = kappa * Math.sqrt(mode * (1 - mode) / (total + kappa * kappa));

   if (bUpper)
      return ((mode + delta) > 1) ? 1.0 : (mode + delta);

   return ((mode - delta) < 0) ? 0.0 : (mode - delta);
}

/** @summary Calculates the boundaries using the  mid-P binomial
  * @memberof Math */
function eff_MidPInterval(total,passed,level,bUpper) {
   const alpha = 1. - level, alpha_min = alpha / 2 , tol = 1e-9; // tolerance
   let pmin = 0, pmax = 1, p = 0;

   // treat special case for 0<passed<1
   // do a linear interpolation of the upper limit values
   if (passed > 0 && passed < 1) {
      const p0 = eff_MidPInterval(total, 0.0, level, bUpper),
            p1 = eff_MidPInterval(total, 1.0, level, bUpper);
      p = (p1 - p0) * passed + p0;
      return p;
   }

   while (Math.abs(pmax - pmin) > tol) {
      p = (pmin + pmax) / 2;
      // double v = 0.5 * ROOT::Math::binomial_pdf(int(passed), p, int(total));
      // make it work for non integer using the binomial - beta relationship
      let v = 0.5 * beta_pdf(p, passed + 1., total - passed + 1) / (total + 1);
      // if (passed > 0)
      //    v += ROOT::Math::binomial_cdf(int(passed - 1), p, int(total));
      // compute the binomial cdf at passed -1
      if ((passed - 1) >= 0)
         v += beta_cdf_c(p, passed, total - passed + 1);

      const vmin = bUpper ? alpha_min : 1. - alpha_min;
      if (v > vmin)
         pmin = p;
      else
         pmax = p;
   }

   return p;
}

/** @summary for a central confidence interval for a Beta distribution
  * @memberof Math */
function eff_Bayesian(total,passed,level,bUpper,alpha,beta) {
   const a = passed + alpha,
         b = total - passed + beta;
   if (bUpper) {
      if ((a > 0) && (b > 0))
         return beta_quantile((1 + level) / 2,a,b);
      else
         return 1;
   } else if ((a > 0) && (b > 0))
      return beta_quantile((1 - level) / 2,a,b);

   return 0;
}

/** @summary Return function to calculate boundary of TEfficiency
  * @memberof Math */
function getTEfficiencyBoundaryFunc(option, isbayessian) {
   const kFCP = 0,       // Clopper-Pearson interval (recommended by PDG)
         kFNormal = 1,   // Normal approximation
         kFWilson = 2,   // Wilson interval
         kFAC = 3,       // Agresti-Coull interval
         kFFC = 4,       // Feldman-Cousins interval, too complicated for JavaScript
         // kBJeffrey = 5,  // Jeffrey interval (Prior ~ Beta(0.5,0.5)
         // kBUniform = 6,  // Prior ~ Uniform = Beta(1,1)
         // kBBayesian = 7, // User specified Prior ~ Beta(fBeta_alpha,fBeta_beta)
         kMidP = 8;      // Mid-P Lancaster interval

   if (isbayessian)
      return eff_Bayesian;

   switch (option) {
      case kFCP:
         return eff_ClopperPearson;
      case kFNormal:
         return eff_Normal;
      case kFWilson:
         return eff_Wilson;
      case kFAC:
         return eff_AgrestiCoull;
      case kFFC:
         console.log('Feldman-Cousins interval kFFC not supported; using kFCP');
         return eff_ClopperPearson;
      case kMidP:
         return eff_MidPInterval;
      // case kBJeffrey:
      // case kBUniform:
      // case kBBayesian: return eff_ClopperPearson;
   }
   console.log(`Not recognized stat option ${option}, using kFCP`);
   return eff_ClopperPearson;
}

/** @summary Square function
  * @memberof Math */
function Sq(x) {
   return x * x;
}

/** @summary Pi function
  * @memberof Math */
function Pi() {
   return Math.PI;
}

/** @summary TwoPi function
  * @memberof Math */
function TwoPi() {
   return 2 * Math.PI;
}

/** @summary PiOver2 function
  * @memberof Math */
function PiOver2()
{
   return Math.PI / 2;
}

/** @summary PiOver4 function
  * @memberof Math */
function PiOver4()
{
   return Math.PI / 4;
}

/** @summary InvPi function
  * @memberof Math */
function InvPi()
{
   return 1 / Math.PI;
}

var jsroot_math = /*#__PURE__*/Object.freeze({
__proto__: null,
Beta: Beta,
BetaDist: BetaDist,
BetaDistI: BetaDistI,
BetaIncomplete: BetaIncomplete,
BreitWigner: BreitWigner,
Chebyshev0: Chebyshev0,
Chebyshev1: Chebyshev1,
Chebyshev10: Chebyshev10,
Chebyshev2: Chebyshev2,
Chebyshev3: Chebyshev3,
Chebyshev4: Chebyshev4,
Chebyshev5: Chebyshev5,
Chebyshev6: Chebyshev6,
Chebyshev7: Chebyshev7,
Chebyshev8: Chebyshev8,
Chebyshev9: Chebyshev9,
ChebyshevN: ChebyshevN,
FDist: fdistribution_pdf,
FDistI: fdistribution_cdf,
Gamma: gamma,
GammaDist: GammaDist,
Gaus: Gaus,
InvPi: InvPi,
Landau: Landau,
LaplaceDist: LaplaceDist,
LaplaceDistI: LaplaceDistI,
LogNormal: LogNormal,
Pi: Pi,
PiOver2: PiOver2,
PiOver4: PiOver4,
Polynomial1eval: Polynomial1eval,
Polynomialeval: Polynomialeval,
Prob: Prob,
Sq: Sq,
Student: Student,
StudentI: StudentI,
TwoPi: TwoPi,
beta: beta,
beta_cdf_c: beta_cdf_c,
beta_pdf: beta_pdf,
beta_quantile: beta_quantile,
breitwigner_cdf: breitwigner_cdf,
breitwigner_cdf_c: breitwigner_cdf_c,
cauchy_cdf: cauchy_cdf,
cauchy_cdf_c: cauchy_cdf_c,
cauchy_pdf: cauchy_pdf,
chisquared_cdf: chisquared_cdf,
chisquared_cdf_c: chisquared_cdf_c,
chisquared_pdf: chisquared_pdf,
crystalball_cdf: crystalball_cdf,
crystalball_cdf_c: crystalball_cdf_c,
crystalball_function: crystalball_function,
crystalball_pdf: crystalball_pdf,
erf: erf,
erfc: erfc,
expo: expo,
exponential_cdf: exponential_cdf,
exponential_cdf_c: exponential_cdf_c,
fdistribution_cdf: fdistribution_cdf,
fdistribution_cdf_c: fdistribution_cdf_c,
fdistribution_pdf: fdistribution_pdf,
gamma: gamma,
gamma_pdf: gamma_pdf,
gamma_quantile: gamma_quantile,
gamma_quantile_c: gamma_quantile_c,
gaus: gaus,
gausn: gausn,
gaussian_cdf: normal_cdf,
gaussian_cdf_c: normal_cdf_c,
gaussian_pdf: gaussian_pdf,
gausxy: gausxy,
getTEfficiencyBoundaryFunc: getTEfficiencyBoundaryFunc,
igam: igam,
igamc: igamc,
igami: igami,
inc_beta: inc_beta,
inc_gamma: inc_gamma,
inc_gamma_c: inc_gamma_c,
incbet: incbet,
incbi: incbi,
landau: landau,
landau_pdf: landau_pdf,
landaun: landaun,
lgam: lgam,
lgamma: lgamma,
lognormal_cdf: lognormal_cdf,
lognormal_cdf_c: lognormal_cdf_c,
lognormal_pdf: lognormal_pdf,
ndtri: ndtri,
normal_cdf: normal_cdf,
normal_cdf_c: normal_cdf_c,
normal_pdf: normal_pdf,
normal_quantile: normal_quantile,
normal_quantile_c: normal_quantile_c,
pseries: pseries,
stirf: stirf,
tdistribution_cdf: tdistribution_cdf,
tdistribution_cdf_c: tdistribution_cdf_c,
tdistribution_pdf: tdistribution_pdf,
tgamma: gamma
});

/** @summary Display progress message in the left bottom corner.
  * @desc Previous message will be overwritten
  * if no argument specified, any shown messages will be removed
  * @param {string} msg - message to display
  * @param {number} [tmout] - optional timeout in milliseconds, after message will disappear
  * @param {function} [click_handle] - optional handle to process click events
  * @private */
function showProgress(msg, tmout, click_handle) {
   if (isBatchMode() || (typeof document === 'undefined'))
      return;

   const id = 'jsroot_progressbox', modal = (settings.ProgressBox === 'modal') && isFunc(internals._modalProgress) ? internals._modalProgress : null;
   let box = select('#' + id);

   if (!settings.ProgressBox) {
      if (modal)
         modal();
      return box.remove();
   }

   if (!arguments.length || !msg) {
      if ((tmout !== -1) || (!box.empty() && box.property('with_timeout')))
         box.remove();
      if (modal)
         modal();
      return;
   }

   if (modal) {
      box.remove();
      modal(msg, click_handle);
   } else {
      if (box.empty()) {
         box = select(document.body)
               .append('div').attr('id', id)
               .attr('style', 'position: fixed; min-width: 100px; height: auto; overflow: visible; z-index: 101; border: 1px solid #999; background: #F8F8F8; left: 10px; bottom: 10px;');
         box.append('p');
      }

      box.property('with_timeout', false);

      const p = box.select('p');

      if (isStr(msg)) {
         p.html(msg)
          .on('click', isFunc(click_handle) ? click_handle : null)
          .attr('title', isFunc(click_handle) ? 'Click element to abort current operation' : '');
      }

      p.attr('style', 'font-size: 10px; margin-left: 10px; margin-right: 10px; margin-top: 3px; margin-bottom: 3px');
   }

   if (Number.isFinite(tmout) && (tmout > 0)) {
      if (!box.empty())
         box.property('with_timeout', true);
      setTimeout(() => showProgress('', -1), tmout);
   }
}

/** @summary Tries to close current browser tab
  * @desc Many browsers do not allow simple window.close() call,
  * therefore try several workarounds
  * @private */
function closeCurrentWindow() {
   if (typeof window !== 'undefined') {
      window.close();
      window.open('', '_self').close();
   }
}

/** @summary Tries to open ui5
  * @private */
function tryOpenOpenUI(sources, args) {
   if (!sources?.length) {
      if (isFunc(args.rejectFunc)) {
         args.rejectFunc(Error('openui5 was not possible to load'));
         args.rejectFunc = null;
      }
      return;
   }

   // where to take openui5 sources
   let src = sources.shift();

   if ((src.indexOf('roothandler') === 0) && (src.indexOf('://') < 0))
      src = src.replace(/:\//g, '://');

   if (settings.Debug)
      console.log('Try openui5 from ' + src);

   const element = document.createElement('script');
   element.setAttribute('type', 'text/javascript');
   element.setAttribute('id', 'sap-ui-bootstrap');
   // use nojQuery while we are already load jquery and jquery-ui, later one can use directly sap-ui-core.js

   // this is location of openui5 scripts when working with THttpServer or when scripts are installed inside JSROOT
   element.setAttribute('src', src + (args.ui5dbg ? 'resources/sap-ui-core-dbg.js' : 'resources/sap-ui-core.js')); // latest openui5 version

   element.setAttribute('data-sap-ui-libs', args.openui5libs ?? 'sap.m, sap.ui.layout, sap.ui.unified, sap.ui.commons');
   // element.setAttribute('data-sap-ui-language', args.openui5language ?? 'en');
   element.setAttribute('data-sap-ui-theme', args.openui5theme || (settings.DarkMode ? 'sap_fiori_3_dark' : 'sap_fiori_3'));
   element.setAttribute('data-sap-ui-compatVersion', 'edge');
   element.setAttribute('data-sap-ui-async', 'true');
   // element.setAttribute('data-sap-ui-bindingSyntax', 'complex');

   element.setAttribute('data-sap-ui-preload', 'async'); // '' to disable Component-preload.js

   element.setAttribute('data-sap-ui-evt-oninit', 'completeUI5Loading()');

   element.onerror = function() {
      // remove failed element
      element.parentNode.removeChild(element);
      // and try next
      tryOpenOpenUI(sources, args);
   };

   element.onload = function() {
      args.load_src = src;
   };

   document.head.appendChild(element);
}


/** @summary load openui5
  * @return {Promise} for loading ready
  * @private */
async function loadOpenui5(args) {
   // very simple - openui5 was loaded before and will be used as is
   if (typeof globalThis.sap === 'object')
      return globalThis.sap;

   if (!args)
      args = {};

   let rootui5sys = exports.source_dir.replace(/jsrootsys/g, 'rootui5sys');

   if (rootui5sys === exports.source_dir) {
      // if jsrootsys location not detected, try to guess it
      if (window.location.port && (window.location.pathname.indexOf('/win') >= 0) && (!args.openui5src || args.openui5src === 'nojsroot' || args.openui5src === 'jsroot'))
         rootui5sys = window.location.origin + window.location.pathname + '../rootui5sys/';
      else
         rootui5sys = undefined;
   }

   const openui5_sources = [];
   let openui5_dflt = 'https://openui5.hana.ondemand.com/1.135.0/',
       openui5_root = rootui5sys ? rootui5sys + 'distribution/' : '';

   if (isStr(args.openui5src)) {
      switch (args.openui5src) {
         case 'nodefault':
            openui5_dflt = '';
            break;
         case 'default':
            openui5_sources.push(openui5_dflt);
            openui5_dflt = '';
            break;
         case 'nojsroot':
            /* openui5_root = ''; */
            break;
         case 'jsroot':
            openui5_sources.push(openui5_root);
            openui5_root = '';
            break;
         default:
            openui5_sources.push(args.openui5src);
            break;
      }
   } else if (args.ui5dbg)
      openui5_root = ''; // exclude ROOT version in debug mode

   if (openui5_root && (openui5_sources.indexOf(openui5_root) < 0))
      openui5_sources.push(openui5_root);
   if (openui5_dflt && (openui5_sources.indexOf(openui5_dflt) < 0))
      openui5_sources.push(openui5_dflt);

   return new Promise((resolve, reject) => {
      args.resolveFunc = resolve;
      args.rejectFunc = reject;

      globalThis.completeUI5Loading = function() {
         console.log(`Load openui5 version ${globalThis.sap.ui.version} from ${args.load_src}`);

         globalThis.sap.ui.loader.config({
            paths: {
               jsroot: exports.source_dir,
               rootui5: rootui5sys
            }
         });

         if (args.resolveFunc) {
            args.resolveFunc(globalThis.sap);
            args.resolveFunc = null;
         }

         delete globalThis.completeUI5Loading;
      };

      tryOpenOpenUI(openui5_sources, args);
   });
}

/* eslint-disable @stylistic/js/key-spacing */
/* eslint-disable @stylistic/js/comma-spacing */
/* eslint-disable @stylistic/js/object-curly-spacing */

// some icons taken from http://uxrepo.com/
const ToolbarIcons = {
   camera: { path: 'M 152.00,304.00c0.00,57.438, 46.562,104.00, 104.00,104.00s 104.00-46.562, 104.00-104.00s-46.562-104.00-104.00-104.00S 152.00,246.562, 152.00,304.00z M 480.00,128.00L 368.00,128.00 c-8.00-32.00-16.00-64.00-48.00-64.00L 192.00,64.00 c-32.00,0.00-40.00,32.00-48.00,64.00L 32.00,128.00 c-17.60,0.00-32.00,14.40-32.00,32.00l0.00,288.00 c0.00,17.60, 14.40,32.00, 32.00,32.00l 448.00,0.00 c 17.60,0.00, 32.00-14.40, 32.00-32.00L 512.00,160.00 C 512.00,142.40, 497.60,128.00, 480.00,128.00z M 256.00,446.00c-78.425,0.00-142.00-63.574-142.00-142.00c0.00-78.425, 63.575-142.00, 142.00-142.00c 78.426,0.00, 142.00,63.575, 142.00,142.00 C 398.00,382.426, 334.427,446.00, 256.00,446.00z M 480.00,224.00l-64.00,0.00 l0.00-32.00 l 64.00,0.00 L 480.00,224.00 z' },
   disk: { path: 'M384,0H128H32C14.336,0,0,14.336,0,32v448c0,17.656,14.336,32,32,32h448c17.656,0,32-14.344,32-32V96L416,0H384z M352,160   V32h32v128c0,17.664-14.344,32-32,32H160c-17.664,0-32-14.336-32-32V32h128v128H352z M96,288c0-17.656,14.336-32,32-32h256   c17.656,0,32,14.344,32,32v192H96V288z' },
   question: { path: 'M256,512c141.375,0,256-114.625,256-256S397.375,0,256,0S0,114.625,0,256S114.625,512,256,512z M256,64   c63.719,0,128,36.484,128,118.016c0,47.453-23.531,84.516-69.891,110.016C300.672,299.422,288,314.047,288,320   c0,17.656-14.344,32-32,32c-17.664,0-32-14.344-32-32c0-40.609,37.25-71.938,59.266-84.031   C315.625,218.109,320,198.656,320,182.016C320,135.008,279.906,128,256,128c-30.812,0-64,20.227-64,64.672   c0,17.664-14.336,32-32,32s-32-14.336-32-32C128,109.086,193.953,64,256,64z M256,449.406c-18.211,0-32.961-14.75-32.961-32.969   c0-18.188,14.75-32.953,32.961-32.953c18.219,0,32.969,14.766,32.969,32.953C288.969,434.656,274.219,449.406,256,449.406z' },
   undo: { path: 'M450.159,48.042c8.791,9.032,16.983,18.898,24.59,29.604c7.594,10.706,14.146,22.207,19.668,34.489  c5.509,12.296,9.82,25.269,12.92,38.938c3.113,13.669,4.663,27.834,4.663,42.499c0,14.256-1.511,28.863-4.532,43.822  c-3.009,14.952-7.997,30.217-14.953,45.795c-6.955,15.577-16.202,31.52-27.755,47.826s-25.88,32.9-42.942,49.807  c-5.51,5.444-11.787,11.67-18.834,18.651c-7.033,6.98-14.496,14.366-22.39,22.168c-7.88,7.802-15.955,15.825-24.187,24.069  c-8.258,8.231-16.333,16.203-24.252,23.888c-18.3,18.13-37.354,37.016-57.191,56.65l-56.84-57.445  c19.596-19.472,38.54-38.279,56.84-56.41c7.75-7.685,15.772-15.604,24.108-23.757s16.438-16.163,24.33-24.057  c7.894-7.893,15.356-15.33,22.402-22.312c7.034-6.98,13.312-13.193,18.821-18.651c22.351-22.402,39.165-44.648,50.471-66.738  c11.279-22.09,16.932-43.567,16.932-64.446c0-15.785-3.217-31.005-9.638-45.671c-6.422-14.665-16.229-28.504-29.437-41.529  c-3.282-3.282-7.358-6.395-12.217-9.325c-4.871-2.938-10.381-5.503-16.516-7.697c-6.121-2.201-12.815-3.992-20.058-5.373  c-7.242-1.374-14.9-2.064-23.002-2.064c-8.218,0-16.802,0.834-25.788,2.507c-8.961,1.674-18.053,4.429-27.222,8.271  c-9.189,3.842-18.456,8.869-27.808,15.089c-9.358,6.219-18.521,13.819-27.502,22.793l-59.92,60.271l93.797,94.058H0V40.91  l93.27,91.597l60.181-60.532c13.376-15.018,27.222-27.248,41.536-36.697c14.308-9.443,28.608-16.776,42.89-21.992  c14.288-5.223,28.505-8.74,42.623-10.557C294.645,0.905,308.189,0,321.162,0c13.429,0,26.389,1.185,38.84,3.562  c12.478,2.377,24.2,5.718,35.192,10.029c11.006,4.311,21.126,9.404,30.374,15.265C434.79,34.724,442.995,41.119,450.159,48.042z' },
   arrow_right: { path: 'M30.796,226.318h377.533L294.938,339.682c-11.899,11.906-11.899,31.184,0,43.084c11.887,11.899,31.19,11.893,43.077,0  l165.393-165.386c5.725-5.712,8.924-13.453,8.924-21.539c0-8.092-3.213-15.84-8.924-21.551L338.016,8.925  C332.065,2.975,324.278,0,316.478,0c-7.802,0-15.603,2.968-21.539,8.918c-11.899,11.906-11.899,31.184,0,43.084l113.391,113.384  H30.796c-16.822,0-30.463,13.645-30.463,30.463C0.333,212.674,13.974,226.318,30.796,226.318z' },
   arrow_up: { path: 'M295.505,629.446V135.957l148.193,148.206c15.555,15.559,40.753,15.559,56.308,0c15.555-15.538,15.546-40.767,0-56.304  L283.83,11.662C276.372,4.204,266.236,0,255.68,0c-10.568,0-20.705,4.204-28.172,11.662L11.333,227.859  c-7.777,7.777-11.666,17.965-11.666,28.158c0,10.192,3.88,20.385,11.657,28.158c15.563,15.555,40.762,15.555,56.317,0  l148.201-148.219v493.489c0,21.993,17.837,39.82,39.82,39.82C277.669,669.267,295.505,651.439,295.505,629.446z' },
   arrow_diag: { path: 'M279.875,511.994c-1.292,0-2.607-0.102-3.924-0.312c-10.944-1.771-19.333-10.676-20.457-21.71L233.97,278.348  L22.345,256.823c-11.029-1.119-19.928-9.51-21.698-20.461c-1.776-10.944,4.031-21.716,14.145-26.262L477.792,2.149  c9.282-4.163,20.167-2.165,27.355,5.024c7.201,7.189,9.199,18.086,5.024,27.356L302.22,497.527  C298.224,506.426,289.397,511.994,279.875,511.994z M118.277,217.332l140.534,14.294c11.567,1.178,20.718,10.335,21.878,21.896  l14.294,140.519l144.09-320.792L118.277,217.332z' },
   auto_zoom: { path: 'M505.441,242.47l-78.303-78.291c-9.18-9.177-24.048-9.171-33.216,0c-9.169,9.172-9.169,24.045,0.006,33.217l38.193,38.188  H280.088V80.194l38.188,38.199c4.587,4.584,10.596,6.881,16.605,6.881c6.003,0,12.018-2.297,16.605-6.875  c9.174-9.172,9.174-24.039,0.011-33.217L273.219,6.881C268.803,2.471,262.834,0,256.596,0c-6.229,0-12.202,2.471-16.605,6.881  l-78.296,78.302c-9.178,9.172-9.178,24.045,0,33.217c9.177,9.171,24.051,9.171,33.21,0l38.205-38.205v155.4H80.521l38.2-38.188  c9.177-9.171,9.177-24.039,0.005-33.216c-9.171-9.172-24.039-9.178-33.216,0L7.208,242.464c-4.404,4.403-6.881,10.381-6.881,16.611  c0,6.227,2.477,12.207,6.881,16.61l78.302,78.291c4.587,4.581,10.599,6.875,16.605,6.875c6.006,0,12.023-2.294,16.61-6.881  c9.172-9.174,9.172-24.036-0.005-33.211l-38.205-38.199h152.593v152.063l-38.199-38.211c-9.171-9.18-24.039-9.18-33.216-0.022  c-9.178,9.18-9.178,24.059-0.006,33.222l78.284,78.302c4.41,4.404,10.382,6.881,16.611,6.881c6.233,0,12.208-2.477,16.611-6.881  l78.302-78.296c9.181-9.18,9.181-24.048,0-33.205c-9.174-9.174-24.054-9.174-33.21,0l-38.199,38.188v-152.04h152.051l-38.205,38.199  c-9.18,9.175-9.18,24.037-0.005,33.211c4.587,4.587,10.596,6.881,16.604,6.881c6.01,0,12.024-2.294,16.605-6.875l78.303-78.285  c4.403-4.403,6.887-10.378,6.887-16.611C512.328,252.851,509.845,246.873,505.441,242.47z' },
   statbox: {
      path: 'M28.782,56.902H483.88c15.707,0,28.451-12.74,28.451-28.451C512.331,12.741,499.599,0,483.885,0H28.782   C13.074,0,0.331,12.741,0.331,28.451C0.331,44.162,13.074,56.902,28.782,56.902z' +
         'M483.885,136.845H28.782c-15.708,0-28.451,12.741-28.451,28.451c0,15.711,12.744,28.451,28.451,28.451H483.88   c15.707,0,28.451-12.74,28.451-28.451C512.331,149.586,499.599,136.845,483.885,136.845z' +
         'M483.885,273.275H28.782c-15.708,0-28.451,12.731-28.451,28.452c0,15.707,12.744,28.451,28.451,28.451H483.88   c15.707,0,28.451-12.744,28.451-28.451C512.337,286.007,499.599,273.275,483.885,273.275z' +
         'M256.065,409.704H30.492c-15.708,0-28.451,12.731-28.451,28.451c0,15.707,12.744,28.451,28.451,28.451h225.585   c15.707,0,28.451-12.744,28.451-28.451C284.516,422.436,271.785,409.704,256.065,409.704z'
   },
   circle: { path: 'M256,256 m-150,0 a150,150 0 1,0 300,0 a150,150 0 1,0 -300,0' },
   three_circles: { path: 'M256,85 m-70,0 a70,70 0 1,0 140,0 a70,70 0 1,0 -140,0  M256,255 m-70,0 a70,70 0 1,0 140,0 a70,70 0 1,0 -140,0  M256,425 m-70,0 a70,70 0 1,0 140,0 a70,70 0 1,0 -140,0 ' },
   diamand: { path: 'M256,0L384,256L256,511L128,256z' },
   rect: { path: 'M90,90h352v352h-352z' },
   cross: { path: 'M80,40l176,176l176,-176l40,40l-176,176l176,176l-40,40l-176,-176l-176,176l-40,-40l176,-176l-176,-176z' },
   vrgoggles: { size: '245.82 141.73', path: 'M175.56,111.37c-22.52,0-40.77-18.84-40.77-42.07S153,27.24,175.56,27.24s40.77,18.84,40.77,42.07S198.08,111.37,175.56,111.37ZM26.84,69.31c0-23.23,18.25-42.07,40.77-42.07s40.77,18.84,40.77,42.07-18.26,42.07-40.77,42.07S26.84,92.54,26.84,69.31ZM27.27,0C11.54,0,0,12.34,0,28.58V110.9c0,16.24,11.54,30.83,27.27,30.83H99.57c2.17,0,4.19-1.83,5.4-3.7L116.47,118a8,8,0,0,1,12.52-.18l11.51,20.34c1.2,1.86,3.22,3.61,5.39,3.61h72.29c15.74,0,27.63-14.6,27.63-30.83V28.58C245.82,12.34,233.93,0,218.19,0H27.27Z' },
   th2colorz: { recs: [{ x: 128, y: 486, w: 256, h: 26, f: 'rgb(38,62,168)' }, { y: 461, f: 'rgb(22,82,205)' }, { y: 435, f: 'rgb(16,100,220)' }, { y: 410, f: 'rgb(18,114,217)' }, { y: 384, f: 'rgb(20,129,214)' }, { y: 358, f: 'rgb(14,143,209)' }, { y: 333, f: 'rgb(9,157,204)' }, { y: 307, f: 'rgb(13,167,195)' }, { y: 282, f: 'rgb(30,175,179)' }, { y: 256, f: 'rgb(46,183,164)' }, { y: 230, f: 'rgb(82,186,146)' }, { y: 205, f: 'rgb(116,189,129)' }, { y: 179, f: 'rgb(149,190,113)' }, { y: 154, f: 'rgb(179,189,101)' }, { y: 128, f: 'rgb(209,187,89)' }, { y: 102, f: 'rgb(226,192,75)' }, { y: 77, f: 'rgb(244,198,59)' }, { y: 51, f: 'rgb(253,210,43)' }, { y: 26, f: 'rgb(251,230,29)' }, { y: 0, f: 'rgb(249,249,15)' }] },
   th2color: { recs: [{x:0,y:256,w:13,h:39,f:'rgb(38,62,168)'},{x:13,y:371,w:39,h:39},{y:294,h:39},{y:256,h:39},{y:218,h:39},{x:51,y:410,w:39,h:39},{y:371,h:39},{y:333,h:39},{y:294},{y:256,h:39},{y:218,h:39},{y:179,h:39},{y:141,h:39},{y:102,h:39},{y:64},{x:90,y:448,w:39,h:39},{y:410},{y:371,h:39},{y:333,h:39,f:'rgb(22,82,205)'},{y:294},{y:256,h:39,f:'rgb(16,100,220)'},{y:218,h:39},{y:179,h:39,f:'rgb(22,82,205)'},{y:141,h:39},{y:102,h:39,f:'rgb(38,62,168)'},{y:64},{y:0,h:27},{x:128,y:448,w:39,h:39},{y:410},{y:371,h:39},{y:333,h:39,f:'rgb(22,82,205)'},{y:294,f:'rgb(20,129,214)'},{y:256,h:39,f:'rgb(9,157,204)'},{y:218,h:39,f:'rgb(14,143,209)'},{y:179,h:39,f:'rgb(20,129,214)'},{y:141,h:39,f:'rgb(16,100,220)'},{y:102,h:39,f:'rgb(22,82,205)'},{y:64,f:'rgb(38,62,168)'},{y:26,h:39},{y:0,h:27},{x:166,y:486,h:14},{y:448,h:39},{y:410},{y:371,h:39,f:'rgb(22,82,205)'},{y:333,h:39,f:'rgb(20,129,214)'},{y:294,f:'rgb(82,186,146)'},{y:256,h:39,f:'rgb(179,189,101)'},{y:218,h:39,f:'rgb(116,189,129)'},{y:179,h:39,f:'rgb(82,186,146)'},{y:141,h:39,f:'rgb(14,143,209)'},{y:102,h:39,f:'rgb(16,100,220)'},{y:64,f:'rgb(38,62,168)'},{y:26,h:39},{x:205,y:486,w:39,h:14},{y:448,h:39},{y:410},{y:371,h:39,f:'rgb(16,100,220)'},{y:333,h:39,f:'rgb(9,157,204)'},{y:294,f:'rgb(149,190,113)'},{y:256,h:39,f:'rgb(244,198,59)'},{y:218,h:39},{y:179,h:39,f:'rgb(226,192,75)'},{y:141,h:39,f:'rgb(13,167,195)'},{y:102,h:39,f:'rgb(18,114,217)'},{y:64,f:'rgb(22,82,205)'},{y:26,h:39,f:'rgb(38,62,168)'},{x:243,y:448,w:39,h:39},{y:410},{y:371,h:39,f:'rgb(18,114,217)'},{y:333,h:39,f:'rgb(30,175,179)'},{y:294,f:'rgb(209,187,89)'},{y:256,h:39,f:'rgb(251,230,29)'},{y:218,h:39,f:'rgb(249,249,15)'},{y:179,h:39,f:'rgb(226,192,75)'},{y:141,h:39,f:'rgb(30,175,179)'},{y:102,h:39,f:'rgb(18,114,217)'},{y:64,f:'rgb(38,62,168)'},{y:26,h:39},{x:282,y:448,h:39},{y:410},{y:371,h:39,f:'rgb(18,114,217)'},{y:333,h:39,f:'rgb(14,143,209)'},{y:294,f:'rgb(149,190,113)'},{y:256,h:39,f:'rgb(226,192,75)'},{y:218,h:39,f:'rgb(244,198,59)'},{y:179,h:39,f:'rgb(149,190,113)'},{y:141,h:39,f:'rgb(9,157,204)'},{y:102,h:39,f:'rgb(18,114,217)'},{y:64,f:'rgb(38,62,168)'},{y:26,h:39},{x:320,y:448,w:39,h:39},{y:410},{y:371,h:39,f:'rgb(22,82,205)'},{y:333,h:39,f:'rgb(20,129,214)'},{y:294,f:'rgb(46,183,164)'},{y:256,h:39},{y:218,h:39,f:'rgb(82,186,146)'},{y:179,h:39,f:'rgb(9,157,204)'},{y:141,h:39,f:'rgb(20,129,214)'},{y:102,h:39,f:'rgb(16,100,220)'},{y:64,f:'rgb(38,62,168)'},{y:26,h:39},{x:358,y:448,h:39},{y:410},{y:371,h:39,f:'rgb(22,82,205)'},{y:333,h:39},{y:294,f:'rgb(16,100,220)'},{y:256,h:39,f:'rgb(20,129,214)'},{y:218,h:39,f:'rgb(14,143,209)'},{y:179,h:39,f:'rgb(18,114,217)'},{y:141,h:39,f:'rgb(22,82,205)'},{y:102,h:39,f:'rgb(38,62,168)'},{y:64},{y:26,h:39},{x:397,y:448,w:39,h:39},{y:371,h:39},{y:333,h:39},{y:294,f:'rgb(22,82,205)'},{y:256,h:39},{y:218,h:39},{y:179,h:39,f:'rgb(38,62,168)'},{y:141,h:39},{y:102,h:39},{y:64},{y:26,h:39},{x:435,y:410,h:39},{y:371,h:39},{y:333,h:39},{y:294},{y:256,h:39},{y:218,h:39},{y:179,h:39},{y:141,h:39},{y:102,h:39},{y:64},{x:474,y:256,h:39},{y:179,h:39}] },
   th2draw3d: {
      path: 'M172.768,0H51.726C23.202,0,0.002,23.194,0.002,51.712v89.918c0,28.512,23.2,51.718,51.724,51.718h121.042   c28.518,0,51.724-23.2,51.724-51.718V51.712C224.486,23.194,201.286,0,172.768,0z M177.512,141.63c0,2.611-2.124,4.745-4.75,4.745   H51.726c-2.626,0-4.751-2.134-4.751-4.745V51.712c0-2.614,2.125-4.739,4.751-4.739h121.042c2.62,0,4.75,2.125,4.75,4.739 L177.512,141.63L177.512,141.63z ' +
            'M460.293,0H339.237c-28.521,0-51.721,23.194-51.721,51.712v89.918c0,28.512,23.2,51.718,51.721,51.718h121.045   c28.521,0,51.721-23.2,51.721-51.718V51.712C512.002,23.194,488.802,0,460.293,0z M465.03,141.63c0,2.611-2.122,4.745-4.748,4.745   H339.237c-2.614,0-4.747-2.128-4.747-4.745V51.712c0-2.614,2.133-4.739,4.747-4.739h121.045c2.626,0,4.748,2.125,4.748,4.739 V141.63z ' +
            'M172.768,256.149H51.726c-28.524,0-51.724,23.205-51.724,51.726v89.915c0,28.504,23.2,51.715,51.724,51.715h121.042   c28.518,0,51.724-23.199,51.724-51.715v-89.915C224.486,279.354,201.286,256.149,172.768,256.149z M177.512,397.784   c0,2.615-2.124,4.736-4.75,4.736H51.726c-2.626-0.006-4.751-2.121-4.751-4.736v-89.909c0-2.626,2.125-4.753,4.751-4.753h121.042 c2.62,0,4.75,2.116,4.75,4.753L177.512,397.784L177.512,397.784z ' +
            'M460.293,256.149H339.237c-28.521,0-51.721,23.199-51.721,51.726v89.915c0,28.504,23.2,51.715,51.721,51.715h121.045   c28.521,0,51.721-23.199,51.721-51.715v-89.915C512.002,279.354,488.802,256.149,460.293,256.149z M465.03,397.784   c0,2.615-2.122,4.736-4.748,4.736H339.237c-2.614,0-4.747-2.121-4.747-4.736v-89.909c0-2.626,2.121-4.753,4.747-4.753h121.045 c2.615,0,4.748,2.116,4.748,4.753V397.784z'
   },

   /* eslint-enable @stylistic/js/key-spacing */
   /* eslint-enable @stylistic/js/comma-spacing */
   /* eslint-enable @stylistic/js/object-curly-spacing */

   createSVG(group, btn, size, title, arg) {
      const use_dark = (arg === true) || (arg === false) ? arg : settings.DarkMode,
            opacity0 = (arg === 'browser') ? (browser.touches ? 0.2 : 0) : (use_dark ? 0.8 : 0.2),
            svg = group.append('svg:svg')
                     .attr('width', size + 'px')
                     .attr('height', size + 'px')
                     .attr('viewBox', '0 0 512 512')
                     .style('overflow', 'hidden')
                     .style('cursor', 'pointer')
                     .style('fill', use_dark ? 'rgba(255, 224, 160)' : 'steelblue')
                     .style('opacity', opacity0)
                     .property('opacity0', opacity0)
                     .property('opacity1', use_dark ? 1 : 0.8)
                     .on('mouseenter', function() {
                        const elem = select(this);
                        elem.style('opacity', elem.property('opacity1'));
                        const func = elem.node()._mouseenter;
                        if (isFunc(func))
                           func();
                     })
                     .on('mouseleave', function() {
                        const elem = select(this);
                        elem.style('opacity', elem.property('opacity0'));
                        const func = elem.node()._mouseleave;
                        if (isFunc(func))
                           func();
                     });

      if ('recs' in btn) {
         const rec = {};
         for (let n = 0; n < btn.recs.length; ++n) {
            Object.assign(rec, btn.recs[n]);
            svg.append('rect').attr('x', rec.x).attr('y', rec.y)
               .attr('width', rec.w).attr('height', rec.h)
               .style('fill', rec.f);
         }
      } else
         svg.append('svg:path').attr('d', btn.path);


      //  special rect to correctly get mouse events for whole button area
      svg.append('svg:rect').attr('x', 0).attr('y', 0).attr('width', 512).attr('height', 512)
         .style('opacity', 0).style('fill', 'none').style('pointer-events', 'visibleFill')
         .append('svg:title').text(title);

      return svg;
   }

}; // ToolbarIcons


/** @summary Register handle to react on window resize
  * @desc function used to react on browser window resize event
  * While many resize events could come in short time,
  * resize will be handled with delay after last resize event
  * @param {object|string} handle can be function or object with checkResize function or dom where painting was done
  * @param {number} [delay] - one could specify delay after which resize event will be handled
  * @protected */
function registerForResize(handle, delay) {
   if (!handle || isBatchMode() || (typeof window === 'undefined') || (typeof document === 'undefined'))
      return;

   let myInterval = null;

   function ResizeTimer() {
      myInterval = null;

      document.body.style.cursor = 'wait';
      if (isFunc(handle))
         handle();
      else if (isFunc(handle?.checkResize))
         handle.checkResize();
      else {
         const node = new BasePainter(handle).selectDom();
         if (!node.empty()) {
            const mdi = node.property('mdi');
            if (isFunc(mdi?.checkMDIResize))
               mdi.checkMDIResize();
            else
               resize(node.node());
         }
      }
      document.body.style.cursor = 'auto';
   }

   window.addEventListener('resize', () => {
      if (myInterval)
         clearTimeout(myInterval);
      myInterval = setTimeout(ResizeTimer, Math.max(20, delay || 300));
   });
}

/** @summary Detect mouse right button
  * @private */
function detectRightButton(event) {
   return (event?.buttons === 2) || (event?.button === 2);
}

/** @summary Add move handlers for drawn element
  * @private */
function addMoveHandler(painter, enabled = true, hover_handler = false) {
   if (!settings.MoveResize || painter.isBatchMode() || !painter.getG())
      return;

   if (painter.getPadPainter()?.isEditable() === false)
      enabled = false;

   if (!enabled) {
      if (painter.getG().property('assigned_move')) {
         const drag_move = drag().subject(Object);
         drag_move.on('start', null).on('drag', null).on('end', null);
         painter.getG()
               .style('cursor', null)
               .property('assigned_move', null)
               .call(drag_move);
      }
      return;
   }

   if (painter.getG().property('assigned_move'))
      return;

   const drag_move = drag().subject(Object);
   let not_changed = true, move_disabled = false;

   drag_move
      .on('start', function(evnt) {
         move_disabled = this.moveEnabled ? !this.moveEnabled() : false;
         if (move_disabled || detectRightButton(evnt.sourceEvent))
            return;
         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();
         const pos = pointer(evnt, this.getG().node());
         not_changed = true;
         if (this.moveStart)
            this.moveStart(pos[0], pos[1], evnt.sourceEvent);
      }.bind(painter)).on('drag', function(evnt) {
         if (move_disabled)
            return;
         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();
         not_changed = false;
         if (this.moveDrag)
            this.moveDrag(evnt.dx, evnt.dy, evnt.sourceEvent);
      }.bind(painter)).on('end', function(evnt) {
         if (move_disabled)
            return;
         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();
         if (this.moveEnd)
            this.moveEnd(not_changed, evnt.sourceEvent);

         let arg = null;
         if (not_changed) {
            // if not changed - provide click position
            const pos = pointer(evnt, this.getG().node());
            arg = { x: pos[0], y: pos[1], dbl: false };
         }
         this.getPadPainter()?.selectObjectPainter(this, arg);
      }.bind(painter));

   painter.getG()
          .style('cursor', hover_handler ? 'pointer' : 'move')
          .property('assigned_move', true)
          .call(drag_move);

   if (hover_handler) {
      painter.getG().on('mouseenter', () => painter.getG().style('text-decoration', 'underline'))
                    .on('mouseleave', () => painter.getG().style('text-decoration', null));
   }
}

/** @summary Inject style
  * @param {String} code - css string
  * @private */
function injectStyle(code, node, tag) {
   if (isBatchMode() || !code || (typeof document === 'undefined'))
      return true;

   const styles = (node || document).getElementsByTagName('style');
   for (let n = 0; n < styles.length; ++n) {
      if (tag && styles[n].getAttribute('tag') === tag) {
         styles[n].innerText = code;
         return true;
      }

      if (styles[n].innerText === code)
         return true;
   }

   const element = document.createElement('style');
   if (tag)
      element.setAttribute('tag', tag);
   element.innerText = code;
   (node || document.head).appendChild(element);
   return true;
}

/** @summary Select predefined style
  * @private */
function selectgStyle(name) {
   gStyle.fName = name;
   switch (name) {
      case 'Modern': Object.assign(gStyle, { fFrameBorderMode: 0, fFrameFillColor: 0,
         fCanvasBorderMode: 0, fCanvasColor: 0, fPadBorderMode: 0, fPadColor: 0, fStatColor: 0,
         fTitleAlign: 23, fTitleX: 0.5, fTitleBorderSize: 0, fTitleColor: 0, fTitleStyle: 0,
         fOptStat: 1111, fStatY: 0.935,
         fLegendBorderSize: 1, fLegendFont: 42, fLegendTextSize: 0, fLegendFillColor: 0 });
         break;
      case 'Plain': Object.assign(gStyle, { fFrameBorderMode: 0,
         fCanvasBorderMode: 0, fPadBorderMode: 0, fPadColor: 0, fCanvasColor: 0,
         fTitleColor: 0, fTitleBorderSize: 0, fStatColor: 0, fStatBorderSize: 1, fLegendBorderSize: 1 });
         break;
      case 'Bold': Object.assign(gStyle, { fCanvasColor: 10, fCanvasBorderMode: 0,
         fFrameLineWidth: 3, fFrameFillColor: 10,
         fPadColor: 10, fPadTickX: 1, fPadTickY: 1, fPadBottomMargin: 0.15, fPadLeftMargin: 0.15,
         fTitleColor: 10, fTitleTextColor: 600, fStatColor: 10 });
         break;
   }
}

let _storage_prefix = 'jsroot_';

/** @summary Set custom prefix for the local storage
  * @private */
function setStoragePrefix(prefix) {
   _storage_prefix = prefix || 'jsroot_';
}

/** @summary Save object in local storage
  * @private */
function saveLocalStorage(obj, expires, name) {
   if (typeof localStorage === 'undefined')
      return;
   if (Number.isFinite(expires) && (expires < 0))
      localStorage.removeItem(_storage_prefix + name);
   else
      localStorage.setItem(_storage_prefix + name, btoa_func(JSON.stringify(obj)));
}

/** @summary Read object from storage with specified name
  * @private */
function readLocalStorage(name) {
   if (typeof localStorage === 'undefined')
      return null;
   const v = localStorage.getItem(_storage_prefix + name),
         s = v ? JSON.parse(atob_func(v)) : null;
   return isObject(s) ? s : null;
}

/** @summary Save JSROOT settings in local storage
  * @param {Number} [expires] - delete settings when negative
  * @param {String} [name] - storage name, 'settings' by default
  * @private */
function saveSettings(expires = 365, name = 'settings') {
   saveLocalStorage(settings, expires, name);
}

/** @summary Read JSROOT settings from specified cookie parameter
  * @param {Boolean} only_check - when true just checks if settings were stored before with provided name
  * @param {String} [name] - storage name, 'settings' by default
  * @private */
function readSettings(only_check = false, name = 'settings') {
   const s = readLocalStorage(name);
   if (!s)
      return false;
   if (!only_check)
      Object.assign(settings, s);
   return true;
}

/** @summary Save JSROOT gStyle object in local storage
  * @param {Number} [expires] - delete style when negative
  * @param {String} [name] - storage name, 'style' by default
  * @private */
function saveStyle(expires = 365, name = 'style') {
   saveLocalStorage(gStyle, expires, name);
}

/** @summary Read JSROOT gStyle object from local storage
  * @param {Boolean} [only_check] - when true just checks if settings were stored before with provided name
  * @param {String} [name] - storage name, 'style' by default
  * @private */
function readStyle(only_check = false, name = 'style') {
   const s = readLocalStorage(name);
   if (!s)
      return false;
   if (!only_check)
      Object.assign(gStyle, s);
   return true;
}

let _saveFileFunc = null;

/** @summary Returns image file content as it should be stored on the disc
  * @desc Replaces all kind of base64 coding
  * @private */
function getBinFileContent(content) {
   if (content.indexOf(prSVG) === 0)
      return decodeURIComponent(content.slice(prSVG.length));

   if (content.indexOf(prJSON) === 0)
      return decodeURIComponent(content.slice(prJSON.length));

   if ((content.indexOf('data:image/') === 0) || (content.indexOf('data:application/pdf') === 0)) {
      const p = content.indexOf('base64,');
      if (p > 0)
         return atob_func(content.slice(p + 7));
   }

   return content;
}

/** @summary Returns type of file content
  * @private */
function getContentType(content) {
   if (content.indexOf('data:'))
      return '';

   const p = content.indexOf(';');
   return (p > 0) ? content.slice(5, p) : '';
}

/** @summary Function store content as file with filename
  * @private */
async function saveFile(filename, content) {
   if (isFunc(_saveFileFunc))
      return _saveFileFunc(filename, getBinFileContent(content));
   if (isNodeJs()) {
      return Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(fs => {
         fs.writeFileSync(filename, getBinFileContent(content));
         return true;
      });
   } else if (typeof document === 'undefined')
      return false;

   const a = document.createElement('a');
   a.download = filename;
   a.style.display = 'none';
   let fileURL = '';
   const contentType = getContentType(content);

   if ((content.length > 1e6) && (contentType === 'application/pdf')) {
      // large PDF files do not work in the browser with plain base64 coding
      const bindata = getBinFileContent(content),
            len = bindata.length,
            buffer = new ArrayBuffer(len),
            view = new DataView(buffer, 0, len);
      for (let i = 0; i < len; ++i)
         view.setUint8(i, bindata.charCodeAt(i));
      const blob = new Blob([buffer], { type: contentType });
      fileURL = URL.createObjectURL(blob);
      a.href = fileURL;
   } else
      a.href = content;

   document.body.appendChild(a);

   return new Promise(resolve => {
      a.addEventListener('click', () => {
         if (fileURL) {
            setTimeout(() => {
               a.parentNode.removeChild(a);
               URL.revokeObjectURL(fileURL);
            }, 3000);
         } else
            a.parentNode.removeChild(a);
         resolve(true);
      });
      a.click();
   });
}

/** @summary Function store content as file with filename
  * @private */
function setSaveFile(func) {
   _saveFileFunc = func;
}

/** @summary Returns color id for the color
  * @private */
function getColorId(col) {
   const arr = getRootColors();
   let id = -1;
   if (isStr(col)) {
      if (!col || (col === 'none'))
         id = 0;
      else {
         for (let k = 1; k < arr.length; ++k) {
            if (arr[k] === col) {
               id = k;
               break;
            }
         }
      }
      if ((id < 0) && (col.indexOf('rgb') === 0))
         id = 9999;
   } else if (Number.isInteger(col) && arr[col]) {
      id = col;
      col = arr[id];
   }

   return { id, col };
}

/** @summary Produce exec string for WebCanvas to set color value
  * @desc Color can be id or string, but should belong to list of known colors
  * For higher color numbers TColor::GetColor(r,g,b) will be invoked to ensure color is exists
  * @private */
function getColorExec(col, method) {
   const d = getColorId(col);

   if (d.id < 0)
      return '';

   // for higher color numbers ensure that such color exists
   if (d.id >= 50) {
      const c = color(d.col);
      d.id = `TColor::GetColor(${c.r},${c.g},${c.b})`;
   }

   return `exec:${method}(${d.id})`;
}

/** @summary Change object member in the painter
  * @desc Used when interactively change in the menu
  * Special handling for color is provided
  * @private */
function changeObjectMember(painter, member, val, is_color) {
   if (is_color) {
      const d = getColorId(val);
      if ((d.id < 0) || (d.id === 9999))
         return;
      val = d.id;
   }

   const obj = painter?.getObject();
   if (obj && (obj[member] !== undefined))
      obj[member] = val;
}

Object.assign(internals.jsroot, { addMoveHandler, registerForResize });

const kToFront = '__front__', kNoReorder = '__no_reorder',
      sDfltName = 'root_ctx_menu', sDfltDlg = '_dialog',
      sSub = 'sub:', sEndsub = 'endsub:',
      sColumn = 'column:', sEndcolumn = 'endcolumn:',
      sSeparator = 'separator', sHeader = 'header:';

/**
 * @summary Abstract class for creating context menu
 *
 * @desc Use {@link createMenu} to create instance of the menu
 * @private
 */

class JSRootMenu {

   constructor(painter, menuname, show_event) {
      this.painter = painter;
      this.menuname = menuname;
      if (isObject(show_event) && (show_event.clientX !== undefined) && (show_event.clientY !== undefined))
         this.show_evnt = { clientX: show_event.clientX, clientY: show_event.clientY, skip_close: show_event.skip_close };

      this.remove_handler = () => this.remove();
      this.element = null;
      this.cnt = 0;
   }

   native() { return false; }

   async load() { return this; }

   /** @summary Returns object with mouse event position when context menu was activated
     * @desc Return object will have members 'clientX' and 'clientY' */
   getEventPosition() { return this.show_evnt; }

   add(/* name, arg, func, title */) {
      throw Error('add() method has to be implemented in the menu');
   }

   /** @summary Returns menu size */
   size() { return this.cnt; }

   /** @summary Close and remove menu */
   remove() {
      if (!this.element)
         return;

      if (this.show_evnt?.skip_close) {
         this.show_evnt.skip_close = 0;
         return;
      }

      this.element.remove();
      this.element = null;
      if (isFunc(this.resolveFunc)) {
         const func = this.resolveFunc;
         delete this.resolveFunc;
         func();
      }
      document.body.removeEventListener('click', this.remove_handler);
   }

   show(/* event */) {
      throw Error('show() method has to be implemented in the menu class');
   }

   /** @summary Add checked menu item
     * @param {boolean} flag - flag
     * @param {string} name - item name
     * @param {function} func - func called when item is selected
     * @param {string} [title] - optional title */
   addchk(flag, name, arg, func, title) {
      let handler = func;
      if (isFunc(arg)) {
         title = func;
         func = arg;
         handler = res => func(res === '1');
         arg = flag ? '0' : '1';
      }
      this.add((flag ? 'chk:' : 'unk:') + name, arg, handler, title);
   }

   /** @summary Add sub-menu */
   sub(name, arg, func, title) {
      this.add(sSub + name, arg, func, title);
   }

   /** @summary Mark end of submenu */
   endsub() { this.add(sEndsub); }

   /** @summary Start column with items */
   column() { this.add(sColumn); }

   /** @summary End column with items */
   endcolumn() { this.add(sEndcolumn); }

   /** @summary Add separator */
   separator() { this.add(sSeparator); }

   /** @summary Add menu header - must be first entry */
   header(name, title) {
      this.add(sHeader + name, undefined, undefined, title);
   }

   /** @summary Add draw sub-menu with draw options
     * @protected */
   addDrawMenu(top_name, opts, call_back, title) {
      if (!opts || !opts.length)
         return;

      let without_sub = false;
      if (top_name.indexOf('nosub:') === 0) {
         without_sub = true;
         top_name = top_name.slice(6);
      }

      if (opts.length === 1) {
         if (opts[0] === kInspect)
            top_name = top_name.replace('Draw', 'Inspect');
         this.add(top_name, opts[0], call_back);
         return;
      }

      const used = {};

      if (!without_sub)
         this.sub(top_name, opts[0], call_back, title);

      if ((opts.indexOf('') >= 0) && (!without_sub || opts[0]))
         this.add(this._use_plain_text ? '<dflt>' : '&lt;dflt&gt;', '', call_back);

      for (let i = 0; i < opts.length; ++i) {
         let name = opts[i];
         if (!name || used[name])
            continue;
         used[name] = true;

         const group = [];
         if (opts.length > 5) {
            // check if there are similar options, which can be grouped again
            for (let i2 = i + 1; i2 < opts.length; ++i2) {
               if (opts[i2] && !used[opts[i2]] && (opts[i2].indexOf(name) === 0))
                  group.push(opts[i2]);
               else if (name.length < 4)
                  break;
            }
         }

         if (without_sub)
            name = top_name + ' ' + name;

         if (group.length) {
            this.sub(name, opts[i], call_back);
            group.forEach(sub => {
               this.add(sub, sub, call_back);
               used[sub] = true;
            });
            this.endsub();
         } else if (name === kInspect) {
            this.sub(name, opts[i], call_back, 'Inspect object content');
            for (let k = 0; k < 10; ++k)
               this.add(k.toString(), kInspect + k, call_back, `Inspect object and expand to level ${k}`);
            this.endsub();
         } else
            this.add(name, opts[i], call_back);
      }
      if (!without_sub) {
         this.add('<input>', () => {
            const opt = isFunc(this.painter?.getDrawOpt) ? this.painter.getDrawOpt() : opts[0];
            this.input('Provide draw option', opt, 'text').then(call_back);
         }, 'Enter draw option in dialog');
         this.endsub();
      }
   }

   /** @summary Add redraw menu for the painter
     * @protected */
   addRedrawMenu(painter) {
      if (!painter || !isFunc(painter.redrawWith) || !isFunc(painter.getSupportedDrawOptions))
         return false;

      const opts = painter.getSupportedDrawOptions();

      this.addDrawMenu(`Draw ${painter.getClassName()} with`, opts, arg => {
         if ((arg.indexOf(kInspect) === 0) && isFunc(painter.showInspector))
            return painter.showInspector(arg);

         painter.redrawWith(arg);
      });
      return true;
   }

   /** @summary Add color selection menu entries
     * @protected */
   addColorMenu(name, value, set_func, fill_kind) {
      if (value === undefined)
         return;
      const useid = !isStr(value);
      this.sub(name, () => {
         this.input('Enter color ' + (useid ? '(only id number)' : '(name or id)'), value, useid ? 'int' : 'text', useid ? 0 : undefined, useid ? 9999 : undefined).then(col => {
            const id = parseInt(col);
            if (Number.isInteger(id) && getColor(id))
               col = getColor(id);
            else if (useid)
               return;

            set_func(useid ? id : col);
         });
      });

      for (let ncolumn = 0; ncolumn < 5; ++ncolumn) {
         this.column();

         for (let nrow = 0; nrow < 10; nrow++) {
            let n = ncolumn * 10 + nrow;
            if (!useid)
               --n; // use -1 as none color

            const col = (n < 0) || ((n === 0) && (fill_kind === 1)) ? 'none' : getColor(n),
                  lbl = (n <= 0) || ((col[0] !== '#') && (col.indexOf('rgb') < 0)) ? col : `col ${n}`,
                  fill = (n === 1) ? 'white' : 'black',
                  stroke = (n === 1) ? 'red' : 'black',
                  rect = (value === (useid ? n : col)) ? `<rect width="50" height="18" style="fill:none;stroke-width:3px;stroke:${stroke}"></rect>` : '',
                  svg = `<svg width="50" height="18" style="margin:0px;background-color:${col}">${rect}<text x="4" y="12" style='font-size:12px' fill="${fill}">${lbl}</text></svg>`;

            this.add(svg, (useid ? n : col), res => set_func(useid ? parseInt(res) : res), 'Select color ' + col);
         }

         this.endcolumn();
         if (!this.native())
            break;
      }

      this.endsub();
   }

   /** @summary Add size selection menu entries
     * @protected */
   addSizeMenu(name, min, max, step, size_value, set_func, title) {
      if (size_value === undefined)
         return;

      let values = [], miss_current = false;
      if (isObject(step)) {
         values = step;
         step = 1;
      } else {
         for (let sz = min; sz <= max; sz += step)
            values.push(sz);
      }

      const match = v => Math.abs(v - size_value) < (max - min) * 1e-5,
            conv = (v, more) => {
               if ((v === size_value) && miss_current)
                  more = true;
               if (step >= 1)
                  return v.toFixed(0);
               if (step >= 0.1)
                  return v.toFixed(more ? 2 : 1);
               return v.toFixed(more ? 4 : 2);
            };

      if (values.findIndex(match) < 0) {
         miss_current = true;
         values.push(size_value);
         values = values.sort((a, b) => a > b);
      }

      this.sub(name, () => this.input('Enter value of ' + name, conv(size_value, true), (step >= 1) ? 'int' : 'float').then(set_func), title);
      values.forEach(v => this.addchk(match(v), conv(v), v, res => set_func((step >= 1) ? Number.parseInt(res) : Number.parseFloat(res))));
      this.endsub();
   }

   /** @summary Add palette menu entries
     * @protected */
   addPaletteMenu(curr, set_func) {
      const add = (id, name, title, more) => {
         if (!name)
            name = `pal ${id}`;
         else if (!title)
            title = name;
         if (title)
            title += `, code ${id}`;
         this.addchk((id === curr) || more, name, id, set_func, title || name);
      };

      this.sub('Palette', () => this.input('Enter palette code [1..113]', curr, 'int', 1, 113).then(set_func));

      this.column();
      add(57, 'Bird', 'Default color palette', (curr > 113));
      add(55, 'Rainbow');
      add(51, 'Deep Sea');
      add(52, 'Grayscale', 'New gray scale');
      add(1, '', 'Old gray scale', (curr > 0) && (curr < 10));
      add(50, 'ROOT 5', 'Default color palette in ROOT 5', (curr >= 10) && (curr < 51));
      add(53, '', 'Dark body radiator');
      add(54, '', 'Two-color hue');
      add(56, '', 'Inverted dark body radiator');
      add(58, 'Cubehelix');
      add(59, '', 'Green Red Violet');
      add(60, '', 'Blue Red Yellow');
      add(61, 'Ocean');
      this.endcolumn();

      if (!this.native())
         return this.endsub();

      this.column();
      add(62, '', 'Color Printable On Grey');
      add(63, 'Alpine');
      add(64, 'Aquamarine');
      add(65, 'Army');
      add(66, 'Atlantic');
      add(67, 'Aurora');
      add(68, 'Avocado');
      add(69, 'Beach');
      add(70, 'Black Body');
      add(71, '', 'Blue Green Yellow');
      add(72, 'Brown Cyan');
      add(73, 'CMYK');
      add(74, 'Candy');
      this.endcolumn();

      this.column();
      add(75, 'Cherry');
      add(76, 'Coffee');
      add(77, '', 'Dark Rain Bow');
      add(78, '', 'Dark Terrain');
      add(79, 'Fall');
      add(80, 'Fruit Punch');
      add(81, 'Fuchsia');
      add(82, 'Grey Yellow');
      add(83, '', 'Green Brown Terrain');
      add(84, 'Green Pink');
      add(85, 'Island');
      add(86, 'Lake');
      add(87, '', 'Light Temperature');
      this.endcolumn();

      this.column();
      add(88, '', 'Light Terrain');
      add(89, 'Mint');
      add(90, 'Neon');
      add(91, 'Pastel');
      add(92, 'Pearl');
      add(93, 'Pigeon');
      add(94, 'Plum');
      add(95, 'Red Blue');
      add(96, 'Rose');
      add(97, 'Rust');
      add(98, '', 'Sandy Terrain');
      add(99, 'Sienna');
      add(100, 'Solar');
      this.endcolumn();

      this.column();
      add(101, '', 'South West');
      add(102, '', 'Starry Night');
      add(103, '', 'Sunset');
      add(104, '', 'Temperature Map');
      add(105, '', 'Thermometer');
      add(106, 'Valentine');
      add(107, '', 'Visible Spectrum');
      add(108, '', 'Water Melon');
      add(109, 'Cool');
      add(110, 'Copper');
      add(111, '', 'Gist Earth');
      add(112, 'Viridis');
      add(113, 'Cividis');
      this.endcolumn();

      this.endsub();
   }

   /** @summary Add rebin menu entries
     * @protected */
   addRebinMenu(rebin_func) {
      this.sub('Rebin', () => this.input('Enter rebin value', 2, 'int', 2).then(rebin_func));
      for (let sz = 2; sz <= 7; sz++)
         this.add(sz.toString(), sz, res => rebin_func(parseInt(res)));
      this.endsub();
   }

   /** @summary Add selection menu entries
     * @param {String} name - name of submenu
     * @param {Array} values - array of string entries used as list for selection
     * @param {String|Number} value - currently selected value, either name or index
     * @param {Function} set_func - function called when item selected, either name or index depending from value parameter
     * @param {String} [title] - optional title for menu items
     * @protected */
   addSelectMenu(name, values, value, set_func, title) {
      const use_number = (typeof value === 'number');
      this.sub(name, undefined, undefined, title);
      for (let n = 0; n < values.length; ++n)
         this.addchk(use_number ? (n === value) : (values[n] === value), values[n], use_number ? n : values[n], res => set_func(use_number ? Number.parseInt(res) : res));
      this.endsub();
   }

   /** @summary Add RColor selection menu entries
     * @protected */
   addRColorMenu(name, value, set_func) {
      const colors = ['default', 'black', 'white', 'red', 'green', 'blue', 'yellow', 'magenta', 'cyan'];

      this.sub(name, () => {
         this.input('Enter color name - empty string will reset color', value).then(set_func);
      });
      let fillcol = 'black';
      for (let n = 0; n < colors.length; ++n) {
         const coltxt = colors[n];
         let match = false, bkgr = '';
         if (n > 0) {
            bkgr = 'background-color:' + coltxt;
            fillcol = (coltxt === 'white') ? 'black' : 'white';

            if (isStr(value) && value && (value !== 'auto') && (value[0] !== '['))
               match = (rgb(value).toString() === rgb(coltxt).toString());
         } else
            match = !value;

         const svg = `<svg width='100' height='18' style='margin:0px;${bkgr}'><text x='4' y='12' style='font-size:12px' fill='${fillcol}'>${coltxt}</text></svg>`;
         this.addchk(match, svg, coltxt, res => set_func(res === 'default' ? null : res));
      }
      this.endsub();
   }

   /** @summary Add items to change RAttrText
     * @protected */
   addRAttrTextItems(fontHandler, opts, set_func) {
      if (!opts)
         opts = {};
      this.addRColorMenu('color', fontHandler.color, value => set_func({ name: 'color', value }));
      if (fontHandler.scaled)
         this.addSizeMenu('size', 0.01, 0.10, 0.01, fontHandler.size / fontHandler.scale, value => set_func({ name: 'size', value }));
      else
         this.addSizeMenu('size', 6, 20, 2, fontHandler.size, value => set_func({ name: 'size', value }));

      this.addSelectMenu('family', [kArial, 'Times New Roman', 'Courier New', 'Symbol'], fontHandler.name, value => set_func({ name: 'font_family', value }));

      this.addSelectMenu('style', ['normal', 'italic', 'oblique'], fontHandler.style || 'normal', res => set_func({ name: 'font_style', value: res === 'normal' ? null : res }));

      this.addSelectMenu('weight', ['normal', 'lighter', 'bold', 'bolder'], fontHandler.weight || 'normal', res => set_func({ name: 'font_weight', value: res === 'normal' ? null : res }));

      if (!opts.noalign)
         this.add('align');
      if (!opts.noangle)
         this.add('angle');
   }

   /** @summary Add line style menu
     * @private */
   addLineStyleMenu(name, value, set_func) {
      this.sub(name, () => this.input('Enter line style id (1-solid)', value, 'int', 1, 11).then(val => {
         if (getSvgLineStyle(val))
            set_func(val);
      }));
      for (let n = 1; n < 11; ++n) {
         const dash = getSvgLineStyle(n),
               svg = `<svg width='100' height='14'><text x='2' y='13' style='font-size:12px'>${n}</text><line x1='30' y1='7' x2='100' y2='7' stroke='black' stroke-width='3' stroke-dasharray='${dash}'></line></svg>`;

         this.addchk((value === n), svg, n, arg => set_func(parseInt(arg)));
      }
      this.endsub();
   }

   /** @summary Add fill style menu
     * @private */
   addFillStyleMenu(name, value, color_index, set_func) {
      this.sub(name, () => {
         this.input('Enter fill style id (1001-solid, 3100..4000)', value, 'int', 0, 4000).then(id => {
            if ((id >= 0) && (id <= 4000))
               set_func(id);
         });
      });

      const supported = [1, 1001];
      for (let k = 3001; k < 3025; ++k)
         supported.push(k);
      supported.push(3144, 3244, 3344, 3305, 3315, 3325, 3490, 3481, 3472);

      for (let n = 0; n < supported.length; ++n) {
         if (n % 7 === 0)
            this.column();

         const selected = (value === supported[n]);

         if (typeof document !== 'undefined') {
            const svgelement = select(document.createElement('svg')),
                  handler = new TAttFillHandler({ color: color_index || 1, pattern: supported[n], svg: svgelement });
            svgelement.attr('width', 60).attr('height', 24);
            if (selected)
               svgelement.append('rect').attr('x', 0).attr('y', 0).attr('width', 60).attr('height', 24).style('stroke', 'red').style('fill', 'none').style('stroke-width', '3px');
            svgelement.append('rect').attr('x', 3).attr('y', 3).attr('width', 54).attr('height', 18).style('stroke', 'none').call(handler.func);
            this.add(svgelement.node().outerHTML, supported[n], arg => set_func(parseInt(arg)), `Pattern : ${supported[n]}` + (selected ? ' Active' : ''));
         } else
            this.addchk(selected, supported[n].toString(), supported[n], arg => set_func(parseInt(arg)));
         if (n % 7 === 6)
            this.endcolumn();
      }
      this.endsub();
   }

   /** @summary Add font selection menu
     * @private */
   addFontMenu(name, value, set_func) {
      const prec = value && Number.isInteger(value) ? value % 10 : 2;

      this.sub(name, () => {
         this.input('Enter font id from [0..20]', Math.floor(value / 10), 'int', 0, 20).then(id => {
            if ((id >= 0) && (id <= 20))
               set_func(id * 10 + prec);
         });
      });

      this.column();

      const doc = getDocument();

      for (let n = 1; n < 20; ++n) {
         const id = n * 10 + prec,
               handler = new FontHandler(id, 14),
               txt = select(doc.createElementNS(nsSVG, 'text'));
         let fullname = handler.getFontName(), qual = '';
         if (handler.weight) {
            qual += 'b';
            fullname += ' ' + handler.weight;
         }
         if (handler.style) {
            qual += handler.style[0];
            fullname += ' ' + handler.style;
         }
         if (qual)
            qual = ' ' + qual;
         txt.attr('x', 1).attr('y', 15).text(fullname.split(' ')[0] + qual);
         handler.setFont(txt);

         const rect = (value !== id) ? '' : '<rect width=\'90\' height=\'18\' style=\'fill:none;stroke:black\'></rect>',
               svg = `<svg width='90' height='18'>${txt.node().outerHTML}${rect}</svg>`;
         this.add(svg, id, arg => set_func(parseInt(arg)), `${id}: ${fullname}`);

         if (n === 10) {
            this.endcolumn();
            this.column();
         }
      }

      this.endcolumn();
      this.endsub();
   }

   /** @summary Add align selection menu
     * @private */
   addAlignMenu(name, value, set_func) {
      this.sub(name, () => {
         this.input('Enter align like 12 or 31', value).then(arg => {
            const id = parseInt(arg);
            if ((id < 11) || (id > 33))
               return;
            const h = Math.floor(id / 10), v = id % 10;
            if ((h > 0) && (h < 4) && (v > 0) && (v < 4))
               set_func(id);
         });
      });

      const hnames = ['left', 'middle', 'right'], vnames = ['bottom', 'centered', 'top'];
      for (let h = 1; h < 4; ++h) {
         for (let v = 1; v < 4; ++v)
            this.addchk(h * 10 + v === value, `${h * 10 + v}: ${hnames[h - 1]} ${vnames[v - 1]}`, h * 10 + v, arg => set_func(parseInt(arg)));
      }

      this.endsub();
   }

   /** @summary Fill context menu for graphical attributes in painter
     * @desc this method used to fill entries for different attributes of the object
     * like TAttFill, TAttLine, TAttText
     * There is special handling for the frame where attributes handled by the pad
     * @private */
   addAttributesMenu(painter, preffix) {
      const is_frame = painter === painter.getFramePainter(),
            pp = is_frame ? painter.getPadPainter() : null,
            redraw_arg = !preffix && !is_frame ? 'attribute' : true;
      if (!preffix)
         preffix = '';

      if (painter.lineatt?.used) {
         this.sub(`${preffix}Line att`);
         this.addSizeMenu('width', 1, 10, 1, painter.lineatt.width, arg => {
            painter.lineatt.change(undefined, arg);
            changeObjectMember(painter, 'fLineWidth', arg);
            changeObjectMember(pp, 'fFrameLineWidth', arg);
            painter.interactiveRedraw(redraw_arg, `exec:SetLineWidth(${arg})`);
         });
         if (!painter.lineatt.nocolor) {
            this.addColorMenu('color', painter.lineatt.color, arg => {
               painter.lineatt.change(arg);
               changeObjectMember(painter, 'fLineColor', arg, true);
               changeObjectMember(pp, 'fFrameLineColor', arg, true);
               painter.interactiveRedraw(redraw_arg, getColorExec(arg, 'SetLineColor'));
            });
         }
         this.addLineStyleMenu('style', painter.lineatt.style, id => {
            painter.lineatt.change(undefined, undefined, id);
            changeObjectMember(painter, 'fLineStyle', id);
            changeObjectMember(pp, 'fFrameLineStyle', id);
            painter.interactiveRedraw(redraw_arg, `exec:SetLineStyle(${id})`);
         });
         this.endsub();

         if (!is_frame && painter.lineatt?.excl_side) {
            this.sub('Exclusion');
            this.sub('side');
            for (let side = -1; side <= 1; ++side) {
               this.addchk((painter.lineatt.excl_side === side), side, side,
                           arg => { painter.lineatt.changeExcl(parseInt(arg)); painter.interactiveRedraw(); });
            }
            this.endsub();

            this.addSizeMenu('width', 10, 100, 10, painter.lineatt.excl_width,
                             arg => { painter.lineatt.changeExcl(undefined, arg); painter.interactiveRedraw(); });

            this.endsub();
         }
      }

      if (painter.fillatt?.used) {
         this.sub(`${preffix}Fill att`);
         this.addColorMenu('color', painter.fillatt.colorindx, arg => {
            painter.fillatt.change(arg, undefined, painter.getCanvSvg());
            changeObjectMember(painter, 'fFillColor', arg, true);
            changeObjectMember(pp, 'fFrameFillColor', arg, true);
            painter.interactiveRedraw(redraw_arg, getColorExec(arg, 'SetFillColor'));
         }, painter.fillatt.kind);
         this.addFillStyleMenu('style', painter.fillatt.pattern, painter.fillatt.colorindx, id => {
            painter.fillatt.change(undefined, id, painter.getCanvSvg());
            changeObjectMember(painter, 'fFillStyle', id);
            changeObjectMember(pp, 'fFrameFillStyle', id);
            painter.interactiveRedraw(redraw_arg, `exec:SetFillStyle(${id})`);
         });
         this.endsub();
      }

      if (painter.markeratt?.used) {
         this.sub(`${preffix}Marker att`);
         this.addColorMenu('color', painter.markeratt.color, arg => {
            changeObjectMember(painter, 'fMarkerColor', arg, true);
            painter.markeratt.change(arg);
            painter.interactiveRedraw(redraw_arg, getColorExec(arg, 'SetMarkerColor'));
         });
         this.addSizeMenu('size', 0.5, 6, 0.5, painter.markeratt.size, arg => {
            changeObjectMember(painter, 'fMarkerSize', arg);
            painter.markeratt.change(undefined, undefined, arg);
            painter.interactiveRedraw(redraw_arg, `exec:SetMarkerSize(${arg})`);
         });

         this.sub('style');
         const supported = [1, 2, 3, 4, 5, 6, 7, 8, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34];

         for (let n = 0; n < supported.length; ++n) {
            const clone = new TAttMarkerHandler({ style: supported[n], color: painter.markeratt.color, size: 1.7 }),
                  svg = `<svg width='60' height='18'><text x='1' y='12' style='font-size:12px'>${supported[n].toString()}</text><path stroke='black' fill='${clone.fill ? 'black' : 'none'}' d='${clone.create(40, 8)}'></path></svg>`;

            this.addchk(painter.markeratt.style === supported[n], svg, supported[n],
                        arg => { painter.markeratt.change(undefined, parseInt(arg)); painter.interactiveRedraw(redraw_arg, `exec:SetMarkerStyle(${arg})`); });
         }
         this.endsub();
         this.endsub();
      }

      if (painter.textatt?.used) {
         this.sub(`${preffix}Text att`);

         this.addFontMenu('font', painter.textatt.font, arg => {
            changeObjectMember(painter, 'fTextFont', arg);
            painter.textatt.change(arg);
            painter.interactiveRedraw(true, `exec:SetTextFont(${arg})`);
         });

         const rel = painter.textatt.size < 1.0;

         this.addSizeMenu('size', rel ? 0.03 : 6, rel ? 0.20 : 26, rel ? 0.01 : 2, painter.textatt.size, arg => {
            changeObjectMember(painter, 'fTextSize', arg);
            painter.textatt.change(undefined, arg);
            painter.interactiveRedraw(true, `exec:SetTextSize(${arg})`);
         });

         this.addColorMenu('color', painter.textatt.color, arg => {
            changeObjectMember(painter, 'fTextColor', arg, true);
            painter.textatt.change(undefined, undefined, arg);
            painter.interactiveRedraw(true, getColorExec(arg, 'SetTextColor'));
         });

         this.addAlignMenu('align', painter.textatt.align, arg => {
            changeObjectMember(painter, 'fTextAlign', arg);
            painter.textatt.change(undefined, undefined, undefined, arg);
            painter.interactiveRedraw(true, `exec:SetTextAlign(${arg})`);
         });

         if (painter.textatt.can_rotate) {
            this.addSizeMenu('angle', -180, 180, 45, painter.textatt.angle, arg => {
               changeObjectMember(painter, 'fTextAngle', arg);
               painter.textatt.change(undefined, undefined, undefined, undefined, arg);
               painter.interactiveRedraw(true, `exec:SetTextAngle(${arg})`);
            });
         }

         this.endsub();
      }
   }

   /** @summary Fill context menu for axis
     * @private */
   addTAxisMenu(EAxisBits, painter, faxis, kind, axis_painter, frame_painter) {
      const is_gaxis = faxis._typename === clTGaxis;

      this.add('Divisions', () => this.input('Set Ndivisions', faxis.fNdivisions, 'int', 0).then(val => {
         faxis.fNdivisions = val; painter.interactiveRedraw('pad', `exec:SetNdivisions(${val})`, kind);
      }));

      this.sub('Labels');
      this.addchk(faxis.TestBit(EAxisBits.kCenterLabels), 'Center',
                  arg => { faxis.SetBit(EAxisBits.kCenterLabels, arg); painter.interactiveRedraw('pad', `exec:CenterLabels(${arg})`, kind); });
      this.addchk(faxis.TestBit(EAxisBits.kLabelsVert), 'Rotate',
                  arg => { faxis.SetBit(EAxisBits.kLabelsVert, arg); painter.interactiveRedraw('pad', `exec:SetBit(TAxis::kLabelsVert,${arg})`, kind); });
      this.addColorMenu('Color', faxis.fLabelColor,
                        arg => { faxis.fLabelColor = arg; painter.interactiveRedraw('pad', getColorExec(arg, 'SetLabelColor'), kind); });
      this.addSizeMenu('Offset', -0.02, 0.1, 0.01, faxis.fLabelOffset,
                       arg => { faxis.fLabelOffset = arg; painter.interactiveRedraw('pad', `exec:SetLabelOffset(${arg})`, kind); });
      let a = faxis.fLabelSize >= 1;
      this.addSizeMenu('Size', a ? 2 : 0.02, a ? 30 : 0.11, a ? 2 : 0.01, faxis.fLabelSize,
                       arg => { faxis.fLabelSize = arg; painter.interactiveRedraw('pad', `exec:SetLabelSize(${arg})`, kind); });

      if (frame_painter && (axis_painter?.kind === kAxisLabels) && (faxis.fNbins > 20)) {
         this.add('Find label', () => this.input('Label id').then(id => {
            if (!id)
               return;
            for (let bin = 0; bin < faxis.fNbins; ++bin) {
               const lbl = axis_painter.formatLabels(bin);
               if (lbl === id)
                  return frame_painter.zoomSingle(kind, Math.max(0, bin - 4), Math.min(faxis.fNbins, bin + 5));
            }
         }), 'Zoom into region around specific label');
      }
      if (frame_painter && faxis.fLabels) {
         const ignore = `${kind}_ignore_labels`;
         this.addchk(!frame_painter[ignore], 'Custom', flag => {
            frame_painter[ignore] = !flag;
            painter.interactiveRedraw('pad');
         }, `Use of custom labels in axis ${kind}`);
      }
      this.endsub();

      this.sub('Title');
      this.add('SetTitle', () => {
         this.input('Enter axis title', faxis.fTitle).then(t => {
            faxis.fTitle = t;
            painter.interactiveRedraw('pad', `exec:SetTitle("${t}")`, kind);
         });
      });
      this.addchk(faxis.TestBit(EAxisBits.kCenterTitle), 'Center',
                  arg => { faxis.SetBit(EAxisBits.kCenterTitle, arg); painter.interactiveRedraw('pad', `exec:CenterTitle(${arg})`, kind); });
      if (!painter?.hasSnapId()) {
         this.addchk(faxis.TestBit(EAxisBits.kOppositeTitle), 'Opposite',
                     arg => { faxis.SetBit(EAxisBits.kOppositeTitle, arg); painter.redrawPad(); });
      }
      this.addchk(faxis.TestBit(EAxisBits.kRotateTitle), 'Rotate',
                  arg => { faxis.SetBit(EAxisBits.kRotateTitle, arg); painter.interactiveRedraw('pad', is_gaxis ? `exec:SetBit(TAxis::kRotateTitle, ${arg})` : `exec:RotateTitle(${arg})`, kind); });
      this.addColorMenu('Color', is_gaxis ? faxis.fTextColor : faxis.fTitleColor, arg => {
         if (is_gaxis)
            faxis.fTextColor = arg;
         else
            faxis.fTitleColor = arg;

         painter.interactiveRedraw('pad', getColorExec(arg, 'SetTitleColor'), kind);
      });
      this.addSizeMenu('Offset', 0, 3, 0.2, faxis.fTitleOffset,
                       arg => { faxis.fTitleOffset = arg; painter.interactiveRedraw('pad', `exec:SetTitleOffset(${arg})`, kind); });
      a = faxis.fTitleSize >= 1;
      this.addSizeMenu('Size', a ? 2 : 0.02, a ? 30 : 0.11, a ? 2 : 0.01, faxis.fTitleSize,
                       arg => { faxis.fTitleSize = arg; painter.interactiveRedraw('pad', `exec:SetTitleSize(${arg})`, kind); });
      this.endsub();

      this.sub('Ticks');
      if (is_gaxis) {
         this.addColorMenu('Color', faxis.fLineColor,
                           arg => { faxis.fLineColor = arg; painter.interactiveRedraw('pad', getColorExec(arg, 'SetLineColor'), kind); });
         this.addSizeMenu('Size', -0.05, 0.055, 0.01, faxis.fTickSize,
                          arg => { faxis.fTickSize = arg; painter.interactiveRedraw('pad', `exec:SetTickLength(${arg})`, kind); });
      } else {
         this.addColorMenu('Color', faxis.fAxisColor,
                           arg => { faxis.fAxisColor = arg; painter.interactiveRedraw('pad', getColorExec(arg, 'SetAxisColor'), kind); });
         this.addSizeMenu('Size', -0.05, 0.055, 0.01, faxis.fTickLength,
                          arg => { faxis.fTickLength = arg; painter.interactiveRedraw('pad', `exec:SetTickLength(${arg})`, kind); });
      }
      this.endsub();

      if (is_gaxis) {
         this.add('Options', () => this.input('Enter TGaxis options like +L or -G', faxis.fChopt, 'string').then(arg => {
            faxis.fChopt = arg;
            painter.interactiveRedraw('pad', `exec:SetOption("${arg}")`, kind);
         }));
      }
   }

   /** @summary Fill menu to edit settings properties
     * @private */
   addSettingsMenu(with_hierarchy, alone, handle_func) {
      if (!isFunc(handle_func))
         handle_func = () => {};
      if (alone)
         this.header('Settings');
      else
         this.sub('Settings');

      this.sub('Files');

      if (with_hierarchy) {
         this.addchk(settings.OnlyLastCycle, 'Last cycle', flag => {
            settings.OnlyLastCycle = flag;
            handle_func('refresh');
         });

         this.addchk(!settings.SkipStreamerInfos, 'Streamer infos', flag => {
            settings.SkipStreamerInfos = !flag;
            handle_func('refresh');
         });
      }

      this.addchk(settings.UseStamp, 'Use stamp arg', flag => { settings.UseStamp = flag; });
      this.addSizeMenu('Max ranges', 1, 1000, [1, 10, 20, 50, 200, 1000], settings.MaxRanges, value => { settings.MaxRanges = value; }, 'Maximal number of ranges in single http request');

      this.addchk(settings.HandleWrongHttpResponse, 'Handle wrong http response', flag => { settings.HandleWrongHttpResponse = flag; }, 'Let detect and solve problem when server returns wrong Content-Length header, see https://github.com/root-project/jsroot/issues/189');
      this.addchk(settings.WithCredentials, 'With credentials', flag => { settings.WithCredentials = flag; }, 'Submit http request with user credentials');

      this.endsub();

      this.sub('Toolbar');
      this.addchk(settings.ToolBar === false, 'Off', flag => { settings.ToolBar = !flag; });
      this.addchk(settings.ToolBar === true, 'On', flag => { settings.ToolBar = flag; });
      this.addchk(settings.ToolBar === 'popup', 'Popup', flag => { settings.ToolBar = flag ? 'popup' : false; });
      this.separator();
      this.addchk(settings.ToolBarSide === 'left', 'Left side', flag => { settings.ToolBarSide = flag ? 'left' : 'right'; });
      this.addchk(settings.ToolBarVert, 'Vertical', flag => { settings.ToolBarVert = flag; });
      this.endsub();

      this.sub('Interactive');
      this.addchk(settings.Tooltip, 'Tooltip', flag => { settings.Tooltip = flag; });
      this.addchk(settings.ContextMenu, 'Context menus', flag => { settings.ContextMenu = flag; });
      this.sub('Zooming');
      this.addchk(settings.Zooming, 'Global', flag => { settings.Zooming = flag; });
      this.addchk(settings.ZoomMouse, 'Mouse', flag => { settings.ZoomMouse = flag; });
      this.addchk(settings.ZoomWheel, 'Wheel', flag => { settings.ZoomWheel = flag; });
      this.addchk(settings.ZoomTouch, 'Touch', flag => { settings.ZoomTouch = flag; });
      this.endsub();
      this.addchk(settings.HandleKeys, 'Keypress handling', flag => { settings.HandleKeys = flag; });
      this.addchk(!settings.UserSelect, 'User select', flag => { settings.UserSelect = flag ? '' : 'none'; }, 'Set "user-select: none" for drawings to avoid text selection ');
      this.addchk(settings.MoveResize, 'Move and resize', flag => { settings.MoveResize = flag; });
      this.addchk(settings.DragAndDrop, 'Drag and drop', flag => { settings.DragAndDrop = flag; });
      this.addchk(settings.DragGraphs, 'Drag graph points', flag => { settings.DragGraphs = flag; });
      this.addSelectMenu('Progress box', ['off', 'on', 'modal'], isStr(settings.ProgressBox) ? settings.ProgressBox : (settings.ProgressBox ? 'on' : 'off'), value => {
         settings.ProgressBox = (value === 'off') ? false : (value === ' on' ? true : value);
      });
      this.endsub();

      this.sub('Drawing');
      this.addSelectMenu('Optimize', ['None', 'Smart', 'Always'], settings.OptimizeDraw, value => { settings.OptimizeDraw = value; }, 'Histogram drawing optimization');
      this.sub('SmallPad', undefined, undefined, 'Minimal pad size drawn normally');
      this.add(`width ${settings.SmallPad?.width ?? 0}px`, () => this.input('Small pad width', settings.SmallPad?.width, 'int', 1, 1000).then(val => { settings.SmallPad.width = val; }));
      this.add(`height ${settings.SmallPad?.height ?? 0}px`, () => this.input('Small pad height', settings.SmallPad?.height, 'int', 1, 800).then(val => { settings.SmallPad.height = val; }));
      this.add('disable', () => { settings.SmallPad = { width: 0, height: 0 }; }, 'disable small pad drawing optimization');
      this.add('default', () => { settings.SmallPad = { width: 150, height: 100 }; }, 'Set to default 150x100 dimension');
      this.endsub();
      this.addPaletteMenu(settings.Palette, pal => { settings.Palette = pal; });
      this.addchk(settings.AutoStat, 'Auto stat box', flag => { settings.AutoStat = flag; });
      this.addchk(settings.LoadSymbolTtf, 'Load symbol.ttf', flag => { settings.LoadSymbolTtf = flag; }, 'Use symbol.ttf font file to render greek symbols, also used in PDF');

      this.sub('Axis');
      this.addchk(settings.StripAxisLabels, 'Strip labels', flag => { settings.StripAxisLabels = flag; }, 'Provide shorter labels like 10^0 -> 1');
      this.addchk(settings.CutAxisLabels, 'Cut labels', flag => { settings.CutAxisLabels = flag; }, 'Remove labels which may exceed graphical range');
      this.add(`Tilt angle ${settings.AxisTiltAngle}`, () => this.input('Axis tilt angle', settings.AxisTiltAngle, 'int', 0, 180).then(val => { settings.AxisTiltAngle = val; }));
      this.endsub();
      this.addSelectMenu('Latex', ['Off', 'Symbols', 'Normal', 'MathJax', 'Force MathJax'], settings.Latex, value => { settings.Latex = value; });
      this.addSelectMenu('3D rendering', ['Default', 'WebGL', 'Image'], settings.Render3D, value => { settings.Render3D = value; });
      this.addSelectMenu('WebGL embeding', ['Default', 'Overlay', 'Embed'], settings.Embed3D, value => { settings.Embed3D = value; });
      if (internals.setDefaultDrawOpt) {
         this.add('Default options', () => this.input('List of options like TH2:lego2;TH3:glbox2', settings._dflt_drawopt || '').then(v => {
            settings._dflt_drawopt = v;
            internals.setDefaultDrawOpt(v);
         }), 'Configure custom default draw options for some classes');
      }
      this.endsub();

      this.sub('Geometry');
      this.add('Grad per segment:  ' + settings.GeoGradPerSegm, () => this.input('Grad per segment in geometry', settings.GeoGradPerSegm, 'int', 1, 60).then(val => { settings.GeoGradPerSegm = val; }));
      this.addchk(settings.GeoCompressComp, 'Compress composites', flag => { settings.GeoCompressComp = flag; });
      this.endsub();

      if (with_hierarchy) {
         this.sub('Browser');
         this.add('Hierarchy limit:  ' + settings.HierarchyLimit, () => this.input('Max number of items in hierarchy', settings.HierarchyLimit, 'int', 10, 100000).then(val => {
            settings.HierarchyLimit = val;
            handle_func('refresh');
         }));
         this.add('Browser width:  ' + settings.BrowserWidth, () => this.input('Browser width in px', settings.BrowserWidth, 'int', 50, 2000).then(val => {
            settings.BrowserWidth = val;
            handle_func('width');
         }));
         this.endsub();
      }

      this.add('Dark mode: ' + (settings.DarkMode ? 'On' : 'Off'), () => {
         settings.DarkMode = !settings.DarkMode;
         handle_func('dark');
      });

      const setStyleField = arg => { gStyle[arg.slice(1)] = parseInt(arg[0]); },
            addStyleIntField = (name, field, arr) => {
               this.sub(name);
               const curr = gStyle[field] >= arr.length ? 1 : gStyle[field];
               for (let v = 0; v < arr.length; ++v)
                  this.addchk(curr === v, arr[v], `${v}${field}`, setStyleField);
               this.endsub();
            };

      this.sub('gStyle');

      this.sub('Canvas');
      this.addColorMenu('Color', gStyle.fCanvasColor, col => { gStyle.fCanvasColor = col; });
      addStyleIntField('Draw date', 'fOptDate', ['Off', 'Current time', 'File create time', 'File modify time']);
      this.add(`Time zone: ${settings.TimeZone}`, () => this.input('Input time zone like UTC. empty string - local timezone', settings.TimeZone, 'string').then(val => { settings.TimeZone = val; }));
      addStyleIntField('Draw file', 'fOptFile', ['Off', 'File name', 'Full file URL', 'Item name']);
      this.addSizeMenu('Date X', 0.01, 0.1, 0.01, gStyle.fDateX, x => { gStyle.fDateX = x; }, 'configure gStyle.fDateX for date/item name drawings');
      this.addSizeMenu('Date Y', 0.01, 0.1, 0.01, gStyle.fDateY, y => { gStyle.fDateY = y; }, 'configure gStyle.fDateY for date/item name drawings');
      this.endsub();

      this.sub('Pad');
      this.addColorMenu('Color', gStyle.fPadColor, col => { gStyle.fPadColor = col; });
      this.sub('Grid');
      this.addchk(gStyle.fPadGridX, 'X', flag => { gStyle.fPadGridX = flag; });
      this.addchk(gStyle.fPadGridY, 'Y', flag => { gStyle.fPadGridY = flag; });
      this.addColorMenu('Color', gStyle.fGridColor, col => { gStyle.fGridColor = col; });
      this.addSizeMenu('Width', 1, 10, 1, gStyle.fGridWidth, w => { gStyle.fGridWidth = w; });
      this.addLineStyleMenu('Style', gStyle.fGridStyle, st => { gStyle.fGridStyle = st; });
      this.endsub();
      addStyleIntField('Ticks X', 'fPadTickX', ['normal', 'ticks on both sides', 'labels on both sides']);
      addStyleIntField('Ticks Y', 'fPadTickY', ['normal', 'ticks on both sides', 'labels on both sides']);
      addStyleIntField('Log X', 'fOptLogx', ['off', 'on', 'log 2']);
      addStyleIntField('Log Y', 'fOptLogy', ['off', 'on', 'log 2']);
      addStyleIntField('Log Z', 'fOptLogz', ['off', 'on', 'log 2']);
      this.endsub();

      this.sub('Frame');
      this.addColorMenu('Fill color', gStyle.fFrameFillColor, col => { gStyle.fFrameFillColor = col; });
      this.addFillStyleMenu('Fill style', gStyle.fFrameFillStyle, gStyle.fFrameFillColor, id => { gStyle.fFrameFillStyle = id; });
      this.addColorMenu('Line color', gStyle.fFrameLineColor, col => { gStyle.fFrameLineColor = col; });
      this.addSizeMenu('Line width', 1, 10, 1, gStyle.fFrameLineWidth, w => { gStyle.fFrameLineWidth = w; });
      this.addLineStyleMenu('Line style', gStyle.fFrameLineStyle, st => { gStyle.fFrameLineStyle = st; });
      this.addSizeMenu('Border size', 0, 10, 1, gStyle.fFrameBorderSize, sz => { gStyle.fFrameBorderSize = sz; });
      this.addSelectMenu('Border mode', ['Down', 'Off', 'Up'], gStyle.fFrameBorderMode + 1, v => { gStyle.fFrameBorderMode = v - 1; });

      // fFrameBorderMode: 0,
      this.sub('Margins');
      this.addSizeMenu('Bottom', 0, 0.5, 0.05, gStyle.fPadBottomMargin, v => { gStyle.fPadBottomMargin = v; });
      this.addSizeMenu('Top', 0, 0.5, 0.05, gStyle.fPadTopMargin, v => { gStyle.fPadTopMargin = v; });
      this.addSizeMenu('Left', 0, 0.5, 0.05, gStyle.fPadLeftMargin, v => { gStyle.fPadLeftMargin = v; });
      this.addSizeMenu('Right', 0, 0.5, 0.05, gStyle.fPadRightMargin, v => { gStyle.fPadRightMargin = v; });
      this.endsub();
      this.endsub();

      this.sub('Title');
      this.addColorMenu('Fill color', gStyle.fTitleColor, col => { gStyle.fTitleColor = col; });
      this.addFillStyleMenu('Fill style', gStyle.fTitleStyle, gStyle.fTitleColor, id => { gStyle.fTitleStyle = id; });
      this.addColorMenu('Text color', gStyle.fTitleTextColor, col => { gStyle.fTitleTextColor = col; });
      this.addSizeMenu('Border size', 0, 10, 1, gStyle.fTitleBorderSize, sz => { gStyle.fTitleBorderSize = sz; });
      this.addSizeMenu('Font size', 0.01, 0.1, 0.01, gStyle.fTitleFontSize, sz => { gStyle.fTitleFontSize = sz; });
      this.addFontMenu('Font', gStyle.fTitleFont, fnt => { gStyle.fTitleFont = fnt; });
      this.addSizeMenu('X: ' + gStyle.fTitleX.toFixed(2), 0.0, 1.0, 0.1, gStyle.fTitleX, v => { gStyle.fTitleX = v; });
      this.addSizeMenu('Y: ' + gStyle.fTitleY.toFixed(2), 0.0, 1.0, 0.1, gStyle.fTitleY, v => { gStyle.fTitleY = v; });
      this.addSizeMenu('W: ' + gStyle.fTitleW.toFixed(2), 0.0, 1.0, 0.1, gStyle.fTitleW, v => { gStyle.fTitleW = v; });
      this.addSizeMenu('H: ' + gStyle.fTitleH.toFixed(2), 0.0, 1.0, 0.1, gStyle.fTitleH, v => { gStyle.fTitleH = v; });
      this.endsub();

      this.sub('Stat box');
      this.addColorMenu('Fill color', gStyle.fStatColor, col => { gStyle.fStatColor = col; });
      this.addFillStyleMenu('Fill style', gStyle.fStatStyle, gStyle.fStatColor, id => { gStyle.fStatStyle = id; });
      this.addColorMenu('Text color', gStyle.fStatTextColor, col => { gStyle.fStatTextColor = col; });
      this.addSizeMenu('Border size', 0, 10, 1, gStyle.fStatBorderSize, sz => { gStyle.fStatBorderSize = sz; });
      this.addSizeMenu('Font size', 0, 30, 5, gStyle.fStatFontSize, sz => { gStyle.fStatFontSize = sz; });
      this.addFontMenu('Font', gStyle.fStatFont, fnt => { gStyle.fStatFont = fnt; });
      this.add('Stat format', () => this.input('Stat format', gStyle.fStatFormat).then(fmt => { gStyle.fStatFormat = fmt; }));
      this.addSizeMenu('X: ' + gStyle.fStatX.toFixed(2), 0.2, 1.0, 0.1, gStyle.fStatX, v => { gStyle.fStatX = v; });
      this.addSizeMenu('Y: ' + gStyle.fStatY.toFixed(2), 0.2, 1.0, 0.1, gStyle.fStatY, v => { gStyle.fStatY = v; });
      this.addSizeMenu('Width: ' + gStyle.fStatW.toFixed(2), 0.1, 1.0, 0.1, gStyle.fStatW, v => { gStyle.fStatW = v; });
      this.addSizeMenu('Height: ' + gStyle.fStatH.toFixed(2), 0.1, 1.0, 0.1, gStyle.fStatH, v => { gStyle.fStatH = v; });
      this.endsub();

      this.sub('Legend');
      this.addColorMenu('Fill color', gStyle.fLegendFillColor, col => { gStyle.fLegendFillColor = col; });
      this.addFillStyleMenu('Fill style', gStyle.fLegendFillStyle, gStyle.fLegendFillColor, id => { gStyle.fLegendFillStyle = id; });
      this.addSizeMenu('Border size', 0, 10, 1, gStyle.fLegendBorderSize, sz => { gStyle.fLegendBorderSize = sz; });
      this.addFontMenu('Font', gStyle.fLegendFont, fnt => { gStyle.fLegendFont = fnt; });
      this.addSizeMenu('Text size', 0, 0.1, 0.01, gStyle.fLegendTextSize, v => { gStyle.fLegendTextSize = v; }, 'legend text size, when 0 - auto adjustment is used');
      this.endsub();

      this.sub('Histogram');
      this.addchk(gStyle.fOptTitle === 1, 'Hist title', flag => { gStyle.fOptTitle = flag ? 1 : 0; });
      this.addchk(gStyle.fOrthoCamera, 'Orthographic camera', flag => { gStyle.fOrthoCamera = flag; });
      this.addchk(gStyle.fHistMinimumZero, 'Base0', flag => { gStyle.fHistMinimumZero = flag; }, 'when true, BAR and LEGO drawing using base = 0');
      this.add('Text format', () => this.input('Paint text format', gStyle.fPaintTextFormat).then(fmt => { gStyle.fPaintTextFormat = fmt; }));
      this.add('Time offset', () => this.input('Time offset in seconds, default is 788918400 for 1/1/1995', gStyle.fTimeOffset, 'int').then(ofset => { gStyle.fTimeOffset = ofset; }));
      this.addSizeMenu('ErrorX: ' + gStyle.fErrorX.toFixed(2), 0.0, 1.0, 0.1, gStyle.fErrorX, v => { gStyle.fErrorX = v; });
      this.addSizeMenu('End error', 0, 12, 1, gStyle.fEndErrorSize, v => { gStyle.fEndErrorSize = v; }, 'size in pixels of end error for E1 draw options, gStyle.fEndErrorSize');
      this.addSizeMenu('Top margin', 0.0, 0.5, 0.05, gStyle.fHistTopMargin, v => { gStyle.fHistTopMargin = v; }, 'Margin between histogram top and frame top');
      this.addColorMenu('Fill color', gStyle.fHistFillColor, col => { gStyle.fHistFillColor = col; });
      this.addFillStyleMenu('Fill style', gStyle.fHistFillStyle, gStyle.fHistFillColor, id => { gStyle.fHistFillStyle = id; });
      this.addColorMenu('Line color', gStyle.fHistLineColor, col => { gStyle.fHistLineColor = col; });
      this.addSizeMenu('Line width', 1, 10, 1, gStyle.fHistLineWidth, w => { gStyle.fHistLineWidth = w; });
      this.addLineStyleMenu('Line style', gStyle.fHistLineStyle, st => { gStyle.fHistLineStyle = st; });
      this.endsub();

      this.separator();
      this.sub('Predefined');
      ['Modern', 'Plain', 'Bold'].forEach(name => this.addchk((gStyle.fName === name), name, name, selectgStyle));
      this.endsub();

      this.endsub(); // gStyle

      this.separator();

      this.add('Save settings', () => {
         const promise = readSettings(true) ? Promise.resolve(true) : this.confirm('Save settings', 'Pressing OK one agreess that JSROOT will store settings in browser local storage');
         promise.then(res => {
            if (res) {
               saveSettings();
               saveStyle();
            }
         });
      }, 'Store settings and gStyle in browser local storage');
      this.add('Delete settings', () => {
         saveSettings(-1);
         saveStyle(-1);
      }, 'Delete settings and gStyle from browser local storage');

      if (!alone)
         this.endsub();
   }

   /** @summary Run modal dialog
     * @return {Promise} with html element inside dialog
     * @private */
   async runModal() {
      throw Error('runModal() must be reimplemented');
   }

   /** @summary Show modal info dialog
     * @param {String} title - title
     * @param {String} message - message
     * @protected */
   info(title, message) {
      return this.runModal(title, `<p>${message}</p>`, { height: 120, width: 400, resizable: true });
   }

   /** @summary Show confirm dialog
     * @param {String} title - title
     * @param {String} message - message
     * @return {Promise} with true when 'Ok' pressed or false when 'Cancel' pressed
     * @protected */
   async confirm(title, message) {
      return this.runModal(title, message, { btns: true, height: 120, width: 400 }).then(elem => Boolean(elem));
   }

   /** @summary Input value
     * @return {Promise} with input value
     * @param {string} title - input dialog title
     * @param value - initial value
     * @param {string} [kind] - use 'text' (default), 'number', 'float' or 'int'
     * @protected */
   async input(title, value, kind, min, max) {
      if (!kind)
         kind = 'text';
      const inp_type = (kind === 'int') ? 'number' : 'text';
      let ranges = '';
      if ((value === undefined) || (value === null))
         value = '';
      if (kind === 'int') {
         if (min !== undefined)
            ranges += ` min="${min}"`;
         if (max !== undefined)
            ranges += ` max="${max}"`;
      }

      const main_content =
         '<form><fieldset style="padding:0; border:0">' +
            `<input type="${inp_type}" value="${value}" ${ranges} style="width:98%;display:block" class="jsroot_dlginp"/>` +
         '</fieldset></form>';

      return new Promise(resolveFunc => {
         this.runModal(title, main_content, { btns: true, height: 150, width: 400 }).then(element => {
            if (!element)
               return;
            let val = element.querySelector('.jsroot_dlginp').value;
            if (kind === 'float') {
               val = Number.parseFloat(val);
               if (Number.isFinite(val))
                  resolveFunc(val);
            } else if (kind === 'int') {
               val = parseInt(val);
               if (Number.isInteger(val))
                  resolveFunc(val);
            } else
               resolveFunc(val);
         });
      });
   }

   /** @summary Let input arguments from the method
     * @return {Promise} with method argument */
   async showMethodArgsDialog(method) {
      const dlg_id = this.menuname + sDfltDlg;
      let main_content = '<form> <fieldset style="padding:0; border:0">';

      for (let n = 0; n < method.fArgs.length; ++n) {
         const arg = method.fArgs[n];
         arg.fValue = arg.fDefault;
         if (arg.fValue === '""')
            arg.fValue = '';
         main_content += `<label for="${dlg_id}_inp${n}">${arg.fName}</label>
                          <input type='text' tabindex="${n + 1}" id="${dlg_id}_inp${n}" value="${arg.fValue}" style="width:100%;display:block"/>`;
      }

      main_content += '</fieldset></form>';

      return new Promise(resolveFunc => {
         this.runModal(method.fClassName + '::' + method.fName, main_content, { btns: true, height: 100 + method.fArgs.length * 60, width: 400, resizable: true }).then(element => {
            if (!element)
               return;
            let args = '';

            for (let k = 0; k < method.fArgs.length; ++k) {
               const arg = method.fArgs[k];
               let value = element.querySelector(`#${dlg_id}_inp${k}`).value;
               if (value === '')
                  value = arg.fDefault;
               if ((arg.fTitle === 'Option_t*') || (arg.fTitle === 'const char*')) {
                  // check quotes,
                  // TODO: need to make more precise checking of escape characters
                  if (!value)
                     value = '""';
                  if (value[0] !== '"')
                     value = '"' + value;
                  if (value.at(-1) !== '"')
                     value += '"';
               }

               args += (k > 0 ? ',' : '') + value;
            }

            resolveFunc(args);
         });
      });
   }

   /** @summary Let input arguments from the Command
     * @return {Promise} with command argument */
   async showCommandArgsDialog(cmdname, args) {
      const dlg_id = this.menuname + sDfltDlg;
      let main_content = '<form> <fieldset style="padding:0; border:0">';

      for (let n = 0; n < args.length; ++n) {
         main_content += `<label for="${dlg_id}_inp${n}">arg${n + 1}</label>` +
                         `<input type='text' id="${dlg_id}_inp${n}" value="${args[n]}" style="width:100%;display:block"/>`;
      }

      main_content += '</fieldset></form>';

      return new Promise(resolveFunc => {
         this.runModal('Arguments for command ' + cmdname, main_content, { btns: true, height: 110 + args.length * 60, width: 400, resizable: true }).then(element => {
            if (!element)
               return resolveFunc(null);

            const resargs = [];
            for (let k = 0; k < args.length; ++k)
               resargs.push(element.querySelector(`#${dlg_id}_inp${k}`).value);
            resolveFunc(resargs);
         });
      });
   }

} // class JSRootMenu

/**
 * @summary Context menu class using plain HTML/JavaScript
 *
 * @desc Use {@link createMenu} to create instance of the menu
 * based on {@link https://github.com/L1quidH2O/ContextMenu.js}
 * @private
 */

class StandaloneMenu extends JSRootMenu {

   constructor(painter, menuname, show_event) {
      super(painter, menuname, show_event);

      this.code = [];
      this._use_plain_text = true;
      this.stack = [this.code];
   }

   native() { return true; }

   /** @summary Load required modules, noop for that menu class */
   async load() { return this; }

   /** @summary Add menu item
     * @param {string} name - item name
     * @param {function} func - func called when item is selected */
   add(name, arg, func, title) {
      let curr = this.stack.at(-1);

      if (name === sSeparator)
         return curr.push({ divider: true });

      if (name.indexOf(sHeader) === 0)
         return curr.push({ text: name.slice(sHeader.length), header: true, title });

      if (name === sEndsub) {
         this.stack.pop();
         curr = this.stack.at(-1);
         if (!curr.at(-1).sub.length)
            curr.at(-1).sub = undefined;
         return;
      }

      if (name === sEndcolumn)
         return this.stack.pop();

      if (isFunc(arg)) {
         title = func;
         func = arg;
         arg = name;
      }

      const elem = {};
      curr.push(elem);

      if (name === sColumn) {
         elem.column = true;
         elem.sub = [];
         this.stack.push(elem.sub);
         return;
      }

      if (name.indexOf(sSub) === 0) {
         name = name.slice(4);
         elem.sub = [];
         this.stack.push(elem.sub);
      }

      if (name.indexOf('chk:') === 0) {
         elem.checked = true;
         name = name.slice(4);
      } else if (name.indexOf('unk:') === 0) {
         elem.checked = false;
         name = name.slice(4);
      }

      elem.text = name;
      elem.title = title;
      elem.arg = arg;
      elem.func = func;
   }

   /** @summary Returns size of main menu */
   size() { return this.code.length; }

   /** @summary Build HTML elements of the menu
     * @private */
   _buildContextmenu(menu, left, top, loc) {
      const doc = getDocument(),
            outer = doc.createElement('div'),
            clname = 'jsroot_ctxt_container',
            clfocus = 'jsroot_ctxt_focus',
            clcolumn = 'jsroot_ctxt_column',
            container_style =
         'position: absolute; top: 0; user-select: none; z-index: 100000; background-color: rgb(250, 250, 250); margin: 0; padding: 0px; width: auto;' +
         'min-width: 100px; box-shadow: 0px 0px 10px rgb(0, 0, 0, 0.2); border: 3px solid rgb(215, 215, 215); font-family: Arial, helvetica, sans-serif, serif;' +
         'font-size: 13px; color: rgb(0, 0, 0, 0.8); line-height: 15px;';

      // if loc !== doc.body then its a submenu, so it needs to have position: relative;
      if (loc === doc.body) {
         // delete all elements with menu className
         const deleteElems = doc.getElementsByClassName(clname);
         for (let k = deleteElems.length - 1; k >= 0; --k)
            deleteElems[k].parentNode.removeChild(deleteElems[k]);

         outer.className = clname;
         outer.style = container_style;
         outer.style.position = 'fixed';
         outer.style.left = left + 'px';
         outer.style.top = top + 'px';
      } else if ((left < 0) && (top === left)) {
         // column
         outer.className = clcolumn;
         outer.style.float = 'left';
         outer.style.width = (100 / -left).toFixed(1) + '%';
      } else {
         outer.className = clname;
         outer.style = container_style;
         outer.style.left = -loc.offsetLeft + loc.offsetWidth + 'px';
      }

      let need_check_area = false, ncols = 0;
      menu.forEach(d => {
         if (d.checked)
            need_check_area = true;
         if (d.column)
            ncols++;
      });

      menu.forEach(d => {
         if (ncols > 0) {
            outer.style.display = 'flex';
            if (d.column)
               this._buildContextmenu(d.sub, -ncols, -ncols, outer);
            return;
         }

         if (d.divider) {
            const hr = doc.createElement('hr');
            hr.style = 'width: 85%; margin: 3px auto; border: 1px solid rgb(0, 0, 0, 0.15)';
            outer.appendChild(hr);
            return;
         }

         const item = doc.createElement('div');
         item.style.position = 'relative';
         outer.appendChild(item);

         if (d.header) {
            item.style = 'background-color: lightblue; padding: 3px 7px; font-weight: bold; border-bottom: 1px;';

            let url = '', title = '';
            if (d.title) {
               const p = d.title.indexOf('https://');
               if (p >= 0) {
                  url = d.title.slice(p);
                  title = d.title.slice(0, p);
               } else
                  title = d.title;
            }
            if (!url)
               item.innerText = d.text;
            else {
               item.style.display = 'flex';
               item.style['justify-content'] = 'space-between';

               const txt = doc.createElement('span');
               txt.innerText = d.text;
               txt.style = 'display: inline-block; margin: 0;';
               item.appendChild(txt);

               const anchor = doc.createElement('span');
               anchor.style = 'margin: 0; color: blue; opacity: 0.1; margin-left: 7px; right: 3px; display: inline-block; cursor: pointer;';
               anchor.textContent = '?';
               anchor.title = url;
               anchor.addEventListener('click', () => {
                  const cp = this.painter?.getCanvPainter();
                  if (cp?.canSendWebsocket())
                     cp.sendWebsocket(`SHOWURL:${url}`);
                  else
                     window.open(url);
               });
               anchor.addEventListener('mouseenter', () => { anchor.style.opacity = 1; });
               anchor.addEventListener('mouseleave', () => { anchor.style.opacity = 0.1; });
               item.appendChild(anchor);
            }
            if (title)
               item.setAttribute('title', title);

            return;
         }

         const hovArea = doc.createElement('div');
         hovArea.style = 'width: 100%; height: 100%; display: flex; justify-content: space-between; cursor: pointer;';
         if (d.title)
            hovArea.setAttribute('title', d.title);

         item.appendChild(hovArea);
         if (!d.text)
            d.text = 'item';

         const text = doc.createElement('div');
         text.style = 'margin: 0; padding: 3px 7px; pointer-events: none; white-space: nowrap';

         if (d.text.indexOf('<svg') >= 0) {
            if (need_check_area) {
               text.style.display = 'flex';

               const chk = doc.createElement('span');
               chk.innerText = d.checked ? '\u2713' : '';
               chk.style.display = 'inline-block';
               chk.style.width = '1em';
               text.appendChild(chk);

               const sub = doc.createElement('div');
               sub.innerHTML = d.text;
               text.appendChild(sub);
            } else
               text.innerHTML = d.text;
         } else {
            if (need_check_area) {
               const chk = doc.createElement('span');
               chk.innerText = d.checked ? '\u2713' : '';
               chk.style.display = 'inline-block';
               chk.style.width = '1em';
               text.appendChild(chk);
            }

            const sub = doc.createElement('span');
            sub.textContent = d.text;
            text.appendChild(sub);
         }

         hovArea.appendChild(text);

         function changeFocus(fitem, on) {
            if (on) {
               fitem.classList.add(clfocus);
               fitem.style['background-color'] = 'rgb(220, 220, 220)';
            } else if (fitem.classList.contains(clfocus)) {
               fitem.style['background-color'] = null;
               fitem.classList.remove(clfocus);
               fitem.querySelector(`.${clname}`)?.remove();
            }
         }

         if (d.extraText || d.sub) {
            const extraText = doc.createElement('span');
            extraText.className = 'jsroot_ctxt_extraText';
            extraText.style = 'margin: 0; padding: 3px 7px; color: rgba(0, 0, 0, 0.6);';
            extraText.textContent = d.sub ? '\u25B6' : d.extraText;
            hovArea.appendChild(extraText);

            if (d.sub && browser.touches) {
               extraText.addEventListener('click', evnt => {
                  evnt.preventDefault();
                  evnt.stopPropagation();
                  const was_active = item.parentNode.querySelector(`.${clfocus}`);

                  if (was_active)
                     changeFocus(was_active, false);

                  if (item !== was_active) {
                     changeFocus(item, true);
                     this._buildContextmenu(d.sub, 0, 0, item);
                  }
               });
            }
         }

         if (!browser.touches) {
            hovArea.addEventListener('mouseenter', () => {
               if (this.prevHovArea)
                  this.prevHovArea.style['background-color'] = null;
               hovArea.style['background-color'] = 'rgb(235, 235, 235)';
               this.prevHovArea = hovArea;

               outer.childNodes.forEach(chld => changeFocus(chld, false));

               if (d.sub) {
                  changeFocus(item, true);
                  this._buildContextmenu(d.sub, 0, 0, item);
               }
            });
         }

         if (d.func) {
            item.addEventListener('click', evnt => {
               const func = this.painter ? d.func.bind(this.painter) : d.func;
               func(d.arg);
               evnt.stopPropagation();
               this.remove();
            });
         }
      });

      loc.appendChild(outer);

      const docWidth = doc.documentElement.clientWidth, docHeight = doc.documentElement.clientHeight;

      // Now determine where the contextmenu will be
      if (loc === doc.body) {
         if (left + outer.offsetWidth > docWidth) {
            // Does sub-contextmenu overflow window width?
            outer.style.left = (docWidth - outer.offsetWidth) + 'px';
         }
         if (outer.offsetHeight > docHeight) {
            // is the contextmenu height larger than the window height?
            outer.style.top = 0;
            outer.style.overflowY = 'scroll';
            outer.style.overflowX = 'hidden';
            outer.style.height = docHeight + 'px';
         } else if (top + outer.offsetHeight > docHeight) {
            // Does contextmenu overflow window height?
            outer.style.top = (docHeight - outer.offsetHeight) + 'px';
         }
      } else if (outer.className !== clcolumn) {
         // if its sub-contextmenu
         const dimensionsLoc = loc.getBoundingClientRect(), dimensionsOuter = outer.getBoundingClientRect();

         // Does sub-contextmenu overflow window width?
         if (dimensionsOuter.left + dimensionsOuter.width > docWidth)
            outer.style.left = (-loc.offsetLeft - dimensionsOuter.width) + 'px';


         if (dimensionsOuter.height > docHeight) {
            // is the sub-contextmenu height larger than the window height?
            outer.style.top = -dimensionsOuter.top + 'px';
            outer.style.overflowY = 'scroll';
            outer.style.overflowX = 'hidden';
            outer.style.height = docHeight + 'px';
         } else if (dimensionsOuter.height < docHeight && dimensionsOuter.height > docHeight / 2) {
            // is the sub-contextmenu height smaller than the window height AND larger than half of window height?
            if (dimensionsOuter.top - docHeight / 2 >= 0) { // If sub-contextmenu is closer to bottom of the screen
               outer.style.top = (-dimensionsOuter.top - dimensionsOuter.height + docHeight) + 'px';
            } else { // If sub-contextmenu is closer to top of the screen
               outer.style.top = (-dimensionsOuter.top) + 'px';
            }
         } else if (dimensionsOuter.top + dimensionsOuter.height > docHeight) {
            // Does sub-contextmenu overflow window height?
            outer.style.top = (-dimensionsOuter.height + dimensionsLoc.height) + 'px';
         }
      }
      return outer;
   }

   /** @summary Show standalone menu */
   async show(event) {
      this.remove();

      if (!event && this.show_evnt)
         event = this.show_evnt;

      const doc = getDocument(),
            woffset = typeof window === 'undefined' ? { x: 0, y: 0 } : { x: window.scrollX, y: window.scrollY };

      doc.body.addEventListener('click', this.remove_handler);

      doc.getElementById(this.menuname)?.remove();

      this.element = this._buildContextmenu(this.code, (event?.clientX || 0) + woffset.x, (event?.clientY || 0) + woffset.y, doc.body);

      this.element.setAttribute('id', this.menuname);

      return this;
   }

   /** @summary Run modal elements with standalone code */
   createModal(title, main_content, args) {
      if (!args)
         args = {};

      if (!args.Ok)
         args.Ok = 'Ok';

      const modal = { args }, dlg_id = (this?.menuname ?? sDfltName) + sDfltDlg;
      select(`#${dlg_id}`).remove();
      select(`#${dlg_id}_block`).remove();

      const w = Math.min(args.width || 450, Math.round(0.9 * browser.screenWidth));
      modal.block = select('body').append('div')
                                   .attr('id', `${dlg_id}_block`)
                                   .attr('class', 'jsroot_dialog_block')
                                   .attr('style', 'z-index: 100000; position: absolute; left: 0px; top: 0px; bottom: 0px; right: 0px; opacity: 0.2; background-color: white');
      modal.element = select('body')
                      .append('div')
                      .attr('id', dlg_id)
                      .attr('class', 'jsroot_dialog')
                      .style('position', 'absolute')
                      .style('width', `${w}px`)
                      .style('left', '50%')
                      .style('top', '50%')
                      .style('z-index', 100001)
                      .attr('tabindex', '0');

      modal.element.html(
         '<div style=\'position: relative; left: -50%; top: -50%; border: solid green 3px; padding: 5px; display: flex; flex-flow: column; background-color: white\'>' +
           `<div style='flex: 0 1 auto; padding: 5px'>${title}</div>` +
           `<div class='jsroot_dialog_content' style='flex: 1 1 auto; padding: 5px'>${main_content}</div>` +
           '<div class=\'jsroot_dialog_footer\' style=\'flex: 0 1 auto; padding: 5px\'>' +
              `<button class='jsroot_dialog_button' style='float: right; width: fit-content; margin-right: 1em'>${args.Ok}</button>` +
              (args.btns ? '<button class=\'jsroot_dialog_button\' style=\'float: right; width: fit-content; margin-right: 1em\'>Cancel</button>' : '') +
         '</div></div>'
      );

      modal.done = function(res) {
         if (this._done)
            return;
         this._done = true;
         if (isFunc(this.call_back))
            this.call_back(res);
         this.element.remove();
         this.block.remove();
      };

      modal.setContent = function(content, btn_text) {
         if (!this._done) {
            this.element.select('.jsroot_dialog_content').html(content);
            if (btn_text) {
               this.args.Ok = btn_text;
               this.element.select('.jsroot_dialog_button').text(btn_text);
            }
         }
      };

      modal.element.on('keyup', evnt => {
         if ((evnt.code === 'Enter') || (evnt.code === 'Escape')) {
            evnt.preventDefault();
            evnt.stopPropagation();
            modal.done(evnt.code === 'Enter' ? modal.element.node() : null);
         }
      });
      modal.element.on('keydown', evnt => {
         if ((evnt.code === 'Enter') || (evnt.code === 'Escape')) {
            evnt.preventDefault();
            evnt.stopPropagation();
         }
      });
      modal.element.selectAll('.jsroot_dialog_button').on('click', evnt => {
         modal.done(args.btns && (select(evnt.target).text() === args.Ok) ? modal.element.node() : null);
      });

      let f = modal.element.select('.jsroot_dialog_content').select('input');
      if (f.empty())
         f = modal.element.select('.jsroot_dialog_footer').select('button');
      if (!f.empty())
         f.node().focus();
      return modal;
   }

   /** @summary Run modal elements with standalone code */
   async runModal(title, main_content, args) {
      const modal = this.createModal(title, main_content, args);
      return new Promise(resolveFunc => {
         modal.call_back = resolveFunc;
      });
   }

} // class StandaloneMenu


/** @summary Create JSROOT menu
  * @desc See {@link JSRootMenu} class for detailed list of methods
  * @param {object} [evnt] - event object like mouse context menu event
  * @param {object} [handler] - object with handling function, in this case one not need to bind function
  * @param {string} [menuname] - optional menu name
  * @example
  * import { createMenu } from 'https://root.cern/js/latest/modules/gui/menu.mjs';
  * let menu = await createMenu());
  * menu.add('First', () => console.log('Click first'));
  * let flag = true;
  * menu.addchk(flag, 'Checked', arg => console.log(`Now flag is ${arg}`));
  * menu.show(); */
function createMenu(evnt, handler, menuname) {
   const menu = new StandaloneMenu(handler, menuname || sDfltName, evnt);
   return menu.load();
}

/** @summary Close previously created and shown JSROOT menu
  * @param {string} [menuname] - optional menu name */
function closeMenu(menuname) {
   const element = getDocument().getElementById(menuname || sDfltName);
   element?.remove();
   return Boolean(element);
}

/** @summary Returns true if menu or modal dialog present
  * @private */
function hasMenu(menuname) {
   const doc = getDocument();
   if (doc.getElementById(menuname || sDfltName))
      return true;
   if (doc.getElementById((menuname || sDfltName) + sDfltDlg))
      return true;
   return false;
}

/** @summary Fill and show context menu for painter object
  * @private */
function showPainterMenu(evnt, painter, kind) {
   if (isFunc(evnt.stopPropagation)) {
      evnt.stopPropagation(); // disable main context menu
      evnt.preventDefault();  // disable browser context menu
   }

   createMenu(evnt, painter).then(menu => {
      painter.fillContextMenu(menu);
      if (kind === kNoReorder)
         kind = undefined;
      else if (isFunc(painter.bringToFront))
         menu.add('Bring to front', () => painter.bringToFront(true));
      if (kind === kToFront)
         kind = undefined;
      return painter.fillObjectExecMenu(menu, kind);
   }).then(menu => menu.show());
}

/** @summary Internal method to implement modal progress
  * @private */
internals._modalProgress = function(msg, click_handle) {
   if (!msg || !isStr(msg)) {
      internals.modal?.done();
      delete internals.modal;
      return;
   }

   if (!internals.modal)
      internals.modal = StandaloneMenu.prototype.createModal('Progress', msg);

   internals.modal.setContent(msg, click_handle ? 'Abort' : 'Ok');

   internals.modal.call_back = click_handle;
};

/** @summary Assign handler for context menu for painter draw element
  * @private */
function assignContextMenu(painter, kind) {
   if (!painter?.isBatchMode())
      painter?.getG()?.on('contextmenu', settings.ContextMenu ? evnt => showPainterMenu(evnt, painter, kind) : null);
}

Object.assign(internals.jsroot, { createMenu, closeMenu, assignContextMenu, kToFront, kNoReorder });

/** @summary Return time offset value for given TAxis object
  * @private */
function getTimeOffset(axis) {
   const dflt_time_offset = 788918400000;

   if (!axis)
      return dflt_time_offset;
   const idF = axis.fTimeFormat.indexOf('%F');
   if (idF < 0)
      return gStyle.fTimeOffset * 1000;
   let sof = axis.fTimeFormat.slice(idF + 2);
   // default string in axis offset
   if (sof.indexOf('1995-01-01 00:00:00s0') === 0)
      return dflt_time_offset;
   // another default string with unix time
   if (sof.indexOf('1970-01-01 00:00:00s0') === 0)
      return 0;
   // special case, used from DABC painters
   if ((sof === '0') || (sof === ''))
      return 0;

   // decode time from ROOT string
   const next = (separ, min, max) => {
      const pos = sof.indexOf(separ);
      if (pos < 0)
         return min;
      const val = parseInt(sof.slice(0, pos));
      sof = sof.slice(pos + 1);
      if (!Number.isInteger(val) || (val < min) || (val > max))
         return min;
      return val;
   };
   // eslint-disable-next-line one-var
   const year = next('-', 1900, 2900),
         month = next('-', 1, 12) - 1,
         day = next(' ', 1, 31),
         hour = next(':', 0, 23),
         min = next(':', 0, 59),
         sec = next('s', 0, 59),
         msec = next(' ', 0, 999);

   let offset = Date.UTC(year, month, day, hour, min, sec, msec);

   // now also handle suffix like GMT or GMT -0600
   sof = sof.toUpperCase();

   if (sof.indexOf('GMT') === 0) {
      sof = sof.slice(4).trim();
      if (sof.length > 3) {
         let p = 0, sign = 1000;
         if (sof[0] === '-') {
            p = 1;
            sign = -1e3;
         }
         offset -= sign * (parseInt(sof.slice(p, p + 2)) * 3600 + parseInt(sof.slice(p + 2, p + 4)) * 60);
      }
   }

   return offset;
}

/** @summary Return true when GMT option configured in time format
  * @private */
function getTimeGMT(axis) {
   const fmt = axis?.fTimeFormat ?? '';
   return (fmt.indexOf('gmt') > 0) || (fmt.indexOf('GMT') > 0);
}

/** @summary Tries to choose time format for provided time interval
  * @private */
function chooseTimeFormat(awidth, ticks) {
   if (awidth < 0.5)
      return ticks ? '%S.%L' : '%H:%M:%S.%L';
   if (awidth < 30)
      return ticks ? '%Mm%S' : '%H:%M:%S';
   awidth /= 60;
   if (awidth < 30)
      return ticks ? '%Hh%M' : '%d/%m %H:%M';
   awidth /= 60;
   if (awidth < 12)
      return ticks ? '%d-%Hh' : '%d/%m/%y %Hh';
   awidth /= 24;
   if (awidth < 15.218425)
      return ticks ? '%d/%m' : '%d/%m/%y';
   awidth /= 30.43685;
   if (awidth < 6)
      return '%d/%m/%y';
   awidth /= 12;
   if (awidth < 2)
      return ticks ? '%m/%y' : '%d/%m/%y';
   return '%Y';
}

/**
  * @summary Base axis painter methods
  *
  * @private
  */

const AxisPainterMethods = {

   initAxisPainter() {
      this.name = 'yaxis';
      this.kind = kAxisNormal;
      this.func = null;
      this.order = 0; // scaling order for axis labels

      this.full_min = 0;
      this.full_max = 1;
      this.scale_min = 0;
      this.scale_max = 1;
      this.ticks = []; // list of major ticks
   },

   /** @summary Cleanup axis painter */
   cleanupAxisPainter() {
      this.ticks = [];
      delete this.format;
      delete this.func;
      delete this.tfunc1;
      delete this.tfunc2;
      delete this.gr;
   },

   /** @summary Assign often used members of frame painter */
   assignFrameMembers(fp, axis) {
      fp[`gr${axis}`] = this.gr;                 // fp.grx
      fp[`log${axis}`] = this.log;               // fp.logx
      fp[`scale_${axis}min`] = this.scale_min;   // fp.scale_xmin
      fp[`scale_${axis}max`] = this.scale_max;   // fp.scale_xmax
   },

   /** @summary Convert axis value into the Date object */
   convertDate(v) {
      const dt = new Date(this.timeoffset + v * 1000);
      let res = dt;
      if (!this.timegmt && settings.TimeZone) {
         try {
            const ms = dt.getMilliseconds();
            res = new Date(dt.toLocaleString('en-US', { timeZone: settings.TimeZone }));
            res.setMilliseconds(ms);
         } catch {
            res = dt;
         }
      }
      return res;
   },

   /** @summary Convert graphical point back into axis value */
   revertPoint(pnt) {
      const value = this.func.invert(pnt);
      return this.kind === kAxisTime ? (value - this.timeoffset) / 1000 : value;
   },

   /** @summary Provide label for time axis */
   formatTime(dt, asticks) {
      return asticks ? this.tfunc1(dt) : this.tfunc2(dt);
   },

   /** @summary Provide label for log axis */
   formatLog(d, asticks, fmt) {
      const val = parseFloat(d), rnd = Math.round(val);
      if (!asticks)
         return ((rnd === val) && (Math.abs(rnd) < 1e9)) ? rnd.toString() : floatToString(val, fmt || gStyle.fStatFormat);
      if (val <= 0)
         return null;
      let vlog = Math.log10(val);
      const base = this.logbase;
      if (base !== 10)
         vlog /= Math.log10(base);
      if (this.moreloglabels || (Math.abs(vlog - Math.round(vlog)) < 0.001)) {
         if (!this.noexp && (asticks !== 2))
            return this.formatExp(base, Math.floor(vlog + 0.01), val);
         if (Math.abs(base - Math.E) < 0.001)
            return floatToString(val, fmt || gStyle.fStatFormat);
         return (vlog < 0) ? val.toFixed(Math.round(-vlog + 0.5)) : val.toFixed(0);
      }
      return null;
   },

   /** @summary Detect if tick is extra-ticks */
   isExtraLogTick(val) {
      if (!this.log)
         return false;
      let vlog = Math.log10(val);
      if (this.logbase !== 10)
         vlog /= Math.log10(this.logbase);

      return Math.abs(vlog - Math.round(vlog)) >= 0.001;
   },

   /** @summary Provide label for normal axis */
   formatNormal(d, asticks, fmt) {
      let val = parseFloat(d);
      if (asticks && this.order)
         val /= Math.pow(10, this.order);

      if (gStyle.fStripDecimals && (val === Math.round(val)))
         return Math.abs(val) < 1e9 ? val.toFixed(0) : val.toExponential(4);

      if (asticks) {
         if (this.ndig > 10)
            return val.toExponential(this.ndig - 11);
         let res = val.toFixed(this.ndig);
         const p = res.indexOf('.');
         if ((p > 0) && settings.StripAxisLabels) {
            while ((res.length >= p) && ((res.at(-1) === '0') || (res.at(-1) === '.')))
               res = res.slice(0, res.length - 1);
         }
         return res;
      }

      return floatToString(val, fmt || '8.6g');
   },

   /** @summary Provide label for exponential form */
   formatExp(base, order, value) {
      let res = '';
      const sbase = Math.abs(base - Math.E) < 0.001 ? 'e' : base.toString();
      if (value) {
         value = Math.round(value / Math.pow(base, order));
         if (settings.StripAxisLabels) {
            if (order === 0)
               return value.toString();
            else if ((order === 1) && (value === 1))
               return sbase;
         }
         if (value !== 1)
            res = value.toString() + (settings.Latex ? '#times' : 'x');
      }
      res += sbase;
      if (settings.Latex > constants$1.Latex.Symbols)
         return res + `^{${order}}`;
      const superscript_symbols = {
         0: '\u2070', 1: '\xB9', 2: '\xB2', 3: '\xB3', 4: '\u2074', 5: '\u2075',
         6: '\u2076', 7: '\u2077', 8: '\u2078', 9: '\u2079', '-': '\u207B'
      }, str = order.toString();
      for (let n = 0; n < str.length; ++n)
         res += superscript_symbols[str[n]];
      return res;
   },

   /** @summary Convert 'raw' axis value into text */
   axisAsText(value, fmt) {
      if (this.kind === kAxisTime)
         value = this.convertDate(value);
      if (this.format)
         return this.format(value, false, fmt);
      return value.toPrecision(4);
   },

   /** @summary Produce ticks for d3.scaleLog
     * @desc Fixing following problem, described [here]{@link https://stackoverflow.com/questions/64649793} */
   poduceLogTicks(func, number) {
      const linearArray = arr => {
         if (arr.length < 2)
            return false;
         let sum1 = 0, sum2 = 0;
         for (let k = 1; k < arr.length; ++k) {
            const diff = (arr[k] - arr[k - 1]);
            sum1 += diff;
            sum2 += diff ** 2;
         }
         const mean = sum1 / (arr.length - 1),
               dev = sum2 / (arr.length - 1) - mean ** 2;

         if (dev <= 0)
            return true;
         if (Math.abs(mean) < 1e-100)
            return false;
         return Math.sqrt(dev) / mean < 1e-6;
      };

      let arr = func.ticks(number);

      while ((number > 4) && linearArray(arr)) {
         number = Math.round(number * 0.8);
         arr = func.ticks(number);
      }

      // if still linear array, try to sort out 'bad' ticks
      if ((number < 5) && linearArray(arr) && this.logbase && (this.logbase !== 10)) {
         const arr2 = [];
         arr.forEach(val => {
            const pow = Math.log10(val) / Math.log10(this.logbase);
            if (Math.abs(Math.round(pow) - pow) < 0.01)
               arr2.push(val);
         });
         if (arr2.length)
            arr = arr2;
      }

      return arr;
   },

   /** @summary Produce axis ticks */
   produceTicks(ndiv, ndiv2) {
      if (!this.noticksopt) {
         const total = ndiv * (ndiv2 || 1);

         if (this.log)
            return this.poduceLogTicks(this.func, total);

         const dom = this.func.domain(),
               check = ticks => {
                  if (ticks.length <= total)
                     return true;
                  if (ticks.length > total + 1)
                     return false;
                  return (ticks[0] === dom[0]) || (ticks[total] === dom[1]); // special case of N+1 ticks, but match any range
               }, res1 = this.func.ticks(total);
         if (ndiv2 || check(res1))
            return res1;

         const res2 = this.func.ticks(Math.round(total * 0.7));
         return (res2.length > 2) && check(res2) ? res2 : res1;
      }

      const dom = this.func.domain(), ticks = [];
      if (ndiv2)
         ndiv = (ndiv - 1) * ndiv2;
      for (let n = 0; n <= ndiv; ++n)
         ticks.push((dom[0] * (ndiv - n) + dom[1] * n) / ndiv);
      return ticks;
   },

   /** @summary Method analyze mouse wheel event and returns item with suggested zooming range */
   analyzeWheelEvent(evnt, dmin, item, test_ignore) {
      if (!item)
         item = {};

      let delta = 0, delta_left = 1, delta_right = 1;

      if ('dleft' in item) {
         delta_left = item.dleft;
         delta = 1;
      }
      if ('dright' in item) {
         delta_right = item.dright;
         delta = 1;
      }

      if (item.delta)
         delta = item.delta;
      else if (evnt)
         delta = evnt.wheelDelta ? -evnt.wheelDelta : (evnt.deltaY || evnt.detail);

      if (!delta || (test_ignore && item.ignore))
         return;

      delta = (delta < 0) ? -0.2 : 0.2;
      delta_left *= delta;
      delta_right *= delta;

      const lmin = item.min = this.scale_min,
            lmax = item.max = this.scale_max,
            gmin = this.full_min,
            gmax = this.full_max;

      if ((item.min === item.max) && (delta < 0)) {
         item.min = gmin;
         item.max = gmax;
      }

      if (item.min >= item.max)
         return;

      if (item.reverse)
         dmin = 1 - dmin;

      if ((dmin > 0) && (dmin < 1)) {
         if (this.log) {
            let factor = (item.min > 0) ? Math.log10(item.max / item.min) : 2;
            if (factor > 10)
               factor = 10;
            else if (factor < 0.01)
               factor = 0.01;
            item.min /= Math.pow(10, factor * delta_left * dmin);
            item.max *= Math.pow(10, factor * delta_right * (1 - dmin));
            // special handling for Z scale - limit zooming of color scale
            if (this.minposbin && this.name === 'zaxis')
               item.min = Math.max(item.min, 0.3 * this.minposbin);
         } else if ((delta_left === -delta_right) && !item.reverse) {
            // shift left/right, try to keep range constant
            let delta_shift = (item.max - item.min) * delta_right * dmin;

            if ((Math.round(item.max) === item.max) && (Math.round(item.min) === item.min) && (Math.abs(delta_shift) > 1))
               delta_shift = Math.round(delta_shift);

            if (item.min + delta_shift < gmin)
               delta_shift = gmin - item.min;
            else if (item.max + delta_shift > gmax)
               delta_shift = gmax - item.max;

            if (delta_shift) {
               item.min += delta_shift;
               item.max += delta_shift;
            } else {
               delete item.min;
               delete item.max;
            }
         } else {
            let rx_left = (item.max - item.min), rx_right = rx_left;
            if (delta_left > 0)
               rx_left = 1.001 * rx_left / (1 - delta_left);
            item.min += -delta_left * dmin * rx_left;
            if (delta_right > 0)
               rx_right = 1.001 * rx_right / (1 - delta_right);
            item.max -= -delta_right * (1 - dmin) * rx_right;
         }
         if (item.min >= item.max)
            item.min = item.max = undefined;
         else if (delta_left !== delta_right) {
            // extra check case when moving left or right
            if (((item.min < gmin) && (lmin === gmin)) ||
                ((item.max > gmax) && (lmax === gmax)))
               item.min = item.max = undefined;
         } else {
            item.min = Math.max(item.min, gmin);
            item.max = Math.min(item.max, gmax);
         }
      } else
         item.min = item.max = undefined;

      item.changed = (item.min !== undefined) && (item.max !== undefined);

      return item;
   }

}; // AxisPainterMethods


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

class TAxisPainter extends ObjectPainter {

   /** @summary constructor
     * @param {object|string} dom - identifier or dom element
     * @param {object} axis - object to draw
     * @param {boolean} embedded - if true, painter used in other objects painters */
   constructor(dom, axis, embedded) {
      super(dom, axis);

      this.is_gaxis = axis?._typename === clTGaxis;

      Object.assign(this, AxisPainterMethods);
      this.initAxisPainter();

      this.embedded = embedded; // indicate that painter embedded into the histogram painter
      this.invert_side = false;
      this.lbls_both_sides = false; // draw labels on both sides
   }

   /** @summary cleanup painter */
   cleanup() {
      this.cleanupAxisPainter();
      delete this.hist_painter;
      delete this.hist_axis;
      delete this.is_gaxis;
      super.cleanup();
   }

   /** @summary Use in GED to identify kind of axis */
   getAxisType() { return clTAxis; }

   /** @summary Configure axis painter
     * @desc Axis can be drawn inside frame <g> group with offset to 0 point for the frame
     * Therefore one should distinguish when calculated coordinates used for axis drawing itself or for calculation of frame coordinates
     * @private */
   configureAxis(name, min, max, smin, smax, vertical, range, opts) {
      const axis = this.getObject();

      this.name = name;
      this.full_min = min;
      this.full_max = max;
      this.kind = kAxisNormal;
      this.vertical = vertical;
      this.log = opts.log || 0;
      this.minposbin = opts.minposbin;
      this.ignore_labels = opts.ignore_labels;
      this.noexp_changed = opts.noexp_changed;
      this.symlog = opts.symlog || false;
      this.reverse = opts.reverse || false;
      // special flag to change align of labels on vertical axis
      // it is workaround shown in TGaxis docu
      this.reverseAlign = this.vertical && this.reverse && this.is_gaxis && (axis.fX1 !== axis.fX2);
      this.swap_side = opts.swap_side || false;
      this.fixed_ticks = opts.fixed_ticks || null;
      this.maxTickSize = opts.maxTickSize || 0;
      this.value_axis = opts.value_axis ?? false; // use fMinimum/fMaximum from source object

      if (opts.time_scale || axis.fTimeDisplay) {
         this.kind = kAxisTime;
         this.timeoffset = getTimeOffset(axis);
         this.timegmt = getTimeGMT(axis);
      } else if (opts.axis_func)
         this.kind = kAxisFunc;
      else
         this.kind = !axis.fLabels || this.ignore_labels ? kAxisNormal : kAxisLabels;

      if (this.kind === kAxisTime)
         this.func = time().domain([this.convertDate(smin), this.convertDate(smax)]);
      else if (this.log) {
         if ((this.log === 1) || (this.log === 10))
            this.logbase = 10;
         else if (this.log === 3)
            this.logbase = Math.E;
         else
            this.logbase = Math.round(this.log);

         if (smax <= 0)
            smax = 1;

         if (opts.log_min_nz)
            this.log_min_nz = opts.log_min_nz;
         else if (axis && opts.logcheckmin) {
            let v = 0;
            for (let i = 0; i < axis.fNbins; ++i) {
               v = axis.GetBinLowEdge(i + 1);
               if (v > 0)
                  break;
               v = axis.GetBinCenter(i + 1);
               if (v > 0)
                  break;
            }
            if (v > 0)
               this.log_min_nz = v;
         }

         if ((smin <= 0) && this.log_min_nz)
            smin = this.log_min_nz;

         if ((smin <= 0) || (smin >= smax))
            smin = smax * (opts.logminfactor || 1e-4);

         if (this.kind === kAxisFunc)
            this.func = this.createFuncHandle(opts.axis_func, this.logbase, smin, smax);
         else
            this.func = log().base(this.logbase).domain([smin, smax]);
      } else if (this.symlog) {
         let v = Math.max(Math.abs(smin), Math.abs(smax));
         if (Number.isInteger(this.symlog) && (this.symlog > 0))
            v *= Math.pow(10, -1 * this.symlog);
         else
            v *= 0.01;
         this.func = symlog().constant(v).domain([smin, smax]);
      } else if (this.kind === kAxisFunc)
         this.func = this.createFuncHandle(opts.axis_func, 0, smin, smax);
      else
         this.func = linear().domain([smin, smax]);

      if (this.vertical ^ this.reverse)
         [range[0], range[1]] = [range[1], range[0]];

      this.func.range(range);

      this.scale_min = smin;
      this.scale_max = smax;

      if (this.kind === kAxisTime)
         this.gr = val => this.func(this.convertDate(val));
      else if (this.log)
         this.gr = val => { return (val < this.scale_min) ? (this.vertical ? this.func.range()[0] + 5 : -5) : this.func(val); };
      else
         this.gr = this.func;

      delete this.format;// remove formatting func

      let ndiv = 508;
      if (this.is_gaxis)
         ndiv = axis.fNdiv;
      else if (axis)
         ndiv = axis.fNdivisions ? Math.max(axis.fNdivisions, 4) : 0;

      this.nticks = ndiv % 100;
      this.nticks2 = (ndiv % 10000 - this.nticks) / 100;
      this.nticks3 = Math.floor(ndiv / 10000);

      if (axis && !this.is_gaxis)
         this.nticks = Math.min(this.nticks, 20);

      let gr_range = Math.abs(this.func.range()[1] - this.func.range()[0]);
      if (gr_range <= 0)
         gr_range = 100;

      if (this.kind === kAxisTime) {
         this.nticks = Math.min(this.nticks, 8);

         const scale_range = this.scale_max - this.scale_min,
               idF = axis.fTimeFormat.indexOf('%F'),
               tf2 = chooseTimeFormat(scale_range / gr_range, false);
         let tf1 = (idF >= 0) ? axis.fTimeFormat.slice(0, idF) : axis.fTimeFormat;

         if (!tf1 || (scale_range < 0.1 * (this.full_max - this.full_min)))
            tf1 = chooseTimeFormat(scale_range / this.nticks, true);

         this.tfunc1 = this.tfunc2 = this.timegmt ? utcFormat(tf1) : timeFormat(tf1);
         if (tf2 !== tf1)
            this.tfunc2 = this.timegmt ? utcFormat(tf2) : timeFormat(tf2);

         this.format = this.formatTime;
      } else if (this.log) {
         if (this.nticks2 > 1) {
            this.nticks *= this.nticks2; // all log ticks (major or minor) created centrally
            this.nticks2 = 1;
         }
         this.noexp = axis?.TestBit(EAxisBits.kNoExponent);
         if ((this.scale_max < 300) && (this.scale_min > 0.3) && !this.noexp_changed && (this.log === 1))
            this.noexp = true;
         this.moreloglabels = axis?.TestBit(EAxisBits.kMoreLogLabels);
         this.format = this.formatLog;
      } else if (this.kind === kAxisLabels) {
         this.nticks = 50; // for text output allow max 50 names
         const scale_range = this.scale_max - this.scale_min;
         if (this.nticks > scale_range)
            this.nticks = Math.round(scale_range);

         this.regular_labels = true;

         if (axis?.fNbins && axis?.fLabels) {
            if ((axis.fNbins !== Math.round(axis.fXmax - axis.fXmin)) ||
                axis.fXmin || (axis.fXmax !== axis.fNbins))
               this.regular_labels = false;
         }

         this.nticks2 = 1;

         this.format = this.formatLabels;
      } else {
         this.order = 0;
         this.ndig = 0;
         this.format = this.formatNormal;
      }
   }

   /** @summary Check zooming value for log scale
    * @private */
   checkZoomMin(value) {
      return this.log && this.log_min_nz ? Math.max(value, this.log_min_nz) : value;
   }

   /** @summary Return scale min */
   getScaleMin() {
      return this.func?.domain()[0] ?? 0;
   }

   /** @summary Return scale max */
   getScaleMax() {
      return this.func?.domain()[1] ?? 0;
   }

   /** @summary Return true if labels may be removed while they are not fit to graphical range */
   cutLabels() {
      if (!settings.CutAxisLabels)
         return false;
      if (isStr(settings.CutAxisLabels))
         return settings.CutAxisLabels.indexOf(this.name) >= 0;
      return this.vertical; // cut vertical axis by default
   }

   /** @summary Provide label for axis value */
   formatLabels(d) {
      const a = this.getObject();
      let indx = parseFloat(d);
      if (!this.regular_labels)
         indx = Math.round((indx - a.fXmin) / (a.fXmax - a.fXmin) * a.fNbins);
      else
         indx = Math.floor(indx);
      if ((indx < 0) || (indx >= a.fNbins))
         return null;
      const arr = a.fLabels.arr;
      for (let i = 0; i < arr.length; ++i) {
         if (arr[i].fUniqueID === indx + 1)
            return arr[i].fString;
      }
      return null;
   }

   /** @summary Creates array with minor/middle/major ticks */
   createTicks(only_major_as_array, optionNoexp, optionNoopt, optionInt) {
      if (optionNoopt && this.nticks && (this.kind === kAxisNormal))
         this.noticksopt = true;

      const handle = { painter: this, nminor: 0, nmiddle: 0, nmajor: 0, func: this.func, minor: [], middle: [], major: [] };
      let ticks = [];

      if (this.fixed_ticks) {
         this.fixed_ticks.forEach(v => {
            if ((v >= this.scale_min) && (v <= this.scale_max))
               ticks.push(v);
         });
      } else if (this.kind === kAxisLabels) {
         handle.lbl_pos = [];
         const axis = this.getObject();
         for (let n = 0; n <= axis.fNbins; ++n) {
            const x = this.regular_labels ? n : axis.fXmin + n / axis.fNbins * (axis.fXmax - axis.fXmin);
            if ((x >= this.scale_min) && (x <= this.scale_max)) {
               handle.lbl_pos.push(x);
               ticks.push(x);
            }
         }
      } else
         ticks = this.produceTicks(this.nticks);

      handle.minor = handle.middle = handle.major = ticks;

      if (only_major_as_array) {
         const res = handle.major, delta = (this.scale_max - this.scale_min) * 1e-5;
         if (res.at(0) > this.scale_min + delta)
            res.unshift(this.scale_min);
         if (res.at(-1) < this.scale_max - delta)
            res.push(this.scale_max);
         return res;
      }

      if ((this.nticks2 > 1) && (!this.log || (this.logbase === 10)) && !this.fixed_ticks) {
         handle.minor = handle.middle = this.produceTicks(handle.major.length, this.nticks2);
         const gr_range = Math.abs(this.func.range()[1] - this.func.range()[0]);
         // avoid black filling by middle-size
         if ((handle.middle.length <= handle.major.length) || (handle.middle.length > gr_range))
            handle.minor = handle.middle = handle.major;
         else if ((this.nticks3 > 1) && !this.log) {
            handle.minor = this.produceTicks(handle.middle.length, this.nticks3);
            if ((handle.minor.length <= handle.middle.length) || (handle.minor.length > gr_range))
               handle.minor = handle.middle;
         }
      }

      handle.reset = function() {
         this.nminor = this.nmiddle = this.nmajor = 0;
      };

      handle.next = function(doround) {
         if (this.nminor >= this.minor.length)
            return false;

         this.tick = this.minor[this.nminor++];
         this.grpos = this.func(this.tick);
         if (doround)
            this.grpos = Math.round(this.grpos);
         this.kind = 3;

         if ((this.nmiddle < this.middle.length) && (Math.abs(this.grpos - this.func(this.middle[this.nmiddle])) < 1)) {
            this.nmiddle++;
            this.kind = 2;
         }

         if ((this.nmajor < this.major.length) && (Math.abs(this.grpos - this.func(this.major[this.nmajor])) < 1)) {
            this.nmajor++;
            this.kind = 1;
         }
         return true;
      };

      handle.last_major = function() {
         return (this.kind !== 1) ? false : this.nmajor === this.major.length;
      };

      handle.next_major_grpos = function() {
         return this.nmajor >= this.major.length ? null : this.func(this.major[this.nmajor]);
      };

      handle.get_modifier = function() {
         return this.painter.findLabelModifier(this.painter.getObject(), this.nmajor - 1, this.major);
      };

      this.order = 0;
      this.ndig = 0;

      // at the moment when drawing labels, we can try to find most optimal text representation for them
      if (((this.kind === kAxisNormal) || (this.kind === kAxisFunc)) && !this.log && handle.major.length) {
         let maxorder = 0, minorder = 0, exclorder3 = false;

         if (!optionNoexp && !this.cutLabels()) {
            const maxtick = Math.max(Math.abs(handle.major.at(0)), Math.abs(handle.major.at(-1))),
                  mintick = Math.min(Math.abs(handle.major.at(0)), Math.abs(handle.major.at(-1))),
                  ord1 = (maxtick > 0) ? Math.round(Math.log10(maxtick) / 3) * 3 : 0,
                  ord2 = (mintick > 0) ? Math.round(Math.log10(mintick) / 3) * 3 : 0;

            exclorder3 = (maxtick < 2e4); // do not show 10^3 for values below 20000

            if (maxtick || mintick) {
               maxorder = Math.max(ord1, ord2) + 3;
               minorder = Math.min(ord1, ord2) - 3;
            }
         }

         // now try to find best combination of order and ndig for labels
         let bestorder = 0, bestndig = this.ndig, bestlen = 1e10;

         for (let order = minorder; order <= maxorder; order += 3) {
            if (exclorder3 && (order === 3))
               continue;
            this.order = order;
            this.ndig = 0;
            let lbls = [], indx = 0, totallen = 0;
            while (indx < handle.major.length) {
               const v0 = handle.major[indx],
                     lbl = this.format(v0, true);

               let bad_value = lbls.indexOf(lbl) >= 0;
               if (!bad_value) {
                  try {
                     const v1 = parseFloat(lbl) * Math.pow(10, order);
                     bad_value = (Math.abs(v0) > 1e-30) && (Math.abs(v1 - v0) / Math.abs(v0) > 1e-8);
                  } catch {
                     console.warn('Failure by parsing of', lbl);
                     bad_value = true;
                  }
               }
               if (bad_value) {
                  if (++this.ndig > 15) {
                     totallen += 1e10;
                     break; // not too many digits, anyway it will be exponential
                  }
                  lbls = [];
                  indx = totallen = 0;
               } else {
                  lbls.push(lbl);
                  const p = lbl.indexOf('.');
                  if (!order && !optionNoexp && ((p > gStyle.fAxisMaxDigits) || ((p < 0) && (lbl.length > gStyle.fAxisMaxDigits)))) {
                     totallen += 1e10; // do not use order = 0 when too many digits are there
                     exclorder3 = false;
                  }
                  totallen += lbl.length;
                  indx++;
               }
            }

            // for order === 0 we should virtually remove '0.' and extra label on top
            if (!order && (this.ndig < 4))
               totallen -= handle.major.length * 2 + 3;

            if (totallen < bestlen) {
               bestlen = totallen;
               bestorder = this.order;
               bestndig = this.ndig;
            }
         }

         this.order = bestorder;
         this.ndig = bestndig;

         if (optionInt) {
            if (this.order)
               console.warn(`Axis painter - integer labels are configured, but axis order ${this.order} is preferable`);
            if (this.ndig)
               console.warn(`Axis painter - integer labels are configured, but ${this.ndig} decimal digits are required`);
            this.ndig = 0;
            this.order = 0;
         }
      }

      return handle;
   }

   /** @summary Is labels should be centered */
   isCenteredLabels() {
      if (this.kind === kAxisLabels)
         return true;
      if (this.log)
         return false;
      return this.getObject()?.TestBit(EAxisBits.kCenterLabels);
   }

   /** @summary Is labels should be rotated */
   isRotateLabels() {
      return this.getObject()?.TestBit(EAxisBits.kLabelsVert);
   }

   /** @summary Is title should be rotated */
   isRotateTitle() {
      return this.getObject()?.TestBit(EAxisBits.kRotateTitle);
   }

   /** @summary Add interactive elements to draw axes title */
   addTitleDrag(title_g, vertical, offset_k, reverse, axis_length) {
      if (!settings.MoveResize || this.isBatchMode())
         return;

      let drag_rect = null, x_0, y_0, i_0,
          acc_x, acc_y, new_x, new_y, sign_0, alt_pos, curr_indx, can_indx0 = true;
      const drag_move = drag().subject(Object);

      drag_move.on('start', evnt => {
         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();

         const box = title_g.node().getBBox(), // check that elements visible, request precise value
               title_length = vertical ? box.height : box.width;

         x_0 = new_x = acc_x = title_g.property('shift_x');
         y_0 = new_y = acc_y = title_g.property('shift_y');

         sign_0 = vertical ? (acc_x > 0) : (acc_y > 0); // sign should remain
         can_indx0 = !this.hist_painter?.getSnapId(); // online canvas does not allow alternate position

         alt_pos = vertical ? [axis_length, axis_length / 2, 0] : [0, axis_length / 2, axis_length]; // possible positions
         const off = vertical ? -title_length / 2 : title_length / 2;
         if (this.title_align === 'middle') {
            alt_pos[0] += off;
            alt_pos[2] -= off;
         } else if (this.title_align === 'begin') {
            alt_pos[1] -= off;
            alt_pos[2] -= 2 * off;
         } else { // end
            alt_pos[0] += 2 * off;
            alt_pos[1] += off;
         }

         if (this.titleCenter)
            curr_indx = 1;
         else if ((reverse ^ this.titleOpposite) && can_indx0)
            curr_indx = 0;
         else
            curr_indx = 2;

         alt_pos[curr_indx] = vertical ? acc_y : acc_x;
         i_0 = curr_indx;

         drag_rect = title_g.append('rect')
            .attr('x', box.x)
            .attr('y', box.y)
            .attr('width', box.width)
            .attr('height', box.height)
            .style('cursor', 'move')
            .call(addHighlightStyle, true);
         //   .style('pointer-events','none'); // let forward double click to underlying elements
      }).on('drag', evnt => {
         if (!drag_rect)
            return;

         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();

         acc_x += evnt.dx;
         acc_y += evnt.dy;

         let set_x, set_y, besti = can_indx0 ? 0 : 1;
         const p = vertical ? acc_y : acc_x;

         for (let i = 1; i < 3; ++i) {
            if (Math.abs(p - alt_pos[i]) < Math.abs(p - alt_pos[besti]))
               besti = i;
         }

         if (vertical) {
            set_x = acc_x;
            set_y = alt_pos[besti];
         } else {
            set_y = acc_y;
            set_x = alt_pos[besti];
         }

         if (sign_0 === (vertical ? (set_x > 0) : (set_y > 0))) {
            new_x = set_x;
            new_y = set_y;
            curr_indx = besti;
            makeTranslate(title_g, new_x, new_y);
         }
      }).on('end', evnt => {
         if (!drag_rect)
            return;

         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();

         title_g.property('shift_x', new_x)
                .property('shift_y', new_y);

         const axis = this.getObject(), axis2 = this.source_axis,
               setBit = (bit, on) => {
                  axis?.SetBit(bit, on);
                  axis2?.SetBit(bit, on);
               };

         this.titleOffset = (vertical ? new_x : new_y) / offset_k;
         const offset = this.titleOffset / this.offsetScaling / this.titleSize;
         if (axis)
            axis.fTitleOffset = offset;
         if (axis2)
            axis2.fTitleOffset = offset;

         if (curr_indx === 1) {
            setBit(EAxisBits.kCenterTitle, true);
            this.titleCenter = true;
            setBit(EAxisBits.kOppositeTitle, false);
            this.titleOpposite = false;
         } else if (curr_indx === 0) {
            setBit(EAxisBits.kCenterTitle, false);
            this.titleCenter = false;
            setBit(EAxisBits.kOppositeTitle, true);
            this.titleOpposite = true;
         } else {
            setBit(EAxisBits.kCenterTitle, false);
            this.titleCenter = false;
            setBit(EAxisBits.kOppositeTitle, false);
            this.titleOpposite = false;
         }

         drag_rect.remove();
         drag_rect = null;

         if ((x_0 !== new_x) || (y_0 !== new_y) || (i_0 !== curr_indx))
            this.submitAxisExec(`SetTitleOffset(${offset});;SetBit(${EAxisBits.kCenterTitle},${this.titleCenter ? 1 : 0})`);

         if (this.hist_painter && this.hist_axis)
            this.hist_painter.getCanvPainter()?.producePadEvent('select', this.hist_painter.getPadPainter(), this);
      });

      title_g.style('cursor', 'move').call(drag_move);
   }

   /** @summary Configure hist painter which creates axis - to be able submit execs
     * @private */
   setHistPainter(hist_painter, axis_name) {
      this.hist_painter = hist_painter;
      this.hist_axis = axis_name;
   }

   /** @summary Submit exec for the axis - if possible
     * @private */
   submitAxisExec(exec, only_gaxis) {
      const snapid = this.hist_painter?.getSnapId();
      if (snapid && this.hist_axis && !only_gaxis)
         this.submitCanvExec(exec, `${snapid}#${this.hist_axis}`);
      else if (this.is_gaxis)
         this.submitCanvExec(exec);
   }

   /** @summary Produce svg path for axis ticks */
   produceTicksPath(handle, side, tickSize, ticksPlusMinus, secondShift, real_draw) {
      let path1 = '', path2 = '';
      this.ticks = [];

      while (handle.next(true)) {
         let h1 = Math.round(tickSize / 4), h2 = 0;

         if (handle.kind < 3)
            h1 = Math.round(tickSize / 2);

         if (handle.kind === 1) {
            // if not showing labels, not show large tick
            // FIXME: for labels last tick is smaller,
            if (!this.isExtraLogTick(handle.tick) && (this.format(handle.tick, true) !== null))
               h1 = tickSize;
            this.ticks.push(handle.grpos); // keep graphical positions of major ticks
         }

         if (ticksPlusMinus > 0)
            h2 = -h1;
         else if (side < 0) {
            h2 = -h1;
            h1 = 0;
         }

         path1 += this.vertical ? `M${h1},${handle.grpos}H${h2}` : `M${handle.grpos},${-h1}V${-h2}`;

         if (secondShift)
            path2 += this.vertical ? `M${secondShift - h1},${handle.grpos}H${secondShift - h2}` : `M${handle.grpos},${secondShift + h1}V${secondShift + h2}`;
      }

      return real_draw ? path1 + path2 : '';
   }

   /** @summary Returns modifier for axis label */
   findLabelModifier(axis, nlabel, positions) {
      if (!axis.fModLabs)
         return null;
      for (let n = 0; n < axis.fModLabs.arr.length; ++n) {
         const mod = axis.fModLabs.arr[n];

         if ((mod.fLabValue !== undefined) && (mod.fLabNum === 0)) {
            const eps = this.log ? positions[nlabel] * 1e-6 : (this.scale_max - this.scale_min) * 1e-6;
            if (Math.abs(mod.fLabValue - positions[nlabel]) < eps)
               return mod;
         }

         if ((mod.fLabNum === nlabel + 1) ||
             ((mod.fLabNum < 0) && (nlabel === positions.length + mod.fLabNum)))
            return mod;
      }
      return null;
   }

   /** @summary Draw axis labels
     * @return {Promise} with array label size and max width */
   async drawLabels(axis_g, axis, w, h, handle, labelsside, labelsFont, labeloffset, tickSize, ticksPlusMinus, max_text_width, frame_ygap) {
      const center_lbls = this.isCenteredLabels(),
            label_g = [axis_g.append('svg:g').attr('class', 'axis_labels')],
            lbl_pos = handle.lbl_pos || handle.major,
            tilt_angle = gStyle.AxisTiltAngle ?? 25;
      let rotate_lbls = this.isRotateLabels(),
          textscale = 1, flipscale = 1, maxtextlen = 0, applied_scale = 0,
          lbl_tilt = false, any_modified = false, max_textwidth = 0, max_tiltsize = 0;

      if (this.lbls_both_sides)
         label_g.push(axis_g.append('svg:g').attr('class', 'axis_labels').attr('transform', this.vertical ? `translate(${w})` : `translate(0,${-h})`));

      if (frame_ygap > 0)
         max_tiltsize = frame_ygap / Math.sin(tilt_angle / 180 * Math.PI) - Math.tan(tilt_angle / 180 * Math.PI);

      // function called when text is drawn to analyze width, required to correctly scale all labels
      // must be function to correctly handle 'this' argument
      function process_drawtext_ready(painter) {
         const textwidth = this.result_width;
         max_textwidth = Math.max(max_textwidth, textwidth);

         const maxwidth = !this.gap_before ? 0.9 * this.gap_after : (!this.gap_after ? 0.9 * this.gap_before : this.gap_before * 0.45 + this.gap_after * 0.45);

         if (!painter.vertical && !rotate_lbls && this.result_height && maxwidth)
            flipscale = Math.min(flipscale, maxwidth / this.result_height);

         if (textwidth && ((!painter.vertical && !rotate_lbls) || (painter.vertical && rotate_lbls)) && !painter.log)
            textscale = Math.min(textscale, maxwidth / textwidth);
         else if (painter.vertical && max_text_width && this.normal_side && (max_text_width - labeloffset > 20) && (textwidth > max_text_width - labeloffset))
            textscale = Math.min(textscale, (max_text_width - labeloffset) / textwidth);

         if ((textscale > 0.0001) && (textscale < 0.7) && !any_modified &&
              !painter.vertical && !rotate_lbls && (label_g.length === 1) && (lbl_tilt === false)) {
            if (maxtextlen > 5)
               lbl_tilt = true;
         }

         let scale = textscale;

         if (lbl_tilt) {
            if (max_tiltsize && max_textwidth) {
               scale = Math.min(1, 0.8 * max_tiltsize / max_textwidth);
               if (scale < textscale) {
                  // if due to tilt scale is even smaller - ignore tilting
                  lbl_tilt = 0;
                  scale = textscale;
               }
            } else
               scale *= 3;
         }

         if (((scale > 0.0001) && (scale < 1)) || (lbl_tilt !== false)) {
            applied_scale = 1 / scale;
            painter.scaleTextDrawing(applied_scale, label_g[0]);
         }
      }

      // check if short labels can be rotated
      if (!this.vertical && this.regular_labels && !rotate_lbls) {
         let tlen = 0;
         for (let nmajor = 0; nmajor < lbl_pos.length; ++nmajor) {
            const text = this.format(lbl_pos[nmajor], true);
            if (text)
               tlen = Math.max(tlen, text.length);
         }

         if ((tlen > 2) && (tlen <= 5) && (lbl_pos.length * labelsFont.size > w / 2)) {
            rotate_lbls = true;
            lbl_tilt = 0;
         }
      }

      const draw_labels = (lcnt, side) => {
         return this.startTextDrawingAsync(labelsFont, 'font', label_g[lcnt]).then(() => {
            let lastpos = 0;
            const fix_coord = this.vertical ? -labeloffset * side : labeloffset * side + ticksPlusMinus * tickSize;

            for (let nmajor = 0; nmajor < lbl_pos.length; ++nmajor) {
               let text = this.format(lbl_pos[nmajor], true);
               if (text === null)
                  continue;

               const mod = this.findLabelModifier(axis, nmajor, lbl_pos);
               if (mod?.fTextSize === 0)
                  continue;

               if (mod)
                  any_modified = true;
               if (mod?.fLabText)
                  text = mod.fLabText;

               const arg = { text, color: labelsFont.color, latex: 1, draw_g: label_g[lcnt], normal_side: (lcnt === 0) };
               let pos = Math.round(this.func(lbl_pos[nmajor]));

               // exclude labels for extra log ticks
               if (lastpos && this.vertical && (Math.abs(pos - lastpos) < labelsFont.size * 1.1) && this.isExtraLogTick(lbl_pos[nmajor])) {
                  lastpos = pos;
                  continue;
               }

               if (mod?.fTextColor > 0)
                  arg.color = this.getColor(mod.fTextColor);

               arg.gap_before = (nmajor > 0) ? Math.abs(Math.round(pos - this.func(lbl_pos[nmajor - 1]))) : 0;

               arg.gap_after = (nmajor < lbl_pos.length - 1) ? Math.abs(Math.round(this.func(lbl_pos[nmajor + 1]) - pos)) : 0;

               if (center_lbls) {
                  const gap = arg.gap_after || arg.gap_before;
                  pos = Math.round(pos - ((this.vertical !== this.reverse) ? 0.5 * gap : -0.5 * gap));
                  if ((pos < -5) || (pos > (this.vertical ? h : w) + 5))
                     continue;
               }

               maxtextlen = Math.max(maxtextlen, text.length);

               if (this.vertical) {
                  arg.x = fix_coord;
                  arg.y = pos;
                  const flag = this.optionLeft || this.reverseAlign || (side < 0);
                  arg.align = rotate_lbls ? (flag ? 23 : 21) : (flag ? 12 : 32);
                  if (this.cutLabels()) {
                     const gap = labelsFont.size * (rotate_lbls ? 1.5 : 0.6);
                     if ((pos < gap) || (pos > h - gap))
                        continue;
                  }
               } else {
                  arg.x = pos;
                  arg.y = fix_coord;
                  arg.align = rotate_lbls ? ((side < 0) ? 12 : 32) : ((side < 0) ? 21 : 23);
                  if (this.log && !this.noexp && !this.vertical && arg.align === 23) {
                     arg.align = 21;
                     arg.y += labelsFont.size;
                  } else if (arg.align % 10 === 3)
                     arg.y -= labelsFont.size * 0.1; // font takes 10% more by top align

                  if (this.cutLabels()) {
                     const gap = labelsFont.size * (rotate_lbls ? 0.4 : 1.5);
                     if ((pos < gap) || (pos > w - gap))
                        continue;
                  }
               }

               if (rotate_lbls)
                  arg.rotate = 270;
               else if (mod && mod.fTextAngle !== -1)
                  arg.rotate = -mod.fTextAngle;

               // only for major text drawing scale factor need to be checked
               // for modified labels ignore scaling
               if ((lcnt === 0) && !mod?.fLabText)
                  arg.post_process = process_drawtext_ready;

               this.drawText(arg);

               // workaround for symlog where labels can be compressed to close
               if (this.symlog && lastpos && (pos !== lastpos) && ((this.vertical && !rotate_lbls) || (!this.vertical && rotate_lbls))) {
                  const axis_step = Math.abs(pos - lastpos);
                  textscale = Math.min(textscale, 1.1 * axis_step / labelsFont.size);
               }

               lastpos = pos;
            }

            if (this.order) {
               let xoff = 0, yoff = 0;
               if (this.name === 'xaxis') {
                  xoff = gStyle.fXAxisExpXOffset || 0;
                  yoff = gStyle.fXAxisExpYOffset || 0;
               } else if (this.name === 'yaxis') {
                  xoff = gStyle.fYAxisExpXOffset || 0;
                  yoff = gStyle.fYAxisExpYOffset || 0;
               }

               if (xoff)
                  xoff = Math.round(xoff * (this.getPadPainter()?.getPadWidth() ?? 0));
               if (yoff)
                  yoff = Math.round(yoff * (this.getPadPainter()?.getPadHeight() ?? 0));

               this.drawText({
                  color: labelsFont.color,
                  x: xoff + (this.vertical ? side * 5 : w + 5),
                  y: yoff + (this.has_obstacle ? fix_coord : (this.vertical ? -3 : -3 * side)),
                  align: this.vertical ? ((side < 0) ? 30 : 10) : ((this.has_obstacle ^ (side < 0)) ? 13 : 10),
                  latex: 1,
                  text: '#times' + this.formatExp(10, this.order),
                  draw_g: label_g[lcnt]
               });
            }

            if ((lcnt > 0) && applied_scale)
               this.scaleTextDrawing(applied_scale, label_g[lcnt]);

            return this.finishTextDrawing(label_g[lcnt], true);
         });
      };

      return draw_labels(0, labelsside).then(() => {
         return label_g.length < 2 ? true : draw_labels(1, -labelsside);
      }).then(() => {
         this._maxlbllen = maxtextlen; // for internal use in palette painter

         if (lbl_tilt) {
            label_g[0].selectAll('text').each(function() {
               const txt = select(this), tr = txt.attr('transform');
               if (lbl_tilt)
                  txt.attr('transform', `${tr} rotate(${tilt_angle})`).style('text-anchor', 'start');
            });
         }

         return max_textwidth;
      });
   }

   /** @summary Extract major draw attributes, which are also used in interactive operations
     * @private  */
   extractDrawAttributes(scalingSize, w, h) {
      const axis = this.getObject();
      let pp = this.getPadPainter();
      if (axis.$use_top_pad)
         pp = pp?.getPadPainter(); // workaround for ratio plot
      const pad_w = pp?.getPadWidth() || scalingSize || w / 0.8, // use factor 0.8 as ratio between frame and pad size
            pad_h = pp?.getPadHeight() || scalingSize || h / 0.8,
            // if no external scaling size use scaling as in TGaxis.cxx:1448 - NDC axis length is in the scaling factor
            tickScalingSize = scalingSize || (this.vertical ? h / pad_h * pad_w : w / pad_w * pad_h),
            bit_plus = axis.TestBit(EAxisBits.kTickPlus), bit_minus = axis.TestBit(EAxisBits.kTickMinus);

      let tickSize, titleColor, titleFontId, offset;

      this.scalingSize = scalingSize || Math.max(Math.min(pad_w, pad_h), 10);

      if (this.is_gaxis) {
         const optionSize = axis.fChopt.indexOf('S') >= 0;
         this.optionUnlab = axis.fChopt.indexOf('U') >= 0;
         this.optionMinus = (axis.fChopt.indexOf('-') >= 0) || bit_minus;
         this.optionPlus = (axis.fChopt.indexOf('+') >= 0) || bit_plus;
         this.optionNoopt = (axis.fChopt.indexOf('N') >= 0);  // no ticks position optimization
         this.optionInt = (axis.fChopt.indexOf('I') >= 0);  // integer labels
         this.optionText = (axis.fChopt.indexOf('T') >= 0);  // text scaling?
         this.optionLeft = (axis.fChopt.indexOf('L') >= 0);  // left text align
         this.optionRight = (axis.fChopt.indexOf('R') >= 0);  // right text align
         this.optionCenter = (axis.fChopt.indexOf('C') >= 0);  // center text align
         this.createAttLine({ attr: axis });
         tickSize = optionSize ? axis.fTickSize : 0.03;
         titleColor = this.getColor(axis.fTextColor);
         titleFontId = axis.fTextFont;
         offset = axis.fLabelOffset;
         // workaround for old reverse axes where offset is not properly working
         if (this.reverse && (!this.vertical || (!this.optionMinus && (axis.fX1 !== axis.fX2))))
            offset = -offset;
      } else {
         this.optionUnlab = false;
         if (!bit_plus && !bit_minus) {
            this.optionMinus = this.vertical ^ this.invert_side;
            this.optionPlus = !this.optionMinus;
         } else {
            this.optionPlus = bit_plus;
            this.optionMinus = bit_minus;
         }
         this.optionNoopt = false;  // no ticks position optimization
         this.optionInt = false;  // integer labels
         this.optionText = false;
         this.createAttLine({ color: axis.fAxisColor, width: 1, style: 1 });
         tickSize = axis.fTickLength;
         titleColor = this.getColor(axis.fTitleColor);
         titleFontId = axis.fTitleFont;
         offset = axis.fLabelOffset;
      }

      offset += (this.vertical ? 0.002 : 0.005);

      if (this.kind === kAxisLabels)
         this.optionText = true;

      this.optionNoexp = axis.TestBit(EAxisBits.kNoExponent);

      this.ticksSize = Math.round(tickSize * tickScalingSize);
      if (scalingSize && (this.ticksSize < 0))
         this.ticksSize = -this.ticksSize;

      if (this.maxTickSize && (this.ticksSize > this.maxTickSize))
         this.ticksSize = this.maxTickSize;

      // now used only in 3D drawing
      this.ticksColor = this.lineatt.color;
      this.ticksWidth = this.lineatt.width;

      const k = this.optionText ? 0.66666 : 1; // set TGaxis.cxx, line 1504
      this.labelSize = Math.round((axis.fLabelSize < 1) ? k * axis.fLabelSize * this.scalingSize : k * axis.fLabelSize);
      this.labelsOffset = Math.round(offset * this.scalingSize);
      this.labelsFont = new FontHandler(axis.fLabelFont, this.labelSize, scalingSize);
      if ((this.labelSize <= 0) || (Math.abs(axis.fLabelOffset) > 1.1))
         this.optionUnlab = true; // disable labels when size not specified
      this.labelsFont.setColor(this.getColor(axis.fLabelColor));

      this.fTitle = axis.fTitle;
      if (this.fTitle) {
         this.titleSize = (axis.fTitleSize >= 1) ? axis.fTitleSize : Math.round(axis.fTitleSize * this.scalingSize);
         this.titleFont = new FontHandler(titleFontId, this.titleSize, scalingSize);
         this.titleFont.setColor(titleColor);
         this.offsetScaling = (axis.fTitleSize >= 1) ? 1 : (this.vertical ? pad_w : pad_h) / this.scalingSize;
         this.titleOffset = axis.fTitleOffset;
         if (!this.titleOffset && this.name[0] === 'x')
            this.titleOffset = gStyle.fXaxis.fTitleOffset;
         this.titleOffset *= this.titleSize * this.offsetScaling;
         this.titleCenter = axis.TestBit(EAxisBits.kCenterTitle);
         this.titleOpposite = axis.TestBit(EAxisBits.kOppositeTitle);
      } else {
         delete this.titleSize;
         delete this.titleFont;
         delete this.offsetScaling;
         delete this.titleOffset;
         delete this.titleCenter;
         delete this.titleOpposite;
      }
   }

   /** @summary function draws TAxis or TGaxis object
     * @return {Promise} for drawing ready */
   async drawAxis(layer, w, h, transform, secondShift, disable_axis_drawing, max_text_width, calculate_position, frame_ygap) {
      const axis = this.getObject(),
            swap_side = this.swap_side || false;
      let axis_g = layer, draw_lines = true;

      // shift for second ticks set (if any)
      if (!secondShift)
         secondShift = 0;
      else if (this.invert_side)
         secondShift = -secondShift;

      this.extractDrawAttributes(undefined, w, h);

      if (this.is_gaxis)
         draw_lines = Boolean(axis.fLineColor);

      if (!this.is_gaxis || (this.name === 'zaxis')) {
         axis_g = layer.selectChild(`.${this.name}_container`);
         if (axis_g.empty())
            axis_g = layer.append('svg:g').attr('class', `${this.name}_container`);
         else
            axis_g.selectAll('*').remove();
      }

      let axis_lines = '';
      if (draw_lines) {
         axis_lines = 'M0,0' + (this.vertical ? `v${h}` : `h${w}`);
         if (secondShift)
            axis_lines += this.vertical ? `M${secondShift},0v${h}` : `M0,${secondShift}h${w}`;
      }

      axis_g.attr('transform', transform);

      let side = 1, ticksPlusMinus = 0;

      if (this.optionPlus && this.optionMinus)
         side = ticksPlusMinus = 1;
      else if (this.optionMinus)
         side = (swap_side ^ this.vertical) ? 1 : -1;
      else if (this.optionPlus)
         side = (swap_side ^ this.vertical) ? -1 : 1;

      // first draw ticks

      const handle = this.createTicks(false, this.optionNoexp, this.optionNoopt, this.optionInt);

      axis_lines += this.produceTicksPath(handle, side, this.ticksSize, ticksPlusMinus, secondShift, draw_lines && !disable_axis_drawing && !this.disable_ticks);

      if (!disable_axis_drawing && axis_lines && !this.lineatt.empty()) {
         axis_g.append('svg:path')
               .attr('d', axis_lines)
               .call(this.lineatt.func);
      }

      let title_shift_x = 0, title_shift_y = 0, title_g, labelsMaxWidth = 0;
      // draw labels (sometime on both sides)
      const labelSize = Math.max(this.labelsFont.size, 5),
            pr = (disable_axis_drawing || this.optionUnlab)
                ? Promise.resolve(0)
                : this.drawLabels(axis_g, axis, w, h, handle, side, this.labelsFont, this.labelsOffset, this.ticksSize, ticksPlusMinus, max_text_width, frame_ygap);

      return pr.then(maxw => {
         labelsMaxWidth = maxw;

         if (settings.Zooming && !this.disable_zooming && !this.isBatchMode()) {
            const r = axis_g.append('svg:rect')
                            .attr('class', 'axis_zoom')
                            .style('opacity', '0')
                            .style('cursor', 'crosshair');

            if (this.vertical) {
               const rw = Math.max(labelsMaxWidth, 2 * labelSize) + 3;
               r.attr('x', (side > 0) ? -rw : 0).attr('y', 0)
                .attr('width', rw).attr('height', h);
            } else {
               r.attr('x', 0).attr('y', (side > 0) ? 0 : -labelSize - 3)
                .attr('width', w).attr('height', labelSize + 3);
            }
         }

         this.position = 0;

         if (calculate_position) {
            const node1 = axis_g.node(),
                  node2 = this.getPadPainter()?.getPadSvg().node();
            if (isFunc(node1?.getBoundingClientRect) && isFunc(node2?.getBoundingClientRect)) {
               const rect1 = node1.getBoundingClientRect(),
                     rect2 = node2.getBoundingClientRect();
               this.position = rect1.left - rect2.left; // use to control left position of Y scale
            }
            if (node1 && !node2)
               console.warn('Why PAD element missing when search for position');
         }

         if (!this.fTitle || disable_axis_drawing)
            return;

         title_g = axis_g.append('svg:g').attr('class', 'axis_title');

         return this.startTextDrawingAsync(this.titleFont, 'font', title_g);
      }).then(() => {
         if (!title_g)
            return;

         const rotate = this.isRotateTitle() ? -1 : 1,
               xor_reverse = swap_side ^ this.titleOpposite, myxor = (rotate < 0) ^ xor_reverse;

         let title_offest_k = side;

         this.title_align = this.titleCenter ? 'middle' : (myxor ? 'begin' : 'end');

         if (this.vertical) {
            title_offest_k *= -1.6;

            title_shift_x = Math.round(title_offest_k * this.titleOffset);

            title_shift_y = Math.round(this.titleCenter ? h / 2 : (xor_reverse ? h : 0));

            this.drawText({
               align: this.title_align + ';middle',
               rotate: (rotate < 0) ? 90 : 270,
               text: this.fTitle, color: this.titleFont.color, draw_g: title_g
            });
         } else {
            title_offest_k *= 1.6;

            title_shift_x = Math.round(this.titleCenter ? w / 2 : (xor_reverse ? 0 : w));
            title_shift_y = Math.round(title_offest_k * this.titleOffset);
            this.drawText({
               align: this.title_align + ';middle',
               rotate: (rotate < 0) ? 180 : 0,
               text: this.fTitle, color: this.titleFont.color, draw_g: title_g
            });
         }

         this.addTitleDrag(title_g, this.vertical, title_offest_k, swap_side, this.vertical ? h : w);

         return this.finishTextDrawing(title_g);
      }).then(() => {
         if (title_g) {
            if (!this.titleOffset && this.vertical)
               title_shift_x = Math.round(-side * ((labelsMaxWidth || labelSize) + 0.7 * this.offsetScaling * this.titleSize));
            makeTranslate(title_g, title_shift_x, title_shift_y);
            title_g.property('shift_x', title_shift_x)
                  .property('shift_y', title_shift_y);
         }

         return this;
      });
   }

} // class TAxisPainter

const logminfactorX = 0.0001, logminfactorY = 3e-4;

/** @summary Configure tooltip enable flag for painter
  * @private */
function setPainterTooltipEnabled(painter, on) {
   if (!painter)
      return;

   const fp = painter.getFramePainter();
   if (isFunc(fp?.setTooltipEnabled)) {
      fp.setTooltipEnabled(on);
      fp.processFrameTooltipEvent(null);
   }
   // this is 3D control object
   if (isFunc(painter.control?.setTooltipEnabled))
      painter.control.setTooltipEnabled(on);
}

/** @summary Return pointers on touch event
  * @private */
function get_touch_pointers(event, node) {
   return event.$touch_arr ?? pointers(event, node);
}

/** @summary Returns coordinates transformation func
  * @private */
function getEarthProjectionFunc(id) {
   switch (id) {
      // Aitoff2xy
      case 1: return (l, b) => {
         const DegToRad = Math.PI / 180,
               alpha2 = (l / 2) * DegToRad,
               delta = b * DegToRad,
               r2 = Math.sqrt(2),
               f = 2 * r2 / Math.PI,
               cdec = Math.cos(delta),
               denom = Math.sqrt(1.0 + cdec * Math.cos(alpha2));
         return {
            x: cdec * Math.sin(alpha2) * 2.0 * r2 / denom / f / DegToRad,
            y: Math.sin(delta) * r2 / denom / f / DegToRad
         };
      };
      // mercator
      case 2: return (l, b) => { return { x: l, y: Math.log(Math.tan((Math.PI / 2 + b / 180 * Math.PI) / 2)) }; };
      // sinusoidal
      case 3: return (l, b) => { return { x: l * Math.cos(b / 180 * Math.PI), y: b }; };
      // parabolic
      case 4: return (l, b) => { return { x: l * (2.0 * Math.cos(2 * b / 180 * Math.PI / 3) - 1), y: 180 * Math.sin(b / 180 * Math.PI / 3) }; };
      // Mollweide projection
      case 5: return (l, b) => {
         const theta0 = b * Math.PI / 180;
         let theta = theta0, num, den;
         for (let i = 0; i < 100; i++) {
            num = 2 * theta + Math.sin(2 * theta) - Math.PI * Math.sin(theta0);
            den = 4 * (Math.cos(theta) ** 2);
            if (den < 1e-20) {
               theta = theta0;
               break;
            }
            theta -= num / den;
            if (Math.abs(num / den) < 1e-4)
               break;
         }
         return {
            x: l * Math.cos(theta),
            y: 90 * Math.sin(theta)
         };
      };
   }
}

/** @summary Unzoom preselected range for main histogram painter
  * @desc Used with TGraph where Y zooming selected with fMinimum/fMaximum but histogram
  * axis range can be wider. Or for normal histogram drawing when preselected range smaller than histogram range
  * @private */
function unzoomHistogramYRange(main) {
   if (!isFunc(main?.getDimension) || main.getDimension() !== 1)
      return;

   const ymin = main.draw_content ? main.hmin : main.ymin,
         ymax = main.draw_content ? main.hmax : main.ymax;

   if ((main.zoom_ymin !== main.zoom_ymax) && (ymin !== ymax) &&
        (ymin <= main.zoom_ymin) && (main.zoom_ymax <= ymax))
      main.zoom_ymin = main.zoom_ymax = 0;
}

// global, allow single drag at once
let drag_rect = null, drag_kind = '', drag_painter = null;

/** @summary Check if dragging performed currently
  * @private */
function is_dragging(painter, kind) {
   return drag_rect && (drag_painter === painter) && (drag_kind === kind);
}

/** @summary Add drag for interactive rectangular elements for painter
  * @private */
function addDragHandler(_painter, arg) {
   if (!settings.MoveResize)
      return;

   const painter = _painter, pp = painter.getPadPainter();
   if (pp?.isFastDrawing() || pp?.isBatchMode())
      return;
   // cleanup all drag elements when canvas is not editable
   if (pp?.isEditable() === false)
      arg.cleanup = true;

   if (!isFunc(arg.getDrawG))
      arg.getDrawG = () => painter?.getG();

   function makeResizeElements(group, handler) {
      function addElement(cursor, d) {
         const clname = 'js_' + cursor.replace(/[-]/g, '_');
         let elem = group.selectChild('.' + clname);
         if (arg.cleanup)
            return elem.remove();
         if (elem.empty())
            elem = group.append('path').classed(clname, true);
         elem.style('opacity', 0).style('cursor', cursor).attr('d', d);
         if (handler)
            elem.call(handler);
      }

      addElement('nw-resize', 'M2,2h15v-5h-20v20h5Z');
      addElement('ne-resize', `M${arg.width - 2},2h-15v-5h20v20h-5 Z`);
      addElement('sw-resize', `M2,${arg.height - 2}h15v5h-20v-20h5Z`);
      addElement('se-resize', `M${arg.width - 2},${arg.height - 2}h-15v5h20v-20h-5Z`);

      if (!arg.no_change_x) {
         addElement('w-resize', `M-3,18h5v${Math.max(0, arg.height - 2 * 18)}h-5Z`);
         addElement('e-resize', `M${arg.width + 3},18h-5v${Math.max(0, arg.height - 2 * 18)}h5Z`);
      }
      if (!arg.no_change_y) {
         addElement('n-resize', `M18,-3v5h${Math.max(0, arg.width - 2 * 18)}v-5Z`);
         addElement('s-resize', `M18,${arg.height + 3}v-5h${Math.max(0, arg.width - 2 * 18)}v5Z`);
      }
   }

   const complete_drag = (newx, newy, newwidth, newheight) => {
      drag_painter = null;
      drag_kind = '';
      if (drag_rect) {
         drag_rect.remove();
         drag_rect = null;
      }

      const draw_g = arg.getDrawG();

      if (!draw_g)
         return false;

      const oldx = arg.x, oldy = arg.y;

      if (arg.minwidth && newwidth < arg.minwidth)
         newwidth = arg.minwidth;
      if (arg.minheight && newheight < arg.minheight)
         newheight = arg.minheight;

      const change_size = (newwidth !== arg.width) || (newheight !== arg.height),
            change_pos = (newx !== oldx) || (newy !== oldy);

      arg.x = newx;
      arg.y = newy;
      arg.width = newwidth;
      arg.height = newheight;

      if (!arg.no_transform)
         makeTranslate(draw_g, newx, newy);

      setPainterTooltipEnabled(painter, true);

      makeResizeElements(draw_g);

      if (change_size || change_pos) {
         if (change_size && isFunc(arg.resize))
            arg.resize(newwidth, newheight);

         if (change_pos && isFunc(arg.move))
            arg.move(newx, newy, newx - oldx, newy - oldy);

         if (change_size || change_pos) {
            if (arg.obj) {
               const rect = arg.pad_rect ?? pp.getPadRect();
               arg.obj.fX1NDC = newx / rect.width;
               arg.obj.fX2NDC = (newx + newwidth) / rect.width;
               arg.obj.fY1NDC = 1 - (newy + newheight) / rect.height;
               arg.obj.fY2NDC = 1 - newy / rect.height;
               arg.obj.$modifiedNDC = true; // indicate that NDC was interactively changed, block in updated
            } else if (isFunc(arg.move_resize))
               arg.move_resize(newx, newy, newwidth, newheight);

            if (isFunc(arg.redraw))
               arg.redraw(arg);
         }
      }

      return change_size || change_pos;
   }, drag_move = drag().subject(Object), drag_move_off = drag().subject(Object);

   drag_move_off.on('start', null).on('drag', null).on('end', null);

   drag_move
      .on('start', evnt => {
         if (detectRightButton(evnt.sourceEvent) || drag_kind)
            return;
         if (isFunc(arg.is_disabled) && arg.is_disabled('move'))
            return;

         closeMenu(); // close menu
         setPainterTooltipEnabled(painter, false); // disable tooltip

         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();

         const pad_rect = arg.pad_rect ?? pp.getPadRect(), handle = {
            x: arg.x, y: arg.y, width: arg.width, height: arg.height,
            acc_x1: arg.x, acc_y1: arg.y,
            pad_w: pad_rect.width - arg.width,
            pad_h: pad_rect.height - arg.height,
            drag_tm: new Date(),
            path: `v${arg.height}h${arg.width}v${-arg.height}z`,
            evnt_x: evnt.x, evnt_y: evnt.y
         };

         drag_painter = painter;
         drag_kind = 'move';
         drag_rect = select(arg.getDrawG().node().parentNode).append('path')
            .attr('d', `M${handle.acc_x1},${handle.acc_y1}${handle.path}`)
            .style('cursor', 'move')
            .style('pointer-events', 'none') // let forward double click to underlying elements
            .property('drag_handle', handle)
            .call(addHighlightStyle, true);
      }).on('drag', evnt => {
         if (!is_dragging(painter, 'move'))
            return;

         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();

         const handle = drag_rect.property('drag_handle');

         if (!arg.no_change_x)
            handle.acc_x1 += evnt.dx;
         if (!arg.no_change_y)
            handle.acc_y1 += evnt.dy;

         handle.x = Math.min(Math.max(handle.acc_x1, 0), handle.pad_w);
         handle.y = Math.min(Math.max(handle.acc_y1, 0), handle.pad_h);

         drag_rect.attr('d', `M${handle.x},${handle.y}${handle.path}`);
      }).on('end', evnt => {
         if (!is_dragging(painter, 'move'))
            return;

         evnt.sourceEvent.stopPropagation();
         evnt.sourceEvent.preventDefault();

         const handle = drag_rect.property('drag_handle');

         if (complete_drag(handle.x, handle.y, arg.width, arg.height) === false) {
            const spent = (new Date()).getTime() - handle.drag_tm.getTime();

            if (arg.ctxmenu && (spent > 600))
               showPainterMenu({ clientX: handle.evnt_x, clientY: handle.evnt_y, skip_close: 1 }, painter);
            else if (arg.canselect && (spent <= 600))
               painter.getPadPainter()?.selectObjectPainter(painter);
         }
      });

   const drag_resize = drag().subject(Object);

   drag_resize
      .on('start', function(evnt) {
         if (detectRightButton(evnt.sourceEvent) || drag_kind)
            return;
         if (isFunc(arg.is_disabled) && arg.is_disabled('resize'))
            return;

         closeMenu(); // close menu
         setPainterTooltipEnabled(painter, false); // disable tooltip

         evnt.sourceEvent.stopPropagation();
         evnt.sourceEvent.preventDefault();

         const pad_rect = arg.pad_rect ?? pp.getPadRect(), handle = {
            x: arg.x, y: arg.y, width: arg.width, height: arg.height,
            acc_x1: arg.x, acc_y1: arg.y,
            acc_x2: arg.x + arg.width, acc_y2: arg.y + arg.height,
            pad_w: pad_rect.width, pad_h: pad_rect.height
         };

         drag_painter = painter;
         drag_kind = 'resize';
         drag_rect = select(arg.getDrawG().node().parentNode)
            .append('rect')
            .style('cursor', select(this).style('cursor'))
            .attr('x', handle.acc_x1)
            .attr('y', handle.acc_y1)
            .attr('width', handle.acc_x2 - handle.acc_x1)
            .attr('height', handle.acc_y2 - handle.acc_y1)
            .property('drag_handle', handle)
            .call(addHighlightStyle, true);
      }).on('drag', function(evnt) {
         if (!is_dragging(painter, 'resize'))
            return;

         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();

         const handle = drag_rect.property('drag_handle'),
               elem = select(this),
               dx = arg.no_change_x ? 0 : evnt.dx,
               dy = arg.no_change_y ? 0 : evnt.dy;

         if (elem.classed('js_nw_resize')) {
            handle.acc_x1 += dx;
            handle.acc_y1 += dy;
         } else if (elem.classed('js_ne_resize')) {
            handle.acc_x2 += dx;
            handle.acc_y1 += dy;
         } else if (elem.classed('js_sw_resize')) {
            handle.acc_x1 += dx;
            handle.acc_y2 += dy;
         } else if (elem.classed('js_se_resize')) {
            handle.acc_x2 += dx;
            handle.acc_y2 += dy;
         } else if (elem.classed('js_w_resize'))
            handle.acc_x1 += dx;
         else if (elem.classed('js_n_resize'))
            handle.acc_y1 += dy;
         else if (elem.classed('js_e_resize'))
            handle.acc_x2 += dx;
         else if (elem.classed('js_s_resize'))
            handle.acc_y2 += dy;

         const x1 = Math.max(0, handle.acc_x1), x2 = Math.min(handle.acc_x2, handle.pad_w),
               y1 = Math.max(0, handle.acc_y1), y2 = Math.min(handle.acc_y2, handle.pad_h);

         handle.x = Math.min(x1, x2);
         handle.y = Math.min(y1, y2);
         handle.width = Math.abs(x2 - x1);
         handle.height = Math.abs(y2 - y1);

         drag_rect.attr('x', handle.x).attr('y', handle.y).attr('width', handle.width).attr('height', handle.height);
      }).on('end', evnt => {
         if (!is_dragging(painter, 'resize'))
            return;

         evnt.sourceEvent.preventDefault();

         const handle = drag_rect.property('drag_handle');

         complete_drag(handle.x, handle.y, handle.width, handle.height);
      });

   if (!arg.only_resize)
      arg.getDrawG().style('cursor', arg.cleanup ? null : 'move').call(arg.cleanup ? drag_move_off : drag_move);

   if (!arg.only_move)
      makeResizeElements(arg.getDrawG(), drag_resize);
}

/** @summary Tooltip handler class
  * @private */
class TooltipHandler extends ObjectPainter {

   // cannot use private members because of RFramePainter

   /** @desc only canvas info_layer can be used while other pads can overlay
     * @return layer where frame tooltips are shown */
   hints_layer() {
      return this.getCanvPainter()?.getLayerSvg('info_layer') ?? select(null);
   }

   /** @return true if tooltip is shown, use to prevent some other action */
   isTooltipShown() {
      if (!this.tooltip_enabled || !this.isTooltipAllowed())
         return false;
      const hintsg = this.hints_layer().selectChild('.objects_hints');
      return hintsg.empty() ? false : hintsg.property('hints_pad') === this.getPadPainter()?.getPadName();
   }

   /** @summary set tooltips enabled on/off */
   setTooltipEnabled(enabled) {
      if (enabled !== undefined)
         this.tooltip_enabled = enabled;
   }

   /** @summary central function which let show selected hints for the object */
   processFrameTooltipEvent(pnt, evnt) {
      if (pnt?.handler) {
         // special use of interactive handler in the frame painter
         const rect = this.getG()?.selectChild('.main_layer');
         if (!rect || rect.empty())
            pnt = null; // disable
         else if (pnt.touch && evnt) {
            const pos = get_touch_pointers(evnt, rect.node());
            pnt = (pos && pos.length === 1) ? { touch: true, x: pos[0][0], y: pos[0][1] } : null;
         } else if (evnt) {
            const pos = pointer(evnt, rect.node());
            pnt = { touch: false, x: pos[0], y: pos[1] };
         }
      }

      let nhints = 0, nexact = 0, maxlen = 0, lastcolor1 = 0, usecolor1 = false;
      const hmargin = 3, wmargin = 3, hstep = 1.2,
            frame_rect = this.getFrameRect(),
            pp = this.getPadPainter(),
            pad_width = pp?.getPadWidth(),
            scale = pp?.getPadScale() ?? 1,
            textheight = (pnt?.touch ? 15 : 11) * scale,
            font = new FontHandler(160, textheight),
            disable_tootlips = !this.isTooltipAllowed() || !this.tooltip_enabled;

      if (pnt) {
         pnt.disabled = disable_tootlips; // indicate that highlighting is not required
         pnt.painters = true; // get also painter
      }

      // collect tooltips from pad painter - it has list of all drawn objects
      const hints = pp?.processPadTooltipEvent(pnt) ?? [];

      if (pnt && frame_rect)
         pp.deliverWebCanvasEvent('move', frame_rect.x + pnt.x, frame_rect.y + pnt.y, hints ? hints[0]?.painter?.getSnapId() : '');

      for (let n = 0; n < hints.length; ++n) {
         const hint = hints[n];
         if (!hint)
            continue;

         if (hint.user_info !== undefined)
            hint.painter?.provideUserTooltip(hint.user_info);

         if (!hint.lines?.length) {
            hints[n] = null;
            continue;
         }

         // check if fully duplicated hint already exists
         for (let k = 0; k < n; ++k) {
            const hprev = hints[k];
            let diff = false;
            if (!hprev || (hprev.lines.length !== hint.lines.length))
               continue;
            for (let l = 0; l < hint.lines.length && !diff; ++l) {
               if (hprev.lines[l] !== hint.lines[l])
                  diff = true;
            }
            if (!diff) {
               hints[n] = null;
               break;
            }
         }
         if (!hints[n])
            continue;

         nhints++;

         if (hint.exact)
            nexact++;

         hint.lines.forEach(line => { maxlen = Math.max(maxlen, line.length); });

         hint.height = Math.round(hint.lines.length * textheight * hstep + 2 * hmargin - textheight * (hstep - 1));

         if ((hint.color1 !== undefined) && (hint.color1 !== 'none')) {
            if (lastcolor1 && (lastcolor1 !== hint.color1))
               usecolor1 = true;
            lastcolor1 = hint.color1;
         }
      }

      let path_name = null, same_path = hints.length > 1;
      for (let n = 0; n < hints.length; ++n) {
         const hint = hints[n], p = hint?.lines ? hint.lines[0]?.lastIndexOf('/') : -1;
         if (p > 0) {
            const path = hint.lines[0].slice(0, p + 1);
            if (path_name === null)
               path_name = path;
            else if (path_name !== path)
               same_path = false;
         } else
            same_path = false;
      }

      const layer = this.hints_layer(),
            show_only_best = nhints > 15,
            coordinates = pnt ? Math.round(pnt.x) + ',' + Math.round(pnt.y) : '';
      let hintsg = layer.selectChild('.objects_hints'), // group with all tooltips
          title = '', name = '', info = '',
          hint0 = null, best_dist2 = 1e10, best_hint = null;

      // try to select hint with exact match of the position when several hints available
      for (let k = 0; k < hints.length; ++k) {
         if (!hints[k])
            continue;
         if (!hint0)
            hint0 = hints[k];

         // select exact hint if this is the only one
         if (hints[k].exact && (nexact < 2) && (!hint0 || !hint0.exact)) {
            hint0 = hints[k];
            break;
         }

         if (!pnt || (hints[k].x === undefined) || (hints[k].y === undefined))
            continue;

         const dist2 = (pnt.x - hints[k].x) ** 2 + (pnt.y - hints[k].y) ** 2;
         if (dist2 < best_dist2) {
            best_dist2 = dist2;
            best_hint = hints[k];
         }
      }

      if ((!hint0 || !hint0.exact) && (best_dist2 < 400))
         hint0 = best_hint;

      if (hint0) {
         name = (hint0.lines && hint0.lines.length > 1) ? hint0.lines[0] : hint0.name;
         title = hint0.title || '';
         info = hint0.line;
         if (!info && hint0.lines)
            info = hint0.lines.slice(1).join(' ');
      }

      this.showObjectStatus(name, title, info, coordinates);

      // end of closing tooltips
      if (!pnt || disable_tootlips || !hints.length || (maxlen === 0) || (show_only_best && !best_hint)) {
         hintsg.remove();
         return;
      }

      // we need to set pointer-events=none for all elements while hints
      // placed in front of so-called interactive rect in frame, used to catch mouse events

      if (hintsg.empty()) {
         hintsg = layer.append('svg:g')
            .attr('class', 'objects_hints')
            .style('pointer-events', 'none');
      }

      let frame_shift = { x: 0, y: 0 }, trans = frame_rect.transform || '';
      if (!pp?.isCanvas()) {
         frame_shift = getAbsPosInCanvas(pp.getPadSvg(), frame_shift);
         trans = `translate(${frame_shift.x},${frame_shift.y}) ${trans}`;
      }

      // copy transform attributes from frame itself
      hintsg.attr('transform', trans)
            .property('last_point', pnt)
            .property('hints_pad', pp.getPadName());

      let viewmode = hintsg.property('viewmode') || '',
          actualw = 0, posx = pnt.x + frame_rect.hint_delta_x;

      if (show_only_best || (nhints === 1)) {
         viewmode = 'single';
         posx += 15;
      } else {
         // if there are many hints, place them left or right

         let bleft = 0.5, bright = 0.5;

         if (viewmode === 'left')
            bright = 0.7;
         else if (viewmode === 'right')
            bleft = 0.3;

         if (posx <= bleft * frame_rect.width) {
            viewmode = 'left';
            posx = 20;
         } else if (posx >= bright * frame_rect.width) {
            viewmode = 'right';
            posx = frame_rect.width - 60;
         } else
            posx = hintsg.property('startx');
      }

      if (viewmode !== hintsg.property('viewmode')) {
         hintsg.property('viewmode', viewmode);
         hintsg.selectAll('*').remove();
      }

      let curry = 10, // normal y coordinate
          gapy = 10,  // y coordinate, taking into account all gaps
          gapminx = -1111, gapmaxx = -1111;
      const minhinty = -frame_shift.y,
            cp = this.getCanvPainter(),
            maxhinty = cp.getPadHeight() - frame_rect.y - frame_shift.y;

      for (let n = 0; n < hints.length; ++n) {
         let hint = hints[n],
             group = hintsg.selectChild(`.painter_hint_${n}`);

         if (show_only_best && (hint !== best_hint))
            hint = null;

         if (hint === null) {
            group.remove();
            continue;
         }

         const was_empty = group.empty();

         if (was_empty) {
            group = hintsg.append('svg:svg')
               .attr('class', `painter_hint_${n}`)
               .attr('opacity', 0) // use attribute, not style to make animation with d3.transition()
               .style('overflow', 'hidden')
               .style('pointer-events', 'none');
         }

         if (viewmode === 'single')
            curry = pnt.touch ? (pnt.y - hint.height - 5) : Math.min(pnt.y + 15, maxhinty - hint.height - 3) + frame_rect.hint_delta_y;
         else {
            for (let n2 = 0; (n2 < hints.length) && (gapy < maxhinty); ++n2) {
               const hint2 = hints[n2];
               if (!hint2)
                  continue;
               if ((hint2.y >= gapy - 5) && (hint2.y <= gapy + hint2.height + 5)) {
                  gapy = hint2.y + 10;
                  n2 = -1;
               }
            }
            if ((gapminx === -1111) && (gapmaxx === -1111))
               gapminx = gapmaxx = hint.x;
            gapminx = Math.min(gapminx, hint.x);
            gapmaxx = Math.min(gapmaxx, hint.x);
         }

         group.attr('x', posx)
              .attr('y', curry)
              .property('curry', curry)
              .property('gapy', gapy);

         curry += hint.height + 5;
         gapy += hint.height + 5;

         if (!was_empty)
            group.selectAll('*').remove();

         group.attr('width', 60)
              .attr('height', hint.height);

         const r = group.append('rect')
            .attr('x', 0)
            .attr('y', 0)
            .attr('width', 60)
            .attr('height', hint.height)
            .style('fill', 'lightgrey')
            .style('pointer-events', 'none');

         if (nhints > 1) {
            const col = usecolor1 ? hint.color1 : hint.color2;
            if (col && (col !== 'none'))
               r.style('stroke', col);
         }
         r.attr('stroke-width', hint.exact ? 3 : 1);

         for (let l = 0; l < (hint.lines?.length ?? 0); l++) {
            let line = hint.lines[l];
            if (l === 0 && path_name && same_path)
               line = line.slice(path_name.length);
            if (line) {
               const txt = group.append('svg:text')
                  .attr('text-anchor', 'start')
                  .attr('x', wmargin)
                  .attr('y', hmargin + l * textheight * hstep)
                  .attr('dy', '.8em')
                  .style('fill', 'black')
                  .style('pointer-events', 'none')
                  .call(font.func)
                  .text(line),
                     box = getElementRect(txt, 'bbox');

               actualw = Math.max(actualw, box.width);
            }
         }

         function translateFn() {
            // We only use 'd', but list d,i,a as params just to show can have them as params.
            // Code only really uses d and t.
            return function(/* d, i, a */) {
               return function(t) {
                  return t < 0.8 ? '0' : (t - 0.8) * 5;
               };
            };
         }

         if (was_empty) {
            if (settings.TooltipAnimation > 0)
               group.transition().duration(settings.TooltipAnimation).attrTween('opacity', translateFn());
            else
               group.attr('opacity', 1);
         }
      }

      actualw += 2 * wmargin;

      const svgs = hintsg.selectAll('svg');

      if ((viewmode === 'right') && (posx + actualw > frame_rect.width - 20)) {
         posx = frame_rect.width - actualw - 20;
         svgs.attr('x', posx);
      }

      if ((viewmode === 'single') && (posx + actualw > pad_width - frame_rect.x) && (posx > actualw + 20)) {
         posx -= (actualw + 20);
         svgs.attr('x', posx);
      }

      // if gap not very big, apply gapy coordinate to open view on the histogram
      if ((viewmode !== 'single') && (gapy < maxhinty) && (gapy !== curry)) {
         if ((gapminx <= posx + actualw + 5) && (gapmaxx >= posx - 5))
            svgs.attr('y', function() { return select(this).property('gapy'); });
      } else if ((viewmode !== 'single') && (curry > maxhinty)) {
         const shift = Math.max((maxhinty - curry - 10), minhinty);
         if (shift < 0)
            svgs.attr('y', function() { return select(this).property('curry') + shift; });
      }

      if (actualw > 10)
         svgs.attr('width', actualw).select('rect').attr('width', actualw);

      hintsg.property('startx', posx);

      if (cp._highlight_connect && isFunc(cp.processHighlightConnect))
         cp.processHighlightConnect(hints);
   }

} // class TooltipHandler


/** @summary Frame interactivity class
  * @private */
class FrameInteractive extends TooltipHandler {

   // cannot use private members because of RFramePainter

   /** @summary Adding basic interactivity */
   addBasicInteractivity() {
      this.setTooltipEnabled(true);

      if (this.$can_drag) {
         addDragHandler(this, {
            obj: this, x: this.getFrameX(), y: this.getFrameY(), width: this.getFrameWidth(), height: this.getFrameHeight(),
            is_disabled: kind => { return (kind === 'move') && this.mode3d; },
            only_resize: true, minwidth: 20, minheight: 20, redraw: () => this.sizeChanged()
         });
      }

      const top_rect = this.getG().selectChild('path'),
            main_svg = this.getG().selectChild('.main_layer');

      top_rect.style('pointer-events', 'visibleFill')  // let process mouse events inside frame
              .style('cursor', 'default');             // show normal cursor

      main_svg.style('pointer-events', 'visibleFill')
              .style('cursor', 'default')
              .property('handlers_set', 0);

      const handlers_set = this.getPadPainter()?.isFastDrawing() ? 0 : 1;

      if (main_svg.property('handlers_set') !== handlers_set) {
         const close_handler = handlers_set ? evnt => this.processFrameTooltipEvent(null, evnt) : null,
               mouse_handler = handlers_set ? evnt => this.processFrameTooltipEvent({ handler: true, touch: false }, evnt) : null;

         main_svg.property('handlers_set', handlers_set)
                 .on('mouseenter', mouse_handler)
                 .on('mousemove', mouse_handler)
                 .on('mouseleave', close_handler);

         if (browser.touches) {
            const touch_handler = handlers_set ? evnt => this.processFrameTooltipEvent({ handler: true, touch: true }, evnt) : null;

            main_svg.on('touchstart', touch_handler)
                    .on('touchmove', touch_handler)
                    .on('touchend', close_handler)
                    .on('touchcancel', close_handler);
         }
      }

      main_svg.attr('x', 0)
              .attr('y', 0)
              .attr('width', this.getFrameWidth())
              .attr('height', this.getFrameHeight());

      const hintsg = this.hints_layer().selectChild('.objects_hints');
      // if tooltips were visible before, try to reconstruct them after short timeout
      if (!hintsg.empty() && this.isTooltipAllowed() && (hintsg.property('hints_pad') === this.getPadPainter()?.getPadName()))
         setTimeout(() => this.processFrameTooltipEvent(hintsg.property('last_point'), null), 10);
   }

   getFrameSvg() { return this.getPadPainter().getFrameSvg(); }

   /** @summary Add interactive handlers */
   async addFrameInteractivity(for_second_axes) {
      const pp = this.getPadPainter(),
            svg = this.getFrameSvg();
      if (pp?.isFastDrawing() || svg.empty())
         return this;

      if (for_second_axes) {
         // add extra handlers for second axes
         const svg_x2 = svg.selectAll('.x2axis_container'),
               svg_y2 = svg.selectAll('.y2axis_container');
         if (settings.ContextMenu) {
            svg_x2.on('contextmenu', evnt => this.showContextMenu('x2', evnt));
            svg_y2.on('contextmenu', evnt => this.showContextMenu('y2', evnt));
         }
         svg_x2.on('mousemove', evnt => this.showAxisStatus('x2', evnt));
         svg_y2.on('mousemove', evnt => this.showAxisStatus('y2', evnt));
         return this;
      }

      const svg_x = svg.selectAll('.xaxis_container'),
            svg_y = svg.selectAll('.yaxis_container');

      this.can_zoom_x = this.can_zoom_y = settings.Zooming;

      if (pp?.options) {
         if (pp.options.NoZoomX)
            this.can_zoom_x = false;
         if (pp.options.NoZoomY)
            this.can_zoom_y = false;
      }

      if (!svg.property('interactive_set')) {
         this.addKeysHandler();

         this.zoom_kind = 0; // 0 - none, 1 - XY, 2 - only X, 3 - only Y, (+100 for touches)
         this.zoom_rect = null;
         this.zoom_origin = null;  // original point where zooming started
         this.zoom_curr = null;    // current point for zooming
      }

      if (settings.Zooming) {
         if (settings.ZoomMouse) {
            svg.on('mousedown', evnt => this.startRectSel(evnt));
            svg.on('dblclick', evnt => this.mouseDoubleClick(evnt));
         }
         if (settings.ZoomWheel)
            svg.on('wheel', evnt => this.mouseWheel(evnt));
      }

      if (browser.touches && ((settings.Zooming && settings.ZoomTouch) || settings.ContextMenu))
         svg.on('touchstart', evnt => this.startTouchZoom(evnt));

      if (settings.ContextMenu) {
         if (browser.touches) {
            svg_x.on('touchstart', evnt => this.startSingleTouchHandling('x', evnt));
            svg_y.on('touchstart', evnt => this.startSingleTouchHandling('y', evnt));
         }
         svg.on('contextmenu', evnt => this.showContextMenu('', evnt));
         svg_x.on('contextmenu', evnt => this.showContextMenu('x', evnt));
         svg_y.on('contextmenu', evnt => this.showContextMenu('y', evnt));
      }

      svg_x.on('mousemove', evnt => this.showAxisStatus('x', evnt));
      svg_y.on('mousemove', evnt => this.showAxisStatus('y', evnt));

      svg.property('interactive_set', true);

      return this;
   }

   /** @summary Handle key press */
   processKeyPress(evnt) {
      // no custom keys handling when menu is present
      if (hasMenu())
         return true;

      const allowed = ['PageUp', 'PageDown', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'PrintScreen', 'Escape', '*'],
            main = this.selectDom(),
            pp = this.getPadPainter();
      let key = evnt.key;

      if (!settings.HandleKeys || main.empty() || (this.isEnabledKeys() === false) ||
          (getActivePad() !== pp) || (allowed.indexOf(key) < 0))
         return false;

      if (evnt.shiftKey)
         key = `Shift ${key}`;
      if (evnt.altKey)
         key = `Alt ${key}`;
      if (evnt.ctrlKey)
         key = `Ctrl ${key}`;

      const zoom = { name: 'x', dleft: 0, dright: 0 };

      switch (key) {
         case 'ArrowLeft':
            zoom.dleft = -1;
            zoom.dright = 1;
            break;
         case 'ArrowRight':
            zoom.dleft = 1;
            zoom.dright = -1;
            break;
         case 'Ctrl ArrowLeft':
            zoom.dleft = zoom.dright = -1;
            break;
         case 'Ctrl ArrowRight':
            zoom.dleft = zoom.dright = 1;
            break;
         case 'ArrowUp':
            zoom.name = 'y';
            zoom.dleft = 1;
            zoom.dright = -1;
            break;
         case 'ArrowDown':
            zoom.name = 'y';
            zoom.dleft = -1;
            zoom.dright = 1;
            break;
         case 'Ctrl ArrowUp':
            zoom.name = 'y';
            zoom.dleft = zoom.dright = 1;
            break;
         case 'Ctrl ArrowDown':
            zoom.name = 'y';
            zoom.dleft = zoom.dright = -1;
            break;
         case 'Escape':
            pp?.enlargePad(null, false, true);
            return true;
      }

      if (zoom.dleft || zoom.dright) {
         if (!settings.Zooming)
            return false;
         // in 3d mode with orbit control ignore simple arrows
         if (this.mode3d && key.indexOf('Ctrl'))
            return false;
         this.analyzeMouseWheelEvent(null, zoom, 0.5);
         if (zoom.changed)
            this.zoomSingle(zoom.name, zoom.min, zoom.max, true);
         evnt.stopPropagation();
         evnt.preventDefault();
      } else {
         const func = pp?.findPadButton(key);
         if (func) {
            pp.clickPadButton(func);
            evnt.stopPropagation();
            evnt.preventDefault();
         }
      }

      return true; // just process any key press
   }

   /** @summary Function called when frame is clicked and object selection can be performed
     * @desc such event can be used to select */
   processFrameClick(pnt, dblckick) {
      const pp = this.getPadPainter();
      if (!pp)
         return;

      pnt.painters = true; // provide painters reference in the hints
      pnt.disabled = true; // do not invoke graphics

      // collect tooltips from pad painter - it has list of all drawn objects
      const hints = pp.processPadTooltipEvent(pnt);
      let exact = null, res;
      for (let k = 0; (k < hints.length) && !exact; ++k) {
         if (hints[k] && hints[k].exact)
            exact = hints[k];
      }

      if (exact) {
         const handler = dblckick ? this.getDblclickHandler() : this.getClickHandler();
         if (isFunc(handler))
            res = handler(exact.user_info, pnt);
      }

      if (!dblckick) {
         pp.selectObjectPainter(exact ? exact.painter : this,
                                { x: pnt.x + this.getFrameX(), y: pnt.y + this.getFrameY() });
      }

      return res;
   }

   /** @summary Check mouse moving  */
   shiftMoveHanlder(evnt, pos0) {
      if (evnt.buttons === this._shifting_buttons) {
         const frame = this.getFrameSvg(),
               pos = pointer(evnt, frame.node()),
               main_svg = this.getG().selectChild('.main_layer'),
               dx = pos0[0] - pos[0],
               dy = (this.scales_ndim === 1) ? 0 : pos0[1] - pos[1],
               w = this.getFrameWidth(), h = this.getFrameHeight();

         this._shifting_dx = dx;
         this._shifting_dy = dy;

         main_svg.attr('viewBox', `${dx} ${dy} ${w} ${h}`);

         evnt.preventDefault();
         evnt.stopPropagation();
      }
   }

   /** @summary mouse up handler for shifting */
   shiftUpHanlder(evnt) {
      evnt.preventDefault();

      select(window).on('mousemove.shiftHandler', null)
                       .on('mouseup.shiftHandler', null);

      if ((this._shifting_dx !== undefined) && (this._shifting_dy !== undefined))
         this.performScalesShift();
   }

   /** @summary Shift scales on defined positions */
   performScalesShift() {
      const w = this.getFrameWidth(), h = this.getFrameHeight(),
            main_svg = this.getG().selectChild('.main_layer'),
            gr = this.getGrFuncs(),
            xmin = gr.revertAxis('x', this._shifting_dx),
            xmax = gr.revertAxis('x', this._shifting_dx + w),
            ymin = gr.revertAxis('y', this._shifting_dy + h),
            ymax = gr.revertAxis('y', this._shifting_dy);

      main_svg.attr('viewBox', `0 0 ${w} ${h}`);

      this._shifting_dx = this._shifting_dy = undefined;

      setPainterTooltipEnabled(this, true);

      if (this.scales_ndim === 1)
         this.zoomSingle('x', xmin, xmax);
      else
         this.zoom(xmin, xmax, ymin, ymax);
   }

   /** @summary Start mouse rect zooming */
   startRectSel(evnt) {
      // ignore when touch selection is activated
      if (this.zoom_kind > 100)
         return;

      const frame = this.getFrameSvg(),
            pos = pointer(evnt, frame.node());

      if ((evnt.buttons === 3) || (evnt.button === 1)) {
         this.clearInteractiveElements();
         this._shifting_buttons = evnt.buttons;

         if (!evnt.$emul) {
            select(window).on('mousemove.shiftHandler', evnt2 => this.shiftMoveHanlder(evnt2, pos))
                             .on('mouseup.shiftHandler', evnt2 => this.shiftUpHanlder(evnt2), true);
         }

         setPainterTooltipEnabled(this, false);
         evnt.preventDefault();
         evnt.stopPropagation();
         return;
      }

      // ignore all events from non-left button
      if (evnt.button)
         return;

      evnt.preventDefault();

      this.clearInteractiveElements();

      const w = this.getFrameWidth(), h = this.getFrameHeight();

      this.zoom_lastpos = pos;
      this.zoom_curr = [Math.max(0, Math.min(w, pos[0])), Math.max(0, Math.min(h, pos[1]))];

      this.zoom_origin = [0, 0];
      this.zoom_second = false;

      if ((pos[0] < 0) || (pos[0] > w)) {
         this.zoom_second = (pos[0] > w) && this.y2_handle;
         this.zoom_kind = 3; // only y
         this.zoom_origin[1] = this.zoom_curr[1];
         this.zoom_curr[0] = w;
         this.zoom_curr[1] += 1;
      } else if ((pos[1] < 0) || (pos[1] > h)) {
         this.zoom_second = (pos[1] < 0) && this.x2_handle;
         this.zoom_kind = 2; // only x
         this.zoom_origin[0] = this.zoom_curr[0];
         this.zoom_curr[0] += 1;
         this.zoom_curr[1] = h;
      } else {
         this.zoom_kind = 1; // x and y
         this.zoom_origin[0] = this.zoom_curr[0];
         this.zoom_origin[1] = this.zoom_curr[1];
      }

      if (!evnt.$emul) {
         select(window).on('mousemove.zoomRect', evnt2 => this.moveRectSel(evnt2))
                          .on('mouseup.zoomRect', evnt2 => this.endRectSel(evnt2), true);
      }

      this.zoom_rect = null;

      // disable tooltips in frame painter
      setPainterTooltipEnabled(this, false);

      evnt.stopPropagation();

      if (this.zoom_kind !== 1)
         return postponePromise(() => this.startLabelsMove(), 500);
   }

   /** @summary Starts labels move */
   startLabelsMove() {
      if (this.zoom_rect)
         return;

      const handle = (this.zoom_kind === 2) ? this.x_handle : this.y_handle;

      if (!isFunc(handle?.processLabelsMove) || !this.zoom_lastpos)
         return;

      if (handle.processLabelsMove('start', this.zoom_lastpos))
         this.zoom_labels = handle;
   }

   /** @summary Process mouse rect zooming */
   moveRectSel(evnt) {
      if ((this.zoom_kind === 0) || (this.zoom_kind > 100))
         return;

      evnt.preventDefault();
      const m = pointer(evnt, this.getFrameSvg().node());

      if (this.zoom_labels)
         return this.zoom_labels.processLabelsMove('move', m);

      this.zoom_lastpos[0] = m[0];
      this.zoom_lastpos[1] = m[1];

      m[0] = Math.max(0, Math.min(this.getFrameWidth(), m[0]));
      m[1] = Math.max(0, Math.min(this.getFrameHeight(), m[1]));

      switch (this.zoom_kind) {
         case 1:
            this.zoom_curr[0] = m[0];
            this.zoom_curr[1] = m[1];
            break;
         case 2:
            this.zoom_curr[0] = m[0];
            break;
         case 3:
            this.zoom_curr[1] = m[1];
            break;
      }

      const x = Math.min(this.zoom_origin[0], this.zoom_curr[0]),
            y = Math.min(this.zoom_origin[1], this.zoom_curr[1]),
            w = Math.abs(this.zoom_curr[0] - this.zoom_origin[0]),
            h = Math.abs(this.zoom_curr[1] - this.zoom_origin[1]);

      if (!this.zoom_rect) {
         // ignore small changes, can be switching to labels move
         if ((this.zoom_kind !== 1) && ((w < 2) || (h < 2)))
            return;

         this.zoom_rect = this.getFrameSvg()
                              .append('rect')
                              .style('pointer-events', 'none')
                              .call(addHighlightStyle, true);
      }

      this.zoom_rect.attr('x', x).attr('y', y).attr('width', w).attr('height', h);
   }

   /** @summary Finish mouse rect zooming */
   endRectSel(evnt) {
      if ((this.zoom_kind === 0) || (this.zoom_kind > 100))
         return;

      evnt.preventDefault();

      if (!evnt.$emul) {
         select(window).on('mousemove.zoomRect', null)
                          .on('mouseup.zoomRect', null);
      }

      const m = pointer(evnt, this.getFrameSvg().node());
      let kind = this.zoom_kind, pr;

      if (this.zoom_labels)
         this.zoom_labels.processLabelsMove('stop', m);
      else {
         const changed = [this.can_zoom_x, this.can_zoom_y];
         m[0] = Math.max(0, Math.min(this.getFrameWidth(), m[0]));
         m[1] = Math.max(0, Math.min(this.getFrameHeight(), m[1]));

         switch (this.zoom_kind) {
            case 1:
               this.zoom_curr[0] = m[0];
               this.zoom_curr[1] = m[1];
               break;
            case 2: // only X
               this.zoom_curr[0] = m[0];
               changed[1] = false;
               break;
            case 3: // only Y
               this.zoom_curr[1] = m[1];
               changed[0] = false;
               break;
         }

         let xmin, xmax, ymin, ymax, isany = false,
             namex = 'x', namey = 'y';

         if (changed[0] && (Math.abs(this.zoom_curr[0] - this.zoom_origin[0]) > 5)) {
            if (this.zoom_second && (this.zoom_kind === 2))
               namex = 'x2';
            const v1 = this.revertAxis(namex, this.zoom_origin[0]),
                  v2 = this.revertAxis(namex, this.zoom_curr[0]);
            xmin = Math.min(v1, v2);
            xmax = Math.max(v1, v2);
            isany = true;
         }

         if (changed[1] && (Math.abs(this.zoom_curr[1] - this.zoom_origin[1]) > 5)) {
            if (this.zoom_second && (this.zoom_kind === 3))
               namey = 'y2';

            const v1 = this.revertAxis(namey, this.zoom_origin[1]),
                  v2 = this.revertAxis(namey, this.zoom_curr[1]);
            ymin = Math.min(v1, v2);
            ymax = Math.max(v1, v2);
            isany = true;
         }

         if (this.swap_xy() && !this.zoom_second)
            [xmin, xmax, ymin, ymax] = [ymin, ymax, xmin, xmax];

         if (namex === 'x2') {
            pr = this.zoomSingle(namex, xmin, xmax, true);
            kind = 0;
         } else if (namey === 'y2') {
            pr = this.zoomSingle(namey, ymin, ymax, true);
            kind = 0;
         } else if (isany) {
            pr = this.zoom(xmin, xmax, ymin, ymax, undefined, undefined, true);
            kind = 0;
         }
      }

      const pnt = (kind === 1) ? { x: this.zoom_origin[0], y: this.zoom_origin[1] } : null;

      this.clearInteractiveElements();

      // if no zooming was done, select active object instead
      switch (kind) {
         case 1:
            this.processFrameClick(pnt);
            break;
         case 2:
            this.getPadPainter()?.selectObjectPainter(this.x_handle);
            break;
         case 3:
            this.getPadPainter()?.selectObjectPainter(this.y_handle);
            break;
      }

      // return promise - if any
      return pr;
   }

   /** @summary Handle mouse double click on frame */
   mouseDoubleClick(evnt) {
      evnt.preventDefault();
      const m = pointer(evnt, this.getFrameSvg().node()),
            fw = this.getFrameWidth(), fh = this.getFrameHeight();
      this.clearInteractiveElements();

      const valid_x = (m[0] >= 0) && (m[0] <= fw),
            valid_y = (m[1] >= 0) && (m[1] <= fh);

      if (valid_x && valid_y && this.getDblclickHandler()) {
         if (this.processFrameClick({ x: m[0], y: m[1] }, true))
            return;
      }

      let kind = (this.can_zoom_x ? 'x' : '') + (this.can_zoom_y ? 'y' : '') + 'z';
      if (!valid_x) {
         if (!this.can_zoom_y)
            return;
         kind = this.swap_xy() ? 'x' : 'y';
         if ((m[0] > fw) && this[kind + '2_handle'])
            kind += '2'; // let unzoom second axis
      } else if (!valid_y) {
         if (!this.can_zoom_x)
            return;
         kind = this.swap_xy() ? 'y' : 'x';
         if ((m[1] < 0) && this[kind + '2_handle'])
            kind += '2'; // let unzoom second axis
      }
      return this.unzoom(kind).then(changed => {
         if (changed)
            return;
         const pp = this.getPadPainter(), rect = this.getFrameRect();
         return pp?.selectObjectPainter(pp, { x: m[0] + rect.x, y: m[1] + rect.y, dbl: true });
      });
   }

   /** @summary Start touch zoom */
   startTouchZoom(evnt) {
      evnt.preventDefault();
      evnt.stopPropagation();

      // in case when zooming was started, block any other kind of events
      // also prevent zooming together with active dragging
      if (this.zoom_kind || drag_kind)
         return;

      const arr = get_touch_pointers(evnt, this.getFrameSvg().node());

      // normally double-touch will be handled
      // touch with single click used for context menu
      if (arr.length === 1) {
         // this is touch with single element

         const now = new Date().getTime();
         let tmdiff = 1e10, dx = 100, dy = 100;

         if (this.last_touch_time && this.last_touch_pos) {
            tmdiff = now - this.last_touch_time;
            dx = Math.abs(arr[0][0] - this.last_touch_pos[0]);
            dy = Math.abs(arr[0][1] - this.last_touch_pos[1]);
         }

         this.last_touch_time = now;
         this.last_touch_pos = arr[0];

         if ((tmdiff < 500) && (dx < 20) && (dy < 20)) {
            this.clearInteractiveElements();
            this.unzoom('xyz');

            delete this.last_touch_time;
         } else if (settings.ContextMenu)
            this.startSingleTouchHandling('', evnt);
      }

      if ((arr.length !== 2) || !settings.Zooming || !settings.ZoomTouch)
         return;

      this.clearInteractiveElements();

      // clear single touch handler
      this.endSingleTouchHandling(null);

      const pnt1 = arr[0], pnt2 = arr[1], w = this.getFrameWidth(), h = this.getFrameHeight();

      this.zoom_curr = [Math.min(pnt1[0], pnt2[0]), Math.min(pnt1[1], pnt2[1])];
      this.zoom_origin = [Math.max(pnt1[0], pnt2[0]), Math.max(pnt1[1], pnt2[1])];
      this.zoom_second = false;

      if ((this.zoom_curr[0] < 0) || (this.zoom_curr[0] > w)) {
         this.zoom_second = (this.zoom_curr[0] > w) && this.y2_handle;
         this.zoom_kind = 103; // only y
         this.zoom_curr[0] = 0;
         this.zoom_origin[0] = w;
      } else if ((this.zoom_origin[1] > h) || (this.zoom_origin[1] < 0)) {
         this.zoom_second = (this.zoom_origin[1] < 0) && this.x2_handle;
         this.zoom_kind = 102; // only x
         this.zoom_curr[1] = 0;
         this.zoom_origin[1] = h;
      } else
         this.zoom_kind = 101; // x and y

      drag_kind = 'zoom'; // block other possible dragging

      setPainterTooltipEnabled(this, false);

      this.zoom_rect = this.getFrameSvg().append('rect')
            .attr('id', 'zoomRect')
            .attr('x', this.zoom_curr[0])
            .attr('y', this.zoom_curr[1])
            .attr('width', this.zoom_origin[0] - this.zoom_curr[0])
            .attr('height', this.zoom_origin[1] - this.zoom_curr[1])
            .call(addHighlightStyle, true);

      if (!evnt.$emul) {
         select(window).on('touchmove.zoomRect', evnt2 => this.moveTouchZoom(evnt2))
                          .on('touchcancel.zoomRect', evnt2 => this.endTouchZoom(evnt2))
                          .on('touchend.zoomRect', evnt2 => this.endTouchZoom(evnt2));
      }
   }

   /** @summary Move touch zooming */
   moveTouchZoom(evnt) {
      if (this.zoom_kind < 100)
         return;

      evnt.preventDefault();

      const arr = get_touch_pointers(evnt, this.getFrameSvg().node());

      if (arr.length !== 2)
         return this.clearInteractiveElements();

      const pnt1 = arr[0], pnt2 = arr[1];

      if (this.zoom_kind !== 103) {
         this.zoom_curr[0] = Math.min(pnt1[0], pnt2[0]);
         this.zoom_origin[0] = Math.max(pnt1[0], pnt2[0]);
      }
      if (this.zoom_kind !== 102) {
         this.zoom_curr[1] = Math.min(pnt1[1], pnt2[1]);
         this.zoom_origin[1] = Math.max(pnt1[1], pnt2[1]);
      }

      this.zoom_rect.attr('x', this.zoom_curr[0])
                     .attr('y', this.zoom_curr[1])
                     .attr('width', this.zoom_origin[0] - this.zoom_curr[0])
                     .attr('height', this.zoom_origin[1] - this.zoom_curr[1]);

      if ((this.zoom_origin[0] - this.zoom_curr[0] > 10) || (this.zoom_origin[1] - this.zoom_curr[1] > 10))
         setPainterTooltipEnabled(this, false);

      evnt.stopPropagation();
   }

   /** @summary End touch zooming handler */
   endTouchZoom(evnt) {
      if (this.zoom_kind < 100)
         return;

      drag_kind = ''; // reset global flag

      evnt.preventDefault();
      if (!evnt.$emul) {
         select(window).on('touchmove.zoomRect', null)
                          .on('touchend.zoomRect', null)
                          .on('touchcancel.zoomRect', null);
      }

      let xmin, xmax, ymin, ymax, isany = false, namex = 'x', namey = 'y';
      const xid = this.swap_xy() ? 1 : 0, yid = 1 - xid, changed = [true, true];

      if (this.zoom_kind === 102)
         changed[1] = false;
      if (this.zoom_kind === 103)
         changed[0] = false;

      if (changed[xid] && (Math.abs(this.zoom_curr[xid] - this.zoom_origin[xid]) > 10)) {
         if (this.zoom_second && (this.zoom_kind === 102))
            namex = 'x2';
         xmin = Math.min(this.revertAxis(namex, this.zoom_origin[xid]), this.revertAxis(namex, this.zoom_curr[xid]));
         xmax = Math.max(this.revertAxis(namex, this.zoom_origin[xid]), this.revertAxis(namex, this.zoom_curr[xid]));
         isany = true;
      }

      if (changed[yid] && (Math.abs(this.zoom_curr[yid] - this.zoom_origin[yid]) > 10)) {
         if (this.zoom_second && (this.zoom_kind === 103))
            namey = 'y2';
         ymin = Math.min(this.revertAxis(namey, this.zoom_origin[yid]), this.revertAxis(namey, this.zoom_curr[yid]));
         ymax = Math.max(this.revertAxis(namey, this.zoom_origin[yid]), this.revertAxis(namey, this.zoom_curr[yid]));
         isany = true;
      }

      this.clearInteractiveElements();
      delete this.last_touch_time;

      if (namex === 'x2')
         this.zoomSingle(namex, xmin, xmax, true);
      else if (namey === 'y2')
         this.zoomSingle(namey, ymin, ymax, true);
      else if (isany)
         this.zoom(xmin, xmax, ymin, ymax, undefined, undefined, true);

      evnt.stopPropagation();
   }

   /** @summary Analyze zooming with mouse wheel */
   analyzeMouseWheelEvent(event, item, dmin, test_ignore, second_side) {
      // if there is second handle, use it
      const handle2 = second_side ? this[item.name + '2_handle'] : null;
      if (handle2) {
         item.second = Object.assign({}, item);
         return handle2.analyzeWheelEvent(event, dmin, item.second, test_ignore);
      }
      const handle = this[item.name + '_handle'];
      return handle?.analyzeWheelEvent(event, dmin, item, test_ignore);
   }

   /** @summary return true if default Y zooming should be enabled
      * @desc it is typically for 2-Dim histograms or
      * when histogram not draw, defined by other painters */
   isAllowedDefaultYZooming() {
      if (this.self_drawaxes)
         return true;

      let res;
      this.forEachPainter(objp => {
         res = res ?? objp._wheel_zoomy;
      }, 'objects');
      return res;
   }

   /** @summary Handles mouse wheel event */
   mouseWheel(evnt) {
      evnt.stopPropagation();
      evnt.preventDefault();
      this.clearInteractiveElements();

      const itemx = { name: 'x', reverse: this.reverse_x() },
            itemy = { name: 'y', reverse: this.reverse_y(), ignore: !this.isAllowedDefaultYZooming() },
            cur = pointer(evnt, this.getFrameSvg().node()),
            w = this.getFrameWidth(), h = this.getFrameHeight();

      if (this.can_zoom_x)
         this.analyzeMouseWheelEvent(evnt, this.swap_xy() ? itemy : itemx, cur[0] / w, (cur[1] >= 0) && (cur[1] <= h), cur[1] < 0);

      if (this.can_zoom_y)
         this.analyzeMouseWheelEvent(evnt, this.swap_xy() ? itemx : itemy, 1 - cur[1] / h, (cur[0] >= 0) && (cur[0] <= w), cur[0] > w);

      let pr = this.zoom(itemx.min, itemx.max, itemy.min, itemy.max, undefined, undefined, itemx.changed || itemy.changed);

      if (itemx.second)
         pr = pr.then(() => this.zoomSingle('x2', itemx.second.min, itemx.second.max, itemx.second.changed));

      if (itemy.second)
         pr = pr.then(() => this.zoomSingle('y2', itemy.second.min, itemy.second.max, itemy.second.changed));

      return pr;
   }

   /** @summary Show frame context menu */
   showContextMenu(kind, evnt, obj) {
      // disable context menu left/right buttons clicked
      if (evnt?.buttons === 3)
         return evnt.preventDefault();

      // ignore context menu when touches zooming is ongoing or
      if (this.zoom_kind && (this.zoom_kind > 100))
         return;

      let pnt, menu_painter = this, exec_painter = null,
          frame_corner = false, fp = null; // object used to show context menu
      const svg_node = this.getFrameSvg().node();

      if (isFunc(evnt?.stopPropagation)) {
         evnt.preventDefault();
         evnt.stopPropagation(); // disable main context menu
         const ms = pointer(evnt, svg_node),
               tch = get_touch_pointers(evnt, svg_node);
         if (tch.length === 1)
            pnt = { x: tch[0][0], y: tch[0][1], touch: true };
         else if (ms.length === 2)
            pnt = { x: ms[0], y: ms[1], touch: false };
      } else if ((evnt?.x !== undefined) && (evnt?.y !== undefined) && (evnt?.clientX === undefined)) {
         pnt = evnt;
         const rect = svg_node.getBoundingClientRect();
         evnt = { clientX: rect.left + pnt.x, clientY: rect.top + pnt.y };
      }

      if ((kind === 'painter') && obj) {
         menu_painter = obj;
         kind = '';
      } else if (kind === 'main') {
         menu_painter = this.getMainPainter(true);
         kind = '';
      } else if (!kind) {
         const pp = this.getPadPainter();
         let sel = null;

         fp = this;
         if (pnt && pp) {
            pnt.painters = true; // assign painter for every tooltip
            const hints = pp.processPadTooltipEvent(pnt);
            let bestdist = 1000;
            for (let n = 0; n < hints.length; ++n) {
               if (hints[n]?.menu) {
                  const dist = hints[n].menu_dist ?? 7;
                  if (dist < bestdist) {
                     sel = hints[n].painter;
                     bestdist = dist;
                  }
               }
            }
         }

         if (sel)
            menu_painter = sel;
         else
            kind = 'frame';

         if (pnt)
            frame_corner = (pnt.x > 0) && (pnt.x < 20) && (pnt.y > 0) && (pnt.y < 20);

         fp.setLastEventPos(pnt);
      } else if ((kind === 'x') || (kind === 'y') || (kind === 'z') || (kind === 'pal')) {
         exec_painter = this.getMainPainter(true); // histogram painter delivers items for axis menu

         if (this.v7_frame && isFunc(exec_painter?.v7EvalAttr))
            exec_painter = null;
      }

      if (!exec_painter)
         exec_painter = menu_painter;

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

      this.clearInteractiveElements();

      return createMenu(evnt, menu_painter).then(menu => {
         let domenu = menu.painter.fillContextMenu(menu, kind, obj);

         // fill frame menu by default - or append frame elements when activated in the frame corner
         if (fp && (!domenu || (frame_corner && (kind !== 'frame'))))
            domenu = fp.fillContextMenu(menu);

         if (domenu) {
            return exec_painter.fillObjectExecMenu(menu, kind).then(menu2 => {
               // suppress any running zooming
               setPainterTooltipEnabled(menu2.painter, false);
               return menu2.show().then(() => setPainterTooltipEnabled(menu2.painter, true));
            });
         }
      });
   }

   /** @summary Activate touch handling on frame
    * @private */
   startSingleTouchHandling(kind, evnt) {
      const arr = get_touch_pointers(evnt, this.getFrameSvg().node());
      if (arr.length !== 1)
         return;

      evnt.preventDefault();
      evnt.stopPropagation();
      closeMenu();

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

      this._shifting_dx = 0;
      this._shifting_dy = 0;

      setPainterTooltipEnabled(this, false);

      select(window).on('touchmove.singleTouch', kind ? null : evnt2 => this.moveTouchHandling(evnt2, kind, arr[0]))
                       .on('touchcancel.singleTouch', evnt2 => this.endSingleTouchHandling(evnt2, kind, arr[0], tm))
                       .on('touchend.singleTouch', evnt2 => this.endSingleTouchHandling(evnt2, kind, arr[0], tm));
   }

   /** @summary Moving of touch pointer
    * @private */
   moveTouchHandling(evnt, kind, pos0) {
      const frame = this.getFrameSvg(),
            main_svg = this.getG().selectChild('.main_layer');
      let pos;

      try {
         pos = get_touch_pointers(evnt, frame.node())[0];
      } catch {
         pos = [0, 0];
         if (evnt?.changedTouches)
            pos = [evnt.changedTouches[0].clientX, evnt.changedTouches[0].clientY];
      }

      const dx = pos0[0] - pos[0],
            dy = (this.scales_ndim === 1) ? 0 : pos0[1] - pos[1],
            w = this.getFrameWidth(), h = this.getFrameHeight();

      this._shifting_dx = dx;
      this._shifting_dy = dy;

      main_svg.attr('viewBox', `${dx} ${dy} ${w} ${h}`);
   }

   /** @summary Process end-touch event, which can cause content menu to appear
    * @private */
   endSingleTouchHandling(evnt, kind, pos, tm) {
      evnt?.preventDefault();
      evnt?.stopPropagation();

      setPainterTooltipEnabled(this, true);

      select(window).on('touchmove.singleTouch', null)
                       .on('touchcancel.singleTouch', null)
                       .on('touchend.singleTouch', null);

      if (evnt === null)
         return;

      if (Math.abs(this._shifting_dx) > 2 || Math.abs(this._shifting_dy) > 2)
         this.performScalesShift();
      else if (new Date().getTime() - tm > 700)
         this.showContextMenu(kind, { x: pos[0], y: pos[1] });
   }

   /** @summary Clear frame interactive elements */
   clearInteractiveElements() {
      closeMenu();
      this.zoom_kind = 0;
      this.zoom_rect?.remove();
      delete this.zoom_rect;
      delete this.zoom_curr;
      delete this.zoom_origin;
      delete this.zoom_lastpos;
      delete this.zoom_labels;

      // enable tooltip in frame painter
      setPainterTooltipEnabled(this, true);
   }

   /** @summary Assign interactive methods to frame painter */
   static assign(fpainter) {
      const src = FrameInteractive.prototype,
            src2 = TooltipHandler.prototype;
      Object.getOwnPropertyNames(src).forEach(name => {
         if (name !== 'constructor')
            fpainter[name] = src[name];
      });
      Object.getOwnPropertyNames(src2).forEach(name => {
         if (name !== 'constructor')
            fpainter[name] = src2[name];
      });
   }

} // class FrameInteractive

/**
 * @summary Painter class for TFrame, main handler for interactivity
 * @private
 */

class TFramePainter extends FrameInteractive {

   #frame_x; // frame X coordinate
   #frame_y; // frame Y coordinate
   #frame_width; // frame width
   #frame_height; // frame height
   #frame_trans; // transform of frame element
   #swap_xy;  // swap X/Y axis on the frame
   #reverse_x; // reverse X axis
   #reverse_y; // reverse Y axis
   #reverse_x2; // reverse X2 axis
   #reverse_y2; // reverse Y2 axis
   #border_mode; // frame border mode
   #border_size; // frame border size
   #axes_drawn; // when axes are drawn
   #axes2_drawn; // when axes are drawn
   #shrink_frame_left; // shrink frame on left side
   #projection; // id of projection function
   #click_handler; // handle for click events
   #dblclick_handler; // handle for double click events
   #keys_handler; // assigned handler for keyboard events
   #enabled_keys;  // when keyboard press handling enabled
   #last_event_pos; // position of last event

   /** @summary constructor
     * @param {object|string} dom - DOM element for drawing or element id
     * @param {object} frame - TFrame object */
   constructor(dom, frame) {
      super(dom, frame?.$dummy ? null : frame);
      this.zoom_kind = 0;
      this.mode3d = false;
      this.#shrink_frame_left = 0.0;
      this.xmin = this.xmax = 0; // no scale specified, wait for objects drawing
      this.ymin = this.ymax = 0; // no scale specified, wait for objects drawing
      this.ranges_set = false;
      this.#axes_drawn = false;
      this.#axes2_drawn = false;
      this.#border_mode = gStyle.fFrameBorderMode;
      this.#border_size = gStyle.fFrameBorderSize;
      this.#projection = 0; // different projections
   }

   /** @summary Returns frame painter - object itself */
   getFramePainter() { return this; }

   /** @summary Returns true if it is ROOT6 frame
     * @private */
   is_root6() { return true; }

   /** @summary Returns frame or sub-objects, used in GED editor */
   getObject(place) {
      if (place === 'xaxis')
         return this.xaxis;
      if (place === 'yaxis')
         return this.yaxis;
      return super.getObject();
   }

   /** @summary Set active flag for frame - can block some events
     * @private */
   setFrameActive(on) {
      this.#enabled_keys = on && settings.HandleKeys;
      // used only in 3D mode where control is used
      if (this.control)
         this.control.enableKeys = this.#enabled_keys;
   }

   /** @summary Returns true if keys handling enabled
     * @private */
   isEnabledKeys() { return this.#enabled_keys; }

   /** @summary Returns true if X/Y axis swapped */
   swap_xy() { return this.#swap_xy; }

   /** @summary Is reverse x */
   reverse_x() { return this.#reverse_x; }

   /** @summary Is reverse x */
   reverse_y() { return this.#reverse_y; }

   /** @summary Shrink frame size
     * @private */
   shrinkFrame(shrink_left, shrink_right) {
      this.fX1NDC += shrink_left;
      this.fX2NDC -= shrink_right;
   }

   /** @summary Set position of last context menu event
    * @private */
   setLastEventPos(pnt) { this.#last_event_pos = pnt; }

   /** @summary Return position of last event
     * @private */
   getLastEventPos() { return this.#last_event_pos; }

   /** @summary Returns coordinates transformation func */
   getProjectionFunc() { return getEarthProjectionFunc(this.#projection); }

   /** @summary Recalculate frame ranges using specified projection functions */
   recalculateRange(Proj, change_x, change_y) {
      this.#projection = Proj || 0;

      if ((this.#projection === 2) && ((this.scale_ymin <= -90) || (this.scale_ymax >= 90))) {
         console.warn(`Mercator Projection: Latitude out of range ${this.scale_ymin} ${this.scale_ymax}`);
         this.#projection = 0;
      }

      const func = this.getProjectionFunc();
      if (!func)
         return;

      const pnts = [func(this.scale_xmin, this.scale_ymin),
                    func(this.scale_xmin, this.scale_ymax),
                    func(this.scale_xmax, this.scale_ymax),
                    func(this.scale_xmax, this.scale_ymin)];
      if (this.scale_xmin < 0 && this.scale_xmax > 0) {
         pnts.push(func(0, this.scale_ymin));
         pnts.push(func(0, this.scale_ymax));
      }
      if (this.scale_ymin < 0 && this.scale_ymax > 0) {
         pnts.push(func(this.scale_xmin, 0));
         pnts.push(func(this.scale_xmax, 0));
      }

      this.original_xmin = this.scale_xmin;
      this.original_xmax = this.scale_xmax;
      this.original_ymin = this.scale_ymin;
      this.original_ymax = this.scale_ymax;

      if (change_x)
         this.scale_xmin = this.scale_xmax = pnts[0].x;
      if (change_y)
         this.scale_ymin = this.scale_ymax = pnts[0].y;

      for (let n = 1; n < pnts.length; ++n) {
         if (change_x) {
            this.scale_xmin = Math.min(this.scale_xmin, pnts[n].x);
            this.scale_xmax = Math.max(this.scale_xmax, pnts[n].x);
         }
         if (change_y) {
            this.scale_ymin = Math.min(this.scale_ymin, pnts[n].y);
            this.scale_ymax = Math.max(this.scale_ymax, pnts[n].y);
         }
      }
   }

   /** @summary Configure frame axes ranges */
   setAxesRanges(xaxis, xmin, xmax, yaxis, ymin, ymax, zaxis, zmin, zmax, hpainter) {
      this.ranges_set = true;

      this.xaxis = xaxis;
      this.xmin = xmin;
      this.xmax = xmax;

      this.yaxis = yaxis;
      this.ymin = ymin;
      this.ymax = ymax;

      this.zaxis = zaxis;
      this.zmin = zmin;
      this.zmax = zmax;

      if (hpainter?.check_pad_range) {
         delete hpainter.check_pad_range;
         const ndim = hpainter.getDimension();
         this.applyAxisZoom('x');
         if (ndim > 1)
            this.applyAxisZoom('y');
         if (ndim > 2)
            this.applyAxisZoom('z');
      }

      if (hpainter && !hpainter._checked_zooming) {
         hpainter._checked_zooming = true;

         if (hpainter.options.minimum !== kNoZoom) {
            this.zoom_zmin = hpainter.options.minimum;
            this.zoom_zmax = this.zmax;
         }
         if (hpainter.options.maximum !== kNoZoom) {
            this.zoom_zmax = hpainter.options.maximum;
            this.zoom_zmin ??= this.zmin;
         }
      }
   }

   /** @summary Configure secondary frame axes ranges */
   setAxes2Ranges(second_x, xaxis, xmin, xmax, second_y, yaxis, ymin, ymax) {
      if (second_x) {
         this.x2axis = xaxis;
         this.x2min = xmin;
         this.x2max = xmax;
      }
      if (second_y) {
         this.y2axis = yaxis;
         this.y2min = ymin;
         this.y2max = ymax;
      }
   }

   /** @summary Returns associated axis object */
   getAxis(name) {
      switch (name) {
         case 'x': return this.xaxis;
         case 'y': return this.yaxis;
         case 'z': return this.zaxis;
         case 'x2': return this.x2axis;
         case 'y2': return this.y2axis;
      }
      return null;
   }

   /** @summary Apply axis zooming from pad user range
     * @private */
   applyPadUserRange(pad, name) {
      if (!pad)
         return;

      // seems to be, not always user range calculated
      let umin = pad[`fU${name}min`],
          umax = pad[`fU${name}max`],
          eps = 1e-7;

      if (name === 'x') {
         if ((Math.abs(pad.fX1) > eps) || (Math.abs(pad.fX2 - 1) > eps)) {
            const dx = pad.fX2 - pad.fX1;
            umin = pad.fX1 + dx * pad.fLeftMargin;
            umax = pad.fX2 - dx * pad.fRightMargin;
         }
      } else if ((Math.abs(pad.fY1) > eps) || (Math.abs(pad.fY2 - 1) > eps)) {
         const dy = pad.fY2 - pad.fY1;
         umin = pad.fY1 + dy * pad.fBottomMargin;
         umax = pad.fY2 - dy * pad.fTopMargin;
      }

      if ((umin >= umax) || (Math.abs(umin) < eps && Math.abs(umax - 1) < eps))
         return;

      if (pad[`fLog${name}`] > 0) {
         umin = Math.exp(umin * Math.log(10));
         umax = Math.exp(umax * Math.log(10));
      }

      const aname = !this.#swap_xy ? name : (name === 'x' ? 'y' : 'x'),
            smin = this[`scale_${aname}min`],
            smax = this[`scale_${aname}max`];

      eps = (smax - smin) * 1e-7;

      if ((Math.abs(umin - smin) > eps) || (Math.abs(umax - smax) > eps)) {
         this[`zoom_${aname}min`] = umin;
         this[`zoom_${aname}max`] = umax;
      }
   }

   /** @summary Apply zooming from TAxis attributes */
   applyAxisZoom(name) {
      if (this.zoomChangedInteractive(name))
         return;
      this[`zoom_${name}min`] = this[`zoom_${name}max`] = 0;

      const axis = this.getAxis(name);

      if (axis?.TestBit(EAxisBits.kAxisRange)) {
         if ((axis.fFirst !== axis.fLast) && ((axis.fFirst > 1) || (axis.fLast < axis.fNbins))) {
            this[`zoom_${name}min`] = axis.fFirst > 1 ? axis.GetBinLowEdge(axis.fFirst) : axis.fXmin;
            this[`zoom_${name}max`] = axis.fLast < axis.fNbins ? axis.GetBinLowEdge(axis.fLast + 1) : axis.fXmax;
            // reset user range for main painter
            axis.SetBit(EAxisBits.kAxisRange, false);
            axis.fFirst = 1;
            axis.fLast = axis.fNbins;
         }
      }
   }

   /** @summary Create x,y objects which maps user coordinates into pixels
     * @desc While only first painter really need such object, all others just reuse it
     * following functions are introduced
     *    this.GetBin[X/Y]  return bin coordinate
     *    this.[x,y]  these are d3.scale objects
     *    this.gr[x,y]  converts root scale into graphical value
     * @private */
   createXY(opts) {
      this.cleanXY(); // remove all previous configurations

      if (!opts)
         opts = { ndim: 1 };

      this.#swap_xy = opts.swap_xy || false;
      this.#reverse_x = opts.reverse_x || false;
      this.#reverse_y = opts.reverse_y || false;

      this.logx = this.logy = 0;

      const w = this.getFrameWidth(), h = this.getFrameHeight(),
            pp = this.getPadPainter(), pad = pp.getRootPad(),
            pad_logx = pad.fLogx,
            pad_logy = (opts.ndim === 1 ? pad.fLogv : undefined) ?? pad.fLogy;

      this.scales_ndim = opts.ndim;

      this.scale_xmin = this.xmin;
      this.scale_xmax = this.xmax;

      this.scale_ymin = this.ymin;
      this.scale_ymax = this.ymax;

      if (opts.extra_y_space) {
         const log_scale = this.#swap_xy ? pad_logx : pad_logy;
         if (log_scale && (this.scale_ymax > 0))
            this.scale_ymax = Math.exp(Math.log(this.scale_ymax) * 1.1);
         else
            this.scale_ymax += (this.scale_ymax - this.scale_ymin) * 0.1;
      }

      if (opts.check_pad_range) {
         // take zooming out of pad or axis attributes
         this.applyAxisZoom('x');
         if (opts.ndim > 1)
            this.applyAxisZoom('y');
         if (opts.ndim > 2)
            this.applyAxisZoom('z');

         // Use configured pad range - only when main histogram drawn with SAME draw option
         if (opts.check_pad_range === 'pad_range') {
            this.applyPadUserRange(pad, 'x');
            this.applyPadUserRange(pad, 'y');
         }
      }

      if ((opts.zoom_xmin !== opts.zoom_xmax) && ((this.zoom_xmin === this.zoom_xmax) || !this.zoomChangedInteractive('x'))) {
         this.zoom_xmin = opts.zoom_xmin;
         this.zoom_xmax = opts.zoom_xmax;
      }

      if ((opts.zoom_ymin !== opts.zoom_ymax) && ((this.zoom_ymin === this.zoom_ymax) || !this.zoomChangedInteractive('y'))) {
         this.zoom_ymin = opts.zoom_ymin;
         this.zoom_ymax = opts.zoom_ymax;
      }

      let orig_x = true, orig_y = true;

      if (this.zoom_xmin !== this.zoom_xmax) {
         this.scale_xmin = this.zoom_xmin;
         this.scale_xmax = this.zoom_xmax;
         orig_x = false;
      }

      if (this.zoom_ymin !== this.zoom_ymax) {
         this.scale_ymin = this.zoom_ymin;
         this.scale_ymax = this.zoom_ymax;
         orig_y = false;
      }

      // projection should be assigned
      this.recalculateRange(opts.Proj, orig_x, orig_y);

      this.x_handle = new TAxisPainter(pp, this.xaxis, true);
      this.x_handle.setHistPainter(opts.hist_painter, 'x');

      this.x_handle.configureAxis('xaxis', this.xmin, this.xmax, this.scale_xmin, this.scale_xmax, this.#swap_xy, this.#swap_xy ? [0, h] : [0, w], {
         reverse: this.#reverse_x,
         log: this.#swap_xy ? pad_logy : pad_logx,
         ignore_labels: this.x_ignore_labels,
         noexp_changed: this.x_noexp_changed,
         fixed_ticks: opts.xticks,
         symlog: this.#swap_xy ? opts.symlog_y : opts.symlog_x,
         log_min_nz: opts.xmin_nz && (opts.xmin_nz <= this.xmax) ? 0.9 * opts.xmin_nz : 0,
         logcheckmin: (opts.ndim > 1) || !this.#swap_xy,
         logminfactor: logminfactorX
      });

      this.x_handle.assignFrameMembers(this, 'x');

      this.y_handle = new TAxisPainter(pp, this.yaxis, true);
      this.y_handle.setHistPainter(opts.hist_painter, 'y');

      this.y_handle.configureAxis('yaxis', this.ymin, this.ymax, this.scale_ymin, this.scale_ymax, !this.#swap_xy, this.#swap_xy ? [0, w] : [0, h], {
         value_axis: opts.ndim === 1,
         reverse: this.#reverse_y,
         log: this.#swap_xy ? pad_logx : pad_logy,
         ignore_labels: this.y_ignore_labels,
         noexp_changed: this.y_noexp_changed,
         fixed_ticks: opts.yticks,
         symlog: this.#swap_xy ? opts.symlog_x : opts.symlog_y,
         log_min_nz: opts.ymin_nz && (opts.ymin_nz <= this.ymax) ? 0.5 * opts.ymin_nz : 0,
         logcheckmin: (opts.ndim > 1) || this.#swap_xy,
         logminfactor: logminfactorY
      });

      this.y_handle.assignFrameMembers(this, 'y');

      this.setRootPadRange(pad);
   }

   /** @summary Create x,y objects for drawing of second axes
     * @private */
   createXY2(opts) {
      if (!opts)
         opts = { ndim: this.scales_ndim ?? 1 };

      this.#reverse_x2 = opts.reverse_x || false;
      this.#reverse_y2 = opts.reverse_y || false;

      this.logx2 = this.logy2 = 0;

      const w = this.getFrameWidth(), h = this.getFrameHeight(),
            pp = this.getPadPainter(), pad = pp.getRootPad();

      if (opts.second_x) {
         this.scale_x2min = this.x2min;
         this.scale_x2max = this.x2max;
      }

      if (opts.second_y) {
         this.scale_y2min = this.y2min;
         this.scale_y2max = this.y2max;
      }

      if (opts.extra_y_space && opts.second_y) {
         const log_scale = this.#swap_xy ? pad.fLogx : pad.fLogy;
         if (log_scale && (this.scale_y2max > 0))
            this.scale_y2max = Math.exp(Math.log(this.scale_y2max) * 1.1);
         else
            this.scale_y2max += (this.scale_y2max - this.scale_y2min) * 0.1;
      }

      if ((this.zoom_x2min !== this.zoom_x2max) && opts.second_x) {
         this.scale_x2min = this.zoom_x2min;
         this.scale_x2max = this.zoom_x2max;
      }

      if ((this.zoom_y2min !== this.zoom_y2max) && opts.second_y) {
         this.scale_y2min = this.zoom_y2min;
         this.scale_y2max = this.zoom_y2max;
      }

      if (opts.second_x) {
         this.x2_handle = new TAxisPainter(pp, this.x2axis, true);
         this.x2_handle.setHistPainter(opts.hist_painter, 'x');

         this.x2_handle.configureAxis('x2axis', this.x2min, this.x2max, this.scale_x2min, this.scale_x2max, this.#swap_xy, this.#swap_xy ? [0, h] : [0, w], {
            reverse: this.#reverse_x2,
            log: this.#swap_xy ? pad.fLogy : pad.fLogx,
            ignore_labels: this.x2_ignore_labels,
            noexp_changed: this.x2_noexp_changed,
            logcheckmin: (opts.ndim > 1) || !this.#swap_xy,
            logminfactor: logminfactorX
         });

         this.x2_handle.assignFrameMembers(this, 'x2');
      }

      if (opts.second_y) {
         this.y2_handle = new TAxisPainter(pp, this.y2axis, true);
         this.y2_handle.setHistPainter(opts.hist_painter, 'y');

         this.y2_handle.configureAxis('y2axis', this.y2min, this.y2max, this.scale_y2min, this.scale_y2max, !this.#swap_xy, this.#swap_xy ? [0, w] : [0, h], {
            reverse: this.#reverse_y2,
            log: this.#swap_xy ? pad.fLogx : pad.fLogy,
            ignore_labels: this.y2_ignore_labels,
            noexp_changed: this.y2_noexp_changed,
            logcheckmin: (opts.ndim > 1) || this.#swap_xy,
            log_min_nz: opts.ymin_nz && (opts.ymin_nz < this.y2max) ? 0.5 * opts.ymin_nz : 0,
            logminfactor: logminfactorY
         });

         this.y2_handle.assignFrameMembers(this, 'y2');
      }
   }

   /** @summary Return functions to create x/y points based on coordinates
     * @desc In default case returns frame painter itself
     * @private */
   getGrFuncs(second_x, second_y) {
      const use_x2 = second_x && this.grx2,
            use_y2 = second_y && this.gry2;
      if (!use_x2 && !use_y2)
         return this;

      return {
         use_x2,
         grx: use_x2 ? this.grx2 : this.grx,
         logx: this.logx,
         x_handle: use_x2 ? this.x2_handle : this.x_handle,
         scale_xmin: use_x2 ? this.scale_x2min : this.scale_xmin,
         scale_xmax: use_x2 ? this.scale_x2max : this.scale_xmax,
         use_y2,
         gry: use_y2 ? this.gry2 : this.gry,
         logy: this.logy,
         y_handle: use_y2 ? this.y2_handle : this.y_handle,
         scale_ymin: use_y2 ? this.scale_y2min : this.scale_ymin,
         scale_ymax: use_y2 ? this.scale_y2max : this.scale_ymax,
         fp: this,
         _remap(name) {
            if ((name === 'x') && this.use_x2)
               return 'x2';
            if ((name === 'y') && this.use_y2)
               return 'y2';
            return name;
         },
         swap_xy() { return this.fp.swap_xy(); },
         isAxisZoomed(name) { return this.fp.isAxisZoomed(this._remap(name)); },
         revertAxis(name, v) { return this.fp.revertAxis(this._remap(name), v); },
         axisAsText(name, v) { return this.fp.axisAsText(this._remap(name), v); },
         getFrameWidth() { return this.fp.getFrameWidth(); },
         getFrameHeight() { return this.fp.getFrameHeight(); }
      };
   }

   /** @summary Set selected range back to TPad object
     * @private */
   setRootPadRange(pad, is3d) {
      if (!pad || !this.ranges_set)
         return;

      if (is3d) {
         // this is fake values, algorithm should be copied from TView3D class of ROOT
         pad.fUxmin = pad.fUymin = -0.9;
         pad.fUxmax = pad.fUymax = 0.9;
      } else {
         pad.fLogx = this.#swap_xy ? this.logy : this.logx;
         pad.fUxmin = pad.fLogx ? Math.log10(this.scale_xmin) : this.scale_xmin;
         pad.fUxmax = pad.fLogx ? Math.log10(this.scale_xmax) : this.scale_xmax;
         pad.fLogy = this.#swap_xy ? this.logx : this.logy;
         pad.fUymin = pad.fLogy ? Math.log10(this.scale_ymin) : this.scale_ymin;
         pad.fUymax = pad.fLogy ? Math.log10(this.scale_ymax) : this.scale_ymax;
      }

      const rx = pad.fUxmax - pad.fUxmin,
            ry = pad.fUymax - pad.fUymin,
            mx = Math.max(0.001, 1 - pad.fLeftMargin - pad.fRightMargin),
            my = Math.max(0.001, 1 - pad.fBottomMargin - pad.fTopMargin);

      pad.fX1 = pad.fUxmin - rx / mx * pad.fLeftMargin;
      pad.fX2 = pad.fUxmax + rx / mx * pad.fRightMargin;
      pad.fY1 = pad.fUymin - ry / my * pad.fBottomMargin;
      pad.fY2 = pad.fUymax + ry / my * pad.fTopMargin;
   }

   /** @summary Draw axes grids
     * @desc Called immediately after axes drawing */
   drawGrids(draw_grids) {
      const layer = this.getFrameSvg().selectChild('.axis_layer');

      layer.selectAll('.xgrid').remove();
      layer.selectAll('.ygrid').remove();

      const pp = this.getPadPainter(),
            pad = pp?.getRootPad(true),
            h = this.getFrameHeight(),
            w = this.getFrameWidth(),
            grid_style = gStyle.fGridStyle;

      // add a grid on x axis, if the option is set
      if (pad?.fGridx && draw_grids && this.x_handle?.ticks) {
         const colid = (gStyle.fGridColor > 0) ? gStyle.fGridColor : (this.getAxis('x')?.fAxisColor ?? 1);
         let gridx = '';
         this.x_handle.ticks.forEach(pos => {
            gridx += this.#swap_xy ? `M0,${pos}h${w}` : `M${pos},0v${h}`;
         });

         layer.append('svg:path')
              .attr('class', 'xgrid')
              .attr('d', gridx)
              .style('stroke', this.getColor(colid) || 'black')
              .style('stroke-width', gStyle.fGridWidth)
              .style('stroke-dasharray', getSvgLineStyle(grid_style));
      }

      // add a grid on y axis, if the option is set
      if (pad?.fGridy && draw_grids && this.y_handle?.ticks) {
         const colid = (gStyle.fGridColor > 0) ? gStyle.fGridColor : (this.getAxis('y')?.fAxisColor ?? 1);
         let gridy = '';
         this.y_handle.ticks.forEach(pos => {
            gridy += this.#swap_xy ? `M${pos},0v${h}` : `M0,${pos}h${w}`;
         });

         layer.append('svg:path')
              .attr('class', 'ygrid')
              .attr('d', gridy)
              .style('stroke', this.getColor(colid) || 'black')
              .style('stroke-width', gStyle.fGridWidth)
              .style('stroke-dasharray', getSvgLineStyle(grid_style));
      }
   }

   /** @summary Converts 'raw' axis value into text */
   axisAsText(axis, value) {
      const handle = this[`${axis}_handle`];

      if (handle)
         return handle.axisAsText(value, settings[axis.toUpperCase() + 'ValuesFormat']);

      return value.toPrecision(4);
   }

   /** @summary Identify if requested axes are drawn
     * @desc Checks if x/y axes are drawn. Also if second side is already there */
   hasDrawnAxes(second_x, second_y) { return !second_x && !second_y ? this.#axes_drawn : this.#axes2_drawn; }

   /** @summary draw axes,
     * @return {Promise} which ready when drawing is completed  */
   async drawAxes(shrink_forbidden, disable_x_draw, disable_y_draw, AxisPos, has_x_obstacle, has_y_obstacle, enable_grids) {
      this.cleanAxesDrawings();

      if ((this.xmin === this.xmax) || (this.ymin === this.ymax))
         return false;

      if (AxisPos === undefined)
         AxisPos = 0;

      const layer = this.getFrameSvg().selectChild('.axis_layer'),
            w = this.getFrameWidth(),
            h = this.getFrameHeight(),
            pp = this.getPadPainter(),
            pad = pp.getRootPad(true),
            draw_grids = enable_grids && (pad?.fGridx || pad?.fGridy);

      this.x_handle.invert_side = (AxisPos >= 10);
      this.x_handle.lbls_both_sides = !this.x_handle.invert_side && (pad?.fTickx > 1); // labels on both sides
      this.x_handle.has_obstacle = has_x_obstacle;

      this.y_handle.invert_side = ((AxisPos % 10) === 1);
      this.y_handle.lbls_both_sides = !this.y_handle.invert_side && (pad?.fTicky > 1); // labels on both sides
      this.y_handle.has_obstacle = has_y_obstacle;

      const draw_horiz = this.#swap_xy ? this.y_handle : this.x_handle,
            draw_vertical = this.#swap_xy ? this.x_handle : this.y_handle;

      if ((!disable_x_draw || !disable_y_draw) && pp.isFastDrawing())
         disable_x_draw = disable_y_draw = true;

      let pr = Promise.resolve(true);

      if (!disable_x_draw || !disable_y_draw || draw_grids) {
         draw_vertical.optionLeft = draw_vertical.invert_side; // text align

         const can_adjust_frame = !shrink_forbidden && settings.CanAdjustFrame,

               pr1 = draw_horiz.drawAxis(layer, w, h,
                                         draw_horiz.invert_side ? null : `translate(0,${h})`,
                                         pad?.fTickx ? -h : 0, disable_x_draw,
                                         undefined, false, pp.getPadHeight() - h - this.getFrameY()),

               pr2 = draw_vertical.drawAxis(layer, w, h,
                                            draw_vertical.invert_side ? `translate(${w})` : null,
                                            pad?.fTicky ? w : 0, disable_y_draw,
                                            draw_vertical.invert_side ? 0 : this.#frame_x, can_adjust_frame);

         pr = Promise.all([pr1, pr2]).then(() => {
            this.drawGrids(draw_grids);

            if (!can_adjust_frame)
               return;

            let shrink = 0.0;
            const ypos = draw_vertical.position;

            if ((-0.2 * w < ypos) && (ypos < 0)) {
               shrink = -ypos / w + 0.001;
               this.#shrink_frame_left += shrink;
            } else if ((ypos > 0) && (ypos < 0.3 * w) && (this.#shrink_frame_left > 0) && (ypos / w > this.#shrink_frame_left)) {
               shrink = -this.#shrink_frame_left;
               this.#shrink_frame_left = 0.0;
            }

            if (shrink) {
               this.shrinkFrame(shrink, 0);
               return this.redraw().then(() => this.drawAxes(true));
            }
         });
      }

      return pr.then(() => {
         if (!shrink_forbidden)
            this.#axes_drawn = true;
         return true;
      });
   }

   /** @summary draw second axes (if any)  */
   async drawAxes2(second_x, second_y) {
      const layer = this.getFrameSvg().selectChild('.axis_layer'),
            w = this.getFrameWidth(),
            h = this.getFrameHeight(),
            pp = this.getPadPainter(),
            pad = pp.getRootPad(true);

      if (second_x) {
         this.x2_handle.invert_side = true;
         this.x2_handle.lbls_both_sides = false;
         this.x2_handle.has_obstacle = false;
      }

      if (second_y) {
         this.y2_handle.invert_side = true;
         this.y2_handle.lbls_both_sides = false;
      }

      let draw_horiz = this.#swap_xy ? this.y2_handle : this.x2_handle,
          draw_vertical = this.#swap_xy ? this.x2_handle : this.y2_handle;

      if ((draw_horiz || draw_vertical) && pp.isFastDrawing())
         draw_horiz = draw_vertical = null;

      let pr1, pr2;

      if (draw_horiz) {
         pr1 = draw_horiz.drawAxis(layer, w, h,
                                   draw_horiz.invert_side ? null : `translate(0,${h})`,
                                   pad?.fTickx ? -h : 0, false,
                                   undefined, false);
      }

      if (draw_vertical) {
         draw_vertical.optionLeft = draw_vertical.invert_side;
         pr2 = draw_vertical.drawAxis(layer, w, h,
                                      draw_vertical.invert_side ? `translate(${w})` : null,
                                      pad?.fTicky ? w : 0, false,
                                      draw_vertical.invert_side ? 0 : this.#frame_x, false);
      }

      return Promise.all([pr1, pr2]).then(() => {
         this.#axes2_drawn = true;
         return true;
      });
   }


   /** @summary Update frame attributes
     * @private */
   updateAttributes(force) {
      const pp = this.getPadPainter(),
            pad = pp?.getRootPad(true),
            tframe = this.getObject();

      if ((this.fX1NDC === undefined) || (force && !this.$modifiedNDC)) {
         if (!pad) {
            this.fX1NDC = gStyle.fPadLeftMargin;
            this.fX2NDC = 1 - gStyle.fPadRightMargin;
            this.fY1NDC = gStyle.fPadBottomMargin;
            this.fY2NDC = 1 - gStyle.fPadTopMargin;
         } else {
            this.fX1NDC = pad.fLeftMargin;
            this.fX2NDC = 1 - pad.fRightMargin;
            this.fY1NDC = pad.fBottomMargin;
            this.fY2NDC = 1 - pad.fTopMargin;
         }
      }

      if (tframe) {
         this.createAttFill({ attr: tframe });
         this.#border_mode = tframe.fBorderMode;
         this.#border_size = tframe.fBorderSize;
      } else if (!this.fillatt) {
         if (pad?.fFrameFillColor)
            this.createAttFill({ pattern: pad.fFrameFillStyle, color: pad.fFrameFillColor });
         else if (pad)
            this.createAttFill({ attr: pad });
         else
            this.createAttFill({ pattern: gStyle.fFrameFillStyle, color: gStyle.fFrameFillColor });

         // force white color for the canvas frame
         if (!tframe && this.fillatt.empty() && pp?.isCanvas())
            this.fillatt.setSolidColor('white');
         else if ((pad?.fFillStyle === 4000) && !this.fillatt.empty()) // special case of transpad.C macro, which set transparent pad
            this.fillatt.setOpacity(0);
      }

      if (!tframe && (pad?.fFrameLineColor !== undefined))
         this.createAttLine({ color: pad.fFrameLineColor, width: pad.fFrameLineWidth, style: pad.fFrameLineStyle });
      else if (tframe)
         this.createAttLine({ attr: tframe, color: 'black' });
      else
         this.createAttLine({ color: gStyle.fFrameLineColor, width: gStyle.fFrameLineWidth, style: gStyle.fFrameLineStyle });
   }

   /** @summary Function called at the end of resize of frame
     * @desc One should apply changes to the pad
     * @private */
   sizeChanged() {
      const pad = this.getPadPainter()?.getRootPad(true);

      if (pad) {
         pad.fLeftMargin = this.fX1NDC;
         pad.fRightMargin = 1 - this.fX2NDC;
         pad.fBottomMargin = this.fY1NDC;
         pad.fTopMargin = 1 - this.fY2NDC;
         this.setRootPadRange(pad);
      }

      this.interactiveRedraw('pad', 'frame');
   }

   /** @summary Remove all kinds of X/Y function for axes transformation */
   cleanXY() {
      delete this.grx;
      delete this.gry;
      delete this.grz;
      delete this.grx2;
      delete this.gry2;

      this.x_handle?.cleanup();
      this.y_handle?.cleanup();
      this.z_handle?.cleanup();
      this.x2_handle?.cleanup();
      this.y2_handle?.cleanup();

      delete this.x_handle;
      delete this.y_handle;
      delete this.z_handle;
      delete this.x2_handle;
      delete this.y2_handle;
   }

   /** @summary remove all axes drawings */
   cleanAxesDrawings() {
      this.x_handle?.removeG();
      this.y_handle?.removeG();
      this.z_handle?.removeG();
      this.x2_handle?.removeG();
      this.y2_handle?.removeG();

      this.getG()?.selectChild('.axis_layer').selectAll('*').remove();
      this.#axes_drawn = this.#axes2_drawn = false;
   }

   /** @summary Returns frame rectangle plus extra info for hint display */
   cleanFrameDrawings() {
      // cleanup all 3D drawings if any
      if (isFunc(this.create3DScene))
         this.create3DScene(-1);

      this.cleanAxesDrawings();
      this.cleanXY();

      this.ranges_set = false;

      this.xmin = this.xmax = 0;
      this.ymin = this.ymax = 0;
      this.zmin = this.zmax = 0;

      this.zoom_xmin = this.zoom_xmax = 0;
      this.zoom_ymin = this.zoom_ymax = 0;
      this.zoom_zmin = this.zoom_zmax = 0;

      this.scale_xmin = this.scale_xmax = 0;
      this.scale_ymin = this.scale_ymax = 0;
      this.scale_zmin = this.scale_zmax = 0;

      this.getG()?.selectChild('.main_layer').selectAll('*').remove();
      this.getG()?.selectChild('.upper_layer').selectAll('*').remove();

      this.xaxis = null;
      this.yaxis = null;
      this.zaxis = null;

      this.getG()?.selectAll('*').remove();
      this.getG()?.on('mousedown', null)
                  .on('dblclick', null)
                  .on('wheel', null)
                  .on('contextmenu', null)
                  .property('interactive_set', null)
                  .remove();

      this.setG(undefined); // frame <g> element managed by the pad

      if (this.#keys_handler) {
         window.removeEventListener('keydown', this.#keys_handler, false);
         this.#keys_handler = undefined;
      }
   }

   /** @summary Cleanup frame */
   cleanup() {
      this.cleanFrameDrawings();
      this.#click_handler = undefined;
      this.#dblclick_handler = undefined;
      this.#enabled_keys = undefined;

      this.getPadPainter()?.setFramePainter(this, false);

      super.cleanup();
   }

   /** @summary Redraw TFrame */
   redraw(/* reason */) {
      const pp = this.getPadPainter();
      pp?.setFramePainter(this, true);

      // first update all attributes from objects
      this.updateAttributes();

      const rect = pp?.getPadRect() ?? { width: 10, height: 10 },
            lm = Math.round(rect.width * this.fX1NDC),
            tm = Math.round(rect.height * (1 - this.fY2NDC)),
            rotate = pp?.options?.RotateFrame,
            w = Math.round(rect.width * (this.fX2NDC - this.fX1NDC)),
            h = Math.round(rect.height * (this.fY2NDC - this.fY1NDC));

      this.#frame_x = lm;
      this.#frame_y = tm;
      this.#frame_width = rotate ? h : w;
      this.#frame_height = rotate ? w : h;
      this.#frame_trans = rotate ? `rotate(-90,${lm},${tm}) translate(${lm - h},${tm})` : makeTranslate(lm, tm);
      this.$can_drag = !rotate && !pp?.options?.FixFrame;

      return this.mode3d ? this : this.createFrameG();
   }

   /** @summary Create frame element and update all attributes
    * @private */
   createFrameG() {
      // this is svg:g object - container for every other items belonging to frame
      let g = this.setG(this.getFrameSvg()),
          top_rect, main_svg;

      if (g.empty()) {
         g = this.setG(this.getPadPainter().getLayerSvg('primitives_layer').append('svg:g').attr('class', 'root_frame'));

         // empty title on the frame required to suppress title of the canvas
         if (!this.isBatchMode())
            g.append('svg:title').text('');

         top_rect = g.append('svg:path');

         main_svg = g.append('svg:svg')
                     .attr('class', 'main_layer')
                     .attr('x', 0)
                     .attr('y', 0)
                     .attr('overflow', 'hidden');

         g.append('svg:g').attr('class', 'axis_layer');
         g.append('svg:g').attr('class', 'upper_layer');
      } else {
         top_rect = g.selectChild('path');
         main_svg = g.selectChild('.main_layer');
      }

      this.#axes_drawn = this.#axes2_drawn = false;

      g.attr('transform', this.#frame_trans);

      top_rect.attr('d', `M0,0H${this.#frame_width}V${this.#frame_height}H0Z`)
              .call(this.fillatt.func)
              .call(this.lineatt.func);

      main_svg.attr('width', this.#frame_width)
              .attr('height', this.#frame_height)
              .attr('viewBox', `0 0 ${this.#frame_width} ${this.#frame_height}`);

      g.selectAll('.frame_deco').remove();
      if (this.#border_mode && this.fillatt.hasColor()) {
         const paths = getBoxDecorations(0, 0, this.#frame_width, this.#frame_height, this.#border_mode, this.#border_size || 2, this.#border_size || 2);
         g.insert('svg:path', '.main_layer')
          .attr('class', 'frame_deco')
          .attr('d', paths[0])
          .call(this.fillatt.func)
          .style('fill', rgb(this.fillatt.color).brighter(0.5).formatRgb());
         g.insert('svg:path', '.main_layer')
          .attr('class', 'frame_deco')
          .attr('d', paths[1])
          .call(this.fillatt.func)
          .style('fill', rgb(this.fillatt.color).darker(0.5).formatRgb());
      }

      return this;
   }

   /** @summary Change log state of specified axis
     * @param {string} axis - name of axis like 'x' or 'y'
     * @param {number} value - 0 (linear), 1 (log) or 2 (log2) */
   changeAxisLog(axis, value) {
      const pp = this.getPadPainter(),
            pad = pp?.getRootPad(true);
      if (!pad)
         return;

      pp.options._interactively_changed = true;

      const name = `fLog${axis}`;

      // do not allow log scale for labels
      if (!pad[name]) {
         if (this.#swap_xy && axis === 'x')
            axis = 'y';
         else if (this.#swap_xy && axis === 'y')
            axis = 'x';
         const handle = this[`${axis}_handle`];
         if (handle?.kind === kAxisLabels)
            return;
      }

      if ((value === 'toggle') || (value === undefined))
         value = pad[name] ? 0 : 1;

      // directly change attribute in the pad
      pad[name] = value;

      return this.interactiveRedraw('pad', `log${axis}`);
   }

   /** @summary Toggle log state on the specified axis */
   toggleAxisLog(axis) {
      return this.changeAxisLog(axis, 'toggle');
   }

   /** @summary Fill context menu for the frame
     * @desc It could be appended to the histogram menus */
   fillContextMenu(menu, kind, obj) {
      const main = this.getMainPainter(true),
            wrk = main?.$stack_hist ? main.getPrimary() : main,
            pp = this.getPadPainter(),
            pad = pp?.getRootPad(true),
            is_pal = kind === 'pal';

      if (is_pal)
         kind = 'z';

      if ((kind === 'x') || (kind === 'y') || (kind === 'z') || (kind === 'x2') || (kind === 'y2')) {
         const faxis = obj || this[kind + 'axis'],
               handle = this[`${kind}_handle`];
         if (!isFunc(faxis?.TestBit))
            return false;
         const hist_painter = handle?.hist_painter || main;

         menu.header(`${kind.toUpperCase()} axis`, `${urlClassPrefix}${clTAxis}.html`);

         menu.sub('Range');
         menu.add('Zoom', () => {
            let min = this[`zoom_${kind}min`] ?? this[`${kind}min`],
                max = this[`zoom_${kind}max`] ?? this[`${kind}max`];
            if (min === max) {
               min = this[`${kind}min`];
               max = this[`${kind}max`];
            }
            menu.input('Enter zoom range like: [min, max]', `[${min}, ${max}]`).then(v => {
               const arr = JSON.parse(v);
               if (arr && Array.isArray(arr) && (arr.length === 2)) {
                  let flag = false;
                  if (arr[0] < faxis.fXmin) {
                     faxis.fFirst = 0;
                     flag = true;
                  } else
                     faxis.fFirst = 1;
                  if (arr[1] > faxis.fXmax) {
                     faxis.fLast = faxis.fNbins + 1;
                     flag = true;
                  } else
                     faxis.fLast = faxis.fNbins;
                  faxis.SetBit(EAxisBits.kAxisRange, flag);
                  hist_painter?.scanContent();
                  this.zoomSingle(kind, arr[0], arr[1], true).then(res => {
                     if (!res && flag)
                        this.interactiveRedraw('pad');
                  });
               }
            });
         });
         menu.add('Unzoom', () => {
            this.unzoomSingle(kind).then(res => {
               if (!res && (faxis.fFirst !== faxis.fLast)) {
                  faxis.fFirst = faxis.fLast = 0;
                  hist_painter?.scanContent();
                  this.interactiveRedraw('pad');
               }
            });
         });
         if (handle?.value_axis && isFunc(wrk?.accessMM)) {
            menu.add('Minimum', () => {
               menu.input(`Enter minimum value or ${kNoZoom} as default`, wrk.accessMM(true), 'float').then(v => {
                  this[`zoom_${kind}min`] = this[`zoom_${kind}max`] = undefined;
                  wrk.accessMM(true, v);
               });
            });
            menu.add('Maximum', () => {
               menu.input(`Enter maximum value or ${kNoZoom} as default`, wrk.accessMM(false), 'float').then(v => {
                  this[`zoom_${kind}min`] = this[`zoom_${kind}max`] = undefined;
                  wrk.accessMM(false, v);
               });
            });
         }
         menu.endsub();

         if (pad) {
            const member = 'fLog' + kind[0];
            menu.sub('SetLog ' + kind[0], () => {
               menu.input('Enter log kind: 0 - off, 1 - log10, 2 - log2, 3 - ln, ...', pad[member], 'int', 0, 10000).then(v => {
                  this.changeAxisLog(kind[0], v);
               });
            });
            menu.addchk(pad[member] === 0, 'linear', () => this.changeAxisLog(kind[0], 0));
            menu.addchk(pad[member] === 1, 'log10', () => this.changeAxisLog(kind[0], 1));
            menu.addchk(pad[member] === 2, 'log2', () => this.changeAxisLog(kind[0], 2));
            menu.addchk(pad[member] === 3, 'ln', () => this.changeAxisLog(kind[0], 3));
            menu.addchk(pad[member] === 4, 'log4', () => this.changeAxisLog(kind[0], 4));
            menu.addchk(pad[member] === 8, 'log8', () => this.changeAxisLog(kind[0], 8));
            menu.endsub();
         }
         menu.addchk(faxis.TestBit(EAxisBits.kMoreLogLabels), 'More log', flag => {
            faxis.SetBit(EAxisBits.kMoreLogLabels, flag);
            if (hist_painter?.getSnapId() && (kind.length === 1))
               hist_painter.interactiveRedraw('pad', `exec:SetMoreLogLabels(${flag})`, kind);
            else
               this.interactiveRedraw('pad');
         });
         menu.addchk(handle?.noexp ?? faxis.TestBit(EAxisBits.kNoExponent), 'No exponent', flag => {
            faxis.SetBit(EAxisBits.kNoExponent, flag);
            if (handle)
               handle.noexp_changed = true;
            this[`${kind}_noexp_changed`] = true;
            if (hist_painter?.getSnapId() && (kind.length === 1))
               hist_painter.interactiveRedraw('pad', `exec:SetNoExponent(${flag})`, kind);
            else
               this.interactiveRedraw('pad');
         });

         if ((kind === 'z') && isFunc(hist_painter?.fillPaletteMenu))
            hist_painter.fillPaletteMenu(menu, !is_pal);

         menu.addTAxisMenu(EAxisBits, hist_painter || this, faxis, kind, handle, this);
         return true;
      }

      const alone = menu.size() === 0;

      if (alone)
         menu.header('Frame', `${urlClassPrefix}${clTFrame}.html`);
      else
         menu.separator();

      if (this.zoom_xmin !== this.zoom_xmax)
         menu.add('Unzoom X', () => this.unzoom('x'));
      if (this.zoom_ymin !== this.zoom_ymax)
         menu.add('Unzoom Y', () => this.unzoom('y'));
      if (this.zoom_zmin !== this.zoom_zmax)
         menu.add('Unzoom Z', () => this.unzoom('z'));
      if (this.zoom_x2min !== this.zoom_x2max)
         menu.add('Unzoom X2', () => this.unzoom('x2'));
      if (this.zoom_y2min !== this.zoom_y2max)
         menu.add('Unzoom Y2', () => this.unzoom('y2'));
      menu.add('Unzoom all', () => this.unzoom('all'));

      if (pad) {
         menu.addchk(pad.fLogx, 'SetLogx', () => this.toggleAxisLog('x'));
         menu.addchk(pad.fLogy, 'SetLogy', () => this.toggleAxisLog('y'));

         if (isFunc(main?.getDimension) && (main.getDimension() > 1))
            menu.addchk(pad.fLogz, 'SetLogz', () => this.toggleAxisLog('z'));
         menu.separator();
      }

      menu.addchk(this.isTooltipAllowed(), 'Show tooltips', () => this.setTooltipAllowed('toggle'));
      menu.addAttributesMenu(this, alone ? '' : 'Frame ');

      menu.sub('Border');
      menu.addSelectMenu('Mode', ['Down', 'Off', 'Up'], this.#border_mode + 1, v => {
         this.#border_mode = v - 1;
         this.interactiveRedraw(true, `exec:SetBorderMode(${v - 1})`);
      }, 'Frame border mode');
      menu.addSizeMenu('Size', 0, 20, 2, this.#border_size, v => {
         this.#border_size = v;
         this.interactiveRedraw(true, `exec:SetBorderSize(${v})`);
      }, 'Frame border size');
      menu.endsub();

      menu.add('Save to gStyle', () => {
         gStyle.fPadBottomMargin = this.fY1NDC;
         gStyle.fPadTopMargin = 1 - this.fY2NDC;
         gStyle.fPadLeftMargin = this.fX1NDC;
         gStyle.fPadRightMargin = 1 - this.fX2NDC;
         this.fillatt?.saveToStyle('fFrameFillColor', 'fFrameFillStyle');
         this.lineatt?.saveToStyle('fFrameLineColor', 'fFrameLineWidth', 'fFrameLineStyle');
         gStyle.fFrameBorderMode = this.#border_mode;
         gStyle.fFrameBorderSize = this.#border_size;
      }, 'Store frame position and graphical attributes to gStyle');

      menu.separator();

      menu.sub('Save as');
      const fmts = ['svg', 'png', 'jpeg', 'webp'];
      if (internals.makePDF)
         fmts.push('pdf');
      fmts.forEach(fmt => menu.add(`frame.${fmt}`, () => pp.saveAs(fmt, 'frame', `frame.${fmt}`)));
      menu.endsub();

      return true;
   }

   /** @summary Fill option object used in TWebCanvas
     * @private */
   fillWebObjectOptions(res) {
      res.fcust = 'frame';
      res.fopt = [this.scale_xmin || 0, this.scale_ymin || 0, this.scale_xmax || 0, this.scale_ymax || 0];
   }

   /** @summary Returns frame X position */
   getFrameX() { return this.#frame_x || 0; }

   /** @summary Returns frame Y position */
   getFrameY() { return this.#frame_y || 0; }

   /** @summary Returns frame width */
   getFrameWidth() { return this.#frame_width || 0; }

   /** @summary Returns frame height */
   getFrameHeight() { return this.#frame_height || 0; }

   /** @summary Returns frame rectangle plus extra info for hint display */
   getFrameRect() {
      return {
         x: this.#frame_x || 0,
         y: this.#frame_y || 0,
         width: this.getFrameWidth(),
         height: this.getFrameHeight(),
         transform: this.getG()?.attr('transform') || '',
         hint_delta_x: 0,
         hint_delta_y: 0
      };
   }

   /** @summary Configure user-defined click handler
     * @desc Function will be called every time when frame click was performed
     * As argument, tooltip object with selected bins will be provided
     * If handler function returns true, default handling of click will be disabled */
   configureUserClickHandler(handler) {
      this.#click_handler = isFunc(handler) ? handler : null;
   }

   /** @summary Returns actual click handler */
   getClickHandler() { return this.#click_handler; }

   /** @summary Configure user-defined dblclick handler
     * @desc Function will be called every time when double click was called
     * As argument, tooltip object with selected bins will be provided
     * If handler function returns true, default handling of dblclick (unzoom) will be disabled */
   configureUserDblclickHandler(handler) {
      this.#dblclick_handler = isFunc(handler) ? handler : null;
   }

   /** @summary Returns actual double-click handler */
   getDblclickHandler() { return this.#dblclick_handler; }

   /** @summary Function can be used for zooming into specified range
     * @desc if both limits for each axis 0 (like xmin === xmax === 0), axis will be un-zoomed
     * @param {number} xmin
     * @param {number} xmax
     * @param {number} [ymin]
     * @param {number} [ymax]
     * @param {number} [zmin]
     * @param {number} [zmax]
     * @param [interactive] - if changes was performed interactively
     * @return {Promise} with boolean flag if zoom operation was performed */
   async zoom(xmin, xmax, ymin, ymax, zmin, zmax, interactive) {
      if (xmin === 'x') {
         xmin = xmax;
         xmax = ymin;
         interactive = ymax;
         ymin = ymax = undefined;
      } else if (xmin === 'y') {
         interactive = ymax;
         ymax = ymin;
         ymin = xmax;
         xmin = xmax = undefined;
      } else if (xmin === 'z') {
         interactive = ymax;
         zmin = xmax;
         zmax = ymin;
         xmin = xmax = ymin = ymax = undefined;
      }

      let zoom_x = (xmin !== xmax), zoom_y = (ymin !== ymax), zoom_z = (zmin !== zmax),
          unzoom_x = false, unzoom_y = false, unzoom_z = false;

      if (zoom_x) {
         let cnt = 0;
         xmin = this.x_handle?.checkZoomMin(xmin) ?? xmin;
         if (xmin <= this.xmin) {
            xmin = this.xmin;
            cnt++;
         }
         if (xmax >= this.xmax) {
            xmax = this.xmax;
            cnt++;
         }
         if (cnt === 2) {
            zoom_x = false;
            unzoom_x = true;
         }
      } else
         unzoom_x = (xmin === xmax) && (xmin === 0);

      if (zoom_y) {
         let cnt = 0;
         ymin = this.y_handle?.checkZoomMin(ymin) ?? ymin;
         if (ymin <= this.ymin) {
            ymin = this.ymin;
            cnt++;
         }
         if (ymax >= this.ymax) {
            ymax = this.ymax;
            cnt++;
         }
         if ((cnt === 2) && (this.scales_ndim !== 1)) {
            zoom_y = false;
            unzoom_y = true;
         }
      } else
         unzoom_y = (ymin === ymax) && (ymin === 0);

      if (zoom_z) {
         let cnt = 0;
         zmin = this.z_handle?.checkZoomMin(zmin) ?? zmin;
         if (zmin <= this.zmin) {
            zmin = this.zmin;
            cnt++;
         }
         if (zmax >= this.zmax) {
            zmax = this.zmax;
            cnt++;
         }
         if ((cnt === 2) && (this.scales_ndim > 2)) {
            zoom_z = false;
            unzoom_z = true;
         }
      } else
         unzoom_z = (zmin === zmax) && (zmin === 0);

      let changed = false;

      // first process zooming (if any)
      if (zoom_x || zoom_y || zoom_z) {
         this.forEachPainter(obj => {
            if (!isFunc(obj.canZoomInside))
               return;
            if (zoom_x && obj.canZoomInside('x', xmin, xmax)) {
               this.zoom_xmin = xmin;
               this.zoom_xmax = xmax;
               changed = true;
               zoom_x = false;
               if (interactive)
                  this.zoomChangedInteractive('x', interactive);
            }
            if (zoom_y && obj.canZoomInside('y', ymin, ymax)) {
               this.zoom_ymin = ymin;
               this.zoom_ymax = ymax;
               changed = true;
               zoom_y = false;
               if (interactive)
                  this.zoomChangedInteractive('y', interactive);
            }
            if (zoom_z && obj.canZoomInside('z', zmin, zmax)) {
               this.zoom_zmin = zmin;
               this.zoom_zmax = zmax;
               changed = true;
               zoom_z = false;
               if (interactive)
                  this.zoomChangedInteractive('z', interactive);
            }
         });
      }

      // and process unzoom, if any
      if (unzoom_x || unzoom_y || unzoom_z) {
         if (unzoom_x) {
            if (this.zoom_xmin !== this.zoom_xmax)
               changed = true;
            this.zoom_xmin = this.zoom_xmax = 0;
            if (interactive)
               this.zoomChangedInteractive('x', interactive);
         }
         if (unzoom_y) {
            if (this.zoom_ymin !== this.zoom_ymax) {
               changed = true;
               unzoomHistogramYRange(this.getMainPainter());
            }
            this.zoom_ymin = this.zoom_ymax = 0;
            if (interactive)
               this.zoomChangedInteractive('y', interactive);
         }
         if (unzoom_z) {
            if (this.zoom_zmin !== this.zoom_zmax)
               changed = true;
            this.zoom_zmin = this.zoom_zmax = 0;
            if (interactive)
               this.zoomChangedInteractive('z', interactive);
         }

         // than try to unzoom all overlapped objects
         if (!changed) {
            this.forEachPainter(painter => {
               if (isFunc(painter?.unzoomUserRange)) {
                  if (painter.unzoomUserRange(unzoom_x, unzoom_y, unzoom_z))
                     changed = true;
               }
            }, 'objects');
         }
      }
      if (!changed)
         return false;

      return this.interactiveRedraw('pad', 'zoom').then(() => true);
   }

   /** @summary Zooming of single axis
     * @param {String} name - axis name like x/y/z but also second axis x2 or y2
     * @param {Number} vmin - axis minimal value, 0 for unzoom
     * @param {Number} vmax - axis maximal value, 0 for unzoom
     * @param {Boolean} [interactive] - if change was performed interactively
     * @protected */
   async zoomSingle(name, vmin, vmax, interactive) {
      const handle = this[`${name}_handle`], name_min = `zoom_${name}min`, name_max = `zoom_${name}max`;
      if (!handle && (name !== 'z'))
         return false;

      let zoom_v = (vmin !== vmax), unzoom_v = false;

      if (zoom_v) {
         let cnt = 0;
         vmin = handle?.checkZoomMin(vmin) ?? vmin;
         if (vmin <= this[name + 'min']) {
            vmin = this[name + 'min'];
            cnt++;
         }
         if (vmax >= this[name + 'max']) {
            vmax = this[name + 'max'];
            cnt++;
         }
         if (cnt === 2) {
            zoom_v = false;
            unzoom_v = true;
         }
      } else
         unzoom_v = (vmin === vmax) && (vmin === 0);

      let changed = false;

      // first process zooming
      if (zoom_v) {
         this.forEachPainter(obj => {
            if (zoom_v && isFunc(obj.canZoomInside) && obj.canZoomInside(name[0], vmin, vmax)) {
               this[name_min] = vmin;
               this[name_max] = vmax;
               changed = true;
               zoom_v = false;
            }
         });
      }

      // and process unzoom, if any
      if (unzoom_v) {
         if (this[name_min] !== this[name_max]) {
            changed = true;
            if (name === 'y')
               unzoomHistogramYRange(this.getMainPainter());
         }
         this[name_min] = this[name_max] = 0;
      }

      if (!changed)
         return false;

      if (interactive)
         this.zoomChangedInteractive(name, interactive);

      return this.interactiveRedraw('pad', 'zoom').then(() => true);
   }

   /** @summary Unzoom single axis */
   async unzoomSingle(name, interactive) {
      return this.zoomSingle(name, 0, 0, typeof interactive === 'undefined' ? 'unzoom' : interactive);
   }

   /** @summary Checks if specified axis zoomed */
   isAxisZoomed(axis) {
      return this[`zoom_${axis}min`] !== this[`zoom_${axis}max`];
   }

   /** @summary Unzoom specified axes
     * @return {Promise} with boolean flag if zooming changed */
   async unzoom(dox, doy, doz) {
      if (dox === 'all')
         return this.unzoomSingle('x2').then(() => this.unzoomSingle('y2')).then(() => this.unzoom('xyz'));

      if ((dox === 'x2') || (dox === 'y2'))
         return this.unzoomSingle(dox);

      if (typeof dox === 'undefined')
         dox = doy = doz = true;
      else if (isStr(dox)) {
         doz = dox.indexOf('z') >= 0;
         doy = dox.indexOf('y') >= 0;
         dox = dox.indexOf('x') >= 0;
      }

      return this.zoom(dox ? 0 : undefined, dox ? 0 : undefined,
                       doy ? 0 : undefined, doy ? 0 : undefined,
                       doz ? 0 : undefined, doz ? 0 : undefined,
                       'unzoom');
   }

   /** @summary Reset all zoom attributes
     * @private */
   resetZoom() {
      ['x', 'y', 'z', 'x2', 'y2'].forEach(n => {
         this[`zoom_${n}min`] = undefined;
         this[`zoom_${n}max`] = undefined;
         this[`zoom_changed_${n}`] = undefined;
      });
   }

   /** @summary Mark/check if zoom for specific axis was changed interactively
     * @private */
   zoomChangedInteractive(axis, value) {
      if (axis === 'reset') {
         this.zoom_changed_x = this.zoom_changed_y = this.zoom_changed_z = undefined;
         return;
      }
      if (!axis || axis === 'any')
         return this.zoom_changed_x || this.zoom_changed_y || this.zoom_changed_z;

      if ((axis !== 'x') && (axis !== 'y') && (axis !== 'z'))
         return;

      const fld = 'zoom_changed_' + axis;
      if (value === undefined)
         return this[fld];

      // special handling of unzoom, only if was never changed before flag set to true
      if (value === 'unzoom')
         this[fld] = (this[fld] === undefined);
      else if (value)
         this[fld] = true;
   }

   /** @summary Convert graphical coordinate into axis value */
   revertAxis(axis, pnt) {
      if (this.#swap_xy)
         axis = (axis[0] === 'x') ? 'y' : 'x';
      return this[`${axis}_handle`]?.revertPoint(pnt) ?? 0;
   }

   /** @summary Show axis status message
     * @desc method called normally when mouse enter main object element
     * @private */
   showAxisStatus(axis_name, evnt) {
      const taxis = this.getAxis(axis_name),
            m = pointer(evnt, this.getFrameSvg().node());
      let hint_name = axis_name,
          hint_title = clTAxis,
          id = (axis_name === 'x') ? 0 : 1;

      if (taxis) {
         hint_name = taxis.fName;
         hint_title = taxis.fTitle || `TAxis object for ${axis_name}`;
      }
      if (this.#swap_xy)
         id = 1 - id;

      const axis_value = this.revertAxis(axis_name, m[id]);

      this.showObjectStatus(hint_name, hint_title, `${axis_name} : ${this.axisAsText(axis_name, axis_value)}`, `${m[0]},${m[1]}`);
   }

   /** @summary Add interactive keys handlers
    * @private */
   addKeysHandler() {
      if (this.isBatchMode() || this.#keys_handler || (typeof window === 'undefined'))
         return;

      this.#keys_handler = evnt => this.processKeyPress(evnt);

      window.addEventListener('keydown', this.#keys_handler, false);
   }

   /** @summary Add interactive functionality to the frame
     * @private */
   addInteractivity(for_second_axes) {
      if (this.isBatchMode() || (!settings.Zooming && !settings.ContextMenu))
         return false;

      if (!for_second_axes)
         this.addBasicInteractivity();

      return this.addFrameInteractivity(for_second_axes);
   }

} // class TFramePainter

/** @summary Current hierarchy painter
  * @desc Instance of {@link HierarchyPainter} object
  * @private */
let _first_hpainter = null;

/** @summary Returns current hierarchy painter object
  * @private */
function getHPainter() { return _first_hpainter; }

/** @summary Set hierarchy painter object
  * @private */
function setHPainter(hp) { _first_hpainter = hp; }

/**
 * @summary Base class to manage multiple document interface for drawings
 *
 * @private
 */

class MDIDisplay extends BasePainter {

   /** @summary constructor */
   constructor(frameid) {
      super();
      this.frameid = frameid;
      if (frameid !== '$batch$') {
         this.setDom(frameid);
         this.selectDom().property('mdi', this);
      }
      this.cleanupFrame = cleanup; // use standard cleanup function by default
      this.active_frame_title = ''; // keep title of active frame
   }

   /** @summary Assign func which called for each newly created frame */
   setInitFrame(func) {
      this.initFrame = func;
      this.forEachFrame(frame => func(frame));
   }

   /** @summary method called before new frame is created */
   beforeCreateFrame(title) { this.active_frame_title = title; }

   /** @summary method called after new frame is created
     * @private */
   afterCreateFrame(frame) {
      if (isFunc(this.initFrame))
         this.initFrame(frame);
      return frame;
   }

   /** @summary method dedicated to iterate over existing panels
     * @param {function} userfunc is called with arguments (frame)
     * @param {boolean} only_visible let select only visible frames */
   forEachFrame(userfunc, only_visible) {
      console.warn(`forEachFrame not implemented in MDIDisplay ${typeof userfunc} ${only_visible}`);
   }

   /** @summary method dedicated to iterate over existing panels
     * @param {function} userfunc is called with arguments (painter, frame)
     * @param {boolean} only_visible let select only visible frames */
   forEachPainter(userfunc, only_visible) {
      this.forEachFrame(frame => {
         new ObjectPainter(frame).forEachPainter(painter => userfunc(painter, frame));
      }, only_visible);
   }

   /** @summary Returns total number of drawings */
   numDraw() {
      let cnt = 0;
      this.forEachFrame(() => ++cnt);
      return cnt;
   }

   /** @summary Search for the frame using item name */
   findFrame(searchtitle, force) {
      let found_frame = null;

      this.forEachFrame(frame => {
         if (select(frame).attr('frame_title') === searchtitle)
            found_frame = frame;
      });

      if (!found_frame && force)
         found_frame = this.createFrame(searchtitle);

      return found_frame;
   }

   /** @summary Activate frame */
   activateFrame(frame) { this.active_frame_title = frame ? select(frame).attr('frame_title') : ''; }

   /** @summary Return active frame */
   getActiveFrame() { return this.findFrame(this.active_frame_title); }

   /** @summary perform resize for each frame
     * @protected */
   checkMDIResize(only_frame_id, size) {
      let resized_frame = null;

      this.forEachPainter((painter, frame) => {
         if (only_frame_id && (select(frame).attr('id') !== only_frame_id))
            return;

         if ((painter.getItemName() !== null) && isFunc(painter.checkResize)) {
            // do not call resize for many painters on the same frame
            if (resized_frame === frame)
               return;
            painter.checkResize(size);
            resized_frame = frame;
         }
      });
   }

   /** @summary Cleanup all drawings */
   cleanup() {
      this.active_frame_title = '';

      this.forEachFrame(this.cleanupFrame);

      this.selectDom().html('').property('mdi', null);
   }

} // class MDIDisplay


/**
 * @summary Custom MDI display
 *
 * @desc All HTML frames should be created before and add via {@link CustomDisplay#addFrame} calls
 * @private
 */

class CustomDisplay extends MDIDisplay {

   constructor() {
      super('dummy');
      this.frames = {}; // array of configured frames
   }

   addFrame(divid, itemname) {
      const prev = this.frames[divid] || '';
      this.frames[divid] = prev + (itemname + ';');
   }

   forEachFrame(userfunc) {
      const ks = Object.keys(this.frames);
      for (let k = 0; k < ks.length; ++k) {
         const node = select('#' + ks[k]);
         if (!node.empty())
            userfunc(node.node());
      }
   }

   createFrame(title) {
      this.beforeCreateFrame(title);

      const ks = Object.keys(this.frames);
      for (let k = 0; k < ks.length; ++k) {
         const items = this.frames[ks[k]];
         if (items.indexOf(title + ';') >= 0)
            return select('#' + ks[k]).node();
      }
      return null;
   }

   cleanup() {
      super.cleanup();
      this.forEachFrame(frame => select(frame).html(''));
   }

} // class CustomDisplay

/**
 * @summary Generic grid MDI display
 *
 * @private
 */

class GridDisplay extends MDIDisplay {

   /** @summary Create GridDisplay instance
   * @param {string} frameid - where grid display is created
   * @param {string} kind - kind of grid
   * @desc  following kinds are supported
   *    - vertical or horizontal - only first letter matters, defines basic orientation
   *    - 'x' in the name disable interactive separators
   *    - v4 or h4 - 4 equal elements in specified direction
   *    - v231 -  created 3 vertical elements, first divided on 2, second on 3 and third on 1 part
   *    - v23_52 - create two vertical elements with 2 and 3 subitems, size ratio 5:2
   *    - gridNxM - normal grid layout without interactive separators
   *    - gridiNxM - grid layout with interactive separators
   *    - simple - no layout, full frame used for object drawings */
   constructor(frameid, kind, kind2) {
      super(frameid);

      this.framecnt = 0;
      this.getcnt = 0;
      this.groups = [];
      this.vertical = kind && (kind[0] === 'v');
      this.use_separarators = !kind || (kind.indexOf('x') < 0);
      this.simple_layout = false;

      const dom = this.selectDom();
      dom.style('overflow', 'hidden');

      if (kind === 'simple') {
         this.simple_layout = true;
         this.use_separarators = false;
         this.framecnt = 1;
         return;
      }

      let num = 2, arr, sizes, chld_sizes;

      if (kind === 'projxy') {
         this.vertical = false;
         this.use_separarators = true;
         arr = [2, 2];
         sizes = [1, 3];
         chld_sizes = [[3, 1], [3, 1]];
         kind = '';
         this.match_sizes = true;
      } else if ((kind.indexOf('grid') === 0) || kind2) {
         if (kind2)
            kind = kind + 'x' + kind2;
         else
            kind = kind.slice(4).trim();
         this.use_separarators = false;
         if (kind[0] === 'i') {
            this.use_separarators = true;
            kind = kind.slice(1);
         }

         const separ = kind.indexOf('x');
         let sizex, sizey;

         if (separ > 0) {
            sizey = parseInt(kind.slice(separ + 1));
            sizex = parseInt(kind.slice(0, separ));
         } else
            sizex = sizey = parseInt(kind);

         if (!Number.isInteger(sizex))
            sizex = 3;
         if (!Number.isInteger(sizey))
            sizey = 3;

         if (sizey > 1) {
            this.vertical = true;
            num = sizey;
            if (sizex > 1)
               arr = new Array(num).fill(sizex);
         } else if (sizex > 1) {
            this.vertical = false;
            num = sizex;
         } else {
            this.simple_layout = true;
            this.use_separarators = false;
            this.framecnt = 1;
            return;
         }
         kind = '';
      }

      if (kind && kind.indexOf('_') > 0) {
         let arg = parseInt(kind.slice(kind.indexOf('_') + 1), 10);
         if (Number.isInteger(arg) && (arg > 10)) {
            kind = kind.slice(0, kind.indexOf('_'));
            sizes = [];
            while (arg > 0) {
               sizes.unshift(Math.max(arg % 10, 1));
               arg = Math.round((arg - sizes[0]) / 10);
               if (sizes[0] === 0)
                  sizes[0] = 1;
            }
         }
      }

      kind = kind ? parseInt(kind.replace(/^\D+/g, ''), 10) : 0;
      if (Number.isInteger(kind) && (kind > 1)) {
         if (kind < 10)
            num = kind;
         else {
            arr = [];
            while (kind > 0) {
               arr.unshift(kind % 10);
               kind = Math.round((kind - arr[0]) / 10);
               if (arr[0] === 0)
                  arr[0] = 1;
            }
            num = arr.length;
         }
      }

      if (sizes?.length !== num)
         sizes = undefined;
      if (chld_sizes?.length !== num)
         chld_sizes = undefined;

      if (!this.simple_layout)
         this.createGroup(this, dom, num, arr, sizes, chld_sizes);
   }

   /** @summary Create frames group
     * @private */
   createGroup(handle, main, num, childs, sizes, childs_sizes) {
      if (!sizes)
         sizes = new Array(num);
      let sum1 = 0, sum2 = 0;
      for (let n = 0; n < num; ++n)
         sum1 += (sizes[n] || 1);
      for (let n = 0; n < num; ++n) {
         sizes[n] = Math.round(100 * (sizes[n] || 1) / sum1);
         sum2 += sizes[n];
         if (n === num - 1)
            sizes[n] += (100 - sum2); // make 100%
      }

      for (let cnt = 0; cnt < num; ++cnt) {
         const group = { id: cnt, drawid: -1, position: 0, size: sizes[cnt], parent: handle };
         if (cnt > 0)
            group.position = handle.groups[cnt - 1].position + handle.groups[cnt - 1].size;
         group.position0 = group.position;

         if (!childs || !childs[cnt] || childs[cnt] < 2)
            group.drawid = this.framecnt++;

         handle.groups.push(group);

         const elem = main.append('div').attr('groupid', group.id);

         // remember HTML node only when need to match sizes of different groups
         if (handle.match_sizes)
            group.node = elem.node();

         if (handle.vertical)
            elem.style('float', 'bottom').style('height', group.size.toFixed(2) + '%').style('width', '100%');
         else
            elem.style('float', 'left').style('width', group.size.toFixed(2) + '%').style('height', '100%');

         if (group.drawid >= 0) {
            elem.classed('jsroot_newgrid', true);
            if (isStr(this.frameid))
               elem.attr('id', `${this.frameid}_${group.drawid}`);
         } else
            elem.style('display', 'flex').style('flex-direction', handle.vertical ? 'row' : 'column');

         if (childs && (childs[cnt] > 1)) {
            group.vertical = !handle.vertical;
            group.groups = [];
            elem.style('overflow', 'hidden');
            this.createGroup(group, elem, childs[cnt], null, childs_sizes ? childs_sizes[cnt] : null);
         }
      }

      if (this.use_separarators && isFunc(this.createSeparator)) {
         for (let cnt = 1; cnt < num; ++cnt)
            this.createSeparator(handle, main, handle.groups[cnt]);
      }
   }

   /** @summary Handle interactive separator movement
     * @private */
   handleSeparator(elem, action) {
      const findGroup = (node, grid) => {
         let chld = node?.firstChild;
         while (chld) {
            if (chld.getAttribute('groupid') === grid)
               return select(chld);
            chld = chld.nextSibling;
         }
         // should never happen, but keep it here like
         return select(node).select(`[groupid='${grid}']`);
      }, setGroupSize = (h, node, grid) => {
         const name = h.vertical ? 'height' : 'width',
               size = h.groups[grid].size.toFixed(2) + '%';
         findGroup(node, grid).style(name, size)
                              .selectAll('.jsroot_separator').style(name, size);
      }, resizeGroup = (node, grid) => {
         let sel = findGroup(node, grid);
         if (!sel.classed('jsroot_newgrid'))
            sel = sel.select('.jsroot_newgrid');
         sel.each(function() { resize(this); });
      }, posSepar = (h, group, separ) => {
         separ.style(h.vertical ? 'top' : 'left', `calc(${group.position.toFixed(2)}% - 2px)`);
      };
      // eslint-disable-next-line one-var
      const separ = select(elem),
            parent = elem.parentNode,
            handle = separ.property('handle'),
            id = separ.property('separator_id'),
            group = handle.groups[id];

      if (action === 'start') {
         group.startpos = group.position;
         group.acc_drag = 0;
         return;
      }

      let needResize, needSetSize = false;

      if (action === 'end') {
         if (Math.abs(group.startpos - group.position) < 0.5)
            return;
         needResize = true;
      } else {
         let pos;
         if (action === 'restore')
            pos = group.position0;
         else if (handle.vertical) {
            group.acc_drag += action.dy;
            pos = group.startpos + ((group.acc_drag + 2) / parent.clientHeight) * 100;
         } else {
            group.acc_drag += action.dx;
            pos = group.startpos + ((group.acc_drag + 2) / parent.clientWidth) * 100;
         }

         const diff = group.position - pos;
         if ((Math.abs(diff) < 0.3) || (Math.min(handle.groups[id - 1].size - diff, group.size + diff) < 3))
            return; // if no significant change, do nothing

         handle.groups[id - 1].size -= diff;
         group.size += diff;
         group.position = pos;

         posSepar(handle, group, separ);

         needSetSize = true;
         needResize = (action === 'restore');
      }

      if (needSetSize) {
         setGroupSize(handle, parent, id - 1);
         setGroupSize(handle, parent, id);
      }

      if (needResize) {
         resizeGroup(parent, id - 1);
         resizeGroup(parent, id);
      }

      // now handling match of the sizes
      if (!handle.parent?.match_sizes)
         return;

      for (let k = 0; k < handle.parent.groups.length; ++k) {
         const hh = handle.parent.groups[k];
         if ((hh === handle) || !hh.node)
            continue;
         hh.groups[id].size = handle.groups[id].size;
         hh.groups[id].position = handle.groups[id].position;
         hh.groups[id - 1].size = handle.groups[id - 1].size;
         hh.groups[id - 1].position = handle.groups[id - 1].position;
         if (needSetSize) {
            select(hh.node).selectAll('.jsroot_separator').each(function() {
               const s = select(this);
               if (s.property('separator_id') === id)
                  posSepar(hh, hh.groups[id], s);
            });
            setGroupSize(hh, hh.node, id - 1);
            setGroupSize(hh, hh.node, id);
         }
         if (needResize) {
            resizeGroup(hh.node, id - 1);
            resizeGroup(hh.node, id);
         }
      }
   }

   /** @summary Create group separator
     * @private */
   createSeparator(handle, main, group) {
      const separ = main.append('div');

      separ.classed('jsroot_separator', true)
           .property('handle', handle)
           .property('separator_id', group.id)
           .attr('style', 'pointer-events: all; border: 0; margin: 0; padding: 0; position: absolute;')
           .style(handle.vertical ? 'top' : 'left', `calc(${group.position.toFixed(2)}% - 2px)`)
           .style(handle.vertical ? 'width' : 'height', (handle.size?.toFixed(2) || 100) + '%')
           .style(handle.vertical ? 'height' : 'width', '5px')
           .style('cursor', handle.vertical ? 'ns-resize' : 'ew-resize')
           .append('div').attr('style', 'position: absolute;' + (handle.vertical
                       ? 'left: 0; right: 0; top: 50%; height: 3px; border-top: 1px dotted #ff0000'
                       : 'top: 0; bottom: 0; left: 50%; width: 3px; border-left: 1px dotted #ff0000'));

      const pthis = this, drag_move =
        drag().on('start', function() { pthis.handleSeparator(this, 'start'); })
                 .on('drag', function(evnt) { pthis.handleSeparator(this, evnt); })
                 .on('end', function() { pthis.handleSeparator(this, 'end'); });

      separ.call(drag_move).on('dblclick', function() { pthis.handleSeparator(this, 'restore'); });

      // need to get touches events handling in drag
      if (browser.touches && !main.on('touchmove'))
         main.on('touchmove', () => {});
   }

   /** @summary Call function for each frame */
   forEachFrame(userfunc) {
      if (this.simple_layout)
         userfunc(this.getGridFrame());
      else
         this.selectDom().selectAll('.jsroot_newgrid').each(function() { userfunc(this); });
   }

   /** @summary Returns active frame */
   getActiveFrame() {
      if (this.simple_layout)
         return this.getGridFrame();

      let found = super.getActiveFrame();
      this.forEachFrame(frame => {
         if (!found)
            found = frame;
      });

      return found;
   }

   /** @summary Returns number of frames in grid layout */
   numGridFrames() { return this.framecnt; }

   /** @summary Return grid frame by its id */
   getGridFrame(id) {
      if (this.simple_layout)
         return this.selectDom('origin').node();
      let res = null;
      this.selectDom().selectAll('.jsroot_newgrid').each(function() {
         if (id-- === 0)
            res = this;
      });
      return res;
   }

   /** @summary Create new frame */
   createFrame(title) {
      this.beforeCreateFrame(title);

      let frame = null, maxloop = this.framecnt || 2;

      while (!frame && maxloop--) {
         frame = this.getGridFrame(this.getcnt);
         if (!this.simple_layout && this.framecnt)
            this.getcnt = (this.getcnt + 1) % this.framecnt;

         if (select(frame).classed('jsroot_fixed_frame'))
            frame = null;
      }

      if (frame) {
         this.cleanupFrame(frame);
         select(frame).attr('frame_title', title);
      }

      return this.afterCreateFrame(frame);
   }

} // class GridDisplay


// ================================================

/**
 * @summary Tabs-based display
 *
 * @private
 */

class TabsDisplay extends MDIDisplay {

   constructor(frameid) {
      super(frameid);
      this.cnt = 0; // use to count newly created frames
      this.selectDom().style('overflow', 'hidden');
   }

   /** @summary Cleanup all drawings */
   cleanup() {
      this.selectDom().style('overflow', null);
      this.cnt = 0;
      super.cleanup();
   }

   /** @summary call function for each frame */
   forEachFrame(userfunc, only_visible) {
      if (!isFunc(userfunc))
         return;

      if (only_visible) {
         const active = this.getActiveFrame();
         if (active)
            userfunc(active);
         return;
      }

      const main = this.selectDom().select('.jsroot_tabs_main');

      main.selectAll('.jsroot_tabs_draw').each(function() {
         userfunc(this);
      });
   }

   /** @summary modify tab state by id */
   modifyTabsFrame(frame_id, action) {
      const top = this.selectDom().select('.jsroot_tabs'),
            labels = top.select('.jsroot_tabs_labels'),
            main = top.select('.jsroot_tabs_main');

      labels.selectAll('.jsroot_tabs_label').each(function() {
         const id = select(this).property('frame_id'),
               is_same = (id === frame_id),
               active_color = settings.DarkMode ? '#333' : 'white';

         if (action === 'activate') {
            select(this).style('background', is_same ? active_color : (settings.DarkMode ? 'black' : '#ddd'))
                           .style('color', settings.DarkMode ? '#ddd' : 'inherit')
                           .style('border-color', active_color);
         } else if ((action === 'close') && is_same)
            this.parentNode.remove();
      });

      let selected_frame, other_frame;

      main.selectAll('.jsroot_tabs_draw').each(function() {
         const match = select(this).property('frame_id') === frame_id;
         if (match)
            selected_frame = this;
         else
            other_frame = this;
         if (action === 'activate')
            select(this).style('background', settings.DarkMode ? 'black' : 'white');
      });

      if (!selected_frame)
         return;

      if (action === 'activate')
         selected_frame.parentNode.appendChild(selected_frame);
         // super.activateFrame(selected_frame);
      else if (action === 'close') {
         const was_active = (selected_frame === this.getActiveFrame());
         cleanup(selected_frame);
         selected_frame.remove();

         if (was_active)
            this.activateFrame(other_frame);
      }
   }

   /** @summary activate frame */
   activateFrame(frame) {
      if (frame)
         this.modifyTabsFrame(select(frame).property('frame_id'), 'activate');
      super.activateFrame(frame);
   }

   /** @summary create new frame */
   createFrame(title) {
      this.beforeCreateFrame(title);

      const dom = this.selectDom();
      let top = dom.select('.jsroot_tabs'), labels, main;

      if (top.empty()) {
         top = dom.append('div').attr('class', 'jsroot_tabs')
                  .attr('style', 'display: flex; flex-direction: column; position: absolute; overflow: hidden; left: 0px; top: 0px; bottom: 0px; right: 0px;');
         labels = top.append('div').attr('class', 'jsroot_tabs_labels')
                     .attr('style', 'white-space: nowrap; position: relative; overflow-x: auto');
         main = top.append('div').attr('class', 'jsroot_tabs_main')
                     .attr('style', 'margin: 0; flex: 1 1 0%; position: relative');
      } else {
         labels = top.select('.jsroot_tabs_labels');
         main = top.select('.jsroot_tabs_main');
      }

      const frame_id = this.cnt++, mdi = this;
      let lbl = title;

      if (!lbl || !isStr(lbl))
         lbl = `frame_${frame_id}`;

      if (lbl.length > 15) {
         let p = lbl.lastIndexOf('/');
         if (p === lbl.length - 1)
            p = lbl.lastIndexOf('/', p - 1);
         if ((p > 0) && (lbl.length - p < 20) && (lbl.length - p > 1))
            lbl = lbl.slice(p + 1);
         else
            lbl = '...' + lbl.slice(lbl.length - 17);
      }

      labels.append('span')
         .attr('tabindex', 0)
         .append('label')
         .attr('class', 'jsroot_tabs_label')
         .attr('style', 'border: 1px solid; display: inline-block; font-size: 1rem; left: 1px;' +
                        'margin-left: 3px; padding: 0px 5px 1px 5px; position: relative; vertical-align: bottom;')
         .property('frame_id', frame_id)
         .text(lbl)
         .attr('title', title)
         .on('click', function(evnt) {
            evnt.preventDefault(); // prevent handling in close button
            mdi.modifyTabsFrame(select(this).property('frame_id'), 'activate');
         }).append('button')
         .attr('title', 'close')
         .attr('style', 'margin-left: .5em; padding: 0; font-size: 0.5em; width: 1.8em; height: 1.8em; vertical-align: center;')
         .text('\u2715')
         .on('click', function() {
            mdi.modifyTabsFrame(select(this.parentNode).property('frame_id'), 'close');
         });

      const draw_frame = main.append('div')
                           .attr('frame_title', title)
                           .attr('class', 'jsroot_tabs_draw')
                           .attr('style', 'overflow: hidden; position: absolute; left: 0px; top: 0px; bottom: 0px; right: 0px;')
                           .property('frame_id', frame_id);

      this.modifyTabsFrame(frame_id, 'activate');

      return this.afterCreateFrame(draw_frame.node());
   }

   /** @summary Handle changes in dark mode */
   changeDarkMode() {
      const frame = this.getActiveFrame();
      this.modifyTabsFrame(select(frame).property('frame_id'), 'activate');
   }

} // class TabsDisplay


/**
 * @summary Generic flexible MDI display
 *
 * @private
 */

class FlexibleDisplay extends MDIDisplay {

   constructor(frameid) {
      super(frameid);
      this.cnt = 0; // use to count newly created frames
      this.selectDom().on('contextmenu', evnt => this.showContextMenu(evnt))
                      .style('overflow', 'auto');
   }

   /** @summary Cleanup all drawings */
   cleanup() {
      this.selectDom().style('overflow', null)
                      .on('contextmenu', null);
      this.cnt = 0;
      super.cleanup();
   }

   /** @summary call function for each frame */
   forEachFrame(userfunc, only_visible) {
      if (!isFunc(userfunc))
         return;

      const mdi = this, top = this.selectDom().select('.jsroot_flex_top');

      top.selectAll('.jsroot_flex_draw').each(function() {
         // check if only visible specified
         if (only_visible && (mdi.getFrameState(this) === 'min'))
            return;

         userfunc(this);
      });
   }

   /** @summary return active frame */
   getActiveFrame() {
      let found = super.getActiveFrame();
      if (found && select(found.parentNode).property('state') !== 'min')
         return found;

      found = null;
      this.forEachFrame(frame => { found = frame; }, true);
      return found;
   }

   /** @summary activate frame */
   activateFrame(frame) {
      if ((frame === 'first') || (frame === 'last')) {
         let res = null;
         this.forEachFrame(f => {
            if (frame === 'last' || !res)
               res = f;
         }, true);
         frame = res;
      }
      if ((frame?.getAttribute('class') !== 'jsroot_flex_draw') || (this.getActiveFrame() === frame))
         return;

      super.activateFrame(frame);

      const main = frame.parentNode;
      main.parentNode.append(main);

      if (this.getFrameState(frame) !== 'min') {
         selectActivePad({ pp: getElementCanvPainter(frame), active: true });
         resize(frame);
      }
   }

   /** @summary get frame state */
   getFrameState(frame) {
      const main = select(frame.parentNode);
      return main.property('state');
   }

   /** @summary returns frame rect */
   getFrameRect(frame) {
      if (this.getFrameState(frame) === 'max') {
         const top = this.selectDom().select('.jsroot_flex_top');
         return { x: 0, y: 0, w: top.node().clientWidth, h: top.node().clientHeight };
      }

      const main = select(frame.parentNode), left = main.style('left'), top = main.style('top');
      return {
         x: parseInt(left.slice(0, left.length - 2)), y: parseInt(top.slice(0, top.length - 2)),
         w: main.node().clientWidth, h: main.node().clientHeight
      };
   }

   /** @summary change frame state */
   changeFrameState(frame, newstate, no_redraw) {
      const main = select(frame.parentNode),
            state = main.property('state'),
            top = this.selectDom().select('.jsroot_flex_top');

      if (state === newstate)
         return false;

      if (state === 'normal')
         main.property('original_style', main.attr('style'));

      // clear any previous settings
      top.style('overflow', null);

      switch (newstate) {
         case 'min':
            main.style('height', 'auto').style('width', 'auto');
            main.select('.jsroot_flex_draw').style('display', 'none');
            break;
         case 'max':
            main.style('height', '100%').style('width', '100%').style('left', '').style('top', '');
            main.select('.jsroot_flex_draw').style('display', null);
            top.style('overflow', 'hidden');
            break;
         default:
            main.select('.jsroot_flex_draw').style('display', null);
            main.attr('style', main.property('original_style'));
      }

      main.select('.jsroot_flex_header').selectAll('button').each(function(d) {
         const btn = select(this);
         if (((d.t === 'minimize') && (newstate === 'min')) ||
             ((d.t === 'maximize') && (newstate === 'max')))
            btn.text('\u259E').attr('title', 'restore');
         else
            btn.text(d.n).attr('title', d.t);
      });

      main.property('state', newstate);
      main.select('.jsroot_flex_resize').style('display', (newstate === 'normal') ? null : 'none');

      // adjust position of new minified rect
      if (newstate === 'min') {
         const rect = this.getFrameRect(frame),
               ww = top.node().clientWidth,
               hh = top.node().clientHeight,
               arr = [], step = 4,
               crossX = (r1, r2) => ((r1.x <= r2.x) && (r1.x + r1.w >= r2.x)) || ((r2.x <= r1.x) && (r2.x + r2.w >= r1.x)),
               crossY = (r1, r2) => ((r1.y <= r2.y) && (r1.y + r1.h >= r2.y)) || ((r2.y <= r1.y) && (r2.y + r2.h >= r1.y));

         this.forEachFrame(f => {
            if ((f !== frame) && (this.getFrameState(f) === 'min'))
               arr.push(this.getFrameRect(f));
         });

         rect.y = hh;
         do {
            rect.x = step;
            rect.y -= rect.h + step;
            let maxx = step, iscrossed = false;
            arr.forEach(r => {
               if (crossY(r, rect)) {
                  maxx = Math.max(maxx, r.x + r.w + step);
                  if (crossX(r, rect))
                     iscrossed = true;
               }
            });
            if (iscrossed)
               rect.x = maxx;
         } while ((rect.x + rect.w > ww - step) && (rect.y > 0));
         if (rect.y < 0) {
            rect.x = step;
            rect.y = hh - rect.h - step;
         }

         main.style('left', rect.x + 'px').style('top', rect.y + 'px');
      } else if (!no_redraw)
         resize(frame);

      return true;
   }

   /** @summary handle button click
     * @private */
   _clickButton(btn) {
      const kind = select(btn).datum(),
            main = select(btn.parentNode.parentNode),
            frame = main.select('.jsroot_flex_draw').node();

      if (kind.t === 'close') {
         this.cleanupFrame(frame);
         main.remove();
         this.activateFrame('last'); // set active as last non-minified window
         return;
      }

      const state = main.property('state');
      let newstate;
      if (kind.t === 'maximize')
         newstate = (state === 'max') ? 'normal' : 'max';
      else
         newstate = (state === 'min') ? 'normal' : 'min';

      if (this.changeFrameState(frame, newstate))
         this.activateFrame(newstate !== 'min' ? frame : 'last');
   }

   /** @summary create new frame */
   createFrame(title) {
      this.beforeCreateFrame(title);

      const mdi = this,
            dom = this.selectDom();
      let top = dom.select('.jsroot_flex_top');

      if (top.empty()) {
         top = dom.append('div')
                  .attr('class', 'jsroot_flex_top')
                  .attr('style', 'overflow: auto; position: relative; height: 100%; width: 100%');
      }

      const w = top.node().clientWidth,
            h = top.node().clientHeight,
            main = top.append('div');

      main.html('<div class=\'jsroot_flex_header\' style=\'height: 23px; overflow: hidden; background-color: lightblue\'>' +
                '<p style=\'margin: 1px; float: left; font-size: 14px; padding-left: 5px\'></p></div>' +
                `<div id='${this.frameid}_cont${this.cnt}' class='jsroot_flex_draw' style='overflow: hidden; width: 100%; height: calc(100% - 24px); background: white'></div>` +
                '<div class=\'jsroot_flex_resize\' style=\'position: absolute; right: 3px; bottom: 1px; overflow: hidden; cursor: nwse-resize\'>&#x25FF;</div>');

      main.select('.jsroot_flex_header p').text(title);

      main.attr('class', 'jsroot_flex_frame')
          .style('position', 'absolute')
          .style('left', Math.round(w * (this.cnt % 5) / 10) + 'px')
          .style('top', Math.round(h * (this.cnt % 5) / 10) + 'px')
          .style('width', Math.round(w * 0.58) + 'px')
          .style('height', Math.round(h * 0.58) + 'px')
          .style('border', '1px solid black')
          .style('box-shadow', '1px 1px 2px 2px #aaa')
          .property('state', 'normal')
          .select('.jsroot_flex_header')
          .on('contextmenu', evnt => mdi.showContextMenu(evnt, true))
          .on('click', function() { mdi.activateFrame(select(this.parentNode).select('.jsroot_flex_draw').node()); })
          .selectAll('button')
          .data([{ n: '\u2715', t: 'close' }, { n: '\u2594', t: 'maximize' }, { n: '\u2581', t: 'minimize' }])
          .enter()
          .append('button')
          .attr('type', 'button')
          .attr('style', 'float: right; padding: 0; width: 1.4em; text-align: center; font-size: 10px; margin-top: 2px; margin-right: 4px')
          .attr('title', d => d.t)
          .text(d => d.n)
          .on('click', function() { mdi._clickButton(this); });

      let moving_frame = null, moving_div = null, doing_move = false, current = [];
      const drag_object = drag().subject(Object);
      drag_object.on('start', function(evnt) {
         if (evnt.sourceEvent.target.type === 'button')
            return mdi._clickButton(evnt.sourceEvent.target);

         if (detectRightButton(evnt.sourceEvent))
            return;

         const mframe = select(this.parentNode);
         if (!mframe.classed('jsroot_flex_frame') || (mframe.property('state') === 'max'))
            return;

         doing_move = !select(this).classed('jsroot_flex_resize');
         if (!doing_move && (mframe.property('state') === 'min'))
            return;

         mdi.activateFrame(mframe.select('.jsroot_flex_draw').node());

         moving_div = top.append('div').attr('style', mframe.attr('style')).style('border', '2px dotted #00F');

         if (mframe.property('state') === 'min') {
            moving_div.style('width', mframe.node().clientWidth + 'px')
                      .style('height', mframe.node().clientHeight + 'px');
         }

         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();

         moving_frame = mframe;
         current = [];
      }).on('drag', evnt => {
         if (!moving_div)
            return;
         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();
         const changeProp = (i, name, dd) => {
            if (i >= current.length) {
               const v = moving_div.style(name);
               current[i] = parseInt(v.slice(0, v.length - 2));
            }
            current[i] += dd;
            moving_div.style(name, Math.max(0, current[i]) + 'px');
         };
         if (doing_move) {
            changeProp(0, 'left', evnt.dx);
            changeProp(1, 'top', evnt.dy);
         } else {
            changeProp(0, 'width', evnt.dx);
            changeProp(1, 'height', evnt.dy);
         }
      }).on('end', evnt => {
         if (!moving_div)
            return;
         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();
         if (doing_move) {
            moving_frame.style('left', moving_div.style('left'));
            moving_frame.style('top', moving_div.style('top'));
         } else {
            moving_frame.style('width', moving_div.style('width'));
            moving_frame.style('height', moving_div.style('height'));
         }
         moving_div.remove();
         moving_div = null;
         if (!doing_move)
            resize(moving_frame.select('.jsroot_flex_draw').node());
      });

      main.select('.jsroot_flex_header').call(drag_object);
      main.select('.jsroot_flex_resize').call(drag_object);

      const draw_frame = main.select('.jsroot_flex_draw')
                           .attr('frame_title', title)
                           .property('frame_cnt', this.cnt++)
                           .node();

      return this.afterCreateFrame(draw_frame);
   }

   /** @summary minimize all frames */
   minimizeAll() {
      this.forEachFrame(frame => this.changeFrameState(frame, 'min'));
   }

   /** @summary show all frames which are minimized */
   showAll() {
      this.forEachFrame(frame => {
         if (this.getFrameState(frame) === 'min')
            this.changeFrameState(frame, 'normal');
      });
   }

   /** @summary close all frames */
   closeAllFrames() {
      const arr = [];
      this.forEachFrame(frame => arr.push(frame));
      arr.forEach(frame => {
         this.cleanupFrame(frame);
         select(frame.parentNode).remove();
      });
   }

   /** @summary cascade frames */
   sortFrames(kind) {
      const arr = [];
      this.forEachFrame(frame => {
         const state = this.getFrameState(frame);
         if (state === 'min')
            return;
         if (state === 'max')
            this.changeFrameState(frame, 'normal', true);
         arr.push(frame);
      });

      if (!arr.length)
         return;

      const top = this.selectDom(),
            w = top.node().clientWidth,
            h = top.node().clientHeight,
            dx = Math.min(40, Math.round(w * 0.4 / arr.length)),
            dy = Math.min(40, Math.round(h * 0.4 / arr.length));
      let nx = Math.ceil(Math.sqrt(arr.length)), ny = nx;

      // calculate number of divisions for 'tile' sorting
      if ((nx > 1) && (nx * (nx - 1) >= arr.length)) {
         if (w > h)
            ny--;
         else
            nx--;
      }

      arr.forEach((frame, i) => {
         const main = select(frame.parentNode);
         if (kind === 'cascade') {
            main.style('left', (i * dx) + 'px')
                .style('top', (i * dy) + 'px')
                .style('width', Math.round(w * 0.58) + 'px')
                .style('height', Math.round(h * 0.58) + 'px');
         } else {
            main.style('left', Math.round(w / nx * (i % nx)) + 'px')
                .style('top', Math.round(h / ny * ((i - i % nx) / nx)) + 'px')
                .style('width', Math.round(w / nx - 4) + 'px')
                .style('height', Math.round(h / ny - 4) + 'px');
         }
         resize(frame);
      });
   }

   /** @summary context menu */
   showContextMenu(evnt, is_header) {
      // no context menu for no windows
      if (this.numDraw() === 0)
         return;
      // handle context menu only for MDI area or for window header
      if (!is_header && evnt.target.getAttribute('class') !== 'jsroot_flex_top')
         return;

      evnt.preventDefault();

      const arr = [];
      let nummin = 0;
      this.forEachFrame(f => {
         arr.push(f);
         if (this.getFrameState(f) === 'min')
            nummin++;
      });
      const active = this.getActiveFrame();

      arr.sort((f1, f2) => (select(f1).property('frame_cnt') < select(f2).property('frame_cnt') ? -1 : 1));

      createMenu(evnt, this).then(menu => {
         menu.header('Flex');
         menu.add('Cascade', () => this.sortFrames('cascade'), 'Cascade frames');
         menu.add('Tile', () => this.sortFrames('tile'), 'Tile all frames');
         if (nummin < arr.length)
            menu.add('Minimize all', () => this.minimizeAll(), 'Minimize all frames');
         if (nummin > 0)
            menu.add('Show all', () => this.showAll(), 'Restore minimized frames');
         menu.add('Close all', () => this.closeAllFrames());
         menu.separator();

         arr.forEach((f, i) => {
            menu.addchk((f === active), ((this.getFrameState(f) === 'min') ? '[min] ' : '') + select(f).attr('frame_title'), i, arg => {
               const frame = arr[arg];
               if (this.getFrameState(frame) === 'min')
                  this.changeFrameState(frame, 'normal');
               this.activateFrame(frame);
            });
         });

         menu.show();
      });
   }

} // class FlexibleDisplay


/**
 * @summary Batch MDI display
 *
 * @desc Can be used together with hierarchy painter in node.js
 * @private
 */

class BatchDisplay extends MDIDisplay {

   constructor(width, height, jsdom_body) {
      super('$batch$');
      this.frames = []; // array of configured frames
      this.width = width || settings.CanvasWidth;
      this.height = height || settings.CanvasHeight;
      this.jsdom_body = jsdom_body || select('body'); // d3 body handle
   }

   /** @summary Call function for each frame */
   forEachFrame(userfunc) {
      this.frames.forEach(userfunc);
   }

   /** @summary Create batch frame */
   createFrame(title) {
      this.beforeCreateFrame(title);

      const frame =
         this.jsdom_body.append('div')
             .style('visible', 'hidden')
             .attr('width', this.width).attr('height', this.height)
             .style('width', this.width + 'px').style('height', this.height + 'px')
             .attr('id', 'jsroot_batch_' + this.frames.length)
             .attr('frame_title', title);

      this.frames.push(frame.node());

      return this.afterCreateFrame(frame.node());
   }

   /** @summary Create final frame */
   createFinalBatchFrame() {
      const cnt = this.numFrames(), prs = [];

      for (let n = 0; n < cnt; ++n) {
         const json = this.makeJSON(n, 1, true);
         if (json)
            select(this.frames[n]).text('json:' + btoa_func(json));
         else
            prs.push(this.makeSVG(n, true));
      }

      return Promise.all(prs).then(() => {
         this.jsdom_body.append('div')
             .attr('id', 'jsroot_batch_final')
             .text(`${cnt}`);
      });
   }

   /** @summary Returns number of created frames */
   numFrames() { return this.frames.length; }

   /** @summary returns JSON representation if any
     * @desc Now works only for inspector, can be called once */
   makeJSON(id, spacing, keep_frame) {
      const frame = this.frames[id];
      if (!frame)
         return;
      const obj = select(frame).property('_json_object_');
      if (obj) {
         select(frame).property('_json_object_', null);
         cleanup(frame);
         if (!keep_frame)
            select(frame).remove();
         return toJSON(obj, spacing);
      }
   }

   /** @summary Create SVG for specified frame id - used in testing */
   makeSVG(id, keep_frame) {
      const frame = this.frames[id];
      if (!frame)
         return;
      const main = select(frame),
            mainsvg = main.select('svg');
      if (mainsvg.empty())
         return;

      const style_filter = mainsvg.style('filter');

      mainsvg.attr('xmlns', nsSVG)
             .attr('title', null).attr('style', null).attr('class', null).attr('x', null).attr('y', null);

      if (!mainsvg.attr('width') && !mainsvg.attr('height'))
         mainsvg.attr('width', this.width).attr('height', this.height);

      if (style_filter)
         mainsvg.style('filter', style_filter);

      function clear_element() {
         const elem = select(this);
         if (elem.style('display') === 'none')
            elem.remove();
      }

      main.selectAll('g.root_frame').each(clear_element);
      main.selectAll('svg').each(clear_element);

      if (internals.batch_png) {
         return svgToImage(compressSVG(main.html()), 'png').then(href => {
            select(this.frames[id]).text('png:' + href);
         });
      }

      if (keep_frame)
         return true;

      const svg = compressSVG(main.html());

      cleanup(frame);
      main.remove();
      return svg;
   }

} // class BatchDisplay


/**
  * @summary Special browser layout
  *
  * @desc Contains three different areas for browser (left), status line (bottom) and central drawing
  * Main application is normal browser, but also used in other applications like ROOT6 canvas
  * @private
  */

class BrowserLayout {

   #float_left;
   #float_top;
   #max_left;
   #max_top;
   #float_width;
   #float_height;
   #max_width;
   #max_height;
   #hsepar_position;
   #vsepar_position;
   #hsepar_move;
   #vsepar_move;

   /** @summary Constructor */
   constructor(id, hpainter, objpainter) {
      this.gui_div = id;
      this.hpainter = hpainter; // painter for browser area (if any)
      this.objpainter = objpainter; // painter for object area (if any)
      this.browser_kind = null; // should be 'float' or 'fix'
   }

   /** @summary Selects main element */
   main() { return select('#' + this.gui_div); }

   /** @summary Selects browser div */
   browser() { return this.main().select('.jsroot_browser'); }

   /** @summary Selects drawing div */
   drawing() { return select(`#${this.gui_div}_drawing`); }

   /** @summary Selects drawing div */
   status() { return select(`#${this.gui_div}_status`); }

   /** @summary Returns drawing divid */
   drawing_divid() { return this.gui_div + '_drawing'; }

   /** @summary Check resize action */
   checkResize() {
      if (isFunc(this.hpainter?.checkResize))
         this.hpainter.checkResize();
      else if (isFunc(this.objpainter?.checkResize))
         this.objpainter.checkResize(true);
   }

   /** @summary Create or update CSS style */
   createStyle() {
      const bkgr_color = settings.DarkMode ? 'black' : '#E6E6FA',
            title_color = settings.DarkMode ? '#ccc' : 'inherit',
            text_color = settings.DarkMode ? '#ddd' : 'inherit',
            input_style = settings.DarkMode ? `background-color: #222; color: ${text_color}` : '';

      injectStyle(
         '.jsroot_browser { pointer-events: none; position: absolute; left: 0px; top: 0px; bottom: 0px; right: 0px; margin: 0px; border: 0px; overflow: hidden; }' +
         `.jsroot_draw_area { background-color: ${bkgr_color}; overflow: hidden; margin: 0px; border: 0px; }` +
         `.jsroot_browser_area { color: ${text_color}; background-color: ${bkgr_color}; font-size: 12px; font-family: Verdana; pointer-events: all; box-sizing: initial; }` +
         `.jsroot_browser_area input { ${input_style} }` +
         `.jsroot_browser_area select { ${input_style} }` +
         `.jsroot_browser_title { font-family: Verdana; font-size: 20px; color: ${title_color}; }` +
         '.jsroot_browser_btns { pointer-events: all; display: flex; flex-direction: column; }' +
         '.jsroot_browser_area p { margin-top: 5px; margin-bottom: 5px; white-space: nowrap; }' +
         '.jsroot_browser_hierarchy { flex: 1; margin-top: 2px; }' +
         `.jsroot_status_area { background-color: ${bkgr_color}; overflow: hidden; font-size: 12px; font-family: Verdana; pointer-events: all; }` +
         '.jsroot_browser_resize { position: absolute; right: 3px; bottom: 3px; margin-bottom: 0px; margin-right: 0px; opacity: 0.5; cursor: se-resize; z-index: 1; }',
         this.main().node(), 'browser_layout_style');
   }

   /** @summary method used to create basic elements
     * @desc should be called only once */
   create(with_browser) {
      const main = this.main();

      main.append('div').attr('id', this.drawing_divid())
                        .classed('jsroot_draw_area', true)
                        .style('position', 'absolute')
                        .style('left', 0).style('top', 0).style('bottom', 0).style('right', 0);

      if (with_browser)
         main.append('div').classed('jsroot_browser', true);

      this.createStyle();
   }

   /** @summary Create buttons in the layout */
   createBrowserBtns() {
      const br = this.browser();
      if (br.empty())
         return;
      let btns = br.select('.jsroot_browser_btns');
      if (btns.empty()) {
         btns = br.append('div')
                  .attr('class', 'jsroot jsroot_browser_btns')
                  .attr('style', 'position: absolute; left: 7px; top: 7px');
      } else
         btns.html('');
      return btns;
   }

   /** @summary Remove browser buttons */
   removeBrowserBtns() {
      this.browser().select('.jsroot_browser_btns').remove();
   }

   /** @summary Set browser content */
   setBrowserContent(guiCode) {
      const main = this.browser();
      if (main.empty())
         return;

      main.insert('div', '.jsroot_browser_btns').classed('jsroot_browser_area', true)
           .style('position', 'absolute').style('left', '0px').style('top', '0px').style('bottom', '0px').style('width', '250px')
           .style('overflow', 'hidden')
           .style('padding-left', '5px')
           .style('display', 'flex').style('flex-direction', 'column')   /* use the flex model */
           .html(`<p class='jsroot_browser_title'>title</p><div class='jsroot_browser_resize' style='display:none'>&#9727</div>${guiCode}`);
   }

   /** @summary Check if there is browser content */
   hasContent() {
      const main = this.browser();
      return main.empty() ? false : !main.select('.jsroot_browser_area').empty();
   }

   /** @summary Delete content */
   deleteContent(keep_status) {
      const main = this.browser();
      if (main.empty())
         return;

      if (!keep_status)
         this.createStatusLine(0, 'delete');

      this.toggleBrowserVisisbility(true);

      if (keep_status) {
         // try to delete only content, not status
         main.select('.jsroot_browser_area').remove();
         main.select('.jsroot_browser_btns').remove();
         main.select('.jsroot_v_separator').remove();
      } else
         main.selectAll('*').remove();

      delete this.browser_visible;
      delete this.browser_kind;

      this.checkResize();
   }

   /** @summary Returns true when status line exists */
   hasStatus() {
      const main = this.browser();
      return main.empty() ? false : !this.status().empty();
   }

   /** @summary Set browser title text
     * @desc Title also used for dragging of the float browser */
   setBrowserTitle(title) {
      const main = this.browser(),
            elem = !main.empty() ? main.select('.jsroot_browser_title') : null;
      if (elem)
         elem.text(title).style('cursor', this.browser_kind === 'flex' ? 'move' : null);
      return elem;
   }

   /** @summary Toggle browser kind
     * @desc used together with browser buttons */
   toggleKind(browser_kind) {
      if (this.browser_visible !== 'changing') {
         if (browser_kind === this.browser_kind)
            this.toggleBrowserVisisbility();
         else
            this.toggleBrowserKind(browser_kind);
      }
   }

   /** @summary Creates status line */
   async createStatusLine(height, mode) {
      const main = this.browser();
      if (main.empty())
         return '';

      const id = this.gui_div + '_status',
            line = select('#' + id),
            is_visible = !line.empty();

      if (mode === 'toggle')
         mode = !is_visible;
      else if (mode === 'delete') {
         mode = false;
         height = 0;
         delete this.status_layout;
      } else if (mode === undefined) {
         mode = true;
         this.status_layout = 'app';
      }

      if (is_visible) {
         if (mode === true)
            return id;

         const hsepar = main.select('.jsroot_h_separator');

         hsepar.remove();
         line.remove();

         if (this.status_layout !== 'app')
            delete this.status_layout;

         if (this.status_handler && (internals.showStatus === this.status_handler)) {
            delete internals.showStatus;
            delete this.status_handler;
         }

         this.adjustSeparators(null, 0, true);
         return '';
      }

      if (mode === false)
         return '';

      const left_pos = this.drawing().style('left');

      main.insert('div', '.jsroot_browser_area')
          .attr('id', id)
          .classed('jsroot_status_area', true)
          .style('position', 'absolute').style('left', left_pos).style('height', '20px').style('bottom', '0px').style('right', '0px')
          .style('margin', 0).style('border', 0);

      const separ_color = settings.DarkMode ? 'grey' : 'azure',
            hsepar = main.insert('div', '.jsroot_browser_area')
                       .classed('jsroot_h_separator', true)
                       .attr('style', `pointer-events: all; border: 0; margin: 0; padding: 0; background-color: ${separ_color}; position: absolute; left: ${left_pos}; right: 0; bottom: 20px; height: 5px; cursor: ns-resize;`),

            drag_move = drag().on('start', () => {
               this.#hsepar_move = this.#hsepar_position;
               hsepar.style('background-color', 'grey');
            }).on('drag', evnt => {
               this.#hsepar_move -= evnt.dy; // hsepar is position from bottom
               this.adjustSeparators(null, Math.max(5, Math.round(this.#hsepar_move)));
            }).on('end', () => {
               this.#hsepar_move = undefined;
               hsepar.style('background-color', null);
               this.checkResize();
            });

      hsepar.call(drag_move);

      // need to get touches events handling in drag
      if (browser.touches && !main.on('touchmove'))
         main.on('touchmove', () => {});

      if (!height || isStr(height))
         height = this.last_hsepar_height || 20;

      this.adjustSeparators(null, height, true);

      if (this.status_layout === 'app')
         return id;

      this.status_layout = new GridDisplay(id, 'horizx4_1213');

      const frame_titles = ['object name', 'object title', 'mouse coordinates', 'object info'];
      for (let k = 0; k < 4; ++k) {
         select(this.status_layout.getGridFrame(k))
           .attr('title', frame_titles[k]).style('overflow', 'hidden').style('display', 'flex').style('align-items', 'center')
           .append('label').attr('style', 'margin: 5px 5px 5px 3px; font-size: 14px; white-space: nowrap;');
      }

      internals.showStatus = this.status_handler = this.showStatus.bind(this);

      return id;
   }

   /** @summary Adjust separator positions */
   adjustSeparators(vsepar, hsepar, redraw, first_time) {
      if (!this.gui_div)
         return;

      const main = this.browser(), w = 5;

      if ((hsepar === null) && first_time && !main.select('.jsroot_h_separator').empty()) {
         // if separator set for the first time, check if status line present
         hsepar = main.select('.jsroot_h_separator').style('bottom');
         if (isStr(hsepar) && (hsepar.length > 2) && (hsepar.indexOf('px') === hsepar.length - 2))
            hsepar = hsepar.slice(0, hsepar.length - 2);
         else
            hsepar = null;
      }

      if (hsepar !== null) {
         hsepar = parseInt(hsepar);
         const elem = main.select('.jsroot_h_separator');
         let hlimit = 0;

         if (!elem.empty()) {
            if (hsepar < 5)
               hsepar = 5;

            const maxh = main.node().clientHeight - w;
            if (maxh > 0) {
               if (hsepar < 0)
                  hsepar += maxh;
               if (hsepar > maxh)
                  hsepar = maxh;
            }

            this.last_hsepar_height = hsepar;
            elem.style('bottom', hsepar + 'px').style('height', w + 'px');
            this.status().style('height', hsepar + 'px');
            hlimit = hsepar + w;
         }

         this.#hsepar_position = hsepar;

         this.drawing().style('bottom', `${hlimit}px`);
      }

      if (vsepar !== null) {
         vsepar = Math.max(50, Number.parseInt(vsepar));
         this.#vsepar_position = vsepar;
         main.select('.jsroot_browser_area').style('width', (vsepar - 5) + 'px');
         this.drawing().style('left', (vsepar + w) + 'px');
         main.select('.jsroot_h_separator').style('left', (vsepar + w) + 'px');
         this.status().style('left', (vsepar + w) + 'px');
         main.select('.jsroot_v_separator').style('left', vsepar + 'px').style('width', w + 'px');
      }

      if (redraw)
         this.checkResize();
   }

   /** @summary Show status information inside special fields of browser layout */
   showStatus(...msgs) {
      if (!isObject(this.status_layout) || !isFunc(this.status_layout.getGridFrame))
         return;

      let maxh = 0;
      for (let n = 0; n < 4; ++n) {
         const lbl = this.status_layout.getGridFrame(n).querySelector('label');
         maxh = Math.max(maxh, lbl.clientHeight);
         lbl.innerText = msgs[n] || '';
      }

      if (!this.status_layout.first_check) {
         this.status_layout.first_check = true;
         if ((maxh > 5) && ((maxh > this.last_hsepar_height) || (maxh < this.last_hsepar_height + 5)))
            this.adjustSeparators(null, maxh, true);
      }
   }

   /** @summary Toggle browser visibility */
   toggleBrowserVisisbility(fast_close) {
      if (!this.gui_div || isStr(this.browser_visible))
         return;

      const main = this.browser(),
            area = main.select('.jsroot_browser_area');
      if (area.empty())
         return;

      const vsepar = main.select('.jsroot_v_separator'),
            drawing = select(`#${this.gui_div}_drawing`);
      let tgt = area.property('last_left'),
          tgt_separ = area.property('last_vsepar'),
          tgt_drawing = area.property('last_drawing');

      if (!this.browser_visible) {
         if (fast_close)
            return;
         area.property('last_left', null).property('last_vsepar', null).property('last_drawing', null);
      } else {
         area.property('last_left', area.style('left'));
         if (!vsepar.empty()) {
            area.property('last_vsepar', vsepar.style('left'));
            area.property('last_drawing', drawing.style('left'));
         }

         tgt = (-area.node().clientWidth - 10) + 'px';
         const mainw = main.node().clientWidth;

         if (vsepar.empty() && (area.node().offsetLeft > mainw / 2))
            tgt = (mainw + 10) + 'px';

         tgt_separ = '-10px';
         tgt_drawing = '0px';
      }

      const visible_at_the_end = !this.browser_visible, _duration = fast_close ? 0 : 700;

      this.browser_visible = 'changing';

      area.transition().style('left', tgt).duration(_duration).on('end', () => {
         if (fast_close)
            return;
         this.browser_visible = visible_at_the_end;
         if (visible_at_the_end)
            this.setButtonsPosition();
      });

      if (!visible_at_the_end)
         main.select('.jsroot_browser_btns').transition().style('left', '7px').style('top', '7px').duration(_duration);

      if (!vsepar.empty()) {
         vsepar.transition().style('left', tgt_separ).duration(_duration);
         drawing.transition().style('left', tgt_drawing).duration(_duration).on('end', this.checkResize.bind(this));
      }

      if (this.status_layout && (this.browser_kind === 'fix')) {
         main.select('.jsroot_h_separator').transition().style('left', tgt_drawing).duration(_duration);
         main.select('.jsroot_status_area').transition().style('left', tgt_drawing).duration(_duration);
      }
   }

   /** @summary Adjust browser size */
   adjustBrowserSize(onlycheckmax) {
      if (!this.gui_div || (this.browser_kind !== 'float'))
         return;

      const main = this.browser();
      if (main.empty())
         return;

      const area = main.select('.jsroot_browser_area'),
            cont = main.select('.jsroot_browser_hierarchy'),
            chld = select(cont.node().firstChild);

      if (onlycheckmax) {
         if (area.node().parentNode.clientHeight - 10 < area.node().clientHeight)
            area.style('bottom', '0px').style('top', '0px');
         return;
      }

      if (chld.empty())
         return;

      const h1 = cont.node().clientHeight,
            h2 = chld.node().clientHeight;
      if ((h2 !== undefined) && (h2 < h1 * 0.7))
         area.style('bottom', '');
   }

   /** @summary Set buttons position */
   setButtonsPosition() {
      if (!this.gui_div)
         return;

      const main = this.browser(),
            btns = main.select('.jsroot_browser_btns');
      if (btns.empty())
         return;

      let top = 7, left = 7;
      if (this.browser_visible) {
         const area = main.select('.jsroot_browser_area');
         top = area.node().offsetTop + 7;
         left = area.node().offsetLeft - main.node().offsetLeft + area.node().clientWidth - 27;
      }

      btns.style('left', `${left}px`).style('top', `${top}px`);
   }

   /** @summary Toggle browser kind */
   async toggleBrowserKind(kind) {
      if (!this.gui_div)
         return null;

      if (!kind) {
         if (!this.browser_kind)
            return null;
         kind = (this.browser_kind === 'float') ? 'fix' : 'float';
      }

      const main = this.browser(),
            area = main.select('.jsroot_browser_area');

      if (this.browser_kind === 'float') {
         area.style('bottom', '0px')
             .style('top', '0px')
             .style('width', '')
             .style('height', '')
             .classed('jsroot_float_browser', false)
             .style('border', null);
      } else if (this.browser_kind === 'fix') {
         main.select('.jsroot_v_separator').remove();
         area.style('left', '0px');
         this.drawing().style('left', '0px'); // reset size
         main.select('.jsroot_h_separator').style('left', '0px');
         this.status().style('left', '0px'); // reset left
         this.checkResize();
      }

      this.browser_kind = kind;
      this.browser_visible = true;

      main.select('.jsroot_browser_resize').style('display', (kind === 'float') ? null : 'none');
      main.select('.jsroot_browser_title').style('cursor', (kind === 'float') ? 'move' : null);

      if (kind === 'float') {
         area.style('bottom', '40px')
             .classed('jsroot_float_browser', true)
             .style('border', 'solid 3px white');

         const drag_move = drag().on('start', () => {
            const sl = area.style('left'), st = area.style('top');
            this.#float_left = parseInt(sl.slice(0, sl.length - 2));
            this.#float_top = parseInt(st.slice(0, st.length - 2));
            this.#max_left = Math.max(0, main.node().clientWidth - area.node().offsetWidth - 1);
            this.#max_top = Math.max(0, main.node().clientHeight - area.node().offsetHeight - 1);
         }).filter(evnt => {
            return main.select('.jsroot_browser_title').node() === evnt.target;
         }).on('drag', evnt => {
            this.#float_left += evnt.dx;
            this.#float_top += evnt.dy;
            area.style('left', Math.min(Math.max(0, this.#float_left), this.#max_left) + 'px')
                .style('top', Math.min(Math.max(0, this.#float_top), this.#max_top) + 'px');
            this.setButtonsPosition();
         });

         // eslint-disable-next-line one-var
         const drag_resize = drag().on('start', () => {
            const sw = area.style('width');
            this.#float_width = parseInt(sw.slice(0, sw.length - 2));
            this.#float_height = area.node().clientHeight;
            this.#max_width = main.node().clientWidth - area.node().offsetLeft - 1;
            this.#max_height = main.node().clientHeight - area.node().offsetTop - 1;
         }).on('drag', evnt => {
            this.#float_width += evnt.dx;
            this.#float_height += evnt.dy;

            area.style('width', Math.min(Math.max(100, this.#float_width), this.#max_width) + 'px')
            .style('height', Math.min(Math.max(100, this.#float_height), this.#max_height) + 'px');

            this.setButtonsPosition();
         });

         main.call(drag_move);
         main.select('.jsroot_browser_resize').call(drag_resize);

         this.adjustBrowserSize();
      } else {
         area.style('left', '0px').style('top', '0px').style('bottom', '0px').style('height', null);

         const separ_color = settings.DarkMode ? 'grey' : 'azure',
               vsepar = main.append('div').classed('jsroot_v_separator', true)
                           .attr('style', `pointer-events: all; border: 0; margin: 0; padding: 0; background-color: ${separ_color}; position: absolute; top: 0; bottom: 0; cursor: ew-resize;`);

         // eslint-disable-next-line one-var
         const drag_move = drag().on('start', () => {
            this.#vsepar_move = this.#vsepar_position;
            vsepar.style('background-color', 'grey');
         }).on('drag', evnt => {
            this.#vsepar_move += evnt.dx;
            this.setButtonsPosition();
            settings.BrowserWidth = Math.max(50, Math.round(this.#vsepar_move));
            this.adjustSeparators(settings.BrowserWidth, null);
         }).on('end', () => {
            this.#vsepar_move = undefined;
            vsepar.style('background-color', null);
            this.checkResize();
         });

         vsepar.call(drag_move);

         // need to get touches events handling in drag
         if (browser.touches && !main.on('touchmove'))
            main.on('touchmove', () => {});

         this.adjustSeparators(settings.BrowserWidth, null, true, true);
      }

      this.setButtonsPosition();

      return this;
   }

} // class BrowserLayout

const clTButton = 'TButton', kIsGrayscale = BIT(22),
      // identifier used in TWebCanvas painter
      webSnapIds = { kObject: 1, kSVG: 2, kSubPad: 3, kColors: 4, kStyle: 5, kFont: 6 };

// eslint-disable-next-line one-var
const PadButtonsHandler = {

   getButtonSize(fact) {
      const cp = this.getCanvPainter();
      return Math.round((fact || 1) * (cp?.getPadScale() || 1) * (cp === this ? 16 : 12));
   },

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

      const group = this.getLayerSvg('btns_layer'),
            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;
            this.btns_active_flag = true;
            break;
         case 'enterbtn':
            this.btns_active_flag = true;
            return; // do nothing, just cleanup timeout
         case 'timeout':
            break;
         case 'toggle':
            state = !state;
            btn.property('buttons_state', state);
            is_visible = state;
            break;
         case 'disable':
         case 'leavebtn':
            this.btns_active_flag = false;
            if (!state)
               btn.property('timout_handler', setTimeout(() => this.toggleButtonsVisibility('timeout'), 1200));
            return;
      }

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


   alignButtons(btns, width, height) {
      const sz0 = this.getButtonSize(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');
      let found_func = '';
      if (!group.empty()) {
         group.selectAll('svg').each(function() {
            if (select(this).attr('key') === keyname)
               found_func = select(this).attr('name');
         });
      }
      return found_func;
   },

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

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

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

      const istop = this.isTopPad(), y = 0;
      let ctrl, x = group.property('leftside') ? this.getButtonSize(1.25) : 0;

      if (this.isFastDrawing()) {
         ctrl = ToolbarIcons.createSVG(group, ToolbarIcons.circle, this.getButtonSize(), '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, this.getButtonSize(), 'Toggle tool buttons', false)
                            .attr('name', 'Toggle').attr('x', 0).attr('y', 0)
                            .property('buttons_state', (settings.ToolBar !== 'popup') || browser.touches)
                            .on('click', evnt => this.toggleButtonsVisibility('toggle', evnt));
         ctrl.node()._mouseenter = () => this.toggleButtonsVisibility('enable');
         ctrl.node()._mouseleave = () => this.toggleButtonsVisibility('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, this.getButtonSize(),
                                               item.tooltip + (istop ? '' : (` on pad ${this.getPadName()}`)) + (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 = () => this.toggleButtonsVisibility('enterbtn');
            svg.node()._mouseleave = () => this.toggleButtonsVisibility('leavebtn');

            x += this.getButtonSize(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


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

   const obj = { _typename: 'TWebObjectOptions', snapid: painter.getSnapId(), 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 {

   #iscan;      // is canvas flag
   #pad_name;   // name of the pad
   #pad;        // TPad object
   #painters;   // painters in the pad
   #pad_scale;  // scale factor of the pad
   #pad_x;      // pad x coordinate
   #pad_y;      // pad y coordinate
   #pad_width;  // pad width
   #pad_height; // pad height
   #doing_draw; // drawing handles
   #pad_draw_disabled; // disable drawing of the pad
   #last_grayscale; // grayscale change flag
   #custom_palette; // custom palette
   #custom_colors;  // custom colors
   #custom_palette_indexes; // custom palette indexes
   #custom_palette_colors; // custom palette colors
   #frame_painter_ref; // frame painter
   #main_painter_ref; // main painter on the pad
   #snap_primitives; // stored snap primitives from web canvas
   #has_execs; // indicate is pad has TExec objects assigned
   #deliver_move_events; // deliver move events to server
   #readonly; // if changes on pad is not allowed
   #num_primitives; // number of primitives
   #num_specials;  // number of special objects - if counted
   #auto_color_cnt; // counter used in assigning auto colors
   #auto_palette; // palette for creating of automatic colors
   #fixed_size; // fixed size flag
   #has_canvas; // indicate if top canvas painter exists
   #fast_drawing; // fast drawing flag
   #resize_tmout; // timeout handle for resize
   #start_draw_tm;  // time when start drawing primitives

   /** @summary constructor
     * @param {object|string} dom - DOM element for drawing or element id
     * @param {object} pad - TPad object to draw
     * @param {String} [opt] - draw option
     * @param {boolean} [iscan] - if TCanvas object
     * @param [add_to_primitives] - add pad painter to canvas
     * */
   constructor(dom, pad, opt, iscan, add_to_primitives) {
      super(dom, pad);
      this.#pad = pad;
      this.#iscan = iscan; // indicate if working with canvas
      this.#pad_name = '';
      if (!iscan && pad?.fName) {
         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.#pad_name) || ((this.#pad_name === 'button') && (pad._typename === clTButton)))
            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;

      if (opt !== undefined)
         this.decodeOptions(opt);

      if (add_to_primitives) {
         if ((add_to_primitives !== 'webpad') && this.getCanvSvg().empty()) {
            // one can draw pad without canvas
            this.#has_canvas = false;
            this.#pad_name = '';
            this.setTopPainter();
         } else {
            // pad painter will be registered in the parent pad
            this.addToPadPrimitives();
         }
      }

      if (pad?.$disable_drawing)
         this.#pad_draw_disabled = true;
   }

   /** @summary returns pad painter
    * @protected */
   getPadPainter() { return this.isTopPad() ? null : super.getPadPainter(); }

   /** @summary returns canvas painter
     * @protected */
   getCanvPainter(try_select) { return this.isTopPad() ? this : super.getCanvPainter(try_select); }

   /** @summary Returns pad name
     * @protected */
   getPadName() { return this.#pad_name; }

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

      if (isBatchMode())
         return true;

      return this.isTopPad() ? false : this.getCanvPainter()?.isBatchMode();
   }

   /** @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 true if button */
   isButton() { return this.matchObjectType(clTButton); }

   /** @summary Returns true if read-only mode is enabled */
   isReadonly() { return this.#readonly; }

   /** @summary Returns true if it is canvas
    * @param {Boolean} [is_online = false] - if specified, checked if it is canvas with configured connection to server */
   isCanvas(is_online = false) {
      if (!this.#iscan)
         return false;
      if (is_online === true)
         return isFunc(this.getWebsocket) && this.getWebsocket();
      return isStr(is_online) ? this.#iscan === is_online : true;
   }

   /** @summary Returns true if it is canvas or top pad without canvas */
   isTopPad() { return this.isCanvas() || !this.#has_canvas; }

   /** @summary Canvas main svg element
     * @return {object} d3 selection with canvas svg
     * @protected */
   getCanvSvg() { return this.selectDom().select('.root_canvas'); }

   /** @summary Pad svg element
     * @return {object} d3 selection with pad svg
     * @protected */
   getPadSvg() {
      const c = this.getCanvSvg();
      if (!this.#pad_name || c.empty())
         return c;

      return c.select('.primitives_layer .__root_pad_' + this.#pad_name);
   }

   /** @summary Method selects immediate layer under canvas/pad main element
     * @param {string} name - layer name lik 'primitives_layer', 'btns_layer', 'info_layer'
     * @protected */
   getLayerSvg(name) { return this.getPadSvg().selectChild('.' + name); }

   /** @summary Returns svg element for the frame in current pad
     * @protected */
   getFrameSvg() {
      const layer = this.getLayerSvg('primitives_layer');
      if (layer.empty())
         return layer;
      let node = layer.node().firstChild;
      while (node) {
         const elem = select(node);
         if (elem.classed('root_frame'))
            return elem;
         node = node.nextSibling;
      }
      return select(null);
   }

   /** @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.getPadSvg();
      if (!svg_p.empty()) {
         svg_p.property('pad_painter', null);
         if (!this.isCanvas())
            svg_p.remove();
      }

      this.#main_painter_ref = undefined;
      this.#frame_painter_ref = undefined;
      this.#pad_x = this.#pad_y = this.#pad_width = this.#pad_height = undefined;
      this.#doing_draw = undefined;
      this.#snap_primitives = undefined;
      this.#last_grayscale = undefined;
      this.#custom_palette = this.#custom_colors = this.#custom_palette_indexes = this.#custom_palette_colors = undefined;

      this.#painters = [];
      this.#pad = undefined;
      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 Assign actual frame painter
     * @private */
   setFramePainter(fp, on) {
      if (on)
         this.#frame_painter_ref = fp;
      else if (this.#frame_painter_ref === fp)
         this.#frame_painter_ref = undefined;
   }

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

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

   /** @summary get pad height */
   getPadScale() { return this.#pad_scale || 1; }

   /** @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 is_any = false;

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

      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, prim;
      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.getMainPainter() === painter) {
            this.setMainPainter(undefined, true);
            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(no_recursion) {
      return this.#custom_palette || (no_recursion ? null : this.getCanvPainter()?.getCustomPalette(true));
   }

   _getCustomPaletteIndexes() { return this.#custom_palette_indexes; }

   /** @summary Provides automatic color
    * @desc Uses ROOT colors palette if possible
    * @private */
   getAutoColor(numprimitives) {
      numprimitives = Math.max(numprimitives || (this.#num_primitives || 5) - (this.#num_specials || 0), 2);

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

      let indexes = this._getCustomPaletteIndexes();
      if (!indexes) {
         const cp = this.getCanvPainter();
         if ((cp !== this) && isFunc(cp?._getCustomPaletteIndexes))
            indexes = cp._getCustomPaletteIndexes();
      }

      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 Returns number of painters
     * @protected */
   getNumPainters() { return this.#painters.length; }

   /** @summary Add painter to pad list of painters
     * @protected */
   addToPrimitives(painter) {
      if (this.#painters.indexOf(painter) < 0)
         this.#painters.push(painter);
      return this;
   }

   /** @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 canp = this.isTopPad() ? this : this.getCanvPainter();

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

      if (pos && !this.isTopPad())
         pos = getAbsPosInCanvas(this.getPadSvg(), 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.isCanvas() ? this.getCanvSvg().selectChild('.canvas_fillrect') : this.getPadSvg().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 = !this.hasSnapId() && settings.SmallPad && ((w < settings.SmallPad.width) || (h < settings.SmallPad.height));
      if (was_fast !== this.#fast_drawing)
         this.showPadButtons();
   }

   /** @summary Return fast drawing flag
     * @private */
   isFastDrawing() { return this.#fast_drawing; }

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

   /** @summary Returns true if default pad range is configured
     * @private */
   isDefaultPadRange() {
      if (!this.#pad)
         return true;
      return (this.#pad.fX1 === 0) && (this.#pad.fX2 === 1) && (this.#pad.fY1 === 0) && (this.#pad.fY2 === 1);
   }

   /** @summary Set grayscale mode for the canvas
     * @private */
   setGrayscale(flag) {
      if (!this.isTopPad())
         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 => {
            if (isFunc(p.clearHistPalette))
               p.clearHistPalette();
         });
      }

      this.setColors(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 Set fixed-size canvas
    * @private */
   _setFixedSize(on) { this.#fixed_size = on; }

   /** @summary Create SVG element for canvas */
   createCanvasSvg(check_resize, new_size) {
      const is_batch = this.isBatchMode(), lmt = 5;
      let factor, svg, rect, 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');

         info = this.getLayerSvg('info_layer');
         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)
            svg.style('user-select', settings.UserSelect || null);

         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)) {
         if (!this.hasSnapId()) {
            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);

      this.#pad_scale = settings.CanvasScale || 1;
      this.#pad_x = 0;
      this.#pad_y = 0;
      this.#pad_width = rect.width * this.#pad_scale;
      this.#pad_height = rect.height * this.#pad_scale;

      svg.attr('viewBox', `0 0 ${this.#pad_width} ${this.#pad_height}`)
         .attr('preserveAspectRatio', 'none')  // we do not preserve relative ratio
         .property('height_factor', factor)
         .property('draw_x', this.#pad_x)
         .property('draw_y', this.#pad_y)
         .property('draw_width', this.#pad_width)
         .property('draw_height', this.#pad_height);

      this.addPadBorder(svg, frect);

      this.setFastDrawing(this.#pad_width * (1 - this.#pad.fLeftMargin - this.#pad.fRightMargin), this.#pad_height * (1 - this.#pad.fBottomMargin - this.#pad.fTopMargin));

      if (this.alignButtons && btns)
         this.alignButtons(btns, this.#pad_width, this.#pad_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(this.#pad_height * (1 - gStyle.fDateY)),
               date = new Date();
         let posx = Math.round(this.#pad_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');
      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.isTopPad())
         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 online canvas itself for enlarge
      if (is_dblclick && this.isCanvas(true) && (this.enlargeMain('state') === 'off'))
         return;

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

      if (this.isTopPad() || (!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.isTopPad()) {
         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.getPadSvg();
         svg_border = svg_pad.selectChild('.root_pad_border');
         if (!is_batch)
            btns = this.getLayerSvg('btns_layer');
         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.#pad_name, true)
             .attr('pad', 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.#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_scale = this.getCanvPainter().getPadScale();
      this.#pad_x = x;
      this.#pad_y = y;
      this.#pad_width = w;
      this.#pad_height = h;

      this.addPadBorder(svg_pad, svg_border, true);

      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$1.Embed3D.Overlay) {
         this.selectDom().select('.draw3d_' + this.#pad_name)
              .style('display', pad_visible ? '' : 'none');
      }

      if (this.alignButtons && btns)
         this.alignButtons(btns, this.#pad_width, this.#pad_height);

      return pad_visible;
   }

   /** @summary Add border decorations
     * @private */
   addPadBorder(svg_pad, svg_border, draw_line) {
      if (!svg_border)
         return;

      svg_border.attr('d', `M0,0H${this.#pad_width}V${this.#pad_height}H0Z`)
                .call(this.fillatt.func);
      if (draw_line)
         svg_border.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 arr = getBoxDecorations(0, 0, this.#pad_width, this.#pad_height, this.#pad.fBorderMode, this.#pad.fBorderSize, this.#pad.fBorderSize);

         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', arr[0])
                    .call(this.fillatt.func)
                    .style('fill', rgb(this.fillatt.color).brighter(0.5).formatRgb());
         svg_border2.attr('d', arr[1])
                    .call(this.fillatt.func)
                    .style('fill', rgb(this.fillatt.color).darker(0.5).formatRgb());
      } else {
         svg_border1.remove();
         svg_border2.remove();
      }
   }

   /** @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.isCanvas() || !this.isEditable())
         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,
         is_disabled: kind => svg_can.property('pad_enlarged') || this.btns_active_flag ||
                             (kind === 'move' && (this.options._disable_dragging || this.getFramePainter()?.mode3d)),
         getDrawG: () => this.getPadSvg(),
         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 => {
               const subpad = p.getRootPad();
               subpad.fAbsXlowNDC += (subpad.fAbsXlowNDC - x0) * (scale_w - 1) + shift_x;
               subpad.fAbsYlowNDC += (subpad.fAbsYlowNDC - y0) * (scale_h - 1) + shift_y;
               subpad.fAbsWNDC *= scale_w;
               subpad.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.isTopPad()) {
         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;
      }

      const o = this.getOptions(true);

      if ((obj._typename === clTObjArray) && (obj.name === 'ListOfColors')) {
         if (o?.CreatePalette) {
            let arr = [];
            for (let n = obj.arr.length - o.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.length)
               this.#custom_palette = new ColorPalette(arr);
         }

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

         // copy existing colors and extend with new values
         this.#custom_colors = o?.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 = (!o || (!missing && !o.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(s => match((s.fKind === webSnapIds.kObject) ? s.fSnapshot : null));

      return snap ? snap.fSnapshot : 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)
         this.#doing_draw = undefined;
      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.isCanvas())
            this.#start_draw_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_draw_tm) {
            const spenttm = new Date().getTime() - this.#start_draw_tm;
            if (spenttm > 1000)
               console.log(`Canvas ${this.#pad?.fName || '---'} drawing took ${(spenttm * 1e-3).toFixed(2)}s`);
            this.#start_draw_tm = undefined;
         }

         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) {
      let color = this.#pad.fFillColor;
      if (!use_existing) {
         if (color < 15)
            color = 19;
         else if (color < 20)
            color--;
      }

      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$1(clTList);
      this.#pad.fPrimitives.Clear();

      if ((!nx && !ny) || !this.#pad.Divide(nx, ny, 0.01, 0.01, color))
         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.getRootPad()?.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) {
      const pad = this.getRootPad(true);
      if (!pad)
         return false;

      menu.header(`${pad._typename}::${pad.fName}`, `${urlClassPrefix}${pad._typename}.html`);

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

      menu.addchk(pad.fGridx, 'Grid x', flag => {
         pad.fGridx = flag ? 1 : 0;
         this.interactiveRedraw('pad', `exec:SetGridx(${flag ? 1 : 0})`);
      });
      menu.addchk(pad.fGridy, 'Grid y', flag => {
         pad.fGridy = flag ? 1 : 0;
         this.interactiveRedraw('pad', `exec:SetGridy(${flag ? 1 : 0})`);
      });
      menu.sub('Ticks x');
      menu.addchk(pad.fTickx === 0, 'normal', () => {
         pad.fTickx = 0;
         this.interactiveRedraw('pad', 'exec:SetTickx(0)');
      });
      menu.addchk(pad.fTickx === 1, 'ticks on both sides', () => {
         pad.fTickx = 1;
         this.interactiveRedraw('pad', 'exec:SetTickx(1)');
      });
      menu.addchk(pad.fTickx === 2, 'labels on both sides', () => {
         pad.fTickx = 2;
         this.interactiveRedraw('pad', 'exec:SetTickx(2)');
      });
      menu.endsub();
      menu.sub('Ticks y');
      menu.addchk(pad.fTicky === 0, 'normal', () => {
         pad.fTicky = 0;
         this.interactiveRedraw('pad', 'exec:SetTicky(0)');
      });
      menu.addchk(pad.fTicky === 1, 'ticks on both sides', () => {
         pad.fTicky = 1;
         this.interactiveRedraw('pad', 'exec:SetTicky(1)');
      });
      menu.addchk(pad.fTicky === 2, 'labels on both sides', () => {
         pad.fTicky = 2;
         this.interactiveRedraw('pad', 'exec:SetTicky(2)');
      });
      menu.endsub();

      menu.addchk(pad.fEditable, 'Editable', flag => {
         pad.fEditable = flag;
         this.interactiveRedraw('pad', `exec:SetEditable(${flag})`);
      });

      if (this.isCanvas()) {
         menu.addchk(pad.TestBit(kIsGrayscale), 'Gray scale', flag => {
            this.setGrayscale(flag);
            this.interactiveRedraw('pad', `exec:SetGrayscale(${flag})`);
         });
      }

      menu.sub('Border');
      menu.addSelectMenu('Mode', ['Down', 'Off', 'Up'], pad.fBorderMode + 1, v => {
         pad.fBorderMode = v - 1;
         this.interactiveRedraw(true, `exec:SetBorderMode(${v - 1})`);
      }, 'Pad border mode');
      menu.addSizeMenu('Size', 0, 20, 2, pad.fBorderSize, v => {
         pad.fBorderSize = v;
         this.interactiveRedraw(true, `exec:SetBorderSize(${v})`);
      }, 'Pad border size');
      menu.endsub();

      menu.addAttributesMenu(this);

      if (!this.isCanvas(true)) {
         // if not online canvas -
         const do_divide = arg => {
            if (!arg || !isStr(arg))
               return;
            // delete auto_canvas flag to prevent deletion
            if (this.#iscan === 'auto')
               this.#iscan = true;
            this.cleanPrimitives(true);
            if (arg === 'reset')
               return;
            const arr = arg.split('x');
            if (arr.length === 1)
               this.divide(Number.parseInt(arr[0]));
            else if (arr.length === 2)
               this.divide(Number.parseInt(arr[0]), Number.parseInt(arr[1]));
         };

         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', 'reset'].forEach(item => menu.add(item, item, do_divide));
         menu.endsub();

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

         if (this.isCanvas()) {
            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.isTopPad() && this.hasObjectsToDraw()))
         menu.addchk(this.isPadEnlarged(), 'Enlarge ' + (this.isCanvas() ? 'canvas' : 'pad'), () => this.enlargePad());

      const fname = this.#pad_name || (this.isCanvas() ? '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.isCanvas(), `${fname}.${fmt}`)));
      if (this.isCanvas()) {
         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 || isPadPainter(sub))
               res = sub.redraw(reason);

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

      return sync_promise.then(() => {
         if (this.isCanvas())
            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.getPadSvg();
      if (!elem.empty() && elem.property('can3d') === constants$1.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 || !this.isTopPad())
         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.isCanvas() && this.#pad && this.online_canvas && !this.embed_canvas && !this.isBatchMode()) {
            if (this.#resize_tmout)
               clearTimeout(this.#resize_tmout);
            this.#resize_tmout = setTimeout(() => {
               this.#resize_tmout = undefined;
               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) {
      const pad = this.getRootPad();
      if (!obj || !pad)
         return false;

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

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

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

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

      // this is main coordinates for sub-pad relative to canvas
      pad.fAbsWNDC = obj.fAbsWNDC;
      pad.fAbsHNDC = obj.fAbsHNDC;
      pad.fAbsXlowNDC = obj.fAbsXlowNDC;
      pad.fAbsYlowNDC = obj.fAbsYlowNDC;

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

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

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

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

      const fp = this.getFramePainter();
      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$1(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$1(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.hasSnapId()) {
         // keep snap id in painter, will be used for the
         if (this.#painters.indexOf(objpainter) < 0)
            this.#painters.push(objpainter);

         objpainter.assignSnapId(lst[indx].fObjectID);
         const setSubSnaps = p => {
            if (!p.isPrimary())
               return;
            for (let k = 0; k < this.#painters.length; ++k) {
               const sub = this.#painters[k];
               if (sub.isSecondary(p) && sub.getSecondaryId()) {
                  sub.assignSnapId(p.getSnapId() + '#' + sub.getSecondaryId());
                  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),
            o = this.getOptions(true);

      // set global list of colors
      if (!o || o.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 = o?.LocalColors ? colors : null;

      // set palette
      if (snap.fSnapshot.fBuf && (!o || !o.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
         this.#custom_palette = this.#custom_palette_indexes = this.#custom_palette_colors = undefined;
   }

   /** @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, pindx, indx) {
      if (indx === undefined) {
         indx = -1;
         this.#num_primitives = lst?.length ?? 0;
      }

      ++indx; // change to the next snap

      if (!lst || (indx >= lst.length))
         return this;

      const snap = lst[indx], is_subpad = (snap.fKind === webSnapIds.kSubPad);

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

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

      // try to locate existing object painter, only allowed when redrawing pad snap
      let objpainter, promise;

      while ((pindx !== undefined) && (pindx < this.#painters.length)) {
         const subp = this.#painters[pindx++];
         if (subp.getSnapId() === snap.fObjectID) {
            objpainter = subp;
            break;
         } else if (subp.getSnapId() && !subp.isSecondary() && !is_subpad) {
            console.warn(`Mismatch in snapid between painter ${subp.getSnapId()} secondary: ${subp.isSecondary()} type: ${subp.getClassName()} and primitive ${snap.fObjectID} kind ${snap.fKind} type ${snap.fSnapshot?._typename}`);
            break;
         }
      }

      if (objpainter) {
         // painter exists - try to update drawing
         if (is_subpad)
            promise = objpainter.redrawPadSnap(snap);
         else 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();
         }
      } else if (is_subpad) {
         const subpad = snap.fSnapshot;

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

         const padpainter = new TPadPainter(this, subpad, snap.fOption, false, 'webpad');
         padpainter.assignSnapId(snap.fObjectID);
         padpainter.is_active_pad = Boolean(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

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

         padpainter.createPadSvg();

         if (padpainter.matchObjectType(clTPad) && snap.fPrimitives.length)
            padpainter.addPadButtons(true);
         pindx++; // new painter will be add
         promise = padpainter.drawNextSnap(snap.fPrimitives).then(() => padpainter.addPadInteractive());
      } else if (((snap.fKind === webSnapIds.kObject) || (snap.fKind === webSnapIds.kSVG)) && (snap.fOption !== '__ignore_drawing__')) {
         // here the case of normal drawing
         pindx++; // new painter will be add
         promise = this.drawObject(this, snap.fSnapshot, snap.fOption).then(objp => this.addObjectPainter(objp, lst, indx));
      }

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

   /** @summary Return painter with specified id
     * @private */
   findSnap(snapid) {
      if (this.getSnapId() === 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.getSnapId() !== 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 = Boolean(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_move_events = this.#has_execs || first.fExecs?.arr?.length;

      if (!this.hasSnapId()) {
         // first time getting snap, create all gui elements first

         this.assignSnapId(snap.fObjectID);

         this.assignObject(first);
         this.#pad = first; // first object is pad

         // this._setFixedSize(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._setFixedSize(false);

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

         if (!this.isBatchMode() && this.online_canvas && !this.use_openui && !this.brlayout && mainid && isStr(mainid) && !getHPainter()) {
            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.isCanvas())
         this.createCanvasSvg(2);
      else
         this.createPadSvg(true);

      let missmatch = false;

      // match painters with new list of primitives
      if (!snap.fWithoutPrimitives) {
         let i = 0, k = 0;
         while (k < this.#painters.length) {
            const sub = this.#painters[k];

            // skip check secondary painters or painters without snapid
            if (!sub.hasSnapId() || sub.isSecondary()) {
               k++;
               continue; // look only for painters with snapid
            }

            if (i >= snap.fPrimitives.length)
               break;

            const prim = snap.fPrimitives[i];

            // only real objects drawing checked for existing painters
            if ((prim.fKind !== webSnapIds.kSubPad) && (prim.fKind !== webSnapIds.kObject) && (prim.fKind !== webSnapIds.kSVG)) {
               i++;
               continue; // look only for primitives of real objects
            }

            if (prim.fObjectID === sub.getSnapId()) {
               i++;
               k++;
            } else {
               missmatch = true;
               break;
            }
         }

         let cnt = 1000;
         // remove painters without primitives, limit number of checks
         while (!missmatch && (k < this.#painters.length) && (--cnt >= 0)) {
            if (this.removePrimitive(k) === -111)
               missmatch = true;
         }
         if (cnt < 0)
            missmatch = true;
      }

      if (missmatch) {
         const old_painters = this.#painters;
         this.#painters = [];
         old_painters.forEach(objp => objp.cleanup());
         this.setMainPainter(undefined, true);
         if (isFunc(this.removePadButtons))
            this.removePadButtons();
         this.addPadButtons(true);
      }

      return this.drawNextSnap(snap.fPrimitives, missmatch ? undefined : 0).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, snapid) {
      if (!this.is_active_pad || this.doingDraw() || x === undefined || y === undefined)
         return;
      if ((kind === 'move') && !this.#deliver_move_events)
         return;
      const cp = this.getCanvPainter();
      if (!cp || !cp.canSendWebsocket(2) || cp.isReadonly())
         return;

      const msg = JSON.stringify([this.getSnapId(), kind, x.toString(), y.toString(), 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.isReadonly())
         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.isCanvas() ? this : this.getCanvPainter();

      if (this.getSnapId()) {
         elem = {
            _typename: 'TWebPadOptions', snapid: this.getSnapId(),
            active: Boolean(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.isCanvas()) {
            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 fp = this.getFramePainter(),
            p = this.getPadSvg();

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

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

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

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

         if (fp.zoom_zmin !== fp.zoom_zmax) {
            r.zz1 = fp.zoom_zmin;
            r.zz2 = fp.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 /= Math.log10(log);
         return value;
      }, frect = fp.getFrameRect();

      r.ux1 = func(fp.logx, r.ux1, 0);
      r.ux2 = func(fp.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(fp.logy, r.uy1, 0);
      r.uy2 = func(fp.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.getPadSvg().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.getSnapId())
            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.#pad_name || (this.isCanvas() ? 'canvas' : 'pad')) + '.' + kind;

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

         if ((browser.qt6 || browser.cef3) && this.getSnapId()) {
            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() : (full_canvas ? this.getCanvSvg() : this.getPadSvg()),
            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.getPadSvg() };
         items.push(item);

         // remove buttons from each sub-pad
         const btns = pp.getLayerSvg('btns_layer');
         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$1.Embed3D.Overlay) && (can3d !== constants$1.Embed3D.Embed))
            return;

         const main = isFunc(fp.getRenderer) ? fp : fp.getMainPainter(),
               canvas = isFunc(main.getRenderer) ? main.getRenderer()?.domElement : null;
         if (!isFunc(main?.render3D) || !isObject(canvas))
            return;

         const sz2 = fp.getSizeFor3d(constants$1.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$1.Embed3D.Embed) {
            item.foreign = item.prnt.select('.' + sz2.clname);
            item.foreign.remove();
         }

         const svg_frame = fp.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'),
          viewBox = '';
      if (use_frame) {
         const fp = this.getFramePainter();
         width = fp.getFrameWidth();
         height = fp.getFrameHeight();
      }
      const scale = this.getPadScale();
      if (scale !== 1) {
         viewBox = ` viewBox="0 0 ${width} ${height}"`;
         width = Math.round(width / scale);
         height = Math.round(height / scale);
      }
      if (settings.DarkMode || this.#pad?.$dark)
         viewBox += ' style="filter: invert(100%)"';

      const arg = (file_format === 'pdf')
         ? { node: elem.node(), width, height, scale, reset_tranform: use_frame }
         : compressSVG(`<svg width="${width}" height="${height}"${viewBox} 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');

            menu.add(this.isCanvas() ? 'Canvas' : '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;
                  if (isFunc(pp.getMenuHeader))
                     name = pp.getMenuHeader();
                  else {
                     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 });

      if (!this.isTopPad() && funcname.indexOf('Pad') && (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.isCanvas() ? 'CanvasSnapShot' : 'PadSnapShot', 'Ctrl PrintScreen');

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

      const add_enlarge = !this.isTopPad() && 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),
            o = this.setOptions({ GlobalColors: true, LocalColors: false, CreatePalette: 0, IgnorePalette: false, RotateFrame: false, FixFrame: false }, opt);

      if (d.check('NOCOLORS') || d.check('NOCOL'))
         o.GlobalColors = o.LocalColors = false;
      if (d.check('LCOLORS') || d.check('LCOL')) {
         o.GlobalColors = false;
         o.LocalColors = true;
      }
      if (d.check('NOPALETTE') || d.check('NOPAL'))
         o.IgnorePalette = true;
      if (d.check('ROTATE'))
         o.RotateFrame = true;
      if (d.check('FIXFRAME'))
         o.FixFrame = true;
      if (d.check('FIXSIZE') && this.isCanvas())
         this._setFixedSize(true);
      if (d.check('CP', true))
         o.CreatePalette = d.partAsInt(0, 0);
      if (d.check('NOZOOMX'))
         o.NoZoomX = true;
      if (d.check('NOZOOMY'))
         o.NoZoomY = true;
      if (d.check('GRAYSCALE'))
         pad.SetBit(kIsGrayscale, true);

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

      function padOpt(name, func) {
         if (d.check(name))
            forEach(func, pad);
      }

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

      if (!d.empty() && pad?.fPrimitives) {
         for (let n = 0; n < pad.fPrimitives.arr.length; ++n) {
            if (d.check(`SUB${n}_`, true))
               pad.fPrimitives.opt[n] = d.part;
         }
      }

      this.storeDrawOpt(opt);
   }

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

      painter.createPadSvg();

      if (painter.matchObjectType(clTPad) && (painter.isTopPad() || 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

const kShowEventStatus = BIT(15),
   // kAutoExec = BIT(16),
      kMenuBar = BIT(17),
      kShowToolBar = BIT(18),
      kShowEditor = BIT(19),
   // kMoveOpaque = BIT(20),
   // kResizeOpaque = BIT(21),
   // kIsGrayscale = BIT(22),
      kShowToolTips = BIT(23);

/** @summary direct draw of TFrame object,
  * @desc pad or canvas should already exist
  * @private */
function directDrawTFrame(dom, obj, opt) {
   const fp = new TFramePainter(dom, obj);
   fp.addToPadPrimitives();
   if (opt === '3d')
      fp.mode3d = true;
   return fp.redraw();
}

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

class TCanvasPainter extends TPadPainter {

   #websocket; // WebWindow handle used for communication with server
   #changed_layout; // modified layout
   #getmenu_callback;  // function called when menu items get from server
   #online_fixed_size; // when size fixed for online canvas
   #all_sections_showed; // set once after online canvas drawn
   #last_highlight_msg; // last highligh msg send to server

   /** @summary Constructor */
   constructor(dom, canvas, opt, kind = true) {
      super(dom, canvas, opt, kind);
      this.#websocket = null;
      this.tooltip_allowed = settings.Tooltip;
   }

   /** @summary Cleanup canvas painter */
   cleanup() {
      if (this.#changed_layout)
         this.setLayoutKind('simple');
      this.#changed_layout = undefined;
      super.cleanup();
   }

   /** @summary Returns canvas name */
   getCanvasName() { return this.getObjectName(); }

   /** @summary Returns layout kind */
   getLayoutKind() {
      const origin = this.selectDom('origin'),
            layout = origin.empty() ? '' : origin.property('layout');

      return layout || 'simple';
   }

   /** @summary Set canvas layout kind */
   setLayoutKind(kind, main_selector) {
      const origin = this.selectDom('origin');
      if (!origin.empty()) {
         if (!kind)
            kind = 'simple';
         origin.property('layout', kind);
         origin.property('layout_selector', (kind !== 'simple') && main_selector ? main_selector : null);
         this.#changed_layout = (kind !== 'simple'); // use in cleanup
      }
   }

   /** @summary Changes layout
     * @return {Promise} indicating when finished */
   async changeLayout(layout_kind, mainid) {
      const current = this.getLayoutKind();
      if (current === layout_kind)
         return true;

      const origin = this.selectDom('origin'),
            sidebar2 = origin.select('.side_panel2'),
            lst = [];
      let sidebar = origin.select('.side_panel'),
          main = this.selectDom(), force;

      while (main.node().firstChild)
         lst.push(main.node().removeChild(main.node().firstChild));

      if (!sidebar.empty())
         cleanup(sidebar.node());
      if (!sidebar2.empty())
         cleanup(sidebar2.node());

      this.setLayoutKind('simple'); // restore defaults
      origin.html(''); // cleanup origin

      if (layout_kind === 'simple') {
         main = origin;
         for (let k = 0; k < lst.length; ++k)
            main.node().appendChild(lst[k]);
         this.setLayoutKind(layout_kind);
         force = true;
      } else {
         const grid = new GridDisplay(origin.node(), layout_kind);

         if (mainid === undefined)
            mainid = (layout_kind.indexOf('vert') === 0) ? 0 : 1;

         main = select(grid.getGridFrame(mainid));
         main.classed('central_panel', true).style('position', 'relative');

         if (mainid === 2) {
            // left panel for Y
            sidebar = select(grid.getGridFrame(0));
            sidebar.classed('side_panel2', true).style('position', 'relative');
            // bottom panel for X
            sidebar = select(grid.getGridFrame(3));
            sidebar.classed('side_panel', true).style('position', 'relative');
         } else {
            sidebar = select(grid.getGridFrame(1 - mainid));
            sidebar.classed('side_panel', true).style('position', 'relative');
         }

         // now append all childs to the new main
         for (let k = 0; k < lst.length; ++k)
            main.node().appendChild(lst[k]);

         this.setLayoutKind(layout_kind, '.central_panel');

         // remove reference to MDIDisplay, solves resize problem
         origin.property('mdi', null);
      }

      // resize main drawing and let draw extras
      resize(main.node(), force);
      return true;
   }

   /** @summary Toggle projection
     * @return {Promise} indicating when ready
     * @private */
   async toggleProjection(kind) {
      delete this.proj_painter;

      if (kind)
         this.proj_painter = { X: false, Y: false }; // just indicator that drawing can be preformed

      if (isFunc(this.showUI5ProjectionArea))
         return this.showUI5ProjectionArea(kind);

      let layout = 'simple', mainid;

      switch (kind) {
         case 'XY':
            layout = 'projxy';
            mainid = 2;
            break;
         case 'X':
         case 'bottom':
            layout = 'vert2_31';
            mainid = 0;
            break;
         case 'Y':
         case 'left':
            layout = 'horiz2_13';
            mainid = 1;
            break;
         case 'top':
            layout = 'vert2_13';
            mainid = 1;
            break;
         case 'right':
            layout = 'horiz2_31';
            mainid = 0;
            break;
      }

      return this.changeLayout(layout, mainid);
   }

   /** @summary Draw projection for specified histogram
     * @private */
   async drawProjection(kind, hist, hopt) {
      if (!this.proj_painter)
         return false; // ignore drawing if projection not configured

      if (hopt === undefined)
         hopt = 'hist';
      if (!kind)
         kind = 'X';

      if (!this.proj_painter[kind]) {
         this.proj_painter[kind] = 'init';

         const canv = create$1(clTCanvas),
               pad = this.getRootPad(),
               fp = this.getFramePainter();
         let drawopt;

         if (kind === 'X') {
            canv.fLeftMargin = pad.fLeftMargin;
            canv.fRightMargin = pad.fRightMargin;
            canv.fLogx = fp.logx;
            canv.fUxmin = fp.logx ? Math.log10(fp.scale_xmin) : fp.scale_xmin;
            canv.fUxmax = fp.logx ? Math.log10(fp.scale_xmax) : fp.scale_xmax;
            drawopt = 'fixframe';
         } else if (kind === 'Y') {
            canv.fBottomMargin = pad.fBottomMargin;
            canv.fTopMargin = pad.fTopMargin;
            canv.fLogx = fp.logy;
            canv.fUxmin = fp.logy ? Math.log10(fp.scale_ymin) : fp.scale_ymin;
            canv.fUxmax = fp.logy ? Math.log10(fp.scale_ymax) : fp.scale_ymax;
            drawopt = 'rotate';
         }

         canv.fPrimitives.Add(hist, hopt);

         const promise = isFunc(this.drawInUI5ProjectionArea)
                          ? this.drawInUI5ProjectionArea(canv, drawopt, kind)
                          : this.drawInSidePanel(canv, drawopt, kind);

         return promise.then(painter => {
            this.proj_painter[kind] = painter;
            return painter;
         });
      } else if (isStr(this.proj_painter[kind])) {
         console.log('Not ready with first painting', kind);
         return true;
      }

      this.proj_painter[kind].getMainPainter()?.updateObject(hist, hopt);
      return this.proj_painter[kind].redrawPad();
   }

   /** @summary Checks if canvas shown inside ui5 widget
     * @desc Function should be used only from the func which supposed to be replaced by ui5
     * @private */
   testUI5() {
      return this.use_openui ?? false;
   }

   /** @summary Draw in side panel
     * @private */
   async drawInSidePanel(canv, opt, kind) {
      const sel = ((this.getLayoutKind() === 'projxy') && (kind === 'Y')) ? '.side_panel2' : '.side_panel',
            side = this.selectDom('origin').select(sel);
      return side.empty() ? null : this.drawObject(side.node(), canv, opt);
   }

   /** @summary Show message
     * @desc Used normally with web-based canvas and handled in ui5
     * @private */
   showMessage(msg) {
      if (!this.testUI5())
         showProgress(msg, 7000);
   }

   /** @summary Function called when canvas menu item Save is called */
   saveCanvasAsFile(fname) {
      const pnt = fname.indexOf('.');
      this.createImage(fname.slice(pnt + 1))
          .then(res => this.sendWebsocket(`SAVE:${fname}:${res}`));
   }

   /** @summary Send command to server to save canvas with specified name
     * @desc Should be only used in web-based canvas
     * @private */
   sendSaveCommand(fname) {
      this.sendWebsocket('PRODUCE:' + fname);
   }

   /** @summary Submit menu request
     * @private */
   async submitMenuRequest(_painter, _kind, reqid) {
      // only single request can be handled, no limit better in RCanvas
      return new Promise(resolveFunc => {
         this.#getmenu_callback = resolveFunc;
         this.sendWebsocket('GETMENU:' + reqid); // request menu items for given painter
      });
   }

   /** @summary Submit object exec request
     * @private */
   submitExec(painter, exec, snapid) {
      if (this.isReadonly() || !painter)
         return;

      if (!snapid)
         snapid = painter.getSnapId();
      if (snapid && isStr(snapid) && exec)
         return this.sendWebsocket(`OBJEXEC:${snapid}:${exec}`);
   }

   /** @summary Return assigned web socket
    * @private */
   getWebsocket() { return this.#websocket; }

   /** @summary Return true if message can be send via web socket
    * @private */
   canSendWebsocket(noper = 1) { return this.#websocket?.canSend(noper); }

   /** @summary Send text message with web socket
     * @desc used for communication with server-side of web canvas
     * @private */
   sendWebsocket(msg) {
      if (this.#websocket?.canSend()) {
         this.#websocket.send(msg);
         return true;
      }
      console.warn(`DROP SEND: ${msg}`);
      return false;
   }

   /** @summary Close websocket connection to canvas
     * @private */
   closeWebsocket(force) {
      if (this.#websocket) {
         this.#websocket.close(force);
         this.#websocket.cleanup();
         this.#websocket = undefined;
      }
   }

   /** @summary Use provided connection for the web canvas
     * @private */
   useWebsocket(handle) {
      this.closeWebsocket();

      this.#websocket = handle;
      this.#websocket.setReceiver(this);
      this.#websocket.connect();
   }

   /** @summary set, test or reset timeout of specified name
     * @desc Used to prevent overloading of websocket for specific function */
   websocketTimeout(name, tm) {
      if (!this.#websocket)
         return;
      if (!this.#websocket._tmouts)
         this.#websocket._tmouts = {};

      const handle = this.#websocket._tmouts[name];
      if (tm === undefined)
         return handle !== undefined;

      if (tm === 'reset') {
         if (handle) {
            clearTimeout(handle);
            delete this.#websocket._tmouts[name];
         }
      } else if (!handle && Number.isInteger(tm))
         this.#websocket._tmouts[name] = setTimeout(() => { delete this.#websocket._tmouts[name]; }, tm);
   }

   /** @summary Handler for websocket open event
     * @private */
   onWebsocketOpened(/* handle */) {
      // indicate that we are ready to receive any following commands
   }

   /** @summary Handler for websocket close event
     * @private */
   onWebsocketClosed(/* handle */) {
      if (!this.embed_canvas)
         closeCurrentWindow();
   }

   /** @summary Handle websocket messages
     * @private */
   onWebsocketMsg(handle, msg) {
      // console.log(`GET len:${msg.length} msg:${msg.slice(0,60)}`);

      if (msg === 'CLOSE') {
         this.onWebsocketClosed();
         this.closeWebsocket(true);
      } else if (msg.slice(0, 6) === 'SNAP6:') {
         // This is snapshot, produced with TWebCanvas
         const p1 = msg.indexOf(':', 6),
               version = msg.slice(6, p1),
               snap = parse$1(msg.slice(p1 + 1));

         this.syncDraw(true)
             .then(() => {
                if (!this.getSnapId())
                   this.resizeBrowser(snap.fSnapshot.fWindowWidth, snap.fSnapshot.fWindowHeight);
                if (!this.getSnapId() && isFunc(this.setFixedCanvasSize))
                   this.#online_fixed_size = this.setFixedCanvasSize(snap.fSnapshot.fCw, snap.fSnapshot.fCh, snap.fFixedSize);
             })
             .then(() => this.redrawPadSnap(snap))
             .then(() => {
                this.completeCanvasSnapDrawing();
                let ranges = this.getWebPadOptions(); // all data, including sub-pads
                if (ranges)
                   ranges = ':' + ranges;
                handle.send(`READY6:${version}${ranges}`); // send ready message back when drawing completed
                this.confirmDraw();
             }).catch(err => {
                if (isFunc(this.showConsoleError))
                   this.showConsoleError(err);
                else
                   console.log(err);
             });
      } else if (msg.slice(0, 5) === 'MENU:') {
         // this is menu with exact identifier for object
         const lst = parse$1(msg.slice(5));
         if (isFunc(this.#getmenu_callback)) {
            this.#getmenu_callback(lst);
            this.#getmenu_callback = undefined;
         }
      } else if (msg.slice(0, 4) === 'CMD:') {
         msg = msg.slice(4);
         const p1 = msg.indexOf(':'),
               cmdid = msg.slice(0, p1),
               cmd = msg.slice(p1 + 1),
               reply = `REPLY:${cmdid}:`;
         if ((cmd === 'SVG') || (cmd === 'PNG') || (cmd === 'JPEG') || (cmd === 'WEBP') || (cmd === 'PDF')) {
            this.createImage(cmd.toLowerCase())
                .then(res => handle.send(reply + res));
         } else {
            console.log(`Unrecognized command ${cmd}`);
            handle.send(reply);
         }
      } else if ((msg.slice(0, 7) === 'DXPROJ:') || (msg.slice(0, 7) === 'DYPROJ:')) {
         const kind = msg[1],
               hist = parse$1(msg.slice(7));
         this.websocketTimeout(`proj${kind}`, 'reset');
         this.drawProjection(kind, hist);
      } else if (msg.slice(0, 5) === 'CTRL:') {
         const ctrl = parse$1(msg.slice(5)) || {};
         let resized = false;
         if ((ctrl.title !== undefined) && (typeof document !== 'undefined'))
            document.title = ctrl.title;
         if (ctrl.x && ctrl.y && typeof window !== 'undefined') {
            window.moveTo(ctrl.x, ctrl.y);
            resized = true;
         }
         if (ctrl.w && ctrl.h) {
            this.resizeBrowser(Number.parseInt(ctrl.w), Number.parseInt(ctrl.h));
            resized = true;
         }
         if (ctrl.cw && ctrl.ch && isFunc(this.setFixedCanvasSize)) {
            this.#online_fixed_size = this.setFixedCanvasSize(Number.parseInt(ctrl.cw), Number.parseInt(ctrl.ch), true);
            resized = true;
         }
         const kinds = ['Menu', 'StatusBar', 'Editor', 'ToolBar', 'ToolTips'];
         kinds.forEach(kind => {
            if (ctrl[kind] !== undefined)
               this.showSection(kind, ctrl[kind] === '1');
         });

         if (ctrl.edit) {
            const obj_painter = this.findSnap(ctrl.edit);
            if (obj_painter) {
               this.showSection('Editor', true)
                   .then(() => this.producePadEvent('select', obj_painter.getPadPainter(), obj_painter));
            }
         }

         if (ctrl.winstate && typeof window !== 'undefined') {
            if (ctrl.winstate === 'iconify')
               window.blur();
            else
               window.focus();
         }

         if (resized)
            this.sendResized(true);
      } else
         console.log(`unrecognized msg ${msg}`);
   }

   /** @summary Send RESIZED message to client to inform about changes in canvas/window geometry
     * @private */
   sendResized(force) {
      const pad = this.getRootPad();
      if (!pad || (typeof window === 'undefined'))
         return;
      const cw = this.getPadWidth(), ch = this.getPadHeight(),
            wx = window.screenLeft, wy = window.screenTop,
            ww = window.outerWidth, wh = window.outerHeight,
            fixed = this.#online_fixed_size ? 1 : 0;
      if (!force) {
         force = (cw > 0) && (ch > 0) && ((pad.fCw !== cw) || (pad.fCh !== ch));
         if (force) {
            pad.fCw = cw;
            pad.fCh = ch;
         }
      }
      if (force)
         this.sendWebsocket(`RESIZED:${JSON.stringify([wx, wy, ww, wh, cw, ch, fixed])}`);
   }

   /** @summary Handle pad button click event */
   clickPadButton(funcname, evnt) {
      if (funcname === 'ToggleGed')
         return this.activateGed(this, null, 'toggle');
      if (funcname === 'ToggleStatus')
         return this.activateStatusBar('toggle');
      return super.clickPadButton(funcname, evnt);
   }

   /** @summary Returns true if event status shown in the canvas */
   hasEventStatus() {
      if (this.testUI5())
         return false;
      if (this.brlayout)
         return this.brlayout.hasStatus();
      return getHPainter()?.hasStatusLine() ?? false;
   }

   /** @summary Check if status bar can be toggled
     * @private */
   canStatusBar() {
      return this.testUI5() || this.brlayout || getHPainter();
   }

   /** @summary Show/toggle event status bar
     * @private */
   activateStatusBar(state) {
      if (this.testUI5())
         return;
      if (this.brlayout)
         this.brlayout.createStatusLine(23, state);
      else
         getHPainter()?.createStatusLine(23, state);
      this.processChanges('sbits', this);
   }

   /** @summary Show online canvas status
     * @private */
   showCanvasStatus(...msgs) {
      if (this.testUI5())
         return;

      const br = this.brlayout || getHPainter()?.brlayout;
      br?.showStatus(...msgs);
   }

   /** @summary Returns true if GED is present on the canvas */
   hasGed() {
      if (this.testUI5())
         return false;
      return this.brlayout?.hasContent() ?? false;
   }

   /** @summary Function used to de-activate GED
     * @private */
   removeGed() {
      if (this.testUI5())
         return;

      this.registerForPadEvents(null);

      if (this.ged_view) {
         this.ged_view.getController().cleanupGed();
         this.ged_view.destroy();
         delete this.ged_view;
      }
      this.brlayout?.deleteContent(true);
      this.processChanges('sbits', this);
   }

   /** @summary Get view data for ui5 panel
     * @private */
   getUi5PanelData(/* panel_name */) {
      return { jsroot: { settings, create: create$1, parse: parse$1, toJSON, loadScript, EAxisBits, getColorExec } };
   }

   /** @summary Function used to activate GED
     * @return {Promise} when GED is there
     * @private */
   async activateGed(objpainter, kind, mode) {
      if (this.testUI5() || !this.brlayout)
         return false;

      if (this.brlayout.hasContent()) {
         if ((mode === 'toggle') || (mode === false))
            this.removeGed();
         else
            objpainter?.getPadPainter()?.selectObjectPainter(objpainter);

         return true;
      }

      if (mode === false)
         return false;

      const btns = this.brlayout.createBrowserBtns();

      ToolbarIcons.createSVG(btns, ToolbarIcons.diamand, 15, 'toggle fix-pos mode', 'browser')
                  .style('margin', '3px').on('click', () => this.brlayout.toggleKind('fix'));

      ToolbarIcons.createSVG(btns, ToolbarIcons.circle, 15, 'toggle float mode', 'browser')
                  .style('margin', '3px').on('click', () => this.brlayout.toggleKind('float'));

      ToolbarIcons.createSVG(btns, ToolbarIcons.cross, 15, 'delete GED', 'browser')
                  .style('margin', '3px').on('click', () => this.removeGed());

      // be aware, that jsroot_browser_hierarchy required for flexible layout that element use full browser area
      this.brlayout.setBrowserContent('<div class=\'jsroot_browser_hierarchy\' id=\'ged_placeholder\'>Loading GED ...</div>');
      this.brlayout.setBrowserTitle('GED');
      this.brlayout.toggleBrowserKind(kind || 'float');

      return new Promise(resolveFunc => {
         loadOpenui5().then(sap => {
            select('#ged_placeholder').text('');

            sap.ui.require(['sap/ui/model/json/JSONModel', 'sap/ui/core/mvc/XMLView'], (JSONModel, XMLView) => {
               const oModel = new JSONModel({ handle: null });

               XMLView.create({
                  viewName: 'rootui5.canv.view.Ged',
                  viewData: this.getUi5PanelData('Ged')
               }).then(oGed => {
                  oGed.setModel(oModel);

                  oGed.placeAt('ged_placeholder');

                  this.ged_view = oGed;

                  // TODO: should be moved into Ged controller - it must be able to detect canvas painter itself
                  this.registerForPadEvents(oGed.getController().padEventsReceiver.bind(oGed.getController()));

                  objpainter?.getPadPainter()?.selectObjectPainter(objpainter);

                  this.processChanges('sbits', this);

                  resolveFunc(true);
               });
            });
         });
      });
   }

   /** @summary Show section of canvas  like menu or editor */
   async showSection(that, on) {
      if (this.testUI5())
         return false;

      switch (that) {
         case 'Menu': break;
         case 'StatusBar': this.activateStatusBar(on); break;
         case 'Editor': return this.activateGed(this, null, on);
         case 'ToolBar': break;
         case 'ToolTips': this.setTooltipAllowed(on); break;
      }
      return true;
   }

   /** @summary Send command to start fit panel code on the server
     * @private */
   startFitPanel(standalone) {
      if (!this.getWebsocket())
         return false;

      const new_conn = standalone ? null : this.getWebsocket().createChannel();

      this.sendWebsocket('FITPANEL:' + (standalone ? 'standalone' : new_conn.getChannelId()));

      return new_conn;
   }

   /** @summary Complete handling of online canvas drawing
     * @private */
   completeCanvasSnapDrawing() {
      const pad = this.getRootPad();
      if (!pad)
         return;

      this.addPadInteractive();

      if ((typeof document !== 'undefined') && !this.embed_canvas && this.getWebsocket())
         document.title = pad.fTitle;

      if (this.#all_sections_showed)
         return;
      this.#all_sections_showed = true;

      // used in Canvas.controller.js to avoid browser resize because of initial sections show/hide
      this._ignore_section_resize = true;

      this.showSection('Menu', pad.TestBit(kMenuBar));
      this.showSection('StatusBar', pad.TestBit(kShowEventStatus));
      this.showSection('ToolBar', pad.TestBit(kShowToolBar));
      this.showSection('Editor', pad.TestBit(kShowEditor));
      this.showSection('ToolTips', pad.TestBit(kShowToolTips) || this._highlight_connect);

      this._ignore_section_resize = false;
   }

   /** @summary Handle highlight in canvas - deliver information to server
     * @private */
   processHighlightConnect(hints) {
      if (!hints?.length || !this._highlight_connect ||
          this.doingDraw() || !this.canSendWebsocket(2))
         return;

      const hint = hints[0] || hints[1];
      if (!hint || !hint.painter?.getSnapId() || !hint.user_info)
         return;
      const pp = hint.painter.getPadPainter() || this;
      if (!pp.getSnapId())
         return;

      const arr = [pp.getSnapId(), hint.painter.getSnapId(), '0', '0'];

      if ((hint.user_info.binx !== undefined) && (hint.user_info.biny !== undefined)) {
         arr[2] = hint.user_info.binx.toString();
         arr[3] = hint.user_info.biny.toString();
      } else if (hint.user_info.bin !== undefined)
         arr[2] = hint.user_info.bin.toString();

      const msg = JSON.stringify(arr);

      if (this.#last_highlight_msg !== msg) {
         this.#last_highlight_msg = msg;
         this.sendWebsocket(`HIGHLIGHT:${msg}`);
      }
   }

   /** @summary Method informs that something was changed in the canvas
     * @desc used to update information on the server (when used with web6gui)
     * @private */
   processChanges(kind, painter, subelem) {
      // check if we could send at least one message more - for some meaningful actions
      if (this.isReadonly() || !this.canSendWebsocket(2) || !isStr(kind))
         return;

      let msg = '';
      if (!painter)
         painter = this;
      switch (kind) {
         case 'sbits':
            msg = 'STATUSBITS:' + this.getStatusBits();
            break;
         case 'frame': // when changing frame
         case 'zoom':  // when changing zoom inside frame
            if (!isFunc(painter.getWebPadOptions))
               painter = painter.getPadPainter();
            if (isFunc(painter.getWebPadOptions))
               msg = 'OPTIONS6:' + painter.getWebPadOptions('only_this');
            break;
         case 'padpos': // when changing pad position
            msg = 'OPTIONS6:' + painter.getWebPadOptions('with_subpads');
            break;
         case 'drawopt':
            if (painter.getSnapId())
               msg = 'DRAWOPT:' + JSON.stringify([painter.getSnapId(), painter.getDrawOpt() || '']);
            break;
         case 'pave_moved': {
            const info = createWebObjectOptions(painter);
            if (info)
               msg = 'PRIMIT6:' + toJSON(info);
            break;
         }
         case 'logx':
         case 'logy':
         case 'logz': {
            const pp = painter.getPadPainter(),
                  pad = pp?.getRootPad();
            if (pp?.getSnapId() && pad) {
               const name = 'SetLog' + kind[3], value = pad['fLog' + kind[3]];
               painter = pp;
               kind = `exec:${name}(${value})`;
            }
            break;
         }
      }

      if (!msg && isFunc(painter?.getSnapId) && (kind.slice(0, 5) === 'exec:')) {
         const snapid = painter.getSnapId(subelem);
         if (snapid) {
            msg = 'PRIMIT6:' + toJSON({
               _typename: 'TWebObjectOptions',
               snapid, opt: kind.slice(5), fcust: 'exec', fopt: []
            });
         }
      }

      if (msg) {
         // console.log(`Sending ${msg.length} ${msg.slice(0,40)}`);
         this.sendWebsocket(msg);
      } else
         console.log(`Unprocessed changes ${kind} for painter of ${painter?.getObject()?._typename} subelem ${subelem}`);
   }

   /** @summary Select active pad on the canvas */
   selectActivePad(pad_painter, obj_painter, click_pos) {
      if (!this.getSnapId() || !pad_painter)
         return; // only interactive canvas

      let arg = null, ischanged = false;
      const is_button = pad_painter.matchObjectType(clTButton);

      if (pad_painter.getSnapId() && this.getWebsocket())
         arg = { _typename: 'TWebPadClick', padid: pad_painter.getSnapId(), objid: '', x: -1, y: -1, dbl: false };

      if (!pad_painter.is_active_pad && !is_button) {
         ischanged = true;
         this.forEachPainterInPad(pp => pp.drawActiveBorder(null, pp === pad_painter), 'pads');
      }

      if (obj_painter?.hasSnapId() && arg) {
         ischanged = true;
         arg.objid = obj_painter.getSnapId();
      }

      if (click_pos && arg) {
         ischanged = true;
         arg.x = Math.round(click_pos.x || 0);
         arg.y = Math.round(click_pos.y || 0);
         if (click_pos.dbl)
            arg.dbl = true;
      }

      if (arg && (ischanged || is_button))
         this.sendWebsocket('PADCLICKED:' + toJSON(arg));
   }

   /** @summary Return actual TCanvas status bits  */
   getStatusBits() {
      let bits = 0;
      if (this.hasEventStatus())
         bits |= kShowEventStatus;
      if (this.hasGed())
         bits |= kShowEditor;
      if (this.isTooltipAllowed())
         bits |= kShowToolTips;
      if (this.use_openui)
         bits |= kMenuBar;
      return bits;
   }

   /** @summary produce JSON for TCanvas, which can be used to display canvas once again */
   produceJSON(spacing) {
      const canv = this.getObject();

      if (canv._typename !== clTCanvas)
         return;

      const fill0 = (canv.fFillStyle === 0),
            axes = [], hists = [], prims = [];

      if (fill0)
         canv.fFillStyle = 1001;

      this.forEachPainterInPad(pp => {
         const pad = pp.getRootPad(true);
         if (pp.getNumPainters() && pad?.fPrimitives && !pad.fPrimitives.arr.length) {
            // create list of primitives when missing
            prims.push(pad.fPrimitives);
            pp.forEachPainterInPad(p => {
               // ignore all secondary painters
               if (p.isSecondary())
                  return;
               const subobj = p.getObject();
               if (subobj?._typename)
                  pad.fPrimitives.Add(subobj, p.getDrawOpt());
            }, 'objects');
         }

         const main = pp.getMainPainter(),
               fp = pp.getFramePainter();
         if (!isFunc(main?.getHisto) || !isFunc(main?.getDimension))
            return;

         // write selected range into TAxis properties
         const hist = main.getHisto(),
               ndim = main.getDimension();
         if (!hist?.fXaxis)
            return;

         const setAxisRange = (name, axis) => {
            if (fp?.zoomChangedInteractive(name)) {
               axes.push({ axis, f: axis.fFirst, l: axis.fLast, b: axis.fBits });
               axis.fFirst = main.getSelectIndex(name, 'left', 1);
               axis.fLast = main.getSelectIndex(name, 'right');
               axis.SetBit(EAxisBits.kAxisRange, (axis.fFirst > 0) || (axis.fLast < axis.fNbins));
            }
         };

         setAxisRange('x', hist.fXaxis);
         if (ndim > 1)
            setAxisRange('y', hist.fYaxis);
         if (ndim > 2)
            setAxisRange('z', hist.fZaxis);
         if ((ndim === 2) && fp?.zoomChangedInteractive('z')) {
            hists.push({ hist, min: hist.fMinimum, max: hist.fMaximum });
            hist.fMinimum = fp.zoom_zmin ?? fp.zmin;
            hist.fMaximum = fp.zoom_zmax ?? fp.zmax;
         }
      }, 'pads');

      const res = toJSON(canv, spacing);

      if (fill0)
         canv.fFillStyle = 0;

      axes.forEach(e => {
         e.axis.fFirst = e.f;
         e.axis.fLast = e.l;
         e.axis.fBits = e.b;
      });

      hists.forEach(e => {
         e.hist.fMinimum = e.min;
         e.hist.fMaximum = e.max;
      });

      prims.forEach(lst => lst.Clear());

      return res;
   }

   /** @summary resize browser window */
   resizeBrowser(fullW, fullH) {
      if (!fullW || !fullH || this.isBatchMode() || this.embed_canvas || this.batch_mode)
         return;

      // workaround for qt-based display where inner window size is used
      if (browser.qt6 && fullW > 100 && fullH > 60) {
         fullW -= 3;
         fullH -= 30;
      }

      this.getWebsocket()?.resizeWindow(fullW, fullH);
   }

   /** @summary draw TCanvas */
   static async draw(dom, can, opt) {
      const nocanvas = !can;
      if (nocanvas)
         can = create$1(clTCanvas);

      const painter = new TCanvasPainter(dom, can, opt, nocanvas ? 'auto' : true);
      painter.checkSpecialsInPrimitives(can, true);

      if (!nocanvas && can.fCw && can.fCh) {
         const d = painter.selectDom();
         let apply_size;
         if (!painter.isBatchMode()) {
            const rect0 = d.node().getBoundingClientRect();
            apply_size = !rect0.height && (rect0.width > 0.1 * can.fCw);
         } else {
            const arg = d.property('_batch_use_canvsize');
            apply_size = arg || (arg === undefined);
         }
         if (apply_size) {
            d.style('width', can.fCw + 'px').style('height', can.fCh + 'px')
              .attr('width', can.fCw).attr('height', can.fCh);
            painter._setFixedSize(true);
         }
      }

      painter.createCanvasSvg(0);

      painter.addPadButtons();

      if (nocanvas && opt.indexOf('noframe') < 0)
         directDrawTFrame(painter, null);

      // select global reference - required for keys handling
      selectActivePad({ pp: painter, active: true });

      return painter.drawPrimitives().then(() => {
         painter.addPadInteractive();
         painter.showPadButtons();
         return painter;
      });
   }

} // class TCanvasPainter


/** @summary Ensure TCanvas and TFrame for the painter object
  * @param {Object} painter  - painter object to process
  * @param {string|boolean} frame_kind  - false for no frame or '3d' for special 3D mode
  * @desc Assign dom, creates TCanvas if necessary, add to list of pad painters */
async function ensureTCanvas(painter, frame_kind) {
   if (!painter)
      return Promise.reject(Error('Painter not provided in ensureTCanvas'));

   // simple check - if canvas there, can use painter
   const noframe = (frame_kind === false) || (frame_kind === '3d') ? 'noframe' : '',
         createCanv = () => {
            if ((noframe !== 'noframe') || !isFunc(painter.getUserRanges))
               return null;
            const ranges = painter.getUserRanges();
            if (!ranges)
               return null;
            const canv = create$1(clTCanvas),
                  dx = (ranges.maxx - ranges.minx) || 1,
                  dy = (ranges.maxy - ranges.miny) || 1;
            canv.fX1 = ranges.minx - dx * gStyle.fPadLeftMargin;
            canv.fX2 = ranges.maxx + dx * gStyle.fPadRightMargin;
            canv.fY1 = ranges.miny - dy * gStyle.fPadBottomMargin;
            canv.fY2 = ranges.maxy + dy * gStyle.fPadTopMargin;
            return canv;
         },
         pad_painter = painter.getPadPainter() || getDomCanvasPainter(painter.selectDom()),
         promise = pad_painter ? Promise.resolve(pad_painter) :
                     TCanvasPainter.draw(painter.getDom(), createCanv(), noframe);

   return promise.then(pp => {
      if ((frame_kind !== false) && pp.getFrameSvg().selectChild('.main_layer').empty() && !pp.getFramePainter())
         directDrawTFrame(pp, null, frame_kind);

      painter.addToPadPrimitives(pp);
      return painter;
   });
}

/** @summary draw TPad snapshot from TWebCanvas
  * @private */
async function drawTPadSnapshot(dom, snap, opt) {
   const can = create$1(clTCanvas),
         painter = new TCanvasPainter(dom, can, opt);
   painter.addPadButtons();

   return painter.syncDraw(true).then(() => painter.redrawPadSnap(snap)).then(() => {
      painter.confirmDraw();
      painter.showPadButtons();
      return painter;
   });
}

/** @summary draw TFrame object
  * @private */
async function drawTFrame(dom, obj, opt) {
   const fp = new TFramePainter(dom, obj);
   fp.mode3d = opt === '3d';
   return ensureTCanvas(fp, false).then(() => fp.redraw());
}

Object.assign(internals.jsroot, { ensureTCanvas, TPadPainter, TCanvasPainter });

var TCanvasPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TCanvasPainter: TCanvasPainter,
TPadPainter: TPadPainter,
drawTFrame: drawTFrame,
drawTPadSnapshot: drawTPadSnapshot,
ensureTCanvas: ensureTCanvas
});

const kTakeStyle = BIT(17), kPosTitle = 'postitle', kAutoPlace = 'autoplace', kDefaultDrawOpt = 'brNDC';

/** @summary Returns true if stat box on default place and can be adjusted
  * @private */
function isDefaultStatPosition(pt) {
   const test = (v1, v2) => (Math.abs(v1 - v2) < 1e-3);
   return test(pt.fX1NDC, gStyle.fStatX - gStyle.fStatW) &&
          test(pt.fY1NDC, gStyle.fStatY - gStyle.fStatH) &&
          test(pt.fX2NDC, gStyle.fStatX) &&
          test(pt.fY2NDC, gStyle.fStatY);
}

/**
 * @summary painter for TPave-derived classes
 *
 * @private
 */

class TPavePainter extends ObjectPainter {

   #pave_x; // x position of pave
   #pave_y; // y position of pave
   #palette_vertical; // when palette drawing vertical
   #swap_side; // swap palette side
   #has_fit; // has fit info
   #fit_dim; // dimension of fit function
   #fit_cnt; // lines number in fit info

   /** @summary constructor
     * @param {object|string} dom - DOM element for drawing or element id
     * @param {object} pave - TPave-based object */
   constructor(dom, pave, opt) {
      super(dom, pave, opt);
      this.Enabled = true;
      this.UseContextMenu = true;
   }

   /** @summary Auto place legend on the frame
     * @return {Promise} with boolean flag if position was changed  */
   async autoPlaceLegend(pt, pad, keep_origin) {
      const main_svg = this.getPadPainter().getFrameSvg().selectChild('.main_layer');

      let svg_code = main_svg.node().outerHTML;

      svg_code = compressSVG(svg_code);

      svg_code = `<svg xmlns="${nsSVG}"` + svg_code.slice(4);

      const lm = pad?.fLeftMargin ?? gStyle.fPadLeftMargin,
            rm = pad?.fRightMargin ?? gStyle.fPadRightMargin,
            tm = pad?.fTopMargin ?? gStyle.fPadTopMargin,
            bm = pad?.fBottomMargin ?? gStyle.fPadBottomMargin;

      return svgToImage(svg_code).then(canvas => {
         if (!canvas)
            return false;

         let nX = 100, nY = 100;
         const context = canvas.getContext('2d'),
               arr = context.getImageData(0, 0, canvas.width, canvas.height).data,
               boxW = Math.floor(canvas.width / nX), boxH = Math.floor(canvas.height / nY),
               raster = new Array(nX * nY);

         if (arr.length !== canvas.width * canvas.height * 4) {
            console.log(`Image size missmatch in TLegend autoplace ${arr.length} expected ${canvas.width * canvas.height * 4}`);
            nX = nY = 0;
         }

         for (let ix = 0; ix < nX; ++ix) {
            const px1 = ix * boxW, px2 = px1 + boxW;
            for (let iy = 0; iy < nY; ++iy) {
               const py1 = iy * boxH, py2 = py1 + boxH;
               let filled = 0;

               for (let x = px1; (x < px2) && !filled; ++x) {
                  for (let y = py1; y < py2; ++y) {
                     const indx = (y * canvas.width + x) * 4;
                     if (arr[indx] || arr[indx + 1] || arr[indx + 2] || arr[indx + 3]) {
                        filled = 1;
                        break;
                     }
                  }
               }
               raster[iy * nX + ix] = filled;
            }
         }

         const legWidth = 0.3 / Math.max(0.2, (1 - lm - rm)),
               legHeight = Math.min(0.5, Math.max(0.1, pt.fPrimitives.arr.length * 0.05)) / Math.max(0.2, (1 - tm - bm)),
               needW = Math.round(legWidth * nX), needH = Math.round(legHeight * nY),

               test = (x, y) => {
                  for (let ix = x; ix < x + needW; ++ix) {
                     for (let iy = y; iy < y + needH; ++iy) {
                        if (raster[iy * nX + ix])
                           return false;
                     }
                  }
                  return true;
               };

         for (let ix = 0; ix < (nX - needW); ++ix) {
            for (let iy = nY - needH - 1; iy >= 0; --iy) {
               if (test(ix, iy)) {
                  pt.fX1NDC = lm + ix / nX * (1 - lm - rm);
                  pt.fX2NDC = pt.fX1NDC + legWidth * (1 - lm - rm);
                  pt.fY2NDC = 1 - tm - iy / nY * (1 - bm - tm);
                  pt.fY1NDC = pt.fY2NDC - legHeight * (1 - bm - tm);
                  return true;
               }
            }
         }
      }).then(res => {
         if (res || keep_origin)
            return res;

         pt.fX1NDC = Math.max(lm ?? 0, pt.fX2NDC - 0.3);
         pt.fX2NDC = Math.min(pt.fX1NDC + 0.3, 1 - rm);
         const h0 = Math.max(pt.fPrimitives ? pt.fPrimitives.arr.length * 0.05 : 0, 0.2);
         pt.fY2NDC = Math.min(1 - tm, pt.fY1NDC + h0);
         pt.fY1NDC = Math.max(pt.fY2NDC - h0, bm);
         return true;
      });
   }

   /** @summary Get draw option for the pave
     * @desc only stats using fOption directly, all other classes - stored in the pad */
   getPaveDrawOption() {
      let opt = this.getDrawOpt();
      if (this.isStats() || !opt)
         opt = this.getObject()?.fOption;
      return opt || kDefaultDrawOpt;
   }

   /** @summary Change pave draw option */
   setPaveDrawOption(opt) {
      if (this.isStats())
         this.getObject().fOption = opt;
      else
         this.storeDrawOpt(opt);
   }

   /** @summary Draw pave and content
     * @return {Promise} */
   async drawPave(arg) {
      if (!this.Enabled) {
         this.removeG();
         return this;
      }

      const pt = this.getObject(),
            opt = this.getPaveDrawOption().toUpperCase(),
            fp = this.getFramePainter(), pp = this.getPadPainter(),
            pad = pp.getRootPad(true);
      let interactive_element, width, height;

      if (pt.fInit === 0) {
         this.stored = Object.assign({}, pt); // store coordinates to use them when updating
         pt.fInit = 1;

         if ((pt._typename === clTPaletteAxis) && !pt.fX1 && !pt.fX2 && !pt.fY1 && !pt.fY2) {
            if (fp) {
               pt.fX1NDC = fp.fX2NDC + 0.01;
               pt.fX2NDC = Math.min(0.96, fp.fX2NDC + 0.06);
               pt.fY1NDC = fp.fY1NDC;
               pt.fY2NDC = fp.fY2NDC;
            } else {
               pt.fX2NDC = 0.8;
               pt.fX1NDC = 0.9;
               pt.fY1NDC = 0.1;
               pt.fY2NDC = 0.9;
            }
         } else if (pt.fOption.indexOf('NDC') >= 0) {
            // check if NDC was modified but fInit was not set
            // wired - ROOT checks fOption even when absolutely different draw option may be specified,
            // happens in stressGraphics.cxx, sg30 where stats box not initialized when call C->Update() in batch mode
            if (pt.fX1NDC < 1e-20 && pt.fX2NDC < 1e-20) {
               pt.fX1NDC = pt.fX1;
               pt.fX2NDC = pt.fX2;
            }
            if (pt.fY1NDC < 1e-20 && pt.fY2NDC < 1e-20) {
               pt.fY1NDC = pt.fY1;
               pt.fY2NDC = pt.fY2;
            }
         } else if (pad && (pad.fX1 === 0) && (pad.fX2 === 1) && (pad.fY1 === 0) && (pad.fY2 === 1) && isStr(arg) && (arg.indexOf('postpone') >= 0)) {
            // special case when pad not yet initialized
            pt.fInit = 0; // do not init until axes drawn
            pt.fX1NDC = pt.fY1NDC = 0.99;
            pt.fX2NDC = pt.fY2NDC = 1;
         } else if (pad) {
            if (pad.fLogx) {
               if (pt.fX1 > 0)
                  pt.fX1 = Math.log10(pt.fX1);
               if (pt.fX2 > 0)
                  pt.fX2 = Math.log10(pt.fX2);
            }
            if (pad.fLogy) {
               if (pt.fY1 > 0)
                  pt.fY1 = Math.log10(pt.fY1);
               if (pt.fY2 > 0)
                  pt.fY2 = Math.log10(pt.fY2);
            }
            pt.fX1NDC = (pt.fX1 - pad.fX1) / (pad.fX2 - pad.fX1);
            pt.fY1NDC = (pt.fY1 - pad.fY1) / (pad.fY2 - pad.fY1);
            pt.fX2NDC = (pt.fX2 - pad.fX1) / (pad.fX2 - pad.fX1);
            pt.fY2NDC = (pt.fY2 - pad.fY1) / (pad.fY2 - pad.fY1);
         } else {
            pt.fX1NDC = pt.fY1NDC = 0.1;
            pt.fX2NDC = pt.fY2NDC = 0.9;
         }
      }

      let promise = Promise.resolve(true);

      if ((pt._typename === clTLegend) && (this.AutoPlace || ((pt.fX1NDC === pt.fX2NDC) && (pt.fY1NDC === pt.fY2NDC)))) {
         promise = this.autoPlaceLegend(pt, pad).then(res => {
            delete this.AutoPlace;
            if (!res) {
               pt.fX1NDC = fp.fX2NDC - 0.2;
               pt.fX2NDC = fp.fX2NDC;
               pt.fY1NDC = fp.fY2NDC - 0.1;
               pt.fY2NDC = fp.fY2NDC;
            }
            return res;
         });
      }

      return promise.then(() => {
         // fill stats before drawing to have coordinates early
         if (this.isStats() && !this.NoFillStats && !pp.isFastDrawing()) {
            const main = pt.$main_painter || this.getMainPainter();

            if (isFunc(main?.fillStatistic)) {
               let dostat = pt.fOptStat, dofit = pt.fOptFit;
               if (pt.TestBit(kTakeStyle) || !Number.isInteger(dostat))
                  dostat = gStyle.fOptStat;
               if (pt.TestBit(kTakeStyle) || !Number.isInteger(dofit))
                  dofit = gStyle.fOptFit;

               // we take statistic from main painter
               if (main.fillStatistic(this, dostat, dofit)) {
                  // adjust the size of the stats box with the number of lines
                  let nlines = pt.fLines?.arr.length || 0;
                  const set_default = (nlines > 0) && !this.moved_interactive && isDefaultStatPosition(pt),
                        // in ROOT TH2 and TH3 always add full stats for fit parameters
                        extrah = this.#has_fit && (this.#fit_dim > 1) ? gStyle.fStatH : 0;
                  if (extrah)
                     nlines -= this.#fit_cnt;
                  let stath = gStyle.fStatH, statw = gStyle.fStatW;
                  if (this.#has_fit)
                     statw = 1.8 * gStyle.fStatW;
                  if ((gStyle.fStatFontSize <= 0) || (gStyle.fStatFont % 10 === 3))
                     stath = nlines * 0.25 * gStyle.fStatH;
                  else if (gStyle.fStatFontSize < 1)
                     stath = nlines * gStyle.fStatFontSize;

                  if (set_default) {
                     // but fit parameters not used in full size calculations
                     pt.fX1NDC = Math.max(0.005, pt.fX2NDC - statw);
                     pt.fY1NDC = Math.max(0.005, pt.fY2NDC - stath - extrah);
                  } else {
                     // when some NDC values are set directly and not match with each other
                     if (pt.fY1NDC > pt.fY2NDC)
                        pt.fY2NDC = Math.min(0.995, pt.fY1NDC + stath + extrah);
                     if (pt.fX1NDC > pt.fX2NDC)
                        pt.fY2NDC = Math.min(0.995, pt.fX1NDC + statw);
                  }
               }
            }
         }

         const pad_rect = pp.getPadRect(),
               brd = pt.fBorderSize,
               noborder = opt.indexOf('NB') >= 0,
               dx = (opt.indexOf('L') >= 0) ? -1 : ((opt.indexOf('R') >= 0) ? 1 : 0),
               dy = (opt.indexOf('T') >= 0) ? -1 : ((opt.indexOf('B') >= 0) ? 1 : 0),
               g = this.createG(); // container used to recalculate coordinates

         this.#pave_x = Math.round(pt.fX1NDC * pad_rect.width);
         this.#pave_y = Math.round((1.0 - pt.fY2NDC) * pad_rect.height);
         width = Math.round((pt.fX2NDC - pt.fX1NDC) * pad_rect.width);
         height = Math.round((pt.fY2NDC - pt.fY1NDC) * pad_rect.height);

         const arc_radius = opt.indexOf('ARC') >= 0 && (pt.fCornerRadius > 0) ? Math.round(Math.min(width, height) * pt.fCornerRadius) : 0;

         makeTranslate(g, this.#pave_x, this.#pave_y);

         this.createAttLine({ attr: pt, width: (brd > 0) ? pt.fLineWidth : 0 });

         this.createAttFill({ attr: pt });
         // need to fill pave while
         if (this.fillatt.empty() && arc_radius)
            this.fillatt.setSolidColor(this.getColor(pt.fFillColor) || 'white');

         if (pt._typename === clTDiamond) {
            const h2 = Math.round(height / 2), w2 = Math.round(width / 2),
                  dpath = `l${w2},${-h2}l${w2},${h2}l${-w2},${h2}z`;

            if (!this.fillatt.empty())
               this.drawBorder(g, width, height, 0, dpath);

            interactive_element = g.append('svg:path')
                                   .attr('d', 'M0,' + h2 + dpath)
                                   .call(this.fillatt.func)
                                   .call(this.lineatt.func);

            const text_g = g.append('svg:g');
            makeTranslate(text_g, Math.round(width / 4), Math.round(height / 4));

            return this.drawPaveText(w2, h2, arg, text_g);
         }

         if (pt.fNpaves) {
            for (let n = pt.fNpaves - 1; n > 0; --n) {
               g.append('svg:path')
                .attr('d', `M${dx * 4 * n},${dy * 4 * n}h${width}v${height}h${-width}z`)
                .call(this.fillatt.func)
                .call(this.lineatt.func);
            }
         } else
            this.drawBorder(g, width, height, arc_radius);

         if (!this.isBatchMode() || !this.fillatt.empty() || (!this.lineatt.empty() && !noborder)) {
            if (arc_radius) {
               interactive_element = g.append('svg:rect')
                                      .attr('width', width)
                                      .attr('height', height)
                                      .attr('rx', arc_radius);
            } else {
               interactive_element = g.append('svg:path')
                                      .attr('d', `M0,0H${width}V${height}H0Z`);
            }
            interactive_element.call(this.fillatt.func);
            if (!noborder)
               interactive_element.call(this.lineatt.func);
         }

         switch (pt._typename) {
            case clTPaveLabel:
            case clTPaveClass:
               return this.drawPaveLabel(width, height, arg);
            case clTPaveStats:
               return this.drawPaveStats(width, height, arg);
            case clTPaveText:
            case clTPavesText:
            case clTDiamond:
               return this.drawPaveText(width, height, arg);
            case clTLegend:
               return this.drawLegend(width, height, arg);
            case clTPaletteAxis:
               return this.drawPaletteAxis(width, height, arg);
         }
      }).then(() => {
         if (this.isBatchMode() || (pt._typename === clTPave))
            return this;

         // here all kind of interactive settings
         interactive_element?.style('pointer-events', 'visibleFill')
                             .on('mouseenter', () => this.showObjectStatus());

         addDragHandler(this, {
            obj: pt, x: this.#pave_x, y: this.#pave_y, width, height,
            minwidth: 10, minheight: 20, canselect: true,
            ctxmenu: browser.touches && settings.ContextMenu && this.UseContextMenu,
            redraw: () => {
               this.moved_interactive = true;
               this.interactiveRedraw(false, 'pave_moved').then(() => this.drawPave());
            }
         });

         if (this.UseContextMenu && settings.ContextMenu)
            this.getG().on('contextmenu', evnt => this.paveContextMenu(evnt));

         if (this.isPalette())
            this.interactivePaletteAxis(width, height);

         return this;
      });
   }

   drawBorder(draw_g, width, height, arc_radius, diamond) {
      const pt = this.getObject(),
            opt = this.getPaveDrawOption().toUpperCase().replaceAll('ARC', '').replaceAll('NDC', ''),
            noborder = this.isPalette() || (opt.indexOf('NB') >= 0),
            dx = (opt.indexOf('L') >= 0) ? -1 : ((opt.indexOf('R') >= 0) ? 1 : 0),
            dy = (opt.indexOf('T') >= 0) ? -1 : ((opt.indexOf('B') >= 0) ? 1 : 0);

      if ((pt.fBorderSize < 2) || (pt.fShadowColor === 0) || (!dx && !dy) || noborder)
         return;

      const scol = this.getColor(pt.fShadowColor),
            brd = pt.fBorderSize,
            brd_width = !this.lineatt.empty() && (this.lineatt.width > 2) ? `${this.lineatt.width - 1}px` : '1px';

      if (diamond) {
         draw_g.append('svg:path')
               .attr('d', `M0,${Math.round(height / 2) + brd}${diamond}`)
               .style('fill', scol)
               .style('stroke', scol)
               .style('stroke-width', brd_width);
      } else if (arc_radius) {
         draw_g.append('svg:rect')
               .attr('width', width)
               .attr('height', height)
               .attr('rx', arc_radius)
               .attr('x', dx * brd)
               .attr('y', dy * brd)
               .style('fill', scol)
               .style('stroke', scol)
               .style('stroke-width', brd_width);
      } else {
         let spath;

         if ((dx < 0) && (dy < 0))
            spath = `M0,0v${height - brd - 1}h${1 - brd}v${2 - height}h${width - 2}v${brd - 1}z`;
         else if ((dx < 0) && (dy > 0))
            spath = `M0,${height}v${brd + 1 - height}h${1 - brd}v${height - 2}h${width - 2}v${1 - brd}z`;
         else if ((dx > 0) && (dy < 0))
            spath = `M${brd + 1},0v${1 - brd}h${width - 2}v${height - 2}h${1 - brd}v${brd + 1 - height}z`;
         else
            spath = `M${width},${brd + 1}h${brd - 1}v${height - 2}h${2 - width}v${1 - brd}h${width - brd - 2}z`;

         draw_g.append('svg:path')
               .attr('d', spath)
               .style('fill', scol)
               .style('stroke', scol)
               .style('stroke-width', brd_width);
      }
   }

   /** @summary Fill option object used in TWebCanvas */
   fillWebObjectOptions(res) {
      const pave = this.getObject();

      if (pave?.fInit) {
         res.fcust = 'pave';
         res.fopt = [pave.fX1NDC, pave.fY1NDC, pave.fX2NDC, pave.fY2NDC];

         if ((pave.fName === 'stats') && this.isStats()) {
            pave.fLines.arr.forEach(entry => {
               if ((entry._typename === clTText) || (entry._typename === clTLatex))
                  res.fcust += `;;${entry.fTitle}`;
            });
         }
      }

      return res;
   }

   /** @summary draw TPaveLabel object */
   async drawPaveLabel(width, height) {
      const pave = this.getObject();
      if (!pave.fLabel || !pave.fLabel.trim())
         return this;

      this.createAttText({ attr: pave, can_rotate: false });

      return this.startTextDrawingAsync(this.textatt.font, height / 1.2)
                 .then(() => this.drawText(this.textatt.createArg({ width, height, text: pave.fLabel, norotate: true })))
                 .then(() => this.finishTextDrawing());
   }

   /** @summary draw TPaveStats object */
   drawPaveStats(width, height) {
      const pt = this.getObject(), lines = [], colors = [];
      let first_stat = 0, num_cols = 0, maxlen = 0;

      // extract only text
      for (let j = 0; j < pt.fLines.arr.length; ++j) {
         const entry = pt.fLines.arr[j];
         if ((entry._typename === clTText) || (entry._typename === clTLatex)) {
            lines.push(entry.fTitle);
            colors.push(entry.fTextColor);
         }
      }

      const nlines = lines.length;

      // adjust font size
      for (let j = 0; j < nlines; ++j) {
         const line = lines[j];
         if (j > 0)
            maxlen = Math.max(maxlen, line.length);
         if ((j === 0) || (line.indexOf('|') < 0))
            continue;
         if (first_stat === 0)
            first_stat = j;
         const parts = line.split('|');
         if (parts.length > num_cols)
            num_cols = parts.length;
      }

      // for characters like 'p' or 'y' several more pixels required to stay in the box when drawn in last line
      const stepy = height / nlines, margin_x = pt.fMargin * width;
      let has_head = false;

      this.createAttText({ attr: pt, can_rotate: false });

      return this.startTextDrawingAsync(this.textatt.font, height / (nlines * 1.2)).then(() => {
         if (nlines === 1)
            this.drawText(this.textatt.createArg({ width, height, text: lines[0], latex: 1, norotate: true }));
         else {
            for (let j = 0; j < nlines; ++j) {
               const y = j * stepy,
                     color = (colors[j] > 1) ? this.getColor(colors[j]) : this.textatt.color;

               if (first_stat && (j >= first_stat)) {
                  const parts = lines[j].split('|');
                  for (let n = 0; n < parts.length; ++n) {
                     this.drawText({
                        align: 'middle', x: width * n / num_cols, y, latex: 0,
                        width: width / num_cols, height: stepy, text: parts[n], color
                     });
                  }
               } else if (lines[j].indexOf('=') < 0) {
                  if (j === 0) {
                     has_head = true;
                     const max_hlen = Math.max(maxlen, Math.round((width - 2 * margin_x) / stepy / 0.65));
                     if (lines[j].length > max_hlen + 5)
                        lines[j] = lines[j].slice(0, max_hlen + 2) + '...';
                  }
                  this.drawText({
                     align: (j === 0) ? 'middle' : 'start', x: margin_x, y,
                     width: width - 2 * margin_x, height: stepy, text: lines[j], color
                  });
               } else {
                  const parts = lines[j].split('='), args = [];

                  for (let n = 0; n < 2; ++n) {
                     const arg = {
                        align: (n === 0) ? 'start' : 'end', x: margin_x, y,
                        width: width - 2 * margin_x, height: stepy, text: n > 0 ? parts[n].trimStart() : parts[n].trimEnd(), color,
                        _expected_width: width - 2 * margin_x, _args: args,
                        post_process(painter) {
                           if (this._args[0].ready && this._args[1].ready)
                              painter.scaleTextDrawing(1.05 * (this._args[0].result_width + this._args[1].result_width) / this._expected_width, painter.getG());
                        }
                     };
                     args.push(arg);
                  }

                  for (let n = 0; n < 2; ++n)
                     this.drawText(args[n]);
               }
            }
         }

         let lpath = '';

         if ((pt.fBorderSize > 0) && has_head)
            lpath += `M0,${Math.round(stepy)}h${width}`;

         if ((first_stat > 0) && (num_cols > 1)) {
            for (let nrow = first_stat; nrow < nlines; ++nrow)
               lpath += `M0,${Math.round(nrow * stepy)}h${width}`;
            for (let ncol = 0; ncol < num_cols - 1; ++ncol)
               lpath += `M${Math.round(width / num_cols * (ncol + 1))},${Math.round(first_stat * stepy)}V${height}`;
         }

         if (lpath)
            this.appendPath(lpath).call(this.lineatt.func);

         // this.getG().classed('most_upper_primitives', true); // this primitive will remain on top of list

         return this.finishTextDrawing(undefined, (nlines > 1));
      });
   }

   /** @summary draw TPaveText object */
   async drawPaveText(width, height, _dummy_arg, text_g) {
      const pt = this.getObject(),
            arr = pt.fLines?.arr || [],
            nlines = arr.length,
            pp = this.getPadPainter(),
            pad_height = pp.getPadHeight(),
            draw_header = pt.fLabel.length,
            promises = [],
            margin_x = pt.fMargin * width,
            stepy = height / (nlines || 1),
            dflt_font_size = 0.85 * stepy;
      let max_font_size = 0;

      this.createAttText({ attr: pt, can_rotate: false });

      // for single line (typically title) limit font size
      if ((nlines === 1) && (this.textatt.size > 0))
         max_font_size = Math.max(3, this.textatt.getSize(pp));

      if (!text_g)
         text_g = this.getG();

      const fast = (nlines === 1) && pp.isFastDrawing();
      let num_txt = 0, num_custom = 0, longest_line = 0, alt_text_size = 0;

      arr.forEach(entry => {
         if (((entry._typename !== clTText) && (entry._typename !== clTLatex)) || !entry.fTitle?.trim())
            return;
         num_txt++;
         if (entry.fX || entry.fY || entry.fTextSize)
            num_custom++;

         if (!entry.fTextSize && !this.textatt.size)
            longest_line = Math.max(longest_line, approximateLabelWidth(entry.fTitle, this.textatt.font, dflt_font_size));
      });

      if (longest_line) {
         alt_text_size = dflt_font_size;
         if (longest_line > 0.92 * width)
            alt_text_size *= (0.92 * width / longest_line);
         alt_text_size = Math.round(alt_text_size);
      }

      const pr = (num_txt > num_custom) ? this.startTextDrawingAsync(this.textatt.font, this.$postitle ? this.textatt.getSize(pp, 1, 0.05) : dflt_font_size, text_g, max_font_size) : Promise.resolve();

      return pr.then(() => {
         for (let nline = 0; nline < nlines; ++nline) {
            const entry = arr[nline], texty = nline * stepy;

            switch (entry._typename) {
               case clTText:
               case clTLatex: {
                  if (!entry.fTitle || !entry.fTitle.trim())
                     continue;

                  let color = entry.fTextColor ? this.getColor(entry.fTextColor) : '';
                  if (!color)
                     color = this.textatt.color;
                  const align = entry.fTextAlign || this.textatt.align,
                        valign = align % 10,
                        halign = (align - valign) / 10;

                  if (entry.fX || entry.fY || entry.fTextSize) {
                     // individual positioning
                     const x = entry.fX ? entry.fX * width : (halign === 1 ? margin_x : (halign === 2 ? width / 2 : width - margin_x)),
                           y = entry.fY ? (1 - entry.fY) * height : (texty + (valign === 2 ? stepy / 2 : (valign === 3 ? stepy : 0))),
                           draw_g = text_g.append('svg:g');

                     promises.push(this.startTextDrawingAsync(this.textatt.font, this.textatt.getAltSize(entry.fTextSize, pp) || alt_text_size, draw_g)
                                       .then(() => this.drawText({
                                          align, x, y, text: entry.fTitle, color,
                                          latex: (entry._typename === clTText) ? 0 : 1, draw_g, fast
                                       }))
                                       .then(() => this.finishTextDrawing(draw_g)));
                  } else {
                     const arg = {
                        x: 0, y: texty, draw_g: text_g,
                        latex: (entry._typename === clTText) ? 0 : 1,
                        text: entry.fTitle, color, fast
                     };

                     if (this.$postitle) {
                        // remember box produced by title text
                        arg.post_process = function(painter) {
                           painter.$titlebox = this.box;
                        };
                     } else {
                        arg.align = align;
                        arg.x = (halign === 1) ? margin_x : 0;
                        arg.width = (halign === 2) ? width : width - margin_x;
                        arg.y = texty + 0.05 * stepy;
                        arg.height = 0.9 * stepy;
                        // prevent expand of normal title on full width
                        // if (this.isTitle() && (halign === 2) && (arg.width > 0.1*pad_width) && (arg.width < 0.7*pad_width)) {
                        //   arg.width -= 0.02*pad_width;
                        //   arg.x = 0.01*pad_width;
                        // }
                     }

                     this.drawText(arg);
                  }
                  break;
               }

               case clTLine: {
                  const lx1 = entry.fX1 ? Math.round(entry.fX1 * width) : 0,
                        lx2 = entry.fX2 ? Math.round(entry.fX2 * width) : width,
                        ly1 = entry.fY1 ? Math.round((1 - entry.fY1) * height) : Math.round(texty + stepy * 0.5),
                        ly2 = entry.fY2 ? Math.round((1 - entry.fY2) * height) : Math.round(texty + stepy * 0.5),
                        lineatt = this.createAttLine(entry);
                  text_g.append('svg:path')
                        .attr('d', `M${lx1},${ly1}L${lx2},${ly2}`)
                        .call(lineatt.func);
                  break;
               }
               case clTBox: {
                  const bx1 = entry.fX1 ? Math.round(entry.fX1 * width) : 0,
                        bx2 = entry.fX2 ? Math.round(entry.fX2 * width) : width,
                        by1 = entry.fY1 ? Math.round((1 - entry.fY1) * height) : Math.round(texty),
                        by2 = entry.fY2 ? Math.round((1 - entry.fY2) * height) : Math.round(texty + stepy),
                        fillatt = this.createAttFill(entry);
                  text_g.append('svg:path')
                        .attr('d', `M${bx1},${by1}H${bx2}V${by2}H${bx1}Z`)
                        .call(fillatt.func);
                  break;
               }
            }
         }

         if (num_txt > num_custom)
            promises.push(this.finishTextDrawing(text_g, num_txt > num_custom + 1));

         if (this.isTitle())
            this.getG().style('display', !num_txt ? 'none' : null);


         return Promise.all(promises).then(() => this);
      }).then(() => {
         if (!draw_header)
            return;

         const w = Math.round(width * 0.5),
               h = Math.round(pad_height * 0.04),
               lbl_g = text_g.append('svg:g');

         makeTranslate(lbl_g, Math.round(width * 0.25), Math.round(-pad_height * 0.02));

         this.drawBorder(lbl_g, w, h);

         lbl_g.append('svg:path')
               .attr('d', `M${0},${0}h${w}v${h}h${-w}z`)
               .call(this.fillatt.func)
               .call(this.lineatt.func);

         return this.startTextDrawingAsync(this.textatt.font, 0.9 * h, lbl_g)
                    .then(() => this.drawText({ align: 22, x: 0, y: 0, width: w, height: h, text: pt.fLabel, color: this.textatt.color, draw_g: lbl_g }))
                    .then(() => promises.push(this.finishTextDrawing(lbl_g)));
      }).then(() => { return this; });
   }

   /** @summary Method used to convert value to string according specified format
     * @desc format can be like 5.4g or 4.2e or 6.4f or 'stat' or 'fit' or 'entries' */
   format(value, fmt) {
      if (!fmt)
         fmt = 'stat';

      const pave = this.getObject();

      switch (fmt) {
         case 'stat':
            fmt = pave.fStatFormat || gStyle.fStatFormat;
            break;
         case 'fit':
            fmt = pave.fFitFormat || gStyle.fFitFormat;
            break;
         case 'entries':
            if ((Math.abs(value) < 1e9) && (Math.round(value) === value))
               return value.toFixed(0);
            fmt = '14.7g';
            break;
      }

      return floatToString(value, fmt || '6.4g');
   }

   /** @summary Draw TLegend object */
   drawLegend(w, h) {
      const legend = this.getObject(),
            nlines = legend.fPrimitives.arr.length,
            ncols = Math.max(1, legend.fNColumns);
      let nrows = Math.round(nlines / ncols),
          any_text = false, has_header = false,
          custom_textg = false; // each text entry has own attributes

      if (nrows * ncols < nlines)
         nrows++;

      const isEmpty = entry => !entry.fObject && !entry.fOption && (!entry.fLabel || !entry.fLabel.trim());

      for (let ii = 0; ii < nlines; ++ii) {
         const entry = legend.fPrimitives.arr[ii];
         if ((ii === 0) && (entry.fOption === 'h'))
            has_header = true;
         if (isEmpty(entry)) {
            if (ncols === 1)
               nrows--;
         } else if (entry.fLabel) {
            any_text = true;
            if ((entry.fTextFont && (entry.fTextFont !== legend.fTextFont)) ||
                (entry.fTextSize && (entry.fTextSize !== legend.fTextSize)))
               custom_textg = true;
         }
      }

      if (has_header && (ncols > 1) && ((nrows - 1) * ncols < nlines - 1))
         nrows++;

      if (nrows < 1)
         nrows = 1;

      const padding_x = Math.round(0.03 * w / ncols),
            padding_y = Math.round(0.03 * h),
            row_height = (h - 2 * padding_y) / (nrows + (nrows - 1) * legend.fEntrySeparation),
            gap_y = row_height * legend.fEntrySeparation;

      let gap_x = padding_x,
          column_width0 = (w - 2 * padding_x - (ncols - 1) * gap_x) / ncols;

      if (legend.fColumnSeparation) {
         column_width0 = (w - 2 * padding_x) / (ncols + (ncols - 1) * legend.fColumnSeparation);
         gap_x = column_width0 * legend.fColumnSeparation;
      }

      // calculate positions of columns by weight - means more letters, more weight
      const column_pos = new Array(ncols + 1).fill(padding_x),
            column_boxwidth = column_width0 * legend.fMargin;
      if (ncols > 1) {
         const column_weight = new Array(ncols).fill(1),
               space_for_text = w - 2 * padding_x - (ncols - 1) * gap_x - ncols * column_boxwidth;

         for (let ii = 0; ii < nlines; ++ii) {
            const entry = legend.fPrimitives.arr[ii];
            if (isEmpty(entry))
               continue; // let discard empty entry
            const icol = ii % ncols;
            column_weight[icol] = Math.max(column_weight[icol], entry.fLabel.length);
         }

         let sum_weight = 0;
         for (let icol = 0; icol < ncols; ++icol)
            sum_weight += column_weight[icol];

         for (let icol = 0; icol < ncols - 1; ++icol)
            column_pos[icol + 1] = column_pos[icol] + column_boxwidth + column_weight[icol] / sum_weight * space_for_text + gap_x;
      }
      column_pos[ncols] = w - padding_x;

      let font_size = row_height,
          max_font_size = 0, // not limited in the beginning
          any_opt = false;

      this.createAttText({ attr: legend, can_rotate: false });

      const pp = this.getPadPainter(),
            tsz = this.textatt.getSize(pp);
      if (tsz && (tsz < font_size))
         font_size = max_font_size = tsz;

      const text_promises = [],
            pr = any_text && !custom_textg ? this.startTextDrawingAsync(this.textatt.font, font_size, undefined, max_font_size) : Promise.resolve();

      return pr.then(() => {
         for (let ii = 0, i = -1; ii < nlines; ++ii) {
            const entry = legend.fPrimitives.arr[ii];
            if (isEmpty(entry))
               continue; // let discard empty entry

            if (ncols === 1)
               ++i;
            else if (!has_header || ii === 0)
               i = ii;
            else
               i = ii - 1 + ncols;

            const lopt = entry.fOption.toLowerCase(),
                  icol = i % ncols, irow = (i - icol) / ncols,
                  x0 = Math.round(column_pos[icol]),
                  y0 = Math.round(padding_y + irow * (row_height + gap_y)),
                  tpos_x = Math.round(x0 + column_boxwidth),
                  mid_x = Math.round(x0 + (column_boxwidth - padding_x) / 2),
                  box_y = Math.round(y0 + row_height * 0.1),
                  box_height = Math.round(row_height * 0.8),
                  mid_y = Math.round(y0 + row_height * 0.5), // center line
                  mo = entry.fObject,
                  draw_fill = lopt.indexOf('f') !== -1,
                  draw_line = lopt.indexOf('l') !== -1,
                  draw_error = lopt.indexOf('e') !== -1,
                  draw_marker = lopt.indexOf('p') !== -1;

            let o_fill = entry, o_marker = entry, o_line = entry,
                painter = null, isany = false;

            if (isObject(mo)) {
               if ('fLineColor' in mo)
                  o_line = mo;
               if ('fFillColor' in mo)
                  o_fill = mo;
               if ('fMarkerColor' in mo)
                  o_marker = mo;
               painter = pp.findPainterFor(mo);
            }

            // Draw fill pattern (in a box)
            if (draw_fill) {
               const fillatt = painter?.fillatt?.used ? painter.fillatt : this.createAttFill(o_fill);
               let lineatt;
               if (!draw_line && !draw_error && !draw_marker) {
                  lineatt = painter?.lineatt?.used ? painter.lineatt : this.createAttLine(o_line);
                  if (lineatt.empty())
                     lineatt = null;
               }

               if (!fillatt.empty() || lineatt) {
                  isany = true;
                  // define x,y as the center of the symbol for this entry
                  this.appendPath(`M${x0},${box_y}v${box_height}h${tpos_x - padding_x - x0}v${-box_height}z`)
                      .call(fillatt.func)
                      .call(lineatt ? lineatt.func : () => {});
               }
            }

            // Draw line and/or error (when specified)
            if (draw_line || draw_error) {
               const lineatt = painter?.lineatt?.used ? painter.lineatt : this.createAttLine(o_line);
               if (!lineatt.empty()) {
                  isany = true;
                  if (draw_line) {
                     this.appendPath(`M${x0},${mid_y}h${tpos_x - padding_x - x0}`)
                         .call(lineatt.func);
                  }
                  if (draw_error) {
                     let endcaps = 0, edx = row_height * 0.05;
                     if (isFunc(painter?.getHisto) && painter.options?.ErrorKind === 1)
                        endcaps = 1; // draw bars for e1 option in histogram
                     else if (isFunc(painter?.getGraph) && mo?.fLineWidth !== undefined && mo?.fMarkerSize !== undefined) {
                        endcaps = painter.options?.Ends ?? 1; // default is 1
                        edx = mo.fLineWidth + gStyle.fEndErrorSize;
                        if (endcaps > 1)
                           edx = Math.max(edx, mo.fMarkerSize * 8 * 0.66);
                     }

                     const eoff = (endcaps === 3) ? 0.2 : 0,
                           ey1 = Math.round(y0 + row_height * eoff),
                           ey2 = Math.round(y0 + row_height * (1 - eoff)),
                           edy = Math.round(edx * 0.66);
                     edx = Math.round(edx);
                     let path = `M${mid_x},${ey1}V${ey2}`;
                     switch (endcaps) {
                        case 1: path += `M${mid_x - edx},${ey1}h${2 * edx}M${mid_x - edx},${ey2}h${2 * edx}`; break; // bars
                        case 2: path += `M${mid_x - edx},${ey1 + edy}v${-edy}h${2 * edx}v${edy}M${mid_x - edx},${ey2 - edy}v${edy}h${2 * edx}v${-edy}`; break; // ]
                        case 3: path += `M${mid_x - edx},${ey1}h${2 * edx}l${-edx},${-edy}zM${mid_x - edx},${ey2}h${2 * edx}l${-edx},${edy}z`; break; // triangle
                        case 4: path += `M${mid_x - edx},${ey1 + edy}l${edx},${-edy}l${edx},${edy}M${mid_x - edx},${ey2 - edy}l${edx},${edy}l${edx},${-edy}`; break; // arrow
                     }
                     this.appendPath(path)
                         .call(lineatt.func)
                         .style('fill', endcaps > 1 ? 'none' : null);
                  }
               }
            }

            // Draw Poly marker
            if (draw_marker) {
               const marker = painter?.markeratt?.used ? painter.markeratt : this.createAttMarker(o_marker);
               if (!marker.empty()) {
                  isany = true;
                  this.appendPath(marker.create(mid_x, mid_y))
                      .call(marker.func);
               }
            }

            // special case - nothing draw, try to show rect with line attributes
            if (!isany && painter?.lineatt && !painter.lineatt.empty()) {
               this.appendPath(`M${x0},${box_y}v${box_height}h${tpos_x - padding_x - x0}v${-box_height}z`)
                   .style('fill', 'none')
                   .call(painter.lineatt.func);
            }

            let pos_x = tpos_x, arg_width = Math.round(column_pos[icol + 1] - pos_x);
            if (isStr(lopt) && (lopt.toLowerCase() !== 'h'))
               any_opt = true;
            else if (!any_opt) {
               pos_x = padding_x;
               arg_width = w - 2 * padding_x;
            }

            if (entry.fLabel) {
               const textatt = this.createAttText({ attr: entry, std: false, attr_alt: legend }),
                     arg = {
                        draw_g: this.getG(), align: textatt.align,
                        x: pos_x, width: arg_width,
                        y: y0, height: Math.round(row_height),
                        scale: (custom_textg && !entry.fTextSize) || !legend.fTextSize,
                        text: entry.fLabel, color: textatt.color
                     };
               if (custom_textg) {
                  arg.draw_g = this.getG().append('svg:g');
                  text_promises.push(this.startTextDrawingAsync(textatt.font, textatt.getSize(pp), arg.draw_g, max_font_size)
                                       .then(() => this.drawText(arg))
                                       .then(() => this.finishTextDrawing(arg.draw_g)));
               } else
                  this.drawText(arg);
            }
         }

         if (any_text && !custom_textg)
            text_promises.push(this.finishTextDrawing());

         // rescale after all entries are shown
         return Promise.all(text_promises);
      });
   }

   /** @summary Returns true if palette drawn in vertical direction */
   isPaletteVertical() { return this.#palette_vertical; }

   /** @summary draw color palette with axis */
   drawPaletteAxis(s_width, s_height, arg) {
      const palette = this.getObject(),
            axis = palette.fAxis,
            g = this.getG(),
            can_move = isStr(arg) && (arg.indexOf('can_move') >= 0),
            postpone_draw = isStr(arg) && (arg.indexOf('postpone') >= 0),
            cjust = isStr(arg) && (arg.indexOf('cjust') >= 0),
            bring_stats_front = isStr(arg) && (arg.indexOf('bring_stats_front') >= 0),
            pp = this.getPadPainter(),
            width = pp.getPadWidth(),
            height = pp.getPadHeight(),
            pad = pp.getRootPad(true),
            main = palette.$main_painter || this.getMainPainter(),
            fp = this.getFramePainter(),
            contour = main.getContour(false),
            levels = contour?.getLevels(),
            is_th3 = isFunc(main.getDimension) && (main.getDimension() === 3),
            is_scatter = isFunc(main.getZaxis),
            log = pad?.fLogv ?? (is_th3 ? false : pad?.fLogz),
            draw_palette = main.getHistPalette(),
            zaxis = is_scatter ? main.getZaxis() : main.getObject()?.fZaxis,
            sizek = pad?.fTickz ? 0.35 : 0.7;

      let zmin = 0, zmax = 100, gzmin, gzmax, axis_transform, axis_second = 0;

      this.#palette_vertical = (palette.fX2NDC - palette.fX1NDC) < (palette.fY2NDC - palette.fY1NDC);

      axis.fTickSize = 0.03; // adjust axis ticks size

      if ((typeof zaxis?.fLabelOffset !== 'undefined') && !is_th3) {
         axis.fBits = zaxis.fBits & ~EAxisBits.kTickMinus & ~EAxisBits.kTickPlus;
         axis.fTitle = zaxis.fTitle;
         axis.fTickSize = zaxis.fTickLength;
         axis.fTitleSize = zaxis.fTitleSize;
         axis.fTitleOffset = zaxis.fTitleOffset;
         axis.fTextColor = zaxis.fTitleColor;
         axis.fTextFont = zaxis.fTitleFont;
         axis.fLineColor = zaxis.fAxisColor;
         axis.fLabelSize = zaxis.fLabelSize;
         axis.fLabelColor = zaxis.fLabelColor;
         axis.fLabelFont = zaxis.fLabelFont;
         axis.fLabelOffset = zaxis.fLabelOffset;
         this.z_handle.setHistPainter(main, is_scatter ? 'hist#z' : 'z');
         this.z_handle.source_axis = zaxis;
      }

      if (contour && fp && !is_th3) {
         if ((fp.zmin !== undefined) && (fp.zmax !== undefined) && (fp.zmin !== fp.zmax)) {
            gzmin = fp.zmin;
            gzmax = fp.zmax;
            zmin = fp.zoom_zmin;
            zmax = fp.zoom_zmax;
            if (zmin === zmax) {
               zmin = gzmin;
               zmax = gzmax;
            }
         } else {
            zmin = levels.at(0);
            zmax = levels.at(-1);
         }
      } else if ((main.gmaxbin !== undefined) && (main.gminbin !== undefined)) {
         // this is case of TH2 (needs only for size adjustment)
         zmin = main.gminbin;
         zmax = main.gmaxbin;
      } else if ((main.hmin !== undefined) && (main.hmax !== undefined)) {
         // this is case of TH1
         zmin = main.hmin;
         zmax = main.hmax;
      }

      g.selectAll('rect').style('fill', 'white');

      if ((gzmin === undefined) || (gzmax === undefined) || (gzmin === gzmax)) {
         gzmin = zmin;
         gzmax = zmax;
      }

      if (this.#palette_vertical) {
         this.#swap_side = palette.fX2NDC < 0.5;
         axis.fChopt = 'S+' + (this.#swap_side ? 'R' : 'L'); // clearly configure text align
         this.z_handle.configureAxis('zaxis', gzmin, gzmax, zmin, zmax, true, [0, s_height], { log, fixed_ticks: cjust ? levels : null, maxTickSize: Math.round(s_width * sizek), swap_side: this.#swap_side, minposbin: main.gminposbin });
         axis_transform = this.#swap_side ? null : `translate(${s_width})`;
         if (pad?.fTickz)
            axis_second = this.#swap_side ? s_width : -s_width;
      } else {
         this.#swap_side = palette.fY1NDC > 0.5;
         axis.fChopt = 'S+';
         this.z_handle.configureAxis('zaxis', gzmin, gzmax, zmin, zmax, false, [0, s_width], { log, fixed_ticks: cjust ? levels : null, maxTickSize: Math.round(s_height * sizek), swap_side: this.#swap_side, minposbin: main.gminposbin });
         axis_transform = this.#swap_side ? null : `translate(0,${s_height})`;
         if (pad?.fTickz)
            axis_second = this.#swap_side ? s_height : -s_height;
      }

      if (!contour || !draw_palette || postpone_draw) {
         // we need such rect to correctly calculate size
         this.appendPath(`M0,0H${s_width}V${s_height}H0Z`)
             .style('fill', 'white');
      } else {
         for (let i = 0; i < levels.length - 1; ++i) {
            let z0 = Math.round(this.z_handle.gr(levels[i])),
                z1 = Math.round(this.z_handle.gr(levels[i + 1])),
                portion = 0.5, d;

            // when not full range fit to the drawn range,
            // calculate portion value that it approximately in the
            // middle of the still visible area

            if (this.#palette_vertical) {
               if ((z1 >= s_height) || (z0 < 0))
                  continue;
               z0 += 1; // ensure correct gap filling between colors

               if (z0 > s_height) {
                  if (z0 > z1 + 1)
                     portion = 0.5 * (s_height - z1) / (z0 - z1 - 1);
                  z0 = s_height;
                  if (z1 < 0)
                     z1 = 0;
               } else if (z1 < 0) {
                  if (z0 > 1)
                     portion = 1 - 0.5 * z0 / (z0 - z1 - 1);
                  z1 = 0;
               }
               d = `M0,${z1}H${s_width}V${z0}H0Z`;
            } else {
               if ((z0 >= s_width) || (z1 < 0))
                  continue;
               z1 += 1; // ensure correct gap filling between colors

               if (z1 > s_width) {
                  if (z1 > z0 + 1)
                     portion = 1 - 0.5 * (s_width - z0) / (z1 - z0 - 1);
                  z1 = s_width;
                  if (z0 < 0)
                     z0 = 0;
               } else if (z0 < 0) {
                  if (z1 > 1)
                     portion = 0.5 * (z1 - 1) / (z1 - z0 - 1);
                  z0 = 0;
               }
               d = `M${z0},0V${s_height}H${z1}V0Z`;
            }

            const lvl = levels[i] * portion + levels[i + 1] * (1 - portion),
                  col = contour.getPaletteColor(draw_palette, lvl);
            if (!col)
               continue;

            // console.log('z0, z1', z0, z1, 'height', s_height, 'col', col, 'portion', portion)

            const r = this.appendPath(d)
                          .style('fill', col)
                          .property('fill0', col)
                          .property('fill1', rgb(col).darker(0.5).formatRgb());

            if (this.isBatchMode())
               continue;

            if (this.isTooltipAllowed()) {
               r.on('mouseover', function() {
                  select(this).transition().duration(100).style('fill', select(this).property('fill1'));
               }).on('mouseout', function() {
                  select(this).transition().duration(100).style('fill', select(this).property('fill0'));
               }).append('svg:title').text(this.z_handle.axisAsText(levels[i]) + ' - ' + this.z_handle.axisAsText(levels[i + 1]));
            }

            if (settings.Zooming)
               r.on('dblclick', () => this.getFramePainter().unzoomSingle('z'));
         }
      }

      if (bring_stats_front)
         this.getPadPainter()?.findPainterFor(null, '', clTPaveStats)?.bringToFront();

      return this.z_handle.drawAxis(g, s_width, s_height, axis_transform, axis_second).then(() => {
         let rect;
         if (can_move) {
            if (settings.ApproxTextSize || isNodeJs()) {
               // for batch testing provide approx estimation
               rect = { x: this.#pave_x, y: this.#pave_y, width: s_width, height: s_height };
               const fsz = this.z_handle.labelsFont?.size || 14;
               if (this.#palette_vertical) {
                  const dx = (this.z_handle._maxlbllen || 3) * 0.6 * fsz;
                  rect.width += dx;
                  if (this.#swap_side)
                     rect.x -= dx;
               } else {
                  rect.height += fsz;
                  if (this.#swap_side)
                     rect.y -= fsz;
               }
            } else if ('getBoundingClientRect' in g.node())
               rect = g.node().getBoundingClientRect();
         }
         if (!rect)
            return this;

         if (this.#palette_vertical) {
            const shift = (this.#pave_x + parseInt(rect.width)) - Math.round(0.995 * width) + 3;

            if (shift > 0) {
               this.#pave_x -= shift;
               makeTranslate(g, this.#pave_x, this.#pave_y);
               palette.fX1NDC -= shift / width;
               palette.fX2NDC -= shift / width;
            }
         } else {
            const shift = Math.round((1.05 - gStyle.fTitleY) * height) - rect.y;
            if (shift > 0) {
               this.#pave_y += shift;
               makeTranslate(g, this.#pave_x, this.#pave_y);
               palette.fY1NDC -= shift / height;
               palette.fY2NDC -= shift / height;
            }
         }

         return this;
      });
   }

   /** @summary Add interactive methods for palette drawing */
   interactivePaletteAxis(s_width, s_height) {
      let doing_zoom = false, sel1 = 0, sel2 = 0, zoom_rect = null;

      const moveRectSel = evnt => {
         if (!doing_zoom)
            return;
         evnt.preventDefault();

         const m = pointer(evnt, this.getG().node());
         if (this.#palette_vertical) {
            sel2 = Math.min(Math.max(m[1], 0), s_height);
            zoom_rect.attr('y', Math.min(sel1, sel2))
                     .attr('height', Math.abs(sel2 - sel1));
         } else {
            sel2 = Math.min(Math.max(m[0], 0), s_width);
            zoom_rect.attr('x', Math.min(sel1, sel2))
                     .attr('width', Math.abs(sel2 - sel1));
         }
      }, endRectSel = evnt => {
         if (!doing_zoom)
            return;

         evnt.preventDefault();
         select(window).on('mousemove.colzoomRect', null)
                          .on('mouseup.colzoomRect', null);
         zoom_rect.remove();
         zoom_rect = null;
         doing_zoom = false;

         const z1 = this.z_handle.revertPoint(sel1),
               z2 = this.z_handle.revertPoint(sel2);

         this.getFramePainter().zoomSingle('z', Math.min(z1, z2), Math.max(z1, z2), true);
      }, startRectSel = evnt => {
         // ignore when touch selection is activated
         if (doing_zoom)
            return;
         doing_zoom = true;

         evnt.preventDefault();
         evnt.stopPropagation();

         const origin = pointer(evnt, this.getG().node());

         zoom_rect = this.getG().append('svg:rect').attr('id', 'colzoomRect').call(addHighlightStyle, true);

         if (this.#palette_vertical) {
            sel1 = sel2 = origin[1];
            zoom_rect.attr('x', '0')
                     .attr('width', s_width)
                     .attr('y', sel1)
                     .attr('height', 1);
         } else {
            sel1 = sel2 = origin[0];
            zoom_rect.attr('x', sel1)
                     .attr('width', 1)
                     .attr('y', 0)
                     .attr('height', s_height);
         }

         select(window).on('mousemove.colzoomRect', moveRectSel)
                          .on('mouseup.colzoomRect', endRectSel, true);
      };

      if (settings.Zooming) {
         this.getG().selectAll('.axis_zoom')
                    .on('mousedown', startRectSel)
                    .on('dblclick', () => this.getFramePainter().zoomSingle('z', 0, 0, true));
      }

      if (settings.ZoomWheel) {
         this.getG().on('wheel', evnt => {
            const pos = pointer(evnt, this.getG().node()),
                  coord = this.#palette_vertical ? (1 - pos[1] / s_height) : pos[0] / s_width,
                  item = this.z_handle.analyzeWheelEvent(evnt, coord);
            if (item?.changed)
               this.getFramePainter().zoomSingle('z', item.min, item.max, true);
         });
      }
   }

   /** @summary Fill context menu items for the TPave object */
   fillContextMenuItems(menu) {
      const pave = this.getObject(),
            set_opt = this.isStats() ? 'SetOption' : 'SetDrawOption';

      menu.sub('Shadow');
      menu.addSizeMenu('size', 0, 12, 1, pave.fBorderSize, arg => {
         pave.fBorderSize = arg;
         this.interactiveRedraw(true, `exec:SetBorderSize(${arg})`);
      });
      menu.addColorMenu('color', pave.fShadowColor, arg => {
         pave.fShadowColor = arg;
         this.interactiveRedraw(true, getColorExec(arg, 'SetShadowColor'));
      });
      const posarr = ['nb', 'tr', 'tl', 'br', 'bl'];
      let value = '', opt = this.getPaveDrawOption(), remain = opt;
      posarr.forEach(nn => {
         const p = remain.indexOf(nn);
         if ((p >= 0) && !value) {
            value = nn;
            remain = remain.slice(0, p) + remain.slice(p + nn.length);
         }
      });
      menu.addSelectMenu('positon', posarr, value || 'nb', arg => {
         arg += remain;
         this.setPaveDrawOption(arg);
         this.interactiveRedraw(true, `exec:${set_opt}("${arg}")`);
      }, 'Direction of pave shadow or nb - off');
      menu.endsub();

      menu.sub('Corner');
      const parc = opt.toLowerCase().indexOf('arc');
      menu.addchk(parc >= 0, 'arc', flag => {
         if (flag)
            opt += ' arc';
         else
            opt = opt.slice(0, parc) + opt.slice(parc + 3);
         this.setPaveDrawOption(opt);
         this.interactiveRedraw(true, `exec:${set_opt}("${opt}")`);
      }, 'Usage of ARC draw option');
      menu.addSizeMenu('radius', 0, 0.2, 0.02, pave.fCornerRadius, val => {
         pave.fCornerRadius = val;
         this.interactiveRedraw(true, `exec:SetCornerRadius(${val})`);
      }, 'Corner radius when ARC is enabled');
      menu.endsub();

      if (this.isStats() || this.isPaveText() || this.isPavesText()) {
         menu.add('Label', () => menu.input('Enter new label', pave.fLabel).then(lbl => {
            pave.fLabel = lbl;
            this.interactiveRedraw('pad', `exec:SetLabel("${lbl}")`);
         }));
         menu.addSizeMenu('Margin', 0, 0.2, 0.02, pave.fMargin, val => {
            pave.fMargin = val;
            this.interactiveRedraw(true, `exec:SetMargin(${val})`);
         });
      }

      if (this.isStats()) {
         menu.add('Default position', () => {
            pave.fX2NDC = gStyle.fStatX;
            pave.fX1NDC = pave.fX2NDC - gStyle.fStatW;
            pave.fY2NDC = gStyle.fStatY;
            pave.fY1NDC = pave.fY2NDC - gStyle.fStatH;
            pave.fInit = 1;
            this.interactiveRedraw(true, 'pave_moved');
         });

         menu.add('Save to gStyle', () => {
            gStyle.fStatX = pave.fX2NDC;
            gStyle.fStatW = pave.fX2NDC - pave.fX1NDC;
            gStyle.fStatY = pave.fY2NDC;
            gStyle.fStatH = pave.fY2NDC - pave.fY1NDC;
            this.fillatt?.saveToStyle('fStatColor', 'fStatStyle');
            gStyle.fStatTextColor = pave.fTextColor;
            gStyle.fStatFontSize = pave.fTextSize;
            gStyle.fStatFont = pave.fTextFont;
            gStyle.fFitFormat = pave.fFitFormat;
            gStyle.fStatFormat = pave.fStatFormat;
            gStyle.fOptStat = pave.fOptStat;
            gStyle.fOptFit = pave.fOptFit;
         }, 'Store stats attributes to gStyle');

         menu.separator();

         menu.add('SetStatFormat', () => {
            menu.input('Enter StatFormat', pave.fStatFormat).then(fmt => {
               if (fmt) {
                  pave.fStatFormat = fmt;
                  this.interactiveRedraw(true, `exec:SetStatFormat("${fmt}")`);
               }
            });
         });
         menu.add('SetFitFormat', () => {
            menu.input('Enter FitFormat', pave.fFitFormat).then(fmt => {
               if (fmt) {
                  pave.fFitFormat = fmt;
                  this.interactiveRedraw(true, `exec:SetFitFormat("${fmt}")`);
               }
            });
         });
         menu.sub('SetOptStat', () => {
            menu.input('Enter OptStat', pave.fOptStat, 'int').then(fmt => {
               pave.fOptStat = fmt;
               this.interactiveRedraw(true, `exec:SetOptStat(${fmt})`);
            });
         });
         const addStatOpt = (pos, name) => {
            let sopt = (pos < 10) ? pave.fOptStat : pave.fOptFit;
            sopt = parseInt(parseInt(sopt) / parseInt(Math.pow(10, pos % 10))) % 10;
            menu.addchk(sopt, name, sopt * 100 + pos, arg => {
               const oldopt = parseInt(arg / 100);
               let newopt = (arg % 100 < 10) ? pave.fOptStat : pave.fOptFit;
               newopt -= (oldopt > 0 ? oldopt : -1) * parseInt(Math.pow(10, arg % 10));
               if (arg % 100 < 10) {
                  pave.fOptStat = newopt;
                  this.interactiveRedraw(true, `exec:SetOptStat(${newopt})`);
               } else {
                  pave.fOptFit = newopt;
                  this.interactiveRedraw(true, `exec:SetOptFit(${newopt})`);
               }
            });
         };

         addStatOpt(0, 'Histogram name');
         addStatOpt(1, 'Entries');
         addStatOpt(2, 'Mean');
         addStatOpt(3, 'Std Dev');
         addStatOpt(4, 'Underflow');
         addStatOpt(5, 'Overflow');
         addStatOpt(6, 'Integral');
         addStatOpt(7, 'Skewness');
         addStatOpt(8, 'Kurtosis');
         menu.endsub();

         menu.sub('SetOptFit', () => {
            menu.input('Enter OptStat', pave.fOptFit, 'int').then(fmt => {
               pave.fOptFit = fmt;
               this.interactiveRedraw(true, `exec:SetOptFit(${fmt})`);
            });
         });
         addStatOpt(10, 'Fit parameters');
         addStatOpt(11, 'Par errors');
         addStatOpt(12, 'Chi square / NDF');
         addStatOpt(13, 'Probability');
         menu.endsub();

         menu.separator();
      } else if (this.isPaveText() || this.isPavesText()) {
         if (this.isPavesText()) {
            menu.addSizeMenu('Paves', 1, 10, 1, pave.fNpaves, val => {
               pave.fNpaves = val;
               this.interactiveRedraw(true, `exec:SetNpaves(${val})`);
            });
         }

         if (this.isTitle()) {
            menu.add('Default position', () => {
               pave.fX1NDC = gStyle.fTitleW > 0 ? gStyle.fTitleX - gStyle.fTitleW / 2 : gStyle.fPadLeftMargin;
               pave.fY1NDC = gStyle.fTitleY - Math.min(gStyle.fTitleFontSize * 1.1, 0.06);
               pave.fX2NDC = gStyle.fTitleW > 0 ? gStyle.fTitleX + gStyle.fTitleW / 2 : 1 - gStyle.fPadRightMargin;
               pave.fY2NDC = gStyle.fTitleY;
               pave.fInit = 1;
               this.interactiveRedraw(true, 'pave_moved');
            });

            menu.add('Save to gStyle', () => {
               gStyle.fTitleX = (pave.fX2NDC + pave.fX1NDC) / 2;
               gStyle.fTitleY = pave.fY2NDC;
               this.fillatt?.saveToStyle('fTitleColor', 'fTitleStyle');
               gStyle.fTitleTextColor = pave.fTextColor;
               gStyle.fTitleFontSize = pave.fTextSize;
               gStyle.fTitleFont = pave.fTextFont;
            }, 'Store title position and graphical attributes to gStyle');
         }
      } else if (pave._typename === clTLegend) {
         menu.sub('Legend');
         menu.add('Autoplace', () => {
            this.autoPlaceLegend(pave, this.getPadPainter()?.getRootPad(true), true).then(res => {
               if (res)
                  this.interactiveRedraw(true, 'pave_moved');
            });
         });
         menu.addSizeMenu('Entry separation', 0, 1, 0.1, pave.fEntrySeparation, v => {
            pave.fEntrySeparation = v;
            this.interactiveRedraw(true, `exec:SetEntrySeparation(${v})`);
         }, 'Vertical entries separation, meaningful values between 0 and 1');
         menu.addSizeMenu('Columns separation', 0, 1, 0.1, pave.fColumnSeparation, v => {
            pave.fColumnSeparation = v;
            this.interactiveRedraw(true, `exec:SetColumnSeparation(${v})`);
         }, 'Horizontal columns separation, meaningful values between 0 and 1');
         menu.addSizeMenu('Num columns', 1, 7, 1, pave.fNColumns, v => {
            pave.fNColumns = v;
            this.interactiveRedraw(true, `exec:SetNColumns(${v})`);
         }, 'Number of columns in the legend');
         menu.endsub();
      }
   }

   /** @summary Show pave context menu */
   paveContextMenu(evnt) {
      if (this.z_handle) {
         const fp = this.getFramePainter();
         if (isFunc(fp?.showContextMenu))
            fp.showContextMenu('pal', evnt);
      } else
         showPainterMenu(evnt, this);
   }

   /** @summary Returns true when stat box is drawn */
   isStats() {
      return this.matchObjectType(clTPaveStats);
   }

   /** @summary Returns true when stat box is drawn */
   isPaveText() {
      return this.matchObjectType(clTPaveText);
   }

   /** @summary Returns true when stat box is drawn */
   isPavesText() {
      return this.matchObjectType(clTPavesText);
   }

   /** @summary Returns true when stat box is drawn */
   isPalette() {
      return this.matchObjectType(clTPaletteAxis);
   }

   /** @summary Returns true when title is drawn */
   isTitle() {
      return this.isPaveText() && (this.getObject()?.fName === kTitle);
   }

   /** @summary Clear text in the pave */
   clearPave() {
      this.getObject().Clear();
   }

   /** @summary Add text to pave */
   addText(txt) {
      this.getObject().AddText(txt);
   }

   /** @summary Remade version of THistPainter::GetBestFormat
     * @private */
   getBestFormat(tv, e) {
      const ie = tv.indexOf('e'), id = tv.indexOf('.');

      if (ie >= 0)
         return (tv.indexOf('+') < 0) || (e >= 1) ? `.${ie - id - 1}e` : '.1f';
      if (id < 0)
         return '.1f';

      return `.${tv.length - id - 1}f`;
   }

   /** @summary Fill function parameters */
   fillFunctionStat(f1, dofit, ndim = 1) {
      this.#has_fit = dofit && f1;

      if (!this.#has_fit)
         return false;

      this.#fit_dim = ndim;
      this.#fit_cnt = 0;

      const print_fval = (ndim === 1) ? dofit % 10 : 1,
            print_ferrors = (ndim === 1) ? Math.floor(dofit / 10) % 10 : 1,
            print_fchi2 = (ndim === 1) ? Math.floor(dofit / 100) % 10 : 1,
            print_fprob = (ndim === 1) ? Math.floor(dofit / 1000) % 10 : 0;

      if (print_fchi2) {
         this.addText('#chi^{2} / ndf = ' + this.format(f1.fChisquare, 'fit') + ' / ' + f1.fNDF);
         this.#fit_cnt++;
      }
      if (print_fprob) {
         this.addText('Prob = ' + this.format(Prob(f1.fChisquare, f1.fNDF)));
         this.#fit_cnt++;
      }
      if (print_fval) {
         for (let n = 0; n < f1.GetNumPars(); ++n) {
            const parname = f1.GetParName(n);
            let parvalue = f1.GetParValue(n), parerr = f1.GetParError(n);
            if (parvalue === undefined) {
               parvalue = '<not avail>';
               parerr = null;
            } else {
               parvalue = this.format(Number(parvalue), 'fit');
               if (print_ferrors && (parerr !== undefined)) {
                  parerr = floatToString(parerr, this.getBestFormat(parvalue, parerr));
                  if (!Number(parerr) && f1.GetParError(n))
                     parerr = floatToString(f1.GetParError(n), '4.2g');
               }
            }

            if (print_ferrors && parerr)
               this.addText(`${parname} = ${parvalue} #pm ${parerr}`);
            else
               this.addText(`${parname} = ${parvalue}`);
            this.#fit_cnt++;
         }
      }

      return true;
   }

   /** @summary Is dummy pos of the pave painter */
   isDummyPos(p) {
      return !p ? true : !p.fInit && !p.fX1 && !p.fX2 && !p.fY1 && !p.fY2 && !p.fX1NDC && !p.fX2NDC && !p.fY1NDC && !p.fY2NDC;
   }

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

      const pave = this.getObject(),
            is_auto = opt === kAutoPlace;

      if (!pave.$modifiedNDC && !this.isDummyPos(obj)) {
         // if position was not modified interactively, update from source object

         if (this.stored && !obj.fInit && (this.stored.fX1 === obj.fX1) &&
             (this.stored.fX2 === obj.fX2) && (this.stored.fY1 === obj.fY1) && (this.stored.fY2 === obj.fY2)) {
            // case when source object not initialized and original coordinates are not changed
            // take over only modified NDC coordinate, used in tutorials/graphics/canvas.C
            if (this.stored.fX1NDC !== obj.fX1NDC)
               pave.fX1NDC = obj.fX1NDC;
            if (this.stored.fX2NDC !== obj.fX2NDC)
               pave.fX2NDC = obj.fX2NDC;
            if (this.stored.fY1NDC !== obj.fY1NDC)
               pave.fY1NDC = obj.fY1NDC;
            if (this.stored.fY2NDC !== obj.fY2NDC)
               pave.fY2NDC = obj.fY2NDC;
         } else {
            pave.fInit = obj.fInit;
            pave.fX1 = obj.fX1;
            pave.fX2 = obj.fX2;
            pave.fY1 = obj.fY1;
            pave.fY2 = obj.fY2;
            pave.fX1NDC = obj.fX1NDC;
            pave.fX2NDC = obj.fX2NDC;
            pave.fY1NDC = obj.fY1NDC;
            pave.fY2NDC = obj.fY2NDC;
         }

         this.stored = Object.assign({}, obj); // store latest coordinates
      }

      pave.fOption = obj.fOption;
      pave.fBorderSize = obj.fBorderSize;
      if (pave.fTextColor !== undefined && obj.fTextColor !== undefined) {
         pave.fTextAngle = obj.fTextAngle;
         pave.fTextSize = obj.fTextSize;
         pave.fTextAlign = obj.fTextAlign;
         pave.fTextColor = obj.fTextColor;
         pave.fTextFont = obj.fTextFont;
      }

      switch (obj._typename) {
         case clTDiamond:
         case clTPaveText:
            pave.fLines = clone(obj.fLines);
            break;
         case clTPavesText:
            pave.fLines = clone(obj.fLines);
            pave.fNpaves = obj.fNpaves;
            break;
         case clTPaveLabel:
         case clTPaveClass:
            pave.fLabel = obj.fLabel;
            break;
         case clTPaveStats:
            pave.fOptStat = obj.fOptStat;
            pave.fOptFit = obj.fOptFit;
            break;
         case clTLegend: {
            const oldprim = pave.fPrimitives;
            pave.fPrimitives = obj.fPrimitives;
            pave.fNColumns = obj.fNColumns;
            this.AutoPlace = is_auto;
            if (oldprim?.arr?.length && (oldprim?.arr?.length === pave.fPrimitives?.arr?.length)) {
               // try to sync object reference, new object does not displayed automatically
               // in ideal case one should use snapids in the entries
               for (let k = 0; k < oldprim.arr.length; ++k) {
                  const oldobj = oldprim.arr[k].fObject, newobj = pave.fPrimitives.arr[k].fObject;
                  if (oldobj && newobj && oldobj._typename === newobj._typename && oldobj.fName === newobj.fName)
                     pave.fPrimitives.arr[k].fObject = oldobj;
               }
            }
            return true;
         }
         case clTPaletteAxis:
            pave.fBorderSize = 1;
            pave.fShadowColor = 0;
            break;
         default:
            return false;
      }

      this.storeDrawOpt(is_auto ? kDefaultDrawOpt : opt);

      return true;
   }

   /** @summary redraw pave object */
   async redraw() {
      return this.drawPave();
   }

   /** @summary cleanup pave painter */
   cleanup() {
      this.z_handle?.cleanup();
      delete this.z_handle;
      const pp = this.getObject();
      if (pp)
         delete pp.$main_painter;
      super.cleanup();
   }

   /** @summary Set position of title
     * @private */
   setTitlePosition(pave, text_width, text_height) {
      const posx = gStyle.fTitleX, posy = gStyle.fTitleY,
            valign = gStyle.fTitleAlign % 10, halign = (gStyle.fTitleAlign - valign) / 10;
      let w = gStyle.fTitleW, h = gStyle.fTitleH, need_readjust = false;

      if (h <= 0) {
         if (text_height)
            h = 1.1 * text_height / this.getPadPainter().getPadHeight();
         else {
            h = 0.05;
            need_readjust = true;
         }
      }

      if (w <= 0) {
         if (text_width)
            w = Math.min(0.7, 0.02 + text_width / this.getPadPainter().getPadWidth());
         else {
            w = 0.5;
            need_readjust = true;
         }
      }

      pave.fX1NDC = halign < 2 ? posx : (halign > 2 ? posx - w : posx - w / 2);
      pave.fY1NDC = valign < 2 ? posy : (valign > 2 ? posy - h : posy - h / 2);
      pave.fX2NDC = pave.fX1NDC + w;
      pave.fY2NDC = pave.fY1NDC + h;
      pave.fInit = 1;

      return need_readjust;
   }

   /** @summary Returns true if object is supported */
   static canDraw(obj) {
      const typ = obj?._typename;
      return typ === clTPave || typ === clTPaveLabel || typ === clTPaveClass || typ === clTPaveStats || typ === clTPaveText ||
             typ === clTPavesText || typ === clTDiamond || typ === clTLegend || typ === clTPaletteAxis;
   }

   /** @summary Draw TPave */
   static async draw(dom, pave, opt) {
      const arg_opt = opt,
            pos_title = (opt === kPosTitle),
            is_auto = (opt === kAutoPlace);
      if (pos_title || is_auto || (isStr(opt) && (opt.indexOf(';') >= 0)))
         opt = ''; // use default - or stored in TPave itself

      const painter = new TPavePainter(dom, pave, opt);

      return ensureTCanvas(painter, false).then(() => {
         if (painter.isTitle()) {
            const pp = painter.getPadPainter(),
                  prev_painter = pp.findPainterFor(null, kTitle, clTPaveText);
            if (prev_painter && (prev_painter !== painter))
               pp.removePrimitive(prev_painter);
            else if (pos_title || painter.isDummyPos(pave)) {
               if (painter.setTitlePosition(pave))
                  painter.$postitle = true;
            }
         } else if (pave._typename === clTPaletteAxis) {
            pave.fBorderSize = 1;
            pave.fShadowColor = 0;

            // check some default values of TGaxis object, otherwise axis will not be drawn
            if (pave.fAxis) {
               if (!pave.fAxis.fChopt)
                  pave.fAxis.fChopt = '+';
               if (!pave.fAxis.fNdiv)
                  pave.fAxis.fNdiv = 12;
               if (!pave.fAxis.fLabelOffset)
                  pave.fAxis.fLabelOffset = 0.005;
            }

            painter.z_handle = new TAxisPainter(painter.getPadPainter(), pave.fAxis, true);

            painter.UseContextMenu = true;
         } else if (pave._typename === clTLegend)
            painter.AutoPlace = is_auto;

         painter.NoFillStats = pave.fName !== 'stats';

         return painter.drawPave(arg_opt);
      }).then(() => {
         const adjust_title = painter.$postitle && painter.$titlebox;

         if (adjust_title)
            painter.setTitlePosition(pave, painter.$titlebox.width, painter.$titlebox.height);

         delete painter.$postitle;
         delete painter.$titlebox;

         return adjust_title ? painter.drawPave(arg_opt) : painter;
      });
   }

} // class TPavePainter

var TPavePainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TPavePainter: TPavePainter,
kPosTitle: kPosTitle
});

const kCARTESIAN = 1, kPOLAR = 2, kCYLINDRICAL = 3, kSPHERICAL = 4, kRAPIDITY = 5,
      kNormal$1 = 0, kPoisson = 1, kPoisson2 = 2,
      kOnlyCheck = 'only-check';
/**
 * @summary Class to decode histograms draw options
 * @desc All options started from capital letter are major drawing options
 * any other draw options are internal settings.
 * @private
 */

class THistDrawOptions {

   constructor() { this.reset(); }

   /** @summary Reset hist draw options */
   reset() {
      Object.assign(this, {
         Axis: 0, RevX: false, RevY: false, SymlogX: 0, SymlogY: 0, xticks: null, yticks: null,
         Bar: false, BarStyle: 0, Curve: false,
         Hist: 1, Line: false, Fill: false,
         Error: 0, ErrorKind: -1, errorX: gStyle.fErrorX,
         Mark: false, Same: false, Scat: false, ScatCoef: 1.0, Func: true, AllFunc: false,
         Arrow: false, Box: false, BoxStyle: 0,
         Text: false, TextAngle: 0, TextKind: '', Char: 0, Color: false, Contour: 0, Cjust: false,
         Lego: 0, Surf: 0, Off: 0, Tri: 0, Proj: 0, AxisPos: 0, Ortho: gStyle.fOrthoCamera,
         Spec: false, Pie: false, List: false, Zscale: false, Zvert: true, PadPalette: false,
         Candle: '', Violin: '', Scaled: null, Circular: 0, Poisson: kNormal$1,
         GLBox: 0, GLColor: false, Project: '', ProfileProj: '', Profile2DProj: '', System: kCARTESIAN,
         AutoColor: false, NoStat: false, ForceStat: false, PadStats: false, PadTitle: false, AutoZoom: false,
         HighRes: 0, Zero: 1, Palette: 0, BaseLine: false, ShowEmpty: false,
         Optimize: settings.OptimizeDraw,
         Mode3D: false, x3dscale: 1, y3dscale: 1, SwapXY: false,
         Render3D: constants$1.Render3D.Default,
         FrontBox: true, BackBox: true,
         need_fillcol: false,
         minimum: kNoZoom, maximum: kNoZoom, ymin: 0, ymax: 0, cutg: null,
         IgnoreMainScale: false, IgnorePalette: false
      });
   }

   isCartesian() { return this.System === kCARTESIAN; }

   is3d() { return this.Lego || this.Surf; }

   /** @summary Base on sumw2 values (re)set some basic draw options, only for 1dim hist */
   decodeSumw2(histo, force) {
      const len = histo.fSumw2?.length ?? 0;
      let isany = false;
      for (let n = 0; n < len; ++n) {
         if (histo.fSumw2[n] > 0) {
            isany = true;
            break;
         }
      }

      if (Number.isInteger(this.Error) || force)
         this.Error = isany ? 1 : 0;

      if (Number.isInteger(this.Hist) || force)
         this.Hist = isany ? 0 : 1;

      if (Number.isInteger(this.Zero) || force)
         this.Zero = isany ? 0 : 1;
   }

   /** @summary Is palette can be used with current draw options */
   canHavePalette() {
      if (this.ndim === 3)
         return this.BoxStyle === 12 || this.BoxStyle === 13 || this.GLBox === 12;

      if (this.ndim === 1)
         return this.Lego === 12 || this.Lego === 14;

      if (this.Mode3D)
         return this.Lego === 12 || this.Lego === 14 || this.Surf === 11 || this.Surf === 12;

      if (this.Color || this.Contour || this.Hist || this.Axis)
         return true;

      return !this.Scat && !this.Box && !this.Arrow && !this.Proj && !this.Candle && !this.Violin && !this.Text;
   }

   /** @summary Decode histogram draw options */
   decode(opt, hdim, histo, pp, pad, painter) {
      this.orginal = opt; // will be overwritten by storeDrawOpt call

      this.cutg_name = '';
      if (isStr(opt) && (hdim === 2)) {
         const p1 = opt.lastIndexOf('['), p2 = opt.lastIndexOf(']');
         if ((p1 >= 0) && (p2 > p1 + 1) && (opt.at(p1 - 1) !== ':')) {
            this.cutg_name = opt.slice(p1 + 1, p2);
            opt = opt.slice(0, p1) + opt.slice(p2 + 1);
            this.cutg = pp?.findInPrimitives(this.cutg_name, clTCutG);
            if (this.cutg)
               this.cutg.$redraw_pad = true;
         }
      }

      const d = new DrawOptions(opt);

      if (hdim === 1)
         this.decodeSumw2(histo, true);

      this.ndim = hdim || 1; // keep dimensions, used for now in GED

      // for old web canvas json
      // TODO: remove in version 8
      d.check('USE_PAD_TITLE');
      d.check('USE_PAD_PALETTE');
      d.check('USE_PAD_STATS');

      if (d.check('IGNORE_PALETTE'))
         this.IgnorePalette = true;

      if (d.check('PAL', true))
         this.Palette = d.partAsInt();
      // this is zooming of histogram content
      if (d.check('MINIMUM:', true)) {
         this.ominimum = true;
         this.minimum = parseFloat(d.part);
      } else {
         this.ominimum = false;
         this.minimum = histo.fMinimum;
      }
      if (d.check('MAXIMUM:', true)) {
         this.omaximum = true;
         this.maximum = parseFloat(d.part);
      } else {
         this.omaximum = false;
         this.maximum = histo.fMaximum;
      }
      if (!this.ominimum && !this.omaximum && this.minimum === this.maximum)
         this.minimum = this.maximum = kNoZoom;

      this.ohmin = d.check('HMIN:', true);
      this.hmin = this.ohmin ? parseFloat(d.part) : undefined;
      this.ohmax = d.check('HMAX:', true);
      this.hmax = this.ohmax ? parseFloat(d.part) : undefined;
      this.zoom_min_max = d.check('ZOOM_MIN_MAX');

      // let configure histogram titles - only for debug purposes
      if (d.check('HTITLE:', true))
         histo.fTitle = decodeURIComponent(d.getPart(true));
      if (d.check('XTITLE:', true))
         histo.fXaxis.fTitle = decodeURIComponent(d.getPart(true));
      if (d.check('YTITLE:', true))
         histo.fYaxis.fTitle = decodeURIComponent(d.getPart(true));
      if (d.check('ZTITLE:', true))
         histo.fZaxis.fTitle = decodeURIComponent(d.getPart(true));
      if (d.check('POISSON2'))
         this.Poisson = kPoisson2;
      if (d.check('POISSON'))
         this.Poisson = kPoisson;

      if (d.check('SHOWEMPTY'))
         this.ShowEmpty = true;

      if (d.check('NOOPTIMIZE'))
         this.Optimize = 0;
      if (d.check('OPTIMIZE'))
         this.Optimize = 2;

      if (d.check('AUTOCOL'))
         this.AutoColor = true;
      if (d.check('AUTOZOOM'))
         this.AutoZoom = true;

      if (d.check('OPTSTAT', true))
         this.optstat = d.partAsInt();
      if (d.check('OPTFIT', true))
         this.optfit = d.partAsInt();

      if (d.check('XTICKS:', 'array'))
         this.xticks = d.array;
      if ((this.ndim > 1) && d.check('YTICKS:', 'array'))
         this.yticks = d.array;

      if (this.optstat || this.optfit)
         histo?.SetBit(kNoStats, false);

      if (d.check('ALLBINS') && histo) {
         histo.fXaxis.fFirst = 0;
         histo.fXaxis.fLast = histo.fXaxis.fNbins + 1;
         histo.fXaxis.SetBit(EAxisBits.kAxisRange);
         if (this.ndim > 1) {
            histo.fYaxis.fFirst = 0;
            histo.fYaxis.fLast = histo.fYaxis.fNbins + 1;
            histo.fYaxis.SetBit(EAxisBits.kAxisRange);
         }
         if (this.ndim > 2) {
            histo.fZaxis.fFirst = 0;
            histo.fZaxis.fLast = histo.fZaxis.fNbins + 1;
            histo.fZaxis.SetBit(EAxisBits.kAxisRange);
         }
      }

      if (d.check('NOSTAT'))
         this.NoStat = true;
      if (d.check('STAT'))
         this.ForceStat = true;

      if (d.check('NOTOOLTIP'))
         painter?.setTooltipAllowed(false);
      if (d.check('TOOLTIP'))
         painter?.setTooltipAllowed(true);

      if (d.check('SYMLOGX', true))
         this.SymlogX = d.partAsInt(0, 3);
      if (d.check('SYMLOGY', true))
         this.SymlogY = d.partAsInt(0, 3);

      if (d.check('X3DSC', true))
         this.x3dscale = d.partAsInt(0, 100) / 100;
      if (d.check('Y3DSC', true))
         this.y3dscale = d.partAsInt(0, 100) / 100;

      if (d.check('PERSPECTIVE') || d.check('PERSP'))
         this.Ortho = false;
      if (d.check('ORTHO'))
         this.Ortho = true;

      let lx = 0, ly = 0, check3dbox = '';
      if (d.check('LOG2XY'))
         lx = ly = 2;
      if (d.check('LOGXY'))
         lx = ly = 1;
      if (d.check('LOG2X'))
         lx = 2;
      if (d.check('LOGX'))
         lx = 1;
      if (d.check('LOG2Y'))
         ly = 2;
      if (d.check('LOGY'))
         ly = 1;
      if (lx && pad) {
         pad.fLogx = lx;
         pad.fUxmin = 0;
         pad.fUxmax = 1;
         pad.fX1 = 0;
         pad.fX2 = 1;
      }
      if (ly && pad) {
         pad.fLogy = ly;
         pad.fUymin = 0;
         pad.fUymax = 1;
         pad.fY1 = 0;
         pad.fY2 = 1;
      }
      if (d.check('LOG2Z') && pad)
         pad.fLogz = 2;
      if (d.check('LOGZ') && pad)
         pad.fLogz = 1;
      if (d.check('LOGV') && pad)
         pad.fLogv = 1; // fictional member, can be introduced in ROOT
      if (d.check('GRIDXY') && pad)
         pad.fGridx = pad.fGridy = 1;
      if (d.check('GRIDX') && pad)
         pad.fGridx = 1;
      if (d.check('GRIDY') && pad)
         pad.fGridy = 1;
      if (d.check('TICKXY') && pad)
         pad.fTickx = pad.fTicky = 1;
      if (d.check('TICKX') && pad)
         pad.fTickx = 1;
      if (d.check('TICKY') && pad)
         pad.fTicky = 1;
      if (d.check('TICKZ') && pad)
         pad.fTickz = 1;
      if (d.check('GRAYSCALE'))
         pp?.setGrayscale(true);

      if (d.check('FILL_', 'color')) {
         this.histoFillColor = d.color;
         this.histoFillPattern = 1001;
      }

      if (d.check('FILLPAT_', true))
         this.histoFillPattern = d.partAsInt();

      if (d.check('LINE_', 'color'))
         this.histoLineColor = painter.getColor(d.color);

      if (d.check('WIDTH_', true))
         this.histoLineWidth = d.partAsInt();

      if (d.check('XAXIS_', 'color'))
         histo.fXaxis.fAxisColor = histo.fXaxis.fLabelColor = histo.fXaxis.fTitleColor = d.color;

      if (d.check('YAXIS_', 'color'))
         histo.fYaxis.fAxisColor = histo.fYaxis.fLabelColor = histo.fYaxis.fTitleColor = d.color;

      if (d.check('X+')) {
         this.AxisPos = 10;
         this.second_x = Boolean(painter?.getMainPainter());
      }
      if (d.check('Y+')) {
         this.AxisPos += 1;
         this.second_y = Boolean(painter?.getMainPainter());
      }

      if (d.check('SAME0'))
         this.Same = this.IgnoreMainScale = true;
      if (d.check('SAMES'))
         this.Same = this.ForceStat = true;
      if (d.check('SAME'))
         this.Same = this.Func = true;

      if (d.check('SPEC'))
         this.Spec = true; // not used

      if (d.check('BASE0') || d.check('MIN0'))
         this.BaseLine = 0;
      else if (gStyle.fHistMinimumZero)
         this.BaseLine = 0;

      if (d.check('PIE'))
         this.Pie = true; // not used

      if (d.check('CANDLE', true))
         this.Candle = d.part || '1';
      if (d.check('VIOLIN', true)) {
         this.Violin = d.part || '1';
         delete this.Candle;
      }
      if (d.check('NOSCALED'))
         this.Scaled = false;
      if (d.check('SCALED'))
         this.Scaled = true;

      if (d.check('GLBOX', true))
         this.GLBox = 10 + d.partAsInt();
      if (d.check('GLCOL'))
         this.GLColor = true;

      d.check('GL'); // suppress GL

      if (d.check('CIRCULAR', true) || d.check('CIRC', true)) {
         this.Circular = 11;
         if (d.part.indexOf('0') >= 0)
            this.Circular = 10; // black and white
         if (d.part.indexOf('1') >= 0)
            this.Circular = 11; // color
         if (d.part.indexOf('2') >= 0)
            this.Circular = 12; // color and width
      }

      this.Chord = d.check('CHORD');

      if (d.check('LEGO', true)) {
         this.Lego = 1;
         if (d.part.indexOf('0') >= 0)
            this.Zero = false;
         if (d.part.indexOf('1') >= 0)
            this.Lego = 11;
         if (d.part.indexOf('2') >= 0)
            this.Lego = 12;
         if (d.part.indexOf('3') >= 0)
            this.Lego = 13;
         if (d.part.indexOf('4') >= 0)
            this.Lego = 14;
         check3dbox = d.part;
         if (d.part.indexOf('Z') >= 0)
            this.Zscale = true;
         if (d.part.indexOf('H') >= 0)
            this.Zvert = false;
      }

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

      if (d.check('POL'))
         this.System = kPOLAR;
      if (d.check('CYL'))
         this.System = kCYLINDRICAL;
      if (d.check('SPH'))
         this.System = kSPHERICAL;
      if (d.check('PSR'))
         this.System = kRAPIDITY;

      if (d.check('SURF', true)) {
         this.Surf = d.partAsInt(10, 1);
         check3dbox = d.part;
         if (d.part.indexOf('Z') >= 0)
            this.Zscale = true;
         if (d.part.indexOf('H') >= 0)
            this.Zvert = false;
      }

      if (d.check('TF3', true))
         check3dbox = d.part;

      if (d.check('ISO', true))
         check3dbox = d.part;

      if (d.check('LIST'))
         this.List = true; // not used

      if (d.check('CONT', true) && (hdim > 1)) {
         this.Contour = 1;
         if (d.part.indexOf('Z') >= 0)
            this.Zscale = true;
         if (d.part.indexOf('H') >= 0)
            this.Zvert = false;
         if (d.part.indexOf('1') >= 0)
            this.Contour = 11;
         else if (d.part.indexOf('2') >= 0)
            this.Contour = 12;
         else if (d.part.indexOf('3') >= 0)
            this.Contour = 13;
         else if (d.part.indexOf('4') >= 0)
            this.Contour = 14;
      }

      // decode bar/hbar option
      if (d.check('HBAR', true))
         this.BarStyle = 20;
      else if (d.check('BAR', true))
         this.BarStyle = 10;
      if (this.BarStyle > 0) {
         this.Hist = false;
         this.need_fillcol = true;
         this.BarStyle += d.partAsInt();
      }

      if (d.check('ARR'))
         this.Arrow = true;

      if (d.check('BOX', true)) {
         this.BoxStyle = 10;
         if (d.part.indexOf('1') >= 0)
            this.BoxStyle = 11;
         else if (d.part.indexOf('2') >= 0)
            this.BoxStyle = 12;
         else if (d.part.indexOf('3') >= 0)
            this.BoxStyle = 13;
         if (d.part.indexOf('Z') >= 0)
            this.Zscale = true;
         if (d.part.indexOf('H') >= 0)
            this.Zvert = false;
      }

      this.Box = this.BoxStyle > 0;

      if (d.check('CJUST'))
         this.Cjust = true;
      if (d.check('COL7'))
         this.Color = 7; // special color mode with use of bar offset
      if (d.check('COL'))
         this.Color = true;
      if (d.check('CHAR'))
         this.Char = 1;
      if (d.check('ALLFUNC'))
         this.AllFunc = true;
      if (d.check('FUNC')) {
         this.Func = true;
         this.Hist = false;
      }
      if (d.check('HAXISG')) {
         this.Axis = 3;
         this.SwapXY = 1;
      }
      if (d.check('HAXIS')) {
         this.Axis = 1;
         this.SwapXY = 1;
      }
      if (d.check('HAXIG')) {
         this.Axis = 2;
         this.SwapXY = 1;
      }
      if (d.check('AXISG'))
         this.Axis = 3;
      if (d.check('AXIS'))
         this.Axis = 1;
      if (d.check('AXIG'))
         this.Axis = 2;

      if (d.check('TEXT', true)) {
         this.Text = true;
         this.Hist = false;
         this.TextAngle = Math.min(d.partAsInt(), 90);
         if (d.part.indexOf('N') >= 0)
            this.TextKind = 'N';
         if (d.part.indexOf('E0') >= 0)
            this.TextLine = true;
         if (d.part.indexOf('E') >= 0)
            this.TextKind = 'E';
      }

      if (d.check('SCAT=', true)) {
         this.Scat = true;
         this.ScatCoef = parseFloat(d.part);
         if (!Number.isFinite(this.ScatCoef) || (this.ScatCoef <= 0))
            this.ScatCoef = 1.0;
      }

      if (d.check('SCAT'))
         this.Scat = true;

      if (d.check('TRI', true)) {
         this.Color = false;
         this.Tri = 1;
         check3dbox = d.part;
         if (d.part.indexOf('ERR') >= 0)
            this.Error = true;
      }

      if (d.check('AITOFF'))
         this.Proj = 1;
      if (d.check('MERCATOR'))
         this.Proj = 2;
      if (d.check('SINUSOIDAL'))
         this.Proj = 3;
      if (d.check('PARABOLIC'))
         this.Proj = 4;
      if (d.check('MOLLWEIDE'))
         this.Proj = 5;
      if (this.Proj > 0)
         this.Contour = 14;

      if (d.check('PROJXY', true)) {
         let flag = true;
         if ((histo?._typename === clTProfile2D) && d.part && !Number.isInteger(Number.parseInt(d.part))) {
            this.Profile2DProj = d.part;
            flag = d.check('PROJXY', true); // allow projxy with projected profile2d
         }
         if (flag)
            this.Project = 'XY' + d.partAsInt(0, 1);
      }

      if (d.check('PROJX', true)) {
         if (histo?._typename === clTProfile)
            this.ProfileProj = d.part || 'B';
         else
            this.Project = 'X' + d.part;
      }
      if (d.check('PROJY', true))
         this.Project = 'Y' + d.part;
      if (d.check('PROJ'))
         this.Project = 'Y1';

      if (check3dbox) {
         if (check3dbox.indexOf('FB') >= 0)
            this.FrontBox = false;
         if (check3dbox.indexOf('BB') >= 0)
            this.BackBox = false;
      }

      if ((hdim === 3) && d.check('FB'))
         this.FrontBox = false;
      if ((hdim === 3) && d.check('BB'))
         this.BackBox = false;

      if (d.check('PFC') && !this._pfc)
         this._pfc = 2;
      if ((d.check('PLC') || this.AutoColor) && !this._plc)
         this._plc = 2;
      if (d.check('PMC') && !this._pmc)
         this._pmc = 2;

      const check_axis_bit = (aopt, axis, bit) => {
         // ignore Z scale options for 2D plots
         if ((axis === 'fZaxis') && (hdim < 3) && !this.Lego && !this.Surf)
            return;
         let flag = d.check(aopt);
         if (pad && pad['$' + aopt]) {
            flag = true;
            pad['$' + aopt] = undefined;
         }
         if (flag && histo)
            histo[axis].SetBit(bit, true);
      };

      check_axis_bit('OTX', 'fXaxis', EAxisBits.kOppositeTitle);
      check_axis_bit('OTY', 'fYaxis', EAxisBits.kOppositeTitle);
      check_axis_bit('OTZ', 'fZaxis', EAxisBits.kOppositeTitle);
      check_axis_bit('CTX', 'fXaxis', EAxisBits.kCenterTitle);
      check_axis_bit('CTY', 'fYaxis', EAxisBits.kCenterTitle);
      check_axis_bit('CTZ', 'fZaxis', EAxisBits.kCenterTitle);
      check_axis_bit('MLX', 'fXaxis', EAxisBits.kMoreLogLabels);
      check_axis_bit('MLY', 'fYaxis', EAxisBits.kMoreLogLabels);
      check_axis_bit('MLZ', 'fZaxis', EAxisBits.kMoreLogLabels);
      check_axis_bit('NOEX', 'fXaxis', EAxisBits.kNoExponent);
      check_axis_bit('NOEY', 'fYaxis', EAxisBits.kNoExponent);
      check_axis_bit('NOEZ', 'fZaxis', EAxisBits.kNoExponent);

      if (d.check('RX') || pad?.$RX)
         this.RevX = true;
      if (d.check('RY') || pad?.$RY)
         this.RevY = true;

      if (d.check('L')) {
         this.Line = true;
         this.Hist = false;
      }
      if (d.check('F')) {
         this.Fill = true;
         this.need_fillcol = true;
      }

      if (d.check('A'))
         this.Axis = -1;
      if (pad?.$ratio_pad === 'up') {
         if (!this.Same)
            this.Axis = 0; // draw both axes
         histo.fXaxis.fLabelSize = 0;
         histo.fXaxis.fTitle = '';
         histo.fYaxis.$use_top_pad = true;
      } else if (pad?.$ratio_pad === 'low') {
         if (!this.Same)
            this.Axis = 0; // draw both axes
         histo.fXaxis.$use_top_pad = true;
         histo.fYaxis.$use_top_pad = true;
         histo.fXaxis.fTitle = 'x';
         const fp = painter?.getCanvPainter().findPainterFor(null, 'upper_pad', clTPad)?.getFramePainter();
         if (fp) {
            painter.zoom_xmin = fp.scale_xmin;
            painter.zoom_xmax = fp.scale_xmax;
         }
      }

      if (d.check('B1')) {
         this.BarStyle = 1;
         this.BaseLine = 0;
         this.Hist = false;
         this.need_fillcol = true;
      }
      if (d.check('B')) {
         this.BarStyle = 1;
         this.Hist = false;
         this.need_fillcol = true;
      }
      if (d.check('C')) {
         this.Curve = true;
         this.Hist = false;
      }
      if (d.check('][')) {
         this.Off = 1;
         this.Hist = true;
      }

      if (d.check('HIST')) {
         this.Hist = true;
         this.Func = true;
         this.Error = false;
      }

      this.Bar = (this.BarStyle > 0);

      delete this.MarkStyle; // remove mark style if any

      if (d.check('P0')) {
         this.Mark = true;
         this.Hist = false;
         this.Zero = true;
      }
      if (d.check('P')) {
         this.Mark = true;
         this.Hist = false;
         this.Zero = false;
      }
      if (d.check('HZ')) {
         this.Zscale = true;
         this.Zvert = false;
      }
      if (d.check('Z'))
         this.Zscale = true;
      if (d.check('*')) {
         this.Mark = true;
         this.MarkStyle = 3;
         this.Hist = false;
      }
      if (d.check('H'))
         this.Hist = true;

      if (d.check('E', true)) {
         this.Error = true;
         if (hdim === 1) {
            this.Zero = false; // do not draw empty bins with errors
            if (this.Hist === 1)
               this.Hist = false;
            if (Number.isInteger(parseInt(d.part[0])))
               this.ErrorKind = parseInt(d.part[0]);
            if ((this.ErrorKind === 3) || (this.ErrorKind === 4))
               this.need_fillcol = true;
            if (this.ErrorKind === 0)
               this.Zero = true; // enable drawing of empty bins
            if (d.part.indexOf('X0') >= 0)
               this.errorX = 0;
         }
      }
      if (d.check('9'))
         this.HighRes = 1;
      if (d.check('0'))
         this.Zero = false;
      if (this.Color && d.check('1'))
         this.Zero = false;

      // flag identifies 3D drawing mode for histogram
      if ((this.Lego > 0) || (hdim === 3) || (((this.Surf > 0) || this.Error) && (hdim === 2)))
         this.Mode3D = true;

      // default draw options for TF1 is line and fill
      if (painter?.isTF1() && (hdim === 1) && (this.Hist === 1) && !this.Line && !this.Fill && !this.Curve && !this.Mark) {
         this.Hist = false;
         this.Curve = settings.FuncAsCurve;
         this.Line = !this.Curve;
         this.Fill = true;
      }

      if ((this.Surf === 15) && (this.System === kPOLAR || this.System === kCARTESIAN))
         this.Surf = 13;
   }

   /** @summary Is X/Y swap is configured */
   swap_xy() { return this.BarStyle >= 20 || this.SwapXY; }

   /** @summary Tries to reconstruct string with hist draw options */
   asString(is_main_hist, pad) {
      let res = '', zopt = '';
      if (this.Zscale)
         zopt = this.Zvert ? 'Z' : 'HZ';
      if (this.Mode3D) {
         if (this.Lego) {
            res = 'LEGO';
            if (!this.Zero)
               res += '0';
            if (this.Lego > 10)
               res += (this.Lego - 10);
            res += zopt;
         } else if (this.Surf) {
            res = 'SURF' + (this.Surf - 10);
            res += zopt;
         }
         if (!this.FrontBox)
            res += 'FB';
         if (!this.BackBox)
            res += 'BB';

         if (this.x3dscale !== 1)
            res += `_X3DSC${Math.round(this.x3dscale * 100)}`;
         if (this.y3dscale !== 1)
            res += `_Y3DSC${Math.round(this.y3dscale * 100)}`;
      } else {
         if (this.Candle)
            res = 'CANDLE' + this.Candle;
         else if (this.Violin)
            res = 'VIOLIN' + this.Violin;
         else if (this.Scat)
            res = 'SCAT';
         else if (this.Color) {
            res = 'COL';
            if (!this.Zero)
               res += '0';
            res += zopt;
            if (this.Axis < 0)
               res += 'A';
         } else if (this.Contour) {
            res = 'CONT';
            if (this.Contour > 10)
               res += (this.Contour - 10);
            res += zopt;
         } else if (this.Bar)
            res = (this.BaseLine === false) ? 'B' : 'B1';
         else if (this.Mark)
            res = this.Zero ? 'P0' : 'P'; // here invert logic with 0
         else if (this.Line) {
            res += 'L';
            if (this.Fill)
               res += 'F';
         } else if (this.Off)
            res = '][';

         if (this.Error) {
            res += 'E';
            if (this.ErrorKind >= 0)
               res += this.ErrorKind;
            if (this.errorX === 0)
               res += 'X0';
         }

         if (this.Cjust)
            res += ' CJUST';

         if (this.Hist === true)
            res += 'HIST';

         if (this.Text) {
            res += 'TEXT';
            if (this.TextAngle)
               res += this.TextAngle;
            res += this.TextKind;
         }
      }

      if (this.Palette && this.canHavePalette())
         res += `_PAL${this.Palette}`;

      if (this.is3d() && this.Ortho && is_main_hist)
         res += '_ORTHO';

      if (this.ProfileProj)
         res += '_PROJX' + this.ProfileProj;

      if (this.Profile2DProj)
         res += '_PROJXY' + this.Profile2DProj;

      if (this.Proj)
         res += '_PROJ' + this.Proj;

      if (this.ShowEmpty)
         res += '_SHOWEMPTY';

      if (this.Same)
         res += this.ForceStat ? 'SAMES' : 'SAME';
      else if (is_main_hist && res) {
         if (this.ForceStat || (this.StatEnabled === true))
            res += '_STAT';
         else if (this.NoStat || (this.StatEnabled === false))
            res += '_NOSTAT';
      }

      if (is_main_hist && pad && res) {
         if (pad.fLogx === 2)
            res += '_LOG2X';
         else if (pad.fLogx)
            res += '_LOGX';
         if (pad.fLogy === 2)
            res += '_LOG2Y';
         else if (pad.fLogy)
            res += '_LOGY';
         if (pad.fLogz === 2)
            res += '_LOG2Z';
         else if (pad.fLogz)
            res += '_LOGZ';
         if (pad.fGridx)
            res += '_GRIDX';
         if (pad.fGridy)
            res += '_GRIDY';
         if (pad.fTickx)
            res += '_TICKX';
         if (pad.fTicky)
            res += '_TICKY';
         if (pad.fTickz)
            res += '_TICKZ';
      }

      if (this.cutg_name)
         res += ` [${this.cutg_name}]`;

      return res;
   }

   /** @return true if hmin and hmax values where specified */
   exact_values_range() { return this.ohmin && this.ohmax; }

} // class THistDrawOptions


/**
 * @summary Handle for histogram contour
 *
 * @private
 */

class HistContour {

   constructor(zmin, zmax) {
      this.arr = [];
      this.colzmin = zmin;
      this.colzmax = zmax;
      this.below_min_indx = -1;
      this.exact_min_indx = 0;
   }

   /** @summary Returns contour levels */
   getLevels() { return this.arr; }

   /** @summary Create normal contour levels */
   createNormal(nlevels, log_scale, zminpositive) {
      if (log_scale) {
         if (this.colzmax <= 0)
            this.colzmax = 1.0;
         if (this.colzmin <= 0) {
            if ((zminpositive === undefined) || (zminpositive <= 0))
               this.colzmin = 0.0001 * this.colzmax;
            else
               this.colzmin = ((zminpositive < 3) || (zminpositive > 100)) ? 0.3 * zminpositive : 1;
         }
         if (this.colzmin >= this.colzmax)
            this.colzmin = 0.0001 * this.colzmax;

         const logmin = Math.log(this.colzmin) / Math.log(10),
               logmax = Math.log(this.colzmax) / Math.log(10),
               dz = (logmax - logmin) / nlevels;
         this.arr.push(this.colzmin);
         for (let level = 1; level < nlevels; level++)
            this.arr.push(Math.exp((logmin + dz * level) * Math.log(10)));
         this.arr.push(this.colzmax);
         this.custom = true;
      } else {
         if ((this.colzmin === this.colzmax) && this.colzmin) {
            this.colzmax += 0.01 * Math.abs(this.colzmax);
            this.colzmin -= 0.01 * Math.abs(this.colzmin);
         }
         const dz = (this.colzmax - this.colzmin) / nlevels;
         for (let level = 0; level <= nlevels; level++)
            this.arr.push(this.colzmin + dz * level);
      }
   }

   /** @summary Create custom contour levels */
   createCustom(levels) {
      this.custom = true;
      for (let n = 0; n < levels.length; ++n)
         this.arr.push(levels[n]);

      if (this.colzmax > this.arr.at(-1))
         this.arr.push(this.colzmax);
   }

   /** @summary Configure indices */
   configIndicies(below_min, exact_min) {
      this.below_min_indx = below_min;
      this.exact_min_indx = exact_min;
   }

   /** @summary Get index based on z value */
   getContourIndex(zc) {
      // bins less than zmin not drawn
      if (zc < this.colzmin)
         return this.below_min_indx;

      // if bin content exactly zmin, draw it when col0 specified or when content is positive
      if (zc === this.colzmin)
         return this.exact_min_indx;

      if (!this.custom)
         return Math.floor(0.01 + (zc - this.colzmin) * (this.arr.length - 1) / (this.colzmax - this.colzmin));

      let l = 0, r = this.arr.length - 1;
      if (zc < this.arr[0])
         return -1;
      if (zc >= this.arr[r])
         return r;
      while (l < r - 1) {
         const mid = Math.round((l + r) / 2);
         if (this.arr[mid] > zc)
            r = mid;
         else
            l = mid;
      }
      return l;
   }

   /** @summary Get palette color */
   getPaletteColor(palette, zc) {
      const zindx = this.getContourIndex(zc);
      if (zindx < 0)
         return null;
      const pindx = palette.calcColorIndex(zindx, this.arr.length);
      return palette.getColor(pindx);
   }

   /** @summary Get palette index */
   getPaletteIndex(palette, zc) {
      const zindx = this.getContourIndex(zc);
      return (zindx < 0) ? null : palette.calcColorIndex(zindx, this.arr.length);
   }

} // class HistContour

/**
 * @summary Handle for updating of secondary functions
 *
 * @private
 */

class FunctionsHandler {

   #extra_painters;
   #newfuncs; // array of functions
   #newopts;  // array of options
   #painter;  // object painter to which functions belongs
   #pad_painter; // pad painter

   constructor(painter, pp, funcs, statpainter, update_statpainter) {
      this.#painter = painter;
      this.#pad_painter = pp;

      const painters = [], update_painters = [],
            only_draw = (statpainter === true);

      this.#newfuncs = [];
      this.#newopts = [];

      // find painters associated with histogram/graph/...
      if (!only_draw) {
         pp?.forEachPainterInPad(objp => {
            if (objp.isSecondary(painter) && objp.getSecondaryId()?.match(/^func_|^indx_/))
               painters.push(objp);
         }, 'objects');
      }

      for (let n = 0; n < funcs?.arr.length; ++n) {
         const func = funcs.arr[n], fopt = funcs.opt[n];
         if (!func?._typename)
            continue;
         if (isFunc(painter.needDrawFunc) && !painter.needDrawFunc(painter.getObject(), func))
            continue;

         let funcpainter = null, func_indx = -1;

         if (!only_draw) {
            // try to find matching object in associated list of painters
            for (let i = 0; i < painters.length; ++i) {
               if (painters[i].matchObjectType(func._typename) && (painters[i].getObjectName() === func.fName)) {
                  funcpainter = painters[i];
                  func_indx = i;
                  break;
               }
            }
            // or just in generic list of painted objects
            if (!funcpainter && func.fName)
               funcpainter = pp?.findPainterFor(null, func.fName, func._typename);
         }

         if (funcpainter) {
            funcpainter.updateObject(func, fopt);
            if (func_indx >= 0) {
               painters.splice(func_indx, 1);
               update_painters.push(funcpainter);
            }
         } else {
            // use arrays index while index is important
            this.#newfuncs[n] = func;
            this.#newopts[n] = fopt;
         }
      }

      // stat painter has to be kept even when no object exists in the list
      if (isObject(statpainter)) {
         const indx = painters.indexOf(statpainter);
         if (indx >= 0)
            painters.splice(indx, 1);
         if (update_statpainter && (update_painters.indexOf(statpainter) < 0))
            update_painters.push(statpainter);
      }

      // remove all function which are not found in new list of functions
      if (painters.length)
         pp?.cleanPrimitives(p => painters.indexOf(p) >= 0);

      if (update_painters.length)
         this.#extra_painters = update_painters;
   }

   /** @summary Draw/update functions selected before */
   drawNext(indx) {
      if (this.#extra_painters) {
         const p = this.#extra_painters.shift();
         if (!this.#extra_painters.length)
            this.#extra_painters = undefined;
         return getPromise(p.redraw()).then(() => this.drawNext(0));
      }

      if (!this.#newfuncs || (indx >= this.#newfuncs.length)) {
         this.#newfuncs = this.#newopts = undefined;
         return Promise.resolve(this.#painter); // simplify drawing
      }

      const func = this.#newfuncs[indx], fopt = this.#newopts[indx];

      if (!func || this.#pad_painter?.findPainterFor(func))
         return this.drawNext(indx + 1);

      const func_id = func?.fName ? `func_${func.fName}` : `indx_${indx}`;

      // Required to correctly draw multiple stats boxes
      // TODO: set reference via weak pointer
      func.$main_painter = this.#painter;

      const promise = TPavePainter.canDraw(func)
            ? TPavePainter.draw(this.#pad_painter, func, fopt)
            : this.#pad_painter.drawObject(this.#pad_painter, func, fopt);

      return promise.then(fpainter => {
         fpainter.setSecondaryId(this.#painter, func_id);
         return this.drawNext(indx + 1);
      });
   }

} // class FunctionsHandler


// TH1 bits
//    kNoStats = BIT(9), don't draw stats box
const kUserContour = BIT(10), // user specified contour levels
//      kCanRebin    = BIT(11), // can rebin axis
//      kLogX        = BIT(15), // X-axis in log scale
      kIsZoomed$1 = BIT(16), // bit set when zooming on Y axis
      kNoTitle$1 = BIT(17); // don't draw the histogram title
//      kIsAverage   = BIT(18);  // Bin contents are average (used by Add)

/**
 * @summary Basic painter for histogram classes
 * @private
 */

class THistPainter extends ObjectPainter {

   #doing_redraw_palette; // set during redrawing of palette
   #ignore_frame; // true when drawing without frame functionality
   #color_palette;  // color palette used in histogram
   #auto_exec; // can be reused when sending option back to server
   #funcs_handler; // special instance for functions drawing
   #contour;  // histogram colors contour

   /** @summary Constructor
     * @param {object|string} dom - DOM element for drawing or element id
     * @param {object} histo - TH1 derived histogram object */
   constructor(dom, histo) {
      super(dom, histo);
      this.draw_content = true;
      this.nbinsx = this.nbinsy = 0;
      this.mode3d = false;
   }

   /** @summary Returns histogram object */
   getHisto() { return this.getObject(); }

   /** @summary Returns histogram axis */
   getAxis(name) {
      const histo = this.getObject();
      switch (name) {
         case 'x': return histo?.fXaxis;
         case 'y': return histo?.fYaxis;
         case 'z': return histo?.fZaxis;
      }
      return null;
   }

   /** @summary Returns true if TProfile */
   isTProfile() { return this.matchObjectType(clTProfile); }

   /** @summary Returns true if histogram drawn instead of TF1/TF2 object */
   isTF1() { return false; }

   /** @summary Returns true if TH1K */
   isTH1K() { return this.matchObjectType('TH1K'); }

   /** @summary Returns true if TH2Poly */
   isTH2Poly() {
      return this.matchObjectType(/^TH2Poly/) || this.matchObjectType(/^TProfile2Poly/);
   }

   /** @summary Clear 3d drawings - if any */
   clear3DScene() {
      const fp = this.getFramePainter();
      if (isFunc(fp?.create3DScene))
         fp.create3DScene(-1);
      this.mode3d = false;
   }

   /** @summary Cleanup histogram painter */
   cleanup() {
      this.clear3DScene();

      this.clearHistPalette();
      this.#contour = undefined;

      super.cleanup();
   }

   /** @summary Returns number of histogram dimensions */
   getDimension() {
      const histo = this.getHisto();
      if (!histo)
         return 0;
      if (histo._typename.match(/^TH2/))
         return 2;
      if (histo._typename === clTProfile2D)
         return 2;
      if (histo._typename.match(/^TH3/))
         return 3;
      if (histo._typename === clTProfile3D)
         return 3;
      if (this.isTH2Poly())
         return 2;
      return 1;
   }

   /** @summary Decode options string opt and fill the option structure */
   decodeOptions(opt) {
      const histo = this.getHisto(),
            hdim = this.getDimension(),
            pp = this.getPadPainter(),
            pad = pp?.getRootPad(true);
      let o = this.getOptions(true);

      if (!o?.reset)
         o = this.setOptions(new THistDrawOptions(), true);
      else
         o.reset();

      // when changing draw option, reset attributes usage
      this.lineatt?.setUsed(false);
      this.fillatt?.setUsed(false);
      this.markeratt?.setUsed(false);

      o.decode(opt || histo.fOption, hdim, histo, pp, pad, this);

      this.storeDrawOpt(opt); // opt will be return as default draw option, used in web canvas
   }

   /** @summary Copy draw options from other painter */
   copyOptionsFrom(src) {
      if (src === this)
         return;
      const o = this.getOptions(), o0 = src.getOptions();

      o.Mode3D = o0.Mode3D;
      o.Zero = o0.Zero;
      if (o0.Mode3D) {
         o.Lego = o0.Lego;
         o.Surf = o0.Surf;
      } else {
         o.Color = o0.Color;
         o.Contour = o0.Contour;
      }
   }

   /** @summary copy draw options to all other histograms in the pad */
   copyOptionsToOthers() {
      this.forEachPainter(painter => {
         if ((painter !== this) && isFunc(painter.copyOptionsFrom))
            painter.copyOptionsFrom(this);
      }, 'objects');
   }

   /** @summary Scan histogram content
     * @abstract */
   scanContent(/* when_axis_changed */) {
      // function will be called once new histogram or
      // new histogram content is assigned
      // one should find min, max, bins number, content min/max values
      // if when_axis_changed === true specified, content will be scanned after axis zoom changed
   }

   /** @summary Check pad ranges when drawing of frame axes will be performed
     * @desc Only if histogram is main painter and drawn with SAME option, pad range can be used
     * In all other cases configured range must be derived from histogram itself */
   checkPadRange() {
      if (this.isMainPainter())
         this.check_pad_range = this.getOptions().Same ? 'pad_range' : true;
   }

   /** @summary Create necessary histogram draw attributes */
   createHistDrawAttributes(only_check_auto) {
      const histo = this.getHisto(), o = this.getOptions();

      if (o._pfc > 1 || o._plc > 1 || o._pmc > 1) {
         const pp = this.getPadPainter();
         if (isFunc(pp?.getAutoColor)) {
            const icolor = pp.getAutoColor(histo.$num_histos);
            this.#auto_exec = '';
            if (o._pfc > 1) {
               o._pfc = 1;
               histo.fFillColor = icolor;
               this.#auto_exec += `SetFillColor(${icolor});;`;
               this.deleteAttr('fill');
            }
            if (o._plc > 1) {
               o._plc = 1;
               histo.fLineColor = icolor;
               this.#auto_exec += `SetLineColor(${icolor});;`;
               this.deleteAttr('line');
            }
            if (o._pmc > 1) {
               o._pmc = 1;
               histo.fMarkerColor = icolor;
               this.#auto_exec += `SetMarkerColor(${icolor});;`;
               this.deleteAttr('marker');
            }
         }
      }

      if (only_check_auto)
         this.deleteAttr();
      else {
         this.createAttFill({ attr: histo, color: o.histoFillColor, pattern: o.histoFillPattern, kind: 1 });
         this.createAttLine({ attr: histo, color0: o.histoLineColor, width: o.histoLineWidth });
      }
   }

   /** @summary Update axes attributes in target histogram
     * @private */
   updateAxes(tgt_histo, src_histo, fp) {
      const copyTAxisMembers = (tgt, src, copy_zoom) => {
         tgt.fTitle = src.fTitle;
         tgt.fLabels = src.fLabels;
         tgt.fXmin = src.fXmin;
         tgt.fXmax = src.fXmax;
         tgt.fTimeDisplay = src.fTimeDisplay;
         tgt.fTimeFormat = src.fTimeFormat;
         tgt.fAxisColor = src.fAxisColor;
         tgt.fLabelColor = src.fLabelColor;
         tgt.fLabelFont = src.fLabelFont;
         tgt.fLabelOffset = src.fLabelOffset;
         tgt.fLabelSize = src.fLabelSize;
         tgt.fNdivisions = src.fNdivisions;
         tgt.fTickLength = src.fTickLength;
         tgt.fTitleColor = src.fTitleColor;
         tgt.fTitleFont = src.fTitleFont;
         tgt.fTitleOffset = src.fTitleOffset;
         tgt.fTitleSize = src.fTitleSize;
         if (copy_zoom) {
            tgt.fFirst = src.fFirst;
            tgt.fLast = src.fLast;
            tgt.fBits = src.fBits;
         }
      };

      copyTAxisMembers(tgt_histo.fXaxis, src_histo.fXaxis, this.hasSnapId() && !fp?.zoomChangedInteractive('x'));
      copyTAxisMembers(tgt_histo.fYaxis, src_histo.fYaxis, this.hasSnapId() && !fp?.zoomChangedInteractive('y'));
      copyTAxisMembers(tgt_histo.fZaxis, src_histo.fZaxis, this.hasSnapId() && !fp?.zoomChangedInteractive('z'));
   }

   /** @summary Update histogram object
     * @param obj - new histogram instance
     * @param opt - new drawing option (optional)
     * @return {Boolean} - true if histogram was successfully updated */
   updateObject(obj, opt) {
      const histo = this.getHisto(),
            fp = this.getFramePainter(),
            pp = this.getPadPainter(),
            o = this.getOptions();

      if (obj !== histo) {
         if (!this.matchObjectType(obj))
            return false;

         // simple replace of object does not help - one can have different
         // complex relations between histogram and stat box, histogram and colz axis,
         // one could have THStack or TMultiGraph object
         // The only that could be done is update of content

         const statpainter = pp?.findPainterFor(this.findStat());

         // copy histogram bits
         if (histo.TestBit(kNoStats) !== obj.TestBit(kNoStats)) {
            histo.SetBit(kNoStats, obj.TestBit(kNoStats));
            // here check only stats bit
            if (statpainter) {
               statpainter.Enabled = !histo.TestBit(kNoStats) && !o.NoStat; // && (!o.Same || o.ForceStat)
               // remove immediately when redraw not called for disabled stats
               if (!statpainter.Enabled)
                  statpainter.removeG();
            }
         }

         histo.SetBit(kIsZoomed$1, obj.TestBit(kIsZoomed$1));

         // special treatment for web canvas - also name can be changed
         if (this.hasSnapId()) {
            histo.fName = obj.fName;
            o._pfc = o._plc = o._pmc = 0; // auto colors should be processed in web canvas
         }

         if (!o._pfc)
            histo.fFillColor = obj.fFillColor;
         histo.fFillStyle = obj.fFillStyle;
         if (!o._plc)
            histo.fLineColor = obj.fLineColor;
         histo.fLineStyle = obj.fLineStyle;
         histo.fLineWidth = obj.fLineWidth;
         if (!o._pmc)
            histo.fMarkerColor = obj.fMarkerColor;
         histo.fMarkerSize = obj.fMarkerSize;
         histo.fMarkerStyle = obj.fMarkerStyle;

         histo.fEntries = obj.fEntries;
         histo.fTsumw = obj.fTsumw;
         histo.fTsumwx = obj.fTsumwx;
         histo.fTsumwx2 = obj.fTsumwx2;
         histo.fXaxis.fNbins = obj.fXaxis.fNbins;
         if (this.getDimension() > 1) {
            histo.fTsumwy = obj.fTsumwy;
            histo.fTsumwy2 = obj.fTsumwy2;
            histo.fTsumwxy = obj.fTsumwxy;
            histo.fYaxis.fNbins = obj.fYaxis.fNbins;
            if (this.getDimension() > 2) {
               histo.fTsumwz = obj.fTsumwz;
               histo.fTsumwz2 = obj.fTsumwz2;
               histo.fTsumwxz = obj.fTsumwxz;
               histo.fTsumwyz = obj.fTsumwyz;
               histo.fZaxis.fNbins = obj.fZaxis.fNbins;
            }
         }

         this.updateAxes(histo, obj, fp);

         histo.fArray = obj.fArray;
         histo.fNcells = obj.fNcells;
         histo.fTitle = obj.fTitle;
         histo.fMinimum = obj.fMinimum;
         histo.fMaximum = obj.fMaximum;
         histo.fSumw2 = obj.fSumw2;

         if (!o.ominimum)
            o.minimum = histo.fMinimum;
         if (!o.omaximum)
            o.maximum = histo.fMaximum;

         if (this.getDimension() === 1)
            o.decodeSumw2(histo);

         if (this.isTProfile())
            histo.fBinEntries = obj.fBinEntries;
         else if (this.isTH1K()) {
            histo.fNIn = obj.fNIn;
            histo.fReady = 0;
         } else if (this.isTH2Poly())
            histo.fBins = obj.fBins;

         // remove old functions, update existing, prepare to draw new one
         this.#funcs_handler = new FunctionsHandler(this, pp, obj.fFunctions, statpainter, this.create_stats);

         const changed_opt = (histo.fOption !== obj.fOption);
         histo.fOption = obj.fOption;

         if (((opt !== undefined) && (o.original !== opt)) || changed_opt)
            this.decodeOptions(opt || histo.fOption);
      }

      if (!o.ominimum)
         o.minimum = histo.fMinimum;
      if (!o.omaximum)
         o.maximum = histo.fMaximum;

      if (!o.ominimum && !o.omaximum && o.minimum === o.maximum)
         o.minimum = o.maximum = kNoZoom;

      if (!fp || !fp.zoomChangedInteractive())
         this.checkPadRange();

      this.scanContent();

      this.histogram_updated = true; // indicate that object updated

      return true;
   }

   /** @summary Access or modify histogram min/max
    * @private */
   accessMM(ismin, v) {
      const name = ismin ? 'minimum' : 'maximum',
            o = this.getOptions();
      if (v === undefined)
         return o[name];

      o[name] = v;

      this.interactiveRedraw('pad', ismin ? `exec:SetMinimum(${v})` : `exec:SetMaximum(${v})`);
   }

   /** @summary Extract axes bins and ranges
     * @desc here functions are defined to convert index to axis value and back
     * was introduced to support non-equidistant bins */
   extractAxesProperties(ndim) {
      const assignTAxisFuncs = axis => {
         if (axis.fXbins.length >= axis.fNbins) {
            axis.GetBinCoord = function(bin) {
               const indx = Math.round(bin);
               if (indx <= 0)
                  return this.fXmin;
               if (indx > this.fNbins)
                  return this.fXmax;
               if (indx === bin)
                  return this.fXbins[indx];
               const indx2 = (bin < indx) ? indx - 1 : indx + 1;
               return this.fXbins[indx] * Math.abs(bin - indx2) + this.fXbins[indx2] * Math.abs(bin - indx);
            };
            axis.FindBin = function(x, add) {
               for (let k = 1; k < this.fXbins.length; ++k) {
                  if (x < this.fXbins[k])
                     return Math.floor(k - 1 + add);
               }
               return this.fNbins;
            };
         } else {
            axis.$binwidth = (axis.fXmax - axis.fXmin) / (axis.fNbins || 1);
            axis.GetBinCoord = function(bin) { return this.fXmin + bin * this.$binwidth; };
            axis.FindBin = function(x, add) { return Math.floor((x - this.fXmin) / this.$binwidth + add); };
         }
      };

      this.nbinsx = this.nbinsy = this.nbinsz = 0;

      const histo = this.getHisto(),
            o = this.getOptions();

      this.nbinsx = histo.fXaxis.fNbins;
      this.xmin = histo.fXaxis.fXmin;
      this.xmax = histo.fXaxis.fXmax;
      if (histo.fXaxis.TestBit(EAxisBits.kAxisRange) && (histo.fXaxis.fFirst !== histo.fXaxis.fLast)) {
         if (histo.fXaxis.fFirst === 0)
            this.xmin = histo.fXaxis.GetBinLowEdge(0);
         if (histo.fXaxis.fLast === this.nbinsx + 1)
            this.xmax = histo.fXaxis.GetBinLowEdge(this.nbinsx + 2);
      }

      assignTAxisFuncs(histo.fXaxis);

      this.ymin = histo.fYaxis.fXmin;
      this.ymax = histo.fYaxis.fXmax;

      if (ndim === 1 && o.exact_values_range()) {
         this.ymin = o.hmin;
         this.ymax = o.hmax;
      }

      if (ndim > 1) {
         this.nbinsy = histo.fYaxis.fNbins;
         if (histo.fYaxis.TestBit(EAxisBits.kAxisRange) && (histo.fYaxis.fFirst !== histo.fYaxis.fLast)) {
            if (histo.fYaxis.fFirst === 0)
               this.ymin = histo.fYaxis.GetBinLowEdge(0);
            if (histo.fYaxis.fLast === this.nbinsy + 1)
               this.ymax = histo.fYaxis.GetBinLowEdge(this.nbinsy + 2);
         }
         assignTAxisFuncs(histo.fYaxis);

         this.zmin = histo.fZaxis.fXmin;
         this.zmax = histo.fZaxis.fXmax;

         if ((ndim === 2) && o.ohmin && o.ohmax) {
            this.zmin = o.hmin;
            this.zmax = o.hmax;
         }
      }

      if (ndim > 2) {
         this.nbinsz = histo.fZaxis.fNbins;
         if (histo.fZaxis.TestBit(EAxisBits.kAxisRange) && (histo.fZaxis.fFirst !== histo.fZaxis.fLast)) {
            if (histo.fZaxis.fFirst === 0)
               this.zmin = histo.fZaxis.GetBinLowEdge(0);
            if (histo.fZaxis.fLast === this.nbinsz + 1)
               this.zmax = histo.fZaxis.GetBinLowEdge(this.nbinsz + 2);
         }
         assignTAxisFuncs(histo.fZaxis);
      }
   }

   /** @summary Draw axes for histogram
      * @desc axes can be drawn only for main histogram */
   async drawAxes() {
      const fp = this.getFramePainter();
      if (!fp)
         return false;

      const histo = this.getHisto(),
            o = this.getOptions();

      // artificially add y range to display axes
      if (this.ymin === this.ymax)
         this.ymax += 1;

      if (!this.isMainPainter()) {
         const opts = {
            second_x: (o.AxisPos >= 10),
            second_y: (o.AxisPos % 10) === 1,
            hist_painter: this
         };

         if ((!opts.second_x && !opts.second_y) || fp.hasDrawnAxes(opts.second_x, opts.second_y))
            return false;

         fp.setAxes2Ranges(opts.second_x, histo.fXaxis, this.xmin, this.xmax, opts.second_y, histo.fYaxis, this.ymin, this.ymax);

         fp.createXY2(opts);

         return fp.drawAxes2(opts.second_x, opts.second_y);
      }

      fp.setAxesRanges(histo.fXaxis, this.xmin, this.xmax, histo.fYaxis, this.ymin, this.ymax, histo.fZaxis, 0, 0);

      fp.createXY({
         ndim: this.getDimension(),
         check_pad_range: this.check_pad_range,
         zoom_xmin: this.zoom_xmin,
         zoom_xmax: this.zoom_xmax,
         zoom_ymin: this.zoom_ymin,
         zoom_ymax: this.zoom_ymax,
         xmin_nz: histo.$xmin_nz,
         ymin_nz: this.ymin_nz ?? histo.$ymin_nz,
         swap_xy: o.swap_xy(),
         xticks: o.xticks,
         yticks: o.yticks,
         reverse_x: o.RevX,
         reverse_y: o.RevY,
         symlog_x: o.SymlogX,
         symlog_y: o.SymlogY,
         Proj: o.Proj,
         extra_y_space: o.Text && (o.BarStyle > 0),
         hist_painter: this
      });

      delete this.check_pad_range;
      delete this.zoom_xmin;
      delete this.zoom_xmax;
      delete this.zoom_ymin;
      delete this.zoom_ymax;

      if (o.Same)
         return false;

      const disable_axis_draw = (o.Axis < 0) || (o.Axis === 2);

      return fp.drawAxes(false, disable_axis_draw, disable_axis_draw,
                         o.AxisPos, o.Zscale && o.Zvert,
                         o.Zscale && !o.Zvert, o.Axis !== 1);
   }

   /** @summary Inform web canvas that something changed in the histogram */
   processOnlineChange(kind) {
      const cp = this.getCanvPainter();
      if (isFunc(cp?.processChanges))
         cp.processChanges(kind, this);
   }

   /** @summary Fill option object used in TWebCanvas */
   fillWebObjectOptions(res) {
      if (this.#auto_exec && res) {
         res.fcust = 'auto_exec:' + this.#auto_exec;
         this.#auto_exec = undefined;
      }
   }

   /** @summary Toggle histogram title drawing */
   toggleTitle(arg) {
      const histo = this.getHisto();
      if (!this.isMainPainter() || !histo)
         return false;
      if (arg === kOnlyCheck)
         return !histo.TestBit(kNoTitle$1);
      histo.InvertBit(kNoTitle$1);
      this.updateHistTitle().then(() => this.processOnlineChange(`exec:SetBit(TH1::kNoTitle,${histo.TestBit(kNoTitle$1) ? 1 : 0})`));
   }

   /** @summary Only redraw histogram title
     * @return {Promise} with painter */
   async updateHistTitle() {
      const o = this.getOptions();

      // case when histogram drawn over other histogram (same option)
      if (!this.isMainPainter() || o.Same || (o.Axis > 0))
         return this;

      const tpainter = this.getPadPainter()?.findPainterFor(null, kTitle, clTPaveText),
            pt = tpainter?.getObject();

      if (!tpainter || !pt)
         return this;

      const histo = this.getHisto(),
            draw_title = !histo.TestBit(kNoTitle$1) && (gStyle.fOptTitle > 0);

      pt.Clear();
      if (draw_title)
         pt.AddText(histo.fTitle);
      return tpainter.redraw().then(() => this);
   }

   /** @summary Draw histogram title
     * @return {Promise} with painter */
   async drawHistTitle() {
      const o = this.getOptions();

      // case when histogram drawn over other histogram (same option)
      if (!this.isMainPainter() || o.Same || (o.Axis > 0))
         return this;

      const histo = this.getHisto(), st = gStyle,
            draw_title = !histo.TestBit(kNoTitle$1) && (st.fOptTitle > 0),
            pp = this.getPadPainter();

      let pt = pp.findInPrimitives(kTitle, clTPaveText);

      if (pt) {
         pt.Clear();
         if (draw_title)
            pt.AddText(histo.fTitle);
         return this;
      }

      pt = create$1(clTPaveText);
      Object.assign(pt, {
         fName: kTitle, fOption: 'blNDC', fFillColor: st.fTitleColor, fFillStyle: st.fTitleStyle, fBorderSize: st.fTitleBorderSize,
         fTextFont: st.fTitleFont, fTextSize: st.fTitleFontSize, fTextColor: st.fTitleTextColor, fTextAlign: 22
      });

      if (draw_title)
         pt.AddText(histo.fTitle);

      return TPavePainter.draw(pp, pt, kPosTitle).then(p => {
         p?.setSecondaryId(this, kTitle);
         return this;
      });
   }

   /** @summary Live change and update of title drawing
     * @desc Used from the GED */
   processTitleChange(arg) {
      const histo = this.getHisto(),
            tpainter = this.getPadPainter()?.findPainterFor(null, kTitle);

      if (!histo || !tpainter)
         return null;

      if (arg === 'check')
         return (!this.isMainPainter() || this.getOptions().Same) ? null : histo;

      tpainter.clearPave();
      tpainter.addText(histo.fTitle);

      tpainter.redraw();

      this.submitCanvExec(`SetTitle("${histo.fTitle}")`);
   }

   /** @summary Update statistics when web canvas is drawn */
   updateStatWebCanvas() {
      if (!this.hasSnapId())
         return;

      const stat = this.findStat(),
            statpainter = this.getPadPainter()?.findPainterFor(stat);

      if (statpainter && !statpainter.hasSnapId())
         statpainter.redraw();
   }

   /** @summary Find stats box in list of functions */
   findStat() {
      return this.findFunction(clTPaveStats, 'stats');
   }

   /** @summary Toggle stat box drawing
     * @private */
   toggleStat(arg) {
      const pp = this.getPadPainter();
      let stat = this.findStat(), statpainter;

      if (!arg)
         arg = '';

      if (!stat) {
         if (arg.indexOf('-check') > 0)
            return false;
         // when stat box created first time, one need to draw it
         stat = this.createStat(true);
      } else
         statpainter = pp.findPainterFor(stat);


      if (arg === kOnlyCheck)
         return statpainter?.Enabled || false;

      if (arg === 'fitpar-check')
         return stat?.fOptFit || false;

      if (arg === 'fitpar-toggle') {
         if (!stat)
            return false;
         stat.fOptFit = stat.fOptFit ? 0 : 1111; // for websocket command should be send to server
         statpainter?.redraw();
         return true;
      }

      let has_stats;

      if (statpainter) {
         statpainter.Enabled = !statpainter.Enabled;
         this.getOptions().StatEnabled = statpainter.Enabled; // used only for interactive
         // when stat box is drawn, it always can be drawn individually while it
         // should be last for colz redrawPad is used
         statpainter.redraw();
         has_stats = statpainter.Enabled;
      } else {
         // return promise which will be used to process
         has_stats = TPavePainter.draw(pp, stat);
      }

      this.processOnlineChange(`exec:SetBit(TH1::kNoStats,${has_stats ? 0 : 1})`, this);

      return has_stats;
   }

   /** @summary Returns true if stats box fill can be ignored */
   isIgnoreStatsFill() {
      return !this.getObject() || (!this.draw_content && !this.create_stats && !this.hasSnapId());
   }

   /** @summary Create stat box for histogram if required */
   createStat(force) {
      const histo = this.getHisto(),
            o = this.getOptions();
      if (!histo)
         return null;

      if (!force && !o.ForceStat) {
         if (o.NoStat || histo.TestBit(kNoStats) || !settings.AutoStat)
            return null;
         if (!this.isMainPainter())
            return null;
      }

      const st = gStyle;
      let stats = this.findStat(),
          optstat = o.optstat,
          optfit = o.optfit;

      if (optstat !== undefined) {
         if (stats)
            stats.fOptStat = optstat;
         o.optstat = undefined;
      } else
         optstat = histo.$custom_stat || st.fOptStat;

      if (optfit !== undefined) {
         if (stats)
            stats.fOptFit = optfit;
         o.optfit = undefined;
      } else
         optfit = st.fOptFit;

      if (!stats && !optstat && !optfit)
         return null;

      this.create_stats = true;

      if (stats)
         return stats;

      stats = create$1(clTPaveStats);
      Object.assign(stats, {
         fName: 'stats', fOptStat: optstat, fOptFit: optfit,
         fX1NDC: st.fStatX - st.fStatW, fY1NDC: st.fStatY - st.fStatH, fX2NDC: st.fStatX, fY2NDC: st.fStatY,
         fTextAlign: 12
      });

      stats.AddText(histo.fName);

      this.addFunction(stats);

      return stats;
   }

   /** @summary Find function in histogram list of functions */
   findFunction(type_name, obj_name) {
      const funcs = this.getHisto()?.fFunctions?.arr;
      if (!funcs)
         return null;

      for (let i = 0; i < funcs.length; ++i) {
         const f = funcs[i];
         if (obj_name && (f.fName !== obj_name))
            continue;
         if (f._typename === type_name)
            return f;
      }

      return null;
   }

   /** @summary Add function to histogram list of functions */
   addFunction(obj, asfirst) {
      const histo = this.getHisto();
      if (!histo || !obj)
         return;

      if (!histo.fFunctions)
         histo.fFunctions = create$1(clTList);

      if (asfirst)
         histo.fFunctions.AddFirst(obj);
      else
         histo.fFunctions.Add(obj);
   }

   /** @summary Check if such function should be drawn directly */
   needDrawFunc(histo, func) {
      const o = this.getOptions();

      if (func._typename === clTPaveStats)
         return (func.fName !== 'stats') || (!histo.TestBit(kNoStats) && !o.NoStat); // && (!o.Same || o.ForceStat))

      if ((func._typename === clTF1) || (func._typename === clTF2))
         return o.AllFunc || !func.TestBit(BIT(9)); // TF1::kNotDraw

      if ((func._typename === 'TGraphDelaunay') || (func._typename === 'TGraphDelaunay2D'))
         return false; // do not try to draw delaunay classes

      return func._typename !== clTPaletteAxis;
   }

   /** @summary Method draws functions from the histogram list of functions
     * @return {Promise} fulfilled when drawing is ready */
   async drawFunctions() {
      const handler = new FunctionsHandler(this, this.getPadPainter(), this.getHisto().fFunctions, true);
      return handler.drawNext(0); // returns this painter
   }

   /** @summary Method used to update functions which are prepared before
     * @return {Promise} fulfilled when drawing is ready */
   async updateFunctions() {
      const res = this.#funcs_handler?.drawNext(0) ?? this;
      this.#funcs_handler = undefined;
      return res;
   }

   /** @summary Returns selected index for specified axis
     * @desc be aware - here indexes starts from 0 */
   getSelectIndex(axis, side, add) {
      let indx, taxis = this.getAxis(axis);
      const nbin = this[`nbins${axis}`] ?? 0,
            o = this.getOptions();

      if (o.second_x && axis === 'x')
         axis = 'x2';
      if (o.second_y && axis === 'y')
         axis = 'y2';
      const fp = this.getFramePainter(),
            min = fp ? fp[`zoom_${axis}min`] : 0,
            max = fp ? fp[`zoom_${axis}max`] : 0;

      if ((min !== max) && taxis) {
         if (side === 'left')
            indx = taxis.FindBin(min, add || 0);
         else
            indx = taxis.FindBin(max, (add || 0) + 0.5);
         if (indx < 0)
            indx = 0;
         else if (indx > nbin)
            indx = nbin;
      } else
         indx = (side === 'left') ? 0 : nbin;

      // TAxis object of histogram, where user range can be stored
      if (taxis) {
         if ((taxis.fFirst === taxis.fLast) || !taxis.TestBit(EAxisBits.kAxisRange) ||
             ((taxis.fFirst === 1) && (taxis.fLast === nbin)))
            taxis = null;
      }

      if (side === 'left') {
         indx = Math.max(indx, 0);
         if (taxis && (taxis.fFirst > 1) && (indx < taxis.fFirst))
            indx = taxis.fFirst - 1;
         else if (taxis?.fFirst === 0) // showing underflow bin
            indx = -1;
      } else {
         indx = Math.min(indx, nbin);
         if (taxis && (taxis.fLast <= nbin) && (indx > taxis.fLast))
            indx = taxis.fLast;
         else if (taxis?.fLast === nbin + 1)
            indx = nbin + 1;
      }

      return indx;
   }

   /** @summary Unzoom user range if any */
   unzoomUserRange(dox, doy, doz) {
      const histo = this.getHisto(),
            o = this.getOptions();

      if (!histo)
         return false;

      let res = false;

      const unzoomTAxis = obj => {
         if (!obj || !obj.TestBit(EAxisBits.kAxisRange))
            return false;
         if (obj.fFirst === obj.fLast)
            return false;
         if ((obj.fFirst <= 1) && (obj.fLast >= obj.fNbins))
            return false;
         obj.InvertBit(EAxisBits.kAxisRange);
         return true;
      }, uzoomMinMax = ndim => {
         if (this.getDimension() !== ndim)
            return false;
         if ((o.minimum === kNoZoom) && (o.maximum === kNoZoom))
            return false;
         if (!this.draw_content)
            return false; // if not drawing content, not change min/max
         o.minimum = o.maximum = kNoZoom;
         this.scanContent(); // to reset ymin/ymax
         return true;
      };

      if (dox && unzoomTAxis(histo.fXaxis))
         res = true;
      if (doy && (unzoomTAxis(histo.fYaxis) || uzoomMinMax(1)))
         res = true;
      if (doz && (unzoomTAxis(histo.fZaxis) || uzoomMinMax(2)))
         res = true;

      return res;
   }

   /** @summary Add different interactive handlers
     * @desc only first (main) painter in list allowed to add interactive functionality
     * Most of interactivity now handled by frame
     * @return {Promise} for ready */
   async addInteractivity() {
      const ismain = this.isMainPainter(),
            second_axis = (this.getOptions().AxisPos > 0),
            fp = (ismain || second_axis) ? this.getFramePainter() : null;
      return fp?.addInteractivity(!ismain && second_axis) ?? false;
   }

   /** @summary Invoke dialog to enter and modify user range */
   changeUserRange(menu, arg) {
      const histo = this.getHisto(),
            taxis = histo ? histo[`f${arg}axis`] : null;
      if (!taxis)
         return;

      let curr = `[1,${taxis.fNbins}]`;
      if (taxis.TestBit(EAxisBits.kAxisRange))
         curr = `[${taxis.fFirst},${taxis.fLast}]`;

      menu.input(`Enter user range for axis ${arg} like [1,${taxis.fNbins}]`, curr).then(res => {
         if (!res)
            return;
         res = JSON.parse(res);
         if (!res || (res.length !== 2))
            return;
         const first = parseInt(res[0]),
               last = parseInt(res[1]);
         if (!Number.isInteger(first) || !Number.isInteger(last))
            return;
         taxis.fFirst = first;
         taxis.fLast = last;
         taxis.SetBit(EAxisBits.kAxisRange, (taxis.fFirst < taxis.fLast) && (taxis.fFirst >= 1) && (taxis.fLast <= taxis.fNbins));

         this.interactiveRedraw();
      });
   }

   /** @summary Start dialog to modify range of axis where histogram values are displayed */
   changeValuesRange(menu) {
      const o = this.getOptions();

      let curr;
      if ((o.minimum !== kNoZoom) && (o.maximum !== kNoZoom))
         curr = `[${o.minimum},${o.maximum}]`;
      else
         curr = `[${this.gminbin},${this.gmaxbin}]`;

      menu.input('Enter min/max hist values or empty string to reset', curr).then(res => {
         res = res ? JSON.parse(res) : [];

         if (!isObject(res) || (res.length !== 2) || !Number.isFinite(res[0]) || !Number.isFinite(res[1]))
            o.minimum = o.maximum = kNoZoom;
         else {
            o.minimum = res[0];
            o.maximum = res[1];
         }

         this.interactiveRedraw();
      });
   }

   /** @summary Execute histogram menu command
     * @desc Used to catch standard menu items and provide local implementation */
   executeMenuCommand(method, args) {
      if (super.executeMenuCommand(method, args))
         return true;

      if (method.fClassName === clTAxis) {
         const p = isStr(method.$execid) ? method.$execid.indexOf('#') : -1,
               kind = p > 0 ? method.$execid.slice(p + 1) : 'x',
               fp = this.getFramePainter();
         if (method.fName === 'UnZoom') {
            fp?.unzoom(kind);
            return true;
         } else if (method.fName === 'SetRange') {
            const axis = fp?.getAxis(kind), bins = JSON.parse(`[${args}]`);
            if (axis && bins?.length === 2)
               fp?.zoom(kind, axis.GetBinLowEdge(bins[0]), axis.GetBinLowEdge(bins[1] + 1));
            // let execute command on server
         } else if (method.fName === 'SetRangeUser') {
            const values = JSON.parse(`[${args}]`);
            if (values?.length === 2)
               fp?.zoom(kind, values[0], values[1]);
            // let execute command on server
         }
      }

      return false;
   }

   /** @summary Fill histogram context menu */
   fillContextMenuItems(menu) {
      const histo = this.getHisto(),
            fp = this.getFramePainter(),
            o = this.getOptions();

      if (!histo)
         return;

      if ((o.Axis <= 0) && !this.isTF1())
         menu.addchk(this.toggleStat(kOnlyCheck), 'Show statbox', () => this.toggleStat());

      if (this.isMainPainter()) {
         menu.sub('Title');
         menu.addchk(this.toggleTitle(kOnlyCheck), 'Show', () => this.toggleTitle());
         menu.add('Edit', () => menu.input('Enter histogram title', histo.fTitle).then(res => {
            setHistogramTitle(histo, res);
            this.interactiveRedraw();
         }));
         menu.endsub();
      }

      if (this.draw_content) {
         if (this.getDimension() === 1)
            menu.add('User range X', () => this.changeUserRange(menu, 'X'));
         else {
            menu.sub('User ranges');
            menu.add('X', () => this.changeUserRange(menu, 'X'));
            menu.add('Y', () => this.changeUserRange(menu, 'Y'));
            if (this.getDimension() > 2)
               menu.add('Z', () => this.changeUserRange(menu, 'Z'));
            else
               menu.add('Values', () => this.changeValuesRange(menu));
            menu.endsub();
         }

         if (isFunc(this.fillHistContextMenu))
            this.fillHistContextMenu(menu);

         menu.addRedrawMenu(this.getPrimary());
      }

      if (o.Mode3D) {
         // menu for 3D drawings

         if (menu.size() > 0)
            menu.separator();

         const main = this.getMainPainter() || this;

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

         menu.addchk(fp?.enable_highlight, 'Highlight bins', () => {
            fp.enable_highlight = !fp.enable_highlight;
            if (!fp.enable_highlight && fp.mode3d && isFunc(fp.highlightBin3D))
               fp.highlightBin3D(null);
         });

         if (isFunc(fp?.render3D)) {
            menu.addchk(main.options.FrontBox, 'Front box', () => {
               main.options.FrontBox = !main.options.FrontBox;
               fp.render3D();
            });
            menu.addchk(main.options.BackBox, 'Back box', () => {
               main.options.BackBox = !main.options.BackBox;
               fp.render3D();
            });
            menu.addchk(fp.camera?.isOrthographicCamera, 'Orthographic camera', flag => {
               main.options.Ortho = flag;
               fp.change3DCamera(flag);
            });
         }

         if (this.draw_content) {
            menu.addchk(!o.Zero, 'Suppress zeros', () => {
               o.Zero = !o.Zero;
               this.interactiveRedraw('pad');
            });

            if ((o.Lego === 12) || (o.Lego === 14)) {
               menu.addchk(o.Zscale, 'Z scale', () => this.toggleColz());
               this.fillPaletteMenu(menu, true);
            }
         }

         if (isFunc(main.control?.reset))
            menu.add('Reset camera', () => main.control.reset());
      }

      if (this.histogram_updated && fp.zoomChangedInteractive())
         menu.add('Let update zoom', () => fp.zoomChangedInteractive('reset'));
   }

   /** @summary Returns snap id for object or sub-element
     * @private */
   getSnapId(subelem) {
      if (this.isTF1() && (subelem === 'x' || subelem === 'y' || subelem === 'z'))
         subelem = 'hist#' + subelem;
      return super.getSnapId(subelem);
   }

   /** @summary Auto zoom into histogram non-empty range
     * @abstract */
   autoZoom() {}

   /** @summary Process click on histogram-defined buttons */
   clickButton(funcname) {
      const fp = this.getFramePainter();
      if (!this.isMainPainter() || !fp)
         return false;

      switch (funcname) {
         case 'ToggleZoom':
            if ((fp.zoom_xmin !== fp.zoom_xmax) || (fp.zoom_ymin !== fp.zoom_ymax) || (fp.zoom_zmin !== fp.zoom_zmax)) {
               const pr = fp.unzoom();
               fp.zoomChangedInteractive('reset');
               return pr;
            }
            if (this.draw_content)
               return this.autoZoom();
            break;
         case 'ToggleLogX': return fp.toggleAxisLog('x');
         case 'ToggleLogY': return fp.toggleAxisLog('y');
         case 'ToggleLogZ': return fp.toggleAxisLog('z');
         case 'ToggleStatBox': return getPromise(this.toggleStat());
         case 'ToggleColorZ': return this.toggleColz();
      }
      return false;
   }

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

      pp.addPadButton('auto_zoom', 'Toggle between unzoom and autozoom-in', 'ToggleZoom', 'Ctrl *');
      pp.addPadButton('arrow_right', 'Toggle log x', 'ToggleLogX', 'PageDown');
      pp.addPadButton('arrow_up', 'Toggle log y', 'ToggleLogY', 'PageUp');
      if (this.getDimension() > 1)
         pp.addPadButton('arrow_diag', 'Toggle log z', 'ToggleLogZ');
      pp.addPadButton('statbox', 'Toggle stat box', 'ToggleStatBox');
      if (!not_shown)
         pp.showPadButtons();
   }

   /** @summary Returns tooltip information for 3D drawings */
   get3DToolTip(indx) {
      const histo = this.getHisto(),
            tip = { bin: indx, name: histo.fName, title: histo.fTitle };
      switch (this.getDimension()) {
         case 1:
            tip.ix = indx;
            tip.iy = 1;
            tip.value = histo.getBinContent(tip.ix);
            tip.error = histo.getBinError(indx);
            tip.lines = this.getBinTooltips(indx - 1);
            break;
         case 2:
            tip.ix = indx % (this.nbinsx + 2);
            tip.iy = (indx - tip.ix) / (this.nbinsx + 2);
            tip.value = histo.getBinContent(tip.ix, tip.iy);
            tip.error = histo.getBinError(indx);
            tip.lines = this.getBinTooltips(tip.ix - 1, tip.iy - 1);
            break;
         case 3:
            tip.ix = indx % (this.nbinsx + 2);
            tip.iy = ((indx - tip.ix) / (this.nbinsx + 2)) % (this.nbinsy + 2);
            tip.iz = (indx - tip.ix - tip.iy * (this.nbinsx + 2)) / (this.nbinsx + 2) / (this.nbinsy + 2);
            tip.value = histo.getBinContent(tip.ix, tip.iy, tip.iz);
            tip.error = histo.getBinError(indx);
            tip.lines = this.getBinTooltips(tip.ix - 1, tip.iy - 1, tip.iz - 1);
            break;
      }

      return tip;
   }

   /** @summary Create contour object for histogram */
   createContour(nlevels, zmin, zmax, zminpositive, custom_levels) {
      const cntr = new HistContour(zmin, zmax),
            ndim = this.getDimension(),
            is_th2poly = this.isTH2Poly(),
            fp = this.getFramePainter(),
            o = this.getOptions();

      if (custom_levels)
         cntr.createCustom(custom_levels);
      else {
         if (nlevels < 2)
            nlevels = gStyle.fNumberContours;
         const pad = this.getPadPainter()?.getRootPad(true),
               logv = pad?.fLogv ?? ((ndim === 2) && pad?.fLogz);

         cntr.createNormal(nlevels, logv ?? 0, zminpositive);
      }

      cntr.configIndicies(o.Zero && !is_th2poly ? -1 : 0, cntr.colzmin || !o.Zero || is_th2poly ? 0 : -1);

      if (fp && (ndim < 3) && !fp.mode3d) {
         fp.zmin = cntr.colzmin;
         fp.zmax = cntr.colzmax;
      }

      this.#contour = cntr;
      return cntr;
   }

   /** @summary Reset contour object
    * @private */
   resetContour() {
      this.#contour = undefined;
   }

   /** @summary Return Z-scale ranges to create contour */
   #getContourRanges(main, fp) {
      const o = this.getOptions(),
            src = (this !== main) && ((main?.minbin !== undefined) || main?.options.ohmin) && !o.IgnoreMainScale && !main?.tt_handle?.ScatterPlot ? main : this;
      let apply_min, zmin = src.minbin, zmax = src.maxbin, zminpos = src.minposbin;

      if (zmin === zmax) {
         if (src.options.ohmin && src.options.ohmax) {
            zmin = src.options.hmin;
            zmax = src.options.hmax;
            zminpos = Math.max(zmin, zmax * 1e-10);
         } else {
            zmin = src.gminbin;
            zmax = src.gmaxbin;
            zminpos = src.gminposbin;
         }
      }

      let gzmin = zmin, gzmax = zmax;
      if (o.minimum !== kNoZoom) {
         zmin = o.minimum;
         gzmin = Math.min(gzmin, zmin);
         apply_min = true;
      }
      if (o.maximum !== kNoZoom) {
         zmax = o.maximum;
         gzmax = Math.max(gzmax, zmax);
         apply_min = false;
      }

      if (zmin >= zmax) {
         if (apply_min || !zmin)
            zmax = zmin + 1;
         else
            zmin = zmax - 1;
      }

      if (fp?.zoomChangedInteractive('z')) {
         const mod = (fp.zoom_zmin !== fp.zoom_zmax);
         zmin = mod ? fp.zoom_zmin : gzmin;
         zmax = mod ? fp.zoom_zmax : gzmax;
      }

      return { zmin, zmax, zminpos, gzmin, gzmax };
   }

   /** @summary Return contour object */
   getContour(force_recreate) {
      if ((force_recreate === false) || (this.#contour && (force_recreate !== true)))
         return this.#contour;

      const main = this.getMainPainter(),
            fp = this.getFramePainter(),
            main_cont = main?.getContour(false);

      if (main_cont && (main !== this) && !this.getOptions().IgnoreMainScale) {
         this.#contour = main_cont;
         return main_cont;
      }

      // if not initialized, first create contour array
      // difference from ROOT - fContour includes also last element with maxbin, which makes easier to build logz
      // when no same0 draw option specified, use main painter for creating contour, also ignore scatter drawing for main painter
      const histo = this.getObject(),
            r = this.#getContourRanges(main, fp);
      let nlevels = 0, custom_levels;

      if (histo.fContour?.length > 1) {
         if (histo.TestBit(kUserContour))
            custom_levels = histo.fContour;
         else
            nlevels = histo.fContour.length;
      }

      const cntr = this.createContour(nlevels, r.zmin, r.zmax, r.zminpos, custom_levels);

      if ((this.getDimension() < 3) && fp) {
         fp.zmin = r.gzmin;
         fp.zmax = r.gzmax;

         if ((r.gzmin !== cntr.colzmin) || (r.gzmax !== cntr.colzmax)) {
            fp.zoom_zmin = cntr.colzmin;
            fp.zoom_zmax = cntr.colzmax;
         } else
            fp.zoom_zmin = fp.zoom_zmax = 0;
      }

      return cntr;
   }

   /** @summary Return levels from contour object */
   getContourLevels(force_recreate) {
      return this.getContour(force_recreate).getLevels();
   }

   /** @summary Returns color palette associated with histogram
     * @desc Create if required, checks pad and canvas for custom palette */
   getHistPalette(force) {
      let pal = force ? null : this.#color_palette;
      if (pal)
         return pal;
      const pp = this.getPadPainter(),
            o = this.getOptions();

      if (!o.Palette) {
         if (isFunc(pp?.getCustomPalette))
            pal = pp.getCustomPalette();
      }
      if (!pal)
         pal = getColorPalette(o.Palette, pp?.isGrayscale());
      this.#color_palette = pal;
      return pal;
   }

   /** @summary Remove palette */
   clearHistPalette() {
      this.#color_palette = undefined;
   }

   /** @summary Fill menu entries for palette */
   fillPaletteMenu(menu, only_palette) {
      const o = this.getOptions();

      menu.addPaletteMenu(o.Palette || settings.Palette, arg => {
         o.Palette = parseInt(arg);
         this.getHistPalette(true);
         this.redraw(); // redraw histogram
      });
      if (!only_palette) {
         menu.add('Default position', () => {
            this.drawColorPalette(o.Zscale, false, true)
                .then(() => this.processOnlineChange('drawopt'));
         }, 'Set default position for palette');

         const pal = this.findFunction(clTPaletteAxis),
               is_vert = !pal ? true : pal.fX2NDC - pal.fX1NDC < pal.fY2NDC - pal.fY1NDC;
         menu.addchk(is_vert, 'Vertical', flag => {
            o.Zvert = flag;
            this.drawColorPalette(o.Zscale, false, 'toggle')
                     .then(() => this.processOnlineChange('drawopt'));
         }, 'Toggle palette vertical/horizontal flag');

         menu.add('Bring to front', () => this.getPadPainter()?.findPainterFor(pal)?.bringToFront());
      }
   }

   /** @summary draw color palette
     * @return {Promise} when done */
   async drawColorPalette(enabled, postpone_draw, can_move) {
      const o = this.getOptions(),
            do_toggle = can_move === 'toggle';

      // in special cases like scatter palette drawing is ignored
      if (o.IgnorePalette)
         return null;

      // only when create new palette, one could change frame size
      const mp = this.getMainPainter();
      if (mp && (mp !== this) && (mp.draw_content !== false) && mp.options.Zscale)
         return null;

      const pp = this.getPadPainter();
      let pal = this.findFunction(clTPaletteAxis),
          pal_painter = pp?.findPainterFor(pal);

      const found_in_func = Boolean(pal);

      if (!pal_painter && !pal && !o.Axis) {
         pal_painter = pp?.findPainterFor(undefined, undefined, clTPaletteAxis);
         if (pal_painter) {
            pal = pal_painter.getObject();
            // add to list of functions
            this.addFunction(pal, true);
         }
      }

      if (!enabled) {
         if (pal_painter && !o.Same) {
            o.Zvert = pal_painter.isPaletteVertical();
            pal_painter.Enabled = false;
            pal_painter.removeG(); // completely remove drawing without need to redraw complete pad
         }

         return null;
      }

      if (!pal) {
         pal = create$1(clTPaletteAxis);

         if (!can_move)
            can_move = !o.Same;

         pal.fInit = 1;
         pal.$can_move = can_move;
         pal.$generated = true;

         if (o.Zvert)
            Object.assign(pal, { fX1NDC: 1.005 - gStyle.fPadRightMargin, fX2NDC: 1.045 - gStyle.fPadRightMargin, fY1NDC: gStyle.fPadBottomMargin, fY2NDC: 1 - gStyle.fPadTopMargin });
         else
            Object.assign(pal, { fX1NDC: gStyle.fPadLeftMargin, fX2NDC: 1 - gStyle.fPadRightMargin, fY1NDC: 1.005 - gStyle.fPadTopMargin, fY2NDC: 1.045 - gStyle.fPadTopMargin });

         Object.assign(pal.fAxis, { fChopt: '+', fLineSyle: 1, fLineWidth: 1, fTextAngle: 0, fTextAlign: 11 });

         if (this.getDimension() === 2) {
            const zaxis = this.getHisto().fZaxis;
            Object.assign(pal.fAxis, {
               fTitle: zaxis.fTitle, fTitleSize: zaxis.fTitleSize,
               fTitleOffset: zaxis.fTitleOffset, fTitleColor: zaxis.fTitleColor,
               fLineColor: zaxis.fAxisColor, fTextSize: zaxis.fLabelSize,
               fTextColor: zaxis.fLabelColor, fTextFont: zaxis.fLabelFont,
               fLabelOffset: zaxis.fLabelOffset
            });
         }

         // place colz in the beginning, that stat box is always drawn on the top
         this.addFunction(pal, true);
      } else if ((pal_painter?.isPaletteVertical() !== undefined) && !do_toggle)
         o.Zvert = pal_painter.isPaletteVertical();

      const fp = this.getFramePainter();

      // keep palette width
      if (can_move && fp && pal.$can_move) {
         if (o.Zvert) {
            if (do_toggle) {
               const d = pal.fY2NDC - pal.fY1NDC;
               pal.fX1NDC = fp.fX2NDC + 0.005;
               pal.fX2NDC = pal.fX1NDC + d;
            }
            if (pal.fX1NDC > (fp.fX1NDC + fp.fX2NDC) * 0.5) {
               pal.fX2NDC = fp.fX2NDC + 0.005 + (pal.fX2NDC - pal.fX1NDC);
               pal.fX1NDC = fp.fX2NDC + 0.005;
            } else {
               pal.fX1NDC = fp.fX1NDC - 0.03 - (pal.fX2NDC - pal.fX1NDC);
               pal.fX2NDC = fp.fX1NDC - 0.03;
            }
            pal.fY1NDC = fp.fY1NDC;
            pal.fY2NDC = fp.fY2NDC;
         } else {
            if (do_toggle) {
               const d = pal.fX2NDC - pal.fX1NDC;
               pal.fY1NDC = fp.fY2NDC + 0.005;
               pal.fY2NDC = pal.fY1NDC + d;
            }

            pal.fX1NDC = fp.fX1NDC;
            pal.fX2NDC = fp.fX2NDC;
            if (pal.fY2NDC > (fp.fY1NDC + fp.fY2NDC) * 0.5) {
               pal.fY2NDC = fp.fY2NDC + 0.005 + (pal.fY2NDC - pal.fY1NDC);
               pal.fY1NDC = fp.fY2NDC + 0.005;
            } else {
               pal.fY1NDC = fp.fY1NDC - 0.05 - (pal.fY2NDC - pal.fY1NDC);
               pal.fY2NDC = fp.fY1NDC - 0.05;
            }
         }
      }

      //  required for z scale setting
      // TODO: use weak reference (via pad list of painters and any kind of string)
      pal.$main_painter = this;

      let arg = 'bring_stats_front', pr;
      if (postpone_draw)
         arg += ';postpone';
      if (can_move && !this.#doing_redraw_palette)
         arg += ';can_move';
      if (o.Cjust)
         arg += ';cjust';

      if (!pal_painter) {
         // when histogram drawn on sub pad, let draw new axis object on the same pad
         pr = TPavePainter.draw(pp, pal, arg).then(_palp => {
            pal_painter = _palp;
            pal_painter.setSecondaryId(this, found_in_func && !pal.$generated ? `func_${pal.fName}` : undefined);
         });
      } else {
         pal_painter.Enabled = true;
         // real drawing will be perform at the end
         if (postpone_draw)
            return pal_painter;
         pr = pal_painter.drawPave(arg);
      }

      return pr.then(() => {
         // mark painter as secondary - not in list of TCanvas primitives
         o.Zvert = pal_painter.isPaletteVertical();

         // make dummy redraw, palette will be updated only from histogram painter
         pal_painter.redraw = () => {};

         let need_redraw = false;

         // special code to adjust frame position to actual position of palette
         if (can_move && fp && !this.#doing_redraw_palette) {
            const pad = pp?.getRootPad(true);

            if (o.Zvert) {
               if ((pal.fX1NDC > 0.5) && (fp.fX2NDC > pal.fX1NDC)) {
                  need_redraw = true;
                  fp.fX2NDC = pal.fX1NDC - 0.01;

                  if (fp.fX1NDC > fp.fX2NDC - 0.1)
                     fp.fX1NDC = Math.max(0, fp.fX2NDC - 0.1);
               } else if ((pal.fX2NDC < 0.5) && (fp.fX1NDC < pal.fX2NDC)) {
                  need_redraw = true;
                  fp.fX1NDC = pal.fX2NDC + 0.05;
                  if (fp.fX2NDC < fp.fX1NDC + 0.1)
                     fp.fX2NDC = Math.min(1, fp.fX1NDC + 0.1);
               }
               if (need_redraw && pad) {
                  pad.fLeftMargin = fp.fX1NDC;
                  pad.fRightMargin = 1 - fp.fX2NDC;
               }
            } else {
               if ((pal.fY1NDC > 0.5) && (fp.fY2NDC > pal.fY1NDC)) {
                  need_redraw = true;
                  fp.fY2NDC = pal.fY1NDC - 0.01;
                  if (fp.fY1NDC > fp.fY2NDC - 0.1)
                     fp.fY1NDC = Math.max(0, fp.fXYNDC - 0.1);
               } else if ((pal.fY2NDC < 0.5) && (fp.fY1NDC < pal.fY2NDC)) {
                  need_redraw = true;
                  fp.fY1NDC = pal.fY2NDC + 0.05;
                  if (fp.fXYNDC < fp.fY1NDC + 0.1)
                     fp.fY2NDC = Math.min(1, fp.fY1NDC + 0.1);
               }
               if (need_redraw && pad) {
                  pad.fTopMargin = fp.fY1NDC;
                  pad.fBottomMargin = 1 - fp.fY2NDC;
               }
            }
         }

         if (!need_redraw)
            return pal_painter;

         this.#doing_redraw_palette = true;

         fp.redraw();

         const pr2 = !postpone_draw ? this.redraw() : Promise.resolve(true);
         return pr2.then(() => {
            this.#doing_redraw_palette = undefined;
            return pal_painter;
         });
      });
   }

   /** @summary Toggle color z palette drawing */
   toggleColz() {
      const o = this.getOptions();

      if (o.canHavePalette()) {
         o.Zscale = !o.Zscale;
         return this.drawColorPalette(o.Zscale, false, true)
                    .then(() => this.processOnlineChange('drawopt'));
      }
   }

   /** @summary Toggle 3D drawing mode */
   toggleMode3D() {
      const o = this.getOptions();

      o.Mode3D = !o.Mode3D;

      if (o.Mode3D) {
         if (!o.Surf && !o.Lego && !o.Error) {
            if ((this.nbinsx >= 50) || (this.nbinsy >= 50))
               o.Lego = o.Scat ? 13 : 14;
            else
               o.Lego = o.Scat ? 1 : 12;

            o.Zero = false; // do not show zeros by default
         }
      }

      this.copyOptionsToOthers();
      return this.interactiveRedraw('pad', 'drawopt');
   }

   /** @summary Get graphics conversion functions for this histogram */
   getHistGrFuncs(rounding = true) {
      let funcs;
      if (this.isUseFrame()) {
         const o = this.getOptions();
         funcs = this.getFramePainter()?.getGrFuncs(o.second_x, o.second_y);
         if (funcs)
            return funcs;
      }

      funcs = this.getAxisToSvgFunc(false, rounding, false) || { x: v => v, y: v => v };

      funcs.$painter = this;
      funcs.grx = funcs.x;
      funcs.gry = funcs.y;
      funcs.logx = funcs.pad?.fLogx;
      funcs.logy = funcs.pad?.fLogy;
      funcs.swap_xy = function() { return this.fp?.swap_xy() ?? false; };
      funcs.getFrameWidth = function() { return this.$painter.getPadPainter().getPadWidth(); };
      funcs.getFrameHeight = function() { return this.$painter.getPadPainter().getPadHeight(); };
      funcs.isAxisZoomed = function() { return false; };
      funcs.revertAxis = function(name, v) { return this.$painter.svgToAxis(name, v); };
      funcs.axisAsText = function(_name, v) { return v.toString(); };
      return funcs;
   }

   /** @summary Prepare handle for color draw */
   prepareDraw(args) {
      if (!args)
         args = { rounding: true, extra: 0, middle: 0 };

      if (args.extra === undefined)
         args.extra = 0;
      if (args.middle === undefined)
         args.middle = 0;
      if (args.pixel_density)
         args.rounding = true;

      const histo = this.getHisto(),
            o = this.getOptions(),
            xaxis = histo.fXaxis,
            yaxis = histo.fYaxis,
            funcs = this.getHistGrFuncs(args.rounding),
            hdim = this.getDimension(),
            res = {
               i1: args.nozoom ? 0 : this.getSelectIndex('x', 'left', 0 - args.extra),
               i2: args.nozoom ? this.nbinsx : this.getSelectIndex('x', 'right', 1 + args.extra),
               j1: (hdim === 1) ? 0 : (args.nozoom ? 0 : this.getSelectIndex('y', 'left', 0 - args.extra)),
               j2: (hdim === 1) ? 1 : (args.nozoom ? this.nbinsy : this.getSelectIndex('y', 'right', 1 + args.extra)),
               min: 0, max: 0, sumz: 0, xbar1: 0, xbar2: 1, ybar1: 0, ybar2: 1,
               width: funcs?.getFrameWidth() ?? 600,
               height: funcs?.getFrameHeight() ?? 400
            };

      if (args.use3d && !funcs.size_x3d || !funcs.size_y3d)
         args.use3d = false;

      if (args.cutg) {
         // if using cutg - define rectangular region
         let i1 = res.i2, i2 = res.i1, j1 = res.j2, j2 = res.j1;
         for (let ii = res.i1; ii < res.i2; ++ii) {
            for (let jj = res.j1; jj < res.j2; ++jj) {
               if (args.cutg.IsInside(xaxis.GetBinCoord(ii + args.middle), yaxis.GetBinCoord(jj + args.middle))) {
                  i1 = Math.min(i1, ii);
                  i2 = Math.max(i2, ii + 1);
                  j1 = Math.min(j1, jj);
                  j2 = Math.max(j2, jj + 1);
               }
            }
         }

         Object.assign(res, { i1, i2, j1, j2 });
      }

      let i, j, x, y, binz, binarea;

      res.grx = res.i1 < 0 ? {} : new Float32Array(res.i2 + 1);
      res.gry = res.j1 < 0 ? {} : new Float32Array(res.j2 + 1);

      if ((typeof histo.fBarOffset === 'number') && (typeof histo.fBarWidth === 'number') && (histo.fBarOffset || (histo.fBarWidth !== 1000))) {
         if (histo.fBarOffset <= 1000)
            res.xbar1 = res.ybar1 = 0.001 * histo.fBarOffset;
         else if (histo.fBarOffset <= 3000)
            res.xbar1 = 0.001 * (histo.fBarOffset - 2000);
         else if (histo.fBarOffset <= 5000)
            res.ybar1 = 0.001 * (histo.fBarOffset - 4000);

         if (histo.fBarWidth <= 1000) {
            res.xbar2 = Math.min(1, res.xbar1 + 0.001 * histo.fBarWidth);
            res.ybar2 = Math.min(1, res.ybar1 + 0.001 * histo.fBarWidth);
         } else if (histo.fBarWidth <= 3000)
            res.xbar2 = Math.min(1, res.xbar1 + 0.001 * (histo.fBarWidth - 2000));
         else if (histo.fBarWidth <= 5000)
            res.ybar2 = Math.min(1, res.ybar1 + 0.001 * (histo.fBarWidth - 4000));
      }

      if (args.original) {
         res.original = true;
         res.origx = res.i1 < 0 ? {} : new Float32Array(res.i2 + 1);
         res.origy = res.j1 < 0 ? {} : new Float32Array(res.j2 + 1);
      }

      // calculate graphical coordinates in advance
      for (i = res.i1; i <= res.i2; ++i) {
         x = xaxis.GetBinCoord(i + args.middle);
         if (funcs.logx && (x <= 0)) {
            res.i1 = i + 1;
            continue;
         }
         if (res.origx)
            res.origx[i] = x;
         res.grx[i] = funcs.grx(x);
         if (args.rounding)
            res.grx[i] = Math.round(res.grx[i]);

         if (args.use3d) {
            if (res.grx[i] < -funcs.size_x3d) {
               res.grx[i] = -funcs.size_x3d;
               if (o.RevX)
                  res.i2 = i;
               else
                  res.i1 = i;
            }
            if (res.grx[i] > funcs.size_x3d) {
               res.grx[i] = funcs.size_x3d;
               if (o.RevX)
                  res.i1 = i;
               else
                  res.i2 = i;
            }
         }
      }

      if (hdim === 1) {
         res.gry[0] = funcs.gry(0);
         res.gry[1] = funcs.gry(1);
      } else {
         for (j = res.j1; j <= res.j2; ++j) {
            y = yaxis.GetBinCoord(j + args.middle);
            if (funcs.logy && (y <= 0)) {
               res.j1 = j + 1;
               continue;
            }
            if (res.origy)
               res.origy[j] = y;
            res.gry[j] = funcs.gry(y);
            if (args.rounding)
               res.gry[j] = Math.round(res.gry[j]);

            if (args.use3d) {
               if (res.gry[j] < -funcs.size_y3d) {
                  res.gry[j] = -funcs.size_y3d;
                  if (o.RevY)
                     res.j2 = j;
                  else
                     res.j1 = j;
               }
               if (res.gry[j] > funcs.size_y3d) {
                  res.gry[j] = funcs.size_y3d;
                  if (o.RevY)
                     res.j1 = j;
                  else
                     res.j2 = j;
               }
            }
         }
      }

      //  find min/max values in selected range
      let is_first = true;
      this.minposbin = 0;

      for (i = res.i1; i < res.i2; ++i) {
         for (j = res.j1; j < res.j2; ++j) {
            binz = histo.getBinContent(i + 1, j + 1);
            res.sumz += binz;
            if (args.pixel_density) {
               binarea = (res.grx[i + 1] - res.grx[i]) * (res.gry[j] - res.gry[j + 1]);
               if (binarea <= 0)
                  continue;
               res.max = Math.max(res.max, binz);
               if ((binz > 0) && ((binz < res.min) || (res.min === 0)))
                  res.min = binz;
               binz /= binarea;
            }
            if (is_first) {
               this.maxbin = this.minbin = binz;
               is_first = false;
            } else {
               this.maxbin = Math.max(this.maxbin, binz);
               this.minbin = Math.min(this.minbin, binz);
            }
            if ((binz > 0) && ((this.minposbin === 0) || (binz < this.minposbin)))
               this.minposbin = binz;
         }
      }

      if (is_first)
         this.maxbin = this.minbin = 0;

      // force recalculation of z levels
      this.resetContour();

      if (args.zrange)
         Object.assign(res, this.#getContourRanges(this.getMainPainter(), this.getFramePainter()));

      return res;
   }

   /** @summary Get tip text for axis bin */
   getAxisBinTip(name, axis, bin) {
      const funcs = this.getHistGrFuncs(),
            handle = funcs[`${name}_handle`],
            x1 = axis.GetBinLowEdge(bin + 1);

      if (handle.kind === kAxisLabels)
         return funcs.axisAsText(name, x1);

      const x2 = axis.GetBinLowEdge(bin + 2);

      if ((handle.kind === kAxisTime) || this.isTF1())
         return funcs.axisAsText(name, (x1 + x2) / 2);

      return `[${funcs.axisAsText(name, x1)}, ${funcs.axisAsText(name, x2)})`;
   }

   /** @summary Internal method to extract up/down errors for the bin
    * @private */
   getBinErrors(histo, bin, content) {
      const err = histo.getBinError(bin),
            res = { low: err, up: err },
            kind = this.getOptions().Poisson || histo.fBinStatErrOpt;

      if (!kind || (histo.fSumw2.fN && histo.fTsumw !== histo.fTsumw2) || (content < 0))
         return res;

      const alpha = (kind === kPoisson2) ? 0.05 : 1 - 0.682689492,
            n = Math.round(content);

      res.poisson = true; // indicate poisson error
      res.low = (n === 0) ? 0 : content - gamma_quantile(alpha / 2, n, 1);
      res.up = gamma_quantile_c(alpha / 2, n + 1, 1) - content;
      return res;
   }

   /** @summary Check assign as main painter
     * @private */
   _checkAssign() {
      const has_main = this.getPadPainter()?.getMainPainter();
      if (this.getOptions().Same)
         this.#ignore_frame = !has_main;
      else if (!has_main)
         this.setAsMainPainter();
   }

   /** @summary Return true when drawn normally on the frame
     * @private */
   isUseFrame() { return !this.#ignore_frame; }

   /** @summary generic draw function for histograms
     * @private */
   static async _drawHist(painter, opt) {
      const need_frame = !isStr(opt) || (opt.toLowerCase().indexOf('same') < 0);
      return ensureTCanvas(painter, need_frame).then(() => {
         painter.decodeOptions(opt);

         painter._checkAssign();

         if (painter.isTH2Poly()) {
            if (painter.options.Mode3D)
               painter.options.Lego = 12; // lego always 12
         }

         painter.checkPadRange();

         painter.scanContent();

         painter.createStat(); // only when required

         return painter.callDrawFunc();
      }).then(() => {
         return painter.drawFunctions();
      }).then(() => {
         return painter.drawHistTitle();
      }).then(() => {
         if (!painter.Mode3D && painter.options.AutoZoom)
            return painter.autoZoom();
      }).then(() => {
         if (painter.options.Project && !painter.mode3d && isFunc(painter.toggleProjection))
            return painter.toggleProjection(painter.options.Project);
      }).then(() => {
         painter.fillToolbar();
         return painter;
      });
   }

} // class THistPainter

/** @summary Build histogram contour lines
  * @private */
function buildHist2dContour(histo, handle, levels, palette, contour_func) {
   const kMAXCONTOUR = 2004,
         kMAXCOUNT = 2000,
         // arguments used in the PaintContourLine
         xarr = new Float32Array(2 * kMAXCONTOUR),
         yarr = new Float32Array(2 * kMAXCONTOUR),
         itarr = new Int32Array(2 * kMAXCONTOUR),
         nlevels = levels.length,
         first_level = levels[0], last_level = levels[nlevels - 1],
         polys = [],
         x = [0, 0, 0, 0], y = [0, 0, 0, 0], zc = [0, 0, 0, 0], ir = [0, 0, 0, 0],
         arrx = handle.grx,
         arry = handle.gry;

   let lj = 0;

   const LinearSearch = zvalue => {
      if (zvalue >= last_level)
         return nlevels - 1;

      for (let kk = 0; kk < nlevels; ++kk) {
         if (zvalue < levels[kk])
            return kk - 1;
      }
      return nlevels - 1;
   }, BinarySearch = zvalue => {
      if (zvalue < first_level)
         return -1;
      if (zvalue >= last_level)
         return nlevels - 1;

      let l = 0, r = nlevels - 1, m;
      while (r - l > 1) {
         m = Math.round((r + l) / 2);
         if (zvalue < levels[m])
            r = m;
         else
            l = m;
      }
      return l;
   }, LevelSearch = nlevels < 10 ? LinearSearch : BinarySearch;
   /* eslint-disable-next-line one-var */
   const PaintContourLine = (elev1, icont1, x1, y1, elev2, icont2, x2, y2) => {
      /* Double_t *xarr, Double_t *yarr, Int_t *itarr, Double_t *levels */
      const vert = (x1 === x2),
            tlen = vert ? (y2 - y1) : (x2 - x1),
            tdif = elev2 - elev1;
      let n = icont1 + 1, ii = lj - 1, icount = 0,
          xlen, pdif, diff, elev;
      const maxii = ii + kMAXCONTOUR / 2 - 3;

      while (n <= icont2 && ii <= maxii) {
         // elev = fH->GetContourLevel(n);
         elev = levels[n];
         diff = elev - elev1;
         pdif = diff / tdif;
         xlen = tlen * pdif;
         if (vert) {
            xarr[ii] = x1;
            yarr[ii] = y1 + xlen;
         } else {
            xarr[ii] = x1 + xlen;
            yarr[ii] = y1;
         }
         itarr[ii] = n;
         icount++;
         ii += 2;
         n++;
      }
      return icount;
   };

   let ipoly, poly, npmax = 0,
       i, j, k, m, n, ljfill, count,
       xsave, ysave, itars, ix, jx;

   for (j = handle.j1; j < handle.j2 - 1; ++j) {
      y[1] = y[0] = (arry[j] + arry[j + 1]) / 2;
      y[3] = y[2] = (arry[j + 1] + arry[j + 2]) / 2;

      for (i = handle.i1; i < handle.i2 - 1; ++i) {
         zc[0] = histo.getBinContent(i + 1, j + 1);
         zc[1] = histo.getBinContent(i + 2, j + 1);
         zc[2] = histo.getBinContent(i + 2, j + 2);
         zc[3] = histo.getBinContent(i + 1, j + 2);

         for (k = 0; k < 4; k++)
            ir[k] = LevelSearch(zc[k]);

         if ((ir[0] !== ir[1]) || (ir[1] !== ir[2]) || (ir[2] !== ir[3]) || (ir[3] !== ir[0])) {
            x[3] = x[0] = (arrx[i] + arrx[i + 1]) / 2;
            x[2] = x[1] = (arrx[i + 1] + arrx[i + 2]) / 2;

            n = zc[0] <= zc[1] ? 0 : 1;
            m = zc[2] <= zc[3] ? 2 : 3;
            if (zc[n] > zc[m])
               n = m;
            n++;
            lj = 1;
            for (ix = 1; ix <= 4; ix++) {
               m = n % 4 + 1;
               ljfill = PaintContourLine(zc[n - 1], ir[n - 1], x[n - 1], y[n - 1], zc[m - 1], ir[m - 1], x[m - 1], y[m - 1]);
               lj += 2 * ljfill;
               n = m;
            }

            n = zc[0] <= zc[1] ? 0 : 1;
            m = zc[2] <= zc[3] ? 2 : 3;
            if (zc[n] > zc[m])
               n = m;
            n++;
            lj = 2;
            for (ix = 1; ix <= 4; ix++) {
               m = (n === 1) ? 4 : n - 1;
               ljfill = PaintContourLine(zc[n - 1], ir[n - 1], x[n - 1], y[n - 1], zc[m - 1], ir[m - 1], x[m - 1], y[m - 1]);
               lj += 2 * ljfill;
               n = m;
            }
            //     Re-order endpoints

            count = 0;
            for (ix = 1; ix <= lj - 5; ix += 2) {
               // count = 0;
               while (itarr[ix - 1] !== itarr[ix]) {
                  xsave = xarr[ix];
                  ysave = yarr[ix];
                  itars = itarr[ix];
                  for (jx = ix; jx <= lj - 5; jx += 2) {
                     xarr[jx] = xarr[jx + 2];
                     yarr[jx] = yarr[jx + 2];
                     itarr[jx] = itarr[jx + 2];
                  }
                  xarr[lj - 3] = xsave;
                  yarr[lj - 3] = ysave;
                  itarr[lj - 3] = itars;
                  if (count > kMAXCOUNT)
                     break;
                  count++;
               }
            }

            if (count > 100)
               continue;

            for (ix = 1; ix <= lj - 2; ix += 2) {
               ipoly = itarr[ix - 1];

               if ((ipoly >= 0) && (ipoly < levels.length)) {
                  poly = polys[ipoly];
                  if (!poly)
                     poly = polys[ipoly] = createTPolyLine(kMAXCONTOUR * 4, true);

                  const np = poly.fLastPoint;
                  if (np < poly.fN - 2) {
                     poly.fX[np + 1] = Math.round(xarr[ix - 1]);
                     poly.fY[np + 1] = Math.round(yarr[ix - 1]);
                     poly.fX[np + 2] = Math.round(xarr[ix]);
                     poly.fY[np + 2] = Math.round(yarr[ix]);
                     poly.fLastPoint = np + 2;
                     npmax = Math.max(npmax, poly.fLastPoint + 1);
                  }
               }
            }
         } // end of if (ir[0]
      } // end of j
   } // end of i

   const polysort = new Int32Array(levels.length);
   let first = 0;
   // find first positive contour
   for (ipoly = 0; ipoly < levels.length; ipoly++) {
      if (levels[ipoly] >= 0) {
         first = ipoly;
         break;
      }
   }

   // store negative contours from 0 to minimum, then all positive contours
   k = 0;
   for (ipoly = first - 1; ipoly >= 0; ipoly--)
      polysort[k++] = ipoly;
   for (ipoly = first; ipoly < levels.length; ipoly++)
      polysort[k++] = ipoly;

   const xp = new Float32Array(2 * npmax),
         yp = new Float32Array(2 * npmax),
         has_func = isFunc(palette.calcColorIndex); // rcanvas for v7

   for (k = 0; k < levels.length; ++k) {
      ipoly = polysort[k];
      poly = polys[ipoly];
      if (!poly)
         continue;

      const colindx = has_func ? palette.calcColorIndex(ipoly, levels.length) : ipoly,
            xx = poly.fX, yy = poly.fY, np2 = poly.fLastPoint + 1,
            xmin = 0, ymin = 0;
      let istart = 0, iminus, iplus, nadd;

      while (true) {
         iminus = npmax;
         iplus = iminus + 1;
         xp[iminus] = xx[istart];
         yp[iminus] = yy[istart];
         xp[iplus] = xx[istart + 1];
         yp[iplus] = yy[istart + 1];
         xx[istart] = xx[istart + 1] = xmin;
         yy[istart] = yy[istart + 1] = ymin;
         while (true) {
            nadd = 0;
            for (i = 2; i < np2; i += 2) {
               if ((iplus < 2 * npmax - 1) && (xx[i] === xp[iplus]) && (yy[i] === yp[iplus])) {
                  iplus++;
                  xp[iplus] = xx[i + 1];
                  yp[iplus] = yy[i + 1];
                  xx[i] = xx[i + 1] = xmin;
                  yy[i] = yy[i + 1] = ymin;
                  nadd++;
               }
               if ((iminus > 0) && (xx[i + 1] === xp[iminus]) && (yy[i + 1] === yp[iminus])) {
                  iminus--;
                  xp[iminus] = xx[i];
                  yp[iminus] = yy[i];
                  xx[i] = xx[i + 1] = xmin;
                  yy[i] = yy[i + 1] = ymin;
                  nadd++;
               }
            }
            if (nadd === 0)
               break;
         }

         if ((iminus + 1 < iplus) && (iminus >= 0))
            contour_func(colindx, xp, yp, iminus, iplus, ipoly);

         istart = 0;
         for (i = 2; i < np2; i += 2) {
            if (xx[i] !== xmin && yy[i] !== ymin) {
               istart = i;
               break;
            }
         }

         if (istart === 0)
            break;
      }
   }
}

/** @summary Handle 3D triangles with color levels */

class Triangles3DHandler {

   constructor(ilevels, grz, grz_min, grz_max, dolines, donormals, dogrid) {
      let levels = [grz_min, grz_max]; // just cut top/bottom parts

      if (ilevels) {
         // recalculate levels into graphical coordinates
         levels = new Float32Array(ilevels.length);
         for (let ll = 0; ll < ilevels.length; ++ll)
            levels[ll] = grz(ilevels[ll]);
      }

      Object.assign(this, { grz_min, grz_max, dolines, donormals, dogrid });

      this.loop = 0;

      const nfaces = [], posbuf = [], posbufindx = [],    // buffers for faces
            pntbuf = new Float32Array(6 * 3), // maximal 6 points
            gridpnts = new Float32Array(2 * 3),
            levels_eps = (levels.at(-1) - levels.at(0)) / levels.length / 1e2;
      let nsegments = 0, lpos = null, lindx = 0,  // buffer for lines
          ngridsegments = 0, grid = null, gindx = 0, // buffer for grid lines segments
          normindx = [],                             // buffer to remember place of vertex for each bin
          pntindx = 0, lastpart = 0, gridcnt = 0;

      function checkSide(z, level1, level2, eps) {
         return (z < level1 - eps) ? -1 : (z > level2 + eps ? 1 : 0);
      }

      this.createNormIndex = function(handle) {
         // for each bin maximal 8 points reserved
         if (handle.donormals)
            normindx = new Int32Array((handle.i2 - handle.i1) * (handle.j2 - handle.j1) * 8).fill(-1);
      };

      this.createBuffers = function() {
         if (!this.loop)
            return;

         for (let lvl = 1; lvl < levels.length; ++lvl) {
            if (nfaces[lvl]) {
               posbuf[lvl] = new Float32Array(nfaces[lvl] * 9);
               posbufindx[lvl] = 0;
            }
         }
         if (this.dolines && (nsegments > 0))
            lpos = new Float32Array(nsegments * 6);
         if (this.dogrid && (ngridsegments > 0))
            grid = new Float32Array(ngridsegments * 6);
      };

      this.addLineSegment = function(x1, y1, z1, x2, y2, z2) {
         if (!this.dolines)
            return;
         const side1 = checkSide(z1, this.grz_min, this.grz_max, 0),
               side2 = checkSide(z2, this.grz_min, this.grz_max, 0);
         if ((side1 === side2) && side1)
            return;
         if (!this.loop)
            return ++nsegments;

         if (side1) {
            const diff = z2 - z1;
            z1 = (side1 < 0) ? this.grz_min : this.grz_max;
            x1 = x2 - (x2 - x1) / diff * (z2 - z1);
            y1 = y2 - (y2 - y1) / diff * (z2 - z1);
         }
         if (side2) {
            const diff = z1 - z2;
            z2 = (side2 < 0) ? this.grz_min : this.grz_max;
            x2 = x1 - (x1 - x2) / diff * (z1 - z2);
            y2 = y1 - (y1 - y2) / diff * (z1 - z2);
         }

         lpos[lindx] = x1;
         lpos[lindx + 1] = y1;
         lpos[lindx + 2] = z1;
         lindx += 3;
         lpos[lindx] = x2;
         lpos[lindx + 1] = y2;
         lpos[lindx + 2] = z2;
         lindx += 3;
      };

      function addCrossingPoint(xx1, yy1, zz1, xx2, yy2, zz2, crossz, with_grid) {
         if (pntindx >= pntbuf.length)
            console.log('more than 6 points???');

         const part = (crossz - zz1) / (zz2 - zz1);
         let shift = 3;
         if (lastpart && (Math.abs(part) < Math.abs(lastpart))) {
            // while second crossing point closer than first to original, move it in memory
            pntbuf[pntindx] = pntbuf[pntindx - 3];
            pntbuf[pntindx + 1] = pntbuf[pntindx - 2];
            pntbuf[pntindx + 2] = pntbuf[pntindx - 1];
            pntindx -= 3;
            shift = 6;
         }

         pntbuf[pntindx] = xx1 + part * (xx2 - xx1);
         pntbuf[pntindx + 1] = yy1 + part * (yy2 - yy1);
         pntbuf[pntindx + 2] = crossz;

         if (with_grid && grid) {
            gridpnts[gridcnt] = pntbuf[pntindx];
            gridpnts[gridcnt + 1] = pntbuf[pntindx + 1];
            gridpnts[gridcnt + 2] = pntbuf[pntindx + 2];
            gridcnt += 3;
         }

         pntindx += shift;
         lastpart = part;
      }

      function rememberVertex(indx, handle, ii, jj) {
         const bin = ((ii - handle.i1) * (handle.j2 - handle.j1) + (jj - handle.j1)) * 8;

         if (normindx[bin] >= 0)
            return console.error('More than 8 vertexes for the bin');

         const pos = bin + 8 + normindx[bin]; // position where write index
         normindx[bin]--;
         normindx[pos] = indx; // at this moment index can be overwritten, means all 8 position are there
      }

      this.addMainTriangle = function(x1, y1, z1, x2, y2, z2, x3, y3, z3, is_first, handle, i, j) {
         for (let lvl = 1; lvl < levels.length; ++lvl) {
            let side1 = checkSide(z1, levels[lvl - 1], levels[lvl], levels_eps),
                side2 = checkSide(z2, levels[lvl - 1], levels[lvl], levels_eps),
                side3 = checkSide(z3, levels[lvl - 1], levels[lvl], levels_eps),
                side_sum = side1 + side2 + side3;

            // always show top segments
            if ((lvl > 1) && (lvl === levels.length - 1) && (side_sum === 3) && (z1 <= this.grz_max))
               side1 = side2 = side3 = side_sum = 0;


            if (side_sum === 3)
               continue;
            if (side_sum === -3)
               return;

            if (!this.loop) {
               let npnts = Math.abs(side2 - side1) + Math.abs(side3 - side2) + Math.abs(side1 - side3);
               if (side1 === 0)
                  ++npnts;
               if (side2 === 0)
                  ++npnts;
               if (side3 === 0)
                  ++npnts;

               if ((npnts === 1) || (npnts === 2))
                  console.error(`FOUND npnts = ${npnts}`);

               if (npnts > 2) {
                  if (nfaces[lvl] === undefined)
                     nfaces[lvl] = 0;
                  nfaces[lvl] += npnts - 2;
               }

               // check if any(contours for given level exists
               if (((side1 > 0) || (side2 > 0) || (side3 > 0)) &&
                   ((side1 !== side2) || (side2 !== side3) || (side3 !== side1)))
                  ++ngridsegments;

               continue;
            }

            gridcnt = 0;

            pntindx = 0;
            if (side1 === 0) {
               pntbuf[pntindx] = x1;
               pntbuf[pntindx + 1] = y1;
               pntbuf[pntindx + 2] = z1;
               pntindx += 3;
            }

            if (side1 !== side2) {
               // order is important, should move from 1->2 point, checked via lastpart
               lastpart = 0;
               if ((side1 < 0) || (side2 < 0))
                  addCrossingPoint(x1, y1, z1, x2, y2, z2, levels[lvl - 1]);
               if ((side1 > 0) || (side2 > 0))
                  addCrossingPoint(x1, y1, z1, x2, y2, z2, levels[lvl], true);
            }

            if (side2 === 0) {
               pntbuf[pntindx] = x2;
               pntbuf[pntindx + 1] = y2;
               pntbuf[pntindx + 2] = z2;
               pntindx += 3;
            }

            if (side2 !== side3) {
               // order is important, should move from 2->3 point, checked via lastpart
               lastpart = 0;
               if ((side2 < 0) || (side3 < 0))
                  addCrossingPoint(x2, y2, z2, x3, y3, z3, levels[lvl - 1]);
               if ((side2 > 0) || (side3 > 0))
                  addCrossingPoint(x2, y2, z2, x3, y3, z3, levels[lvl], true);
            }

            if (side3 === 0) {
               pntbuf[pntindx] = x3;
               pntbuf[pntindx + 1] = y3;
               pntbuf[pntindx + 2] = z3;
               pntindx += 3;
            }

            if (side3 !== side1) {
               // order is important, should move from 3->1 point, checked via lastpart
               lastpart = 0;
               if ((side3 < 0) || (side1 < 0))
                  addCrossingPoint(x3, y3, z3, x1, y1, z1, levels[lvl - 1]);
               if ((side3 > 0) || (side1 > 0))
                  addCrossingPoint(x3, y3, z3, x1, y1, z1, levels[lvl], true);
            }

            if (pntindx === 0)
               continue;
            if (pntindx < 9) {
               console.log(`found ${pntindx / 3} points, must be at least 3`);
               continue;
            }

            if (grid && (gridcnt === 6)) {
               for (let jj = 0; jj < 6; ++jj)
                  grid[gindx + jj] = gridpnts[jj];
               gindx += 6;
            }

            // if three points and surf === 14, remember vertex for each point

            const buf = posbuf[lvl];
            let s = posbufindx[lvl];
            if (this.donormals && (pntindx === 9)) {
               rememberVertex(s, handle, i, j);
               rememberVertex(s + 3, handle, i + 1, is_first ? j + 1 : j);
               rememberVertex(s + 6, handle, is_first ? i : i + 1, j + 1);
            }

            for (let k1 = 3; k1 < pntindx - 3; k1 += 3) {
               buf[s] = pntbuf[0];
               buf[s + 1] = pntbuf[1];
               buf[s + 2] = pntbuf[2];
               s += 3;
               buf[s] = pntbuf[k1];
               buf[s + 1] = pntbuf[k1 + 1];
               buf[s + 2] = pntbuf[k1 + 2];
               s += 3;
               buf[s] = pntbuf[k1 + 3];
               buf[s + 1] = pntbuf[k1 + 4];
               buf[s + 2] = pntbuf[k1 + 5];
               s += 3;
            }
            posbufindx[lvl] = s;
         }
      };

      this.callFuncs = function(meshFunc, linesFunc) {
         for (let lvl = 1; lvl < levels.length; ++lvl) {
            if (posbuf[lvl] && meshFunc)
               meshFunc(lvl, posbuf[lvl], normindx);
         }

         if (lpos && linesFunc) {
            if (nsegments * 6 !== lindx)
               console.error(`SURF lines mismmatch nsegm=${nsegments} lindx=${lindx} diff=${nsegments * 6 - lindx}`);
            linesFunc(false, lpos);
         }

         if (grid && linesFunc) {
            if (ngridsegments * 6 !== gindx)
               console.error(`SURF grid draw mismatch ngridsegm=${ngridsegments} gindx=${gindx} diff=${ngridsegments * 6 - gindx}`);
            linesFunc(true, grid);
         }
      };
   }

} // class Triangles3DHandler


/** @summary Build 3d surface
  * @desc Make it independent from three.js to be able reuse it for 2D case
  * @private */
function buildSurf3D(histo, handle, ilevels, meshFunc, linesFunc) {
   const main_grz = handle.grz,
         arrx = handle.original ? handle.origx : handle.grx,
         arry = handle.original ? handle.origy : handle.gry,
         triangles = new Triangles3DHandler(ilevels, handle.grz, handle.grz_min, handle.grz_max, handle.dolines, handle.donormals, handle.dogrid);
   let i, j, x1, x2, y1, y2, z11, z12, z21, z22;

   triangles.createNormIndex(handle);

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

      for (i = handle.i1; i < handle.i2 - 1; ++i) {
         x1 = handle.original ? 0.5 * (arrx[i] + arrx[i + 1]) : arrx[i];
         x2 = handle.original ? 0.5 * (arrx[i + 1] + arrx[i + 2]) : arrx[i + 1];
         for (j = handle.j1; j < handle.j2 - 1; ++j) {
            y1 = handle.original ? 0.5 * (arry[j] + arry[j + 1]) : arry[j];
            y2 = handle.original ? 0.5 * (arry[j + 1] + arry[j + 2]) : arry[j + 1];

            z11 = main_grz(histo.getBinContent(i + 1, j + 1));
            z12 = main_grz(histo.getBinContent(i + 1, j + 2));
            z21 = main_grz(histo.getBinContent(i + 2, j + 1));
            z22 = main_grz(histo.getBinContent(i + 2, j + 2));

            triangles.addMainTriangle(x1, y1, z11, x2, y2, z22, x1, y2, z12, true, handle, i, j);

            triangles.addMainTriangle(x1, y1, z11, x2, y1, z21, x2, y2, z22, false, handle, i, j);

            triangles.addLineSegment(x1, y2, z12, x1, y1, z11);
            triangles.addLineSegment(x1, y1, z11, x2, y1, z21);

            if (i === handle.i2 - 2)
               triangles.addLineSegment(x2, y1, z21, x2, y2, z22);
            if (j === handle.j2 - 2)
               triangles.addLineSegment(x1, y2, z12, x2, y2, z22);
         }
      }
   }

   triangles.callFuncs(meshFunc, linesFunc);
}


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

let TH2Painter$2 = class TH2Painter extends THistPainter {

   #projection_kind;  // kind of enabled histogram projection
   #projection_widthX; // X width of projection
   #projection_widthY; // Y width of projection
   #can_move_colz; // temporary flag for readjust palette positions
   #hide_frame; // hide frame when drawing
   #chord; // zooming for chord drawing

   /** @summary Use in frame painter to check zoom Y is allowed
    * @protected */
   get _wheel_zoomy() { return true; }

   /** @summary cleanup painter */
   cleanup() {
      delete this.tt_handle;
      this.#chord = undefined;
      super.cleanup();
   }

   /** @summary Returns histogram
    * @desc Also assigns custom getBinContent method for TProfile2D if PROJXY options specified */
   getHisto() {
      const histo = super.getHisto();
      if (histo?._typename === clTProfile2D) {
         if (!histo.$getBinContent)
            histo.$getBinContent = histo.getBinContent;
         switch (this.getOptions().Profile2DProj) {
            case 'B':
               histo.getBinContent = histo.getBinEntries;
               break;
            case 'C=E':
               histo.getBinContent = function(i, j) { return this.getBinError(this.getBin(i, j)); };
               break;
            case 'W':
               histo.getBinContent = function(i, j) { return this.$getBinContent(i, j) * this.getBinEntries(i, j); };
               break;
            default:
               histo.getBinContent = histo.$getBinContent;
               break;
         }
      }
      return histo;
   }

   /** @summary Returns if projection is used */
   isProjection() { return this.#projection_kind; }

   /** @summary Toggle projection */
   toggleProjection(kind, width) {
      if ((kind === 'Projections') || (kind === 'Off'))
         kind = '';

      const parseWidth = arg => {
         if ((arg === 'all') || (arg === 'ALL'))
            return 10000;
         const res = parseInt(arg);
         return res && Number.isInteger(res) ? res : 1;
      };

      let widthX = width, widthY = width;

      if (isStr(kind) && (kind.indexOf('XY') === 0)) {
         const ws = (kind.length > 2) ? kind.slice(2) : '';
         kind = 'XY';
         widthX = widthY = parseInt(ws) || 1;
      } else if (isStr(kind) && (kind.length > 1)) {
         const ps = kind.indexOf('_');
         if ((ps > 0) && (kind[0] === 'X') && (kind[ps + 1] === 'Y')) {
            widthX = parseWidth(kind.slice(1, ps));
            widthY = parseWidth(kind.slice(ps + 2));
            kind = 'XY';
         } else if ((ps > 0) && (kind[0] === 'Y') && (kind[ps + 1] === 'X')) {
            widthY = parseWidth(kind.slice(1, ps));
            widthX = parseWidth(kind.slice(ps + 2));
            kind = 'XY';
         } else {
            widthX = widthY = parseWidth(kind.slice(1));
            kind = kind[0];
         }
      }

      if (!widthX && !widthY)
         widthX = widthY = 1;

      if (kind && (this.#projection_kind === kind)) {
         if ((this.#projection_widthX === widthX) && (this.#projection_widthY === widthY))
            kind = '';
         else {
            this.#projection_widthX = widthX;
            this.#projection_widthY = widthY;
            return;
         }
      }

      delete this.proj_hist;

      const new_proj = (this.#projection_kind === kind) ? '' : kind;
      this.#projection_widthX = widthX;
      this.#projection_widthY = widthY;
      this.#projection_kind = ''; // avoid projection handling until area is created

      return this.provideSpecialDrawArea(new_proj).then(() => {
         this.#projection_kind = new_proj;
         return this.redrawProjection();
      });
   }

   /** @summary Redraw projection */
   async redrawProjection(ii1, ii2, jj1, jj2) {
      if (!this.#projection_kind)
         return false;

      if (jj2 === undefined) {
         if (!this.tt_handle)
            return;
         ii1 = Math.round((this.tt_handle.i1 + this.tt_handle.i2) / 2);
         ii2 = ii1 + 1;
         jj1 = Math.round((this.tt_handle.j1 + this.tt_handle.j2) / 2);
         jj2 = jj1 + 1;
      }

      const canp = this.getCanvPainter();

      if (canp && !canp.isReadonly() && this.hasSnapId()) {
         // this is when projection should be created on the server side
         if (((this.#projection_kind === 'X') || (this.#projection_kind === 'XY')) && !canp.websocketTimeout('projX')) {
            if (canp.sendWebsocket(`EXECANDSEND:DXPROJ:${this.getSnapId()}:ProjectionX("_projx",${jj1 + 1},${jj2},"")`))
               canp.websocketTimeout('projX', 1000);
         }
         if (((this.#projection_kind === 'Y') || (this.#projection_kind === 'XY')) && !canp.websocketTimeout('projY')) {
            if (canp.sendWebsocket(`EXECANDSEND:DYPROJ:${this.getSnapId()}:ProjectionY("_projy",${ii1 + 1},${ii2},"")`))
               canp.websocketTimeout('projY', 1000);
         }
         return true;
      }

      if (this.doing_projection)
         return false;

      this.doing_projection = true;

      const histo = this.getHisto(),
            createXProject = () => {
               const p = createHistogram(clTH1D, this.nbinsx);
               Object.assign(p.fXaxis, histo.fXaxis);
               p.fName = 'xproj';
               p.fTitle = 'X projection';
               return p;
            },
            createYProject = () => {
               const p = createHistogram(clTH1D, this.nbinsy);
               Object.assign(p.fXaxis, histo.fYaxis);
               p.fName = 'yproj';
               p.fTitle = 'Y projection';
               return p;
            },
            fillProjectHist = (kind, p) => {
               let first = 0, last = -1;
               if (kind === 'X') {
                  for (let i = 0; i < this.nbinsx; ++i) {
                     let sum = 0;
                     for (let j = jj1; j < jj2; ++j)
                        sum += histo.getBinContent(i + 1, j + 1);
                     p.setBinContent(i + 1, sum);
                  }
                  p.fTitle = 'X projection ' + (jj1 + 1 === jj2 ? `bin ${jj2}` : `bins [${jj1 + 1} .. ${jj2}]`);
                  if (this.tt_handle) {
                     first = this.tt_handle.i1 + 1;
                     last = this.tt_handle.i2;
                  }
               } else {
                  for (let j = 0; j < this.nbinsy; ++j) {
                     let sum = 0;
                     for (let i = ii1; i < ii2; ++i)
                        sum += histo.getBinContent(i + 1, j + 1);
                     p.setBinContent(j + 1, sum);
                  }
                  p.fTitle = 'Y projection ' + (ii1 + 1 === ii2 ? `bin ${ii2}` : `bins [${ii1 + 1} .. ${ii2}]`);
                  if (this.tt_handle) {
                     first = this.tt_handle.j1 + 1;
                     last = this.tt_handle.j2;
                  }
               }

               if (first < last) {
                  p.fXaxis.fFirst = first;
                  p.fXaxis.fLast = last;
                  p.fXaxis.SetBit(EAxisBits.kAxisRange, (first !== 1) || (last !== p.fXaxis.fNbins));
               }

               // reset statistic before display
               p.fEntries = 0;
               p.fTsumw = 0;
            };

      if (!this.proj_hist) {
         switch (this.#projection_kind) {
            case 'X':
               this.proj_hist = createXProject();
               break;
            case 'XY':
               this.proj_hist = createXProject();
               this.proj_hist2 = createYProject();
               break;
            default:
               this.proj_hist = createYProject();
         }
      }

      if (this.#projection_kind === 'XY') {
         fillProjectHist('X', this.proj_hist);
         fillProjectHist('Y', this.proj_hist2);
         return this.drawInSpecialArea(this.proj_hist, '', 'X')
                    .then(() => this.drawInSpecialArea(this.proj_hist2, '', 'Y'))
                    .then(res => {
                       delete this.doing_projection;
                       return res;
                    });
      }

      fillProjectHist(this.#projection_kind, this.proj_hist);

      return this.drawInSpecialArea(this.proj_hist).then(res => {
         delete this.doing_projection;
         return res;
      });
   }

   /** @summary Execute TH2 menu command
     * @desc Used to catch standard menu items and provide local implementation */
   executeMenuCommand(method, args) {
      if (super.executeMenuCommand(method, args))
         return true;

      if ((method.fName === 'SetShowProjectionX') || (method.fName === 'SetShowProjectionY')) {
         this.toggleProjection(method.fName[17], args && parseInt(args) ? parseInt(args) : 1);
         return true;
      }

      if (method.fName === 'SetShowProjectionXY') {
         this.toggleProjection('X' + args.replaceAll(',', '_Y'));
         return true;
      }

      return false;
   }

   /** @summary Fill histogram context menu */
   fillHistContextMenu(menu) {
      if (!this.isTH2Poly() && this.getPadPainter()?.isCanvas()) {
         let kind = this.#projection_kind || '';
         if (kind)
            kind += this.#projection_widthX;
         if ((this.#projection_widthX !== this.#projection_widthY) && (this.#projection_kind === 'XY'))
            kind = `X${this.#projection_widthX}_Y${this.#projection_widthY}`;

         const sizes = ['1', '2', '3', '5', '10', 'all'];
         if (kind)
            sizes.unshift('');

         menu.sub('Projections', () => menu.input('Input projection kind X1 or XY2 or X3_Y4', kind, 'string').then(val => this.toggleProjection(val)));
         ['X', 'Y', 'XY'].forEach(name => {
            menu.column();
            sizes.forEach(sz => {
               const id = sz ? name + sz : 'Off';
               menu.addchk(kind === id, id, id, arg => this.toggleProjection(arg));
            });
            menu.endcolumn();
         });
         menu.endsub();
      }

      if (!this.isTH2Poly())
         menu.add('Auto zoom-in', () => this.autoZoom());

      const opts = this.getSupportedDrawOptions(),
            o = this.getOptions();

      menu.addDrawMenu('Draw with', opts, arg => {
         if (arg.indexOf(kInspect) === 0)
            return this.showInspector(arg);
         const oldProject = o.Project;
         this.decodeOptions(arg);
         if ((oldProject === o.Project) || this.mode3d)
            this.interactiveRedraw('pad', 'drawopt');
         else
            this.toggleProjection(o.Project);
      });

      if (o.Color || o.Contour || o.Hist || o.Surf || o.Lego === 12 || o.Lego === 14)
         this.fillPaletteMenu(menu, true);
   }

   /** @summary Process click on histogram-defined buttons */
   clickButton(funcname) {
      const res = super.clickButton(funcname);
      if (res)
         return res;

      if (this.isMainPainter()) {
         switch (funcname) {
            case 'ToggleColor': return this.toggleColor();
            case 'Toggle3D': return this.toggleMode3D();
         }
      }

      // all methods here should not be processed further
      return false;
   }

   /** @summary Fill pad toolbar with histogram-related functions */
   fillToolbar() {
      super.fillToolbar(true);

      const pp = this.getPadPainter(),
            o = this.getOptions();
      if (!pp)
         return;

      if (!this.isTH2Poly() && !o.Axis)
         pp.addPadButton('th2color', 'Toggle color', 'ToggleColor');
      if (!o.Axis)
         pp.addPadButton('th2colorz', 'Toggle color palette', 'ToggleColorZ');
      pp.addPadButton('th2draw3d', 'Toggle 3D mode', 'Toggle3D');
      pp.showPadButtons();
   }

   /** @summary Toggle color drawing mode */
   toggleColor() {
      const o = this.getOptions();
      if (o.Mode3D) {
         o.Mode3D = false;
         o.Color = true;
      } else {
         o.Color = !o.Color;
         o.Scat = !o.Color;
      }

      this.#can_move_colz = true; // indicate that next redraw can move Z scale

      this.copyOptionsToOthers();

      return this.interactiveRedraw('pad', 'drawopt');
   }

   /** @summary Perform automatic zoom inside non-zero region of histogram */
   autoZoom() {
      if (this.isTH2Poly())
         return; // not implemented

      const i1 = this.getSelectIndex('x', 'left', -1),
            i2 = this.getSelectIndex('x', 'right', 1),
            j1 = this.getSelectIndex('y', 'left', -1),
            j2 = this.getSelectIndex('y', 'right', 1),
            histo = this.getHisto();

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

      // first find minimum
      let min = histo.getBinContent(i1 + 1, j1 + 1);
      for (let i = i1; i < i2; ++i) {
         for (let j = j1; j < j2; ++j)
            min = Math.min(min, histo.getBinContent(i + 1, j + 1));
      }
      if (min > 0)
         return; // if all points positive, no chance for auto-scale

      let ileft = i2, iright = i1, jleft = j2, jright = j1;

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

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

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

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

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

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

   /** @summary Scan TH2 histogram content */
   scanContent(when_axis_changed) {
      // no need to re-scan histogram while result does not depend from axis selection
      if (when_axis_changed && this.nbinsx && this.nbinsy)
         return;

      const histo = this.getObject(),
            o = this.getOptions();
      let i, j;

      this.extractAxesProperties(2);

      if (this.isTH2Poly()) {
         this.gminposbin = null;
         this.gminbin = this.gmaxbin = 0;

         for (let n = 0, len = histo.fBins.arr.length; n < len; ++n) {
            const bin_content = histo.fBins.arr[n].fContent;
            if (n === 0)
               this.gminbin = this.gmaxbin = bin_content;

            if (bin_content < this.gminbin)
               this.gminbin = bin_content;
            else if (bin_content > this.gmaxbin)
               this.gmaxbin = bin_content;

            if ((bin_content > 0) && ((this.gminposbin === null) || (this.gminposbin > bin_content)))
               this.gminposbin = bin_content;
         }
      } else {
         // global min/max, used at the moment in 3D drawing
         this.gminbin = this.gmaxbin = histo.getBinContent(1, 1);
         this.gminposbin = null;
         for (i = 0; i < this.nbinsx; ++i) {
            for (j = 0; j < this.nbinsy; ++j) {
               const bin_content = histo.getBinContent(i + 1, j + 1);
               if (bin_content < this.gminbin)
                  this.gminbin = bin_content;
               else if (bin_content > this.gmaxbin)
                  this.gmaxbin = bin_content;
               if (bin_content > 0) {
                  if ((this.gminposbin === null) || (this.gminposbin > bin_content))
                     this.gminposbin = bin_content;
               }
            }
         }
      }

      // this value used for logz scale drawing
      if ((this.gminposbin === null) && (this.gmaxbin > 0))
         this.gminposbin = this.gmaxbin * 1e-4;

      let is_content = this.gmaxbin || this.gminbin;

      // for TProfile2D show empty bin if there are entries for it
      if (!is_content && (histo._typename === clTProfile2D)) {
         for (i = 0; i < this.nbinsx && !is_content; ++i) {
            for (j = 0; j < this.nbinsy; ++j) {
               if (histo.getBinEntries(i + 1, j + 1)) {
                  is_content = true;
                  break;
               }
            }
         }
      }

      if (o.Axis > 0) {
         // Paint histogram axis only
         this.draw_content = false;
      } else if (this.isTH2Poly()) {
         this.draw_content = is_content || o.Line || o.Fill || o.Mark;
         if (!this.draw_content && o.Zero) {
            this.draw_content = true;
            o.Line = 1;
         }
      } else
         this.draw_content = is_content || o.ShowEmpty;
   }

   /** @summary Provide histogram min/max used to create canvas ranges
    * @private */
   getUserRanges() {
      const histo = this.getHisto();
      return { minx: histo.fXaxis.fXmin, maxx: histo.fXaxis.fXmax, miny: histo.fYaxis.fXmin, maxy: histo.fYaxis.fXmax };
   }

   /** @summary Count TH2 histogram statistic
     * @desc Optionally one could provide condition function to select special range */
   countStat(cond, count_skew) {
      const histo = this.getHisto(), o = this.getOptions(),
            xaxis = histo.fXaxis, yaxis = histo.fYaxis,
            funcs = this.getHistGrFuncs(),
            res = {
               name: histo.fName, entries: 0, eff_entries: 0, integral: 0,
               meanx: 0, meany: 0, rmsx: 0, rmsy: 0, matrix: [0, 0, 0, 0, 0, 0, 0, 0, 0],
               xmax: 0, ymax: 0, wmax: null, skewx: 0, skewy: 0, skewd: 0, kurtx: 0, kurty: 0, kurtd: 0
            },
            has_counted_stat = !funcs.isAxisZoomed('x') && !funcs.isAxisZoomed('y') && (Math.abs(histo.fTsumw) > 1e-300) && !cond && !o.cutg;
      let stat_sum0 = 0, stat_sumw2 = 0, stat_sumx1 = 0, stat_sumy1 = 0,
          stat_sumx2 = 0, stat_sumy2 = 0,
          xside, yside, xx, yy, zz, xleft, xright, yleft, yright;

      if (!isFunc(cond) && o.cutg)
         cond = (x, y) => o.cutg.IsInside(x, y);

      if (this.isTH2Poly()) {
         const len = histo.fBins.arr.length;
         let i, bin, n, gr, ngr, numgraphs, numpoints;

         for (i = 0; i < len; ++i) {
            bin = histo.fBins.arr[i];

            xside = (bin.fXmin > funcs.scale_xmax) ? 2 : (bin.fXmax < funcs.scale_xmin ? 0 : 1);
            yside = (bin.fYmin > funcs.scale_ymax) ? 2 : (bin.fYmax < funcs.scale_ymin ? 0 : 1);

            xx = yy = numpoints = 0;
            gr = bin.fPoly;
            numgraphs = 1;
            if (gr._typename === clTMultiGraph) {
               numgraphs = bin.fPoly.fGraphs.arr.length;
               gr = null;
            }

            for (ngr = 0; ngr < numgraphs; ++ngr) {
               if (!gr || (ngr > 0))
                  gr = bin.fPoly.fGraphs.arr[ngr];

               for (n = 0; n < gr.fNpoints; ++n) {
                  ++numpoints;
                  xx += gr.fX[n];
                  yy += gr.fY[n];
               }
            }

            if (numpoints > 1) {
               xx /= numpoints;
               yy /= numpoints;
            }

            zz = bin.fContent;

            res.entries += zz;

            res.matrix[yside * 3 + xside] += zz;

            if ((xside !== 1) || (yside !== 1) || (cond && !cond(xx, yy)))
               continue;

            if ((res.wmax === null) || (zz > res.wmax)) {
               res.wmax = zz;
               res.xmax = xx;
               res.ymax = yy;
            }

            if (!has_counted_stat) {
               stat_sum0 += zz;
               stat_sumw2 += zz * zz;
               stat_sumx1 += xx * zz;
               stat_sumy1 += yy * zz;
               stat_sumx2 += xx * xx * zz;
               stat_sumy2 += yy * yy * zz;
            }
         }
      } else {
         xleft = this.getSelectIndex('x', 'left');
         xright = this.getSelectIndex('x', 'right');
         yleft = this.getSelectIndex('y', 'left');
         yright = this.getSelectIndex('y', 'right');

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

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

               zz = histo.getBinContent(xi, yi);

               res.entries += zz;

               res.matrix[yside * 3 + xside] += zz;

               if ((xside !== 1) || (yside !== 1) || (cond && !cond(xx, yy)))
                  continue;

               if ((res.wmax === null) || (zz > res.wmax)) {
                  res.wmax = zz;
                  res.xmax = xx;
                  res.ymax = yy;
               }

               if (!has_counted_stat) {
                  stat_sum0 += zz;
                  stat_sumw2 += zz * zz;
                  stat_sumx1 += xx * zz;
                  stat_sumy1 += yy * zz;
                  stat_sumx2 += xx ** 2 * zz;
                  stat_sumy2 += yy ** 2 * zz;
               }
            }
         }
      }

      if (has_counted_stat) {
         stat_sum0 = histo.fTsumw;
         stat_sumw2 = histo.fTsumw2;
         stat_sumx1 = histo.fTsumwx;
         stat_sumx2 = histo.fTsumwx2;
         stat_sumy1 = histo.fTsumwy;
         stat_sumy2 = histo.fTsumwy2;
      }

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

      if (res.wmax === null)
         res.wmax = 0;
      res.integral = stat_sum0;

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

      res.eff_entries = stat_sumw2 ? stat_sum0 * stat_sum0 / stat_sumw2 : Math.abs(stat_sum0);

      if (count_skew && !this.isTH2Poly()) {
         let sumx3 = 0, sumy3 = 0, sumx4 = 0, sumy4 = 0, np = 0, w;
         for (let xi = xleft; xi < xright; ++xi) {
            xx = xaxis.GetBinCoord(xi + 0.5);
            for (let yi = yleft; yi < yright; ++yi) {
               yy = yaxis.GetBinCoord(yi + 0.5);
               if (cond && !cond(xx, yy))
                  continue;
               w = histo.getBinContent(xi + 1, yi + 1);
               np += w;
               sumx3 += w * Math.pow(xx - res.meanx, 3);
               sumy3 += w * Math.pow(yy - res.meany, 3);
               sumx4 += w * Math.pow(xx - res.meanx, 4);
               sumy4 += w * Math.pow(yy - res.meany, 4);
            }
         }

         const stddev3x = Math.pow(res.rmsx, 3),
               stddev3y = Math.pow(res.rmsy, 3),
               stddev4x = Math.pow(res.rmsx, 4),
               stddev4y = Math.pow(res.rmsy, 4);
         if (np * stddev3x)
            res.skewx = sumx3 / (np * stddev3x);
         if (np * stddev3y)
            res.skewy = sumy3 / (np * stddev3y);
         res.skewd = res.eff_entries > 0 ? Math.sqrt(6 / res.eff_entries) : 0;
         if (np * stddev4x)
            res.kurtx = sumx4 / (np * stddev4x) - 3;
         if (np * stddev4y)
            res.kurty = sumy4 / (np * stddev4y) - 3;
         res.kurtd = res.eff_entries > 0 ? Math.sqrt(24 / res.eff_entries) : 0;
      }

      return res;
   }

   /** @summary Fill TH2 statistic in stat box */
   fillStatistic(stat, dostat, dofit) {
      // no need to refill statistic if histogram is dummy
      if (this.isIgnoreStatsFill())
         return false;

      if (dostat === 1)
         dostat = 1111;

      const print_name = Math.floor(dostat % 10),
            print_entries = Math.floor(dostat / 10) % 10,
            print_mean = Math.floor(dostat / 100) % 10,
            print_rms = Math.floor(dostat / 1000) % 10,
            print_under = Math.floor(dostat / 10000) % 10,
            print_over = Math.floor(dostat / 100000) % 10,
            print_integral = Math.floor(dostat / 1000000) % 10,
            print_skew = Math.floor(dostat / 10000000) % 10,
            print_kurt = Math.floor(dostat / 100000000) % 10,
            data = this.countStat(undefined, (print_skew > 0) || (print_kurt > 0));

      stat.clearPave();

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

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

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

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

      if (print_integral > 0)
         stat.addText('Integral = ' + stat.format(data.matrix[4], 'entries'));

      if (print_skew === 2) {
         stat.addText(`Skewness x = ${stat.format(data.skewx)} #pm ${stat.format(data.skewd)}`);
         stat.addText(`Skewness y = ${stat.format(data.skewy)} #pm ${stat.format(data.skewd)}`);
      } else if (print_skew > 0) {
         stat.addText(`Skewness x = ${stat.format(data.skewx)}`);
         stat.addText(`Skewness y = ${stat.format(data.skewy)}`);
      }

      if (print_kurt === 2) {
         stat.addText(`Kurtosis x = ${stat.format(data.kurtx)} #pm ${stat.format(data.kurtd)}`);
         stat.addText(`Kurtosis y = ${stat.format(data.kurty)} #pm ${stat.format(data.kurtd)}`);
      } else if (print_kurt > 0) {
         stat.addText(`Kurtosis x = ${stat.format(data.kurtx)}`);
         stat.addText(`Kurtosis y = ${stat.format(data.kurty)}`);
      }

      if ((print_under > 0) || (print_over > 0)) {
         const get = i => data.matrix[i].toFixed(0);

         stat.addText(`${get(6)} | ${get(7)} | ${get(7)}`);
         stat.addText(`${get(3)} | ${get(4)} | ${get(5)}`);
         stat.addText(`${get(0)} | ${get(1)} | ${get(2)}`);
      }

      if (dofit)
         stat.fillFunctionStat(this.findFunction(clTF2), dofit, 2);

      return true;
   }

   /** @summary Draw TH2 bins as colors */
   drawBinsColor() {
      const histo = this.getHisto(),
            o = this.getOptions(),
            handle = this.prepareDraw(),
            cntr = this.getContour(),
            palette = this.getHistPalette(),
            entries = [],
            has_sumw2 = histo.fSumw2?.length,
            show_empty = o.ShowEmpty,
            can_merge_x = (o.Color !== 7) || ((handle.xbar1 === 0) && (handle.xbar2 === 1)),
            can_merge_y = (o.Color !== 7) || ((handle.ybar1 === 0) && (handle.ybar2 === 1)),
            colindx0 = cntr.getPaletteIndex(palette, 0);

      let dx, dy, x1, y2, binz, is_zero, colindx, last_entry = null,
          skip_zero = !o.Zero, skip_bin;

      const test_cutg = o.cutg,
            flush_last_entry = () => {
               last_entry.path += `h${dx}v${last_entry.y1 - last_entry.y2}h${-dx}z`;
               last_entry = null;
            };

      // check in the beginning if zero can be skipped
      if (!skip_zero && !show_empty && (colindx0 === null))
         skip_zero = true;

      // special check for TProfile2D - empty bin with no entries shown
      if (skip_zero && (histo?._typename === clTProfile2D))
         skip_zero = 1;

      // now start build
      for (let i = handle.i1; i < handle.i2; ++i) {
         dx = (handle.grx[i + 1] - handle.grx[i]) || 1;
         if (can_merge_x)
            x1 = handle.grx[i];
         else {
            x1 = Math.round(handle.grx[i] + dx * handle.xbar1);
            dx = Math.round(dx * (handle.xbar2 - handle.xbar1)) || 1;
         }

         for (let j = handle.j2 - 1; j >= handle.j1; --j) {
            binz = histo.getBinContent(i + 1, j + 1);
            is_zero = (binz === 0) && (!has_sumw2 || histo.fSumw2[histo.getBin(i + 1, j + 1)] === 0);

            skip_bin = is_zero && ((skip_zero === 1) ? !histo.getBinEntries(i + 1, j + 1) : skip_zero);

            if (skip_bin || (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5), histo.fYaxis.GetBinCoord(j + 0.5)))) {
               if (last_entry)
                  flush_last_entry();
               continue;
            }

            colindx = cntr.getPaletteIndex(palette, binz);

            if (colindx === null) {
               if (is_zero && (show_empty || (skip_zero === 1)))
                  colindx = colindx0 || 0;
               else {
                  if (last_entry)
                     flush_last_entry();
                  continue;
               }
            }

            dy = (handle.gry[j] - handle.gry[j + 1]) || 1;
            if (can_merge_y)
               y2 = handle.gry[j + 1];
            else {
               y2 = Math.round(handle.gry[j] - dy * handle.ybar2);
               dy = Math.round(dy * (handle.ybar2 - handle.ybar1)) || 1;
            }

            const cmd1 = `M${x1},${y2}`;
            let entry = entries[colindx];
            if (!entry)
               entry = entries[colindx] = { path: cmd1 };
            else if (can_merge_y && (entry === last_entry)) {
               entry.y1 = y2 + dy;
               continue;
            } else {
               const ddx = x1 - entry.x1, ddy = y2 - entry.y2;
               if (ddx || ddy) {
                  const cmd2 = `m${ddx},${ddy}`;
                  entry.path += (cmd2.length < cmd1.length) ? cmd2 : cmd1;
               }
            }
            if (last_entry)
               flush_last_entry();

            entry.x1 = x1;
            entry.y2 = y2;

            if (can_merge_y) {
               entry.y1 = y2 + dy;
               last_entry = entry;
            } else
               entry.path += `h${dx}v${dy}h${-dx}z`;
         }
         if (last_entry)
            flush_last_entry();
      }

      entries.forEach((entry, ecolindx) => {
         if (entry)
            this.appendPath(entry.path).attr('fill', palette.getColor(ecolindx));
      });

      return handle;
   }

   /** @summary Draw TH2 bins as colors in polar coordinates */
   drawBinsPolar() {
      const histo = this.getHisto(),
            o = this.getOptions(),
            handle = this.prepareDraw(),
            cntr = this.getContour(),
            palette = this.getHistPalette(),
            entries = [],
            has_sumw2 = histo.fSumw2?.length,
            show_empty = o.ShowEmpty,
            colindx0 = cntr.getPaletteIndex(palette, 0);

      let binz, is_zero, colindx,
          skip_zero = !o.Zero, skip_bin;

      const test_cutg = o.cutg;

      // check in the beginning if zero can be skipped
      if (!skip_zero && !show_empty && (colindx0 === null))
         skip_zero = true;

      // special check for TProfile2D - empty bin with no entries shown
      if (skip_zero && (histo?._typename === clTProfile2D))
         skip_zero = 1;

      handle.getBinPath = function(i, j) {
         const a1 = 2 * Math.PI * Math.max(0, this.grx[i]) / this.width,
               a2 = 2 * Math.PI * Math.min(this.grx[i + 1], this.width) / this.width,
               r2 = Math.min(this.gry[j], this.height) / this.height,
               r1 = Math.max(0, this.gry[j + 1]) / this.height,
               side = a2 - a1 > Math.PI ? 1 : 0; // handle very large sector

         // do not process bins outside visible range
         if ((a2 <= a1) || (r2 <= r1))
            return '';

         const x0 = this.width / 2, y0 = this.height / 2,
               rx1 = r1 * this.width / 2,
               rx2 = r2 * this.width / 2,
               ry1 = r1 * this.height / 2,
               ry2 = r2 * this.height / 2,
               x11 = x0 + rx1 * Math.cos(a1),
               x12 = x0 + rx1 * Math.cos(a2),
               y11 = y0 + ry1 * Math.sin(a1),
               y12 = y0 + ry1 * Math.sin(a2),
               x21 = x0 + rx2 * Math.cos(a1),
               x22 = x0 + rx2 * Math.cos(a2),
               y21 = y0 + ry2 * Math.sin(a1),
               y22 = y0 + ry2 * Math.sin(a2);

         return `M${x11.toFixed(2)},${y11.toFixed(2)}` +
                `A${rx1.toFixed(2)},${ry1.toFixed(2)},0,${side},1,${x12.toFixed(2)},${y12.toFixed(2)}` +
                `L${x22.toFixed(2)},${y22.toFixed(2)}` +
                `A${rx2.toFixed(2)},${ry2.toFixed(2)},0,${side},0,${x21.toFixed(2)},${y21.toFixed(2)}Z`;
      };

      handle.findBin = function(x, y) {
         const x0 = this.width / 2, y0 = this.height / 2;
         let angle = Math.atan2((y - y0) / this.height, (x - x0) / this.width), i, j;
         const radius = Math.abs(Math.cos(angle)) > 0.5 ? (x - x0) / Math.cos(angle) / this.width * 2 : (y - y0) / Math.sin(angle) / this.height * 2;

         if (angle < 0)
            angle += 2 * Math.PI;

         for (i = this.i1; i < this.i2; ++i) {
            const a1 = 2 * Math.PI * this.grx[i] / this.width,
                  a2 = 2 * Math.PI * this.grx[i + 1] / this.width;
            if ((a1 <= angle) && (angle <= a2))
               break;
         }

         for (j = this.j1; j < this.j2; ++j) {
            const r2 = this.gry[j] / this.height,
                  r1 = this.gry[j + 1] / this.height;
            if ((r1 <= radius) && (radius <= r2))
               break;
         }

         return { i, j };
      };

      // now start build
      for (let i = handle.i1; i < handle.i2; ++i) {
         for (let j = handle.j2 - 1; j >= handle.j1; --j) {
            binz = histo.getBinContent(i + 1, j + 1);
            is_zero = (binz === 0) && (!has_sumw2 || histo.fSumw2[histo.getBin(i + 1, j + 1)] === 0);

            skip_bin = is_zero && ((skip_zero === 1) ? !histo.getBinEntries(i + 1, j + 1) : skip_zero);

            if (skip_bin || (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5), histo.fYaxis.GetBinCoord(j + 0.5))))
               continue;

            colindx = cntr.getPaletteIndex(palette, binz);

            if (colindx === null) {
               if (is_zero && (show_empty || (skip_zero === 1)))
                  colindx = colindx0 || 0;
               else
                  continue;
            }

            const cmd = handle.getBinPath(i, j);
            if (!cmd)
               continue;

            const entry = entries[colindx];
            if (!entry)
               entries[colindx] = { path: cmd };
            else
               entry.path += cmd;
         }
      }

      entries.forEach((entry, ecolindx) => {
         if (entry)
            this.appendPath(entry.path).attr('fill', palette.getColor(ecolindx));
      });

      return handle;
   }

   /** @summary Draw histogram bins with projection function */
   drawBinsProjected() {
      const handle = this.prepareDraw({ rounding: false, nozoom: true, extra: 100, original: true }),
            funcs = this.getHistGrFuncs(),
            ilevels = this.getContourLevels(),
            palette = this.getHistPalette(),
            func = isFunc(funcs.getProjectionFunc) ? funcs.getProjectionFunc() : (x, y) => { return { x, y }; };

      handle.grz = z => z;
      handle.grz_min = ilevels.at(0);
      handle.grz_max = ilevels.at(-1);

      buildSurf3D(this.getHisto(), handle, ilevels, (lvl, pos) => {
         let dd = '', lastx, lasty;

         for (let i = 0; i < pos.length; i += 3) {
            const pnt = func(pos[i], pos[i + 1]),
                  x = Math.round(funcs.grx(pnt.x)),
                  y = Math.round(funcs.gry(pnt.y));

            if (i === 0)
               dd = `M${x},${y}`;
            else {
               if ((x === lastx) && (y === lasty))
                  continue;
               if (i % 9 === 0)
                  dd += `m${x - lastx},${y - lasty}`;
               else if (y === lasty)
                  dd += `h${x - lastx}`;
               else if (x === lastx)
                  dd += `v${y - lasty}`;
               else
                  dd += `l${x - lastx},${y - lasty}`;
            }

            lastx = x;
            lasty = y;
         }

         this.appendPath(dd)
             .style('fill', palette.calcColor(lvl, ilevels.length));
      });

      return handle;
   }

   /** @summary Draw histogram bins as contour */
   drawBinsContour() {
      const handle = this.prepareDraw({ rounding: false, extra: 100 }),
            levels = this.getContourLevels(),
            palette = this.getHistPalette(),
            o = this.getOptions();

      /* eslint-disable-next-line one-var */
      const get_segm_intersection = (segm1, segm2) => {
         const s10_x = segm1.x2 - segm1.x1,
               s10_y = segm1.y2 - segm1.y1,
               s32_x = segm2.x2 - segm2.x1,
               s32_y = segm2.y2 - segm2.y1,
               denom = s10_x * s32_y - s32_x * s10_y;

         if (denom === 0)
            return 0; // Collinear
         const denomPositive = denom > 0,
               s02_x = segm1.x1 - segm2.x1,
               s02_y = segm1.y1 - segm2.y1,
               s_numer = s10_x * s02_y - s10_y * s02_x;
         if ((s_numer < 0) === denomPositive)
            return null; // No collision

         const t_numer = s32_x * s02_y - s32_y * s02_x;
         if ((t_numer < 0) === denomPositive)
            return null; // No collision

         if (((s_numer > denom) === denomPositive) || ((t_numer > denom) === denomPositive))
            return null; // No collision
         // Collision detected
         const t = t_numer / denom;
         return { x: Math.round(segm1.x1 + (t * s10_x)), y: Math.round(segm1.y1 + (t * s10_y)) };
      };

      /* eslint-disable-next-line one-var */
      const buildPath = (xp, yp, iminus, iplus, do_close, check_rapair) => {
         let cmd = '', lastx, lasty, x0, y0, isany = false, matched, x, y;
         for (let i = iminus; i <= iplus; ++i) {
            x = Math.round(xp[i]);
            y = Math.round(yp[i]);
            if (!cmd) {
               cmd = `M${x},${y}`;
               x0 = x;
               y0 = y;
            } else if ((i === iplus) && (iminus !== iplus) && (x === x0) && (y === y0)) {
               if (!isany)
                  return ''; // all same points
               cmd += 'z';
               do_close = false;
               matched = true;
            } else {
               const dx = x - lastx, dy = y - lasty;
               if (dx) {
                  isany = true;
                  cmd += dy ? `l${dx},${dy}` : `h${dx}`;
               } else if (dy) {
                  isany = true;
                  cmd += `v${dy}`;
               }
            }

            lastx = x;
            lasty = y;
         }

         if (!do_close || matched || !check_rapair)
            return do_close ? cmd + 'z' : cmd;

         // try to build path which fills area to outside borders
         const points = [{ x: 0, y: 0 }, { x: handle.width, y: 0 }, { x: handle.width, y: handle.height }, { x: 0, y: handle.height }];

         /* eslint-disable-next-line one-var */
         const get_intersect = (indx, di) => {
            const segm = { x1: xp[indx], y1: yp[indx], x2: 2 * xp[indx] - xp[indx + di], y2: 2 * yp[indx] - yp[indx + di] };
            for (let i = 0; i < 4; ++i) {
               const res = get_segm_intersection(segm, { x1: points[i].x, y1: points[i].y, x2: points[(i + 1) % 4].x, y2: points[(i + 1) % 4].y });
               if (res) {
                  res.indx = i + 0.5;
                  return res;
               }
            }
            return null;
         };

         let pnt1, pnt2;
         iminus--;
         while ((iminus < iplus - 1) && !pnt1)
            pnt1 = get_intersect(++iminus, 1);
         if (!pnt1)
            return '';
         iplus++;
         while ((iminus < iplus - 1) && !pnt2)
            pnt2 = get_intersect(--iplus, -1);
         if (!pnt2)
            return '';

         // TODO: now side is always same direction, could be that side should be checked more precise

         let dd = buildPath(xp, yp, iminus, iplus),
             indx = pnt2.indx;
         const side = 1, step = side * 0.5;

         dd += `L${pnt2.x},${pnt2.y}`;

         while (Math.abs(indx - pnt1.indx) > 0.1) {
            indx = Math.round(indx + step) % 4;
            dd += `L${points[indx].x},${points[indx].y}`;
            indx += step;
         }
         return dd + `L${pnt1.x},${pnt1.y}z`;
      };

      if (o.Contour === 14) {
         this.appendPath(`M0,0h${handle.width}v${handle.height}h${-handle.width}z`)
             .style('fill', palette.calcColor(0, levels.length));
      }

      buildHist2dContour(this.getHisto(), handle, levels, palette, (colindx, xp, yp, iminus, iplus, ipoly) => {
         const icol = palette.getColor(colindx);
         let fillcolor = icol, lineatt;

         switch (o.Contour) {
            case 1:
               break;
            case 11:
               fillcolor = 'none';
               lineatt = this.createAttLine({ color: icol, std: false });
               break;
            case 12:
               fillcolor = 'none';
               lineatt = this.createAttLine({ color: 1, style: (ipoly % 5 + 1), width: 1, std: false });
               break;
            case 13:
               fillcolor = 'none';
               lineatt = this.lineatt;
               break;
         }

         const dd = buildPath(xp, yp, iminus, iplus, fillcolor !== 'none', true);
         if (dd) {
            this.appendPath(dd)
                .style('fill', fillcolor)
                .call(lineatt ? lineatt.func : () => {});
         }
      });

      handle.hide_only_zeros = true; // text drawing suppress only zeros

      return handle;
   }

   getGrNPoints(gr) {
      const x = gr.fX, y = gr.fY;
      let npnts = gr.fNpoints;
      if ((npnts > 2) && (x[0] === x[npnts - 1]) && (y[0] === y[npnts - 1]))
         npnts--;
      return npnts;
   }

   /** @summary Create single graph path from TH2PolyBin */
   createPolyGr(funcs, gr, textbin) {
      let grcmd = '', acc_x = 0, acc_y = 0;

      const x = gr.fX, y = gr.fY, flush = () => {
         if (acc_x) {
            grcmd += 'h' + acc_x;
            acc_x = 0;
         }
         if (acc_y) {
            grcmd += 'v' + acc_y;
            acc_y = 0;
         }
      }, addPoint = (x1, y1, x2, y2) => {
         const len = Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
         textbin.sumx += (x1 + x2) * len / 2;
         textbin.sumy += (y1 + y2) * len / 2;
         textbin.sum += len;
      }, npnts = this.getGrNPoints(gr);

      if (npnts < 2)
         return '';

      const grx0 = Math.round(funcs.grx(x[0])),
            gry0 = Math.round(funcs.gry(y[0]));
      let grx = grx0, gry = gry0;

      for (let n = 1; n < npnts; ++n) {
         const nextx = Math.round(funcs.grx(x[n])),
               nexty = Math.round(funcs.gry(y[n])),
               dx = nextx - grx,
               dy = nexty - gry;

         if (textbin)
            addPoint(grx, gry, nextx, nexty);
         if (dx || dy) {
            if (dx === 0) {
               if ((acc_y === 0) || ((dy < 0) !== (acc_y < 0)))
                  flush();
               acc_y += dy;
            } else if (dy === 0) {
               if ((acc_x === 0) || ((dx < 0) !== (acc_x < 0)))
                  flush();
               acc_x += dx;
            } else {
               flush();
               grcmd += `l${dx},${dy}`;
            }

            grx = nextx;
            gry = nexty;
         }
      }

      if (textbin)
         addPoint(grx, gry, grx0, gry0);
      flush();

      return grcmd ? `M${grx0},${gry0}` + grcmd + 'z' : '';
   }

   /** @summary Create path for complete TH2PolyBin */
   createPolyBin(funcs, bin) {
      const arr = (bin.fPoly._typename === clTMultiGraph) ? bin.fPoly.fGraphs.arr : [bin.fPoly];
      let cmd = '';
      for (let k = 0; k < arr.length; ++k)
         cmd += this.createPolyGr(funcs, arr[k]);
      return cmd;
   }

   /** @summary draw TH2Poly bins */
   async drawPolyBins() {
      const histo = this.getHisto(),
            o = this.getOptions(),
            funcs = this.getHistGrFuncs(),
            draw_colors = o.Color || (!o.Line && !o.Fill && !o.Text && !o.Mark),
            draw_lines = o.Line || (o.Text && !draw_colors),
            draw_fill = o.Fill && !draw_colors,
            draw_mark = o.Mark,
            h = funcs.getFrameHeight(),
            textbins = [],
            len = histo.fBins.arr.length;
      let colindx, cmd,
          full_cmd = '', allmarkers_cmd = '',
          bin, item, i, gr0 = null,
          lineatt_match = draw_lines,
          fillatt_match = draw_fill,
          markatt_match = draw_mark;

      // force recalculations of contours
      // use global coordinates
      this.maxbin = this.gmaxbin;
      this.minbin = this.gminbin;
      this.minposbin = this.gminposbin;

      const cntr = draw_colors ? this.getContour(true) : null,
            palette = cntr ? this.getHistPalette() : null,
            rejectBin = bin2 => {
               // check if bin outside visible range
               return ((bin2.fXmin > funcs.scale_xmax) || (bin2.fXmax < funcs.scale_xmin) ||
                       (bin2.fYmin > funcs.scale_ymax) || (bin2.fYmax < funcs.scale_ymin));
            };

      // check if similar fill attributes
      for (i = 0; i < len; ++i) {
         bin = histo.fBins.arr[i];
         if (rejectBin(bin))
            continue;

         const arr = (bin.fPoly._typename === clTMultiGraph) ? bin.fPoly.fGraphs.arr : [bin.fPoly];
         for (let k = 0; k < arr.length; ++k) {
            const gr = arr[k];
            if (!gr0) {
               gr0 = gr;
               continue;
            }
            if (lineatt_match && ((gr0.fLineColor !== gr.fLineColor) || (gr0.fLineWidth !== gr.fLineWidth) || (gr0.fLineStyle !== gr.fLineStyle)))
               lineatt_match = false;
            if (fillatt_match && ((gr0.fFillColor !== gr.fFillColor) || (gr0.fFillStyle !== gr.fFillStyle)))
               fillatt_match = false;
            if (markatt_match && ((gr0.fMarkerColor !== gr.fMarkerColor) || (gr0.fMarkerStyle !== gr.fMarkerStyle) || (gr0.fMarkerSize !== gr.fMarkerSize)))
               markatt_match = false;
         }
         if (!lineatt_match && !fillatt_match && !markatt_match)
            break;
      }

      // do not try color draw optimization as with plain th2 while
      // bins are not rectangular and drawings artifacts are nasty
      // therefore draw each bin separately when doing color draw
      const lineatt0 = lineatt_match && gr0 ? this.createAttLine(gr0) : null,
            fillatt0 = fillatt_match && gr0 ? this.createAttFill(gr0) : null,
            markeratt0 = markatt_match && gr0 ? this.createAttMarker({ attr: gr0, style: o.MarkStyle, std: false }) : null,
            optimize_draw = !draw_colors && (draw_lines ? lineatt_match : true) && (draw_fill ? fillatt_match : true);

      // draw bins
      for (i = 0; i < len; ++i) {
         bin = histo.fBins.arr[i];
         if (rejectBin(bin))
            continue;

         const draw_bin = bin.fContent || o.Zero,
               arr = (bin.fPoly._typename === clTMultiGraph) ? bin.fPoly.fGraphs.arr : [bin.fPoly];

         colindx = draw_colors && draw_bin ? cntr.getPaletteIndex(palette, bin.fContent) : null;

         const textbin = o.Text && draw_bin ? { bin, sumx: 0, sumy: 0, sum: 0 } : null;

         for (let k = 0; k < arr.length; ++k) {
            const gr = arr[k];
            if (markeratt0) {
               const npnts = this.getGrNPoints(gr);
               for (let n = 0; n < npnts; ++n)
                  allmarkers_cmd += markeratt0.create(funcs.grx(gr.fX[n]), funcs.gry(gr.fY[n]));
            }

            cmd = this.createPolyGr(funcs, gr, textbin);
            if (!cmd)
               continue;

            if (optimize_draw)
               full_cmd += cmd;
            else if ((colindx !== null) || draw_fill || draw_lines) {
               item = this.appendPath(cmd);
               if (draw_colors && (colindx !== null))
                  item.style('fill', palette.getColor(colindx));
               else if (draw_fill)
                  item.call(this.createAttFill(gr).func);
               else
                  item.style('fill', 'none');
               if (draw_lines)
                  item.call(this.createAttLine(gr).func);
            }
         } // loop over graphs

         if (textbin?.sum)
            textbins.push(textbin);
      } // loop over bins

      if (optimize_draw) {
         item = this.appendPath(full_cmd);
         if (draw_fill && fillatt0)
            item.call(fillatt0.func);
         else
            item.style('fill', 'none');
         if (draw_lines && lineatt0)
            item.call(lineatt0.func);
      }

      if (markeratt0 && !markeratt0.empty() && allmarkers_cmd) {
         this.appendPath(allmarkers_cmd)
             .call(markeratt0.func);
      } else if (draw_mark) {
         for (i = 0; i < len; ++i) {
            bin = histo.fBins.arr[i];
            if (rejectBin(bin))
               continue;

            const arr = (bin.fPoly._typename === clTMultiGraph) ? bin.fPoly.fGraphs.arr : [bin.fPoly];

            for (let k = 0; k < arr.length; ++k) {
               const gr = arr[k], npnts = this.getGrNPoints(gr),
                     markeratt = this.createAttMarker({ attr: gr, style: o.MarkStyle, std: false });
               if (!npnts || markeratt.empty())
                  continue;

               let cmdm = '';
               for (let n = 0; n < npnts; ++n)
                  cmdm += markeratt.create(funcs.grx(gr.fX[n]), funcs.gry(gr.fY[n]));

               this.appendPath(cmdm)
                   .call(markeratt.func);
            } // loop over graphs
         } // loop over bins
      }

      let pr = Promise.resolve();

      if (textbins.length) {
         const color = this.getColor(histo.fMarkerColor),
               rotate = -1 * o.TextAngle,
               text_g = this.getG().append('svg:g').attr('class', 'th2poly_text'),
               text_size = ((histo.fMarkerSize !== 1) && rotate) ? Math.round(0.02 * h * histo.fMarkerSize) : 12;

         pr = this.startTextDrawingAsync(42, text_size, text_g, text_size).then(() => {
            for (i = 0; i < textbins.length; ++i) {
               const textbin = textbins[i];

               bin = textbin.bin;

               if (textbin.sum > 0) {
                  textbin.midx = Math.round(textbin.sumx / textbin.sum);
                  textbin.midy = Math.round(textbin.sumy / textbin.sum);
               } else {
                  textbin.midx = Math.round(funcs.grx((bin.fXmin + bin.fXmax) / 2));
                  textbin.midy = Math.round(funcs.gry((bin.fYmin + bin.fYmax) / 2));
               }

               let text;

               if (!o.TextKind)
                  text = (Math.round(bin.fContent) === bin.fContent) ? bin.fContent.toString() : floatToString(bin.fContent, gStyle.fPaintTextFormat);
               else {
                  text = bin.fPoly?.fName;
                  if (!text || (text === 'Graph'))
                     text = bin.fNumber.toString();
               }

               this.drawText({ align: 22, x: textbin.midx, y: textbin.midy, rotate, text, color, latex: 0, draw_g: text_g });
            }

            return this.finishTextDrawing(text_g, true);
         });
      }

      return pr.then(() => { return { poly: true }; });
   }

   /** @summary Draw TH2 bins as text */
   async drawBinsText(handle) {
      if (!handle)
         handle = this.prepareDraw({ rounding: false });

      const histo = this.getHisto(),
            o = this.getOptions(),
            test_cutg = o.cutg,
            color = this.getColor(histo.fMarkerColor),
            rotate = -1 * o.TextAngle,
            draw_g = this.getG().append('svg:g').attr('class', 'th2_text'),
            show_err = (o.TextKind === 'E'),
            latex = (show_err && !o.TextLine) ? 1 : 0,
            text_offset = histo.fBarOffset * 1e-3,
            text_size = ((histo.fMarkerSize === 1) || !rotate) ? 20 : Math.round(0.02 * histo.fMarkerSize * handle.height);

      return this.startTextDrawingAsync(42, text_size, draw_g, text_size).then(() => {
         for (let i = handle.i1; i < handle.i2; ++i) {
            const binw = handle.grx[i + 1] - handle.grx[i];
            for (let j = handle.j1; j < handle.j2; ++j) {
               const binz = histo.getBinContent(i + 1, j + 1);
               if ((binz === 0) && !o.ShowEmpty)
                  continue;

               if (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5),
                                                    histo.fYaxis.GetBinCoord(j + 0.5)))
                  continue;

               const binh = handle.gry[j] - handle.gry[j + 1];

               let text = (binz === Math.round(binz)) ? binz.toString() : floatToString(binz, gStyle.fPaintTextFormat);

               if (show_err) {
                  const errs = this.getBinErrors(histo, histo.getBin(i + 1, j + 1), binz);
                  if (errs.poisson) {
                     const lble = `-${floatToString(errs.low, gStyle.fPaintTextFormat)}  +${floatToString(errs.up, gStyle.fPaintTextFormat)}`;
                     if (o.TextLine)
                        text += ' ' + lble;
                     else
                        text = `#splitmline{${text}}{${lble}}`;
                  } else {
                     const lble = (errs.up === Math.round(errs.up)) ? errs.up.toString() : floatToString(errs.up, gStyle.fPaintTextFormat);
                     if (o.TextLine)
                        text += '\xB1' + lble;
                     else
                        text = `#splitmline{${text}}{#pm${lble}}`;
                  }
               }

               let x, y, width, height;

               if (rotate) {
                  x = Math.round(handle.grx[i] + binw * 0.5);
                  y = Math.round(handle.gry[j + 1] + binh * (0.5 + text_offset));
                  width = height = 0;
               } else {
                  x = Math.round(handle.grx[i] + binw * 0.1);
                  y = Math.round(handle.gry[j + 1] + binh * (0.1 + text_offset));
                  width = Math.round(binw * 0.8);
                  height = Math.round(binh * 0.8);
               }

               this.drawText({ align: 22, x, y, width, height, rotate, text, color, latex, draw_g });
            }
         }

         handle.hide_only_zeros = true; // text drawing suppress only zeros

         return this.finishTextDrawing(draw_g, true);
      }).then(() => handle);
   }

   /** @summary Draw TH2 bins as arrows */
   drawBinsArrow() {
      const histo = this.getHisto(),
            o = this.getOptions(),
            test_cutg = o.cutg,
            handle = this.prepareDraw({ rounding: false }),
            cntr = o.Color ? this.getContour() : null,
            palette = o.Color ? this.getHistPalette() : null,
            scale_x = (handle.grx[handle.i2] - handle.grx[handle.i1]) / (handle.i2 - handle.i1 + 1) / 2,
            scale_y = (handle.gry[handle.j2] - handle.gry[handle.j1]) / (handle.j2 - handle.j1 + 1) / 2,
            makeLine = (dx, dy) => { return dx ? (dy ? `l${dx},${dy}` : `h${dx}`) : (dy ? `v${dy}` : ''); },
            entries = [];
      let dn = 1e-30, dx, dy, xc, yc, plain = '',
          dxn, dyn, x1, x2, y1, y2;

      for (let loop = 0; loop < 2; ++loop) {
         for (let i = handle.i1; i < handle.i2; ++i) {
            for (let j = handle.j1; j < handle.j2; ++j) {
               if (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5),
                                                    histo.fYaxis.GetBinCoord(j + 0.5)))
                  continue;

               const bincont = histo.getBinContent(i + 1, j + 1);

               if (i === handle.i1)
                  dx = histo.getBinContent(i + 2, j + 1) - bincont;
               else if (i === handle.i2 - 1)
                  dx = bincont - histo.getBinContent(i, j + 1);
               else
                  dx = 0.5 * (histo.getBinContent(i + 2, j + 1) - histo.getBinContent(i, j + 1));

               if (j === handle.j1)
                  dy = histo.getBinContent(i + 1, j + 2) - bincont;
               else if (j === handle.j2 - 1)
                  dy = bincont - histo.getBinContent(i + 1, j);
               else
                  dy = 0.5 * (histo.getBinContent(i + 1, j + 2) - histo.getBinContent(i + 1, j));

               if (loop === 0)
                  dn = Math.max(dn, Math.abs(dx), Math.abs(dy));
               else {
                  xc = (handle.grx[i] + handle.grx[i + 1]) / 2;
                  yc = (handle.gry[j] + handle.gry[j + 1]) / 2;
                  dxn = scale_x * dx / dn;
                  dyn = scale_y * dy / dn;
                  x1 = xc - dxn;
                  x2 = xc + dxn;
                  y1 = yc - dyn;
                  y2 = yc + dyn;
                  dx = Math.round(x2 - x1);
                  dy = Math.round(y2 - y1);

                  if (dx || dy) {
                     let cmd = `M${Math.round(x1)},${Math.round(y1)}${makeLine(dx, dy)}`;

                     if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
                        const anr = Math.sqrt(9 / (dx ** 2 + dy ** 2)),
                              si = Math.round(anr * (dx + dy)),
                              co = Math.round(anr * (dx - dy));
                        if (si || co)
                           cmd += `m${-si},${co}${makeLine(si, -co)}${makeLine(-co, -si)}`;
                     }

                     if (palette && cntr) {
                        const colindx = cntr.getPaletteIndex(palette, bincont);
                        if (colindx !== null) {
                           const entry = entries[colindx];
                           if (!entry)
                              entries[colindx] = { path: cmd };
                           else
                              entry.path += cmd;
                        }
                     } else
                        plain += cmd;
                  }
               }
            }
         }
      }

      if (plain) {
         this.appendPath(plain)
             .style('fill', 'none')
             .call(this.lineatt.func);
      }

      entries.forEach((entry, colindx) => {
         if (entry) {
            const col0 = this.lineatt.color;
            this.lineatt.color = palette.getColor(colindx);
            this.appendPath(entry.path)
                .attr('fill', 'none')
                .call(this.lineatt.func);
            this.lineatt.color = col0;
         }
      });

      return handle;
   }

   /** @summary Draw TH2 bins as boxes */
   drawBinsBox() {
      const histo = this.getHisto(),
            o = this.getOptions(),
            handle = this.prepareDraw({ rounding: false, zrange: true }),
            absmax = Math.max(Math.abs(handle.zmin), Math.abs(handle.zmax)),
            absmin = Math.max(0, handle.zmin),
            pad = this.getPadPainter().getRootPad(true),
            test_cutg = o.cutg;

      let i, j, binz, absz, res = '', cross = '', btn1 = '', btn2 = '',
          zdiff, dgrx, dgry, xx, yy, ww, hh, xyfactor,
          uselogz = false, logmin = 0;

      if ((pad?.fLogv ?? pad?.fLogz) && (absmax > 0)) {
         uselogz = true;
         const logmax = Math.log(absmax);
         if (absmin > 0)
            logmin = Math.log(absmin);
         else if ((handle.zminpos >= 1) && (handle.zminpos < 100))
            logmin = Math.log(0.7);
         else
            logmin = (handle.zminpos > 0) ? Math.log(0.7 * handle.zminpos) : logmax - 10;
         if (logmin >= logmax)
            logmin = logmax - 10;
         xyfactor = 1.0 / (logmax - logmin);
      } else
         xyfactor = 1.0 / (absmax - absmin);


      // now start build
      for (i = handle.i1; i < handle.i2; ++i) {
         for (j = handle.j1; j < handle.j2; ++j) {
            binz = histo.getBinContent(i + 1, j + 1);
            absz = Math.abs(binz);
            if ((absz === 0) || (absz < absmin))
               continue;

            if (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5),
                                                 histo.fYaxis.GetBinCoord(j + 0.5)))
               continue;

            zdiff = uselogz ? ((absz > 0) ? Math.log(absz) - logmin : 0) : (absz - absmin);
            // area of the box should be proportional to absolute bin content
            zdiff = 0.5 * ((zdiff < 0) ? 1 : (1 - Math.sqrt(zdiff * xyfactor)));
            // avoid oversized bins
            if (zdiff < 0)
               zdiff = 0;

            ww = handle.grx[i + 1] - handle.grx[i];
            hh = handle.gry[j] - handle.gry[j + 1];

            dgrx = zdiff * ww;
            dgry = zdiff * hh;

            xx = Math.round(handle.grx[i] + dgrx);
            yy = Math.round(handle.gry[j + 1] + dgry);

            ww = Math.max(Math.round(ww - 2 * dgrx), 1);
            hh = Math.max(Math.round(hh - 2 * dgry), 1);

            res += `M${xx},${yy}v${hh}h${ww}v${-hh}z`;

            if ((binz < 0) && (o.BoxStyle === 10))
               cross += `M${xx},${yy}l${ww},${hh}m0,${-hh}l${-ww},${hh}`;

            if ((o.BoxStyle === 11) && (ww > 5) && (hh > 5)) {
               const arr = getBoxDecorations(xx, yy, ww, hh, binz < 0 ? -1 : 1, Math.round(ww * 0.1), Math.round(hh * 0.1));
               btn1 += arr[0];
               btn2 += arr[1];
            }
         }
      }

      if (res) {
         const elem = this.appendPath(res).call(this.fillatt.func);
         if ((o.BoxStyle !== 11) && this.fillatt.empty())
            elem.call(this.lineatt.func);
      }

      if (btn1 && this.fillatt.hasColor()) {
         this.appendPath(btn1)
             .call(this.fillatt.func)
             .style('fill', rgb(this.fillatt.color).brighter(0.5).formatRgb());
      }

      if (btn2) {
         this.appendPath(btn2)
             .call(this.fillatt.func)
             .style('fill', !this.fillatt.hasColor() ? 'red' : rgb(this.fillatt.color).darker(0.5).formatRgb());
      }

      if (cross) {
         const elem = this.appendPath(cross).style('fill', 'none');
         if (!this.lineatt.empty())
            elem.call(this.lineatt.func);
         else
            elem.style('stroke', 'black');
      }

      return handle;
   }

   /** @summary Draw histogram bins as candle plot */
   drawBinsCandle() {
      const kNoOption = 0,
            kBox = 1,
            kMedianLine = 10,
            kMedianNotched = 20,
            kMedianCircle = 30,
            kMeanLine = 100,
            kMeanCircle = 300,
            kWhiskerAll = 1000,
            kWhisker15 = 2000,
            kAnchor = 10000,
            kPointsOutliers = 100000,
            kPointsAll = 200000,
            kPointsAllScat = 300000,
            kHistoLeft = 1000000,
            kHistoRight = 2000000,
            kHistoViolin = 3000000,
            kHistoZeroIndicator = 10000000,
            kHorizontal = 100000000,
            fallbackCandle = kBox + kMedianLine + kMeanCircle + kWhiskerAll + kAnchor,
            fallbackViolin = kMeanCircle + kWhiskerAll + kHistoViolin + kHistoZeroIndicator;

      let fOption = kNoOption;

      const o = this.getOptions(), isOption = opt => {
         let mult = 1;
         while (opt >= mult)
            mult *= 10;
         mult /= 10;
         return Math.floor(fOption / mult) % 10 === Math.floor(opt / mult);
      }, parseOption = (opt, is_candle) => {
         let direction = '', preset = '', res = kNoOption;
         const c0 = opt[0], c1 = opt[1];

         if (c0 >= 'A' && c0 <= 'Z')
            direction = c0;
         if (c0 >= '1' && c0 <= '9')
            preset = c0;
         if (c1 >= 'A' && c1 <= 'Z' && preset)
            direction = c1;
         if (c1 >= '1' && c1 <= '9' && direction)
            preset = c1;

         if (is_candle) {
            switch (preset) {
               case '1': res += fallbackCandle; break;
               case '2': res += kBox + kMeanLine + kMedianLine + kWhisker15 + kAnchor + kPointsOutliers; break;
               case '3': res += kBox + kMeanCircle + kMedianLine + kWhisker15 + kAnchor + kPointsOutliers; break;
               case '4': res += kBox + kMeanCircle + kMedianNotched + kWhisker15 + kAnchor + kPointsOutliers; break;
               case '5': res += kBox + kMeanLine + kMedianLine + kWhisker15 + kAnchor + kPointsAll; break;
               case '6': res += kBox + kMeanCircle + kMedianLine + kWhisker15 + kAnchor + kPointsAllScat; break;
               default: res += fallbackCandle;
            }
         } else {
            switch (preset) {
               case '1': res += fallbackViolin; break;
               case '2': res += kMeanCircle + kWhisker15 + kHistoViolin + kHistoZeroIndicator + kPointsOutliers; break;
               default: res += fallbackViolin;
            }
         }

         const l = opt.indexOf('('), r = opt.lastIndexOf(')');
         if ((l >= 0) && (r > l + 1))
            res = parseInt(opt.slice(l + 1, r));

         fOption = res;

         if ((direction === 'Y' || direction === 'H') && !isOption(kHorizontal))
            fOption += kHorizontal;
      }, extractQuantiles = (xx, proj, prob) => {
         let integral = 0, cnt = 0, sum1 = 0;
         const res = { max: 0, first: -1, last: -1, entries: 0 };

         for (let j = 0; j < proj.length; ++j) {
            if (proj[j] > 0) {
               res.max = Math.max(res.max, proj[j]);
               if (res.first < 0)
                  res.first = j;
               res.last = j;
            }
            integral += proj[j];
            sum1 += proj[j] * (xx[j] + xx[j + 1]) / 2;
         }
         if (integral <= 0)
            return null;

         res.entries = integral;
         res.mean = sum1 / integral;
         res.quantiles = new Array(prob.length);
         res.indx = new Array(prob.length);

         for (let j = 0, sum = 0, nextv = 0; j < proj.length; ++j) {
            const v = nextv;
            let x = xx[j];

            // special case - flat integral with const value
            if ((v === prob[cnt]) && (proj[j] === 0) && (v < 0.99)) {
               while ((proj[j] === 0) && (j < proj.length))
                  j++;
               x = (xx[j] + x) / 2; // this will be mid value
            }

            sum += proj[j];
            nextv = sum / integral;
            while ((prob[cnt] >= v) && (prob[cnt] < nextv)) {
               res.indx[cnt] = j;
               res.quantiles[cnt] = x + ((prob[cnt] - v) / (nextv - v)) * (xx[j + 1] - x);
               if (cnt++ === prob.length)
                  return res;
               x = xx[j];
            }
         }

         while (cnt < prob.length) {
            res.indx[cnt] = proj.length - 1;
            res.quantiles[cnt++] = xx.at(-1);
         }

         return res;
      };

      if (o.Candle)
         parseOption(o.Candle, true);
      else if (o.Violin)
         parseOption(o.Violin, false);

      const histo = this.getHisto(),
            handle = this.prepareDraw(),
            cp = this.getCanvPainter(),
            funcs = this.getHistGrFuncs(),
            swapXY = isOption(kHorizontal);
      let scaledViolin = gStyle.fViolinScaled,
          scaledCandle = gStyle.fCandleScaled,
          maxContent = 0,
          markers = '', cmarkers = '', attrcmarkers = null;

      if (o.Scaled !== null)
         scaledViolin = scaledCandle = o.Scaled;
      else if (cp?.online_canvas) ; else if (histo.fTitle.indexOf('unscaled') >= 0)
         scaledViolin = scaledCandle = false;
      else if (histo.fTitle.indexOf('scaled') >= 0)
         scaledViolin = scaledCandle = true;

      if (scaledViolin && (isOption(kHistoRight) || isOption(kHistoLeft) || isOption(kHistoViolin))) {
         for (let i = 0; i < this.nbinsx; ++i) {
            for (let j = 0; j < this.nbinsy; ++j)
               maxContent = Math.max(maxContent, histo.getBinContent(i + 1, j + 1));
         }
      }

      const make_path = (...a) => {
         if (a[1] === 'array')
            a = a[0];
         const l = a.length;
         let i = 2, xx = a[0], yy = a[1],
             res = swapXY ? `M${yy},${xx}` : `M${xx},${yy}`;
         while (i < l) {
            switch (a[i]) {
               case 'Z':
                  return res + 'z';
               case 'V':
                  if (yy !== a[i + 1]) {
                     res += (swapXY ? 'h' : 'v') + (a[i + 1] - yy);
                     yy = a[i + 1];
                  }
                  break;
               case 'H':
                  if (xx !== a[i + 1]) {
                     res += (swapXY ? 'v' : 'h') + (a[i + 1] - xx);
                     xx = a[i + 1];
                  }
                  break;
               default:
                  res += swapXY ? `l${a[i + 1] - yy},${a[i] - xx}` : `l${a[i] - xx},${a[i + 1] - yy}`;
                  xx = a[i];
                  yy = a[i + 1];
            }
            i += 2;
         }
         return res;
      }, make_marker = (x, y) => {
         if (!markers) {
            const mw = gStyle.fCandleCrossLineWidth ?? 1;
            this.createAttMarker({ attr: histo, style: isOption(kPointsAllScat) ? 0 : (mw === 1 ? 5 : 18 * mw + 16) });
            this.markeratt.resetPos();
         }
         markers += swapXY ? this.markeratt.create(y, x) : this.markeratt.create(x, y);
      }, make_cmarker = (x, y) => {
         if (!attrcmarkers) {
            const mw = gStyle.fCandleCircleLineWidth ?? 1;
            attrcmarkers = this.createAttMarker({ attr: histo, style: (mw === 1 ? 24 : 18 * mw + 17), std: false });
            attrcmarkers.resetPos();
         }
         cmarkers += swapXY ? attrcmarkers.create(y, x) : attrcmarkers.create(x, y);
      };

      if (histo.fMarkerColor === 1)
         histo.fMarkerColor = histo.fLineColor;

      handle.candle = []; // array of drawn points

      let xx, bars = '', lines = '', dashed_lines = '', hists = '', hlines = '',
          proj, maxIntegral = 0;

      // Determining the quintiles
      const wRange = gStyle.fCandleWhiskerRange, bRange = gStyle.fCandleBoxRange,
            prob = [(wRange >= 1) ? 1e-15 : 0.5 - wRange / 2.0,
                    (bRange >= 1) ? 1e-14 : 0.5 - bRange / 2.0,
                    0.5,
                    (bRange >= 1) ? 1 - 1e-14 : 0.5 + bRange / 2.0,
                    (wRange >= 1) ? 1 - 1e-15 : 0.5 + wRange / 2.0];

      /* eslint-disable-next-line one-var */
      const produceCandlePoint = (bin_indx, grx_left, grx_right, xindx1, xindx2) => {
         const res = extractQuantiles(xx, proj, prob);
         if (!res)
            return;

         const pnt = { bin: bin_indx, swapXY, fBoxDown: res.quantiles[1], fMedian: res.quantiles[2], fBoxUp: res.quantiles[3] },
               iqr = pnt.fBoxUp - pnt.fBoxDown;
         let fWhiskerDown = res.quantiles[0], fWhiskerUp = res.quantiles[4];

         if (isOption(kWhisker15)) { // Improved whisker definition, with 1.5*iqr
            let pos = pnt.fBoxDown - 1.5 * iqr, indx = res.indx[1];
            while ((xx[indx] > pos) && (indx > 0))
               indx--;
            while (!proj[indx])
               indx++;
            fWhiskerDown = xx[indx]; // use lower edge here
            pos = pnt.fBoxUp + 1.5 * iqr; indx = res.indx[3];
            while ((xx[indx] < pos) && (indx < proj.length))
               indx++;
            while (!proj[indx])
               indx--;
            fWhiskerUp = xx[indx + 1]; // use upper index edge here
         }

         const fMean = res.mean,
               fMedianErr = 1.57 * iqr / Math.sqrt(res.entries);

         // estimate quantiles... simple function... not so nice as GetQuantiles

         // exclude points with negative y when log scale is specified
         if ((fWhiskerDown <= 0) && ((swapXY && funcs.logx) || (!swapXY && funcs.logy)))
            return;

         const w = (grx_right - grx_left);
         let candleWidth, histoWidth,
             center = (grx_left + grx_right) / 2 + histo.fBarOffset / 1000 * w;
         if ((histo.fBarWidth > 0) && (histo.fBarWidth !== 1000))
            candleWidth = histoWidth = w * histo.fBarWidth / 1000;
         else {
            candleWidth = w * 0.66;
            histoWidth = w * 0.8;
         }

         if (scaledViolin && (maxContent > 0))
            histoWidth *= res.max / maxContent;
         if (scaledCandle && (maxIntegral > 0))
            candleWidth *= res.entries / maxIntegral;

         pnt.x1 = Math.round(center - candleWidth / 2);
         pnt.x2 = Math.round(center + candleWidth / 2);
         center = Math.round(center);

         const x1d = Math.round(center - candleWidth / 3),
               x2d = Math.round(center + candleWidth / 3),
               ff = swapXY ? funcs.grx : funcs.gry;

         pnt.yy1 = Math.round(ff(fWhiskerUp));
         pnt.y1 = Math.round(ff(pnt.fBoxUp));
         pnt.y0 = Math.round(ff(pnt.fMedian));
         pnt.y2 = Math.round(ff(pnt.fBoxDown));
         pnt.yy2 = Math.round(ff(fWhiskerDown));

         const y0m = Math.round(ff(fMean)),
               y01 = Math.round(ff(pnt.fMedian + fMedianErr)),
               y02 = Math.round(ff(pnt.fMedian - fMedianErr));

         if (isOption(kHistoZeroIndicator))
            hlines += make_path(center, Math.round(ff(xx[xindx1])), 'V', Math.round(ff(xx[xindx2])));

         if (isOption(kMedianLine))
            lines += make_path(pnt.x1, pnt.y0, 'H', pnt.x2);
         else if (isOption(kMedianNotched))
            lines += make_path(x1d, pnt.y0, 'H', x2d);
         else if (isOption(kMedianCircle))
            make_cmarker(center, pnt.y0);

         if (isOption(kMeanCircle))
            make_cmarker(center, y0m);
         else if (isOption(kMeanLine))
            dashed_lines += make_path(pnt.x1, y0m, 'H', pnt.x2);

         if (isOption(kBox)) {
            if (isOption(kMedianNotched))
               bars += make_path(pnt.x1, pnt.y1, 'V', y01, x1d, pnt.y0, pnt.x1, y02, 'V', pnt.y2, 'H', pnt.x2, 'V', y02, x2d, pnt.y0, pnt.x2, y01, 'V', pnt.y1, 'Z');
            else
               bars += make_path(pnt.x1, pnt.y1, 'V', pnt.y2, 'H', pnt.x2, 'V', pnt.y1, 'Z');
         }

         if (isOption(kAnchor))  // Draw the anchor line
            lines += make_path(pnt.x1, pnt.yy1, 'H', pnt.x2) + make_path(pnt.x1, pnt.yy2, 'H', pnt.x2);

         if (isOption(kWhiskerAll) && !isOption(kHistoZeroIndicator)) { // Whiskers are dashed
            dashed_lines += make_path(center, pnt.y1, 'V', pnt.yy1) + make_path(center, pnt.y2, 'V', pnt.yy2);
         } else if ((isOption(kWhiskerAll) && isOption(kHistoZeroIndicator)) || isOption(kWhisker15))
            lines += make_path(center, pnt.y1, 'V', pnt.yy1) + make_path(center, pnt.y2, 'V', pnt.yy2);


         if (isOption(kPointsOutliers) || isOption(kPointsAll) || isOption(kPointsAllScat)) {
            // reset seed for each projection to have always same pixels
            const rnd = new TRandom(bin_indx * 7521 + Math.round(res.integral)),
                  show_all = !isOption(kPointsOutliers),
                  show_scat = isOption(kPointsAllScat);
            for (let ii = 0; ii < proj.length; ++ii) {
               const bin_content = proj[ii], binx = (xx[ii] + xx[ii + 1]) / 2;
               let marker_x = center, marker_y;

               if (!bin_content)
                  continue;
               if (!show_all && (binx >= fWhiskerDown) && (binx <= fWhiskerUp))
                  continue;

               for (let k = 0; k < bin_content; k++) {
                  if (show_scat)
                     marker_x = center + Math.round(((rnd.random() - 0.5) * candleWidth));

                  if ((bin_content === 1) && !show_scat)
                     marker_y = Math.round(ff(binx));
                  else
                     marker_y = Math.round(ff(xx[ii] + rnd.random() * (xx[ii + 1] - xx[ii])));

                  make_marker(marker_x, marker_y);
               }
            }
         }

         if ((isOption(kHistoRight) || isOption(kHistoLeft) || isOption(kHistoViolin)) && (res.max > 0) && (res.first >= 0)) {
            const arr = [], scale = (swapXY ? -0.5 : 0.5) * histoWidth / res.max;

            xindx1 = Math.max(xindx1, res.first);
            xindx2 = Math.min(xindx2 - 1, res.last);

            if (isOption(kHistoRight) || isOption(kHistoViolin)) {
               let prev_x = center, prev_y = Math.round(ff(xx[xindx1]));
               arr.push(prev_x, prev_y);
               for (let ii = xindx1; ii <= xindx2; ii++) {
                  const curr_x = Math.round(center + scale * proj[ii]),
                        curr_y = Math.round(ff(xx[ii + 1]));
                  if (curr_x !== prev_x) {
                     if (ii !== xindx1)
                        arr.push('V', prev_y);
                     arr.push('H', curr_x);
                  }
                  prev_x = curr_x;
                  prev_y = curr_y;
               }
               arr.push('V', prev_y);
            }

            if (isOption(kHistoLeft) || isOption(kHistoViolin)) {
               let prev_x = center, prev_y = Math.round(ff(xx[xindx2 + 1]));
               if (!arr.length)
                  arr.push(prev_x, prev_y);
               for (let ii = xindx2; ii >= xindx1; ii--) {
                  const curr_x = Math.round(center - scale * proj[ii]),
                        curr_y = Math.round(ff(xx[ii]));
                  if (curr_x !== prev_x) {
                     if (ii !== xindx2)
                        arr.push('V', prev_y);
                     arr.push('H', curr_x);
                  }
                  prev_x = curr_x;
                  prev_y = curr_y;
               }
               arr.push('V', prev_y);
            }

            arr.push('H', center); // complete histogram

            hists += make_path(arr, 'array');

            if (!this.fillatt.empty())
               hists += 'Z';
         }

         handle.candle.push(pnt); // keep point for the tooltip
      };

      if (swapXY) {
         xx = new Array(this.nbinsx + 1);
         proj = new Array(this.nbinsx);
         for (let i = 0; i < this.nbinsx + 1; ++i)
            xx[i] = histo.fXaxis.GetBinLowEdge(i + 1);

         if (scaledCandle) {
            for (let j = 0; j < this.nbinsy; ++j) {
               let sum = 0;
               for (let i = 0; i < this.nbinsx; ++i)
                  sum += histo.getBinContent(i + 1, j + 1);
               maxIntegral = Math.max(maxIntegral, sum);
            }
         }

         for (let j = handle.j1; j < handle.j2; ++j) {
            for (let i = 0; i < this.nbinsx; ++i)
               proj[i] = histo.getBinContent(i + 1, j + 1);

            produceCandlePoint(j, handle.gry[j + 1], handle.gry[j], handle.i1, handle.i2);
         }
      } else {
         xx = new Array(this.nbinsy + 1);
         proj = new Array(this.nbinsy);

         for (let j = 0; j < this.nbinsy + 1; ++j)
            xx[j] = histo.fYaxis.GetBinLowEdge(j + 1);

         if (scaledCandle) {
            for (let i = 0; i < this.nbinsx; ++i) {
               let sum = 0;
               for (let j = 0; j < this.nbinsy; ++j)
                  sum += histo.getBinContent(i + 1, j + 1);
               maxIntegral = Math.max(maxIntegral, sum);
            }
         }

         // loop over visible x-bins
         for (let i = handle.i1; i < handle.i2; ++i) {
            for (let j = 0; j < this.nbinsy; ++j)
               proj[j] = histo.getBinContent(i + 1, j + 1);

            produceCandlePoint(i, handle.grx[i], handle.grx[i + 1], handle.j1, handle.j2);
         }
      }

      if (hlines && (histo.fFillColor > 0)) {
         this.appendPath(hlines)
             .style('stroke', this.getColor(histo.fFillColor));
      }

      const hline_color = (isOption(kHistoZeroIndicator) && histo.fFillStyle) ? this.fillatt.color : this.lineatt.color;
      if (hists && (!this.fillatt.empty() || (hline_color !== 'none'))) {
         this.appendPath(hists)
             .style('stroke', (hline_color !== 'none') ? hline_color : null)
             .style('pointer-events', this.isBatchMode() ? null : 'visibleFill')
             .call(this.fillatt.func);
      }

      if (bars) {
         this.appendPath(bars)
             .call(this.lineatt.func)
             .call(this.fillatt.func);
      }

      if (lines) {
         this.appendPath(lines)
             .call(this.lineatt.func)
             .style('fill', 'none');
      }

      if (dashed_lines) {
         const dashed = this.createAttLine({ attr: histo, style: 2, std: false, color: kBlack });
         this.appendPath(dashed_lines)
             .call(dashed.func)
             .style('fill', 'none');
      }

      if (cmarkers) {
         this.appendPath(cmarkers)
             .call(attrcmarkers.func);
      }

      if (markers) {
         this.appendPath(markers)
             .call(this.markeratt.func);
      }

      return handle;
   }

   /** @summary Draw TH2 bins as scatter plot */
   drawBinsScatter() {
      const histo = this.getHisto(),
            o = this.getOptions(),
            handle = this.prepareDraw({ rounding: true, pixel_density: true }),
            test_cutg = o.cutg,
            colPaths = [], currx = [], curry = [], cell_w = [], cell_h = [],
            scale = o.ScatCoef * ((this.gmaxbin) > 2000 ? 2000 / this.gmaxbin : 1),
            rnd = new TRandom(handle.sumz);
      let colindx, cmd1, cmd2, i, j, binz, cw, ch, factor = 1.0;

      handle.ScatterPlot = true;

      if (scale * handle.sumz < 1e5) {
         // one can use direct drawing of scatter plot without any patterns

         this.createAttMarker({ attr: histo });

         this.markeratt.resetPos();

         let path = '';
         for (i = handle.i1; i < handle.i2; ++i) {
            cw = handle.grx[i + 1] - handle.grx[i];
            for (j = handle.j1; j < handle.j2; ++j) {
               ch = handle.gry[j] - handle.gry[j + 1];
               binz = histo.getBinContent(i + 1, j + 1);

               const npix = Math.round(scale * binz);
               if (npix <= 0)
                  continue;

               if (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5),
                                                    histo.fYaxis.GetBinCoord(j + 0.5)))
                  continue;

               for (let k = 0; k < npix; ++k) {
                  path += this.markeratt.create(
                     Math.round(handle.grx[i] + cw * rnd.random()),
                     Math.round(handle.gry[j + 1] + ch * rnd.random()));
               }
            }
         }

         this.appendPath(path)
             .call(this.markeratt.func);

         return handle;
      }

      // limit filling factor, do not try to produce as many points as filled area;
      if (this.maxbin > 0.7)
         factor = 0.7 / this.maxbin;

      const nlevels = Math.round(handle.max - handle.min),
            cntr = this.createContour((nlevels > 50) ? 50 : nlevels, this.minposbin, this.maxbin, this.minposbin);

      // now start build
      for (i = handle.i1; i < handle.i2; ++i) {
         for (j = handle.j1; j < handle.j2; ++j) {
            binz = histo.getBinContent(i + 1, j + 1);
            if ((binz <= 0) || (binz < this.minbin))
               continue;

            cw = handle.grx[i + 1] - handle.grx[i];
            ch = handle.gry[j] - handle.gry[j + 1];
            if (cw * ch <= 0)
               continue;

            colindx = cntr.getContourIndex(binz / cw / ch);
            if (colindx < 0)
               continue;

            if (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5), histo.fYaxis.GetBinCoord(j + 0.5)))
               continue;

            cmd1 = `M${handle.grx[i]},${handle.gry[j + 1]}`;
            if (colPaths[colindx] === undefined) {
               colPaths[colindx] = cmd1;
               cell_w[colindx] = cw;
               cell_h[colindx] = ch;
            } else {
               cmd2 = `m${handle.grx[i] - currx[colindx]},${handle.gry[j + 1] - curry[colindx]}`;
               colPaths[colindx] += (cmd2.length < cmd1.length) ? cmd2 : cmd1;
               cell_w[colindx] = Math.max(cell_w[colindx], cw);
               cell_h[colindx] = Math.max(cell_h[colindx], ch);
            }

            currx[colindx] = handle.grx[i];
            curry[colindx] = handle.gry[j + 1];

            colPaths[colindx] += `v${ch}h${cw}v${-ch}z`;
         }
      }

      const pp = this.getPadPainter(),
            layer = pp.selectChild('.main_layer');
      let defs = layer.selectChild('defs');
      if (defs.empty() && colPaths.length)
         defs = layer.insert('svg:defs', ':first-child');

      this.createAttMarker({ attr: histo });

      for (colindx = 0; colindx < colPaths.length; ++colindx) {
         if ((colPaths[colindx] !== undefined) && (colindx < cntr.arr.length)) {
            const pattern_id = (pp.getPadName() || 'canv') + `_scatter_${colindx}`;
            let pattern = defs.selectChild(`#${pattern_id}`);
            if (pattern.empty()) {
               pattern = defs.append('svg:pattern')
                             .attr('id', pattern_id)
                             .attr('patternUnits', 'userSpaceOnUse');
            } else
               pattern.selectAll('*').remove();

            let npix = Math.round(factor * cntr.arr[colindx] * cell_w[colindx] * cell_h[colindx]);
            npix = Math.max(1, npix);

            const arrx = new Float32Array(npix), arry = new Float32Array(npix);

            if (npix === 1)
               arrx[0] = arry[0] = 0.5;
            else {
               for (let n = 0; n < npix; ++n) {
                  arrx[n] = rnd.random();
                  arry[n] = rnd.random();
               }
            }

            this.markeratt.resetPos();

            let path = '';
            for (let n = 0; n < npix; ++n)
               path += this.markeratt.create(arrx[n] * cell_w[colindx], arry[n] * cell_h[colindx]);

            pattern.attr('width', cell_w[colindx])
                   .attr('height', cell_h[colindx])
                   .append('svg:path')
                   .attr('d', path)
                   .call(this.markeratt.func);

            this.appendPath(colPaths[colindx])
                .attr('scatter-index', colindx)
                .style('fill', `url(#${pattern_id})`);
         }
      }

      return handle;
   }

   /** @summary Draw TH2 bins in 2D mode */
   draw2DBins() {
      const o = this.getOptions();

      if (this.#hide_frame && this.isMainPainter()) {
         this.getPadPainter().getFrameSvg().style('display', null);
         this.#hide_frame = undefined;
      } else if (o.Same && !this.isUseFrame())
         this.getPadPainter().getFrameSvg().style('display', 'none');

      if (!this.draw_content) {
         if (o.Zscale && o.ohmin && o.ohmax) {
            this.getContour(true);
            this.getHistPalette();
         }
         return this.removeG();
      }

      this.createHistDrawAttributes();

      this.createG(this.isUseFrame());

      let handle, pr;

      if (this.isTH2Poly())
         pr = this.drawPolyBins();
      else {
         if (o.Scat)
            handle = this.drawBinsScatter();

         if (o.System === kPOLAR)
            handle = this.drawBinsPolar();
         else if (o.Arrow)
            handle = this.drawBinsArrow();
         else if (o.Color)
            handle = this.drawBinsColor();
         else if (o.Box)
            handle = this.drawBinsBox();
         else if (o.Proj)
            handle = this.drawBinsProjected();
         else if (o.Contour)
            handle = this.drawBinsContour();
         else if (o.Candle || o.Violin)
            handle = this.drawBinsCandle();

         if (o.Text)
            pr = this.drawBinsText(handle);

         if (!handle && !pr)
            handle = this.drawBinsColor();
      }

      if (handle)
         this.tt_handle = handle;
      else if (pr)
         return pr.then(tt => { this.tt_handle = tt; });
   }

   /** @summary Draw TH2 in circular mode */
   async drawBinsCircular() {
      this.#hide_frame = true;

      const pp = this.getPadPainter(),
            rect = pp.getFrameRect(),
            hist = this.getHisto(),
            circ = this.getOptions().Circular,
            palette = circ > 10 ? this.getHistPalette() : null,
            text_size = 20,
            circle_size = 16,
            axis = hist.fXaxis,
            g = this.createG(),
            getBinLabel = indx => {
               if (axis.fLabels) {
                  for (let i = 0; i < axis.fLabels.arr.length; ++i) {
                     const tstr = axis.fLabels.arr[i];
                     if (tstr.fUniqueID === indx + 1)
                        return tstr.fString;
                  }
               }
               return indx.toString();
            };

      pp.getFrameSvg().style('display', 'none');

      this.assignChordCircInteractive(Math.round(rect.x + rect.width / 2), Math.round(rect.y + rect.height / 2));

      const nbins = Math.min(this.nbinsx, this.nbinsy);

      return this.startTextDrawingAsync(42, text_size, g).then(() => {
         const pnts = [];

         for (let n = 0; n < nbins; n++) {
            const a = (0.5 - n / nbins) * Math.PI * 2,
                  cx = Math.round((0.9 * rect.width / 2 - 2 * circle_size) * Math.cos(a)),
                  cy = Math.round((0.9 * rect.height / 2 - 2 * circle_size) * Math.sin(a)),
                  x = Math.round(0.9 * rect.width / 2 * Math.cos(a)),
                  y = Math.round(0.9 * rect.height / 2 * Math.sin(a)),
                  color = palette?.calcColor(n, nbins) ?? 'black';
            let rotate = Math.round(a / Math.PI * 180), align = 12;

            pnts.push({ x: cx, y: cy, a, color }); // remember points coordinates

            if ((rotate < -90) || (rotate > 90)) {
               rotate += 180;
               align = 32;
            }

            const s2 = Math.round(text_size / 2), s1 = 2 * s2;

            g.append('path')
             .attr('d', `M${cx - s2},${cy} a${s2},${s2},0,1,0,${s1},0a${s2},${s2},0,1,0,${-s1},0z`)
             .style('stroke', color)
             .style('fill', 'none');

            this.drawText({ align, rotate, x, y, text: getBinLabel(n) });
         }

         const max_width = circle_size / 2;
         let max_value = 0, min_value = 0;
         if (circ > 11) {
            for (let i = 0; i < nbins - 1; ++i) {
               for (let j = i + 1; j < nbins; ++j) {
                  const cont = hist.getBinContent(i + 1, j + 1);
                  if (cont > 0) {
                     max_value = Math.max(max_value, cont);
                     if (!min_value || (cont < min_value))
                        min_value = cont;
                  }
               }
            }
         }

         for (let i = 0; i < nbins - 1; ++i) {
            const pi = pnts[i];
            let path = '';

            for (let j = i + 1; j < nbins; ++j) {
               const cont = hist.getBinContent(i + 1, j + 1);
               if (cont <= 0)
                  continue;

               const pj = pnts[j],
                     a = (pi.a + pj.a) / 2,
                     qr = 0.5 * (1 - Math.abs(pi.a - pj.a) / Math.PI), // how far Q point will be away from center
                     qx = Math.round(qr * rect.width / 2 * Math.cos(a)),
                     qy = Math.round(qr * rect.height / 2 * Math.sin(a));

               path += `M${pi.x},${pi.y}Q${qx},${qy},${pj.x},${pj.y}`;

               if ((circ > 11) && (max_value > min_value)) {
                  const width = Math.round((cont - min_value) / (max_value - min_value) * (max_width - 1) + 1);
                  g.append('path').attr('d', path).style('stroke', pi.color).style('stroke-width', width).style('fill', 'none');
                  path = '';
               }
            }
            if (path)
               g.append('path').attr('d', path).style('stroke', pi.color).style('fill', 'none');
         }

         return this.finishTextDrawing();
      }).then(() => {
         if (!this.isBatchMode()) {
            g.insert('path', ':first-child')
             .attr('d', `M${-rect.width / 2},${-rect.height / 2}h${rect.width}v${rect.height}h${-rect.width}z`)
             .style('opacity', 0)
             .style('fill', 'none')
             .style('pointer-events', 'visibleFill');
         }

         return this;
      });
   }

   /** @summary Prepare translation and assign interactive handler */
   assignChordCircInteractive(midx, midy) {
      if (!this.#chord)
         this.#chord = { x: 0, y: 0, zoom: 1 };

      makeTranslate(this.getG(), midx + this.#chord.x, midy + this.#chord.y, this.#chord.zoom);

      if (this.isBatchMode())
         return;

      if (settings.Zooming && settings.ZoomWheel) {
         this.getG().on('wheel', evnt => {
            const pos = pointer(evnt, this.getG().node()),
                  delta = evnt.wheelDelta ? -evnt.wheelDelta : (evnt.deltaY || evnt.detail),
                  prev_zoom = this.#chord.zoom;

            this.#chord.zoom *= (delta > 0) ? 0.8 : 1.2;
            this.#chord.x += pos[0] * (prev_zoom - this.#chord.zoom);
            this.#chord.y += pos[1] * (prev_zoom - this.#chord.zoom);

            makeTranslate(this.getG(), midx + this.#chord.x, midy + this.#chord.y, this.#chord.zoom);
         }).on('dblclick', () => {
            this.#chord.x = this.#chord.y = 0;
            this.#chord.zoom = 1;
            makeTranslate(this.getG(), midx, midy);
         });
      }

      assignContextMenu(this);
   }

   /** @summary Draw histogram bins as chord diagram */
   async drawBinsChord() {
      this.getPadPainter().getFrameSvg().style('display', 'none');
      this.#hide_frame = true;

      const used = [],
            nbins = Math.min(this.nbinsx, this.nbinsy),
            hist = this.getHisto();
      let fullsum = 0, isint = true;
      for (let i = 0; i < nbins; ++i) {
         let sum = 0;
         for (let j = 0; j < nbins; ++j) {
            const cont = hist.getBinContent(i + 1, j + 1);
            if (cont > 0) {
               sum += cont;
               if (isint && (Math.round(cont) !== cont))
                  isint = false;
            }
         }
         if (sum > 0)
            used.push(i);
         fullsum += sum;
      }

      // do not show less than 2 elements
      if (used.length < 2)
         return true;

      let ndig = 0, tickStep = 1;
      const rect = this.getPadPainter().getFrameRect(),
            midx = Math.round(rect.x + rect.width / 2),
            midy = Math.round(rect.y + rect.height / 2),
            palette = this.getHistPalette(),
            outerRadius = Math.max(10, Math.min(rect.width, rect.height) * 0.5 - 60),
            innerRadius = Math.max(2, outerRadius - 10),
            data = [], labels = [],
            formatValue = v => v.toString(),
            formatTicks = v => { return (ndig > 3) ? v.toExponential(0) : v.toFixed(ndig); },
            d3_descending = (a, b) => { return b < a ? -1 : b > a ? 1 : b >= a ? 0 : Number.NaN; };

      if (!isint && fullsum < 10) {
         const lstep = Math.round(Math.log10(fullsum) - 2.3);
         ndig = -lstep;
         tickStep = Math.pow(10, lstep);
      } else if (fullsum > 200) {
         const lstep = Math.round(Math.log10(fullsum) - 2.3);
         tickStep = Math.pow(10, lstep);
      }

      if (tickStep * 250 < fullsum)
         tickStep *= 5;
      else if (tickStep * 100 < fullsum)
         tickStep *= 2;

      for (let i = 0; i < used.length; ++i) {
         data[i] = [];
         for (let j = 0; j < used.length; ++j)
            data[i].push(hist.getBinContent(used[i] + 1, used[j] + 1));
         const axis = hist.fXaxis;
         let lbl = 'indx_' + used[i].toString();
         if (axis.fLabels) {
            for (let k = 0; k < axis.fLabels.arr.length; ++k) {
               const tstr = axis.fLabels.arr[k];
               if (tstr.fUniqueID === used[i] + 1) {
                  lbl = tstr.fString;
                  break;
               }
            }
         }
         labels.push(lbl);
      }

      const g = this.createG();

      this.assignChordCircInteractive(midx, midy);

      const chord$1 = chord()
                    .padAngle(10 / innerRadius)
                    .sortSubgroups(d3_descending)
                    .sortChords(d3_descending),
            chords = chord$1(data),
            group = g.append('g')
                     .attr('font-size', 10)
                     .attr('font-family', 'sans-serif')
                     .selectAll('g')
                     .data(chords.groups)
                     .join('g'),
            arc$1 = arc().innerRadius(innerRadius).outerRadius(outerRadius),
            ribbon = ribbon$1().radius(innerRadius - 1).padAngle(1 / innerRadius);

      function ticks({ startAngle, endAngle, value }) {
         const k = (endAngle - startAngle) / value,
               arr = [];
         for (let z = 0; z <= value; z += tickStep)
            arr.push({ value: z, angle: z * k + startAngle });
         return arr;
      }

      group.append('path')
         .attr('fill', d => palette.calcColor(d.index, used.length))
         .attr('d', arc$1);

      group.append('title').text(d => `${labels[d.index]} ${formatValue(d.value)}`);

      const groupTick = group.append('g')
         .selectAll('g')
         .data(ticks)
         .join('g')
         .attr('transform', d => `rotate(${Math.round(d.angle * 180 / Math.PI - 90)}) translate(${outerRadius})`);
      groupTick.append('line')
         .attr('stroke', 'currentColor')
         .attr('x2', 6);

      groupTick.append('text')
         .attr('x', 8)
         .attr('dy', '0.35em')
         .attr('transform', d => { return (d.angle > Math.PI) ? 'rotate(180) translate(-16)' : null; })
         .attr('text-anchor', d => { return (d.angle > Math.PI) ? 'end' : null; })
         .text(d => formatTicks(d.value));

      group.select('text')
         .attr('font-weight', 'bold')
         .text(function(d) {
            return this.getAttribute('text-anchor') === 'end' ? `↑ ${labels[d.index]}` : `${labels[d.index]} ↓`;
         });

      g.append('g')
       .attr('fill-opacity', 0.8)
       .selectAll('path')
       .data(chords)
       .join('path')
       .style('mix-blend-mode', 'multiply')
       .attr('fill', d => palette.calcColor(d.source.index, used.length))
       .attr('d', ribbon)
       .append('title')
       .text(d => `${formatValue(d.source.value)} ${labels[d.target.index]} → ${labels[d.source.index]}${d.source.index === d.target.index ? '' : `\n${formatValue(d.target.value)} ${labels[d.source.index]} → ${labels[d.target.index]}`}`);

      if (!this.isBatchMode()) {
         g.insert('ellipse', ':first-child')
          .attr('cx', 0)
          .attr('cy', 0)
          .attr('rx', outerRadius * 1.2)
          .attr('ry', outerRadius * 1.2)
          .style('opacity', 0)
          .style('fill', 'none')
          .style('pointer-events', 'visibleFill');
      }

      return true;
   }

   /** @summary Provide text information (tooltips) for histogram bin */
   getBinTooltips(i, j) {
      const histo = this.getHisto(),
            profile2d = this.matchObjectType(clTProfile2D) && isFunc(histo.getBinEntries),
            bincontent = histo.getBinContent(i + 1, j + 1);
      let binz = bincontent;

      if (histo.$baseh)
         binz -= histo.$baseh.getBinContent(i + 1, j + 1);

      const lines = [this.getObjectHint(),
                     'x = ' + this.getAxisBinTip('x', histo.fXaxis, i),
                     'y = ' + this.getAxisBinTip('y', histo.fYaxis, j),
                     `bin = ${histo.getBin(i + 1, j + 1)}  x: ${i + 1}  y: ${j + 1}`,
                     'content = ' + ((binz === Math.round(binz)) ? binz : floatToString(binz, gStyle.fStatFormat))];

      if ((this.getOptions().TextKind === 'E') || profile2d || histo.fSumw2?.length) {
         const errs = this.getBinErrors(histo, histo.getBin(i + 1, j + 1), bincontent);
         if (errs.poisson)
            lines.push('error low = ' + floatToString(errs.low, gStyle.fPaintTextFormat), 'error up = ' + floatToString(errs.up, gStyle.fPaintTextFormat));
         else
            lines.push('error = ' + floatToString(errs.up, gStyle.fPaintTextFormat));
      }

      if (profile2d) {
         const entries = histo.getBinEntries(i + 1, j + 1);
         lines.push('entries = ' + ((entries === Math.round(entries)) ? entries : floatToString(entries, gStyle.fStatFormat)));
      }

      return lines;
   }

   /** @summary Provide text information (tooltips) for candle bin */
   getCandleTooltips(p) {
      const funcs = this.getHistGrFuncs(),
            histo = this.getHisto();

      return [this.getObjectHint(),
              p.swapXY ? 'y = ' + funcs.axisAsText('y', histo.fYaxis.GetBinLowEdge(p.bin + 1))
                       : 'x = ' + funcs.axisAsText('x', histo.fXaxis.GetBinLowEdge(p.bin + 1)),
              'm-25%  = ' + floatToString(p.fBoxDown, gStyle.fStatFormat),
              'median = ' + floatToString(p.fMedian, gStyle.fStatFormat),
              'm+25%  = ' + floatToString(p.fBoxUp, gStyle.fStatFormat)];
   }

   /** @summary Provide text information (tooltips) for poly bin */
   getPolyBinTooltips(binindx, realx, realy) {
      const histo = this.getHisto(),
            bin = histo.fBins.arr[binindx],
            funcs = this.getHistGrFuncs(),
            lines = [];
      let binname = bin.fPoly.fName, numpoints = 0;

      if (binname === 'Graph')
         binname = '';
      if (!binname)
         binname = bin.fNumber;

      if ((realx === undefined) && (realy === undefined)) {
         realx = realy = 0;
         let gr = bin.fPoly, numgraphs = 1;
         if (gr._typename === clTMultiGraph) {
            numgraphs = bin.fPoly.fGraphs.arr.length;
            gr = null;
         }

         for (let ngr = 0; ngr < numgraphs; ++ngr) {
            if (!gr || (ngr > 0))
               gr = bin.fPoly.fGraphs.arr[ngr];

            for (let n = 0; n < gr.fNpoints; ++n) {
               ++numpoints;
               realx += gr.fX[n];
               realy += gr.fY[n];
            }
         }

         if (numpoints > 1) {
            realx /= numpoints;
            realy /= numpoints;
         }
      }

      lines.push(this.getObjectHint(),
                 'x = ' + funcs.axisAsText('x', realx),
                 'y = ' + funcs.axisAsText('y', realy));
      if (numpoints > 0)
         lines.push('npnts = ' + numpoints);
      lines.push(`bin = ${binname}`);
      if (bin.fContent === Math.round(bin.fContent))
         lines.push('content = ' + bin.fContent);
      else
         lines.push('content = ' + floatToString(bin.fContent, gStyle.fStatFormat));
      return lines;
   }

   /** @summary Process tooltip event */
   processTooltipEvent(pnt) {
      const histo = this.getHisto(),
            o = this.getOptions(),
            h = this.tt_handle;
      let ttrect = this.getG()?.selectChild('.tooltip_bin');

      if (!pnt || !this.draw_content || !this.getG() || !h || o.Proj) {
         ttrect?.remove();
         return null;
      }

      if (h.poly) {
         // process tooltips from TH2Poly
         const funcs = this.getHistGrFuncs(),
               realx = funcs.revertAxis('x', pnt.x),
               realy = funcs.revertAxis('y', pnt.y);
         let foundindx = -1, bin;

         if ((realx !== undefined) && (realy !== undefined)) {
            const len = histo.fBins.arr.length;

            for (let i = 0; (i < len) && (foundindx < 0); ++i) {
               bin = histo.fBins.arr[i];

               // found potential bins candidate
               if ((realx < bin.fXmin) || (realx > bin.fXmax) || (realy < bin.fYmin) || (realy > bin.fYmax))
                  continue;

               // ignore empty bins with col0 option
               if (!bin.fContent && !o.Zero)
                  continue;

               let gr = bin.fPoly, numgraphs = 1;
               if (gr._typename === clTMultiGraph) {
                  numgraphs = bin.fPoly.fGraphs.arr.length;
                  gr = null;
               }

               for (let ngr = 0; ngr < numgraphs; ++ngr) {
                  if (!gr || (ngr > 0))
                     gr = bin.fPoly.fGraphs.arr[ngr];
                  if (gr.IsInside(realx, realy)) {
                     foundindx = i;
                     break;
                  }
               }
            }
         }

         if (foundindx < 0) {
            ttrect.remove();
            return null;
         }

         const res = {
            name: histo.fName, title: histo.fTitle,
            x: pnt.x, y: pnt.y,
            color1: this.lineatt?.color ?? 'green',
            color2: this.fillatt?.getFillColorAlt('blue') ?? 'blue',
            exact: true, menu: true,
            lines: this.getPolyBinTooltips(foundindx, realx, realy)
         };

         if (pnt.disabled) {
            ttrect.remove();
            res.changed = true;
         } else {
            if (ttrect.empty()) {
               ttrect = this.appendPath()
                            .attr('class', 'tooltip_bin')
                            .style('pointer-events', 'none')
                            .call(addHighlightStyle);
            }

            res.changed = ttrect.property('current_bin') !== foundindx;

            if (res.changed) {
               ttrect.attr('d', this.createPolyBin(funcs, bin))
                     .style('opacity', '0.7')
                     .property('current_bin', foundindx);
            }
         }

         if (res.changed) {
            res.user_info = {
               obj: histo, name: histo.fName,
               bin: foundindx,
               cont: bin.fContent,
               grx: pnt.x, gry: pnt.y
            };
         }
         return res;
      } else if (h.candle) {
         // process tooltips for candle

         let i, p, match;

         for (i = 0; i < h.candle.length; ++i) {
            p = h.candle[i];
            match = p.swapXY
                      ? ((p.x1 <= pnt.y) && (pnt.y <= p.x2) && (p.yy1 >= pnt.x) && (pnt.x >= p.yy2))
                      : ((p.x1 <= pnt.x) && (pnt.x <= p.x2) && (p.yy1 <= pnt.y) && (pnt.y <= p.yy2));
            if (match)
               break;
         }

         if (!match) {
            ttrect.remove();
            return null;
         }

         const res = {
            name: histo.fName, title: histo.fTitle,
            x: pnt.x, y: pnt.y,
            color1: this.lineatt?.color ?? 'green',
            color2: this.fillatt?.getFillColorAlt('blue') ?? 'blue',
            lines: this.getCandleTooltips(p), exact: true, menu: true
         };

         if (pnt.disabled) {
            ttrect.remove();
            res.changed = true;
         } else {
            if (ttrect.empty()) {
               ttrect = this.appendPath()
                            .attr('class', 'tooltip_bin')
                            .style('pointer-events', 'none')
                            .call(addHighlightStyle)
                            .style('opacity', '0.7');
            }

            res.changed = ttrect.property('current_bin') !== i;

            if (res.changed) {
               ttrect.attr('d', p.swapXY ? `M${p.yy1},${p.x1}H${p.yy2}V${p.x2}H${p.yy1}Z` : `M${p.x1},${p.yy1}H${p.x2}V${p.yy2}H${p.x1}Z`)
                     .property('current_bin', i);
            }
         }

         if (res.changed) {
            res.user_info = {
               obj: histo, name: histo.fName,
               bin: i + 1, cont: p.fMedian, binx: i + 1, biny: 1,
               grx: pnt.x, gry: pnt.y
            };
         }

         return res;
      }

      const fp = this.getFramePainter();
      let i, j, binz = 0, colindx = null, is_pol = false,
          i1, i2, j1, j2, x1, x2, y1, y2;

      if (isFunc(h.findBin)) {
         const bin = h.findBin(pnt.x, pnt.y);
         i = bin?.i ?? h.i2;
         j = bin?.j ?? h.j2;
         is_pol = true;
      } else {
         // search bins position
         if (fp.reverse_x())
            for (i = h.i1; (i < h.i2) && ((pnt.x > h.grx[i]) || (pnt.x < h.grx[i + 1])); ++i);
         else
            for (i = h.i1; (i < h.i2) && ((pnt.x < h.grx[i]) || (pnt.x > h.grx[i + 1])); ++i);


         if (fp.reverse_y())
            for (j = h.j1; (j < h.j2) && ((pnt.y > h.gry[j + 1]) || (pnt.y < h.gry[j])); ++j);
         else
            for (j = h.j1; (j < h.j2) && ((pnt.y < h.gry[j + 1]) || (pnt.y > h.gry[j])); ++j);
      }

      if ((i < h.i2) && (j < h.j2)) {
         i1 = i;
         i2 = i + 1;
         j1 = j;
         j2 = j + 1;
         x1 = h.grx[i1];
         x2 = h.grx[i2];
         y1 = h.gry[j2];
         y2 = h.gry[j1];

         let match = true;

         if (o.Color && !is_pol) {
            // take into account bar settings
            const dx = x2 - x1, dy = y2 - y1;
            x2 = Math.round(x1 + dx * h.xbar2);
            x1 = Math.round(x1 + dx * h.xbar1);
            y2 = Math.round(y1 + dy * h.ybar2);
            y1 = Math.round(y1 + dy * h.ybar1);
            if (fp.reverse_x()) {
               if ((pnt.x > x1) || (pnt.x <= x2))
                  match = false;
            } else if ((pnt.x < x1) || (pnt.x >= x2))
               match = false;

            if (fp.reverse_y()) {
               if ((pnt.y > y1) || (pnt.y <= y2))
                  match = false;
            } else if ((pnt.y < y1) || (pnt.y >= y2))
               match = false;
         }

         binz = histo.getBinContent(i + 1, j + 1);
         if (this.#projection_kind)
            colindx = 0; // just to avoid hide
         else if (!match)
            colindx = null;
         else if (h.hide_only_zeros)
            colindx = (binz === 0) && !o.ShowEmpty ? null : 0;
         else {
            colindx = this.getContour().getPaletteIndex(this.getHistPalette(), binz);
            if ((colindx === null) && (binz === 0) &&
                (o.ShowEmpty || (histo._typename === clTProfile2D && histo.getBinEntries(i + 1, j + 1))))
               colindx = 0;
         }
      }

      if (colindx === null) {
         ttrect.remove();
         return null;
      }

      const res = {
         name: histo.fName, title: histo.fTitle,
         x: pnt.x, y: pnt.y,
         color1: this.lineatt?.color ?? 'green',
         color2: this.fillatt?.getFillColorAlt('blue') ?? 'blue',
         lines: this.getBinTooltips(i, j), exact: true, menu: true
      };

      if (o.Color)
         res.color2 = this.getHistPalette().getColor(colindx);

      if (pnt.disabled && !this.#projection_kind) {
         ttrect.remove();
         res.changed = true;
      } else {
         if (ttrect.empty()) {
            ttrect = this.appendPath()
                         .attr('class', 'tooltip_bin')
                         .style('pointer-events', 'none')
                         .call(addHighlightStyle);
         }

         let binid = i * 10000 + j, path;

         if (this.#projection_kind) {
            const pwx = this.#projection_widthX || 1, ddx = (pwx - 1) / 2;
            if ((this.#projection_kind.indexOf('X')) >= 0 && (pwx > 1)) {
               if (j2 + ddx >= h.j2) {
                  j2 = Math.min(Math.round(j2 + ddx), h.j2);
                  j1 = Math.max(j2 - pwx, h.j1);
               } else {
                  j1 = Math.max(Math.round(j1 - ddx), h.j1);
                  j2 = Math.min(j1 + pwx, h.j2);
               }
            }
            const pwy = this.#projection_widthY || 1, ddy = (pwy - 1) / 2;
            if ((this.#projection_kind.indexOf('Y')) >= 0 && (pwy > 1)) {
               if (i2 + ddy >= h.i2) {
                  i2 = Math.min(Math.round(i2 + ddy), h.i2);
                  i1 = Math.max(i2 - pwy, h.i1);
               } else {
                  i1 = Math.max(Math.round(i1 - ddy), h.i1);
                  i2 = Math.min(i1 + pwy, h.i2);
               }
            }
         }

         if (is_pol)
            path = h.getBinPath(i, j);
         else if (this.#projection_kind === 'X') {
            x1 = 0;
            x2 = fp.getFrameWidth();
            y1 = h.gry[j2];
            y2 = h.gry[j1];
            binid = j1 * 777 + j2 * 333;
         } else if (this.#projection_kind === 'Y') {
            y1 = 0;
            y2 = fp.getFrameHeight();
            x1 = h.grx[i1];
            x2 = h.grx[i2];
            binid = i1 * 777 + i2 * 333;
         } else if (this.#projection_kind === 'XY') {
            y1 = h.gry[j2];
            y2 = h.gry[j1];
            x1 = h.grx[i1];
            x2 = h.grx[i2];
            binid = i1 * 789 + i2 * 653 + j1 * 12345 + j2 * 654321;
            path = `M${x1},0H${x2}V${y1}H${fp.getFrameWidth()}V${y2}H${x2}V${fp.getFrameHeight()}H${x1}V${y2}H0V${y1}H${x1}Z`;
         }

         res.changed = ttrect.property('current_bin') !== binid;

         if (res.changed) {
            ttrect.attr('d', path || `M${x1},${y1}H${x2}V${y2}H${x1}Z`)
                  .style('opacity', '0.7')
                  .property('current_bin', binid);
         }

         if (this.#projection_kind && res.changed)
            this.redrawProjection(i1, i2, j1, j2);
      }

      if (res.changed) {
         res.user_info = {
            obj: histo, name: histo.fName,
            bin: histo.getBin(i + 1, j + 1),
            cont: binz, binx: i + 1, biny: j + 1,
            grx: pnt.x, gry: pnt.y
         };
      }

      return res;
   }

   /** @summary Checks if it makes sense to zoom inside specified axis range */
   canZoomInside(axis, min, max) {
      const o = this.getOptions();

      if (o.Proj)
         return true;

      // z-scale zooming allowed only if special ignore-palette is not provided
      if (axis === 'z') {
         if (this.mode3d)
            return true;
         if (o.IgnorePalette)
            return false;

         const fp = this.getFramePainter(),
               nlevels = Math.max(2 * gStyle.fNumberContours, 100),
               pad = this.getPadPainter().getRootPad(true),
               logv = pad?.fLogv ?? pad?.fLogz;

         if (!fp || (fp.zmin === fp.zmax))
            return true;

         if (logv && (fp.zmin > 0) && (min > 0))
            return nlevels * Math.log(max / min) > Math.log(fp.zmax / fp.zmin);

         return (fp.zmax - fp.zmin) < (max - min) * nlevels;
      }

      let obj = this.getHisto();
      if (obj)
         obj = (axis === 'y') ? obj.fYaxis : obj.fXaxis;

      return !obj || (obj.FindBin(max, 0.5) - obj.FindBin(min, 0) > 1);
   }

   /** @summary Complete palette drawing */
   completePalette(pp) {
      if (!pp)
         return true;

      const o = this.getOptions();
      pp.$main_painter = this;
      o.Zvert = pp.isPaletteVertical();

      // redraw palette till the end when contours are available
      return pp.drawPave(o.Cjust ? 'cjust' : '');
   }

   /** @summary Performs 2D drawing of histogram
     * @return {Promise} when ready */
   async draw2D(/* reason */) {
      this.clear3DScene();

      const o = this.getOptions(),
            need_palette = o.Zscale && o.canHavePalette() && this.isUseFrame();

      // draw new palette, resize frame if required
      return this.drawColorPalette(need_palette, true, this.#can_move_colz).then(async pp => {
         this.#can_move_colz = undefined;
         let pr;
         if (o.Circular && this.isMainPainter())
            pr = this.drawBinsCircular();
         else if (o.Chord && this.isMainPainter())
            pr = this.drawBinsChord();
         else
            pr = this.drawAxes().then(() => this.draw2DBins());

         return pr.then(() => this.completePalette(pp));
      }).then(() => this.updateFunctions())
        .then(() => this.updateHistTitle())
        .then(() => {
           this.updateStatWebCanvas();
           return this.addInteractivity();
        });
   }

   /** @summary Should performs 3D drawing of histogram
     * @desc Disabled in 2D case. just draw default draw options
     * @return {Promise} when ready */
   async draw3D(reason) {
      console.log('3D drawing is disabled, load ./hist/TH2Painter.mjs');
      return this.draw2D(reason);
   }

   /** @summary Call drawing function depending from 3D mode */
   async callDrawFunc(reason) {
      const main = this.getMainPainter(),
            fp = this.getFramePainter(),
            o = this.getOptions();

      if ((main !== this) && fp && (fp.mode3d !== o.Mode3D))
         this.copyOptionsFrom(main);

      if (!o.Mode3D)
         return this.draw2D(reason);

      return this.draw3D(reason).catch(err => {
         const cp = this.getCanvPainter();
         if (isFunc(cp?.showConsoleError))
            cp.showConsoleError(err);
         else
            console.error('Fail to draw histogram in 3D - back to 2D');
         o.Mode3D = false;
         return this.draw2D(reason);
      });
   }

   /** @summary Redraw histogram */
   async redraw(reason) {
      return this.callDrawFunc(reason);
   }

   /** @summary draw TH2 object in 2D only */
   static async draw(dom, histo, opt) {
      return THistPainter._drawHist(new TH2Painter(dom, histo), opt);
   }

}; // class TH2Painter

/** @summary Text 3d axis visibility
  * @private */
function testAxisVisibility(camera, toplevel, fb = false, bb = false) {
   let top;
   if (toplevel?.children) {
      for (let n = 0; n < toplevel.children.length; ++n) {
         top = toplevel.children[n];
         if (top.axis_draw)
            break;
         top = undefined;
      }
   }

   if (!top)
      return;

   if (!camera) {
      // this is case when axis drawing want to be removed
      toplevel.remove(top);
      return;
   }

   const pos = camera.position;
   let qudrant = 1;
   if ((pos.x < 0) && (pos.y >= 0))
      qudrant = 2;
   else if ((pos.x >= 0) && (pos.y >= 0))
      qudrant = 3;
   else if ((pos.x >= 0) && (pos.y < 0))
      qudrant = 4;

   const testVisible = (id, range) => {
      if (id <= qudrant)
         id += 4;
      return (id > qudrant) && (id < qudrant + range);
   }, handleZoomMesh = obj3d => {
      for (let k = 0; k < obj3d.children?.length; ++k) {
         if (obj3d.children[k].zoom !== undefined)
            obj3d.children[k].zoom_disabled = !obj3d.visible;
      }
   };

   for (let n = 0; n < top.children.length; ++n) {
      const chld = top.children[n];
      if (chld.grid)
         chld.visible = bb && testVisible(chld.grid, 3);
      else if (chld.zid) {
         chld.visible = testVisible(chld.zid, 2);
         handleZoomMesh(chld);
      } else if (chld.xyid) {
         chld.visible = testVisible(chld.xyid, 3);
         handleZoomMesh(chld);
      } else if (chld.xyboxid) {
         let range = 5, shift = 0;
         if (bb && !fb) {
            range = 3;
            shift = -2;
         } else if (fb && !bb)
            range = 3;
         else if (!fb && !bb)
            range = chld.bottom ? 3 : 0;
         chld.visible = testVisible(chld.xyboxid + shift, range);
         if (!chld.visible && chld.bottom && bb)
            chld.visible = testVisible(chld.xyboxid, 3);
      } else if (chld.zboxid) {
         let range = 2, shift = 0;
         if (fb && bb)
            range = 5;
         else if (bb && !fb)
            range = 4;
         else if (!bb && fb) {
            shift = -2;
            range = 4;
         }
         chld.visible = testVisible(chld.zboxid + shift, range);
      }
   }
}


function convertLegoBuf(painter, pos, binsx, binsy) {
   if (painter.options.System === kCARTESIAN)
      return pos;
   const fp = painter.getFramePainter();
   let kx = 1 / fp.size_x3d, ky = 1 / fp.size_y3d;
   if (binsx && binsy) {
      kx *= binsx / (binsx - 1);
      ky *= binsy / (binsy - 1);
   }

   if (painter.options.System === kPOLAR) {
      for (let i = 0; i < pos.length; i += 3) {
         const angle = (1 - pos[i] * kx) * Math.PI,
               radius = 0.5 + 0.5 * pos[i + 1] * ky;

         pos[i] = Math.cos(angle) * radius * fp.size_x3d;
         pos[i + 1] = Math.sin(angle) * radius * fp.size_y3d;
      }
   } else if (painter.options.System === kCYLINDRICAL) {
      for (let i = 0; i < pos.length; i += 3) {
         const angle = (1 - pos[i] * kx) * Math.PI,
               radius = 0.5 + pos[i + 2] / fp.size_z3d / 4;

         pos[i] = Math.cos(angle) * radius * fp.size_x3d;
         pos[i + 2] = (1 + Math.sin(angle) * radius) * fp.size_z3d;
      }
   } else if (painter.options.System === kSPHERICAL) {
      for (let i = 0; i < pos.length; i += 3) {
         const phi = (1 + pos[i] * kx) * Math.PI,
               theta = pos[i + 1] * ky * Math.PI,
               radius = 0.5 + pos[i + 2] / fp.size_z3d / 4;

         pos[i] = radius * Math.cos(theta) * Math.cos(phi) * fp.size_x3d;
         pos[i + 1] = radius * Math.cos(theta) * Math.sin(phi) * fp.size_y3d;
         pos[i + 2] = (1 + radius * Math.sin(theta)) * fp.size_z3d;
      }
   } else if (painter.options.System === kRAPIDITY) {
      for (let i = 0; i < pos.length; i += 3) {
         const phi = (1 - pos[i] * kx) * Math.PI,
               theta = pos[i + 1] * ky * Math.PI,
               radius = 0.5 + pos[i + 2] / fp.size_z3d / 4;

         pos[i] = radius * Math.cos(phi) * fp.size_x3d;
         pos[i + 1] = radius * Math.sin(theta) / Math.cos(theta) * fp.size_y3d / 2;
         pos[i + 2] = (1 + radius * Math.sin(phi)) * fp.size_z3d;
      }
   }

   return pos;
}

function createLegoGeom(painter, positions, normals, binsx, binsy) {
   const geometry = new THREE.BufferGeometry();
   if (painter.options.System === kCARTESIAN) {
      geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
      if (normals)
         geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
      else
         geometry.computeVertexNormals();
   } else {
      convertLegoBuf(painter, positions, binsx, binsy);
      geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
      geometry.computeVertexNormals();
   }

   return geometry;
}

function create3DCamera(fp, orthographic) {
   if (fp.camera) {
      fp.scene.remove(fp.camera);
      disposeThreejsObject(fp.camera);
      delete fp.camera;
   }

   if (orthographic)
      fp.camera = new THREE.OrthographicCamera(-1.3 * fp.size_x3d, 1.3 * fp.size_x3d, 2.3 * fp.size_z3d, -0.7 * fp.size_z3d, 0.001, 40 * fp.size_z3d);
   else
      fp.camera = new THREE.PerspectiveCamera(45, fp.scene_width / fp.scene_height, 1, 40 * fp.size_z3d);

   fp.camera.up.set(0, 0, 1);

   fp.pointLight = new THREE.DirectionalLight(0xffffff, 3);
   fp.pointLight.position.set(fp.size_x3d / 2, fp.size_y3d / 2, fp.size_z3d / 2);
   fp.camera.add(fp.pointLight);
   fp.lookat = new THREE.Vector3(0, 0, orthographic ? 0.3 * fp.size_z3d : 0.8 * fp.size_z3d);
   fp.scene.add(fp.camera);
}

/** @summary Returns camera default position
  * @private */
function getCameraDefaultPosition(fp, first_time) {
   const pad = fp.getPadPainter()?.getRootPad(true),
         kz = fp.camera.isOrthographicCamera ? 1 : 1.4;
   let max3dx = Math.max(0.75 * fp.size_x3d, fp.size_z3d),
       max3dy = Math.max(0.75 * fp.size_y3d, fp.size_z3d),
       pos = null;

   if (first_time) {
      pos = new THREE.Vector3();
      if (max3dx === max3dy)
         pos.set(-1.6 * max3dx, -3.5 * max3dy, kz * fp.size_z3d);
      else if (max3dx > max3dy)
         pos.set(-2 * max3dx, -3.5 * max3dy, kz * fp.size_z3d);
      else
         pos.set(-3.5 * max3dx, -2 * max3dy, kz * fp.size_z3d);
   }

   if (pad && (first_time || !fp.zoomChangedInteractive())) {
      if (Number.isFinite(pad.fTheta) && Number.isFinite(pad.fPhi) && ((pad.fTheta !== fp.camera_Theta) || (pad.fPhi !== fp.camera_Phi))) {
         if (!pos)
            pos = new THREE.Vector3();
         max3dx = 3 * Math.max(fp.size_x3d, fp.size_z3d);
         max3dy = 3 * Math.max(fp.size_y3d, fp.size_z3d);
         const phi = (270 - pad.fPhi) / 180 * Math.PI,
               theta = (pad.fTheta - 10) / 180 * Math.PI;
         pos.set(max3dx * Math.cos(phi) * Math.cos(theta),
                 max3dy * Math.sin(phi) * Math.cos(theta),
                 fp.size_z3d + (kz - 0.9) * (max3dx + max3dy) * Math.sin(theta));
      }
   }

   return pos;
}

/** @summary Set default camera position
  * @private */
function setCameraPosition(fp, first_time) {
   const pos = getCameraDefaultPosition(fp, first_time);

   if (pos) {
      fp.camera.position.copy(pos);
      first_time = true;
   }

   if (first_time)
      fp.camera.lookAt(fp.lookat);

   if (first_time && fp.camera.isOrthographicCamera && fp.scene_width && fp.scene_height) {
      const screen_ratio = fp.scene_width / fp.scene_height,
            szx = fp.camera.right - fp.camera.left,
            szy = fp.camera.top - fp.camera.bottom;

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

   fp.camera.updateProjectionMatrix();
}

function getCameraPosition(fp) {
   const p = fp.camera.position, p0 = fp.lookat,
         dist = p.distanceTo(p0),
         dist_xy = Math.sqrt((p.x - p0.x) ** 2 + (p.y - p0.y) ** 2),
         new_theta = Math.atan2((p.z - p0.z) / dist, dist_xy / dist) / Math.PI * 180,
         new_phi = 270 - Math.atan2((p.y - p0.y) / dist_xy, (p.x - p0.x) / dist_xy) / Math.PI * 180,
         pad = fp.getPadPainter()?.getRootPad(true);

   fp.camera_Phi = new_phi >= 360 ? new_phi - 360 : new_phi;
   fp.camera_Theta = new_theta;

   if (pad && Number.isFinite(fp.camera_Phi) && Number.isFinite(fp.camera_Theta)) {
      pad.fPhi = fp.camera_Phi;
      pad.fTheta = fp.camera_Theta;
   }
}

function create3DControl(fp) {
   fp.control = createOrbitControl(fp, fp.camera, fp.scene, fp.renderer, fp.lookat);

   const frame_painter = fp, obj_painter = fp.getMainPainter();

   if (fp.access3dKind() === constants$1.Embed3D.Embed) {
      // tooltip scaling only need when GL canvas embed into
      const scale = fp.getCanvPainter()?.getPadScale();
      if (scale)
         fp.control.tooltip?.setScale(scale);
   }

   fp.control.processMouseMove = function(intersects) {
      let tip = null, mesh = null, zoom_mesh = null;
      const handle_tooltip = frame_painter.isTooltipAllowed();

      for (let i = 0; i < intersects.length; ++i) {
         if (handle_tooltip && isFunc(intersects[i].object?.tooltip)) {
            tip = intersects[i].object.tooltip(intersects[i]);
            if (tip) {
               mesh = intersects[i].object;
               break;
            }
         } else if (intersects[i].object?.zoom && !zoom_mesh)
            zoom_mesh = intersects[i].object;
      }

      if (tip && !tip.use_itself) {
         const delta_x = 1e-4 * frame_painter.size_x3d,
               delta_y = 1e-4 * frame_painter.size_y3d,
               delta_z = 1e-4 * frame_painter.size_z3d;
         if ((tip.x1 > tip.x2) || (tip.y1 > tip.y2) || (tip.z1 > tip.z2))
            console.warn('check 3D hints coordinates');
         tip.x1 -= delta_x;
         tip.x2 += delta_x;
         tip.y1 -= delta_y;
         tip.y2 += delta_y;
         tip.z1 -= delta_z;
         tip.z2 += delta_z;
      }

      frame_painter.highlightBin3D(tip, mesh);

      if (!tip && zoom_mesh && isFunc(frame_painter.get3dZoomCoord)) {
         let axis_name = zoom_mesh.zoom;
         const pnt = zoom_mesh.globalIntersect(this.raycaster),
               axis_value = frame_painter.get3dZoomCoord(pnt, axis_name);

         if ((axis_name === 'z') && zoom_mesh.use_y_for_z)
            axis_name = 'y';

         return {
            name: axis_name,
            title: 'axis object',
            line: axis_name + ' : ' + frame_painter.axisAsText(axis_name, axis_value),
            only_status: true
         };
      }

      return tip?.lines ? tip : '';
   };

   fp.control.processMouseLeave = function() {
      frame_painter.highlightBin3D(null);
   };

   fp.control.contextMenu = function(pos, intersects) {
      let kind = 'painter', p = obj_painter;
      if (intersects) {
         for (let n = 0; n < intersects.length; ++n) {
            const mesh = intersects[n].object;
            if (mesh.zoom) {
               kind = mesh.zoom;
               p = null;
               break;
            }
            if (isFunc(mesh.painter?.fillContextMenu)) {
               p = mesh.painter;
               break;
            }
         }
      }

      const ofp = obj_painter.getFramePainter();
      if (isFunc(ofp?.showContextMenu))
         ofp.showContextMenu(kind, pos, p);
   };
}

/** @summary Create all necessary components for 3D drawings in frame painter
  * @return {Promise} when render3d !== -1
  * @private */
function create3DScene(render3d, x3dscale, y3dscale, orthographic) {
   if (render3d === -1) {
      if (!this.mode3d)
         return;

      if (!isFunc(this.clear3dCanvas)) {
         console.error(`Strange, why mode3d=${this.mode3d} is configured!`);
         return;
      }

      const res = x3dscale ? this.toplevel : null;
      if (res) {
         this.scene?.remove(res);
         this.toplevel = null;
      }

      testAxisVisibility(null, this.toplevel);

      this.clear3dCanvas();

      disposeThreejsObject(this.scene);
      this.control?.cleanup();

      cleanupRender3D(this.renderer);

      delete this.size_x3d;
      delete this.size_y3d;
      delete this.size_z3d;
      delete this.tooltip_mesh;
      delete this.scene;
      delete this.toplevel;
      delete this.camera;
      delete this.pointLight;
      delete this.renderer;
      delete this.control;
      if (this.render_tmout) {
         clearTimeout(this.render_tmout);
         delete this.render_tmout;
      }

      this.mode3d = false;

      if (this.getG() && !x3dscale)
         this.createFrameG();

      return res;
   }

   this.mode3d = true; // indicate 3d mode as hist painter does

   if ('toplevel' in this) {
      // it is indication that all 3D object created, just replace it with empty
      this.scene.remove(this.toplevel);
      disposeThreejsObject(this.toplevel);
      delete this.tooltip_mesh;
      delete this.toplevel;
      this.control?.hideTooltip();

      const newtop = new THREE.Object3D();
      this.scene.add(newtop);
      this.toplevel = newtop;

      this.resize3D(); // set actual sizes

      setCameraPosition(this, false);

      return Promise.resolve(true);
   }

   render3d = getRender3DKind(render3d, this.isBatchMode());

   assign3DHandler(this);

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

   this.size_z3d = 100;
   this.x3dscale = x3dscale || 1;
   this.y3dscale = y3dscale || 1;
   const xy3d = (sz.height > 10) && (sz.width > 10) ? Math.round(sz.width / sz.height * this.size_z3d) : this.size_z3d;
   this.size_x3d = xy3d * this.x3dscale;
   this.size_y3d = xy3d * this.y3dscale;

   return importThreeJs().then(() => {
      // three.js 3D drawing
      this.scene = new THREE.Scene();
      // scene.fog = new Fog(0xffffff, 500, 3000);

      this.toplevel = new THREE.Object3D();
      this.scene.add(this.toplevel);
      this.scene_width = sz.width;
      this.scene_height = sz.height;
      this.scene_x = sz.x ?? 0;
      this.scene_y = sz.y ?? 0;

      this.camera_Phi = 30;
      this.camera_Theta = 30;

      create3DCamera(this, orthographic);

      setCameraPosition(this, true);

      return createRender3D(this.scene_width, this.scene_height, render3d);
   }).then(r => {
      this.renderer = r;
      if (!r)
         return this;

      this.webgl = r.jsroot_render3d === constants$1.Render3D.WebGL;
      this.add3dCanvas(sz, r.jsroot_dom, this.webgl);

      this.first_render_tm = 0;
      this.enable_highlight = false;

      if (!this.isBatchMode() && this.webgl && !isNodeJs())
         create3DControl(this);

      return this;
   });
}

/** @summary Change camera kind in frame painter
  * @private */
function change3DCamera(orthographic) {
   let has_control = false;
   if (this.control) {
      this.control.cleanup();
      delete this.control;
      has_control = true;
   }

   create3DCamera(this, orthographic);
   setCameraPosition(this, true);

   if (has_control)
      create3DControl(this);

   this.render3D();
}

/** @summary Add 3D mesh to frame painter
  * @private */
function add3DMesh(mesh, painter, the_only) {
   if (!mesh)
      return;
   if (!this.toplevel)
      return console.error('3D objects are not yet created in the frame');
   if (painter && the_only)
      this.remove3DMeshes(painter);
   this.toplevel.add(mesh);
   mesh.painter = painter;
}

/** @summary Returns all 3D meshed for specific
  * @private */
function get3DMeshes(painter) {
   const arr = [];
   if (!painter || !this.toplevel)
      return arr;
   for (let i = 0; i < this.toplevel.children.length; ++i) {
      const mesh = this.toplevel.children[i];
      if (mesh.painter === painter)
         arr.push(mesh);
   }
   return arr;
}

/** @summary Remove 3D meshed for specified painter
  * @private */
function remove3DMeshes(painter) {
   const arr = this.get3DMeshes(painter);
   arr.forEach(mesh => {
      this.toplevel.remove(mesh);
      disposeThreejsObject(mesh);
   });
}

/** @summary call 3D rendering of the frame
  * @param {number} tmout - specifies delay, after which actual rendering will be invoked
  * @desc Timeout used to avoid multiple rendering of the picture when several 3D drawings
  * superimposed with each other.
  * If tmout <= 0, rendering performed immediately
  * If tmout === -1111, immediate rendering with SVG renderer is performed
  * @private */
function render3D(tmout) {
   if (tmout === -1111) {
      // special handling for direct SVG renderer
      const doc = getDocument(),
            rrr = createSVGRenderer(false, 0);
      rrr.setSize(this.scene_width, this.scene_height);
      rrr.render(this.scene, this.camera);
      if (rrr.makeOuterHTML) {
         // use text mode, it is faster
         const d = doc.createElement('div');
         d.innerHTML = rrr.makeOuterHTML();
         return d.childNodes[0];
      }
      return rrr.domElement;
   }

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

   const batch_mode = this.isBatchMode();

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

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

   if (!this.renderer)
      return;

   beforeRender3D(this.renderer);

   const tm1 = new Date();

   testAxisVisibility(this.camera, this.toplevel, this.opt3d?.FrontBox, this.opt3d?.BackBox);

   // do rendering, most consuming time
   this.renderer.render(this.scene, this.camera);

   afterRender3D(this.renderer);

   const tm2 = new Date();

   if (this.first_render_tm === 0) {
      this.first_render_tm = tm2.getTime() - tm1.getTime();
      this.enable_highlight = (this.first_render_tm < 1200) && this.isTooltipAllowed();
      if (this.first_render_tm > 500)
         console.log(`three.js r${THREE.REVISION}, first render tm = ${this.first_render_tm}`);
   } else
      getCameraPosition(this);

   if (this.processRender3D) {
      this.forEachPainter(objp => {
         if (isFunc(objp.handleRender3D))
            objp.handleRender3D();
      }, 'objects');
   }
}

/** @summary Returns assigned render object
  * @private */
function getRenderer() { return this.renderer; }

/** @summary Check is 3D drawing need to be resized
  * @private */
function resize3D() {
   const sz = this.getSizeFor3d(this.access3dKind());

   this.apply3dSize(sz);

   if ((this.scene_width === sz.width) && (this.scene_height === sz.height))
      return false;

   if ((sz.width < 10) || (sz.height < 10))
      return false;

   this.scene_width = sz.width;
   this.scene_height = sz.height;

   this.camera.aspect = this.scene_width / this.scene_height;
   this.camera.updateProjectionMatrix();

   this.renderer.setSize(this.scene_width, this.scene_height);

   const xy3d = (sz.height > 10) && (sz.width > 10) ? Math.round(sz.width / sz.height * this.size_z3d) : this.size_z3d,
         x3d = xy3d * this.x3dscale,
         y3d = xy3d * this.y3dscale;

   if ((Math.abs(x3d - this.size_x3d) > 0.15 * this.size_z3d) || (Math.abs(y3d - this.size_y3d) > 0.15 * this.size_z3d)) {
      this.size_x3d = x3d;
      this.size_y3d = y3d;
      this.control?.position0?.copy(getCameraDefaultPosition(this, true));
      return 1; // indicate significant resize
   }

   return true;
}

/** @summary Highlight bin in frame painter 3D drawing
  * @private */
function highlightBin3D(tip, selfmesh) {
   const want_remove = !tip || (tip.x1 === undefined) || !this.enable_highlight;
   let changed = false, tooltip_mesh = null, changed_self = true, mainp = this.getMainPainter();

   if (!mainp?.provideUserTooltip || !mainp?.hasUserTooltip())
      mainp = null;

   if (this.tooltip_selfmesh) {
      changed_self = (this.tooltip_selfmesh !== selfmesh);
      this.tooltip_selfmesh.material.color = this.tooltip_selfmesh.save_color;
      delete this.tooltip_selfmesh;
      changed = true;
   }

   if (this.tooltip_mesh) {
      tooltip_mesh = this.tooltip_mesh;
      this.toplevel.remove(this.tooltip_mesh);
      delete this.tooltip_mesh;
      changed = true;
   }

   if (want_remove) {
      if (changed) {
         this.render3D();
         mainp?.provideUserTooltip(null);
      }
      return;
   }

   if (tip.use_itself) {
      selfmesh.save_color = selfmesh.material.color;
      selfmesh.material.color = new THREE.Color(tip.color);
      this.tooltip_selfmesh = selfmesh;
      changed = changed_self;
   } else {
      changed = true;

      const indicies = Box3D.Indexes,
            normals = Box3D.Normals,
            vertices = Box3D.Vertices,
            color = new THREE.Color(tip.color ? tip.color : 0xFF0000),
            opacity = tip.opacity || 1;

      let pos, norm;

      if (!tooltip_mesh) {
         pos = new Float32Array(indicies.length * 3);
         norm = new Float32Array(indicies.length * 3);
         const geom = new THREE.BufferGeometry();
         geom.setAttribute('position', new THREE.BufferAttribute(pos, 3));
         geom.setAttribute('normal', new THREE.BufferAttribute(norm, 3));
         const material = new THREE.MeshBasicMaterial({ color, opacity, vertexColors: false });
         tooltip_mesh = new THREE.Mesh(geom, material);
      } else {
         pos = tooltip_mesh.geometry.attributes.position.array;
         tooltip_mesh.geometry.attributes.position.needsUpdate = true;
         tooltip_mesh.material.color = color;
         tooltip_mesh.material.opacity = opacity;
      }

      if (tip.x1 === tip.x2)
         console.warn(`same tip X ${tip.x1} ${tip.x2}`);
      if (tip.y1 === tip.y2)
         console.warn(`same tip Y ${tip.y1} ${tip.y2}`);
      if (tip.z1 === tip.z2)
         tip.z2 = tip.z1 + 0.0001;  // avoid zero faces

      for (let k = 0, nn = -3; k < indicies.length; ++k) {
         const vert = vertices[indicies[k]];
         pos[k * 3] = tip.x1 + vert.x * (tip.x2 - tip.x1);
         pos[k * 3 + 1] = tip.y1 + vert.y * (tip.y2 - tip.y1);
         pos[k * 3 + 2] = tip.z1 + vert.z * (tip.z2 - tip.z1);

         if (norm) {
            if (k % 6 === 0)
               nn += 3;
            norm[k * 3] = normals[nn];
            norm[k * 3 + 1] = normals[nn + 1];
            norm[k * 3 + 2] = normals[nn + 2];
         }
      }
      this.tooltip_mesh = tooltip_mesh;
      this.toplevel.add(tooltip_mesh);

      if (tip.$painter && tip.$painter.options.System !== kCARTESIAN) {
         convertLegoBuf(tip.$painter, pos);
         tooltip_mesh.geometry.computeVertexNormals();
      }
   }

   if (changed)
      this.render3D();

   if (changed && tip.$projection && isFunc(tip.$painter?.redrawProjection))
      tip.$painter.redrawProjection(tip.ix - 1, tip.ix, tip.iy - 1, tip.iy);

   if (changed && mainp?.getObject()) {
      mainp.provideUserTooltip({
         obj: mainp.getObject(), name: mainp.getObject().fName,
         bin: tip.bin, cont: tip.value,
         binx: tip.ix, biny: tip.iy, binz: tip.iz,
         grx: (tip.x1 + tip.x2) / 2, gry: (tip.y1 + tip.y2) / 2, grz: (tip.z1 + tip.z2) / 2
      });
   }
}

/** @summary Set options used for 3D drawings
  * @private */
function set3DOptions(hopt) {
   this.opt3d = hopt;
}

/** @summary Draw axes in 3D mode
  * @private */
function drawXYZ(toplevel, AxisPainter, opts) {
   if (!opts)
      opts = { ndim: 2 };

   if (opts.drawany === false)
      opts.draw = false;
   else
      opts.drawany = true;

   const pad = opts.v7 ? null : this.getPadPainter()?.getRootPad(true);
   let grminx = -this.size_x3d, grmaxx = this.size_x3d,
       grminy = -this.size_y3d, grmaxy = this.size_y3d,
       grminz = 0, grmaxz = 2 * this.size_z3d,
       scalingSize = this.size_z3d,
       xmin = this.xmin, xmax = this.xmax,
       ymin = this.ymin, ymax = this.ymax,
       zmin = this.zmin, zmax = this.zmax,
       y_zoomed = false, z_zoomed = false;

   if (!this.size_z3d) {
      grminx = this.xmin;
      grmaxx = this.xmax;
      grminy = this.ymin;
      grmaxy = this.ymax;
      grminz = this.zmin;
      grmaxz = this.zmax;
      scalingSize = (grmaxz - grminz);
   }

   if (('zoom_xmin' in this) && ('zoom_xmax' in this) && (this.zoom_xmin !== this.zoom_xmax)) {
      xmin = this.zoom_xmin;
      xmax = this.zoom_xmax;
   }
   if (('zoom_ymin' in this) && ('zoom_ymax' in this) && (this.zoom_ymin !== this.zoom_ymax)) {
      ymin = this.zoom_ymin;
      ymax = this.zoom_ymax;
      y_zoomed = true;
   }
   if (('zoom_zmin' in this) && ('zoom_zmax' in this) && (this.zoom_zmin !== this.zoom_zmax)) {
      zmin = this.zoom_zmin;
      zmax = this.zoom_zmax;
      z_zoomed = true;
   }

   if (opts.use_y_for_z) {
      this.zmin = this.ymin;
      this.zmax = this.ymax;
      zmin = ymin;
      zmax = ymax;
      z_zoomed = y_zoomed;
      ymin = 0;
      ymax = 1;
   }

   // z axis range used for lego plot
   this.lego_zmin = zmin;
   this.lego_zmax = zmax;

   // factor 1.1 used in ROOT for lego plots
   if ((opts.zmult !== undefined) && !z_zoomed)
      zmax *= opts.zmult;

   this.x_handle = new AxisPainter(null, this.xaxis);
   if (opts.v7) {
      this.x_handle.setPadPainter(this.getPadPainter());
      this.x_handle.assignSnapId(this.getSnapId());
   } else if (opts.hist_painter)
      this.x_handle.setHistPainter(opts.hist_painter, 'x');

   this.x_handle.configureAxis('xaxis', this.xmin, this.xmax, xmin, xmax, false, [grminx, grmaxx],
                               { log: pad?.fLogx ?? 0, reverse: opts.reverse_x, logcheckmin: true });
   this.x_handle.assignFrameMembers(this, 'x');
   this.x_handle.extractDrawAttributes(scalingSize);

   this.y_handle = new AxisPainter(null, this.yaxis);
   if (opts.v7) {
      this.y_handle.setPadPainter(this.getPadPainter());
      this.y_handle.assignSnapId(this.getSnapId());
   } else if (opts.hist_painter)
      this.y_handle.setHistPainter(opts.hist_painter, 'y');
   this.y_handle.configureAxis('yaxis', this.ymin, this.ymax, ymin, ymax, false, [grminy, grmaxy],
                               { log: pad && !opts.use_y_for_z ? pad.fLogy : 0, reverse: opts.reverse_y, logcheckmin: opts.ndim > 1 });
   this.y_handle.assignFrameMembers(this, 'y');
   this.y_handle.extractDrawAttributes(scalingSize);

   this.z_handle = new AxisPainter(null, this.zaxis);
   if (opts.v7) {
      this.z_handle.setPadPainter(this.getPadPainter());
      this.z_handle.assignSnapId(this.getSnapId());
   } else if (opts.hist_painter)
      this.z_handle.setHistPainter(opts.hist_painter, 'z');
   this.z_handle.configureAxis('zaxis', this.zmin, this.zmax, zmin, zmax, false, [grminz, grmaxz], {
      value_axis: (opts.ndim === 1) || (opts.ndim === 2),
      log: ((opts.use_y_for_z || (opts.ndim === 2)) ? pad?.fLogv : undefined) ?? pad?.fLogz ?? 0,
      reverse: opts.reverse_z, logcheckmin: opts.ndim > 2
   });
   this.z_handle.assignFrameMembers(this, 'z');
   this.z_handle.extractDrawAttributes(scalingSize);

   this.setRootPadRange(pad, true); // set some coordinates typical for 3D projections in ROOT

   const textMaterials = {}, lineMaterials = {},
         xticks = this.x_handle.createTicks(false, true),
         yticks = this.y_handle.createTicks(false, true),
         zticks = this.z_handle.createTicks(false, true);
   let text_scale = 1;

   function getLineMaterial(handle, kind) {
      const col = ((kind === 'ticks') ? handle.ticksColor : handle.lineatt.color) || 'black',
            linewidth = (kind === 'ticks') ? handle.ticksWidth : handle.lineatt.width,
            name = `${col}_${linewidth}`;
      if (!lineMaterials[name])
         lineMaterials[name] = new THREE.LineBasicMaterial(getMaterialArgs(col, { linewidth, vertexColors: false }));
      return lineMaterials[name];
   }

   function getTextMaterial(handle, kind, custom_color) {
      const col = custom_color || ((kind === 'title') ? handle.titleFont?.color : handle.labelsFont?.color) || 'black';
      if (!textMaterials[col])
         textMaterials[col] = new THREE.MeshBasicMaterial(getMaterialArgs(col, { vertexColors: false }));
      return textMaterials[col];
   }

   // main element, where all axis elements are placed
   const top = new THREE.Object3D();
   top.axis_draw = true; // mark element as axis drawing
   toplevel.add(top);

   let ticks = [], lbls = [], maxtextheight = 0, maxtextwidth = 0;
   const center_x = this.x_handle.isCenteredLabels(),
         rotate_x = this.x_handle.isRotateLabels();

   while (xticks.next()) {
      const grx = xticks.grpos;
      let is_major = xticks.kind === 1,
          lbl = this.x_handle.format(xticks.tick, 2);

      if (xticks.last_major()) {
         if (!this.x_handle.fTitle)
            lbl = 'x';
      } else if (lbl === null) {
         is_major = false;
         lbl = '';
      }

      if (is_major && lbl && opts.draw && (!center_x || !xticks.last_major())) {
         const mod = xticks.get_modifier();
         if (mod?.fLabText)
            lbl = mod.fLabText;

         const text3d = createLatexGeometry(this, lbl, this.x_handle.labelsFont.size);
         text3d.computeBoundingBox();
         const draw_width = text3d.boundingBox.max.x - text3d.boundingBox.min.x,
               draw_height = text3d.boundingBox.max.y - text3d.boundingBox.min.y;
         text3d.center = true; // place central

         text3d.offsety = this.x_handle.labelsOffset + (grmaxy - grminy) * 0.005;

         maxtextwidth = Math.max(maxtextwidth, draw_width);
         maxtextheight = Math.max(maxtextheight, draw_height);

         if (mod?.fTextColor)
            text3d.color = this.getColor(mod.fTextColor);
         text3d.grx = grx;
         lbls.push(text3d);

         let space = 0;
         if (!xticks.last_major()) {
            space = Math.abs(xticks.next_major_grpos() - grx);
            if ((draw_width > 0) && (space > 0))
               text_scale = Math.min(text_scale, 0.9 * space / draw_width);
         }
         if (rotate_x)
            text3d.rotate = 1;

         if (center_x) {
            if (!space)
               space = Math.min(grx - grminx, grmaxx - grx);
            text3d.grx += space / 2;
         }
      }

      ticks.push(grx, 0, 0, grx, this.x_handle.ticksSize * (is_major ? -1 : -0.6), 0);
   }

   if (this.x_handle.fTitle && opts.draw) {
      const text3d = createLatexGeometry(this, this.x_handle.fTitle, this.x_handle.titleFont.size);
      text3d.computeBoundingBox();
      text3d.center = this.x_handle.titleCenter;
      text3d.opposite = this.x_handle.titleOpposite;
      text3d.offsety = 1.6 * this.x_handle.titleOffset + (grmaxy - grminy) * 0.005;
      text3d.grx = (grminx + grmaxx) / 2; // default position for centered title
      text3d.kind = 'title';
      if (this.x_handle.isRotateTitle())
         text3d.rotate = 2;

      lbls.push(text3d);
   }

   this.get3dZoomCoord = function(point, kind) {
      // return axis coordinate from intersection point with axis geometry
      const min = this[`scale_${kind}min`], max = this[`scale_${kind}max`];
      let pos = point[kind];

      switch (kind) {
         case 'x': pos = (pos + this.size_x3d) / 2 / this.size_x3d; break;
         case 'y': pos = (pos + this.size_y3d) / 2 / this.size_y3d; break;
         case 'z': pos = pos / 2 / this.size_z3d; break;
      }
      if (this['log' + kind])
         pos = Math.exp(Math.log(min) + pos * (Math.log(max) - Math.log(min)));
      else
         pos = min + pos * (max - min);

      return pos;
   };

   const createZoomMesh = (kind, size_3d, use_y_for_z) => {
      const geom = new THREE.BufferGeometry(), tsz = Math.max(this[kind + '_handle'].ticksSize, 0.005 * size_3d);
      let positions;
      if (kind === 'z')
         positions = new Float32Array([0, 0, 0, tsz * 4, 0, 2 * size_3d, tsz * 4, 0, 0, 0, 0, 0, 0, 0, 2 * size_3d, tsz * 4, 0, 2 * size_3d]);
      else
         positions = new Float32Array([-size_3d, 0, 0, size_3d, -tsz * 4, 0, size_3d, 0, 0, -size_3d, 0, 0, -size_3d, -tsz * 4, 0, size_3d, -tsz * 4, 0]);

      geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
      geom.computeVertexNormals();

      const material = new THREE.MeshBasicMaterial({ transparent: true, vertexColors: false, side: THREE.DoubleSide, opacity: 0 }),
            mesh = new THREE.Mesh(geom, material);
      mesh.zoom = kind;
      mesh.size_3d = size_3d;
      mesh.tsz = tsz;
      mesh.use_y_for_z = use_y_for_z;
      if (kind === 'y')
         mesh.rotateZ(Math.PI / 2).rotateX(Math.PI);

      mesh.v1 = new THREE.Vector3(positions[0], positions[1], positions[2]);
      mesh.v2 = new THREE.Vector3(positions[6], positions[7], positions[8]);
      mesh.v3 = new THREE.Vector3(positions[3], positions[4], positions[5]);

      mesh.globalIntersect = function(raycaster) {
         if (!this.v1 || !this.v2 || !this.v3)
            return undefined;

         const plane = new THREE.Plane();
         plane.setFromCoplanarPoints(this.v1, this.v2, this.v3);
         plane.applyMatrix4(this.matrixWorld);

         const v1 = raycaster.ray.origin.clone(),
               v2 = v1.clone().addScaledVector(raycaster.ray.direction, 1e10),
               pnt = plane.intersectLine(new THREE.Line3(v1, v2), new THREE.Vector3());

         if (!pnt)
            return undefined;

         let min = -this.size_3d, max = this.size_3d;
         if (this.zoom === 'z') {
            min = 0;
            max = 2 * this.size_3d;
         }

         if (pnt[this.zoom] < min)
            pnt[this.zoom] = min;
         else if (pnt[this.zoom] > max)
            pnt[this.zoom] = max;

         return pnt;
      };

      mesh.showSelection = function(pnt1, pnt2) {
         // used to show selection

         let tgtmesh = this.children ? this.children[0] : null, gg;
         if (!pnt1 || !pnt2) {
            if (tgtmesh) {
               this.remove(tgtmesh);
               disposeThreejsObject(tgtmesh);
            }
            return tgtmesh;
         }

         if (!this.geometry)
            return false;

         if (!tgtmesh) {
            gg = this.geometry.clone();
            const pos = gg.getAttribute('position').array;

            // original vertices [0, 2, 1, 0, 3, 2]
            if (this.zoom === 'z')
               pos[6] = pos[3] = pos[15] = this.tsz;
            else
               pos[4] = pos[16] = pos[13] = -this.tsz;
            tgtmesh = new THREE.Mesh(gg, new THREE.MeshBasicMaterial({ color: 0xFF00, side: THREE.DoubleSide, vertexColors: false }));
            this.add(tgtmesh);
         } else
            gg = tgtmesh.geometry;

         const pos = gg.getAttribute('position').array;

         if (this.zoom === 'z') {
            pos[2] = pos[11] = pos[8] = pnt1[this.zoom];
            pos[5] = pos[17] = pos[14] = pnt2[this.zoom];
         } else {
            pos[0] = pos[9] = pos[12] = pnt1[this.zoom];
            pos[6] = pos[3] = pos[15] = pnt2[this.zoom];
         }

         gg.getAttribute('position').needsUpdate = true;

         return true;
      };

      return mesh;
   };

   let xcont = new THREE.Object3D(), xtickslines;
   xcont.position.set(0, grminy, grminz);
   xcont.rotation.x = 1 / 4 * Math.PI;
   xcont.xyid = 2;
   xcont.painter = this.x_handle;

   if (opts.draw) {
      xtickslines = createLineSegments(ticks, getLineMaterial(this.x_handle, 'ticks'));
      xcont.add(xtickslines);
   }

   lbls.forEach(lbl => {
      const dx = lbl.boundingBox.max.x - lbl.boundingBox.min.x,
            dy = lbl.boundingBox.max.y - lbl.boundingBox.min.y,
            w = (lbl.rotate === 1) ? dy : dx,
            posx = lbl.center ? lbl.grx - w / 2 : (lbl.opposite ? grminx : grmaxx - w),
            posy = -text_scale * (lbl.rotate === 1 ? maxtextwidth : maxtextheight) - this.x_handle.ticksSize - lbl.offsety,
            m = new THREE.Matrix4();

      // matrix to swap y and z scales and shift along z to its position
      m.set(text_scale, 0, 0, posx,
            0, text_scale, 0, posy,
            0, 0, 1, 0,
            0, 0, 0, 1);

      const mesh = new THREE.Mesh(lbl, getTextMaterial(this.x_handle, lbl.kind, lbl.color));

      if (lbl.rotate)
         mesh.rotateZ(lbl.rotate * Math.PI / 2);
      if (lbl.rotate === 1)
         mesh.translateY(-dy);
      if (lbl.rotate === 2)
         mesh.translateX(-dx);

      mesh.applyMatrix4(m);
      xcont.add(mesh);
   });

   if (opts.zoom && opts.drawany)
      xcont.add(createZoomMesh('x', this.size_x3d));
   top.add(xcont);

   xcont = new THREE.Object3D();
   xcont.position.set(0, grmaxy, grminz);
   xcont.rotation.x = 3 / 4 * Math.PI;
   xcont.painter = this.x_handle;

   if (opts.draw)
      xcont.add(new THREE.LineSegments(xtickslines.geometry, xtickslines.material));

   lbls.forEach(lbl => {
      const dx = lbl.boundingBox.max.x - lbl.boundingBox.min.x,
            dy = lbl.boundingBox.max.y - lbl.boundingBox.min.y,
            w = (lbl.rotate === 1) ? dy : dx,
            posx = lbl.center ? lbl.grx + w / 2 : (lbl.opposite ? grminx + w : grmaxx),
            posy = -text_scale * (lbl.rotate === 1 ? maxtextwidth : maxtextheight) - this.x_handle.ticksSize - lbl.offsety,
            m = new THREE.Matrix4();

      // matrix to swap y and z scales and shift along z to its position
      m.set(-text_scale, 0, 0, posx,
            0, text_scale, 0, posy,
            0, 0, -1, 0,
            0, 0, 0, 1);

      const mesh = new THREE.Mesh(lbl, getTextMaterial(this.x_handle, lbl.kind, lbl.color));
      if (lbl.rotate)
         mesh.rotateZ(lbl.rotate * Math.PI / 2);
      if (lbl.rotate === 1)
         mesh.translateY(-dy);
      if (lbl.rotate === 2)
         mesh.translateX(-dx);
      mesh.applyMatrix4(m);
      xcont.add(mesh);
   });

   xcont.xyid = 4;
   if (opts.zoom && opts.drawany)
      xcont.add(createZoomMesh('x', this.size_x3d));
   top.add(xcont);

   lbls = [];
   text_scale = 1;
   maxtextwidth = maxtextheight = 0;
   ticks = [];

   const center_y = this.y_handle.isCenteredLabels(),
         rotate_y = this.y_handle.isRotateLabels();

   while (yticks.next()) {
      const gry = yticks.grpos;
      let is_major = (yticks.kind === 1),
          lbl = this.y_handle.format(yticks.tick, 2);

      if (yticks.last_major()) {
         if (!this.y_handle.fTitle)
            lbl = 'y';
      } else if (lbl === null) {
         is_major = false;
         lbl = '';
      }

      if (is_major && lbl && opts.draw && (!center_y || !yticks.last_major())) {
         const mod = yticks.get_modifier();
         if (mod?.fLabText)
            lbl = mod.fLabText;

         const text3d = createLatexGeometry(this, lbl, this.y_handle.labelsFont.size);
         text3d.computeBoundingBox();
         const draw_width = text3d.boundingBox.max.x - text3d.boundingBox.min.x,
               draw_height = text3d.boundingBox.max.y - text3d.boundingBox.min.y;
         text3d.center = true;

         maxtextwidth = Math.max(maxtextwidth, draw_width);
         maxtextheight = Math.max(maxtextheight, draw_height);

         if (mod?.fTextColor)
            text3d.color = this.getColor(mod.fTextColor);
         text3d.gry = gry;
         text3d.offsetx = this.y_handle.labelsOffset + (grmaxx - grminx) * 0.005;
         lbls.push(text3d);

         let space = 0;
         if (!yticks.last_major()) {
            space = Math.abs(yticks.next_major_grpos() - gry);
            if (draw_width > 0)
               text_scale = Math.min(text_scale, 0.9 * space / draw_width);
         }
         if (center_y) {
            if (!space)
               space = Math.min(gry - grminy, grmaxy - gry);
            text3d.gry += space / 2;
         }
         if (rotate_y)
            text3d.rotate = 1;
      }
      ticks.push(0, gry, 0, this.y_handle.ticksSize * (is_major ? -1 : -0.6), gry, 0);
   }

   if (this.y_handle.fTitle && opts.draw) {
      const text3d = createLatexGeometry(this, this.y_handle.fTitle, this.y_handle.titleFont.size);
      text3d.computeBoundingBox();
      text3d.center = this.y_handle.titleCenter;
      text3d.opposite = this.y_handle.titleOpposite;
      text3d.offsetx = 1.6 * this.y_handle.titleOffset + (grmaxx - grminx) * 0.005;
      text3d.gry = (grminy + grmaxy) / 2; // default position for centered title
      text3d.kind = 'title';
      if (this.y_handle.isRotateTitle())
         text3d.rotate = 2;
      lbls.push(text3d);
   }

   if (!opts.use_y_for_z) {
      let yticksline, ycont = new THREE.Object3D();
      ycont.position.set(grminx, 0, grminz);
      ycont.rotation.y = -1 / 4 * Math.PI;
      ycont.painter = this.y_handle;
      if (opts.draw) {
         yticksline = createLineSegments(ticks, getLineMaterial(this.y_handle, 'ticks'));
         ycont.add(yticksline);
      }

      lbls.forEach(lbl => {
         const dx = lbl.boundingBox.max.x - lbl.boundingBox.min.x,
               dy = lbl.boundingBox.max.y - lbl.boundingBox.min.y,
               w = (lbl.rotate === 1) ? dy : dx,
               posx = -text_scale * (lbl.rotate === 1 ? maxtextwidth : maxtextheight) - this.y_handle.ticksSize - lbl.offsetx,
               posy = lbl.center ? lbl.gry + w / 2 : (lbl.opposite ? grminy + w : grmaxy),
               m = new THREE.Matrix4();
         m.set(0, text_scale, 0, posx,
               -text_scale, 0, 0, posy,
               0, 0, 1, 0,
               0, 0, 0, 1);

         const mesh = new THREE.Mesh(lbl, getTextMaterial(this.y_handle, lbl.kind, lbl.color));
         if (lbl.rotate)
            mesh.rotateZ(lbl.rotate * Math.PI / 2);
         if (lbl.rotate === 1)
            mesh.translateY(-dy);
         if (lbl.rotate === 2)
            mesh.translateX(-dx);

         mesh.applyMatrix4(m);
         ycont.add(mesh);
      });

      ycont.xyid = 3;
      if (opts.zoom && opts.drawany)
         ycont.add(createZoomMesh('y', this.size_y3d));
      top.add(ycont);

      ycont = new THREE.Object3D();
      ycont.position.set(grmaxx, 0, grminz);
      ycont.rotation.y = -3 / 4 * Math.PI;
      ycont.painter = this.y_handle;
      if (opts.draw)
         ycont.add(new THREE.LineSegments(yticksline.geometry, yticksline.material));

      lbls.forEach(lbl => {
         const dx = lbl.boundingBox.max.x - lbl.boundingBox.min.x,
               dy = lbl.boundingBox.max.y - lbl.boundingBox.min.y,
               w = (lbl.rotate === 1) ? dy : dx,
               posx = -text_scale * (lbl.rotate === 1 ? maxtextwidth : maxtextheight) - this.y_handle.ticksSize - lbl.offsetx,
               posy = lbl.center ? lbl.gry - w / 2 : (lbl.opposite ? grminy : grmaxy - w),
               m = new THREE.Matrix4();

         m.set(0, text_scale, 0, posx,
               text_scale, 0, 0, posy,
               0, 0, -1, 0,
               0, 0, 0, 1);

         const mesh = new THREE.Mesh(lbl, getTextMaterial(this.y_handle, lbl.kind, lbl.color));
         if (lbl.rotate)
            mesh.rotateZ(lbl.rotate * Math.PI / 2);
         if (lbl.rotate === 1)
            mesh.translateY(-dy);
         if (lbl.rotate === 2)
            mesh.translateX(-dx);

         mesh.applyMatrix4(m);
         ycont.add(mesh);
      });
      ycont.xyid = 1;
      if (opts.zoom && opts.drawany)
         ycont.add(createZoomMesh('y', this.size_y3d));
      top.add(ycont);
   }

   lbls = [];
   text_scale = 1;
   ticks = []; // just array, will be used for the buffer geometry

   let zgridx = null, zgridy = null, lastmajorz = null, maxzlblwidth = 0;

   const center_z = this.z_handle.isCenteredLabels(),
         rotate_z = this.z_handle.isRotateLabels();

   if (this.size_z3d && opts.drawany) {
      zgridx = [];
      zgridy = [];
   }

   while (zticks.next()) {
      const grz = zticks.grpos;
      let is_major = (zticks.kind === 1),
          lbl = this.z_handle.format(zticks.tick, 2);

      if (lbl === null) {
         is_major = false;
         lbl = '';
      }

      if (is_major && lbl && opts.draw && (!center_z || !zticks.last_major())) {
         const mod = zticks.get_modifier();
         if (mod?.fLabText)
            lbl = mod.fLabText;

         const text3d = createLatexGeometry(this, lbl, this.z_handle.labelsFont.size);
         text3d.computeBoundingBox();
         const draw_width = text3d.boundingBox.max.x - text3d.boundingBox.min.x,
               draw_height = text3d.boundingBox.max.y - text3d.boundingBox.min.y;
         text3d.translate(-draw_width, -draw_height / 2, 0);

         if (mod?.fTextColor)
            text3d.color = this.getColor(mod.fTextColor);
         text3d.grz = grz;
         lbls.push(text3d);

         if ((lastmajorz !== null) && (draw_height > 0))
            text_scale = Math.min(text_scale, 0.9 * (grz - lastmajorz) / draw_height);

         maxzlblwidth = Math.max(maxzlblwidth, draw_width);

         lastmajorz = grz;
      }

      // create grid
      if (zgridx && is_major)
         zgridx.push(grminx, 0, grz, grmaxx, 0, grz);

      if (zgridy && is_major)
         zgridy.push(0, grminy, grz, 0, grmaxy, grz);

      ticks.push(0, 0, grz, this.z_handle.ticksSize * (is_major ? 1 : 0.6), 0, grz);
   }

   if (zgridx?.length) {
      const material = new THREE.LineDashedMaterial({ color: this.x_handle.ticksColor, dashSize: 2, gapSize: 2 }),
            lines1 = createLineSegments(zgridx, material);

      lines1.position.set(0, grmaxy, 0);
      lines1.grid = 2; // mark as grid
      lines1.visible = false;
      top.add(lines1);

      const lines2 = new THREE.LineSegments(lines1.geometry, material);
      lines2.position.set(0, grminy, 0);
      lines2.grid = 4; // mark as grid
      lines2.visible = false;
      top.add(lines2);
   }

   if (zgridy?.length) {
      const material = new THREE.LineDashedMaterial({ color: this.y_handle.ticksColor, dashSize: 2, gapSize: 2 }),
            lines1 = createLineSegments(zgridy, material);

      lines1.position.set(grmaxx, 0, 0);
      lines1.grid = 3; // mark as grid
      lines1.visible = false;
      top.add(lines1);

      const lines2 = new THREE.LineSegments(lines1.geometry, material);
      lines2.position.set(grminx, 0, 0);
      lines2.grid = 1; // mark as grid
      lines2.visible = false;
      top.add(lines2);
   }

   const zcont = [], zticksline = opts.draw ? createLineSegments(ticks, getLineMaterial(this.z_handle, 'ticks')) : null;
   for (let n = 0; n < 4; ++n) {
      zcont.push(new THREE.Object3D());

      lbls.forEach((lbl, indx) => {
         const m = new THREE.Matrix4(),
               dx = lbl.boundingBox.max.x - lbl.boundingBox.min.x;

         let grz = lbl.grz;

         if (center_z) {
            if (indx < lbls.length - 1)
               grz = (grz + lbls[indx + 1].grz) / 2;
            else if (indx > 0)
               grz = Math.min(1.5 * grz - lbls[indx - 1].grz * 0.5, grmaxz);
         }

         // matrix to swap y and z scales and shift along z to its position
         m.set(-text_scale, 0, 0, this.z_handle.ticksSize + (grmaxx - grminx) * 0.005 + this.z_handle.labelsOffset,
               0, 0, 1, 0,
               0, text_scale, 0, grz);
         const mesh = new THREE.Mesh(lbl, getTextMaterial(this.z_handle));
         if (rotate_z)
            mesh.rotateZ(-Math.PI / 2).translateX(dx / 2);
         mesh.applyMatrix4(m);
         zcont[n].add(mesh);
      });

      if (this.z_handle.fTitle && opts.draw) {
         const text3d = createLatexGeometry(this, this.z_handle.fTitle, this.z_handle.titleFont.size);
         text3d.computeBoundingBox();
         const dx = text3d.boundingBox.max.x - text3d.boundingBox.min.x,
               dy = text3d.boundingBox.max.y - text3d.boundingBox.min.y,
               rotate = this.z_handle.isRotateTitle(),
               posz = this.z_handle.titleCenter ? (grmaxz + grminz - dx) / 2 : (this.z_handle.titleOpposite ? grminz : grmaxz - dx) + (rotate ? dx : 0),
               m = new THREE.Matrix4();

         m.set(-text_scale, 0, 0, this.z_handle.ticksSize + (grmaxx - grminx) * 0.005 + maxzlblwidth + this.z_handle.titleOffset,
               0, 0, 1, 0,
               0, text_scale, 0, posz);
         const mesh = new THREE.Mesh(text3d, getTextMaterial(this.z_handle, 'title'));
         mesh.rotateZ(Math.PI * (rotate ? 1.5 : 0.5));
         if (rotate)
            mesh.translateY(-dy);

         mesh.applyMatrix4(m);
         zcont[n].add(mesh);
      }

      if (opts.draw && zticksline)
         zcont[n].add(n === 0 ? zticksline : new THREE.LineSegments(zticksline.geometry, zticksline.material));

      if (opts.zoom && opts.drawany)
         zcont[n].add(createZoomMesh('z', this.size_z3d, opts.use_y_for_z));

      zcont[n].zid = n + 2;
      top.add(zcont[n]);
      zcont[n].painter = this.z_handle;
   }

   zcont[0].position.set(grminx, grmaxy, 0);
   zcont[0].rotation.z = 3 / 4 * Math.PI;

   zcont[1].position.set(grmaxx, grmaxy, 0);
   zcont[1].rotation.z = 1 / 4 * Math.PI;

   zcont[2].position.set(grmaxx, grminy, 0);
   zcont[2].rotation.z = -1 / 4 * Math.PI;

   zcont[3].position.set(grminx, grminy, 0);
   zcont[3].rotation.z = -3 / 4 * Math.PI;

   if (!opts.drawany)
      return;

   const linex_material = getLineMaterial(this.x_handle),
         linex_geom = createLineSegments([grminx, 0, 0, grmaxx, 0, 0], linex_material, null, true);
   for (let n = 0; n < 2; ++n) {
      let line = new THREE.LineSegments(linex_geom, linex_material);
      line.position.set(0, grminy, n === 0 ? grminz : grmaxz);
      line.xyboxid = 2;
      line.bottom = (n === 0);
      top.add(line);

      line = new THREE.LineSegments(linex_geom, linex_material);
      line.position.set(0, grmaxy, n === 0 ? grminz : grmaxz);
      line.xyboxid = 4;
      line.bottom = (n === 0);
      top.add(line);
   }

   const liney_material = getLineMaterial(this.y_handle),
         liney_geom = createLineSegments([0, grminy, 0, 0, grmaxy, 0], liney_material, null, true);
   for (let n = 0; n < 2; ++n) {
      let line = new THREE.LineSegments(liney_geom, liney_material);
      line.position.set(grminx, 0, n === 0 ? grminz : grmaxz);
      line.xyboxid = 3;
      line.bottom = n === 0;
      top.add(line);

      line = new THREE.LineSegments(liney_geom, liney_material);
      line.position.set(grmaxx, 0, n === 0 ? grminz : grmaxz);
      line.xyboxid = 1;
      line.bottom = n === 0;
      top.add(line);
   }

   const linez_material = getLineMaterial(this.z_handle),
         linez_geom = createLineSegments([0, 0, grminz, 0, 0, grmaxz], linez_material, null, true);
   for (let n = 0; n < 4; ++n) {
      const line = new THREE.LineSegments(linez_geom, linez_material);
      line.zboxid = zcont[n].zid;
      line.position.copy(zcont[n].position);
      top.add(line);
   }
}


/** @summary Converts 3D coordinate to the pad NDC
  * @private */
function convert3DtoPadNDC(x, y, z) {
   x = this.x_handle.gr(x);
   y = this.y_handle.gr(y);
   z = this.z_handle.gr(z);

   const vector = new THREE.Vector3().set(x, y, z);

   // map to normalized device coordinate (NDC) space
   vector.project(this.camera);

   vector.x = (vector.x + 1) / 2;
   vector.y = (vector.y + 1) / 2;

   const pp = this.getPadPainter(),
         pw = pp?.getPadWidth(),
         ph = pp?.getPadHeight();

   if (pw && ph) {
      vector.x = (this.scene_x + vector.x * this.scene_width) / pw;
      vector.y = (this.scene_y + vector.y * this.scene_height) / ph;
   }

   return vector;
}

/** @summary Assign 3D methods for frame painter
  * @private */
function assignFrame3DMethods(fp) {
   Object.assign(fp, {
      create3DScene, add3DMesh, get3DMeshes, remove3DMeshes, getRenderer,
      render3D, resize3D, change3DCamera, highlightBin3D, set3DOptions, drawXYZ, convert3DtoPadNDC
   });
}


/** @summary Create 3D objects in the frame
  * @private */
async function crete3DFrame(painter, AxisPainterClass, render3d = constants$1.Render3D.None) {
   const fp = painter.getFramePainter(),
         o = painter.getOptions(),
         histo = painter.getHisto(),
         ndim = painter.getDimension();

   assignFrame3DMethods(fp);

   return fp.create3DScene(render3d, o.x3dscale, o.y3dscale, o.Ortho).then(() => {
      fp.setAxesRanges(histo.fXaxis, painter.xmin, painter.xmax, histo.fYaxis, painter.ymin, painter.ymax, histo.fZaxis, painter.zmin, painter.zmax, painter);
      fp.set3DOptions(o);
      fp.drawXYZ(fp.toplevel, AxisPainterClass, {
         ndim,
         use_y_for_z: ndim === 1,
         hist_painter: painter,
         zmult: o.zmult ?? 1,
         zoom: (render3d !== constants$1.Render3D.None) && settings.Zooming,
         draw: o.Axis !== -1,
         drawany: o.isCartesian(),
         reverse_x: o.RevX,
         reverse_y: o.RevY
      });
      return fp;
   });
}

function _meshLegoToolTip(intersect) {
   if ((intersect.faceIndex < 0) || (intersect.faceIndex >= this.face_to_bins_index.length))
      return null;

   const p = this.tip_painter;

   if (!p) {
      console.error('painter for tip handling is not there');
      return null;
   }

   const handle = this.handle,
         fp = p.getFramePainter(),
         histo = p.getHisto(),
         tip = p.get3DToolTip(this.face_to_bins_index[intersect.faceIndex]),
         x1 = Math.min(fp.size_x3d, Math.max(-fp.size_x3d, handle.grx[tip.ix - 1] + handle.xbar1 * (handle.grx[tip.ix] - handle.grx[tip.ix - 1]))),
         x2 = Math.min(fp.size_x3d, Math.max(-fp.size_x3d, handle.grx[tip.ix - 1] + handle.xbar2 * (handle.grx[tip.ix] - handle.grx[tip.ix - 1]))),
         y1 = Math.min(fp.size_y3d, Math.max(-fp.size_y3d, handle.gry[tip.iy - 1] + handle.ybar1 * (handle.gry[tip.iy] - handle.gry[tip.iy - 1]))),
         y2 = Math.min(fp.size_y3d, Math.max(-fp.size_y3d, handle.gry[tip.iy - 1] + handle.ybar2 * (handle.gry[tip.iy] - handle.gry[tip.iy - 1])));

   tip.x1 = Math.min(x1, x2);
   tip.x2 = Math.max(x1, x2);
   tip.y1 = Math.min(y1, y2);
   tip.y2 = Math.max(y1, y2);

   let binz1 = this.baseline, binz2 = tip.value;
   if (histo.$baseh)
      binz1 = histo.$baseh.getBinContent(tip.ix, tip.iy);
   if (binz2 < binz1)
      [binz1, binz2] = [binz2, binz1];

   tip.z1 = fp.grz(Math.max(this.zmin, binz1));
   tip.z2 = fp.grz(Math.min(this.zmax, binz2));

   tip.color = this.tip_color;
   tip.$painter = p;
   tip.$projection = (p.getDimension() === 2) && isFunc(p.isProjection) && p.isProjection();

   return tip;
}

/** @summary Draw histograms in 3D mode
  * @private */
function drawBinsLego(painter, is_v7 = false) {
   if (!painter.draw_content)
      return;

   // Perform TH1/TH2 lego plot with BufferGeometry

   const vertices = Box3D.Vertices,
         indicies = Box3D.Indexes,
         vnormals = Box3D.Normals,
         segments = Box3D.Segments,
         // reduced line segments
         rsegments = [0, 1, 1, 2, 2, 3, 3, 0],
         // reduced vertices
         rvertices = [new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 1, 0), new THREE.Vector3(1, 1, 0), new THREE.Vector3(1, 0, 0)],
         fp = painter.getFramePainter(),
         handle = painter.prepareDraw({ rounding: false, use3d: true, extra: 1 }),
         test_cutg = painter.options.cutg,
         i1 = handle.i1, i2 = handle.i2, j1 = handle.j1, j2 = handle.j2,
         histo = painter.getHisto(),
         basehisto = histo.$baseh,
         split_faces = (painter.options.Lego === 11) || (painter.options.Lego === 13), // split each layer on two parts
         use16indx = (histo.getBin(i2, j2) < 0xFFFF); // if bin ID fit into 16 bit, use smaller arrays for intersect indexes

   if ((i1 >= i2) || (j1 >= j2))
      return;

   let zmin, zmax, i, j, k, vert, binz1, binz2, reduced, nobottom, notop,
       axis_zmin = fp.z_handle.getScaleMin(),
       axis_zmax = fp.z_handle.getScaleMax();

   const getBinContent = (ii, jj, level) => {
      // return bin content in binz1, binz2, reduced flags
      // return true if bin should be displayed

      binz2 = histo.getBinContent(ii + 1, jj + 1);
      if (basehisto)
         binz1 = basehisto.getBinContent(ii + 1, jj + 1);
      else if (painter.options.BaseLine !== false)
         binz1 = painter.options.BaseLine;
      else
         binz1 = painter.options.Zero ? axis_zmin : 0;
      if (binz2 < binz1)
         [binz1, binz2] = [binz2, binz1];

      if ((binz1 >= zmax) || (binz2 < zmin))
         return false;

      if (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(ii + 0.5), histo.fYaxis.GetBinCoord(jj + 0.5)))
         return false;

      reduced = (binz2 === zmin) || (binz1 >= binz2);

      if (!reduced || (level > 0))
         return true;

      if (basehisto)
         return false; // do not draw empty bins on top of other bins

      if (painter.options.Zero || (axis_zmin > 0))
         return true;

      return painter.options.ShowEmpty;
   };

   let levels = [axis_zmin, axis_zmax], palette = null;

   // DRAW ALL CUBES

   if ((painter.options.Lego === 12) || (painter.options.Lego === 14)) {
      // drawing colors levels, axis can not exceed palette

      if (is_v7) {
         palette = fp.getHistPalette();
         painter.createContour(fp, palette, { full_z_range: true });
         levels = palette.getContour();
         axis_zmin = levels.at(0);
         axis_zmax = levels.at(-1);
      } else {
         const cntr = painter.createContour(histo.fContour ? histo.fContour.length : 20, fp.lego_zmin, fp.lego_zmax);
         levels = cntr.arr;
         palette = painter.getHistPalette();
      }
   }

   for (let nlevel = 0; nlevel < levels.length - 1; ++nlevel) {
      zmin = levels[nlevel];
      zmax = levels[nlevel + 1];

      // artificially extend last level of color palette to maximal visible value
      if (palette && (nlevel === levels.length - 2) && zmax < axis_zmax)
         zmax = axis_zmax;

      const grzmin = fp.grz(zmin), grzmax = fp.grz(zmax);
      let numvertices = 0, num2vertices = 0;

      // now calculate size of buffer geometry for boxes

      for (i = i1; i < i2; ++i) {
         for (j = j1; j < j2; ++j) {
            if (!getBinContent(i, j, nlevel))
               continue;

            nobottom = !reduced && (nlevel > 0);
            notop = !reduced && (binz2 > zmax) && (nlevel < levels.length - 2);

            numvertices += (reduced ? 12 : indicies.length);
            if (nobottom)
               numvertices -= 6;
            if (notop)
               numvertices -= 6;

            if (split_faces && !reduced) {
               numvertices -= 12;
               num2vertices += 12;
            }
         }
      }

      const positions = new Float32Array(numvertices * 3),
            normals = new Float32Array(numvertices * 3),
            face_to_bins_index = use16indx ? new Uint16Array(numvertices / 3) : new Uint32Array(numvertices / 3),
            pos2 = (num2vertices === 0) ? null : new Float32Array(num2vertices * 3),
            norm2 = (num2vertices === 0) ? null : new Float32Array(num2vertices * 3),
            face_to_bins_indx2 = (num2vertices === 0) ? null : (use16indx ? new Uint16Array(num2vertices / 3) : new Uint32Array(num2vertices / 3));

      let v = 0, v2 = 0, nn;

      for (i = i1; i < i2; ++i) {
         const x1 = handle.grx[i] + handle.xbar1 * (handle.grx[i + 1] - handle.grx[i]),
               x2 = handle.grx[i] + handle.xbar2 * (handle.grx[i + 1] - handle.grx[i]);
         for (j = j1; j < j2; ++j) {
            if (!getBinContent(i, j, nlevel))
               continue;

            nobottom = !reduced && (nlevel > 0);
            notop = !reduced && (binz2 > zmax) && (nlevel < levels.length - 2);

            const y1 = handle.gry[j] + handle.ybar1 * (handle.gry[j + 1] - handle.gry[j]),
                  y2 = handle.gry[j] + handle.ybar2 * (handle.gry[j + 1] - handle.gry[j]),
                  z1 = (binz1 <= zmin) ? grzmin : fp.grz(binz1),
                  z2 = (binz2 > zmax) ? grzmax : fp.grz(binz2);

            nn = 0; // counter over the normals, each normals correspond to 6 vertices
            k = 0; // counter over vertices

            if (reduced) {
               // we skip all side faces, keep only top and bottom
               nn += 12;
               k += 24;
            }

            const bin_index = histo.getBin(i + 1, j + 1);
            let size = indicies.length;
            if (nobottom)
               size -= 6;

            // array over all vertices of the single bin
            while (k < size) {
               vert = vertices[indicies[k]];

               if (split_faces && (k < 12)) {
                  pos2[v2] = x1 + vert.x * (x2 - x1);
                  pos2[v2 + 1] = y1 + vert.y * (y2 - y1);
                  pos2[v2 + 2] = z1 + vert.z * (z2 - z1);

                  norm2[v2] = vnormals[nn];
                  norm2[v2 + 1] = vnormals[nn + 1];
                  norm2[v2 + 2] = vnormals[nn + 2];
                  if (v2 % 9 === 0)
                     face_to_bins_indx2[v2 / 9] = bin_index; // remember which bin corresponds to the face
                  v2 += 3;
               } else {
                  positions[v] = x1 + vert.x * (x2 - x1);
                  positions[v + 1] = y1 + vert.y * (y2 - y1);
                  positions[v + 2] = z1 + vert.z * (z2 - z1);

                  normals[v] = vnormals[nn];
                  normals[v + 1] = vnormals[nn + 1];
                  normals[v + 2] = vnormals[nn + 2];
                  if (v % 9 === 0)
                     face_to_bins_index[v / 9] = bin_index; // remember which bin corresponds to the face
                  v += 3;
               }

               ++k;

               if (k % 6 === 0) {
                  nn += 3;
                  if (notop && (k === indicies.length - 12)) {
                     k += 6;
                     nn += 3; // jump over no-top indexes
                  }
               }
            }
         }
      }

      const geometry = createLegoGeom(painter, positions, normals);
      let rootcolor = is_v7 ? 3 : histo.fFillColor,
          fcolor = painter.getColor(rootcolor);

      if (palette)
         fcolor = is_v7 ? palette.getColor(nlevel) : palette.calcColor(nlevel, levels.length);
      else if ((painter.options.Lego === 1) || (rootcolor < 2)) {
         rootcolor = 1;
         fcolor = 'white';
      }

      const material = new THREE.MeshBasicMaterial(getMaterialArgs(fcolor, { vertexColors: false })),
            mesh = new THREE.Mesh(geometry, material);

      mesh.face_to_bins_index = face_to_bins_index;
      mesh.tip_painter = painter;
      mesh.zmin = axis_zmin;
      mesh.zmax = axis_zmax;
      mesh.baseline = (painter.options.BaseLine !== false) ? painter.options.BaseLine : (painter.options.Zero ? axis_zmin : 0);
      mesh.tip_color = (rootcolor === 3) ? 0xFF0000 : 0x00FF00;
      mesh.handle = handle;
      mesh.tooltip = _meshLegoToolTip;

      fp.add3DMesh(mesh);

      if (num2vertices > 0) {
         const geom2 = createLegoGeom(painter, pos2, norm2),
               color2 = new THREE.Color(rootcolor < 2 ? 0xFF0000 : rgb(fcolor).darker(0.5).toString()),
               material2 = new THREE.MeshBasicMaterial({ color: color2, vertexColors: false }),
               mesh2 = new THREE.Mesh(geom2, material2);
         mesh2.face_to_bins_index = face_to_bins_indx2;
         mesh2.tip_painter = painter;
         mesh2.handle = mesh.handle;
         mesh2.tooltip = _meshLegoToolTip;
         mesh2.zmin = mesh.zmin;
         mesh2.zmax = mesh.zmax;
         mesh2.baseline = mesh.baseline;
         mesh2.tip_color = mesh.tip_color;

         fp.add3DMesh(mesh2);
      }
   }

   // lego3 or lego4 do not draw border lines
   if (painter.options.Lego > 12)
      return;

   // DRAW LINE BOXES

   let numlinevertices = 0, numsegments = 0;

   zmax = axis_zmax;
   zmin = axis_zmin;

   for (i = i1; i < i2; ++i) {
      for (j = j1; j < j2; ++j) {
         if (!getBinContent(i, j, 0))
            continue;

         // calculate required buffer size for line segments
         numlinevertices += (reduced ? rvertices.length : vertices.length);
         numsegments += (reduced ? rsegments.length : segments.length);
      }
   }

   // On some platforms vertex index required to be Uint16 array
   // While we cannot use index for large vertex list
   // skip index usage at all. It happens for relatively large histograms (100x100 bins)
   const uselineindx = (numlinevertices <= 0xFFF0);

   if (!uselineindx)
      numlinevertices = numsegments * 3;

   const lpositions = new Float32Array(numlinevertices * 3),
         lindicies = uselineindx ? new Uint16Array(numsegments) : null,
         grzmin = fp.grz(axis_zmin),
         grzmax = fp.grz(axis_zmax);
   let ll = 0, ii = 0;

   for (i = i1; i < i2; ++i) {
      const x1 = handle.grx[i] + handle.xbar1 * (handle.grx[i + 1] - handle.grx[i]),
            x2 = handle.grx[i] + handle.xbar2 * (handle.grx[i + 1] - handle.grx[i]);
      for (j = j1; j < j2; ++j) {
         if (!getBinContent(i, j, 0))
            continue;

         const y1 = handle.gry[j] + handle.ybar1 * (handle.gry[j + 1] - handle.gry[j]),
               y2 = handle.gry[j] + handle.ybar2 * (handle.gry[j + 1] - handle.gry[j]),
               z1 = (binz1 <= axis_zmin) ? grzmin : fp.grz(binz1),
               z2 = (binz2 > axis_zmax) ? grzmax : fp.grz(binz2),
               seg = reduced ? rsegments : segments,
               vvv = reduced ? rvertices : vertices;

         if (uselineindx) {
            // array of indices for the lines, to avoid duplication of points
            for (k = 0; k < seg.length; ++k) {
               // intersect_index[ii] = bin_index;
               lindicies[ii++] = ll / 3 + seg[k];
            }

            for (k = 0; k < vvv.length; ++k) {
               vert = vvv[k];
               lpositions[ll] = x1 + vert.x * (x2 - x1);
               lpositions[ll + 1] = y1 + vert.y * (y2 - y1);
               lpositions[ll + 2] = z1 + vert.z * (z2 - z1);
               ll += 3;
            }
         } else {
            // copy only vertex positions
            for (k = 0; k < seg.length; ++k) {
               vert = vvv[seg[k]];
               lpositions[ll] = x1 + vert.x * (x2 - x1);
               lpositions[ll + 1] = y1 + vert.y * (y2 - y1);
               lpositions[ll + 2] = z1 + vert.z * (z2 - z1);
               // intersect_index[ll/3] = bin_index;
               ll += 3;
            }
         }
      }
   }

   // create boxes
   const lcolor = is_v7 ? painter.v7EvalColor('line_color', 'lightblue') : painter.getColor(histo.fLineColor),
         material = new THREE.LineBasicMaterial(getMaterialArgs(lcolor, { linewidth: is_v7 ? painter.v7EvalAttr('line_width', 1) : histo.fLineWidth })),
         line = createLineSegments(convertLegoBuf(painter, lpositions), material, uselineindx ? lindicies : null);

   /*
   line.painter = painter;
   line.intersect_index = intersect_index;
   line.tooltip = function(intersect) {
      if ((intersect.index < 0) || (intersect.index >= this.intersect_index.length))
         return null;
      return this.painter.get3DToolTip(this.intersect_index[intersect.index]);
   }
   */

   fp.add3DMesh(line);
}

function _lineErrToolTip(intersect) {
   const pos = Math.floor(intersect.index / 6);
   if ((pos < 0) || (pos >= this.intersect_index.length))
      return null;
   const p = this.tip_painter,
         histo = p.getHisto(),
         fp = p.getFramePainter(),
         tip = p.get3DToolTip(this.intersect_index[pos]),
         tx1 = Math.min(fp.size_x3d, Math.max(-fp.size_x3d, fp.grx(histo.fXaxis.GetBinLowEdge(tip.ix)))),
         tx2 = Math.min(fp.size_x3d, Math.max(-fp.size_x3d, fp.grx(histo.fXaxis.GetBinLowEdge(tip.ix + 1)))),
         ty1 = Math.min(fp.size_y3d, Math.max(-fp.size_y3d, fp.gry(histo.fYaxis.GetBinLowEdge(tip.iy)))),
         ty2 = Math.min(fp.size_y3d, Math.max(-fp.size_y3d, fp.gry(histo.fYaxis.GetBinLowEdge(tip.iy + 1))));

   tip.x1 = Math.min(tx1, tx2);
   tip.x2 = Math.max(tx1, tx2);
   tip.y1 = Math.min(ty1, ty2);
   tip.y2 = Math.max(ty1, ty2);

   tip.z1 = fp.grz(tip.value - tip.error < this.zmin ? this.zmin : tip.value - tip.error);
   tip.z2 = fp.grz(tip.value + tip.error > this.zmax ? this.zmax : tip.value + tip.error);

   tip.color = this.tip_color;

   return tip;
}

/** @summary Draw TH2 histogram in error mode
  * @private */
function drawBinsError3D(painter, is_v7 = false) {
   const fp = painter.getFramePainter(),
         histo = painter.getHisto(),
         handle = painter.prepareDraw({ rounding: false, use3d: true, extra: 1 }),
         zmin = fp.z_handle.getScaleMin(),
         zmax = fp.z_handle.getScaleMax(),
         test_cutg = painter.options.cutg;
   let i, j, bin, binz, errs, x1, y1, x2, y2, z1, z2,
       nsegments = 0, lpos = null, binindx = null, lindx = 0;

   const check_skip_min = () => {
      // return true if minimal histogram value should be skipped
      if (painter.options.Zero || (zmin > 0))
         return false;
      return !painter.options.ShowEmpty;
   };

   // loop over the points - first loop counts points, second fill arrays
   for (let loop = 0; loop < 2; ++loop) {
      for (i = handle.i1; i < handle.i2; ++i) {
         x1 = handle.grx[i];
         x2 = handle.grx[i + 1];
         for (j = handle.j1; j < handle.j2; ++j) {
            binz = histo.getBinContent(i + 1, j + 1);
            if ((binz < zmin) || (binz > zmax))
               continue;
            if ((binz === zmin) && check_skip_min())
               continue;

            if (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5), histo.fYaxis.GetBinCoord(j + 0.5)))
               continue;

            // just count number of segments
            if (loop === 0) {
               nsegments += 3;
               continue;
            }

            bin = histo.getBin(i + 1, j + 1);
            errs = painter.getBinErrors(histo, bin, binz);
            binindx[lindx / 18] = bin;

            y1 = handle.gry[j];
            y2 = handle.gry[j + 1];

            z1 = fp.grz((binz - errs.low < zmin) ? zmin : binz - errs.low);
            z2 = fp.grz((binz + errs.up > zmax) ? zmax : binz + errs.up);

            lpos[lindx] = x1;
            lpos[lindx + 3] = x2;
            lpos[lindx + 1] = lpos[lindx + 4] = (y1 + y2) / 2;
            lpos[lindx + 2] = lpos[lindx + 5] = (z1 + z2) / 2;
            lindx += 6;

            lpos[lindx] = lpos[lindx + 3] = (x1 + x2) / 2;
            lpos[lindx + 1] = y1;
            lpos[lindx + 4] = y2;
            lpos[lindx + 2] = lpos[lindx + 5] = (z1 + z2) / 2;
            lindx += 6;

            lpos[lindx] = lpos[lindx + 3] = (x1 + x2) / 2;
            lpos[lindx + 1] = lpos[lindx + 4] = (y1 + y2) / 2;
            lpos[lindx + 2] = z1;
            lpos[lindx + 5] = z2;
            lindx += 6;
         }
      }

      if (loop === 0) {
         if (nsegments === 0)
            return;
         lpos = new Float32Array(nsegments * 6);
         binindx = new Int32Array(nsegments / 3);
      }
   }

   // create lines
   const lcolor = is_v7 ? painter.v7EvalColor('line_color', 'lightblue') : painter.getColor(histo.fLineColor),
         material = new THREE.LineBasicMaterial(getMaterialArgs(lcolor, { linewidth: is_v7 ? painter.v7EvalAttr('line_width', 1) : histo.fLineWidth })),
         line = createLineSegments(lpos, material);

   line.tip_painter = painter;
   line.intersect_index = binindx;
   line.zmin = zmin;
   line.zmax = zmax;
   line.tip_color = (histo.fLineColor === 3) ? 0xFF0000 : 0x00FF00;
   line.tooltip = _lineErrToolTip;

   fp.add3DMesh(line);
}

/** @summary Draw TH2 as 3D contour plot
  * @private */
function drawBinsContour3D(painter, realz = false, is_v7 = false) {
   // for contour plots one requires handle with full range
   const fp = painter.getFramePainter(),
         handle = painter.prepareDraw({ rounding: false, use3d: true, extra: 100, middle: 0 }),
         histo = painter.getHisto(), // get levels
         levels = painter.getContourLevels(), // init contour if not exists
         palette = painter.getHistPalette(),
         pnts = [];
   let layerz = 2 * fp.size_z3d;

   buildHist2dContour(histo, handle, levels, palette, (colindx, xp, yp, iminus, iplus, ilevel) => {
      // ignore less than three points
      if (iplus - iminus < 3)
         return;

      if (realz) {
         layerz = fp.grz(levels[ilevel]);
         if ((layerz < 0) || (layerz > 2 * fp.size_z3d))
            return;
      }

      for (let i = iminus; i < iplus; ++i) {
         pnts.push(xp[i], yp[i], layerz);
         pnts.push(xp[i + 1], yp[i + 1], layerz);
      }
   });

   const lines = createLineSegments(pnts, create3DLineMaterial(painter, is_v7 ? 'line_' : histo));
   fp.add3DMesh(lines);
}

/** @summary Draw TH2 histograms in surf mode
  * @private */
function drawBinsSurf3D(painter, is_v7 = false) {
   const histo = painter.getHisto(),
         fp = painter.getFramePainter(),
         axis_zmin = fp.z_handle.getScaleMin(),
         main_grz = !fp.logz ? fp.grz : value => { return (value < axis_zmin) ? -0.1 : fp.grz(value); },
         main_grz_min = 0, main_grz_max = 2 * fp.size_z3d;

   let handle = painter.prepareDraw({
      rounding: false, use3d: true, extra: 1, middle: 0.5,
      cutg: isFunc(painter.options?.cutg?.IsInside) ? painter.options?.cutg : null
   });
   if ((handle.i2 - handle.i1 < 2) || (handle.j2 - handle.j1 < 2))
      return;

   let ilevels = null, levels = null, palette = null;

   handle.dolines = true;

   if (is_v7) {
      let need_palette = 0;
      switch (painter.options.Surf) {
         case 11:
            need_palette = 2;
            break;
         case 12:
         case 15: // make surf5 same as surf2
         case 17:
            need_palette = 2;
            handle.dolines = false;
            break;
         case 14:
            handle.dolines = false;
            handle.donormals = true;
            break;
         case 16:
            need_palette = 1;
            handle.dogrid = true;
            handle.dolines = false;
            break;
         default:
            ilevels = fp.z_handle.createTicks(true);
            handle.dogrid = true;
            break;
      }

      if (need_palette > 0) {
         palette = fp.getHistPalette();
         if (need_palette === 2)
            painter.createContour(fp, palette, { full_z_range: true });
         ilevels = palette.getContour();
      }
   } else {
      switch (painter.options.Surf) {
         case 11:
            ilevels = painter.getContourLevels();
            palette = painter.getHistPalette();
            break;
         case 12:
         case 15: // make surf5 same as surf2
         case 17:
            ilevels = painter.getContourLevels();
            palette = painter.getHistPalette();
            handle.dolines = false;
            break;
         case 14:
            handle.dolines = false;
            handle.donormals = true;
            break;
         case 16:
            ilevels = painter.getContourLevels();
            handle.dogrid = true;
            handle.dolines = false;
            break;
         default:
            ilevels = fp.z_handle.createTicks(true);
            handle.dogrid = true;
            break;
      }
   }

   if (ilevels) {
      // recalculate levels into graphical coordinates
      levels = new Float32Array(ilevels.length);
      for (let ll = 0; ll < ilevels.length; ++ll)
         levels[ll] = main_grz(ilevels[ll]);
   } else
      levels = [main_grz_min, main_grz_max]; // just cut top/bottom parts


   handle.grz = main_grz;
   handle.grz_min = main_grz_min;
   handle.grz_max = main_grz_max;

   buildSurf3D(histo, handle, ilevels, (lvl, pos, normindx) => {
      const geometry = createLegoGeom(painter, pos, null, handle.i2 - handle.i1, handle.j2 - handle.j1),
            normals = geometry.getAttribute('normal').array;

      // recalculate normals
      if (handle.donormals && (lvl === 1)) {
         for (let ii = handle.i1; ii < handle.i2; ++ii) {
            for (let jj = handle.j1; jj < handle.j2; ++jj) {
               const bin = ((ii - handle.i1) * (handle.j2 - handle.j1) + (jj - handle.j1)) * 8;

               if (normindx[bin] === -1)
                  continue; // nothing there

               const beg = (normindx[bin] >= 0) ? bin : bin + 9 + normindx[bin],
                     end = bin + 8;
               let sumx = 0, sumy = 0, sumz = 0;

               for (let kk = beg; kk < end; ++kk) {
                  const indx = normindx[kk];
                  if (indx < 0)
                     return console.error('FAILURE in NORMALS RECALCULATIONS');
                  sumx += normals[indx];
                  sumy += normals[indx + 1];
                  sumz += normals[indx + 2];
               }

               sumx /= end - beg;
               sumy /= end - beg;
               sumz /= end - beg;

               for (let kk = beg; kk < end; ++kk) {
                  const indx = normindx[kk];
                  normals[indx] = sumx;
                  normals[indx + 1] = sumy;
                  normals[indx + 2] = sumz;
               }
            }
         }
      }

      let color, material;
      if (is_v7)
         color = palette?.getColor(lvl - 1) ?? painter.getColor(5);
      else if (palette)
         color = palette.calcColor(lvl, levels.length);
      else {
         const indx = painter.options.histoFillColor || histo.fFillColor;
         if (painter.options.Surf === 13)
            color = 'white';
         else if (painter.options.Surf === 14)
            color = indx > 1 ? painter.getColor(indx) : 'grey';
         else
            color = indx > 1 ? painter.getColor(indx) : 'white';
      }

      if (!color)
         color = 'white';
      if (painter.options.Surf === 14)
         material = new THREE.MeshLambertMaterial(getMaterialArgs(color, { side: THREE.DoubleSide, vertexColors: false }));
      else
         material = new THREE.MeshBasicMaterial(getMaterialArgs(color, { side: THREE.DoubleSide, vertexColors: false }));

      const mesh = new THREE.Mesh(geometry, material);

      fp.add3DMesh(mesh);

      mesh.painter = painter; // to let use it with context menu
   }, (isgrid, lpos) => {
      const color = painter.getColor(histo.fLineColor) ?? 'white';
      let material;

      if (isgrid) {
         material = (painter.options.Surf === 1)
                      ? new THREE.LineDashedMaterial({ color: 0x0, dashSize: 2, gapSize: 2 })
                      : new THREE.LineBasicMaterial(getMaterialArgs(color));
      } else
         material = new THREE.LineBasicMaterial(getMaterialArgs(color, { linewidth: histo.fLineWidth }));


      const line = createLineSegments(convertLegoBuf(painter, lpos, handle.i2 - handle.i1, handle.j2 - handle.j1), material);
      line.painter = painter;
      fp.add3DMesh(line);
   });

   if (painter.options.Surf === 17)
      drawBinsContour3D(painter, false, is_v7);

   if (painter.options.Surf === 13) {
      handle = painter.prepareDraw({ rounding: false, use3d: true, extra: 100, middle: 0 });

      // get levels
      const levels2 = painter.getContourLevels(), // init contour
            palette2 = painter.getHistPalette();
      let lastcolindx = -1, layerz = main_grz_max;

      buildHist2dContour(histo, handle, levels2, palette2, (colindx, xp, yp, iminus, iplus) => {
         // no need for duplicated point
         if ((xp[iplus] === xp[iminus]) && (yp[iplus] === yp[iminus]))
            iplus--;

         // ignore less than three points
         if (iplus - iminus < 3)
            return;

         const pnts = [];

         for (let i = iminus; i <= iplus; ++i) {
            if ((i === iminus) || (xp[i] !== xp[i - 1]) || (yp[i] !== yp[i - 1]))
               pnts.push(new THREE.Vector2(xp[i], yp[i]));
         }

         const faces = pnts.length < 3 ? null : THREE.ShapeUtils.triangulateShape(pnts, []);

         if (!faces?.length)
            return;

         if ((lastcolindx < 0) || (lastcolindx !== colindx)) {
            lastcolindx = colindx;
            layerz += 5e-5 * main_grz_max; // change layers Z
         }

         const pos = new Float32Array(faces.length * 9),
               norm = new Float32Array(faces.length * 9);
         let indx = 0;

         for (let n = 0; n < faces.length; ++n) {
            const face = faces[n];
            for (let v = 0; v < 3; ++v) {
               const pnt = pnts[face[v]];
               pos[indx] = pnt.x;
               pos[indx + 1] = pnt.y;
               pos[indx + 2] = layerz;
               norm[indx] = 0;
               norm[indx + 1] = 0;
               norm[indx + 2] = 1;

               indx += 3;
            }
         }

         const geometry = createLegoGeom(painter, pos, norm, handle.i2 - handle.i1, handle.j2 - handle.j1),
               material = new THREE.MeshBasicMaterial(getMaterialArgs(palette2.getColor(colindx), { side: THREE.DoubleSide, opacity: 0.5, vertexColors: false })),
               mesh = new THREE.Mesh(geometry, material);
         mesh.painter = painter;
         fp.add3DMesh(mesh);
      });
   }
}

/** @summary Assign `evalPar` function for TF1 object
  * @private */

function proivdeEvalPar(obj, check_save) {
   obj.$math = jsroot_math;

   let _func = obj.fTitle, isformula = false, pprefix = '[';
   if (_func === 'gaus')
      _func = 'gaus(0)';
   if (isStr(obj.fFormula?.fFormula)) {
      if (obj.fFormula.fFormula.indexOf('[](double*x,double*p)') === 0) {
         isformula = true;
         pprefix = 'p[';
         _func = obj.fFormula.fFormula.slice(21);
      } else {
         _func = obj.fFormula.fFormula;
         pprefix = '[p';
      }

      if (obj.fFormula.fClingParameters && obj.fFormula.fParams) {
         obj.fFormula.fParams.forEach(pair => {
            const regex = new RegExp(`(\\[${pair.first}\\])`, 'g'),
                  parvalue = obj.fFormula.fClingParameters[pair.second];
            _func = _func.replace(regex, (parvalue < 0) ? `(${parvalue})` : parvalue);
         });
      }
   }

   if (!_func)
      return !check_save || (obj.fSave?.length > 2);

   obj.formulas?.forEach(entry => {
      _func = _func.replaceAll(entry.fName, entry.fTitle);
   });

   _func = _func.replace(/\b(TMath::SinH)\b/g, 'Math.sinh')
                .replace(/\b(TMath::CosH)\b/g, 'Math.cosh')
                .replace(/\b(TMath::TanH)\b/g, 'Math.tanh')
                .replace(/\b(TMath::ASinH)\b/g, 'Math.asinh')
                .replace(/\b(TMath::ACosH)\b/g, 'Math.acosh')
                .replace(/\b(TMath::ATanH)\b/g, 'Math.atanh')
                .replace(/\b(TMath::ASin)\b/g, 'Math.asin')
                .replace(/\b(TMath::ACos)\b/g, 'Math.acos')
                .replace(/\b(TMath::Atan)\b/g, 'Math.atan')
                .replace(/\b(TMath::ATan2)\b/g, 'Math.atan2')
                .replace(/\b(sin|SIN|TMath::Sin)\b/g, 'Math.sin')
                .replace(/\b(cos|COS|TMath::Cos)\b/g, 'Math.cos')
                .replace(/\b(tan|TAN|TMath::Tan)\b/g, 'Math.tan')
                .replace(/\b(exp|EXP|TMath::Exp)\b/g, 'Math.exp')
                .replace(/\b(log|LOG|TMath::Log)\b/g, 'Math.log')
                .replace(/\b(log10|LOG10|TMath::Log10)\b/g, 'Math.log10')
                .replace(/\b(pow|POW|TMath::Power)\b/g, 'Math.pow')
                .replace(/\b(pi|PI)\b/g, 'Math.PI')
                .replace(/\b(abs|ABS|TMath::Abs)\b/g, 'Math.abs')
                .replace(/\bsqrt\(/g, 'Math.sqrt(')
                .replace(/\bxygaus\(/g, 'this.$math.gausxy(this, x, y, ')
                .replace(/\bgaus\(/g, 'this.$math.gaus(this, x, ')
                .replace(/\bgausn\(/g, 'this.$math.gausn(this, x, ')
                .replace(/\bexpo\(/g, 'this.$math.expo(this, x, ')
                .replace(/\blandau\(/g, 'this.$math.landau(this, x, ')
                .replace(/\blandaun\(/g, 'this.$math.landaun(this, x, ')
                .replace(/\b(TMath::|ROOT::Math::)/g, 'this.$math.');

   if (_func.match(/^pol[0-9]$/) && (parseInt(_func[3]) === obj.fNpar - 1)) {
      _func = '[0]';
      for (let k = 1; k < obj.fNpar; ++k)
         _func += ` + [${k}] * ` + ((k === 1) ? 'x' : `Math.pow(x,${k})`);
   }

   if (_func.match(/^chebyshev[0-9]$/) && (parseInt(_func[9]) === obj.fNpar - 1)) {
      _func = `this.$math.ChebyshevN(${obj.fNpar - 1}, x, `;
      for (let k = 0; k < obj.fNpar; ++k)
         _func += (k === 0 ? '[' : ', ') + `[${k}]`;
      _func += '])';
   }

   for (let i = 0; i < obj.fNpar; ++i)
      _func = _func.replaceAll(pprefix + i + ']', `(${obj.GetParValue(i)})`);

   for (let n = 2; n < 10; ++n)
      _func = _func.replaceAll(`x^${n}`, `Math.pow(x,${n})`);

   if (isformula) {
      _func = _func.replace(/x\[0\]/g, 'x');
      if (obj._typename === clTF3) {
         _func = _func.replace(/x\[1\]/g, 'y');
         _func = _func.replace(/x\[2\]/g, 'z');
         obj.evalPar = new Function('x', 'y', 'z', _func).bind(obj);
      } else if (obj._typename === clTF2) {
         _func = _func.replace(/x\[1\]/g, 'y');
         obj.evalPar = new Function('x', 'y', _func).bind(obj);
      } else
         obj.evalPar = new Function('x', _func).bind(obj);
   } else if (obj._typename === clTF3)
      obj.evalPar = new Function('x', 'y', 'z', 'return ' + _func).bind(obj);
   else if (obj._typename === clTF2)
      obj.evalPar = new Function('x', 'y', 'return ' + _func).bind(obj);
   else
      obj.evalPar = new Function('x', 'return ' + _func).bind(obj);

   return true;
}


/** @summary Get interpolation in saved buffer
  * @desc Several checks must be done before function can be used
  * @private */
function _getTF1Save(func, x) {
   const np = func.fSave.length - 3,
         xmin = func.fSave[np + 1],
         xmax = func.fSave[np + 2],
         dx = (xmax - xmin) / np;
   if (x < xmin)
      return func.fSave[0];
   if (x > xmax)
      return func.fSave[np];

   const bin = Math.min(np - 1, Math.floor((x - xmin) / dx));
   let xlow = xmin + bin * dx,
       xup = xlow + dx,
       ylow = func.fSave[bin],
       yup = func.fSave[bin + 1];

   if (!Number.isFinite(ylow) && (bin < np - 1)) {
      xlow += dx;
      xup += dx;
      ylow = yup;
      yup = func.fSave[bin + 2];
   } else if (!Number.isFinite(yup) && (bin > 0)) {
      xup -= dx;
      xlow -= dx;
      yup = ylow;
      ylow = func.fSave[bin - 1];
   }

   return ((xup * ylow - xlow * yup) + x * (yup - ylow)) / dx;
}

/** @summary Provide TF1 value
  * @desc First try evaluate, if not possible - check saved buffer
  * @private */
function getTF1Value(func, x, skip_eval = undefined) {
   if (!func)
      return 0;

   let iserr = false;

   if (!skip_eval && !func.evalPar) {
      try {
         if (!proivdeEvalPar(func))
            iserr = true;
      } catch {
         iserr = true;
      }
   }

   if (func.evalPar && !iserr) {
      try {
         return func.evalPar(x);
      } catch {
         /* eslint-disable-next-line  no-useless-assignment */
         iserr = true;
      }
   }

   const np = func.fSave.length - 3;
   return (np < 2) || (func.fSave[np + 1] === func.fSave[np + 2]) ? 0 : _getTF1Save(func, x);
}

const PadDrawOptions = ['LOGXY', 'LOGX', 'LOGY', 'LOGZ', 'LOGV', 'LOG', 'LOG2X', 'LOG2Y', 'LOG2',
                        'LNX', 'LNY', 'LN', 'GRIDXY', 'GRIDX', 'GRIDY', 'TICKXY', 'TICKX', 'TICKY', 'TICKZ', 'FB', 'GRAYSCALE'];

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

let TH1Painter$2 = class TH1Painter extends THistPainter {

   /** @summary Returns histogram
     * @desc Also assigns custom getBinContent method for TProfile if PROJX options specified */
   getHisto() {
      const histo = super.getHisto();
      if (histo?._typename === clTProfile) {
         if (!histo.$getBinContent)
            histo.$getBinContent = histo.getBinContent;
         switch (this.getOptions().ProfileProj) {
            case 'B':
               histo.getBinContent = histo.getBinEntries;
               break;
            case 'C=E':
               histo.getBinContent = histo.getBinError;
               break;
            case 'W':
               histo.getBinContent = function(i) { return this.$getBinContent(i) * this.getBinEntries(i); };
               break;
            default:
               histo.getBinContent = histo.$getBinContent;
               break;
         }
      }
      return histo;
   }

   /** @summary Convert TH1K into normal binned histogram */
   convertTH1K() {
      const histo = this.getObject();
      if (histo.fReady)
         return;

      const arr = histo.fArray, entries = histo.fEntries; // array of values
      histo.fNcells = histo.fXaxis.fNbins + 2;
      histo.fArray = new Float64Array(histo.fNcells).fill(0);
      for (let n = 0; n < histo.fNIn; ++n)
         histo.Fill(arr[n]);
      histo.fReady = 1;
      histo.fEntries = entries;
   }

   /** @summary Scan content of 1-D histogram
     * @desc Detect min/max values for x and y axis
     * @param {boolean} when_axis_changed - true when zooming was changed, some checks may be skipped */
   scanContent(when_axis_changed) {
      if (when_axis_changed && !this.nbinsx)
         when_axis_changed = false;

      if (this.isTH1K())
         this.convertTH1K();

      const histo = this.getHisto(),
            o = this.getOptions();

      if (!when_axis_changed)
         this.extractAxesProperties(1);

      const left = this.getSelectIndex('x', 'left'),
            right = this.getSelectIndex('x', 'right'),
            pad_logy = this.getPadPainter()?.getPadLog(o.swap_xy() ? 'x' : 'y'),
            f1 = o.Func ? this.findFunction(clTF1) : null;

      if (when_axis_changed && (left === this.scan_xleft) && (right === this.scan_xright))
         return;

      // Paint histogram axis only
      this.draw_content = !(o.Axis > 0);

      this.scan_xleft = left;
      this.scan_xright = right;

      const is_profile = this.isTProfile(),
            imin = Math.min(0, left),
            imax = Math.max(this.nbinsx, right);
      let hmin = 0, hmin_nz = 0, hmax = 0, hsum = 0, first = true, value, errs = { low: 0, up: 0 };

      for (let i = imin; i < imax; ++i) {
         value = histo.getBinContent(i + 1);
         hsum += is_profile ? histo.fBinEntries[i + 1] : value;

         if ((i < left) || (i >= right))
            continue;

         if ((value > 0) && ((hmin_nz === 0) || (value < hmin_nz)))
            hmin_nz = value;

         if (first) {
            hmin = hmax = value;
            first = false;
         }

         if (o.Error)
            errs = this.getBinErrors(histo, i + 1, value);

         hmin = Math.min(hmin, value - errs.low);
         hmax = Math.max(hmax, value + errs.up);

         if (f1) {
            // similar code as in THistPainter, line 7196
            const x = histo.fXaxis.GetBinCenter(i + 1),
                  v = getTF1Value(f1, x);
            if (v !== undefined) {
               hmax = Math.max(hmax, v);
               if (pad_logy && (value > 0) && (v > 0.3 * value))
                  hmin_nz = Math.min(hmin_nz, v);
            }
         }
      }

      // account overflow/underflow bins
      if (is_profile)
         hsum += histo.fBinEntries[0] + histo.fBinEntries[this.nbinsx + 1];
      else
         hsum += histo.getBinContent(0) + histo.getBinContent(this.nbinsx + 1);

      this.stat_entries = hsum;

      this.hmin = hmin;
      this.hmax = hmax;

      // this.ymin_nz = hmin_nz; // value can be used to show optimal log scale

      if ((this.nbinsx === 0) || ((Math.abs(hmin) < 1e-300) && (Math.abs(hmax) < 1e-300)))
         this.draw_content = false;

      let set_zoom = false;

      if (this.draw_content || (this.isMainPainter() && (o.Axis > 0) && !o.ohmin && !o.ohmax && (histo.fMinimum === kNoZoom) && (histo.fMaximum === kNoZoom))) {
         if (hmin >= hmax) {
            if (hmin === 0) {
               this.ymin = 0;
               this.ymax = 1;
            } else if (hmin < 0) {
               this.ymin = 2 * hmin;
               this.ymax = 0;
            } else {
               this.ymin = 0;
               this.ymax = hmin * 2;
            }
         } else if (pad_logy) {
            this.ymin = (hmin_nz || hmin) * 0.5;
            this.ymax = hmax * 2 * (0.9 / 0.95);
         } else {
            this.ymin = hmin;
            this.ymax = hmax;
         }
      }

      hmin = o.minimum;
      hmax = o.maximum;

      if ((hmin === hmax) && (hmin !== kNoZoom)) {
         if (hmin < 0) {
            hmin *= 2;
            hmax = 0;
         } else {
            hmin = 0;
            hmax = 2 * hmax || 1;
         }
      }

      let fix_min = false, fix_max = false;

      if (o.ohmin && o.ohmax && !this.draw_content) {
         // case of hstack drawing, zooming allowed only when flag is provided

         if (o.zoom_min_max) {
            if ((hmin !== kNoZoom) && (hmin <= this.ymin))
               hmin = kNoZoom;
            if ((hmax !== kNoZoom) && (hmax >= this.ymax))
               hmax = kNoZoom;
            set_zoom = true;
         } else
            hmin = hmax = kNoZoom;
      } else if ((hmin !== kNoZoom) && (hmax !== kNoZoom) && !this.draw_content &&
          ((this.ymin === this.ymax) || (this.ymin > hmin) || (this.ymax < hmax))) {
         // often appears with TF1 painter where Y range is not set properly
         this.ymin = hmin;
         this.ymax = hmax;
         fix_min = fix_max = true;
      } else {
         if (hmin !== kNoZoom) {
            fix_min = true;
            if (hmin < this.ymin)
               this.ymin = hmin;
            set_zoom = true;
         }
         if (hmax !== kNoZoom) {
            fix_max = true;
            if (hmax > this.ymax)
               this.ymax = hmax;
            set_zoom = true;
         }
      }

      // final adjustment like in THistPainter.cxx line 7309
      if (!o.exact_values_range() && !pad_logy) {
         if (!fix_min) {
            if ((o.BaseLine !== false) && (this.ymin >= 0))
               this.ymin = 0;
            else {
               const positive = (this.ymin >= 0);
               this.ymin -= gStyle.fHistTopMargin * (this.ymax - this.ymin);
               if (positive && (this.ymin < 0))
                  this.ymin = 0;
            }
         }
         if (!fix_max)
            this.ymax += gStyle.fHistTopMargin * (this.ymax - this.ymin);
      }

      // always set zoom when hmin/hmax is configured
      // fMinimum/fMaximum values is a way how ROOT handles Y scale zooming for TH1

      if (!when_axis_changed) {
         if (set_zoom && ((hmin !== kNoZoom) || (hmax !== kNoZoom))) {
            this.zoom_ymin = (hmin === kNoZoom) ? this.ymin : hmin;
            this.zoom_ymax = (hmax === kNoZoom) ? this.ymax : hmax;
         } else {
            delete this.zoom_ymin;
            delete this.zoom_ymax;
         }
      }
   }

   /** @summary Use in frame painter to check zoom Y is allowed
    * @protected */
   get _wheel_zoomy() { return (this.getDimension() > 1) || !this.draw_content; }

   /** @summary Provide histogram min/max used to create canvas ranges
    * @private */
   getUserRanges() {
      const histo = this.getHisto();

      let miny = 0, maxy = 0;

      for (let i = 0; i < histo.fXaxis.fNbins; ++i) {
         const value = histo.getBinContent(i + 1);
         if (i === 0)
            miny = maxy = value;
         else {
            miny = Math.min(miny, value);
            maxy = Math.max(maxy, value);
         }
      }

      if (histo.fMinimum !== kNoZoom)
         miny = histo.fMinimum;

      if (histo.fMaximum !== kNoZoom)
         maxy = histo.fMaximum;

      if (maxy <= miny)
         maxy = miny + 1;

      return { minx: histo.fXaxis.fXmin, maxx: histo.fXaxis.fXmax, miny, maxy };
   }

   /** @summary Count histogram statistic */
   countStat(cond, count_skew) {
      const profile = this.isTProfile(),
            histo = this.getHisto(), xaxis = histo.fXaxis,
            left = this.getSelectIndex('x', 'left'),
            right = this.getSelectIndex('x', 'right'),
            fp = this.getFramePainter(),
            res = {
               name: histo.fName, meanx: 0, meany: 0, rmsx: 0, rmsy: 0, integral: 0,
               entries: (histo.fEntries > 0) ? histo.fEntries : this.stat_entries,
               eff_entries: 0, xmax: 0, wmax: 0, skewx: 0, skewd: 0, kurtx: 0, kurtd: 0
            },
            has_counted_stat = !fp?.isAxisZoomed('x') && (Math.abs(histo.fTsumw) > 1e-300);
      let stat_sumw = 0, stat_sumw2 = 0, stat_sumwx = 0, stat_sumwx2 = 0, stat_sumwy = 0, stat_sumwy2 = 0,
          i, xx, w, xmax = null, wmax = null;

      if (!isFunc(cond))
         cond = null;

      for (i = left; i < right; ++i) {
         xx = xaxis.GetBinCoord(i + 0.5);

         if (cond && !cond(xx))
            continue;

         if (profile) {
            w = histo.fBinEntries[i + 1];
            stat_sumwy += histo.fArray[i + 1];
            stat_sumwy2 += histo.fSumw2[i + 1];
         } else
            w = histo.getBinContent(i + 1);

         if ((xmax === null) || (w > wmax)) {
            xmax = xx;
            wmax = w;
         }

         if (!has_counted_stat) {
            stat_sumw += w;
            stat_sumw2 += w * w;
            stat_sumwx += w * xx;
            stat_sumwx2 += w * xx ** 2;
         }
      }

      // when no range selection done, use original statistic from histogram
      if (has_counted_stat) {
         stat_sumw = histo.fTsumw;
         stat_sumw2 = histo.fTsumw2;
         stat_sumwx = histo.fTsumwx;
         stat_sumwx2 = histo.fTsumwx2;
      }

      res.integral = stat_sumw;

      res.eff_entries = stat_sumw2 ? stat_sumw * stat_sumw / stat_sumw2 : Math.abs(stat_sumw);

      if (Math.abs(stat_sumw) > 1e-300) {
         res.meanx = stat_sumwx / stat_sumw;
         res.meany = stat_sumwy / stat_sumw;
         res.rmsx = Math.sqrt(Math.abs(stat_sumwx2 / stat_sumw - res.meanx ** 2));
         res.rmsy = Math.sqrt(Math.abs(stat_sumwy2 / stat_sumw - res.meany ** 2));
      }

      if (xmax !== null) {
         res.xmax = xmax;
         res.wmax = wmax;
      }

      if (count_skew) {
         let sum3 = 0, sum4 = 0, np = 0;
         for (i = left; i < right; ++i) {
            xx = xaxis.GetBinCoord(i + 0.5);
            if (cond && !cond(xx))
               continue;
            w = profile ? histo.fBinEntries[i + 1] : histo.getBinContent(i + 1);
            np += w;
            sum3 += w * Math.pow(xx - res.meanx, 3);
            sum4 += w * Math.pow(xx - res.meanx, 4);
         }

         const stddev3 = Math.pow(res.rmsx, 3), stddev4 = Math.pow(res.rmsx, 4);
         if (np * stddev3)
            res.skewx = sum3 / (np * stddev3);
         res.skewd = res.eff_entries > 0 ? Math.sqrt(6 / res.eff_entries) : 0;
         if (np * stddev4)
            res.kurtx = sum4 / (np * stddev4) - 3;
         res.kurtd = res.eff_entries > 0 ? Math.sqrt(24 / res.eff_entries) : 0;
      }

      return res;
   }

   /** @summary Fill stat box */
   fillStatistic(stat, dostat, dofit) {
      // no need to refill statistic if histogram is dummy
      if (this.isIgnoreStatsFill())
         return false;

      if (dostat === 1)
         dostat = 1111;
      if (dofit === 1)
         dofit = 111;

      const histo = this.getHisto(),
            print_name = dostat % 10,
            print_entries = Math.floor(dostat / 10) % 10,
            print_mean = Math.floor(dostat / 100) % 10,
            print_rms = Math.floor(dostat / 1000) % 10,
            print_under = Math.floor(dostat / 10000) % 10,
            print_over = Math.floor(dostat / 100000) % 10,
            print_integral = Math.floor(dostat / 1000000) % 10,
            print_skew = Math.floor(dostat / 10000000) % 10,
            print_kurt = Math.floor(dostat / 100000000) % 10,
            data = this.countStat(undefined, (print_skew > 0) || (print_kurt > 0));


      // make empty at the beginning
      stat.clearPave();

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

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

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

         if (print_rms > 0) {
            stat.addText('Std Dev = ' + stat.format(data.rmsx));
            stat.addText('Std Dev y = ' + stat.format(data.rmsy));
         }
      } else {
         if (print_entries > 0)
            stat.addText('Entries = ' + stat.format(data.entries, 'entries'));

         if (print_mean > 0)
            stat.addText('Mean = ' + stat.format(data.meanx));

         if (print_rms > 0)
            stat.addText('Std Dev = ' + stat.format(data.rmsx));

         if (print_under > 0)
            stat.addText('Underflow = ' + stat.format(histo.fArray.length ? histo.fArray[0] : 0, 'entries'));

         if (print_over > 0)
            stat.addText('Overflow = ' + stat.format(histo.fArray.length ? histo.fArray.at(-1) : 0, 'entries'));

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

         if (print_skew === 2)
            stat.addText(`Skewness = ${stat.format(data.skewx)} #pm ${stat.format(data.skewd)}`);
         else if (print_skew > 0)
            stat.addText(`Skewness = ${stat.format(data.skewx)}`);

         if (print_kurt === 2)
            stat.addText(`Kurtosis = ${stat.format(data.kurtx)} #pm ${stat.format(data.kurtd)}`);
         else if (print_kurt > 0)
            stat.addText(`Kurtosis = ${stat.format(data.kurtx)}`);
      }

      if (dofit)
         stat.fillFunctionStat(this.findFunction(clTF1), dofit, 1);

      return true;
   }

   /** @summary Get baseline for bar drawings */
   getBarBaseline(funcs, height) {
      const o = this.getOptions();
      let gry = funcs.swap_xy() ? 0 : height;
      if (Number.isFinite(o.BaseLine) && (o.BaseLine >= funcs.scale_ymin))
         gry = Math.round(funcs.gry(o.BaseLine));
      return gry;
   }

   /** @summary Draw histogram as bars */
   async drawBars(funcs, height) {
      const left = this.getSelectIndex('x', 'left', -1),
            right = this.getSelectIndex('x', 'right', 1),
            histo = this.getHisto(),
            o = this.getOptions(),
            xaxis = histo.fXaxis,
            show_text = o.Text;
      let text_col, text_angle, text_size,
          side = (o.BarStyle > 10) ? o.BarStyle % 10 : 0,
          pr = Promise.resolve();

      if (side > 4)
         side = 4;
      const gry2 = this.getBarBaseline(funcs, height);

      if (show_text) {
         text_col = this.getColor(histo.fMarkerColor);
         text_angle = -1 * o.TextAngle;
         text_size = 20;

         if ((histo.fMarkerSize !== 1) && text_angle)
            text_size = 0.02 * height * histo.fMarkerSize;

         pr = this.startTextDrawingAsync(42, text_size, undefined, text_size);
      }

      return pr.then(() => {
         let bars = '', barsl = '', barsr = '';

         for (let i = left; i < right; ++i) {
            const x1 = xaxis.GetBinLowEdge(i + 1),
                  x2 = xaxis.GetBinLowEdge(i + 2);

            if (funcs.logx && (x2 <= 0))
               continue;

            let grx1 = Math.round(funcs.grx(x1)),
                grx2 = Math.round(funcs.grx(x2)),
                w = grx2 - grx1;
            const y = histo.getBinContent(i + 1);

            if (funcs.logy && (y < funcs.scale_ymin))
               continue;
            const gry1 = Math.round(funcs.gry(y));

            grx1 += Math.round(histo.fBarOffset / 1000 * w);
            w = Math.round(histo.fBarWidth / 1000 * w);

            if (funcs.swap_xy())
               bars += `M${gry2},${grx1}h${gry1 - gry2}v${w}h${gry2 - gry1}z`;
            else
               bars += `M${grx1},${gry1}h${w}v${gry2 - gry1}h${-w}z`;

            if (side > 0) {
               grx2 = grx1 + w;
               w = Math.round(w * side / 10);
               if (funcs.swap_xy()) {
                  barsl += `M${gry2},${grx1}h${gry1 - gry2}v${w}h${gry2 - gry1}z`;
                  barsr += `M${gry2},${grx2}h${gry1 - gry2}v${-w}h${gry2 - gry1}z`;
               } else {
                  barsl += `M${grx1},${gry1}h${w}v${gry2 - gry1}h${-w}z`;
                  barsr += `M${grx2},${gry1}h${-w}v${gry2 - gry1}h${w}z`;
               }
            }

            if (show_text && y) {
               const text = (y === Math.round(y)) ? y.toString() : floatToString(y, gStyle.fPaintTextFormat);

               if (funcs.swap_xy())
                  this.drawText({ align: 12, x: Math.round(gry1 + text_size / 2), y: Math.round(grx1 + 0.1), height: Math.round(w * 0.8), text, color: text_col, latex: 0 });
               else if (text_angle)
                  this.drawText({ align: 12, x: grx1 + w / 2, y: Math.round(gry1 - 2 - text_size / 5), width: 0, height: 0, rotate: text_angle, text, color: text_col, latex: 0 });
               else
                  this.drawText({ align: 22, x: Math.round(grx1 + w * 0.1), y: Math.round(gry1 - 2 - text_size), width: Math.round(w * 0.8), height: text_size, text, color: text_col, latex: 0 });
            }
         }

         if (bars) {
            this.appendPath(bars)
                .call(this.fillatt.func);
         }

         if (barsl) {
            this.appendPath(barsl)
                .call(this.fillatt.func)
                .style('fill', rgb(this.fillatt.color).brighter(0.5).formatRgb());
         }

         if (barsr) {
            this.appendPath(barsr)
                .call(this.fillatt.func)
                .style('fill', rgb(this.fillatt.color).darker(0.5).formatRgb());
         }

         if (show_text)
            return this.finishTextDrawing();
      });
   }

   /** @summary Draw histogram as filled errors */
   drawFilledErrors(funcs) {
      const left = this.getSelectIndex('x', 'left', 0),
            right = this.getSelectIndex('x', 'right', 0),
            histo = this.getHisto(), bins1 = [], bins2 = [];

      for (let i = left; i < right; ++i) {
         const x = histo.fXaxis.GetBinCoord(i + 0.5);
         if (funcs.logx && (x <= 0))
            continue;
         const grx = Math.round(funcs.grx(x)),
               y = histo.getBinContent(i + 1),
               yerrs = this.getBinErrors(histo, i + 1, y);
         if (funcs.logy && (y - yerrs.low < funcs.scale_ymin))
            continue;

         bins1.push({ grx, gry: Math.round(funcs.gry(y + yerrs.up)) });
         bins2.unshift({ grx, gry: Math.round(funcs.gry(y - yerrs.low)) });
      }

      const line = this.getOptions().ErrorKind !== 4,
            path1 = buildSvgCurve(bins1, { line }),
            path2 = buildSvgCurve(bins2, { line, cmd: 'L' });

      this.appendPath(path1 + path2 + 'Z')
          .call(this.fillatt.func);
   }

   /** @summary Draw TH1 as hist/line/curve
     * @return Promise or scalar value */
   async drawNormal(funcs, width, height) {
      const left = this.getSelectIndex('x', 'left', -1),
            right = this.getSelectIndex('x', 'right', 2),
            histo = this.getHisto(),
            o = this.getOptions(),
            want_tooltip = !this.isBatchMode() && settings.Tooltip,
            xaxis = histo.fXaxis,
            exclude_zero = !o.Zero,
            show_errors = o.Error,
            show_curve = o.Curve,
            show_text = o.Text,
            text_profile = show_text && (o.TextKind === 'E') && this.isTProfile() && histo.fBinEntries,
            grpnts = [];
      let res = '', lastbin = false,
          show_markers = o.Mark,
          show_line = o.Line,
          startx, startmidx, currx, curry, x, grx, y, gry, curry_min, curry_max, prevy, prevx, i, bestimin, bestimax,
          path_fill = null, path_err = null, path_marker = null, path_line = '',
          hints_err = null, hints_text = null, hints_marker = null, hsz = 5,
          do_marker = false, do_err = false,
          dend = 0, dlw = 0, my, yerr1, yerr2, bincont, binerr, mx1, mx2, midx, lx, ly, mmx1, mmx2,
          text_col, text_angle, text_size,
          pr = Promise.resolve();

      if (show_errors && !show_markers && (histo.fMarkerStyle > 1))
         show_markers = true;

      if (o.ErrorKind === 2) {
         if (this.fillatt.empty())
            show_markers = true;
         else
            path_fill = '';
      } else if (show_errors) {
         show_line = false;
         path_err = '';
         hints_err = want_tooltip ? '' : null;
         do_err = true;
      }

      dlw = this.lineatt.width + gStyle.fEndErrorSize;
      if (o.ErrorKind === 1)
         dend = Math.floor((this.lineatt.width - 1) / 2);

      if (show_markers) {
         // draw markers also when e2 option was specified
         this.createAttMarker({ attr: histo, style: o.MarkStyle }); // when style not configured, it will be ignored
         if (this.markeratt.size > 0) {
            // simply use relative move from point, can optimize in the future
            path_marker = '';
            do_marker = true;
            this.markeratt.resetPos();
            if ((hints_err === null) && want_tooltip && (!this.markeratt.fill || (this.markeratt.getFullSize() < 7))) {
               hints_marker = '';
               hsz = Math.max(5, Math.round(this.markeratt.getFullSize() * 0.7));
            }
         } else
            show_markers = false;
      }

      const draw_markers = show_errors || show_markers,
            draw_any_but_hist = draw_markers || show_text || show_line || show_curve,
            draw_hist = o.Hist && (!this.lineatt.empty() || !this.fillatt.empty()),
            check_sumw2 = show_errors && histo.fSumw2?.length,
            // if there are too many points, exclude many vertical drawings at the same X position
            // instead define min and max value and made min-max drawing
            use_minmax = draw_any_but_hist || ((right - left) > 3 * width);

      if (!draw_hist && !draw_any_but_hist)
         return this.removeG();

      if (show_text) {
         text_col = this.getColor(histo.fMarkerColor);
         text_angle = -1 * o.TextAngle;
         text_size = 20;

         if ((histo.fMarkerSize !== 1) && text_angle)
            text_size = 0.02 * height * histo.fMarkerSize;

         if (!text_angle && !o.TextKind) {
            const space = width / (right - left + 1);
            if (space < 3 * text_size) {
               text_angle = 270;
               text_size = Math.round(space * 0.7);
            }
         }

         if (want_tooltip && !draw_hist)
            hints_text = '';

         pr = this.startTextDrawingAsync(42, text_size, undefined, text_size);
      }

      return pr.then(() => {
         // just to get correct values for the specified bin
         const extract_bin = bin => {
            bincont = histo.getBinContent(bin + 1);
            if (exclude_zero && (bincont === 0) && (!check_sumw2 || !histo.fSumw2[bin + 1]))
               return false;
            mx1 = Math.round(funcs.grx(xaxis.GetBinLowEdge(bin + 1)));
            mx2 = Math.round(funcs.grx(xaxis.GetBinLowEdge(bin + 2)));
            midx = Math.round((mx1 + mx2) / 2);
            if (startmidx === undefined)
               startmidx = midx;
            my = Math.round(funcs.gry(bincont));
            if (show_errors) {
               binerr = this.getBinErrors(histo, bin + 1, bincont);
               yerr1 = Math.round(my - funcs.gry(bincont + binerr.up)); // up
               yerr2 = Math.round(funcs.gry(bincont - binerr.low) - my); // low
            } else
               yerr1 = yerr2 = 20;

            return true;
         }, draw_errbin = () => {
            let edx = 5;
            if (o.errorX > 0) {
               edx = Math.round((mx2 - mx1) * o.errorX);
               mmx1 = midx - edx;
               mmx2 = midx + edx;
               if (o.ErrorKind === 1)
                  path_err += `M${mmx1 + dend},${my - dlw}v${2 * dlw}m0,-${dlw}h${mmx2 - mmx1 - 2 * dend}m0,-${dlw}v${2 * dlw}`;
               else
                  path_err += `M${mmx1 + dend},${my}h${mmx2 - mmx1 - 2 * dend}`;
            }
            if (o.ErrorKind === 1)
               path_err += `M${midx - dlw},${my - yerr1 + dend}h${2 * dlw}m${-dlw},0v${yerr1 + yerr2 - 2 * dend}m${-dlw},0h${2 * dlw}`;
            else
               path_err += `M${midx},${my - yerr1 + dend}v${yerr1 + yerr2 - 2 * dend}`;
            if (hints_err !== null) {
               const he1 = Math.max(yerr1, 5), he2 = Math.max(yerr2, 5);
               hints_err += `M${midx - edx},${my - he1}h${2 * edx}v${he1 + he2}h${ -2 * edx}z`;
            }
         }, draw_marker = () => {
            if (funcs.swap_xy()) {
               path_marker += this.markeratt.create(my, midx);
               if (hints_marker !== null)
                  hints_marker += `M${my - hsz},${midx - hsz}v${2 * hsz}h${2 * hsz}v${ -2 * hsz}z`;
            } else {
               path_marker += this.markeratt.create(midx, my);
               if (hints_marker !== null)
                  hints_marker += `M${midx - hsz},${my - hsz}h${2 * hsz}v${2 * hsz}h${ -2 * hsz}z`;
            }
         }, draw_bin = bin => {
            if (extract_bin(bin)) {
               if (show_text) {
                  const cont = text_profile ? histo.fBinEntries[bin + 1] : bincont;

                  if (cont) {
                     const arg = text_angle
                        ? { align: 12, x: midx, y: Math.round(my - 2 - text_size / 5), width: 0, height: 0, rotate: text_angle }
                        : { align: 22, x: Math.round(mx1 + (mx2 - mx1) * 0.1), y: Math.round(my - 2 - text_size), width: Math.round((mx2 - mx1) * 0.8), height: text_size };
                     arg.text = (cont === Math.round(cont)) ? cont.toString() : floatToString(cont, gStyle.fPaintTextFormat);
                     arg.color = text_col;
                     arg.latex = 0;
                     if (funcs.swap_xy()) {
                        arg.x = my;
                        arg.y = Math.round(midx - text_size / 2);
                     }
                     this.drawText(arg);
                     if (hints_text !== null)
                        hints_text += `M${mx1},${my - hsz}v${2 * hsz}h${mx2 - mx1}v${ -2 * hsz}z`;
                  }
               }

               if (show_line) {
                  if (funcs.swap_xy())
                     path_line += (path_line ? 'L' : 'M') + `${my},${midx}`; // no optimization
                  else if (!path_line)
                     path_line = `M${midx},${my}`;
                  else if (lx === midx)
                     path_line += `v${my - ly}`;
                  else if (ly === my)
                     path_line += `h${midx - lx}`;
                  else
                     path_line += `l${midx - lx},${my - ly}`;
                  lx = midx;
                  ly = my;
               } else if (show_curve)
                  grpnts.push({ grx: (mx1 + mx2) / 2, gry: funcs.gry(bincont) });

               if (draw_markers) {
                  if ((my >= -yerr1) && (my <= height + yerr2)) {
                     if (path_fill !== null)
                        path_fill += `M${mx1},${my - yerr1}h${mx2 - mx1}v${yerr1 + yerr2 + 1}h${mx1 - mx2}z`;
                     if ((path_marker !== null) && do_marker)
                        draw_marker();
                     if ((path_err !== null) && do_err)
                        draw_errbin();
                  }
               }
            }
         };

         // check if we should draw markers or error marks directly, skipping optimization
         if (do_marker || do_err) {
            if (!settings.OptimizeDraw || ((right - left < 50000) && (settings.OptimizeDraw === 1))) {
               for (i = left; i < right; ++i) {
                  if (extract_bin(i)) {
                     if (path_marker !== null)
                        draw_marker();
                     if (path_err !== null)
                        draw_errbin();
                  }
               }
               do_err = do_marker = false;
            }
         }

         for (i = left; i <= right; ++i) {
            x = xaxis.GetBinLowEdge(i + 1);

            if (this.logx && (x <= 0))
               continue;

            grx = Math.round(funcs.grx(x));

            lastbin = (i === right);

            if (lastbin && (left < right))
               gry = curry;
            else {
               y = histo.getBinContent(i + 1);
               gry = Math.round(funcs.gry(y));
            }

            if (!res) {
               bestimin = bestimax = i;
               prevx = startx = currx = grx;
               prevy = curry_min = curry_max = curry = gry;
               res = `M${currx},${curry}`;
            } else if (use_minmax) {
               if ((grx === currx) && !lastbin) {
                  if (gry < curry_min)
                     bestimax = i;
                  else if (gry > curry_max)
                     bestimin = i;

                  curry_min = Math.min(curry_min, gry);
                  curry_max = Math.max(curry_max, gry);
                  curry = gry;
               } else {
                  if (draw_any_but_hist) {
                     if (bestimin === bestimax)
                        draw_bin(bestimin);
                     else if (bestimin < bestimax) {
                        draw_bin(bestimin);
                        draw_bin(bestimax);
                     } else {
                        draw_bin(bestimax);
                        draw_bin(bestimin);
                     }
                  }

                  // when several points at same X differs, need complete logic
                  if (draw_hist && ((curry_min !== curry_max) || (prevy !== curry_min))) {
                     if (prevx !== currx)
                        res += 'h' + (currx - prevx);

                     if (curry === curry_min) {
                        if (curry_max !== prevy)
                           res += 'v' + (curry_max - prevy);
                        if (curry_min !== curry_max)
                           res += 'v' + (curry_min - curry_max);
                     } else {
                        if (curry_min !== prevy)
                           res += 'v' + (curry_min - prevy);
                        if (curry_max !== curry_min)
                           res += 'v' + (curry_max - curry_min);
                        if (curry !== curry_max)
                           res += 'v' + (curry - curry_max);
                     }

                     prevx = currx;
                     prevy = curry;
                  }

                  if (lastbin && (prevx !== grx))
                     res += 'h' + (grx - prevx);

                  bestimin = bestimax = i;
                  curry_min = curry_max = curry = gry;
                  currx = grx;
               }
               // end of use_minmax
            } else if ((gry !== curry) || lastbin) {
               if (grx !== currx)
                  res += `h${grx - currx}`;
               if (gry !== curry)
                  res += `v${gry - curry}`;
               curry = gry;
               currx = grx;
            }
         }

         const fill_for_interactive = want_tooltip && this.fillatt.empty() && draw_hist && !draw_markers && !show_line && !show_curve && this.isUseFrame();
         let h0 = height + 3;
         if (!fill_for_interactive) {
            const gry0 = Math.round(funcs.gry(0));
            if (gry0 <= 0)
               h0 = -3;
            else if (gry0 < height)
               h0 = gry0;
         }
         const close_path = `L${currx},${h0}H${startx}Z`, add_hist = () => {
            this.appendPath(res + ((!this.fillatt.empty() || fill_for_interactive) ? close_path : ''))
                .style('stroke-linejoin', 'miter')
                .call(this.lineatt.func)
                .call(this.fillatt.func);
         };

         if (res && draw_hist && !this.fillatt.empty()) {
            add_hist();
            res = '';
         }

         if (draw_markers || show_line || show_curve) {
            if (!path_line && grpnts.length) {
               if (funcs.swap_xy())
                  grpnts.forEach(pnt => { [pnt.grx, pnt.gry] = [pnt.gry, pnt.grx]; });
               path_line = buildSvgCurve(grpnts);
            }

            if (path_fill) {
               this.appendPath(path_fill)
                   .call(this.fillatt.func);
            } else if (path_line && !this.fillatt.empty() && !draw_hist) {
               this.appendPath(path_line + `L${midx},${h0}H${startmidx}Z`)
                  .call(this.fillatt.func);
            }

            if (path_err) {
               this.appendPath(path_err)
                   .call(this.lineatt.func);
            }

            if (hints_err) {
               this.appendPath(hints_err)
                   .style('fill', 'none')
                   .style('pointer-events', this.isBatchMode() ? null : 'visibleFill');
            }

            if (path_line) {
               this.appendPath(path_line)
                  .style('fill', 'none')
                  .call(this.lineatt.func);
            }

            if (path_marker) {
               this.appendPath(path_marker)
                  .call(this.markeratt.func);
            }

            if (hints_marker) {
               this.appendPath(hints_marker)
                  .style('fill', 'none')
                  .style('pointer-events', this.isBatchMode() ? null : 'visibleFill');
            }
         }

         if (res && draw_hist)
            add_hist();

         if (hints_text) {
            this.appendPath(hints_text)
                .style('fill', 'none')
                .style('pointer-events', this.isBatchMode() ? null : 'visibleFill');
         }

         if (show_text)
            return this.finishTextDrawing();
      });
   }

   /** @summary Draw TH1 bins in SVG element
     * @return Promise or scalar value */
   draw1DBins() {
      const o = this.getOptions();
      if (o.Same && !this.isUseFrame())
         this.getPadPainter().getFrameSvg().style('display', 'none');

      this.createHistDrawAttributes();

      const funcs = this.getHistGrFuncs(),
            width = funcs.getFrameWidth(),
            height = funcs.getFrameHeight();

      if (!this.draw_content || (width <= 0) || (height <= 0))
         return this.removeG();

      this.createG(this.isUseFrame());

      if (o.Bar) {
         return this.drawBars(funcs, height).then(() => {
            if (o.ErrorKind === 1)
               return this.drawNormal(funcs, width, height);
         });
      }

      if ((o.ErrorKind === 3) || (o.ErrorKind === 4))
         return this.drawFilledErrors(funcs);

      return this.drawNormal(funcs, width, height);
   }

   /** @summary Provide text information (tooltips) for histogram bin */
   getBinTooltips(bin) {
      const tips = [],
            name = this.getObjectHint(),
            funcs = this.getHistGrFuncs(),
            histo = this.getHisto(),
            o = this.getOptions(),
            x1 = histo.fXaxis.GetBinLowEdge(bin + 1),
            x2 = histo.fXaxis.GetBinLowEdge(bin + 2),
            xlbl = this.getAxisBinTip('x', histo.fXaxis, bin);
      let cont = histo.getBinContent(bin + 1);

      if (name)
         tips.push(name);

      if (o.Error || o.Mark || this.isTF1()) {
         tips.push(`x = ${xlbl}`, `y = ${funcs.axisAsText('y', cont)}`);
         if (o.Error) {
            if (xlbl[0] === '[')
               tips.push(`error x = ${((x2 - x1) / 2).toPrecision(4)}`);
            const errs = this.getBinErrors(histo, bin + 1, cont);
            if (errs.poisson)
               tips.push(`error low = ${errs.low.toPrecision(4)}`, `error up = ${errs.up.toPrecision(4)}`);
            else
               tips.push(`error y = ${errs.up.toPrecision(4)}`);
         }
      } else {
         tips.push(`bin = ${bin + 1}`, `x = ${xlbl}`);
         if (histo.$baseh)
            cont -= histo.$baseh.getBinContent(bin + 1);
         if (cont === Math.round(cont))
            tips.push(`entries = ${cont}`);
         else
            tips.push(`entries = ${floatToString(cont, gStyle.fStatFormat)}`);
      }

      return tips;
   }

   /** @summary Process tooltip event */
   processTooltipEvent(pnt) {
      const o = this.getOptions();
      if (!pnt || !this.draw_content || !this.getG() || o.Mode3D) {
         this.getG()?.selectChild('.tooltip_bin').remove();
         return null;
      }

      const funcs = this.getHistGrFuncs(),
            histo = this.getHisto(),
            left = this.getSelectIndex('x', 'left', -1),
            right = this.getSelectIndex('x', 'right', 2),
            draw_hist = this.options.Hist && (!this.lineatt.empty() || !this.fillatt.empty());
      let width = funcs.getFrameWidth(),
          height = funcs.getFrameHeight(),
          show_rect, grx1, grx2, gry1, gry2, gapx = 2,
          l = left, r = right, pnt_x = pnt.x, pnt_y = pnt.y;

      const GetBinGrX = i => {
         const xx = histo.fXaxis.GetBinLowEdge(i + 1);
         return (funcs.logx && (xx <= 0)) ? null : funcs.grx(xx);
      }, GetBinGrY = i => {
         const yy = histo.getBinContent(i + 1);
         if (funcs.logy && (yy < funcs.scale_ymin))
            return funcs.swap_xy() ? -1e3 : 10 * height;
         return Math.round(funcs.gry(yy));
      };

      if (funcs.swap_xy())
         [pnt_x, pnt_y, width, height] = [pnt_y, pnt_x, height, width];

      const descent_order = funcs.x_handle && (funcs.swap_xy() !== funcs.x_handle.reverse);

      while (l < r - 1) {
         const m = Math.round((l + r) * 0.5), xx = GetBinGrX(m);
         if ((xx === null) || (xx < pnt_x - 0.5)) {
            if (descent_order)
               r = m;
            else
               l = m;
         } else if (xx > pnt_x + 0.5) {
            if (descent_order)
               l = m;
            else
               r = m;
         } else {
            l++;
            r--;
         }
      }

      let findbin = r = l;
      grx1 = GetBinGrX(findbin);

      if (descent_order) {
         while ((l > left) && (GetBinGrX(l - 1) < grx1 + 2))
            --l;
         while ((r < right) && (GetBinGrX(r + 1) > grx1 - 2))
            ++r;
      } else {
         while ((l > left) && (GetBinGrX(l - 1) > grx1 - 2))
            --l;
         while ((r < right) && (GetBinGrX(r + 1) < grx1 + 2))
            ++r;
      }

      if (l < r) {
         // many points can be assigned with the same cursor position
         // first try point around mouse y
         let best = height;
         for (let m = l; m <= r; m++) {
            const dist = Math.abs(GetBinGrY(m) - pnt_y);
            if (dist < best) {
               best = dist;
               findbin = m;
            }
         }

         // if best distance still too far from mouse position, just take from between
         if (best > height / 10)
            findbin = Math.round(l + (r - l) / height * pnt_y);

         grx1 = GetBinGrX(findbin);
      }

      grx1 = Math.round(grx1);
      grx2 = Math.round(GetBinGrX(findbin + 1));

      if (o.Bar) {
         const w = grx2 - grx1;
         grx1 += Math.round(histo.fBarOffset / 1000 * w);
         grx2 = grx1 + Math.round(histo.fBarWidth / 1000 * w);
      }

      if (grx1 > grx2)
         [grx1, grx2] = [grx2, grx1];

      const midx = Math.round((grx1 + grx2) / 2),
            midy = gry1 = gry2 = GetBinGrY(findbin);

      if (o.Bar) {
         show_rect = true;
         gapx = 0;
         gry1 = this.getBarBaseline(funcs, height);
         if (gry1 > gry2)
            [gry1, gry2] = [gry2, gry1];
         if (!pnt.touch && (pnt.nproc === 1) && ((pnt_y < gry1) || (pnt_y > gry2)))
            findbin = null;
      } else if ((o.Error && (o.Hist !== true)) || o.Mark || o.Line || o.Curve || (o.Text && !draw_hist)) {
         show_rect = !this.isTF1();

         let msize = 3;
         if (this.markeratt)
            msize = Math.max(msize, this.markeratt.getFullSize());

         if (o.Error) {
            const cont = histo.getBinContent(findbin + 1),
                  binerrs = this.getBinErrors(histo, findbin + 1, cont);

            gry1 = Math.round(funcs.gry(cont + binerrs.up)); // up
            gry2 = Math.round(funcs.gry(cont - binerrs.low)); // low

            if ((cont === 0) && this.isTProfile())
               findbin = null;

            const dx = (grx2 - grx1) * o.errorX;
            grx1 = Math.round(midx - dx);
            grx2 = Math.round(midx + dx);
         }

         // show at least 6 pixels as tooltip rect
         if (grx2 - grx1 < 2 * msize) {
            grx1 = midx - msize;
            grx2 = midx + msize;
         }

         gry1 = Math.min(gry1, midy - msize);
         gry2 = Math.max(gry2, midy + msize);

         if (!pnt.touch && (pnt.nproc === 1)) {
            if ((pnt_y < gry1) || (pnt_y > gry2))
               findbin = null;
         }
      } else {
         // if histogram alone, use old-style with rects
         // if there are too many points at pixel, use circle
         show_rect = (pnt.nproc === 1) && (right - left < width);

         if (show_rect) {
            gry2 = height;

            if (!this.fillatt.empty()) {
               gry2 = Math.min(height, Math.max(0, Math.round(funcs.gry(0))));
               if (gry2 < gry1)
                  [gry1, gry2] = [gry2, gry1];
            }

            // for mouse events pointer should be between y1 and y2
            if (((pnt.y < gry1) || (pnt.y > gry2)) && !pnt.touch)
               findbin = null;
         }
      }

      if (findbin !== null) {
         // if bin on boundary found, check that x position is ok
         if ((findbin === left) && (grx1 > pnt_x + gapx))
            findbin = null;
         else if ((findbin === right - 1) && (grx2 < pnt_x - gapx))
            findbin = null;
         else if ((pnt_x < grx1 - gapx) || (pnt_x > grx2 + gapx))
            findbin = null; // if bars option used check that bar is not match
         else if (!o.Zero && (histo.getBinContent(findbin + 1) === 0) && (histo.getBinError(findbin + 1) === 0))
            findbin = null; // exclude empty bin if empty bins suppressed
      }

      let ttrect = this.getG().selectChild('.tooltip_bin');

      if ((findbin === null) || ((gry2 <= 0) || (gry1 >= height))) {
         ttrect.remove();
         return null;
      }

      const res = {
         name: this.getObjectName(), title: histo.fTitle,
         x: midx, y: midy, exact: true,
         color1: this.lineatt?.color ?? 'green',
         color2: this.fillatt?.getFillColorAlt('blue') ?? 'blue',
         lines: this.getBinTooltips(findbin)
      };

      if (pnt.disabled) {
         // case when tooltip should not highlight bin
         ttrect.remove();
         res.changed = true;
      } else if (show_rect) {
         if (ttrect.empty()) {
            ttrect = this.getG().append('svg:rect')
                                .attr('class', 'tooltip_bin')
                                .style('pointer-events', 'none')
                                .call(addHighlightStyle);
         }

         res.changed = ttrect.property('current_bin') !== findbin;

         if (res.changed) {
            ttrect.attr('x', funcs.swap_xy() ? gry1 : grx1)
                  .attr('width', funcs.swap_xy() ? gry2 - gry1 : grx2 - grx1)
                  .attr('y', funcs.swap_xy() ? grx1 : gry1)
                  .attr('height', funcs.swap_xy() ? grx2 - grx1 : gry2 - gry1)
                  .style('opacity', '0.3')
                  .property('current_bin', findbin);
         }

         res.exact = (Math.abs(midy - pnt_y) <= 5) || ((pnt_y >= gry1) && (pnt_y <= gry2));

         res.menu = res.exact; // one could show context menu when histogram is selected
         // distance to middle point, use to decide which menu to activate
         res.menu_dist = Math.sqrt((midx - pnt_x) ** 2 + (midy - pnt_y) ** 2);
      } else {
         const radius = this.lineatt.width + 3;

         if (ttrect.empty()) {
            ttrect = this.getG().append('svg:circle')
                                .attr('class', 'tooltip_bin')
                                .style('pointer-events', 'none')
                                .attr('r', radius)
                                .call(this.lineatt.func)
                                .call(this.fillatt.func);
         }

         res.exact = (Math.abs(midx - pnt.x) <= radius) && (Math.abs(midy - pnt.y) <= radius);

         res.menu = res.exact; // show menu only when mouse pointer exactly over the histogram
         res.menu_dist = Math.sqrt((midx - pnt.x) ** 2 + (midy - pnt.y) ** 2);

         res.changed = ttrect.property('current_bin') !== findbin;

         if (res.changed) {
            ttrect.attr('cx', midx)
                  .attr('cy', midy)
                  .property('current_bin', findbin);
         }
      }

      if (res.changed) {
         res.user_info = {
            obj: histo, name: histo.fName,
            bin: findbin, cont: histo.getBinContent(findbin + 1),
            grx: midx, gry: midy
         };
      }

      return res;
   }

   /** @summary Fill histogram context menu */
   fillHistContextMenu(menu) {
      menu.add('Auto zoom-in', () => this.autoZoom());

      const opts = this.getSupportedDrawOptions();

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

         this.decodeOptions(arg);

         if (this.getOptions().need_fillcol && this.fillatt?.empty())
            this.fillatt.change(5, 1001);

         // redraw all objects in pad, inform dependent objects
         this.interactiveRedraw('pad', 'drawopt');
      });

      if (!this.hasSnapId() && !this.isTProfile() && !this.isTF1())
         menu.addRebinMenu(sz => this.rebinHist(sz));
   }

   /** @summary Rebin histogram, used via context menu */
   rebinHist(sz) {
      const histo = this.getHisto(),
            xaxis = histo.fXaxis,
            nbins = Math.floor(xaxis.fNbins / sz);
      if (nbins < 2)
         return;

      const arr = new Array(nbins + 2),
            xbins = xaxis.fXbins.length ? new Array(nbins) : null;

      arr[0] = histo.fArray[0];
      let indx = 1;

      for (let i = 1; i <= nbins; ++i) {
         if (xbins)
            xbins[i - 1] = xaxis.fXbins[indx - 1];
         let sum = 0;
         for (let k = 0; k < sz; ++k)
            sum += histo.fArray[indx++];
         arr[i] = sum;
      }

      if (xbins) {
         if (indx <= xaxis.fXbins.length)
            xaxis.fXmax = xaxis.fXbins[indx - 1];
         xaxis.fXbins = xbins;
      } else
         xaxis.fXmax = xaxis.fXmin + (xaxis.fXmax - xaxis.fXmin) / xaxis.fNbins * nbins * sz;


      xaxis.fNbins = nbins;

      let overflow = 0;
      while (indx < histo.fArray.length)
         overflow += histo.fArray[indx++];
      arr[nbins + 1] = overflow;

      histo.fArray = arr;
      histo.fSumw2 = [];

      this.scanContent();

      this.interactiveRedraw('pad');
   }

   /** @summary Perform automatic zoom inside non-zero region of histogram */
   autoZoom() {
      let left = this.getSelectIndex('x', 'left', -1),
          right = this.getSelectIndex('x', 'right', 1);
      const dist = right - left,
            histo = this.getHisto();

      if ((dist === 0) || !histo)
         return;

      // first find minimum
      let min = histo.getBinContent(left + 1);
      for (let indx = left; indx < right; ++indx)
         min = Math.min(min, histo.getBinContent(indx + 1));
      if (min > 0)
         return; // if all points positive, no chance for auto-scale

      while ((left < right) && (histo.getBinContent(left + 1) <= min)) ++left;
      while ((left < right) && (histo.getBinContent(right) <= min)) --right;

      // if singular bin
      if ((left === right - 1) && (left > 2) && (right < this.nbinsx - 2)) {
         --left; ++right;
      }

      if ((right - left < dist) && (left < right))
         return this.getFramePainter().zoom(histo.fXaxis.GetBinLowEdge(left + 1), histo.fXaxis.GetBinLowEdge(right + 1));
   }

   /** @summary Checks if it makes sense to zoom inside specified axis range */
   canZoomInside(axis, min, max) {
      const histo = this.getHisto();

      if ((axis === 'x') && histo && (histo.fXaxis.FindBin(max, 0.5) - histo.fXaxis.FindBin(min, 0) > 1))
         return true;

      if ((axis === 'y') && (Math.abs(max - min) > Math.abs(this.ymax - this.ymin) * 1e-6))
         return true;

      return false;
   }

   /** @summary Performs 2D drawing of histogram
     * @return {Promise} when ready */
   async draw2D(reason) {
      this.clear3DScene();

      this.scanContent(reason === 'zoom');

      const pr = this.isMainPainter() ? this.drawColorPalette(false) : Promise.resolve(true);

      return pr.then(() => this.drawAxes())
               .then(() => this.draw1DBins())
               .then(() => this.updateFunctions())
               .then(() => this.updateHistTitle())
               .then(() => {
                  this.updateStatWebCanvas();
                  return this.addInteractivity();
               });
   }

   /** @summary Should performs 3D drawing of histogram
     * @desc Disable in 2D case, just draw with default options
     * @return {Promise} when ready */
   async draw3D(reason) {
      console.log('3D drawing is disabled, load ./hist/TH1Painter.mjs');
      return this.draw2D(reason);
   }

   /** @summary Call drawing function depending from 3D mode */
   async callDrawFunc(reason) {
      const main = this.getMainPainter(),
            fp = this.getFramePainter(),
            o = this.getOptions();

      if ((main !== this) && fp && (fp.mode3d !== o.Mode3D))
         this.copyOptionsFrom(main);

      if (!o.Mode3D)
         return this.draw2D(reason);

      return this.draw3D(reason).catch(err => {
         const cp = this.getCanvPainter();
         if (isFunc(cp?.showConsoleError))
            cp.showConsoleError(err);
         else
            console.error('Fail to draw histogram in 3D - back to 2D');
         o.Mode3D = false;
         return this.draw2D(reason);
      });
   }

   /** @summary Redraw histogram */
   redraw(reason) {
      return this.callDrawFunc(reason);
   }

   /** @summary draw TH1 object in 2D only */
   static async draw(dom, histo, opt) {
      return THistPainter._drawHist(new TH1Painter(dom, histo), opt);
   }

}; // class TH1Painter

/** @summary Draw 1-D histogram in 3D
  * @private */

class TH1Painter extends TH1Painter$2 {

   /** @summary draw TH1 object in 3D mode */
   async draw3D(reason) {
      this.mode3d = true;

      const fp = this.getFramePainter(), // who makes axis drawing
            is_main = this.isMainPainter(), // is main histogram
            o = this.getOptions();

      o.zmult = 1 + 2 * gStyle.fHistTopMargin;
      let pr = Promise.resolve(true), full_draw = true;

      if (reason === 'resize') {
         const res = is_main ? fp.resize3D() : false;
         if (res !== 1) {
            full_draw = false;
            if (res)
               fp.render3D();
         }
      }

      if (full_draw) {
         this.createHistDrawAttributes(true);

         this.scanContent(reason === 'zoom'); // may be required for axis drawings

         if (is_main)
            pr = crete3DFrame(this, TAxisPainter, o.Render3D);

         if (fp.mode3d) {
            pr = pr.then(() => {
               drawBinsLego(this);
               fp.render3D();
               this.updateStatWebCanvas();
               fp.addKeysHandler();
            });
         }
      }

      if (is_main)
         pr = pr.then(() => this.drawColorPalette(o.Zscale && o.canHavePalette()));

      return pr.then(() => this.updateFunctions())
               .then(() => this.updateHistTitle())
               .then(() => this);
   }

   /** @summary Build three.js object for the histogram */
   static async build3d(histo, opt) {
      const painter = new TH1Painter(null, histo);
      painter.decodeOptions(opt);
      painter.scanContent();

      painter.createHistDrawAttributes(true);
      painter.options.zmult = 1 + 2 * gStyle.fHistTopMargin;

      const fp = new TFramePainter(null, null);
      painter.getFramePainter = () => fp;

      return crete3DFrame(painter, TAxisPainter)
             .then(() => drawBinsLego(painter))
             .then(() => fp.create3DScene(-1, true));
   }


   /** @summary draw TH1 object */
   static async draw(dom, histo, opt) {
      return THistPainter._drawHist(new TH1Painter(dom, histo), opt);
   }

} // class TH1Painter

var TH1Painter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TH1Painter: TH1Painter
});

/** @summary Draw TH2Poly histogram as lego
  * @private */
function drawTH2PolyLego(painter) {
   const histo = painter.getHisto(),
         fp = painter.getFramePainter(),
         axis_zmin = fp.z_handle.getScaleMin(),
         axis_zmax = fp.z_handle.getScaleMax(),
         len = histo.fBins.arr.length,
         z0 = fp.grz(axis_zmin);
   let colindx, bin, i, z1;

   // use global coordinates
   painter.maxbin = painter.gmaxbin;
   painter.minbin = painter.gminbin;
   painter.minposbin = painter.gminposbin;

   const cntr = painter.getContour(true), palette = painter.getHistPalette();

   for (i = 0; i < len; ++i) {
      bin = histo.fBins.arr[i];
      if (bin.fContent < axis_zmin)
         continue;

      colindx = cntr.getPaletteIndex(palette, bin.fContent);
      if (colindx === null)
         continue;

      // check if bin outside visible range
      if ((bin.fXmin > fp.scale_xmax) || (bin.fXmax < fp.scale_xmin) ||
          (bin.fYmin > fp.scale_ymax) || (bin.fYmax < fp.scale_ymin))
         continue;

      z1 = fp.grz((bin.fContent > axis_zmax) ? axis_zmax : bin.fContent);

      const all_pnts = [], all_faces = [];
      let ngraphs = 1, gr = bin.fPoly, nfaces = 0;

      if (gr._typename === clTMultiGraph) {
         ngraphs = bin.fPoly.fGraphs.arr.length;
         gr = null;
      }

      for (let ngr = 0; ngr < ngraphs; ++ngr) {
         if (!gr || (ngr > 0))
            gr = bin.fPoly.fGraphs.arr[ngr];

         const x = gr.fX, y = gr.fY;
         let npnts = gr.fNpoints;
         while ((npnts > 2) && (x[0] === x[npnts - 1]) && (y[0] === y[npnts - 1])) --npnts;

         let pnts, faces;

         for (let ntry = 0; ntry < 2; ++ntry) {
            // run two loops - on the first try to compress data, on second - run as is (removing duplication)

            let lastx, lasty, currx, curry,
                dist2 = fp.size_x3d * fp.size_z3d;
            const dist2limit = (ntry > 0) ? 0 : dist2 / 1e6;

            pnts = [];
            faces = null;

            for (let vert = 0; vert < npnts; ++vert) {
               currx = fp.grx(x[vert]);
               curry = fp.gry(y[vert]);
               if (vert > 0)
                  dist2 = (currx - lastx) ** 2 + (curry - lasty) ** 2;
               if (dist2 > dist2limit) {
                  pnts.push(new THREE.Vector2(currx, curry));
                  lastx = currx;
                  lasty = curry;
               }
            }

            try {
               if (pnts.length > 2)
                  faces = THREE.ShapeUtils.triangulateShape(pnts, []);
            } catch {
               faces = null;
            }

            if (faces && (faces.length > pnts.length - 3))
               break;
         }

         if (faces?.length && pnts) {
            all_pnts.push(pnts);
            all_faces.push(faces);

            nfaces += faces.length * 2;
            if (z1 > z0)
               nfaces += pnts.length * 2;
         }
      }

      const pos = new Float32Array(nfaces * 9);
      let indx = 0;

      for (let ngr = 0; ngr < all_pnts.length; ++ngr) {
         const pnts = all_pnts[ngr], faces = all_faces[ngr];

         for (let layer = 0; layer < 2; ++layer) {
            for (let n = 0; n < faces.length; ++n) {
               const face = faces[n],
                     pnt1 = pnts[face[0]],
                     pnt2 = pnts[face[layer === 0 ? 2 : 1]],
                     pnt3 = pnts[face[layer === 0 ? 1 : 2]];

               pos[indx] = pnt1.x;
               pos[indx + 1] = pnt1.y;
               pos[indx + 2] = layer ? z1 : z0;
               indx += 3;

               pos[indx] = pnt2.x;
               pos[indx + 1] = pnt2.y;
               pos[indx + 2] = layer ? z1 : z0;
               indx += 3;

               pos[indx] = pnt3.x;
               pos[indx + 1] = pnt3.y;
               pos[indx + 2] = layer ? z1 : z0;
               indx += 3;
            }
         }

         if (z1 > z0) {
            for (let n = 0; n < pnts.length; ++n) {
               const pnt1 = pnts.at(n), pnt2 = pnts.at(n > 0 ? n - 1 : -1);

               pos[indx] = pnt1.x;
               pos[indx + 1] = pnt1.y;
               pos[indx + 2] = z0;
               indx += 3;

               pos[indx] = pnt2.x;
               pos[indx + 1] = pnt2.y;
               pos[indx + 2] = z0;
               indx += 3;

               pos[indx] = pnt2.x;
               pos[indx + 1] = pnt2.y;
               pos[indx + 2] = z1;
               indx += 3;

               pos[indx] = pnt1.x;
               pos[indx + 1] = pnt1.y;
               pos[indx + 2] = z0;
               indx += 3;

               pos[indx] = pnt2.x;
               pos[indx + 1] = pnt2.y;
               pos[indx + 2] = z1;
               indx += 3;

               pos[indx] = pnt1.x;
               pos[indx + 1] = pnt1.y;
               pos[indx + 2] = z1;
               indx += 3;
            }
         }
      }

      const geometry = new THREE.BufferGeometry();
      geometry.setAttribute('position', new THREE.BufferAttribute(pos, 3));
      geometry.computeVertexNormals();

      const material = new THREE.MeshBasicMaterial(getMaterialArgs(painter.getHistPalette()?.getColor(colindx), { vertexColors: false, side: THREE.DoubleSide })),
            mesh = new THREE.Mesh(geometry, material);

      fp.add3DMesh(mesh);

      mesh.painter = painter;
      mesh.bins_index = i;
      mesh.draw_z0 = z0;
      mesh.draw_z1 = z1;
      mesh.tip_color = 0x00FF00;

      mesh.tooltip = function(/* intersects */) {
         const p = this.painter,
               tbin = p.getObject().fBins.arr[this.bins_index];
         return {
            use_itself: true, // indicate that use mesh itself for highlighting
            x1: fp.grx(tbin.fXmin),
            x2: fp.grx(tbin.fXmax),
            y1: fp.gry(tbin.fYmin),
            y2: fp.gry(tbin.fYmax),
            z1: this.draw_z0,
            z2: this.draw_z1,
            bin: this.bins_index,
            value: bin.fContent,
            color: this.tip_color,
            lines: p.getPolyBinTooltips(this.bins_index)
         };
      };
   }
}

/** @summary Draw 2-D histogram in 3D
  * @private */
class TH2Painter extends TH2Painter$2 {

   /** @summary Check range for 3D
    * @private */
   checkRangeFor3D(o) {
      const pad = this.getPadPainter()?.getRootPad(true),
            logz = pad?.fLogv ?? pad?.fLogz;
      let zmult = 1;

      if (o.ohmin && o.ohmax) {
         this.zmin = o.hmin;
         this.zmax = o.hmax;
      } else if (o.minimum !== kNoZoom && o.maximum !== kNoZoom) {
         this.zmin = o.minimum;
         this.zmax = o.maximum;
      } else if (this.draw_content || this.gmaxbin) {
         this.zmin = logz ? this.gminposbin * 0.3 : this.gminbin;
         this.zmax = this.gmaxbin;
         zmult = 1 + 2 * gStyle.fHistTopMargin;
      }

      if (logz && (this.zmin <= 0))
         this.zmin = this.zmax * 1e-5;

      this.createHistDrawAttributes(true);
      return zmult;
   }

   /** @summary Create 3D object for histogram bins
    * @private */
   draw3DBins(o) {
      if (this.isTH2Poly())
         drawTH2PolyLego(this);
      else if (o.Contour)
         drawBinsContour3D(this, true);
      else if (o.Surf)
         drawBinsSurf3D(this);
      else if (o.Error)
         drawBinsError3D(this);
      else
         drawBinsLego(this);
   }

   /** @summary draw TH2 object in 3D mode */
   async draw3D(reason) {
      this.mode3d = true;

      const fp = this.getFramePainter(), // who makes axis drawing
            is_main = this.isMainPainter(), // is main histogram
            o = this.getOptions();

      let pr = Promise.resolve(true), full_draw = true;

      if (reason === 'resize') {
         const res = is_main ? fp.resize3D() : false;
         if (res !== 1) {
            full_draw = false;
            if (res)
               fp.render3D();
         }
      }

      if (full_draw) {
         o.zmult = this.checkRangeFor3D(o);

         if (is_main)
            pr = crete3DFrame(this, TAxisPainter, o.Render3D);

         if (fp.mode3d) {
            pr = pr.then(() => {
               if (this.draw_content)
                  this.draw3DBins(o);
               else if (o.Axis && o.Zscale) {
                  this.getContourLevels(true);
                  this.getHistPalette();
               }
               fp.render3D();
               this.updateStatWebCanvas();
               fp.addKeysHandler();
            });
         }
      }

      //  (re)draw palette by resize while canvas may change dimension
      if (is_main) {
         pr = pr.then(() => this.drawColorPalette(o.Zscale && ((o.Lego === 12) || (o.Lego === 14) ||
                                                  (o.Surf === 11) || (o.Surf === 12))));
      }

      return pr.then(() => this.updateFunctions())
               .then(() => this.updateHistTitle())
               .then(() => this);
   }

   /** @summary Build three.js object for the histogram */
   static async build3d(histo, opt, get_painter) {
      const painter = new TH2Painter(null, histo);
      painter.decodeOptions(opt);

      const o = painter.getOptions();
      if (painter.isTH2Poly())
         o.Lego = 12;
      painter.scanContent();

      o.zmult = painter.checkRangeFor3D(o);

      const fp = new TFramePainter(null, null);
      // return dummy frame painter as result
      painter.getFramePainter = () => fp;

      return crete3DFrame(painter, TAxisPainter).then(() => {
         if (painter.draw_content)
            painter.draw3DBins(o);

         return get_painter ? painter : fp.create3DScene(-1, true);
      });
   }

   /** @summary draw TH2 object */
   static async draw(dom, histo, opt) {
      return THistPainter._drawHist(new TH2Painter(dom, histo), opt);
   }

} // class TH2Painter

var TH2Painter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TH2Painter: TH2Painter
});

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

class TH3Painter extends THistPainter {

   #box_option; // actual box option

   /** @summary Returns number of histogram dimensions */
   getDimension() { return 3; }

   /** @summary Scan TH3 histogram content */
   scanContent(when_axis_changed) {
      // no need to re-scan histogram while result does not depend from axis selection
      if (when_axis_changed && this.nbinsx && this.nbinsy && this.nbinsz)
         return;

      const histo = this.getHisto();

      this.extractAxesProperties(3);

      // global min/max, used at the moment in 3D drawing
      this.gminbin = this.gmaxbin = histo.getBinContent(1, 1, 1);
      this.gminposbin = null;

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

               if ((bin_content > 0) && ((this.gminposbin === null) || (this.gminposbin > bin_content)))
                  this.gminposbin = bin_content;
            }
         }
      }

      if ((this.gminposbin === null) && (this.gmaxbin > 0))
         this.gminposbin = this.gmaxbin * 1e-4;

      this.draw_content = this.gmaxbin || this.gminbin;

      this.transferFunc = this.findFunction(clTF1, 'TransferFunction');
      this.transferFunc?.SetBit(BIT(9), true); // TF1::kNotDraw
   }

   /** @summary Count TH3 statistic */
   countStat(cond, count_skew) {
      const histo = this.getHisto(), xaxis = histo.fXaxis, yaxis = histo.fYaxis, zaxis = histo.fZaxis,
            i1 = this.getSelectIndex('x', 'left'),
            i2 = this.getSelectIndex('x', 'right'),
            j1 = this.getSelectIndex('y', 'left'),
            j2 = this.getSelectIndex('y', 'right'),
            k1 = this.getSelectIndex('z', 'left'),
            k2 = this.getSelectIndex('z', 'right'),
            fp = this.getFramePainter(),
            res = {
               name: histo.fName, entries: 0, eff_entries: 0, integral: 0,
               meanx: 0, meany: 0, meanz: 0, rmsx: 0, rmsy: 0, rmsz: 0,
               skewx: 0, skewy: 0, skewz: 0, skewd: 0, kurtx: 0, kurty: 0, kurtz: 0, kurtd: 0
            },
            has_counted_stat = (Math.abs(histo.fTsumw) > 1e-300) && !fp.isAxisZoomed('x') && !fp.isAxisZoomed('y') && !fp.isAxisZoomed('z');
      let xi, yi, zi, xx, xside, yy, yside, zz, zside, cont,
          stat_sum0 = 0, stat_sumw2 = 0, stat_sumx1 = 0, stat_sumy1 = 0,
          stat_sumz1 = 0, stat_sumx2 = 0, stat_sumy2 = 0, stat_sumz2 = 0;

      if (!isFunc(cond))
         cond = null;

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

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

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

               if (cond && !cond(xx, yy, zz))
                  continue;

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

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

      if (has_counted_stat) {
         stat_sum0 = histo.fTsumw;
         stat_sumw2 = histo.fTsumw2;
         stat_sumx1 = histo.fTsumwx;
         stat_sumx2 = histo.fTsumwx2;
         stat_sumy1 = histo.fTsumwy;
         stat_sumy2 = histo.fTsumwy2;
         stat_sumz1 = histo.fTsumwz;
         stat_sumz2 = histo.fTsumwz2;
      }

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

      res.integral = stat_sum0;

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

      res.eff_entries = stat_sumw2 ? stat_sum0 * stat_sum0 / stat_sumw2 : Math.abs(stat_sum0);

      if (count_skew && !this.isTH2Poly()) {
         let sumx3 = 0, sumy3 = 0, sumz3 = 0, sumx4 = 0, sumy4 = 0, sumz4 = 0, np = 0;
         for (xi = i1; xi < i2; ++xi) {
            xx = xaxis.GetBinCoord(xi + 0.5);
            for (yi = j1; yi < j2; ++yi) {
               yy = yaxis.GetBinCoord(yi + 0.5);
               for (zi = k1; zi < k2; ++zi) {
                  zz = zaxis.GetBinCoord(zi + 0.5);
                  if (cond && !cond(xx, yy, zz))
                     continue;
                  const w = histo.getBinContent(xi + 1, yi + 1, zi + 1);
                  np += w;
                  sumx3 += w * Math.pow(xx - res.meanx, 3);
                  sumy3 += w * Math.pow(yy - res.meany, 3);
                  sumz3 += w * Math.pow(zz - res.meany, 3);
                  sumx4 += w * Math.pow(xx - res.meanx, 4);
                  sumy4 += w * Math.pow(yy - res.meany, 4);
                  sumz4 += w * Math.pow(yy - res.meany, 4);
               }
            }
         }

         const stddev3x = Math.pow(res.rmsx, 3),
               stddev3y = Math.pow(res.rmsy, 3),
               stddev3z = Math.pow(res.rmsz, 3),
               stddev4x = Math.pow(res.rmsx, 4),
               stddev4y = Math.pow(res.rmsy, 4),
               stddev4z = Math.pow(res.rmsz, 4);

         if (np * stddev3x)
            res.skewx = sumx3 / (np * stddev3x);
         if (np * stddev3y)
            res.skewy = sumy3 / (np * stddev3y);
         if (np * stddev3z)
            res.skewz = sumz3 / (np * stddev3z);
         res.skewd = res.eff_entries > 0 ? Math.sqrt(6 / res.eff_entries) : 0;

         if (np * stddev4x)
            res.kurtx = sumx4 / (np * stddev4x) - 3;
         if (np * stddev4y)
            res.kurty = sumy4 / (np * stddev4y) - 3;
         if (np * stddev4z)
            res.kurtz = sumz4 / (np * stddev4z) - 3;
         res.kurtd = res.eff_entries > 0 ? Math.sqrt(24 / res.eff_entries) : 0;
      }

      return res;
   }

   /** @summary Fill TH3 statistic in stat box */
   fillStatistic(stat, dostat, dofit) {
      // no need to refill statistic if histogram is dummy
      if (this.isIgnoreStatsFill())
         return false;

      if (dostat === 1)
         dostat = 1111;

      const print_name = dostat % 10,
            print_entries = Math.floor(dostat / 10) % 10,
            print_mean = Math.floor(dostat / 100) % 10,
            print_rms = Math.floor(dostat / 1000) % 10,
            print_integral = Math.floor(dostat / 1000000) % 10,
            print_skew = Math.floor(dostat / 10000000) % 10,
            print_kurt = Math.floor(dostat / 100000000) % 10,
            data = this.countStat(undefined, (print_skew > 0) || (print_kurt > 0));

      stat.clearPave();

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

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

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

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

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

      if (print_skew === 2) {
         stat.addText(`Skewness x = ${stat.format(data.skewx)} #pm ${stat.format(data.skewd)}`);
         stat.addText(`Skewness y = ${stat.format(data.skewy)} #pm ${stat.format(data.skewd)}`);
         stat.addText(`Skewness z = ${stat.format(data.skewz)} #pm ${stat.format(data.skewd)}`);
      } else if (print_skew > 0) {
         stat.addText(`Skewness x = ${stat.format(data.skewx)}`);
         stat.addText(`Skewness y = ${stat.format(data.skewy)}`);
         stat.addText(`Skewness z = ${stat.format(data.skewz)}`);
      }

      if (print_kurt === 2) {
         stat.addText(`Kurtosis x = ${stat.format(data.kurtx)} #pm ${stat.format(data.kurtd)}`);
         stat.addText(`Kurtosis y = ${stat.format(data.kurty)} #pm ${stat.format(data.kurtd)}`);
         stat.addText(`Kurtosis z = ${stat.format(data.kurtz)} #pm ${stat.format(data.kurtd)}`);
      } else if (print_kurt > 0) {
         stat.addText(`Kurtosis x = ${stat.format(data.kurtx)}`);
         stat.addText(`Kurtosis y = ${stat.format(data.kurty)}`);
         stat.addText(`Kurtosis z = ${stat.format(data.kurtz)}`);
      }

      if (dofit)
         stat.fillFunctionStat(this.findFunction(clTF3), dofit, 3);

      return true;
   }

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

      lines.push(this.getObjectHint(),
                 `x = ${this.getAxisBinTip('x', histo.fXaxis, ix)}  xbin=${ix + 1}`,
                 `y = ${this.getAxisBinTip('y', histo.fYaxis, iy)}  ybin=${iy + 1}`,
                 `z = ${this.getAxisBinTip('z', histo.fZaxis, iz)}  zbin=${iz + 1}`);

      const binz = histo.getBinContent(ix + 1, iy + 1, iz + 1);
      if (binz === Math.round(binz))
         lines.push(`entries = ${binz}`);
      else
         lines.push(`entries = ${floatToString(binz, gStyle.fStatFormat)}`);

      if (this.matchObjectType(clTProfile3D)) {
         const errz = histo.getBinError(histo.getBin(ix + 1, iy + 1, iz + 1));
         lines.push('error = ' + ((errz === Math.round(errz)) ? errz.toString() : floatToString(errz, gStyle.fPaintTextFormat)));
      }

      return lines;
   }

   /** @summary draw 3D histogram as scatter plot
     * @desc If there are too many points, box will be displayed
     * @return {Promise|false} either Promise or just false that drawing cannot be performed */
   draw3DScatter() {
      const histo = this.getObject(),
            fp = this.getFramePainter(),
            i1 = this.getSelectIndex('x', 'left', 0.5),
            i2 = this.getSelectIndex('x', 'right', 0),
            j1 = this.getSelectIndex('y', 'left', 0.5),
            j2 = this.getSelectIndex('y', 'right', 0),
            k1 = this.getSelectIndex('z', 'left', 0.5),
            k2 = this.getSelectIndex('z', 'right', 0);
      let i, j, k, bin_content;

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

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

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

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

      const pnts = new PointsCreator(numpixels, fp.webgl, fp.size_x3d / 200),
            bins = new Int32Array(numpixels),
            rnd = new TRandom(sumz);
      let nbin = 0;

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

               for (let n = 0; n < num; ++n) {
                  const binx = histo.fXaxis.GetBinCoord(i + rnd.random()),
                        biny = histo.fYaxis.GetBinCoord(j + rnd.random()),
                        binz = histo.fZaxis.GetBinCoord(k + rnd.random());

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

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

      return pnts.createPoints({ color: this.getColor(histo.fMarkerColor) }).then(mesh => {
         fp.add3DMesh(mesh);

         mesh.bins = bins;
         mesh.tip_painter = this;
         mesh.tip_color = histo.fMarkerColor === 3 ? 0xFF0000 : 0x00FF00;

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

            const p = this.tip_painter,
                  thisto = p.getHisto(),
                  tip = p.get3DToolTip(this.bins[indx]);

            tip.x1 = fp.grx(thisto.fXaxis.GetBinLowEdge(tip.ix));
            tip.x2 = fp.grx(thisto.fXaxis.GetBinLowEdge(tip.ix + 1));
            tip.y1 = fp.gry(thisto.fYaxis.GetBinLowEdge(tip.iy));
            tip.y2 = fp.gry(thisto.fYaxis.GetBinLowEdge(tip.iy + 1));
            tip.z1 = fp.grz(thisto.fZaxis.GetBinLowEdge(tip.iz));
            tip.z2 = fp.grz(thisto.fZaxis.GetBinLowEdge(tip.iz + 1));
            tip.color = this.tip_color;
            tip.opacity = 0.3;

            return tip;
         };

         return true;
      });
   }

   /** @summary Drawing of 3D histogram */
   async draw3DBins() {
      if (!this.draw_content)
         return false;

      const o = this.getOptions();

      let box_option = o.BoxStyle;

      if (!box_option && o.Scat) {
         const promise = this.draw3DScatter();
         if (promise !== false)
            return promise;
         box_option = 12; // fall back to box2 draw option
      } else if (!box_option && !o.GLBox && !o.GLColor && !o.Lego)
         box_option = 12; // default draw option

      const histo = this.getHisto(),
            fp = this.getFramePainter();

      // ensure proper colors
      this.resetContour();

      let use_lambert = false,
          use_helper = false, use_colors = false, use_opacity = 1, exclude_content = -1,
          logv = this.getPadPainter()?.getRootPad()?.fLogv,
          use_scale = true, scale_offset = 0,
          fillcolor = this.getColor(histo.fFillColor),
          tipscale = 0.5, single_bin_geom;

      if (!box_option && o.Lego)
         box_option = (o.Lego === 1) ? 10 : o.Lego;

      if ((o.GLBox === 11) || (o.GLBox === 12)) {
         tipscale = 0.4;
         use_lambert = true;
         if (o.GLBox === 12)
            use_colors = true;

         single_bin_geom = new THREE.SphereGeometry(0.5, fp.webgl ? 16 : 8, fp.webgl ? 12 : 6);
         single_bin_geom.applyMatrix4(new THREE.Matrix4().makeRotationX(Math.PI / 2));
         single_bin_geom.computeVertexNormals();
      } else {
         const indicies = Box3D.Indexes,
               normals = Box3D.Normals,
               vertices = Box3D.Vertices,
               buffer_size = indicies.length * 3,
               single_bin_verts = new Float32Array(buffer_size),
               single_bin_norms = new Float32Array(buffer_size);

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

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

         if (box_option === 12)
            use_colors = true;
         else if (box_option === 13) {
            use_colors = true;
            use_helper = false;
         } else if (o.GLColor) {
            use_colors = true;
            use_opacity = 0.5;
            use_scale = false;
            use_helper = false;
            exclude_content = 0;
            use_lambert = true;
         }

         single_bin_geom = new THREE.BufferGeometry();
         single_bin_geom.setAttribute('position', new THREE.BufferAttribute(single_bin_verts, 3));
         single_bin_geom.setAttribute('normal', new THREE.BufferAttribute(single_bin_norms, 3));
      }

      this.#box_option = box_option;

      if (use_scale && logv) {
         if (this.gminposbin && (this.gmaxbin > this.gminposbin)) {
            scale_offset = Math.log(this.gminposbin) - 0.1;
            use_scale = 1 / (Math.log(this.gmaxbin) - scale_offset);
         } else {
            logv = 0;
            use_scale = 1;
         }
      } else if (use_scale)
         use_scale = (this.gminbin || this.gmaxbin) ? 1 / Math.max(Math.abs(this.gminbin), Math.abs(this.gmaxbin)) : 1;

      const get_bin_weight = content => {
         if ((exclude_content >= 0) && (content < exclude_content))
            return 0;
         if (!use_scale)
            return 1;
         if (logv) {
            if (content <= 0)
               return 0;
            content = Math.log(content) - scale_offset;
         }
         return Math.pow(Math.abs(content * use_scale), 0.3333);
      };
      // eslint-disable-next-line one-var
      const i1 = this.getSelectIndex('x', 'left', 0.5),
            i2 = this.getSelectIndex('x', 'right', 0),
            j1 = this.getSelectIndex('y', 'left', 0.5),
            j2 = this.getSelectIndex('y', 'right', 0),
            k1 = this.getSelectIndex('z', 'left', 0.5),
            k2 = this.getSelectIndex('z', 'right', 0);

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

      const cntr = use_colors ? this.getContour() : null,
            palette = use_colors ? this.getHistPalette() : null,
            bins_matrixes = [], bins_colors = [], bins_ids = [], negative_matrixes = [], bin_opacities = [],
            transfer = (this.transferFunc && proivdeEvalPar(this.transferFunc, true)) ? this.transferFunc : null;

      for (let i = i1; i < i2; ++i) {
         const grx1 = fp.grx(histo.fXaxis.GetBinLowEdge(i + 1)),
               grx2 = fp.grx(histo.fXaxis.GetBinLowEdge(i + 2));
         for (let j = j1; j < j2; ++j) {
            const gry1 = fp.gry(histo.fYaxis.GetBinLowEdge(j + 1)),
                  gry2 = fp.gry(histo.fYaxis.GetBinLowEdge(j + 2));
            for (let k = k1; k < k2; ++k) {
               const bin_content = histo.getBinContent(i + 1, j + 1, k + 1);
               if (!o.GLColor && ((bin_content === 0) || (bin_content < this.gminbin)))
                  continue;

               const wei = get_bin_weight(bin_content);
               if (wei < 1e-3)
                  continue; // do not show very small bins

               if (use_colors) {
                  const colindx = cntr.getPaletteIndex(palette, bin_content);
                  if (colindx === null)
                     continue;
                  bins_colors.push(palette.getColor(colindx));
                  if (transfer) {
                     const op = getTF1Value(transfer, bin_content, false) * 3;
                     bin_opacities.push((!op || op < 0) ? 0 : (op > 1 ? 1 : op));
                  }
               }

               const grz1 = fp.grz(histo.fZaxis.GetBinLowEdge(k + 1)),
                     grz2 = fp.grz(histo.fZaxis.GetBinLowEdge(k + 2));

               // remember bin index for tooltip
               bins_ids.push(histo.getBin(i + 1, j + 1, k + 1));

               const bin_matrix = new THREE.Matrix4();
               bin_matrix.scale(new THREE.Vector3((grx2 - grx1) * wei, (gry2 - gry1) * wei, (grz2 - grz1) * wei));
               bin_matrix.setPosition((grx2 + grx1) / 2, (gry2 + gry1) / 2, (grz2 + grz1) / 2);
               bins_matrixes.push(bin_matrix);
               if (bin_content < 0)
                  negative_matrixes.push(bin_matrix);
            }
         }
      }

      function _getBinTooltip(intersect) {
         let binid = this.binid;

         if (binid === undefined) {
            if ((intersect.instanceId === undefined) || (intersect.instanceId >= this.bins.length))
               return;
            binid = this.bins[intersect.instanceId];
         }

         const p = this.tip_painter,
               thisto = p.getHisto(),
               tip = p.get3DToolTip(binid),
               grx1 = fp.grx(thisto.fXaxis.GetBinCoord(tip.ix - 1)),
               grx2 = fp.grx(thisto.fXaxis.GetBinCoord(tip.ix)),
               gry1 = fp.gry(thisto.fYaxis.GetBinCoord(tip.iy - 1)),
               gry2 = fp.gry(thisto.fYaxis.GetBinCoord(tip.iy)),
               grz1 = fp.grz(thisto.fZaxis.GetBinCoord(tip.iz - 1)),
               grz2 = fp.grz(thisto.fZaxis.GetBinCoord(tip.iz)),
               wei2 = this.get_weight(tip.value) * this.tipscale;

         tip.x1 = (grx2 + grx1) / 2 - (grx2 - grx1) * wei2;
         tip.x2 = (grx2 + grx1) / 2 + (grx2 - grx1) * wei2;
         tip.y1 = (gry2 + gry1) / 2 - (gry2 - gry1) * wei2;
         tip.y2 = (gry2 + gry1) / 2 + (gry2 - gry1) * wei2;
         tip.z1 = (grz2 + grz1) / 2 - (grz2 - grz1) * wei2;
         tip.z2 = (grz2 + grz1) / 2 + (grz2 - grz1) * wei2;
         tip.color = this.tip_color;

         return tip;
      }

      if (use_colors && (transfer || (use_opacity !== 1))) {
         // create individual meshes for each bin
         for (let n = 0; n < bins_matrixes.length; ++n) {
            const opacity = transfer ? bin_opacities[n] : use_opacity,
                  color = new THREE.Color(bins_colors[n]),
                  material = use_lambert ? new THREE.MeshLambertMaterial({ color, opacity, transparent: opacity < 1, vertexColors: false })
                                         : new THREE.MeshBasicMaterial({ color, opacity, transparent: opacity < 1, vertexColors: false }),
                  bin_mesh = new THREE.Mesh(single_bin_geom, material);

            bin_mesh.applyMatrix4(bins_matrixes[n]);

            bin_mesh.tip_painter = this;
            bin_mesh.binid = bins_ids[n];
            bin_mesh.tipscale = tipscale;
            bin_mesh.tip_color = (histo.fFillColor === 3) ? 0xFF0000 : 0x00FF00;
            bin_mesh.get_weight = get_bin_weight;
            bin_mesh.tooltip = _getBinTooltip;

            fp.add3DMesh(bin_mesh);
         }
      } else {
         if (use_colors)
            fillcolor = new THREE.Color(1, 1, 1);

         const material = use_lambert ? new THREE.MeshLambertMaterial({ color: fillcolor, vertexColors: false })
                                      : new THREE.MeshBasicMaterial({ color: fillcolor, vertexColors: false }),
               all_bins_mesh = new THREE.InstancedMesh(single_bin_geom, material, bins_matrixes.length);

         for (let n = 0; n < bins_matrixes.length; ++n) {
            all_bins_mesh.setMatrixAt(n, bins_matrixes[n]);
            if (use_colors)
               all_bins_mesh.setColorAt(n, new THREE.Color(bins_colors[n]));
         }

         all_bins_mesh.tip_painter = this;
         all_bins_mesh.bins = bins_ids;
         all_bins_mesh.tipscale = tipscale;
         all_bins_mesh.tip_color = (histo.fFillColor === 3) ? 0xFF0000 : 0x00FF00;
         all_bins_mesh.get_weight = get_bin_weight;
         all_bins_mesh.tooltip = _getBinTooltip;

         fp.add3DMesh(all_bins_mesh);
      }

      if (use_helper) {
         const helper_material = new THREE.LineBasicMaterial({ color: this.getColor(histo.fLineColor) });
         function addLines(segments, matrixes) {
            if (!matrixes)
               return;
            const positions = new Float32Array(matrixes.length * segments.length * 3);
            for (let i = 0, vvv = 0; i < matrixes.length; ++i) {
               const m = matrixes[i].elements;
               for (let n = 0; n < segments.length; ++n, vvv += 3) {
                  const vert = Box3D.Vertices[segments[n]];
                  positions[vvv] = m[12] + (vert.x - 0.5) * m[0];
                  positions[vvv + 1] = m[13] + (vert.y - 0.5) * m[5];
                  positions[vvv + 2] = m[14] + (vert.z - 0.5) * m[10];
               }
            }
            fp.add3DMesh(createLineSegments(positions, helper_material));
         }

         addLines(Box3D.Segments, bins_matrixes);
         addLines(Box3D.Crosses, negative_matrixes);
      }

      return true;
   }


   /** @summary Redraw TH3 histogram */
   async redraw(reason) {
      const fp = this.getFramePainter(), // who makes axis and 3D drawing
            o = this.getOptions();

      let pr = Promise.resolve(true), full_draw = true;

      if (reason === 'resize') {
         const res = fp.resize3D();
         if (res !== 1) {
            full_draw = false;
            if (res)
               fp.render3D();
         }
      }

      if (full_draw) {
         pr = crete3DFrame(this, TAxisPainter, o.Render3D)
              .then(() => this.draw3DBins())
              .then(() => {
                 fp.render3D();
                 this.updateStatWebCanvas();
                 fp.addKeysHandler();
              });
      }

      if (this.isMainPainter())
         pr = pr.then(() => this.drawColorPalette(o.Zscale && (this.#box_option === 12 || this.#box_option === 13 || o.GLBox === 12)));

      return pr.then(() => this.updateFunctions())
               .then(() => this.updateHistTitle())
               .then(() => this);
   }

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

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

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

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

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

      // first find minimum
      let min = histo.getBinContent(i1 + 1, j1 + 1, k1 + 1);
      for (i = i1; i < i2; ++i) {
         for (j = j1; j < j2; ++j) {
            for (k = k1; k < k2; ++k)
               min = Math.min(min, histo.getBinContent(i + 1, j + 1, k + 1));
         }
      }
      if (min > 0)
         return; // if all points positive, no chance for auto-scale

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

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

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

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

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

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

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

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

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

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

         this.decodeOptions(arg);

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

   /** @summary Build three.js object for the histogram */
   static async build3d(histo, opt) {
      const painter = new TH3Painter(null, histo);
      painter.mode3d = true;
      painter.decodeOptions(opt);
      painter.scanContent();

      const fp = new TFramePainter(null, null);
      painter.getFramePainter = () => fp;

      return crete3DFrame(painter, TAxisPainter)
             .then(() => painter.draw3DBins())
             .then(() => fp.create3DScene(-1, true));
   }

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

      return ensureTCanvas(painter, '3d').then(() => {
         painter.setAsMainPainter();
         painter.decodeOptions(opt);
         painter.checkPadRange();
         painter.scanContent();
         painter.createStat(); // only when required
         return painter.redraw();
      })
      .then(() => painter.drawFunctions())
      .then(() => {
         painter.fillToolbar();
         return painter;
      });
   }

} // class TH3Painter

var TH3Painter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TH3Painter: TH3Painter
});

const kNotEditable = BIT(18),   // bit set if graph is non editable
      clTGraphErrors = 'TGraphErrors',
      clTGraphAsymmErrors = 'TGraphAsymmErrors',
      clTGraphBentErrors = 'TGraphBentErrors',
      clTGraphMultiErrors = 'TGraphMultiErrors';

/**
 * @summary Painter for TGraph object.
 *
 * @private
 */

let TGraphPainter$1 = class TGraphPainter extends ObjectPainter {

   #bins;          // extracted graph bins
   #barwidth;      // width of each bar
   #baroffset;     // offset of each bar
   #redraw_hist;   // indicate that histogram need to be redrawn
   #auto_exec;     // can be reused when sending option back to server
   #funcs_handler; // special instance for functions drawing
   #frame_layer;   // frame layer used for drawing
   #cutg;          // is cutg object
   #cutg_lastsame; // indicate that last point is same as first
   #own_histogram; // if histogram created by TGraphPainter
   #marker_size;   // used marker size
   #move_binindx;  // index of moving bin
   #move_funcs;    // moving functions
   #move_bin;      // moving bin
   #move_x0;       // initial x position
   #move_y0;       // initial y position
   #pos_dx;        // accumulated x change
   #pos_dy;        // accumulated y change
   #has_errors;    // if has errors
   #is_bent;       // if graph has bent errors
   #draw_kind;     // way how graph is drawn

   constructor(dom, graph) {
      super(dom, graph);
      this.axes_draw = false; // indicate if graph histogram was drawn for axes
      this.xmin = this.ymin = this.xmax = this.ymax = 0;
      this.#is_bent = (graph._typename === clTGraphBentErrors);
      this.#has_errors = (graph._typename === clTGraphErrors) ||
                         (graph._typename === clTGraphMultiErrors) ||
                         (graph._typename === clTGraphAsymmErrors) ||
                          this.#is_bent || graph._typename.match(/^RooHist/);
      this.#draw_kind = '';
   }

   /** @summary Use in frame painter to check zoom Y is allowed
    * @protected */
   get _wheel_zoomy() { return true; }

   /** @summary Return drawn graph object */
   getGraph() { return this.getObject(); }

   /** @summary Return histogram object used for axis drawings */
   getHistogram() { return this.getObject()?.fHistogram; }

   /** @summary Return true if histogram not present or has dummy ranges (for requested axis) */
   isDummyHistogram(check_axis) {
      const histo = this.getHistogram();
      if (!histo)
         return true;

      let is_normal = false;
      if (check_axis !== 'y') {
         is_normal ||= ((histo.fXaxis.fXmin !== 0.0011) && (histo.fXaxis.fXmin !== 0)) ||
                       ((histo.fXaxis.fXmax !== 1.1) && (histo.fXaxis.fXmax !== 1));
      }

      if (check_axis !== 'x') {
         is_normal ||= ((histo.fYaxis.fXmin !== 0.0011) && (histo.fYaxis.fXmin !== 0)) ||
                       ((histo.fYaxis.fXmax !== 1.1) && (histo.fYaxis.fXmax !== 1)) ||
                       (histo.fMinimum !== 0.0011) || (histo.fMaximum !== 1.1);
      }

      return !is_normal;
   }

   /** @summary Set histogram object to graph */
   setHistogram(histo) {
      const obj = this.getObject();
      if (obj)
         obj.fHistogram = histo;
   }

   /** @summary Is TScatter object */
   isScatter() { return false; }

   /** @summary Redraw graph
     * @desc may redraw histogram which was used to draw axes
     * @return {Promise} for ready */
   async redraw() {
      let promise = Promise.resolve(true);

      if (this.#redraw_hist) {
         this.#redraw_hist = undefined;
         const hist_painter = this.getMainPainter();
         if (hist_painter?.isSecondary(this) && this.axes_draw)
            promise = hist_painter.redraw();
      }

      return promise.then(() => this.drawGraph()).then(() => {
         const res = this.#funcs_handler?.drawNext(0) ?? this;
         this.#funcs_handler = undefined;
         return res;
      });
   }

   /** @summary Cleanup graph painter */
   cleanup() {
      this.#bins = undefined;
      this.#own_histogram = undefined;
      super.cleanup();
   }

   /** @summary Returns object if this drawing TGraphMultiErrors object */
   get_gme() {
      const graph = this.getGraph();
      return graph?._typename === clTGraphMultiErrors ? graph : null;
   }

   /** @summary Decode options */
   decodeOptions(opt, first_time) {
      if (isStr(opt) && (opt.indexOf('same ') === 0))
         opt = opt.slice(5);

      const graph = this.getGraph(),
            is_gme = Boolean(this.get_gme()),
            has_main = first_time ? Boolean(this.getMainPainter()) : !this.axes_draw;

      function decodeBlock(d, res) {
         Object.assign(res, { Line: 0, Curve: 0, Rect: 0, Mark: 0, Bar: 0, OutRange: 0, EF: 0, Fill: 0, MainError: 1, Ends: 1, ScaleErrX: 1 });

         if (is_gme && d.check('S=', true))
            res.ScaleErrX = d.partAsFloat();

         if (d.check('L'))
            res.Line = 1;
         if (d.check('F'))
            res.Fill = 1;
         if (d.check('CC'))
            res.Curve = 2; // draw all points without reduction
         if (d.check('C'))
            res.Curve = 1;
         if (d.check('*'))
            res.Mark = 103;
         if (d.check('P0'))
            res.Mark = 104;
         if (d.check('P'))
            res.Mark = 1;
         if (d.check('B')) {
            res.Bar = 1;
            res.Errors = 0;
         }
         if (d.check('Z')) {
            res.Errors = 1;
            res.Ends = 0;
         }
         if (d.check('||')) {
            res.Errors = 1;
            res.MainError = 0;
            res.Ends = 1;
         }
         if (d.check('[]')) {
            res.Errors = 1;
            res.MainError = 0;
            res.Ends = 2;
         }
         if (d.check('|>')) {
            res.Errors = 1;
            res.Ends = 3;
         }
         if (d.check('>')) {
            res.Errors = 1;
            res.Ends = 4;
         }
         if (d.check('0')) {
            res.Mark = 1;
            res.Errors = 1;
            res.OutRange = 1;
         }
         if (d.check('1') && (res.Bar === 1))
            res.Bar = 2;
         if (d.check('2')) {
            res.Rect = 1;
            res.Errors = 0;
         }
         if (d.check('3')) {
            res.EF = 1;
            res.Errors = 0;
         }
         if (d.check('4')) {
            res.EF = 2;
            res.Errors = 0;
         }
         if (d.check('5')) {
            res.Rect = 2;
            res.Errors = 0;
         }
         if (d.check('X'))
            res.Errors = 0;
      }

      const res = this.setOptions({
         Axis: '', NoOpt: 0, PadStats: false, PadPalette: false, original: opt,
         second_x: false, second_y: false, individual_styles: false
      });

      let blocks_gme = [];

      if (is_gme && opt) {
         if (opt.indexOf(';') > 0) {
            blocks_gme = opt.split(';');
            opt = blocks_gme.shift();
         } else if (opt.indexOf('_') > 0) {
            blocks_gme = opt.split('_');
            opt = blocks_gme.shift();
         }
      }

      let d = new DrawOptions(opt), hopt = '';

      PadDrawOptions.forEach(name => {
         if (d.check(name))
            hopt += ';' + name;
      });
      if (d.check('XAXIS_', true))
         hopt += ';XAXIS_' + d.part;
      if (d.check('YAXIS_', true))
         hopt += ';YAXIS_' + d.part;

      if (d.empty()) {
         res.original = has_main ? 'lp' : 'alp';
         d = new DrawOptions(res.original);
      }

      if (d.check('FILL_', 'color')) {
         res.graphFillColor = d.color;
         res.graphFillPattern = 1001;
      }

      if (d.check('FILLPAT_', true))
         res.graphFillPattern = d.partAsInt();

      if (d.check('LINE_', 'color'))
         res.graphLineColor = this.getColor(d.color);

      if (d.check('WIDTH_', true))
         res.graphLineWidth = d.partAsInt();

      if (d.check('NOOPT'))
         res.NoOpt = 1;

      if (d.check('POS3D_', true))
         res.pos3d = d.partAsInt() - 0.5;

      if (d.check('PFC') && !res._pfc)
         res._pfc = 2;
      if (d.check('PLC') && !res._plc)
         res._plc = 2;
      if (d.check('PMC') && !res._pmc)
         res._pmc = 2;

      if (d.check('A'))
         res.Axis = d.check('I') ? 'A;' : ' '; // I means invisible axis
      if (d.check('X+')) {
         res.Axis += 'X+';
         res.second_x = has_main;
      }
      if (d.check('Y+')) {
         res.Axis += 'Y+';
         res.second_y = has_main;
      }
      if (d.check('RX'))
         res.Axis += 'RX';
      if (d.check('RY'))
         res.Axis += 'RY';

      if (is_gme) {
         res.blocks = [];
         res.skip_errors_x0 = res.skip_errors_y0 = false;
         if (d.check('X0'))
            res.skip_errors_x0 = true;
         if (d.check('Y0'))
            res.skip_errors_y0 = true;
      }

      decodeBlock(d, res);

      if (is_gme && d.check('S'))
         res.individual_styles = true;

      if (res.Errors === undefined)
         res.Errors = this.#has_errors && (!is_gme || !blocks_gme.length) ? 1 : 0;

      // special case - one could use svg:path to draw many pixels (
      if ((res.Mark === 1) && (graph.fMarkerStyle === 1))
         res.Mark = 101;

      // if no drawing option is selected and if opt === '' nothing is done.
      if ((res.Line + res.Fill + res.Curve + res.Mark + res.Bar + res.EF + res.Rect + res.Errors === 0) && d.empty())
         res.Line = 1;

      if (this.matchObjectType(clTGraphErrors)) {
         const len = graph.fEX.length;
         let m = 0;
         for (let k = 0; k < len; ++k)
            m = Math.max(m, graph.fEX[k], graph.fEY[k]);
         if (m < 1e-100)
            res.Errors = 0;
      }

      this.#cutg = this.matchObjectType(clTCutG);
      this.#cutg_lastsame = this.#cutg && (graph.fNpoints > 3) &&
                            (graph.fX[0] === graph.fX[graph.fNpoints - 1]) && (graph.fY[0] === graph.fY[graph.fNpoints - 1]);

      if (!res.Axis) {
         // check if axis should be drawn
         // either graph drawn directly or
         // graph is first object in list of primitives
         const pad = this.getPadPainter()?.getRootPad(true);
         if (!pad || (pad?.fPrimitives?.arr[0] === this.getObject()))
            res.Axis = ' ';
      }

      res.Axis += hopt;

      for (let bl = 0; bl < blocks_gme.length; ++bl) {
         const subd = new DrawOptions(blocks_gme[bl]), subres = {};
         decodeBlock(subd, subres);
         subres.skip_errors_x0 = res.skip_errors_x0;
         subres.skip_errors_y0 = res.skip_errors_y0;
         res.blocks.push(subres);
      }
   }

   /** @summary Return prepared graph bins
    * @protected */
   _getBins() { return this.#bins; }

   /** @summary Create bins for TF1 drawing */
   createBins() {
      const gr = this.getGraph(),
            o = this.getOptions();
      if (!gr)
         return;

      let kind = 0, npoints = gr.fNpoints;
      if (this.#cutg && this.#cutg_lastsame)
         npoints--;

      if (gr._typename === clTGraphErrors)
         kind = 1;
      else if (gr._typename === clTGraphMultiErrors)
         kind = 2;
      else if (gr._typename === clTGraphAsymmErrors || gr._typename === clTGraphBentErrors || gr._typename.match(/^RooHist/))
         kind = 3;

      this.#bins = new Array(npoints);

      for (let p = 0; p < npoints; ++p) {
         const bin = this.#bins[p] = { x: gr.fX[p], y: gr.fY[p], indx: p };
         switch (kind) {
            case 1:
               bin.exlow = bin.exhigh = gr.fEX[p];
               bin.eylow = bin.eyhigh = gr.fEY[p];
               break;
            case 2:
               bin.exlow = gr.fExL[p];
               bin.exhigh = gr.fExH[p];
               bin.eylow = gr.fEyL[0][p];
               bin.eyhigh = gr.fEyH[0][p];
               break;
            case 3:
               bin.exlow = gr.fEXlow[p];
               bin.exhigh = gr.fEXhigh[p];
               bin.eylow = gr.fEYlow[p];
               bin.eyhigh = gr.fEYhigh[p];
               break;
         }

         if (p === 0) {
            this.xmin = this.xmax = bin.x;
            this.ymin = this.ymax = bin.y;
         }

         if (kind > 0) {
            this.xmin = Math.min(this.xmin, bin.x - bin.exlow, bin.x + bin.exhigh);
            this.xmax = Math.max(this.xmax, bin.x - bin.exlow, bin.x + bin.exhigh);
            this.ymin = Math.min(this.ymin, bin.y - bin.eylow, bin.y + bin.eyhigh);
            this.ymax = Math.max(this.ymax, bin.y - bin.eylow, bin.y + bin.eyhigh);
         } else {
            this.xmin = Math.min(this.xmin, bin.x);
            this.xmax = Math.max(this.xmax, bin.x);
            this.ymin = Math.min(this.ymin, bin.y);
            this.ymax = Math.max(this.ymax, bin.y);
         }
      }

      // workaround, are there better way to show marker at 0,0 on the top of the frame?
      this.#frame_layer = true;
      if ((this.xmin === 0) && (this.ymin === 0) && (npoints > 0) && (this.#bins[0].x === 0) && (this.#bins[0].y === 0) &&
          o.Mark && !o.Line && !o.Curve && !o.Fill)
         this.#frame_layer = 'upper_layer';
   }

   /** @summary Return margins for histogram ranges */
   getHistRangeMargin() { return 0.1; }

   /** @summary Create histogram for graph
     * @desc graph bins should be created when calling this function
     * @param {boolean} [set_x] - set X axis range
     * @param {boolean} [set_y] - set Y axis range */
   createHistogram(set_x = true, set_y = true) {
      const graph = this.getGraph(),
            xmin = this.xmin,
            margin = this.getHistRangeMargin();
      let xmax = this.xmax, ymin = this.ymin, ymax = this.ymax;

      if (xmin >= xmax)
         xmax = xmin + 1;
      if (ymin >= ymax)
         ymax = ymin + 1;
      const dx = (xmax - xmin) * margin, dy = (ymax - ymin) * margin;
      let uxmin = xmin - dx, uxmax = xmax + dx,
          minimum = ymin - dy, maximum = ymax + dy;

      if ((ymin > 0) && (minimum <= 0))
         minimum = (1 - margin) * ymin;
      if ((ymax < 0) && (maximum >= 0))
         maximum = (1 - margin) * ymax;

      const minimum0 = minimum, maximum0 = maximum;
      let histo = this.getHistogram();

      if (!this.isScatter() && !histo?.fXaxis.fTimeDisplay) {
         const pad_logx = this.getPadPainter()?.getPadLog('x');

         if ((uxmin < 0) && (xmin >= 0))
            uxmin = pad_logx ? xmin * (1 - margin) : 0;
         if ((uxmax > 0) && (xmax <= 0))
            uxmax = pad_logx ? (1 + margin) * xmax : 0;
      }

      if (!histo) {
         histo = this.isScatter() ? createHistogram(clTH2F, 30, 30) : createHistogram(clTH1F, 100);
         histo.fName = graph.fName + '_h';
         histo.fBits |= kNoStats;
         this.#own_histogram = true;
         this.setHistogram(histo);
      } else if ((histo.fMaximum !== kNoZoom) && (histo.fMinimum !== kNoZoom) && !this.isDummyHistogram('y')) {
         minimum = histo.fMinimum;
         maximum = histo.fMaximum;
      }

      if (graph.fMinimum !== kNoZoom)
         minimum = ymin = graph.fMinimum;
      if (graph.fMaximum !== kNoZoom)
         maximum = graph.fMaximum;
      if ((minimum < 0) && (ymin >= 0))
         minimum = (1 - margin) * ymin;
      if ((ymax < 0) && (maximum >= 0))
         maximum = (1 - margin) * ymax;

      setHistogramTitle(histo, this.getObject().fTitle);

      if (set_x && !histo.fXaxis.fLabels) {
         histo.fXaxis.fXmin = uxmin;
         histo.fXaxis.fXmax = uxmax;
      }

      if (set_y && !histo.fYaxis.fLabels) {
         histo.fYaxis.fXmin = Math.min(minimum0, minimum);
         histo.fYaxis.fXmax = Math.max(maximum0, maximum);
         if (!this.isScatter()) {
            histo.fMinimum = minimum;
            histo.fMaximum = maximum;
         }
      }

      histo.$xmin_nz = xmin > 0 ? xmin : undefined;
      histo.$ymin_nz = ymin > 0 ? ymin : undefined;

      return histo;
   }

   /** @summary Check if user range can be un-zommed
     * @desc Used when graph points covers larger range than provided histogram */
   unzoomUserRange(dox, doy /* , doz */) {
      const graph = this.getGraph();
      if (this.#own_histogram || !graph)
         return false;

      const histo = this.getHistogram();

      dox = dox && histo && ((histo.fXaxis.fXmin > this.xmin) || (histo.fXaxis.fXmax < this.xmax));
      doy = doy && histo && ((histo.fYaxis.fXmin > this.ymin) || (histo.fYaxis.fXmax < this.ymax));
      if (!dox && !doy)
         return false;

      this.createHistogram(dox, doy);
      this.getMainPainter()?.extractAxesProperties(1); // just to enforce ranges extraction

      return true;
   }

   /** @summary Returns true if graph drawing can be optimize */
   canOptimize() {
      return (settings.OptimizeDraw > 0) && !this.getOptions().NoOpt;
   }

   /** @summary Returns optimized bins - if optimization enabled */
   optimizeBins(maxpnt, filter_func) {
      if ((this.#bins.length < 30) && !filter_func)
         return this.#bins;

      let selbins = null;
      if (isFunc(filter_func)) {
         for (let n = 0; n < this.#bins.length; ++n) {
            if (filter_func(this.#bins[n], n)) {
               if (!selbins)
                  selbins = (n === 0) ? [] : this.#bins.slice(0, n);
            } else if (selbins)
               selbins.push(this.#bins[n]);
         }
      }
      if (!selbins)
         selbins = this.#bins;

      if (!maxpnt)
         maxpnt = 500000;

      if ((selbins.length < maxpnt) || !this.canOptimize())
         return selbins;
      const optbins = [], step = Math.max(2, Math.floor(selbins.length / maxpnt));
      for (let n = 0; n < selbins.length; n += step)
         optbins.push(selbins[n]);

      return optbins;
   }

   /** @summary Check if such function should be drawn directly */
   needDrawFunc(graph, func) {
      if (func._typename === clTPaveStats)
         return (func.fName !== 'stats') || !graph.TestBit(kNoStats); // kNoStats is same for graph and histogram

      if ((func._typename === clTF1) || (func._typename === clTF2))
         return !func.TestBit(BIT(9)); // TF1::kNotDraw

      return true;
   }

   /** @summary Returns tooltip for specified bin */
   getTooltips(d) {
      const fp = this.get_fp(), lines = [], o = this.getOptions(),
            funcs = fp.getGrFuncs(o.second_x, o.second_y),
            gme = this.get_gme();

      lines.push(this.getObjectHint());

      if (d && funcs) {
         if (d.indx !== undefined)
            lines.push('p = ' + d.indx);
         lines.push('x = ' + funcs.axisAsText('x', d.x), 'y = ' + funcs.axisAsText('y', d.y));
         if (gme)
            lines.push('error x = -' + funcs.axisAsText('x', gme.fExL[d.indx]) + '/+' + funcs.axisAsText('x', gme.fExH[d.indx]));
         else if (o.Errors && (funcs.x_handle.kind === kAxisNormal) && (d.exlow || d.exhigh))
            lines.push('error x = -' + funcs.axisAsText('x', d.exlow) + '/+' + funcs.axisAsText('x', d.exhigh));

         if (gme) {
            for (let ny = 0; ny < gme.fNYErrors; ++ny)
               lines.push(`error y${ny} = -${funcs.axisAsText('y', gme.fEyL[ny][d.indx])}/+${funcs.axisAsText('y', gme.fEyH[ny][d.indx])}`);
         } else if ((o.Errors || (o.EF > 0)) && (funcs.y_handle.kind === kAxisNormal) && (d.eylow || d.eyhigh))
            lines.push('error y = -' + funcs.axisAsText('y', d.eylow) + '/+' + funcs.axisAsText('y', d.eyhigh));
      }
      return lines;
   }

   /** @summary Provide frame painter for graph
     * @desc If not exists, emulate its behavior */
   get_fp() {
      let fp = this.getFramePainter();

      if (fp?.grx && fp?.gry)
         return fp;

      // FIXME: check if needed, can be removed easily
      const pp = this.getPadPainter(),
            rect = pp?.getPadRect() || { width: 800, height: 600 };

      fp = {
         pad_layer: true,
         pad: pp?.getRootPad(true) ?? create$1(clTPad),
         pw: rect.width,
         ph: rect.height,
         fX1NDC: 0.1, fX2NDC: 0.9, fY1NDC: 0.1, fY2NDC: 0.9,
         getFrameWidth() { return this.pw; },
         getFrameHeight() { return this.ph; },
         grx(value) {
            if (this.pad.fLogx)
               value = (value > 0) ? Math.log10(value) : this.pad.fUxmin;
            else
               value = (value - this.pad.fX1) / (this.pad.fX2 - this.pad.fX1);
            return value * this.pw;
         },
         gry(value) {
            if (this.pad.fLogv ?? this.pad.fLogy)
               value = (value > 0) ? Math.log10(value) : this.pad.fUymin;
            else
               value = (value - this.pad.fY1) / (this.pad.fY2 - this.pad.fY1);
            return (1 - value) * this.ph;
         },
         revertAxis(name, v) {
            if (name === 'x')
               return v / this.pw * (this.pad.fX2 - this.pad.fX1) + this.pad.fX1;
            if (name === 'y')
               return (1 - v / this.ph) * (this.pad.fY2 - this.pad.fY1) + this.pad.fY1;
            return v;
         },
         getGrFuncs() { return this; }
      };

      return fp;
   }

   /** @summary append exclusion area to created path */
   appendExclusion(is_curve, path, drawbins, excl_width) {
      const extrabins = [];
      for (let n = drawbins.length - 1; n >= 0; --n) {
         const bin = drawbins[n],
               dlen = Math.sqrt(bin.dgrx ** 2 + bin.dgry ** 2);
         if (dlen > 1e-10) {
            // shift point
            bin.grx += excl_width * bin.dgry / dlen;
            bin.gry -= excl_width * bin.dgrx / dlen;
         }
         extrabins.push(bin);
      }

      const path2 = buildSvgCurve(extrabins, { cmd: 'L', line: !is_curve });

      this.appendPath(path + path2 + 'Z')
          .call(this.fillatt.func)
          .style('opacity', 0.75);
   }

   /** @summary draw TGraph bins with specified options
     * @desc Can be called several times */
   drawBins(funcs, options, draw_g, w, h, lineatt, fillatt, main_block) {
      const graph = this.getGraph();
      if (!graph?.fNpoints)
         return;

      let excl_width = 0, drawbins = null;
      // if markers or errors drawn - no need handle events for line drawing
      // this improves interactivity like zooming around graph points
      const line_events_handling = !this.isBatchMode() && (options.Line || options.Errors) ? 'none' : null;

      if (main_block && lineatt.excl_side) {
         excl_width = lineatt.excl_width;
         if ((lineatt.width > 0) && !options.Line && !options.Curve)
            options.Line = 1;
      }

      if (options.EF) {
         drawbins = this.optimizeBins((options.EF > 1) ? 20000 : 0);

         // build lower part
         for (let n = 0; n < drawbins.length; ++n) {
            const bin = drawbins[n];
            bin.grx = funcs.grx(bin.x);
            bin.gry = funcs.gry(bin.y - bin.eylow);
         }

         const path1 = buildSvgCurve(drawbins, { line: options.EF < 2, qubic: true }),
               bins2 = [];

         for (let n = drawbins.length - 1; n >= 0; --n) {
            const bin = drawbins[n];
            bin.gry = funcs.gry(bin.y + bin.eyhigh);
            bins2.push(bin);
         }

         // build upper part (in reverse direction)
         const path2 = buildSvgCurve(bins2, { line: options.EF < 2, cmd: 'L', qubic: true }),
               area = draw_g.append('svg:path')
                            .attr('d', path1 + path2 + 'Z')
                            .call(fillatt.func);

         // Let behaves as ROOT - see JIRA ROOT-8131
         if (fillatt.empty() && fillatt.colorindx)
            area.style('stroke', this.getColor(fillatt.colorindx));
         if (main_block)
            this.#draw_kind = 'lines';
      }

      if (options.Line || options.Fill) {
         let close_symbol = '';
         if (this.#cutg) {
            close_symbol = 'Z';
            if (!options.original)
               options.Fill = 1;
         }

         if (options.Fill) {
            close_symbol = 'Z'; // always close area if we want to fill it
            excl_width = 0;
         }

         if (!drawbins)
            drawbins = this.optimizeBins(0);

         for (let n = 0; n < drawbins.length; ++n) {
            const bin = drawbins[n];
            bin.grx = funcs.grx(bin.x);
            bin.gry = funcs.gry(bin.y);
         }

         const path = buildSvgCurve(drawbins, { line: true, calc: excl_width });

         if (excl_width)
            this.appendExclusion(false, path, drawbins, excl_width);

         const elem = draw_g.append('svg:path')
                            .attr('d', path + close_symbol)
                            .style('pointer-events', line_events_handling);
         if (options.Line)
            elem.call(lineatt.func);

         if (options.Fill)
            elem.call(fillatt.func);
         else
            elem.style('fill', 'none');

         if (main_block)
            this.#draw_kind = 'lines';
      }

      if (options.Curve) {
         let curvebins = drawbins;
         if ((this.#draw_kind !== 'lines') || !curvebins || ((options.Curve === 1) && (curvebins.length > 20000))) {
            curvebins = this.optimizeBins((options.Curve === 1) ? 20000 : 0);
            for (let n = 0; n < curvebins.length; ++n) {
               const bin = curvebins[n];
               bin.grx = funcs.grx(bin.x);
               bin.gry = funcs.gry(bin.y);
            }
         }

         const path = buildSvgCurve(curvebins, { qubic: !excl_width });
         if (excl_width)
            this.appendExclusion(true, path, curvebins, excl_width);

         draw_g.append('svg:path')
               .attr('d', path)
               .call(lineatt.func)
               .style('fill', 'none')
               .style('pointer-events', line_events_handling);
         if (main_block)
            this.#draw_kind = 'lines'; // handled same way as lines
      }

      let nodes = null;

      if (options.Errors || options.Rect || options.Bar) {
         drawbins = this.optimizeBins(5000, (pnt, i) => {
            const grx = funcs.grx(pnt.x);

            // when drawing bars, take all points
            if (!options.Bar && ((grx < 0) || (grx > w)))
               return true;

            const gry = funcs.gry(pnt.y);

            if (!options.Bar && !options.OutRange && ((gry < 0) || (gry > h)))
               return true;

            pnt.grx1 = Math.round(grx);
            pnt.gry1 = Math.round(gry);

            if (this.#has_errors) {
               pnt.grx0 = Math.round(funcs.grx(pnt.x - options.ScaleErrX * pnt.exlow) - grx);
               pnt.grx2 = Math.round(funcs.grx(pnt.x + options.ScaleErrX * pnt.exhigh) - grx);
               pnt.gry0 = Math.round(funcs.gry(pnt.y - pnt.eylow) - gry);
               pnt.gry2 = Math.round(funcs.gry(pnt.y + pnt.eyhigh) - gry);

               if (this.#is_bent) {
                  pnt.grdx0 = Math.round(funcs.gry(pnt.y + graph.fEXlowd[i]) - gry);
                  pnt.grdx2 = Math.round(funcs.gry(pnt.y + graph.fEXhighd[i]) - gry);
                  pnt.grdy0 = Math.round(funcs.grx(pnt.x + graph.fEYlowd[i]) - grx);
                  pnt.grdy2 = Math.round(funcs.grx(pnt.x + graph.fEYhighd[i]) - grx);
               } else
                  pnt.grdx0 = pnt.grdx2 = pnt.grdy0 = pnt.grdy2 = 0;
            }

            return false;
         });

         if (main_block)
            this.#draw_kind = 'nodes';

         nodes = draw_g.selectAll('.grpoint')
                       .data(drawbins)
                       .enter()
                       .append('svg:g')
                       .attr('class', 'grpoint')
                       .attr('transform', d => makeTranslate(d.grx1, d.gry1));
      }

      if (options.Bar) {
         // calculate bar width

         let xmin = 0, xmax = 0;
         for (let i = 0; i < drawbins.length; ++i) {
            if (i === 0)
               xmin = xmax = drawbins[i].grx1;
            else {
               xmin = Math.min(xmin, drawbins[i].grx1);
               xmax = Math.max(xmax, drawbins[i].grx1);
            }
         }

         const sz0 = drawbins.length < 2 ? w / 4 : (xmax - xmin) / drawbins.length,
               bw = sz0 * gStyle.fBarWidth,
               boff = sz0 * gStyle.fBarOffset,
               yy0 = Math.round(funcs.gry(0));
         let usefill = fillatt;

         if (main_block) {
            const fp = this.getFramePainter(),
                  fpcol = !fp?.fillatt?.empty() ? fp.fillatt.getFillColor() : -1;

            if (fpcol === fillatt.getFillColor())
               usefill = this.createAttFill({ color: fpcol === 'white' ? kBlack : kWhite, pattern: 1001, std: false });
         }

         nodes.append('svg:path')
              .attr('d', d => {
                 d.bar = true; // element drawn as bar
                 const dx = bw > 1 ? Math.round(boff - bw / 2) : 0,
                       dw = bw > 1 ? Math.round(bw) : 1,
                       dy = (options.Bar !== 1) ? 0 : ((d.gry1 > yy0) ? yy0 - d.gry1 : 0),
                       dh = (options.Bar !== 1) ? (h > d.gry1 ? h - d.gry1 : 0) : Math.abs(yy0 - d.gry1);
                 return `M${dx},${dy}h${dw}v${dh}h${-dw}z`;
              })
            .call(usefill.func);

         this.#barwidth = bw;
         this.#baroffset = boff;
      }

      if (options.Rect) {
         nodes.filter(d => (d.exlow > 0) && (d.exhigh > 0) && (d.eylow > 0) && (d.eyhigh > 0))
           .append('svg:path')
           .attr('d', d => {
              d.rect = true;
              return `M${d.grx0},${d.gry0}H${d.grx2}V${d.gry2}H${d.grx0}Z`;
           })
           .call(fillatt.func)
           .call(options.Rect === 2 ? lineatt.func : () => {});
      }

      this.error_size = 0;

      if (options.Errors) {
         // to show end of error markers, use line width attribute
         let lw = lineatt.width + gStyle.fEndErrorSize;
         const vv = options.Ends ? `m0,${lw}v${ -2 * lw}` : '',
               hh = options.Ends ? `m${lw},0h${ -2 * lw}` : '';
         let vleft = vv, vright = vv, htop = hh, hbottom = hh, bb;

         const mainLine = (dx, dy) => {
            if (!options.MainError)
               return `M${dx},${dy}`;
            const res = 'M0,0';
            if (dx)
               return res + (dy ? `L${dx},${dy}` : `H${dx}`);
            return dy ? res + `V${dy}` : res;
         };

         switch (options.Ends) {
            case 2:  // option []
               bb = Math.max(lineatt.width + 1, Math.round(lw * 0.66));
               vleft = `m${bb},${lw}h${-bb}v${ -2 * lw}h${bb}`;
               vright = `m${-bb},${lw}h${bb}v${ -2 * lw}h${-bb}`;
               htop = `m${-lw},${bb}v${-bb}h${2 * lw}v${bb}`;
               hbottom = `m${-lw},${-bb}v${bb}h${2 * lw}v${-bb}`;
               break;
            case 3: // option |>
               lw = Math.max(lw, Math.round(graph.fMarkerSize * 8 * 0.66));
               bb = Math.max(lineatt.width + 1, Math.round(lw * 0.66));
               vleft = `l${bb},${lw}v${ -2 * lw}l${-bb},${lw}`;
               vright = `l${-bb},${lw}v${ -2 * lw}l${bb},${lw}`;
               htop = `l${-lw},${bb}h${2 * lw}l${-lw},${-bb}`;
               hbottom = `l${-lw},${-bb}h${2 * lw}l${-lw},${bb}`;
               break;
            case 4: // option >
               lw = Math.max(lw, Math.round(graph.fMarkerSize * 8 * 0.66));
               bb = Math.max(lineatt.width + 1, Math.round(lw * 0.66));
               vleft = `l${bb},${lw}m0,${ -2 * lw}l${-bb},${lw}`;
               vright = `l${-bb},${lw}m0,${ -2 * lw}l${bb},${lw}`;
               htop = `l${-lw},${bb}m${2 * lw},0l${-lw},${-bb}`;
               hbottom = `l${-lw},${-bb}m${2 * lw},0l${-lw},${bb}`;
               break;
         }

         this.error_size = lw;

         lw = Math.floor((lineatt.width - 1) / 2); // one should take into account half of end-cup line width

         let visible = nodes.filter(d => (d.exlow > 0) || (d.exhigh > 0) || (d.eylow > 0) || (d.eyhigh > 0));
         if (options.skip_errors_x0 || options.skip_errors_y0)
            visible = visible.filter(d => (d.x || !options.skip_errors_x0) && (d.y || !options.skip_errors_y0));

         if (!this.isBatchMode() && settings.Tooltip && main_block) {
            visible.append('svg:path')
                   .attr('d', d => `M${d.grx0},${d.gry0}h${d.grx2 - d.grx0}v${d.gry2 - d.gry0}h${d.grx0 - d.grx2}z`)
                   .style('fill', 'none')
                   .style('pointer-events', 'visibleFill');
         }

         visible.append('svg:path')
                .attr('d', d => {
                   d.error = true;
                   return ((d.exlow > 0) ? mainLine(d.grx0 + lw, d.grdx0) + vleft : '') +
                          ((d.exhigh > 0) ? mainLine(d.grx2 - lw, d.grdx2) + vright : '') +
                          ((d.eylow > 0) ? mainLine(d.grdy0, d.gry0 - lw) + hbottom : '') +
                          ((d.eyhigh > 0) ? mainLine(d.grdy2, d.gry2 + lw) + htop : '');
                })
                .style('fill', 'none')
                .call(lineatt.func);
      }

      if (options.Mark) {
         // for tooltips use markers only if nodes were not created
         this.createAttMarker({ attr: graph, style: options.Mark - 100 });

         this.#marker_size = this.markeratt.getFullSize();

         this.markeratt.resetPos();

         const want_tooltip = !this.isBatchMode() && settings.Tooltip && (!this.markeratt.fill || (this.#marker_size < 7)) && !nodes && main_block,
               hsz = Math.max(5, Math.round(this.#marker_size * 0.7)),
               maxnummarker = 1000000 / (this.markeratt.getMarkerLength() + 7); // let produce SVG at maximum 1MB

         let path = '', pnt, grx, gry,
             hints_marker = '', step = 1;

         if (!drawbins)
            drawbins = this.optimizeBins(maxnummarker);
         else if (this.canOptimize() && (drawbins.length > 1.5 * maxnummarker))
            step = Math.min(2, Math.round(drawbins.length / maxnummarker));

         for (let n = 0; n < drawbins.length; n += step) {
            pnt = drawbins[n];
            grx = funcs.grx(pnt.x);
            if ((grx > -this.#marker_size) && (grx < w + this.#marker_size)) {
               gry = funcs.gry(pnt.y);
               if ((gry > -this.#marker_size) && (gry < h + this.#marker_size)) {
                  path += this.markeratt.create(grx, gry);
                  if (want_tooltip)
                     hints_marker += `M${grx - hsz},${gry - hsz}h${2 * hsz}v${2 * hsz}h${ -2 * hsz}z`;
               }
            }
         }

         if (path) {
            draw_g.append('svg:path')
                  .attr('d', path)
                  .call(this.markeratt.func);
            if ((nodes === null) && (this.#draw_kind === 'none') && main_block)
               this.#draw_kind = (options.Mark === 101) ? 'path' : 'mark';
         }
         if (want_tooltip && hints_marker) {
            draw_g.append('svg:path')
                  .attr('d', hints_marker)
                  .style('fill', 'none')
                  .style('pointer-events', 'visibleFill');
         }
      }
   }

   /** @summary append TGraphQQ part */
   appendQQ(funcs, graph) {
      const xqmin = Math.max(funcs.scale_xmin, graph.fXq1),
            xqmax = Math.min(funcs.scale_xmax, graph.fXq2),
            yqmin = Math.max(funcs.scale_ymin, graph.fYq1),
            yqmax = Math.min(funcs.scale_ymax, graph.fYq2),
            makeLine = (x1, y1, x2, y2) => `M${funcs.grx(x1)},${funcs.gry(y1)}L${funcs.grx(x2)},${funcs.gry(y2)}`,
            yxmin = (graph.fYq2 - graph.fYq1) * (funcs.scale_xmin - graph.fXq1) / (graph.fXq2 - graph.fXq1) + graph.fYq1,
            yxmax = (graph.fYq2 - graph.fYq1) * (funcs.scale_xmax - graph.fXq1) / (graph.fXq2 - graph.fXq1) + graph.fYq1;

      let path2;
      if (yxmin < funcs.scale_ymin) {
         const xymin = (graph.fXq2 - graph.fXq1) * (funcs.scale_ymin - graph.fYq1) / (graph.fYq2 - graph.fYq1) + graph.fXq1;
         path2 = makeLine(xymin, funcs.scale_ymin, xqmin, yqmin);
      } else
         path2 = makeLine(funcs.scale_xmin, yxmin, xqmin, yqmin);

      if (yxmax > funcs.scale_ymax) {
         const xymax = (graph.fXq2 - graph.fXq1) * (funcs.scale_ymax - graph.fYq1) / (graph.fYq2 - graph.fYq1) + graph.fXq1;
         path2 += makeLine(xqmax, yqmax, xymax, funcs.scale_ymax);
      } else
         path2 += makeLine(xqmax, yqmax, funcs.scale_xmax, yxmax);

      const latt1 = this.createAttLine({ style: 1, width: 1, color: kBlack, std: false }),
            latt2 = this.createAttLine({ style: 2, width: 1, color: kBlack, std: false });

      this.appendPath(makeLine(xqmin, yqmin, xqmax, yqmax))
          .call(latt1.func)
          .style('fill', 'none');

      this.appendPath(path2)
          .call(latt2.func)
          .style('fill', 'none');
   }

   drawBins3D(/* fp, graph */) {
      console.log('Load ./hist/TGraphPainter.mjs to draw graph in 3D');
   }

   /** @summary Create necessary histogram draw attributes */
   createGraphDrawAttributes(only_check_auto) {
      const graph = this.getGraph(), o = this.getOptions();
      if (o._pfc > 1 || o._plc > 1 || o._pmc > 1) {
         const pp = this.getPadPainter();
         if (isFunc(pp?.getAutoColor)) {
            const icolor = pp.getAutoColor(graph.$num_graphs);
            this.#auto_exec = ''; // can be reused when sending option back to server
            if (o._pfc > 1) {
               o._pfc = 1;
               graph.fFillColor = icolor;
               this.#auto_exec += `SetFillColor(${icolor});;`;
               this.deleteAttr('fill');
            }
            if (o._plc > 1) {
               o._plc = 1;
               graph.fLineColor = icolor;
               this.#auto_exec += `SetLineColor(${icolor});;`;
               this.deleteAttr('line');
            }
            if (o._pmc > 1) {
               o._pmc = 1;
               graph.fMarkerColor = icolor;
               this.#auto_exec += `SetMarkerColor(${icolor});;`;
               this.deleteAttr('marker');
            }
         }
      }

      if (only_check_auto)
         this.deleteAttr();
      else {
         this.createAttLine({ attr: graph, can_excl: true, color0: o.graphLineColor, width: o.graphLineWidth });
         this.createAttFill({ attr: graph, color: o.graphFillColor, pattern: o.graphFillPattern, });
      }
   }

   /** @summary draw TGraph */
   drawGraph() {
      const fp = this.get_fp(),
            graph = this.getGraph(),
            o = this.getOptions();
      if (!fp)
         return;

      // special mode for TMultiGraph 3d drawing
      if (o.pos3d)
         return this.drawBins3D(fp, graph);

      const is_gme = Boolean(this.get_gme()),
            funcs = fp.getGrFuncs(o.second_x, o.second_y),
            w = funcs.getFrameWidth(),
            h = funcs.getFrameHeight(),
            g = this.createG(fp.pad_layer ? false : this.#frame_layer);

      this.createGraphDrawAttributes();

      this.fillatt.used = false; // mark used only when really used

      this.#draw_kind = 'none'; // indicate if special svg:g were created for each bin
      this.#marker_size = 0; // indicate if markers are drawn
      const draw_g = is_gme ? g.append('svg:g') : g;

      this.drawBins(funcs, o, draw_g, w, h, this.lineatt, this.fillatt, true);

      if (graph._typename === 'TGraphQQ')
         this.appendQQ(funcs, graph);

      if (is_gme) {
         const extract_gme_errors = nblock => {
            this.#bins?.forEach(bin => {
               bin.eylow = graph.fEyL[nblock][bin.indx];
               bin.eyhigh = graph.fEyH[nblock][bin.indx];
            });
         };

         for (let k = 0; k < graph.fNYErrors; ++k) {
            const lineatt = !o.individual_styles ? this.lineatt : this.createAttLine({ attr: graph.fAttLine[k], std: false }),
                  fillatt = !o.individual_styles ? this.fillatt : this.createAttFill({ attr: graph.fAttFill[k], std: false }),
                  sub_g = g.append('svg:g'),
                  options = k < o.blocks.length ? o.blocks[k] : o;
            extract_gme_errors(k);
            this.drawBins(funcs, options, sub_g, w, h, lineatt, fillatt);
         }
         extract_gme_errors(0); // ensure that first block kept at the end
      }

      if (!this.isBatchMode()) {
         addMoveHandler(this, this.testEditable());
         assignContextMenu(this, kNoReorder);
      }
   }

   /** @summary Provide tooltip at specified point */
   extractTooltip(pnt) {
      if (!pnt)
         return null;

      if ((this.#draw_kind === 'lines') || (this.#draw_kind === 'path') || (this.#draw_kind === 'mark'))
         return this.extractTooltipForPath(pnt);

      if (this.#draw_kind !== 'nodes')
         return null;

      const fp = this.get_fp(),
            o = this.getOptions(),
            height = fp.getFrameHeight(),
            bw = this.#barwidth,
            boff = this.#baroffset,
            esz = this.error_size,
            isbar1 = (o.Bar === 1),
            funcs = isbar1 ? fp.getGrFuncs(o.second_x, o.second_y) : null,
            msize = this.#marker_size ? Math.round(this.#marker_size / 2 + 1.5) : 0;
      let findbin = null, best_dist2 = 1e10, best = null;

      this.getG().selectAll('.grpoint').each(function() {
         const d = select(this).datum();
         if (d === undefined)
            return;
         let dist2 = (pnt.x - d.grx1) ** 2;
         if (pnt.nproc === 1)
            dist2 += (pnt.y - d.gry1) ** 2;
         if (dist2 >= best_dist2)
            return;

         let rect;

         if (d.error || d.rect || d.marker) {
            rect = {
               x1: Math.min(-esz, d.grx0, -msize),
               x2: Math.max(esz, d.grx2, msize),
               y1: Math.min(-esz, d.gry2, -msize),
               y2: Math.max(esz, d.gry0, msize)
            };
         } else if (d.bar) {
            rect = {
               x1: boff - bw / 2,
               x2: boff + bw / 2,
               y1: 0,
               y2: height - d.gry1
            };

            if (isbar1) {
               const yy0 = funcs.gry(0);
               rect.y1 = (d.gry1 > yy0) ? yy0 - d.gry1 : 0;
               rect.y2 = (d.gry1 > yy0) ? 0 : yy0 - d.gry1;
            }
         } else
            rect = { x1: -5, x2: 5, y1: -5, y2: 5 };

         const matchx = (pnt.x >= d.grx1 + rect.x1) && (pnt.x <= d.grx1 + rect.x2),
               matchy = (pnt.y >= d.gry1 + rect.y1) && (pnt.y <= d.gry1 + rect.y2);

         if (matchx && (matchy || (pnt.nproc > 1))) {
            best_dist2 = dist2;
            findbin = this;
            best = rect;
            best.exact = /* matchx && */ matchy;
         }
      });

      if (findbin === null)
         return null;

      const d = select(findbin).datum(),
            gr = this.getGraph(),
            res = {
               name: gr.fName, title: gr.fTitle,
               x: d.grx1, y: d.gry1,
               color1: this.lineatt.color,
               lines: this.getTooltips(d),
               rect: best, d3bin: findbin
            };

      res.user_info = { obj: gr, name: gr.fName, bin: d.indx, cont: d.y, grx: d.grx1, gry: d.gry1 };

      if (this.fillatt?.used && !this.fillatt?.empty())
         res.color2 = this.fillatt.getFillColor();

      if (best.exact)
         res.exact = true;
      res.menu = res.exact; // activate menu only when exactly locate bin
      res.menu_dist = 3; // distance always fixed
      res.bin = d;
      res.binindx = d.indx;

      return res;
   }

   /** @summary Show tooltip */
   showTooltip(hint) {
      let ttrect = this.getG()?.selectChild('.tooltip_bin');

      if (!hint || !this.getG()) {
         ttrect?.remove();
         return;
      }

      if (hint.usepath)
         return this.showTooltipForPath(hint);

      const d = select(hint.d3bin).datum();

      if (ttrect.empty()) {
         ttrect = this.getG().append('svg:rect')
                             .attr('class', 'tooltip_bin')
                             .style('pointer-events', 'none')
                             .call(addHighlightStyle);
      }

      hint.changed = ttrect.property('current_bin') !== hint.d3bin;

      if (hint.changed) {
         ttrect.attr('x', d.grx1 + hint.rect.x1)
               .attr('width', hint.rect.x2 - hint.rect.x1)
               .attr('y', d.gry1 + hint.rect.y1)
               .attr('height', hint.rect.y2 - hint.rect.y1)
               .style('opacity', '0.3')
               .property('current_bin', hint.d3bin);
      }
   }

   /** @summary Process tooltip event */
   processTooltipEvent(pnt) {
      const hint = this.extractTooltip(pnt);
      if (!pnt || !pnt.disabled)
         this.showTooltip(hint);
      return hint;
   }

   /** @summary Find best bin index for specified point */
   findBestBin(pnt) {
      if (!this.#bins)
         return null;

      const islines = (this.#draw_kind === 'lines'),
            o = this.getOptions(),
            funcs = this.get_fp().getGrFuncs(o.second_x, o.second_y);
      let bestindx = -1,
          bestbin = null,
          bestdist = 1e10,
          dist, grx, gry, n, bin;

      for (n = 0; n < this.#bins.length; ++n) {
         bin = this.#bins[n];

         grx = funcs.grx(bin.x);
         gry = funcs.gry(bin.y);

         dist = (pnt.x - grx) ** 2 + (pnt.y - gry) ** 2;

         if (dist < bestdist) {
            bestdist = dist;
            bestbin = bin;
            bestindx = n;
         }
      }

      // check last point
      if ((bestdist > 100) && islines)
         bestbin = null;

      const radius = Math.max(this.lineatt.width + 3, 4, this.#marker_size);

      if (bestbin)
         bestdist = Math.sqrt((pnt.x - funcs.grx(bestbin.x)) ** 2 + (pnt.y - funcs.gry(bestbin.y)) ** 2);

      if (!islines && (bestdist > radius))
         bestbin = null;

      if (!bestbin)
         bestindx = -1;

      const res = { bin: bestbin, indx: bestindx, dist: bestdist, radius: Math.round(radius) };

      if (!bestbin && islines) {
         bestdist = 1e10;

         const is_inside = (x, x1, x2) => ((x1 >= x) && (x >= x2)) || ((x1 <= x) && (x <= x2));

         let bin0 = this.#bins[0], grx0 = funcs.grx(bin0.x), gry0, posy;
         for (n = 1; n < this.#bins.length; ++n) {
            bin = this.#bins[n];
            grx = funcs.grx(bin.x);

            if (is_inside(pnt.x, grx0, grx)) {
               // if inside interval, check Y distance
               gry0 = funcs.gry(bin0.y);
               gry = funcs.gry(bin.y);

               if (Math.abs(grx - grx0) < 1) {
                  // very close x - check only y
                  posy = pnt.y;
                  dist = is_inside(pnt.y, gry0, gry) ? 0 : Math.min(Math.abs(pnt.y - gry0), Math.abs(pnt.y - gry));
               } else {
                  posy = gry0 + (pnt.x - grx0) / (grx - grx0) * (gry - gry0);
                  dist = Math.abs(posy - pnt.y);
               }

               if (dist < bestdist) {
                  bestdist = dist;
                  res.linex = pnt.x;
                  res.liney = posy;
               }
            }

            bin0 = bin;
            grx0 = grx;
         }

         if (bestdist < radius * 0.5) {
            res.linedist = bestdist;
            res.closeline = true;
         }
      }

      return res;
   }

   /** @summary Check editable flag for TGraph
     * @desc if arg specified changes or toggles editable flag */
   testEditable(arg) {
      const obj = this.getGraph();
      if (!isFunc(obj?.TestBit))
         return false;
      if ((arg === 'toggle') || (arg !== undefined))
         obj.SetBit(kNotEditable, !arg);
      return !obj.TestBit(kNotEditable);
   }

   /** @summary Provide tooltip at specified point for path-based drawing */
   extractTooltipForPath(pnt) {
      if (!this.#bins)
         return null;

      const best = this.findBestBin(pnt);

      if (!best || (!best.bin && !best.closeline))
         return null;

      const islines = (this.#draw_kind === 'lines'),
            ismark = (this.#draw_kind === 'mark'),
            fp = this.get_fp(),
            o = this.getOptions(),
            funcs = fp.getGrFuncs(o.second_x, o.second_y),
            gr = this.getGraph(),
            res = {
               name: gr.fName, title: gr.fTitle,
               x: best.bin ? funcs.grx(best.bin.x) : best.linex,
               y: best.bin ? funcs.gry(best.bin.y) : best.liney,
               color1: this.lineatt.color,
               lines: this.getTooltips(best.bin),
               usepath: true, ismark, islines
            };

      res.user_info = { obj: gr, name: gr.fName, bin: 0, cont: 0, grx: res.x, gry: res.y };

      if (best.closeline) {
         res.menu = res.exact = true;
         res.menu_dist = best.linedist;
      } else if (best.bin) {
         if (o.EF && islines) {
            res.gry1 = funcs.gry(best.bin.y - best.bin.eylow);
            res.gry2 = funcs.gry(best.bin.y + best.bin.eyhigh);
         } else
            res.gry1 = res.gry2 = funcs.gry(best.bin.y);


         res.binindx = best.indx;
         res.bin = best.bin;
         res.radius = best.radius;
         res.user_info.bin = best.indx;
         res.user_info.cont = best.bin.y;

         res.exact = (Math.abs(pnt.x - res.x) <= best.radius) &&
            ((Math.abs(pnt.y - res.gry1) <= best.radius) || (Math.abs(pnt.y - res.gry2) <= best.radius));

         res.menu = res.exact;
         res.menu_dist = Math.sqrt((pnt.x - res.x) ** 2 + Math.min(Math.abs(pnt.y - res.gry1), Math.abs(pnt.y - res.gry2)) ** 2);
      }

      if (this.fillatt?.used && !this.fillatt?.empty())
         res.color2 = this.fillatt.getFillColor();

      if (!islines) {
         res.color1 = this.getColor(gr.fMarkerColor);
         if (!res.color2)
            res.color2 = res.color1;
      }

      return res;
   }

   /** @summary Show tooltip for path drawing */
   showTooltipForPath(hint) {
      let ttbin = this.getG()?.selectChild('.tooltip_bin');

      if (!hint?.bin || !this.getG()) {
         ttbin?.remove();
         return;
      }

      if (ttbin.empty())
         ttbin = this.getG().append('svg:g').attr('class', 'tooltip_bin');

      hint.changed = ttbin.property('current_bin') !== hint.bin;

      if (hint.changed) {
         ttbin.selectAll('*').remove(); // first delete all children
         ttbin.property('current_bin', hint.bin);

         if (hint.ismark) {
            ttbin.append('svg:rect')
                 .style('pointer-events', 'none')
                 .call(addHighlightStyle)
                 .style('opacity', '0.3')
                 .attr('x', Math.round(hint.x - hint.radius))
                 .attr('y', Math.round(hint.y - hint.radius))
                 .attr('width', 2 * hint.radius)
                 .attr('height', 2 * hint.radius);
         } else {
            ttbin.append('svg:circle').attr('cy', Math.round(hint.gry1));
            if (Math.abs(hint.gry1 - hint.gry2) > 1)
               ttbin.append('svg:circle').attr('cy', Math.round(hint.gry2));

            const o = this.getOptions(),
                  elem = ttbin.selectAll('circle')
                            .attr('r', hint.radius)
                            .attr('cx', Math.round(hint.x));

            if (!hint.islines)
               elem.style('stroke', hint.color1 === 'black' ? 'green' : 'black').style('fill', 'none');
            else {
               if (o.Line || o.Curve)
                  elem.call(this.lineatt.func);
               else
                  elem.style('stroke', 'black');
               if (o.Fill)
                  elem.call(this.fillatt.func);
               else
                  elem.style('fill', 'none');
            }
         }
      }
   }

   /** @summary Check if graph moving is enabled */
   moveEnabled() {
      return this.testEditable();
   }

   /** @summary Start moving of TGraph */
   moveStart(x, y, evnt) {
      const o = this.getOptions();
      this.#pos_dx = this.#pos_dy = 0;
      this.#move_funcs = this.get_fp().getGrFuncs(o.second_x, o.second_y);
      const hint = evnt?.shiftKey ? null : this.extractTooltip({ x, y });
      if (hint?.exact && (hint.binindx !== undefined)) {
         this.#move_binindx = hint.binindx;
         this.#move_bin = hint.bin;
         this.#move_x0 = this.#move_funcs.grx(this.#move_bin.x);
         this.#move_y0 = this.#move_funcs.gry(this.#move_bin.y);
      } else
         this.#move_binindx = undefined;
   }

   /** @summary Perform moving */
   moveDrag(dx, dy) {
      this.#pos_dx += dx;
      this.#pos_dy += dy;

      if (this.#move_binindx === undefined)
         makeTranslate(this.getG(), this.#pos_dx, this.#pos_dy);
      else if (this.#move_funcs && this.#move_bin) {
         this.#move_bin.x = this.#move_funcs.revertAxis('x', this.#move_x0 + this.#pos_dx);
         this.#move_bin.y = this.#move_funcs.revertAxis('y', this.#move_y0 + this.#pos_dy);
         this.drawGraph();
      }
   }

   /** @summary Complete moving */
   moveEnd(not_changed) {
      const graph = this.getGraph(), last = graph?.fNpoints - 1;
      let exec = '';

      const changeBin = bin => {
         exec += `SetPoint(${bin.indx},${bin.x},${bin.y});;`;
         graph.fX[bin.indx] = bin.x;
         graph.fY[bin.indx] = bin.y;
         if ((bin.indx === 0) && this.#cutg_lastsame) {
            exec += `SetPoint(${last},${bin.x},${bin.y});;`;
            graph.fX[last] = bin.x;
            graph.fY[last] = bin.y;
         }
      };

      if (this.#move_binindx === undefined) {
         this.getG().attr('transform', null);

         if (this.#move_funcs && this.#bins && !not_changed) {
            for (let k = 0; k < this.#bins.length; ++k) {
               const bin = this.#bins[k];
               bin.x = this.#move_funcs.revertAxis('x', this.#move_funcs.grx(bin.x) + this.#pos_dx);
               bin.y = this.#move_funcs.revertAxis('y', this.#move_funcs.gry(bin.y) + this.#pos_dy);
               changeBin(bin);
            }
            if (graph.$redraw_pad)
               this.redrawPad();
            else
               this.drawGraph();
         }
      } else {
         changeBin(this.#move_bin);
         this.#move_binindx = undefined;
         if (graph.$redraw_pad)
            this.redrawPad();
      }

      this.#move_funcs = undefined;

      if (exec && !not_changed)
         this.submitCanvExec(exec);
   }

   /** @summary Fill option object used in TWebCanvas */
   fillWebObjectOptions(res) {
      if (this.#auto_exec && res) {
         res.fcust = 'auto_exec:' + this.#auto_exec;
         this.#auto_exec = undefined;
      }
   }

   /** @summary Fill context menu */
   fillContextMenuItems(menu) {
      if (!this.hasSnapId()) {
         menu.addchk(this.testEditable(), 'Editable', () => {
            this.testEditable('toggle');
            this.drawGraph();
         });
         if (this.axes_draw) {
            menu.add('Title', () => menu.input('Enter graph title', this.getObject().fTitle).then(res => {
               this.getObject().fTitle = res;
               const hist_painter = this.getMainPainter();
               if (hist_painter?.isSecondary(this)) {
                  setHistogramTitle(hist_painter.getHisto(), res);
                  this.interactiveRedraw('pad');
               }
            }));
         }
         menu.addRedrawMenu(this.getPrimary());
      }
   }

   /** @summary Execute menu command
     * @private */
   executeMenuCommand(method, args) {
      if (super.executeMenuCommand(method, args))
         return true;

      const canp = this.getCanvPainter(), fp = this.get_fp();

      if ((method.fName === 'RemovePoint') || (method.fName === 'InsertPoint')) {
         if (!canp || canp.isReadonly())
            return true; // ignore function

         const pnt = isFunc(fp?.getLastEventPos) ? fp.getLastEventPos() : null,
               hint = this.extractTooltip(pnt);

         if (method.fName === 'InsertPoint') {
            if (pnt) {
               const o = this.getOptions(),
                     funcs = fp.getGrFuncs(o.second_x, o.second_y),
                     userx = funcs.revertAxis('x', pnt.x) ?? 0,
                     usery = funcs.revertAxis('y', pnt.y) ?? 0;
               this.submitCanvExec(`AddPoint(${userx.toFixed(3)}, ${usery.toFixed(3)})`, method.$execid);
            }
         } else if (method.$execid && (hint?.binindx !== undefined))
            this.submitCanvExec(`RemovePoint(${hint.binindx})`, method.$execid);

         return true; // call is processed
      }

      return false;
   }

   /** @summary Update TGraph object members
     * @private */
   _updateMembers(graph, obj) {
      graph.fBits = obj.fBits;
      graph.fTitle = obj.fTitle;
      graph.fX = obj.fX;
      graph.fY = obj.fY;
      ['fEX', 'fEY', 'fExL', 'fExH', 'fEXlow', 'fEXhigh', 'fEYlow', 'fEYhigh',
       'fEXlowd', 'fEXhighd', 'fEYlowd', 'fEYhighd'].forEach(member => {
         if (obj[member] !== undefined)
            graph[member] = obj[member];
      });
      graph.fNpoints = obj.fNpoints;
      graph.fMinimum = obj.fMinimum;
      graph.fMaximum = obj.fMaximum;

      const o = this.getOptions();

      if (this.hasSnapId())
         o._pfc = o._plc = o._pmc = 0; // auto colors should be processed in web canvas

      if (!o._pfc)
         graph.fFillColor = obj.fFillColor;
      graph.fFillStyle = obj.fFillStyle;
      if (!o._plc)
         graph.fLineColor = obj.fLineColor;
      graph.fLineStyle = obj.fLineStyle;
      graph.fLineWidth = obj.fLineWidth;
      if (!o._pmc)
         graph.fMarkerColor = obj.fMarkerColor;
      graph.fMarkerSize = obj.fMarkerSize;
      graph.fMarkerStyle = obj.fMarkerStyle;

      return obj.fFunctions;
   }

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

      const o = this.getOptions();

      if (opt && (opt !== o.original))
         this.decodeOptions(opt);

      const new_funcs = this._updateMembers(this.getObject(), obj);

      this.createBins();

      this.#redraw_hist = undefined;

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

      this.#funcs_handler = new FunctionsHandler(this, this.getPadPainter(), new_funcs);

      return true;
   }

   /** @summary Checks if it makes sense to zoom inside specified axis range
     * @desc allow to zoom TGraph only when at least one point in the range */
   canZoomInside(axis, min, max) {
      const gr = this.getGraph();
      if (!gr || ((axis !== 'x') && (axis !== 'y')))
         return false;

      let arr = gr.fX;
      if (this.isScatter())
         arr = (axis === 'x') ? gr.fX : gr.fY;
      else if (axis !== (this.getOptions().pos3d ? 'y' : 'x'))
         return false;

      for (let n = 0; n < gr.fNpoints; ++n) {
         if ((min < arr[n]) && (arr[n] < max))
            return true;
      }

      return false;
   }

   /** @summary Process click on graph-defined buttons */
   clickButton(funcname) {
      if (funcname !== 'ToggleZoom')
         return false;

      if ((this.xmin === this.xmax) && (this.ymin === this.ymax))
         return false;

      return this.getFramePainter()?.zoom(this.xmin, this.xmax, this.ymin, this.ymax);
   }

   /** @summary Find TF1/TF2 in TGraph list of functions */
   findFunc() {
      return this.getGraph()?.fFunctions?.arr?.find(func => (func._typename === clTF1) || (func._typename === clTF2));
   }

   /** @summary Find stat box in TGraph list of functions */
   findStat() {
      return this.getGraph()?.fFunctions?.arr?.find(func => (func._typename === clTPaveStats) && (func.fName === 'stats'));
   }

   /** @summary Create stat box */
   createStat() {
      const func = this.findFunc();
      if (!func)
         return null;

      let stats = this.findStat();
      if (stats)
         return stats;

      const st = gStyle;

      // do not create stats box when drawing canvas
      if (!st.fOptFit || this.getCanvPainter()?.getRootPad(true)?.fPrimitives?.arr.length)
         return null;

      this.create_stats = true;

      stats = create$1(clTPaveStats);
      Object.assign(stats, {
         fName: 'stats', fOptStat: 0, fOptFit: st.fOptFit, fBorderSize: 1,
         fX1NDC: st.fStatX - st.fStatW, fY1NDC: st.fStatY - st.fStatH, fX2NDC: st.fStatX, fY2NDC: st.fStatY,
         fFillColor: st.fStatColor, fFillStyle: st.fStatStyle
      });

      stats.fTextAngle = 0;
      stats.fTextSize = st.fStatFontSize; // 9 ??
      stats.fTextAlign = 12;
      stats.fTextColor = st.fStatTextColor;
      stats.fTextFont = st.fStatFont;

      stats.AddText(func.fName);

      // while TF1 was found, one can be sure that stats is existing
      this.getGraph().fFunctions.Add(stats);

      return stats;
   }

   /** @summary Fill statistic */
   fillStatistic(stat, _dostat, dofit) {
      const func = this.findFunc();
      if (!func || !dofit)
         return false;

      stat.clearPave();
      stat.fillFunctionStat(func, (dofit === 1) ? 111 : dofit, 1);
      return true;
   }

   /** @summary Draw axis histogram
     * @private */
   async drawAxisHisto() {
      const set_x = this.isDummyHistogram('x'),
            set_y = this.isDummyHistogram('y'),
            histo = this.createHistogram(set_x, set_y);
      return TH1Painter$2.draw(this.getDrawDom(), histo, this.getOptions().Axis);
   }

   /** @summary Draw TGraph
     * @private */
   static async _drawGraph(painter, opt) {
      painter.decodeOptions(opt, true);
      painter.createBins();
      painter.createStat();
      const graph = painter.getGraph();
      if (!settings.DragGraphs)
         graph?.SetBit(kNotEditable, true);

      let promise = Promise.resolve();

      if ((!painter.getMainPainter() || painter.options.second_x || painter.options.second_y) && painter.options.Axis) {
         promise = painter.drawAxisHisto().then(hist_painter => {
            hist_painter?.setSecondaryId(painter, 'hist');
            painter.axes_draw = Boolean(hist_painter);
         });
      }

      return promise.then(() => {
         painter.addToPadPrimitives();
         return painter.drawGraph();
      }).then(() => {
         const handler = new FunctionsHandler(painter, painter.getPadPainter(), graph.fFunctions, true);
         return handler.drawNext(0); // returns painter
      });
   }

   /** @summary Draw TGraph in 2D only */
   static async draw(dom, graph, opt) {
      return TGraphPainter._drawGraph(new TGraphPainter(dom, graph), opt);
   }

}; // class TGraphPainter

var TGraphPainter$2 = /*#__PURE__*/Object.freeze({
__proto__: null,
TGraphPainter: TGraphPainter$1,
clTGraphAsymmErrors: clTGraphAsymmErrors
});

class TGraphPainter extends TGraphPainter$1 {

   /** @summary Draw TGraph points in 3D
     * @private */
   drawBins3D(fp, graph) {
      if (!fp.mode3d || !fp.grx || !fp.gry || !fp.grz || !fp.toplevel)
         return console.log('Frame painter missing base 3d elements');

      const o = this.getOptions();
      if ((fp.zoom_xmin !== fp.zoom_xmax) && ((o.pos3d < fp.zoom_xmin) || (o.pos3d > fp.zoom_xmax)))
         return;

      this.createGraphDrawAttributes(true);

      const drawbins = this.optimizeBins(1000);
      let first = 0, last = drawbins.length - 1;

      if (fp.zoom_ymin !== fp.zoom_ymax) {
         while ((first < last) && (drawbins[first].x < fp.zoom_ymin))
            first++;
         while ((first < last) && (drawbins[last].x > fp.zoom_ymax))
            last--;
      }

      if (first === last)
         return;

      const pnts = [], grx = fp.grx(o.pos3d);
      let p0 = drawbins[first];

      for (let n = first + 1; n <= last; ++n) {
         const p1 = drawbins[n];
         pnts.push(grx, fp.gry(p0.x), fp.grz(p0.y),
                   grx, fp.gry(p1.x), fp.grz(p1.y));
         p0 = p1;
      }

      const lines = createLineSegments(pnts, create3DLineMaterial(this, graph));

      fp.add3DMesh(lines, this, true);

      fp.render3D(100);
   }

   /** @summary Draw axis histogram
     * @private */
   async drawAxisHisto() {
      return TH1Painter.draw(this.getDrawDom(), this.createHistogram(), this.getOptions().Axis);
   }

   /** @summary Draw TGraph */
   static async draw(dom, graph, opt) {
      return TGraphPainter._drawGraph(new TGraphPainter(dom, graph), opt);
   }

} // class TGraphPainter

class RTreeMapTooltip {

   static CONSTANTS = { DELAY: 0, OFFSET_X: 10, OFFSET_Y: -10, PADDING: 8, BORDER_RADIUS: 4 };

   constructor(painter)
   {
      this.painter = painter;
      this.tooltip = null;
      this.content = '';
      this.x = 0;
      this.y = 0;
   }

   cleanup() {
      if (this.tooltip !== null) {
         document.body.removeChild(this.tooltip);
         this.tooltip = null;
      }
   }

   createTooltip()
   {
      if (this.tooltip)
         return;

      this.tooltip = document.createElement('div');
      this.tooltip.style.cssText = `
            position: absolute;
            background: rgba(0, 0, 0, 0.9);
            color: white;
            padding: ${RTreeMapTooltip.CONSTANTS.PADDING}px;
            border-radius: ${RTreeMapTooltip.CONSTANTS.BORDER_RADIUS}px;
            font-size: 12px;
            pointer-events: none;
            z-index: 10000;
            opacity: 0;
            transition: opacity 0.2s;
            max-width: 200px;
            word-wrap: break-word;
        `;
      document.body.appendChild(this.tooltip);
   }

   showTooltip()
   {
      if (!this.tooltip)
         this.createTooltip();

      this.tooltip.innerHTML = this.content;
      this.tooltip.style.left = (this.x + RTreeMapTooltip.CONSTANTS.OFFSET_X) + 'px';
      this.tooltip.style.top = (this.y + RTreeMapTooltip.CONSTANTS.OFFSET_Y) + 'px';
      this.tooltip.style.opacity = '1';
   }

   hideTooltip()
   {
      if (this.tooltip)
         this.tooltip.style.opacity = '0';
   }

   generateTooltipContent(node)
   {
      const isLeaf = node.fNChildren === 0;
      let content = (node.fName.length > 0) ? `<strong>${node.fName}</strong><br>` : '';

      content += `<i>${(isLeaf ? 'Column' : 'Field')}</i><br>`;
      content += `Size: ${this.painter.getDataStr(node.fSize)}<br>`;

      if (isLeaf && node.fType !== undefined)
         content += `Type: ${node.fType}<br>`;

      if (!isLeaf)
         content += `Children: ${node.fNChildren}<br>`;

      const obj = this.painter.getObject();
      if (obj.fNodes && obj.fNodes.length > 0) {
         const totalSize = obj.fNodes[0].fSize,
               percentage = ((node.fSize / totalSize) * 100).toFixed(2);
         content += `Disk Usage: ${percentage}%`;
      }

      return content;
   }

}

function computeFnv(str) {
   const FNV_offset = 14695981039346656037n, FNV_prime = 1099511628211n;
   let h = FNV_offset;
   for (let i = 0; i < str.length; ++i) {
      const octet = BigInt(str.charCodeAt(i) & 0xFF);
      h ^= octet;
      h *= FNV_prime;
   }
   return h;
}

class RTreeMapPainter extends ObjectPainter {

   static CONSTANTS = {
      STROKE_WIDTH: 0.15,
      STROKE_COLOR: 'black',

      COLOR_HOVER_BOOST: 10,

      DATA_UNITS: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'],

      MAIN_TREEMAP: { LEFT: 0.025, BOTTOM: 0.05, RIGHT: 0.825, TOP: 0.9 },

      LEGEND: {
         START_Y: 0.835,
         ITEM_HEIGHT: 0.05,
         BOX_WIDTH: 0.05,
         TEXT_OFFSET_X: 0.01,
         TEXT_OFFSET_Y: 0.01,
         TEXT_LINE_SPACING: 0.015,
         MAX_ITEMS: 10
      },

      TEXT: { SIZE_VW: 0.6, MIN_RECT_WIDTH: 0.025, MIN_RECT_HEIGHT: 0.05, PADDING: 10, LEAF_OFFSET_Y: 0.015 },

      INDENT: 0.005,
      LEGEND_INDENT_MULTIPLIER: 4
   };

   constructor(dom, obj, opt) {
      super(dom, obj, opt);
      this.tooltip = new RTreeMapTooltip(this);
      this.rootIndex = 0;
      this.parentIndices = [];
   }

   cleanup() {
      if (this._frame_hidden) {
         delete this._frame_hidden;
         this.getPadPainter()?.getFrameSvg().style('display', null);
      }

      this.tooltip.cleanup();
      super.cleanup();
   }

   appendRect(begin, end, color, strokeColor = RTreeMapPainter.CONSTANTS.STROKE_COLOR, strokeWidth = RTreeMapPainter.CONSTANTS.STROKE_WIDTH, node = null) {
      const rect = this.getG()
                       .append('rect')
                       .attr('x', this.axisToSvg('x', begin.x, this.isndc))
                       .attr('y', this.axisToSvg('y', begin.y, this.isndc))
                       .attr('width', `${Math.abs(end.x - begin.x) * 100}%`)
                       .attr('height', `${Math.abs(end.y - begin.y) * 100}%`)
                       .attr('fill', color)
                       .attr('stroke', strokeColor)
                       .attr('stroke-width', strokeWidth)
                       .attr('pointer-events', 'fill');

      if (node) {
         rect.datum(node);
         this.attachPointerEventsTreeMap(rect, node);
      }
      return rect;
   }

   appendText(content, pos, size, color, anchor = 'start') {
      return this.getG()
         .append('text')
         .attr('x', this.axisToSvg('x', pos.x, this.isndc))
         .attr('y', this.axisToSvg('y', pos.y, this.isndc))
         .attr('font-size', `${size}vw`)
         .attr('fill', color)
         .attr('text-anchor', anchor)
         .attr('pointer-events', 'none')
         .text(content);
   }

   getRgbList(rgbStr) { return rgbStr.slice(4, -1).split(',').map((x) => parseInt(x)); }

   toRgbStr(rgbList) { return `rgb(${rgbList.join()})`; }

   attachPointerEventsTreeMap(element, node) {
      const original_color = element.attr('fill'), hovered_color = this.toRgbStr(this.getRgbList(original_color)
                          .map((color) => Math.min(color + RTreeMapPainter.CONSTANTS.COLOR_HOVER_BOOST, 255))),
            mouseenter = () => {
               element.attr('fill', hovered_color);
               this.tooltip.content = this.tooltip.generateTooltipContent(node);
               this.tooltip.x = 0;
               this.tooltip.y = 0;
            },
            mouseleave = () => {
               element.attr('fill', original_color);
               this.tooltip.hideTooltip();
            },
            mousemove = (event) => {
               this.tooltip.x = event.pageX;
               this.tooltip.y = event.pageY;
               this.tooltip.showTooltip();
            },
            click = () => {
               const obj = this.getObject(), nodeIndex = obj.fNodes.findIndex((elem) => elem === node);
               if (nodeIndex === this.rootIndex)
                  this.rootIndex = this.parentIndices[nodeIndex];
               else {
                  let parentIndex = nodeIndex;
                  while (this.parentIndices[parentIndex] !== this.rootIndex)
                     parentIndex = this.parentIndices[parentIndex];
                  this.rootIndex = parentIndex;
                  if (obj.fNodes[parentIndex].fNChildren === 0)
                     this.rootIndex = this.parentIndices[nodeIndex];
               }
               this.redraw();
            };
      this.attachPointerEvents(element, { mouseenter, mouseleave, mousemove, click });
   }

   attachPointerEventsLegend(element, type) {
      const rects = this.getG().selectAll('rect'),
            mouseenter = () => { rects.filter((node) => node !== undefined && node.fType !== type).attr('opacity', '0.5'); },
            mouseleave = () => { rects.attr('opacity', '1'); };
      this.attachPointerEvents(element, { mouseenter, mouseleave, mousemove: () => {}, click: () => {} });
   }

   attachPointerEvents(element, events) {
      for (const [key, value] of Object.entries(events))
         element.on(key, value);
   }

   computeColor(n) {
      const hash = Number(computeFnv(String(n)) & 0xFFFFFFFFn),
            r = (hash >> 16) & 0xFF,
            g = (hash >> 8) & 0xFF,
            b = hash & 0xFF;
      return this.toRgbStr([r, g, b]);
   }

   getDataStr(bytes) {
      const units = RTreeMapPainter.CONSTANTS.DATA_UNITS,
            order = Math.floor(Math.log10(bytes) / 3),
            finalSize = bytes / Math.pow(1000, order);
      return `${finalSize.toFixed(2)}${units[order]}`;
   }

   computeWorstRatio(row, width, height, totalSize, horizontalRows) {
      if (row.length === 0)
         return 0;

      const sumRow = row.reduce((sum, child) => sum + child.fSize, 0);
      if (sumRow === 0)
         return 0;

      let worstRatio = 0;
      for (const child of row) {
         const ratio = horizontalRows ? (child.fSize * width * totalSize) / (sumRow * sumRow * height)
                                      : (child.fSize * height * totalSize) / (sumRow * sumRow * width),
               aspectRatio = Math.max(ratio, 1 / ratio);
         if (aspectRatio > worstRatio)
            worstRatio = aspectRatio;
      }
      return worstRatio;
   }

   squarifyChildren(children, rect, horizontalRows, totalSize) {
      const width = rect.topRight.x - rect.bottomLeft.x,
            height = rect.topRight.y - rect.bottomLeft.y,
            remaining = [...children].sort((a, b) => b.fSize - a.fSize),
            result = [],
            remainingBegin = { ...rect.bottomLeft };

      while (remaining.length > 0) {
         const row = [];
         let currentWorstRatio = Infinity;
         const remainingWidth = rect.topRight.x - remainingBegin.x,
               remainingHeight = rect.topRight.y - remainingBegin.y;

         if (remainingWidth <= 0 || remainingHeight <= 0)
            break;

         while (remaining.length > 0) {
            row.push(remaining.shift());
            const newWorstRatio =
               this.computeWorstRatio(row, remainingWidth, remainingHeight, totalSize, horizontalRows);
            if (newWorstRatio > currentWorstRatio) {
               remaining.unshift(row.pop());
               break;
            }
            currentWorstRatio = newWorstRatio;
         }

         const sumRow = row.reduce((sum, child) => sum + child.fSize, 0);
         if (sumRow === 0)
            continue;

         const dimension = horizontalRows ? (sumRow / totalSize * height) : (sumRow / totalSize * width);
         let position = 0;

         for (const child of row) {
            const childDimension = child.fSize / sumRow * (horizontalRows ? width : height),
                  childBegin = horizontalRows ? { x: remainingBegin.x + position, y: remainingBegin.y }
                                              : { x: remainingBegin.x, y: remainingBegin.y + position },
                  childEnd = horizontalRows
                                ? { x: remainingBegin.x + position + childDimension, y: remainingBegin.y + dimension }
                                : { x: remainingBegin.x + dimension, y: remainingBegin.y + position + childDimension };

            result.push({ node: child, rect: { bottomLeft: childBegin, topRight: childEnd } });
            position += childDimension;
         }

         if (horizontalRows)
            remainingBegin.y += dimension;
         else
            remainingBegin.x += dimension;
      }
      return result;
   }

   drawLegend() {
      const obj = this.getObject(),
            diskMap = {};

      let stack = [this.rootIndex];
      while (stack.length > 0) {
         const node = obj.fNodes[stack.pop()];
         if (node.fNChildren === 0)
            diskMap[node.fType] = (diskMap[node.fType] || 0) + node.fSize;
         stack = stack.concat(Array.from({ length: node.fNChildren }, (_, a) => a + node.fChildrenIdx));
      }

      const diskEntries = Object.entries(diskMap)
                             .sort((a, b) => b[1] - a[1])
                             .slice(0, RTreeMapPainter.CONSTANTS.LEGEND.MAX_ITEMS)
                             .filter(([, size]) => size > 0),

            legend = RTreeMapPainter.CONSTANTS.LEGEND;

      diskEntries.forEach(([typeName, size], index) => {
         const posY = legend.START_Y - index * legend.ITEM_HEIGHT,
               posX = legend.START_Y + legend.ITEM_HEIGHT + legend.TEXT_OFFSET_X,
               textSize = RTreeMapPainter.CONSTANTS.TEXT.SIZE_VW,

               rect = this.appendRect({ x: legend.START_Y, y: posY },
                                      { x: legend.START_Y + legend.ITEM_HEIGHT, y: posY - legend.ITEM_HEIGHT },
                                      this.computeColor(typeName));
         this.attachPointerEventsLegend(rect, typeName);

         const diskOccupPercent = `${(size / obj.fNodes[this.rootIndex].fSize * 100).toFixed(2)}%`,
               diskOccup = `(${this.getDataStr(size)} / ${this.getDataStr(obj.fNodes[this.rootIndex].fSize)})`;

         [typeName, diskOccup, diskOccupPercent].forEach(
            (content, i) =>
               this.appendText(content, { x: posX, y: posY - legend.TEXT_OFFSET_Y - legend.TEXT_LINE_SPACING * (i) },
                               textSize, 'black'));
      });
   }

   trimText(textElement, rect) {
      const nodeElem = textElement.node();
      let textContent = nodeElem.textContent;
      const availablePx = Math.abs(this.axisToSvg('x', rect.topRight.x, this.isndc) -
                                   this.axisToSvg('x', rect.bottomLeft.x, this.isndc)) -
                          RTreeMapPainter.CONSTANTS.TEXT.PADDING;

      while (nodeElem.getComputedTextLength && nodeElem.getComputedTextLength() > availablePx && textContent.length > 0) {
         textContent = textContent.slice(0, -1);
         nodeElem.textContent = textContent + '…';
      }
      return textContent;
   }

   drawTreeMap(node, rect, depth = 0) {
      const isLeaf = node.fNChildren === 0,
            color = isLeaf ? this.computeColor(node.fType) : 'rgb(100,100,100)';
      this.appendRect({ x: rect.bottomLeft.x, y: rect.topRight.y }, { x: rect.topRight.x, y: rect.bottomLeft.y }, color,
                      RTreeMapPainter.CONSTANTS.STROKE_COLOR, RTreeMapPainter.CONSTANTS.STROKE_WIDTH, node);

      const rectWidth = rect.topRight.x - rect.bottomLeft.x,
            rectHeight = rect.topRight.y - rect.bottomLeft.y,
            labelBase = `${node.fName} (${this.getDataStr(node.fSize)})`,

            textConstants = RTreeMapPainter.CONSTANTS.TEXT,
            textSize = (rectWidth <= textConstants.MIN_RECT_WIDTH || rectHeight <= textConstants.MIN_RECT_HEIGHT)
                          ? 0
                          : textConstants.SIZE_VW;

      if (textSize > 0) {
         const textElement = this.appendText(labelBase, {
            x: rect.bottomLeft.x + (isLeaf ? rectWidth / 2 : RTreeMapPainter.CONSTANTS.INDENT),
            y: isLeaf ? (rect.bottomLeft.y + rect.topRight.y) / 2 : (rect.topRight.y - textConstants.LEAF_OFFSET_Y)
         },
                                             textSize, 'white', isLeaf ? 'middle' : 'start');
         textElement.textContent = this.trimText(textElement, rect);
      }

      if (!isLeaf && node.fNChildren > 0) {
         const obj = this.getObject(),
               children = obj.fNodes.slice(node.fChildrenIdx, node.fChildrenIdx + node.fNChildren),
               totalSize = children.reduce((sum, child) => sum + child.fSize, 0);

         if (totalSize > 0) {
            const indent = RTreeMapPainter.CONSTANTS.INDENT,
                  innerRect = {
                     bottomLeft: { x: rect.bottomLeft.x + indent, y: rect.bottomLeft.y + indent },
                     topRight: {
                        x: rect.topRight.x - indent,
                        y: rect.topRight.y - indent * RTreeMapPainter.CONSTANTS.LEGEND_INDENT_MULTIPLIER
                     }
                  },

                  width = innerRect.topRight.x - innerRect.bottomLeft.x,
                  height = innerRect.topRight.y - innerRect.bottomLeft.y,
                  horizontalRows = width > height,

                  rects = this.squarifyChildren(children, innerRect, horizontalRows, totalSize);
            rects.forEach(
               ({ node: childNode, rect: childRect }) => { this.drawTreeMap(childNode, childRect, depth + 1); });
         }
      }
   }

   createParentIndices() {
      const obj = this.getObject();
      this.parentIndices = new Array(obj.fNodes.length).fill(0);
      obj.fNodes.forEach((node, index) => {
         for (let i = node.fChildrenIdx; i < node.fChildrenIdx + node.fNChildren; i++)
            this.parentIndices[i] = index;
      });
   }

   getDirectory() {
      const obj = this.getObject();
      let result = '',
          currentIndex = this.rootIndex;
      while (currentIndex !== 0) {
         result = obj.fNodes[currentIndex].fName + '/' + result;
         currentIndex = this.parentIndices[currentIndex];
      }
      return result;
   }

   redraw() {
      const svg = this.getPadPainter().getFrameSvg();
      if (!svg.empty()) {
         svg.style('display', 'none');
         this._frame_hidden = true;
      }

      const obj = this.getObject();
      this.createG();
      this.isndc = true;

      if (obj.fNodes && obj.fNodes.length > 0) {
         this.createParentIndices();
         const mainArea = RTreeMapPainter.CONSTANTS.MAIN_TREEMAP;
         this.drawTreeMap(
            obj.fNodes[this.rootIndex],
            { bottomLeft: { x: mainArea.LEFT, y: mainArea.BOTTOM }, topRight: { x: mainArea.RIGHT, y: mainArea.TOP } });
         this.drawLegend();
         this.appendText(this.getDirectory(), { x: RTreeMapPainter.CONSTANTS.MAIN_TREEMAP.LEFT, y: RTreeMapPainter.CONSTANTS.MAIN_TREEMAP.TOP + 0.01 },
                         RTreeMapPainter.CONSTANTS.TEXT.SIZE_VW, 'black');
      }
      return this;
   }

   static async draw(dom, obj, opt) {
      const painter = new RTreeMapPainter(dom, obj, opt);
      return ensureTCanvas(painter, false).then(() => painter.redraw());
   }

} // class RTreeMapPainter

var RTreeMapPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
RTreeMapPainter: RTreeMapPainter
});

// CSG library for THREE.js


const EPSILON = 1e-5,
      COPLANAR = 0,
      FRONT = 1,
      BACK = 2,
      SPANNING = FRONT | BACK;

class Vertex {

   constructor(x, y, z, nx, ny, nz) {
      this.x = x;
      this.y = y;
      this.z = z;
      this.nx = nx;
      this.ny = ny;
      this.nz = nz;
   }

   setnormal(nx, ny, nz) {
      this.nx = nx;
      this.ny = ny;
      this.nz = nz;
   }

   clone() {
      return new Vertex(this.x, this.y, this.z, this.nx, this.ny, this.nz);
   }

   add(vertex) {
      this.x += vertex.x;
      this.y += vertex.y;
      this.z += vertex.z;
      return this;
   }

   subtract(vertex) {
      this.x -= vertex.x;
      this.y -= vertex.y;
      this.z -= vertex.z;
      return this;
   }

   // multiplyScalar( scalar ) {
   //   this.x *= scalar;
   //   this.y *= scalar;
   //   this.z *= scalar;
   //   return this;
   // }

   // cross( vertex ) {
   //    let x = this.x, y = this.y, z = this.z,
   //        vx = vertex.x, vy = vertex.y, vz = vertex.z;
   //
   //    this.x = y * vz - z * vy;
   //    this.y = z * vx - x * vz;
   //    this.z = x * vy - y * vx;
   //
   //    return this;
   // }

   cross3(vx, vy, vz) {
      const x = this.x, y = this.y, z = this.z;

      this.x = y * vz - z * vy;
      this.y = z * vx - x * vz;
      this.z = x * vy - y * vx;

      return this;
   }


   normalize() {
      const length = Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2);

      this.x /= length;
      this.y /= length;
      this.z /= length;

      return this;
   }

   dot(vertex) {
      return this.x * vertex.x + this.y * vertex.y + this.z * vertex.z;
   }

   diff(vertex) {
      const dx = (this.x - vertex.x),
            dy = (this.y - vertex.y),
            dz = (this.z - vertex.z),
            len2 = this.x ** 2 + this.y ** 2 + this.z ** 2;

      return (dx ** 2 + dy ** 2 + dz ** 2) / (len2 > 0 ? len2 : 1e-10);
   }

   /*
   lerp( a, t ) {
      this.add(
         a.clone().subtract( this ).multiplyScalar( t )
      );

      this.normal.add(
         a.normal.clone().sub( this.normal ).multiplyScalar( t )
      );

      //this.uv.add(
      //   a.uv.clone().sub( this.uv ).multiplyScalar( t )
      //);

      return this;
   };

   interpolate( other, t ) {
      return this.clone().lerp( other, t );
   };
*/

   interpolate(a, t) {
      const t1 = 1 - t;
      return new Vertex(this.x * t1 + a.x * t, this.y * t1 + a.y * t, this.z * t1 + a.z * t,
                        this.nx * t1 + a.nx * t, this.ny * t1 + a.ny * t, this.nz * t1 + a.nz * t);
   }

   applyMatrix4(m) {
      // input: Matrix4 affine matrix

      let x = this.x, y = this.y, z = this.z;
      const e = m.elements;

      this.x = e[0] * x + e[4] * y + e[8] * z + e[12];
      this.y = e[1] * x + e[5] * y + e[9] * z + e[13];
      this.z = e[2] * x + e[6] * y + e[10] * z + e[14];

      x = this.nx;
      y = this.ny;
      z = this.nz;

      this.nx = e[0] * x + e[4] * y + e[8] * z;
      this.ny = e[1] * x + e[5] * y + e[9] * z;
      this.nz = e[2] * x + e[6] * y + e[10] * z;

      return this;
   }

} // class Vertex


let Polygon$1 = class Polygon {

   constructor(vertices, parent, more) {
      this.vertices = vertices || [];
      this.nsign = 1;
      if (parent)
         this.copyProperties(parent, more);
      else if (this.vertices.length)
         this.calculateProperties();
   }

   copyProperties(parent, more) {
      this.normal = parent.normal; // .clone();
      this.w = parent.w;
      this.nsign = parent.nsign;
      if (more && (parent.id !== undefined)) {
         this.id = parent.id;
         this.parent = parent;
      }
      return this;
   }

   calculateProperties(force) {
      if (this.normal && !force)
         return;

      const a = this.vertices[0],
            b = this.vertices[1],
            c = this.vertices[2];

      this.nsign = 1;

      // this.normal = b.clone().subtract(a).cross(c.clone().subtract(a)).normalize();

      this.normal = new Vertex(b.x - a.x, b.y - a.y, b.z - a.z, 0, 0, 0).cross3(c.x - a.x, c.y - a.y, c.z - a.z).normalize();

      this.w = this.normal.dot(a);
      return this;
   }

   clone() {
      const vertice_count = this.vertices.length,
            vertices = [];

      for (let i = 0; i < vertice_count; ++i)
         vertices.push(this.vertices[i].clone());

      return new Polygon(vertices, this);
   }

   flip() {
      // normal is not changed, only sign variable
      // this.normal.multiplyScalar( -1 );
      // this.w *= -1;

      this.nsign *= -1;

      this.vertices.reverse();

      return this;
   }

   classifyVertex(vertex) {
      const side_value = this.nsign * (this.normal.dot(vertex) - this.w);
      if (side_value < -EPSILON)
         return BACK;
      if (side_value > EPSILON)
         return FRONT;
      return COPLANAR;
   }

   classifySide(polygon) {
      let num_positive = 0, num_negative = 0;
      const vertice_count = polygon.vertices.length;

      for (let i = 0; i < vertice_count; ++i) {
         const classification = this.classifyVertex(polygon.vertices[i]);
         if (classification === FRONT)
            ++num_positive;
         else if (classification === BACK)
            ++num_negative;
      }

      if (num_positive > 0 && num_negative === 0)
         return FRONT;
      if (num_positive === 0 && num_negative > 0)
         return BACK;
      if (num_positive === 0 && num_negative === 0)
         return COPLANAR;
      return SPANNING;
   }

   splitPolygon(polygon, coplanar_front, coplanar_back, front, back) {
      const classification = this.classifySide(polygon);

      if (classification === COPLANAR)

         ((this.nsign * polygon.nsign * this.normal.dot(polygon.normal) > 0) ? coplanar_front : coplanar_back).push(polygon);

      else if (classification === FRONT)

         front.push(polygon);

      else if (classification === BACK)

         back.push(polygon);

      else {
         const vertice_count = polygon.vertices.length,
               nnx = this.normal.x,
               nny = this.normal.y,
               nnz = this.normal.z,
               f = [], b = [];
         let i, j, ti, tj, vi, vj, t, v;

         for (i = 0; i < vertice_count; ++i) {
            j = (i + 1) % vertice_count;
            vi = polygon.vertices[i];
            vj = polygon.vertices[j];
            ti = this.classifyVertex(vi);
            tj = this.classifyVertex(vj);

            if (ti !== BACK)
               f.push(vi);
            if (ti !== FRONT)
               b.push(vi);
            if ((ti | tj) === SPANNING) {
               // t = (this.w - this.normal.dot(vi))/this.normal.dot(vj.clone().subtract(vi));
               // v = vi.clone().lerp( vj, t );

               t = (this.w - (nnx * vi.x + nny * vi.y + nnz * vi.z)) / (nnx * (vj.x - vi.x) + nny * (vj.y - vi.y) + nnz * (vj.z - vi.z));

               v = vi.interpolate(vj, t);
               f.push(v);
               b.push(v);
            }
         }

         // if ( f.length >= 3 )
         //    front.push(new Polygon(f).calculateProperties());
         // if ( b.length >= 3 )
         //    back.push(new Polygon(b).calculateProperties());
         if (f.length >= 3)
            front.push(new Polygon(f, polygon, true));
         if (b.length >= 3)
            back.push(new Polygon(b, polygon, true));
      }
   }

}; // class Polygon


class Node {

   constructor(polygons, nodeid) {
      this.polygons = [];
      this.front = this.back = undefined;

      if (!polygons)
         return;

      this.divider = polygons[0].clone();

      const polygon_count = polygons.length,
            front = [], back = [];

      for (let i = 0; i < polygon_count; ++i) {
         if (nodeid !== undefined) {
            polygons[i].id = nodeid++;
            delete polygons[i].parent;
         }

         // by definition polygon should be COPLANAR for itself
         if (i === 0)
            this.polygons.push(polygons[0]);
         else
            this.divider.splitPolygon(polygons[i], this.polygons, this.polygons, front, back);
      }

      if (nodeid !== undefined)
         this.maxnodeid = nodeid;

      if (front.length)
         this.front = new Node(front);

      if (back.length)
         this.back = new Node(back);
   }

   // isConvex(polygons) {
   //   let i, j, len = polygons.length;
   //   for ( i = 0; i < len; ++i )
   //      for ( j = 0; j < len; ++j )
   //         if ( i !== j && polygons[i].classifySide( polygons[j] ) !== BACK )
   //            return false;
   //   return true;
   // }

   build(polygons) {
      const polygon_count = polygons.length,
            front = [], back = [];
      let first = 0;

      if (!this.divider) {
         this.divider = polygons[0].clone();
         this.polygons.push(polygons[0]);
         first = 1;
      }

      for (let i = first; i < polygon_count; ++i)
         this.divider.splitPolygon(polygons[i], this.polygons, this.polygons, front, back);

      if (front.length) {
         if (!this.front)
            this.front = new Node();
         this.front.build(front);
      }

      if (back.length) {
         if (!this.back)
            this.back = new Node();
         this.back.build(back);
      }
   }

   collectPolygons(arr) {
      if (arr === undefined)
         arr = [];
      const len = this.polygons.length;
      for (let i = 0; i < len; ++i)
         arr.push(this.polygons[i]);
      this.front?.collectPolygons(arr);
      this.back?.collectPolygons(arr);
      return arr;
   }

   numPolygons() {
      return this.polygons.length + (this.front?.numPolygons() || 0) + (this.back?.numPolygons() || 0);
   }

   clone() {
      const node = new Node();

      node.divider = this.divider?.clone();
      node.polygons = this.polygons.map(polygon => polygon.clone());
      node.front = this.front?.clone();
      node.back = this.back?.clone();

      return node;
   }

   invert() {
      const polygon_count = this.polygons.length;

      for (let i = 0; i < polygon_count; ++i)
         this.polygons[i].flip();

      this.divider.flip();
      if (this.front)
         this.front.invert();
      if (this.back)
         this.back.invert();

      const temp = this.front;
      this.front = this.back;
      this.back = temp;

      return this;
   }

   clipPolygons(polygons) {
      if (!this.divider)
         return polygons.slice();

      const polygon_count = polygons.length;
      let front = [], back = [];

      for (let i = 0; i < polygon_count; ++i)
         this.divider.splitPolygon(polygons[i], front, back, front, back);

      if (this.front)
         front = this.front.clipPolygons(front);

      back = this.back?.clipPolygons(back) ?? [];

      return front.concat(back);
   }

   clipTo(node) {
      this.polygons = node.clipPolygons(this.polygons);
      this.front?.clipTo(node);
      this.back?.clipTo(node);
   }

} // class Node


function createBufferGeometry(polygons) {
   const polygon_count = polygons.length;
   let i, j, buf_size = 0;

   for (i = 0; i < polygon_count; ++i)
      buf_size += (polygons[i].vertices.length - 2) * 9;

   const positions_buf = new Float32Array(buf_size),
         normals_buf = new Float32Array(buf_size);
   let iii = 0, polygon;

   function CopyVertex(vertex) {
      positions_buf[iii] = vertex.x;
      positions_buf[iii + 1] = vertex.y;
      positions_buf[iii + 2] = vertex.z;

      normals_buf[iii] = polygon.nsign * vertex.nx;
      normals_buf[iii + 1] = polygon.nsign * vertex.ny;
      normals_buf[iii + 2] = polygon.nsign * vertex.nz;
      iii += 3;
   }

   for (i = 0; i < polygon_count; ++i) {
      polygon = polygons[i];
      for (j = 2; j < polygon.vertices.length; ++j) {
         CopyVertex(polygon.vertices[0]);
         CopyVertex(polygon.vertices[j - 1]);
         CopyVertex(polygon.vertices[j]);
      }
   }

   const geometry = new THREE.BufferGeometry();
   geometry.setAttribute('position', new THREE.BufferAttribute(positions_buf, 3));
   geometry.setAttribute('normal', new THREE.BufferAttribute(normals_buf, 3));

   // geometry.computeVertexNormals();
   return geometry;
}


class Geometry {

   constructor(geometry, transfer_matrix, nodeid, flippedMesh) {
      // Convert BufferGeometry to ThreeBSP

      if (geometry instanceof THREE.Mesh) {
         // #todo: add hierarchy support
         geometry.updateMatrix();
         transfer_matrix = this.matrix = geometry.matrix.clone();
         geometry = geometry.geometry;
      } else if (geometry instanceof Node) {
         this.tree = geometry;
         this.matrix = null; // new Matrix4;
         return;
      } else if (geometry instanceof THREE.BufferGeometry) {
         const pos_buf = geometry.getAttribute('position').array,
               norm_buf = geometry.getAttribute('normal').array,
               polygons = [];
         let polygon, vert1, vert2, vert3;

         for (let i = 0; i < pos_buf.length; i += 9) {
            polygon = new Polygon$1();

            vert1 = new Vertex(pos_buf[i], pos_buf[i + 1], pos_buf[i + 2], norm_buf[i], norm_buf[i + 1], norm_buf[i + 2]);
            if (transfer_matrix)
               vert1.applyMatrix4(transfer_matrix);

            vert2 = new Vertex(pos_buf[i + 3], pos_buf[i + 4], pos_buf[i + 5], norm_buf[i + 3], norm_buf[i + 4], norm_buf[i + 5]);
            if (transfer_matrix)
               vert2.applyMatrix4(transfer_matrix);

            vert3 = new Vertex(pos_buf[i + 6], pos_buf[i + 7], pos_buf[i + 8], norm_buf[i + 6], norm_buf[i + 7], norm_buf[i + 8]);
            if (transfer_matrix)
               vert3.applyMatrix4(transfer_matrix);

            if (flippedMesh)
               polygon.vertices.push(vert1, vert3, vert2);
            else
               polygon.vertices.push(vert1, vert2, vert3);

            polygon.calculateProperties(true);
            polygons.push(polygon);
         }

         this.tree = new Node(polygons, nodeid);
         if (nodeid !== undefined)
            this.maxid = this.tree.maxnodeid;
         return;
      } else if (geometry.polygons && (geometry.polygons[0] instanceof Polygon$1)) {
         const polygons = geometry.polygons;

         for (let i = 0; i < polygons.length; ++i) {
            const polygon = polygons[i];
            if (transfer_matrix) {
               const new_vertices = [];

               for (let n = 0; n < polygon.vertices.length; ++n)
                  new_vertices.push(polygon.vertices[n].clone().applyMatrix4(transfer_matrix));

               polygon.vertices = new_vertices;
            }

            polygon.calculateProperties(transfer_matrix);
         }

         this.tree = new Node(polygons, nodeid);
         if (nodeid !== undefined)
            this.maxid = this.tree.maxnodeid;
         return;
      } else
         throw Error('ThreeBSP: Given geometry is unsupported');

      const polygons = [], nfaces = geometry.faces.length;
      let face, polygon, vertex, normal, useVertexNormals;

      for (let i = 0; i < nfaces; ++i) {
         face = geometry.faces[i];
         normal = face.normal;
         // faceVertexUvs = geometry.faceVertexUvs[0][i];
         polygon = new Polygon$1();

         useVertexNormals = face.vertexNormals && (face.vertexNormals.length === 3);

         vertex = geometry.vertices[face.a];
         if (useVertexNormals)
            normal = face.vertexNormals[0];
         // uvs = faceVertexUvs ? new THREE.Vector2( faceVertexUvs[0].x, faceVertexUvs[0].y ) : null;
         vertex = new Vertex(vertex.x, vertex.y, vertex.z, normal.x, normal.y, normal.z /* face.normal, uvs */);
         if (transfer_matrix)
            vertex.applyMatrix4(transfer_matrix);
         polygon.vertices.push(vertex);

         vertex = geometry.vertices[face.b];
         if (useVertexNormals)
            normal = face.vertexNormals[1];
         // uvs = faceVertexUvs ? new THREE.Vector2( faceVertexUvs[1].x, faceVertexUvs[1].y ) : null;
         vertex = new Vertex(vertex.x, vertex.y, vertex.z, normal.x, normal.y, normal.z /* face.normal, uvs */);
         if (transfer_matrix)
            vertex.applyMatrix4(transfer_matrix);
         polygon.vertices.push(vertex);

         vertex = geometry.vertices[face.c];
         if (useVertexNormals)
            normal = face.vertexNormals[2];
         // uvs = faceVertexUvs ? new THREE.Vector2( faceVertexUvs[2].x, faceVertexUvs[2].y ) : null;
         vertex = new Vertex(vertex.x, vertex.y, vertex.z, normal.x, normal.y, normal.z /* face.normal, uvs */);
         if (transfer_matrix)
            vertex.applyMatrix4(transfer_matrix);
         polygon.vertices.push(vertex);

         polygon.calculateProperties(true);
         polygons.push(polygon);
      }

      this.tree = new Node(polygons, nodeid);
      if (nodeid !== undefined)
         this.maxid = this.tree.maxnodeid;
   }

   subtract(other_tree) {
      let a = this.tree.clone();
      const b = other_tree.tree.clone();

      a.invert();
      a.clipTo(b);
      b.clipTo(a);
      b.invert();
      b.clipTo(a);
      b.invert();
      a.build(b.collectPolygons());
      a.invert();
      a = new Geometry(a);
      a.matrix = this.matrix;
      return a;
   }

   union(other_tree) {
      let a = this.tree.clone();
      const b = other_tree.tree.clone();

      a.clipTo(b);
      b.clipTo(a);
      b.invert();
      b.clipTo(a);
      b.invert();
      a.build(b.collectPolygons());
      a = new Geometry(a);
      a.matrix = this.matrix;
      return a;
   }

   intersect(other_tree) {
      let a = this.tree.clone();
      const b = other_tree.tree.clone();

      a.invert();
      b.clipTo(a);
      b.invert();
      a.clipTo(b);
      b.clipTo(a);
      a.build(b.collectPolygons());
      a.invert();
      a = new Geometry(a);
      a.matrix = this.matrix;
      return a;
   }

   tryToCompress(polygons) {
      if (this.maxid === undefined)
         return;

      const arr = [];
      let parts, foundpair,
          nreduce = 0, n, len = polygons.length,
          p, p1, p2, i1, i2;

      // sort out polygons
      for (n = 0; n < len; ++n) {
         p = polygons[n];
         if (p.id === undefined)
            continue;
         if (arr[p.id] === undefined)
            arr[p.id] = [];

         arr[p.id].push(p);
      }

      for (n = 0; n < arr.length; ++n) {
         parts = arr[n];
         if (parts === undefined)
            continue;

         len = parts.length;

         foundpair = (len > 1);

         while (foundpair) {
            foundpair = false;

            for (i1 = 0; i1 < len - 1; ++i1) {
               p1 = parts[i1];
               if (!p1?.parent)
                  continue;
               for (i2 = i1 + 1; i2 < len; ++i2) {
                  p2 = parts[i2];
                  if (p2 && (p1.parent === p2.parent) && (p1.nsign === p2.nsign)) {
                     if (p1.nsign !== p1.parent.nsign)
                        p1.parent.flip();
                     nreduce++;
                     parts[i1] = p1.parent;
                     parts[i2] = null;
                     if (p1.parent.vertices.length < 3)
                        console.log('something wrong with parent');
                     foundpair = true;
                     break;
                  }
               }
            }
         }
      }

      if (nreduce > 0) {
         polygons.splice(0, polygons.length);

         for (n = 0; n < arr.length; ++n) {
            parts = arr[n];
            if (parts !== undefined) {
               for (i1 = 0, len = parts.length; i1 < len; ++i1) {
                  if (parts[i1])
                     polygons.push(parts[i1]);
               }
            }
         }
      }
   }

   direct_subtract(other_tree) {
      const a = this.tree,
            b = other_tree.tree;
      a.invert();
      a.clipTo(b);
      b.clipTo(a);
      b.invert();
      b.clipTo(a);
      b.invert();
      a.build(b.collectPolygons());
      a.invert();
      return this;
   }

   direct_union(other_tree) {
      const a = this.tree,
            b = other_tree.tree;

      a.clipTo(b);
      b.clipTo(a);
      b.invert();
      b.clipTo(a);
      b.invert();
      a.build(b.collectPolygons());
      return this;
   }

   direct_intersect(other_tree) {
      const a = this.tree,
            b = other_tree.tree;

      a.invert();
      b.clipTo(a);
      b.invert();
      a.clipTo(b);
      b.clipTo(a);
      a.build(b.collectPolygons());
      a.invert();
      return this;
   }

   cut_from_plane(other_tree) {
      // just cut peaces from second geometry, which just simple plane

      const a = this.tree,
            b = other_tree.tree;

      a.invert();
      b.clipTo(a);

      return this;
   }

   scale(x, y, z) {
      // try to scale as BufferGeometry
      const polygons = this.tree.collectPolygons();

      for (let i = 0; i < polygons.length; ++i) {
         const polygon = polygons[i];
         for (let k = 0; k < polygon.vertices.length; ++k) {
            const v = polygon.vertices[k];
            v.x *= x;
            v.y *= y;
            v.z *= z;
         }
         polygon.calculateProperties(true);
      }
   }

   toPolygons() {
      const polygons = this.tree.collectPolygons();

      this.tryToCompress(polygons);

      for (let i = 0; i < polygons.length; ++i) {
         delete polygons[i].id;
         delete polygons[i].parent;
      }

      return polygons;
   }

   toBufferGeometry() {
      return createBufferGeometry(this.toPolygons());
   }

   toMesh(material) {
      const geometry = this.toBufferGeometry(),
            mesh = new THREE.Mesh(geometry, material);

      if (this.matrix) {
         mesh.position.setFromMatrixPosition(this.matrix);
         mesh.rotation.setFromRotationMatrix(this.matrix);
      }

      return mesh;
   }

} // class Geometry

/** @summary create geometry to make cut on specified axis
  * @private */
function createNormal(axis_name, pos, size) {
   if (!size || (size < 10000))
      size = 10000;

   let vertices;

   switch (axis_name) {
      case 'x':
         vertices = [new Vertex(pos, -3 * size, size, 1, 0, 0),
                     new Vertex(pos, size, -3 * size, 1, 0, 0),
                     new Vertex(pos, size, size, 1, 0, 0)];
         break;
      case 'y':
         vertices = [new Vertex(-3 * size, pos, size, 0, 1, 0),
                     new Vertex(size, pos, size, 0, 1, 0),
                     new Vertex(size, pos, -3 * size, 0, 1, 0)];
         break;
      // case 'z':
      default:
         vertices = [new Vertex(-3 * size, size, pos, 0, 0, 1),
                     new Vertex(size, -3 * size, pos, 0, 0, 1),
                     new Vertex(size, size, pos, 0, 0, 1)];
   }

   const node = new Node([new Polygon$1(vertices)]);

   return new Geometry(node);
}

const _cfg = {
   GradPerSegm: 6,       // grad per segment in cylinder/spherical symmetry shapes
   CompressComp: true    // use faces compression in composite shapes
}, kShapeType = '$$Shape$$';

/** @summary Returns or set geometry config values
 * @desc Supported 'GradPerSegm' and 'CompressComp'
 * @private */

function geoCfg(name, value) {
   if (value === undefined)
      return _cfg[name];

   _cfg[name] = value;
}

const kindGeo = 0,    // TGeoNode / TGeoShape
      kindEve = 1,    // TEveShape / TEveGeoShapeExtract
      kindShape = 2,  // special kind for single shape handling

      kGetMesh = 'mesh',  // return mesh from createObject3D
      kDeleteMesh = 'delete_mesh', // delete mesh in createObject3D

      /** @summary TGeo-related bits
       * @private */
      geoBITS = {
         kVisNone: BIT(1),  // the volume/node is invisible, as well as daughters
         kVisThis: BIT(2),  // this volume/node is visible
         kVisDaughters: BIT(3),  // all leaves are visible
         kVisOnScreen: BIT(7)},

      clTGeoBBox = 'TGeoBBox',
      clTGeoArb8 = 'TGeoArb8',
      clTGeoCone = 'TGeoCone',
      clTGeoConeSeg = 'TGeoConeSeg',
      clTGeoTube = 'TGeoTube',
      clTGeoTubeSeg = 'TGeoTubeSeg',
      clTGeoCtub = 'TGeoCtub',
      clTGeoTrd1 = 'TGeoTrd1',
      clTGeoTrd2 = 'TGeoTrd2',
      clTGeoPara = 'TGeoPara',
      clTGeoParaboloid = 'TGeoParaboloid',
      clTGeoPcon = 'TGeoPcon',
      clTGeoPgon = 'TGeoPgon',
      clTGeoShapeAssembly = 'TGeoShapeAssembly',
      clTGeoSphere = 'TGeoSphere',
      clTGeoTorus = 'TGeoTorus',
      clTGeoXtru = 'TGeoXtru',
      clTGeoTrap = 'TGeoTrap',
      clTGeoGtra = 'TGeoGtra',
      clTGeoEltu = 'TGeoEltu',
      clTGeoHype = 'TGeoHype',
      clTGeoCompositeShape = 'TGeoCompositeShape',
      clTGeoHalfSpace = 'TGeoHalfSpace',
      clTGeoScaledShape = 'TGeoScaledShape';

/** @summary Test fGeoAtt bits
  * @private */
function testGeoBit(volume, f) {
   const att = volume.fGeoAtt;
   return att === undefined ? false : Boolean(att & f);
}

/** @summary Set fGeoAtt bit
  * @private */
function setGeoBit(volume, f, value) {
   if (volume.fGeoAtt !== undefined)
      volume.fGeoAtt = value ? (volume.fGeoAtt | f) : (volume.fGeoAtt & ~f);
}

/** @summary Toggle fGeoAttBit
  * @private */
function toggleGeoBit(volume, f) {
   if (volume.fGeoAtt !== undefined)
      volume.fGeoAtt ^= f & 0xffffff;
}

/** @summary Implementation of TGeoVolume::InvisibleAll
  * @private */
function setInvisibleAll(volume, flag = true) {
   setGeoBit(volume, geoBITS.kVisThis, !flag);
   // setGeoBit(this, geoBITS.kVisDaughters, !flag);

   if (volume.fNodes) {
      for (let n = 0; n < volume.fNodes.arr.length; ++n) {
         const sub = volume.fNodes.arr[n].fVolume;
         setGeoBit(sub, geoBITS.kVisThis, !flag);
         // setGeoBit(sub, geoBITS.kVisDaughters, !flag);
      }
   }
}

const _warn_msgs = {};

/** @summary method used to avoid duplication of warnings
 * @private */
function geoWarn(msg) {
   if (_warn_msgs[msg] === undefined) {
      _warn_msgs[msg] = true;
      console.warn(msg);
   }
}

/** @summary Analyze TGeo node kind
 *  @desc  0 - TGeoNode
 *         1 - TEveGeoNode
 *        -1 - unsupported
 * @return detected node kind
 * @private */
function getNodeKind(obj) {
   if (!isObject(obj))
      return -1;
   return ('fShape' in obj) && ('fTrans' in obj) ? kindEve : kindGeo;
}

/** @summary Returns number of shapes
  * @desc Used to count total shapes number in composites
  * @private */
function countNumShapes(shape) {
   if (!shape)
      return 0;
   if (shape._typename !== clTGeoCompositeShape)
      return 1;
   return countNumShapes(shape.fNode.fLeft) + countNumShapes(shape.fNode.fRight);
}


/** @summary Returns geo object name
  * @desc Can appends some special suffixes
  * @private */
function getObjectName(obj) {
   return obj?.fName ? (obj.fName + (obj.$geo_suffix || '')) : '';
}

/** @summary Check duplicates
  * @private */
function checkDuplicates(parent, chlds) {
   if (parent) {
      if (parent.$geo_checked)
         return;
      parent.$geo_checked = true;
   }

   const names = [], cnts = [];
   for (let k = 0; k < chlds.length; ++k) {
      const chld = chlds[k];
      if (!chld?.fName)
         continue;
      if (!chld.$geo_suffix) {
         const indx = names.indexOf(chld.fName);
         if (indx >= 0) {
            let cnt = cnts[indx] || 1;
            while (names.indexOf(chld.fName + '#' + cnt) >= 0) ++cnt;
            chld.$geo_suffix = '#' + cnt;
            cnts[indx] = cnt + 1;
         }
      }
      names.push(getObjectName(chld));
   }
}


/** @summary Create normal to plane, defined with three points
  * @private */
function produceNormal(x1, y1, z1, x2, y2, z2, x3, y3, z3) {
   const pA = new THREE.Vector3(x1, y1, z1),
         pB = new THREE.Vector3(x2, y2, z2),
         pC = new THREE.Vector3(x3, y3, z3),
         cb = new THREE.Vector3(),
         ab = new THREE.Vector3();

   cb.subVectors(pC, pB);
   ab.subVectors(pA, pB);
   cb.cross(ab);

   return cb;
}

// ==========================================================================

/**
  * @summary Helper class for geometry creation
  *
  * @private
  */

class GeometryCreator {

   /** @summary Constructor
     * @param numfaces - number of faces */
   constructor(numfaces) {
      this.nfaces = numfaces;
      this.indx = 0;
      this.pos = new Float32Array(numfaces * 9);
      this.norm = new Float32Array(numfaces * 9);
   }

   /** @summary Add face with 3 vertices */
   addFace3(x1, y1, z1, x2, y2, z2, x3, y3, z3) {
      const indx = this.indx, pos = this.pos;
      pos[indx] = x1;
      pos[indx + 1] = y1;
      pos[indx + 2] = z1;
      pos[indx + 3] = x2;
      pos[indx + 4] = y2;
      pos[indx + 5] = z2;
      pos[indx + 6] = x3;
      pos[indx + 7] = y3;
      pos[indx + 8] = z3;
      this.last4 = false;
      this.indx = indx + 9;
   }

   /** @summary Start polygon */
   startPolygon() {}

   /** @summary Stop polygon */
   stopPolygon() {}

   /** @summary Add face with 4 vertices
     * @desc From four vertices one normally creates two faces (1,2,3) and (1,3,4)
     * if (reduce === 1), first face is reduced
     * if (reduce === 2), second face is reduced */
   addFace4(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4, reduce) {
      let indx = this.indx;
      const pos = this.pos;

      if (reduce !== 1) {
         pos[indx] = x1;
         pos[indx + 1] = y1;
         pos[indx + 2] = z1;
         pos[indx + 3] = x2;
         pos[indx + 4] = y2;
         pos[indx + 5] = z2;
         pos[indx + 6] = x3;
         pos[indx + 7] = y3;
         pos[indx + 8] = z3;
         indx += 9;
      }

      if (reduce !== 2) {
         pos[indx] = x1;
         pos[indx + 1] = y1;
         pos[indx + 2] = z1;
         pos[indx + 3] = x3;
         pos[indx + 4] = y3;
         pos[indx + 5] = z3;
         pos[indx + 6] = x4;
         pos[indx + 7] = y4;
         pos[indx + 8] = z4;
         indx += 9;
      }

      this.last4 = (indx !== this.indx + 9);
      this.indx = indx;
   }

   /** @summary Specify normal for face with 4 vertices
     * @desc same as addFace4, assign normals for each individual vertex
     * reduce has same meaning and should be the same */
   setNormal4(nx1, ny1, nz1, nx2, ny2, nz2, nx3, ny3, nz3, nx4, ny4, nz4, reduce) {
      if (this.last4 && reduce)
         return console.error('missmatch between addFace4 and setNormal4 calls');

      let indx = this.indx - (this.last4 ? 18 : 9);
      const norm = this.norm;

      if (reduce !== 1) {
         norm[indx] = nx1;
         norm[indx + 1] = ny1;
         norm[indx + 2] = nz1;
         norm[indx + 3] = nx2;
         norm[indx + 4] = ny2;
         norm[indx + 5] = nz2;
         norm[indx + 6] = nx3;
         norm[indx + 7] = ny3;
         norm[indx + 8] = nz3;
         indx += 9;
      }

      if (reduce !== 2) {
         norm[indx] = nx1;
         norm[indx + 1] = ny1;
         norm[indx + 2] = nz1;
         norm[indx + 3] = nx3;
         norm[indx + 4] = ny3;
         norm[indx + 5] = nz3;
         norm[indx + 6] = nx4;
         norm[indx + 7] = ny4;
         norm[indx + 8] = nz4;
      }
   }

   /** @summary Recalculate Z with provided func */
   recalcZ(func) {
      const pos = this.pos,
            last = this.indx;
      let indx = last - (this.last4 ? 18 : 9);

      while (indx < last) {
         pos[indx + 2] = func(pos[indx], pos[indx + 1], pos[indx + 2]);
         indx += 3;
      }
   }

   /** @summary Calculate normal */
   calcNormal() {
      if (!this.cb) {
         this.pA = new THREE.Vector3();
         this.pB = new THREE.Vector3();
         this.pC = new THREE.Vector3();
         this.cb = new THREE.Vector3();
         this.ab = new THREE.Vector3();
      }

      this.pA.fromArray(this.pos, this.indx - 9);
      this.pB.fromArray(this.pos, this.indx - 6);
      this.pC.fromArray(this.pos, this.indx - 3);

      this.cb.subVectors(this.pC, this.pB);
      this.ab.subVectors(this.pA, this.pB);
      this.cb.cross(this.ab);

      this.setNormal(this.cb.x, this.cb.y, this.cb.z);
   }

   /** @summary Set normal */
   setNormal(nx, ny, nz) {
      let indx = this.indx - 9;
      const norm = this.norm;

      norm[indx] = norm[indx + 3] = norm[indx + 6] = nx;
      norm[indx + 1] = norm[indx + 4] = norm[indx + 7] = ny;
      norm[indx + 2] = norm[indx + 5] = norm[indx + 8] = nz;

      if (this.last4) {
         indx -= 9;
         norm[indx] = norm[indx + 3] = norm[indx + 6] = nx;
         norm[indx + 1] = norm[indx + 4] = norm[indx + 7] = ny;
         norm[indx + 2] = norm[indx + 5] = norm[indx + 8] = nz;
      }
   }

   /** @summary Set normal
     * @desc special shortcut, when same normals can be applied for 1-2 point and 3-4 point */
   setNormal_12_34(nx12, ny12, nz12, nx34, ny34, nz34, reduce = 0) {
      let indx = this.indx - ((reduce > 0) ? 9 : 18);
      const norm = this.norm;

      if (reduce !== 1) {
         norm[indx] = nx12;
         norm[indx + 1] = ny12;
         norm[indx + 2] = nz12;
         norm[indx + 3] = nx12;
         norm[indx + 4] = ny12;
         norm[indx + 5] = nz12;
         norm[indx + 6] = nx34;
         norm[indx + 7] = ny34;
         norm[indx + 8] = nz34;
         indx += 9;
      }

      if (reduce !== 2) {
         norm[indx] = nx12;
         norm[indx + 1] = ny12;
         norm[indx + 2] = nz12;
         norm[indx + 3] = nx34;
         norm[indx + 4] = ny34;
         norm[indx + 5] = nz34;
         norm[indx + 6] = nx34;
         norm[indx + 7] = ny34;
         norm[indx + 8] = nz34;
      }
   }

   /** @summary Create geometry */
   create() {
      if (this.nfaces !== this.indx / 9)
         console.error(`Mismatch with created ${this.nfaces} and filled ${this.indx / 9} number of faces`);

      const geometry = new THREE.BufferGeometry();
      geometry.setAttribute('position', new THREE.BufferAttribute(this.pos, 3));
      geometry.setAttribute('normal', new THREE.BufferAttribute(this.norm, 3));
      return geometry;
   }

}

// ================================================================================

/** @summary Helper class for CsgGeometry creation
  *
  * @private
  */

class PolygonsCreator {

   /** @summary constructor */
   constructor() {
      this.polygons = [];
   }

   /** @summary Start polygon */
   startPolygon(normal) {
      this.multi = 1;
      this.mnormal = normal;
   }

   /** @summary Stop polygon */
   stopPolygon() {
      if (!this.multi)
         return;
      this.multi = 0;
      console.error('Polygon should be already closed at this moment');
   }

   /** @summary Add face with 3 vertices */
   addFace3(x1, y1, z1, x2, y2, z2, x3, y3, z3) {
      this.addFace4(x1, y1, z1, x2, y2, z2, x3, y3, z3, x3, y3, z3, 2);
   }

   /** @summary Add face with 4 vertices
     * @desc From four vertices one normally creates two faces (1,2,3) and (1,3,4)
     * if (reduce === 1), first face is reduced
     * if (reduce === 2), second face is reduced */
   addFace4(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4, reduce = 0) {
      this.v1 = new Vertex(x1, y1, z1, 0, 0, 0);
      this.v2 = (reduce === 1) ? null : new Vertex(x2, y2, z2, 0, 0, 0);
      this.v3 = new Vertex(x3, y3, z3, 0, 0, 0);
      this.v4 = (reduce === 2) ? null : new Vertex(x4, y4, z4, 0, 0, 0);
      this.reduce = reduce;

      if (this.multi) {
         if (reduce !== 2)
            console.error('polygon not supported for not-reduced faces');

         let polygon;

         if (this.multi++ === 1) {
            polygon = new Polygon$1();

            polygon.vertices.push(this.mnormal ? this.v2 : this.v3);
            this.polygons.push(polygon);
         } else {
            polygon = this.polygons.at(-1);
            // check that last vertex equals to v2
            const last = this.mnormal ? polygon.vertices.at(-1) : polygon.vertices.at(0),
                  comp = this.mnormal ? this.v2 : this.v3;

            if (comp.diff(last) > 1e-12)
               console.error('vertex missmatch when building polygon');
         }

         const first = this.mnormal ? polygon.vertices[0] : polygon.vertices.at(-1),
               next = this.mnormal ? this.v3 : this.v2;

         if (next.diff(first) < 1e-12)
            this.multi = 0;
         else if (this.mnormal)
            polygon.vertices.push(this.v3);
         else
            polygon.vertices.unshift(this.v2);
      } else {
         const polygon = new Polygon$1();
         switch (reduce) {
            case 0: polygon.vertices.push(this.v1, this.v2, this.v3, this.v4); break;
            case 1: polygon.vertices.push(this.v1, this.v3, this.v4); break;
            case 2: polygon.vertices.push(this.v1, this.v2, this.v3); break;
         }
         this.polygons.push(polygon);
      }
   }

   /** @summary Specify normal for face with 4 vertices
     * @desc same as addFace4, assign normals for each individual vertex
     * reduce has same meaning and should be the same */
   setNormal4(nx1, ny1, nz1, nx2, ny2, nz2, nx3, ny3, nz3, nx4, ny4, nz4) {
      this.v1.setnormal(nx1, ny1, nz1);
      this.v2?.setnormal(nx2, ny2, nz2);
      this.v3.setnormal(nx3, ny3, nz3);
      this.v4?.setnormal(nx4, ny4, nz4);
   }

   /** @summary Set normal
     * @desc special shortcut, when same normals can be applied for 1-2 point and 3-4 point */
   setNormal_12_34(nx12, ny12, nz12, nx34, ny34, nz34) {
      this.v1.setnormal(nx12, ny12, nz12);
      this.v2?.setnormal(nx12, ny12, nz12);
      this.v3.setnormal(nx34, ny34, nz34);
      this.v4?.setnormal(nx34, ny34, nz34);
   }

   /** @summary Calculate normal */
   calcNormal() {
      if (!this.cb) {
         this.pA = new THREE.Vector3();
         this.pB = new THREE.Vector3();
         this.pC = new THREE.Vector3();
         this.cb = new THREE.Vector3();
         this.ab = new THREE.Vector3();
      }

      this.pA.set(this.v1.x, this.v1.y, this.v1.z);

      if (this.reduce !== 1) {
         this.pB.set(this.v2.x, this.v2.y, this.v2.z);
         this.pC.set(this.v3.x, this.v3.y, this.v3.z);
      } else {
         this.pB.set(this.v3.x, this.v3.y, this.v3.z);
         this.pC.set(this.v4.x, this.v4.y, this.v4.z);
      }

      this.cb.subVectors(this.pC, this.pB);
      this.ab.subVectors(this.pA, this.pB);
      this.cb.cross(this.ab);

      this.setNormal(this.cb.x, this.cb.y, this.cb.z);
   }

   /** @summary Set normal */
   setNormal(nx, ny, nz) {
      this.v1.setnormal(nx, ny, nz);
      this.v2?.setnormal(nx, ny, nz);
      this.v3.setnormal(nx, ny, nz);
      this.v4?.setnormal(nx, ny, nz);
   }

   /** @summary Recalculate Z with provided func */
   recalcZ(func) {
      this.v1.z = func(this.v1.x, this.v1.y, this.v1.z);
      if (this.v2)
         this.v2.z = func(this.v2.x, this.v2.y, this.v2.z);
      this.v3.z = func(this.v3.x, this.v3.y, this.v3.z);
      if (this.v4)
         this.v4.z = func(this.v4.x, this.v4.y, this.v4.z);
   }

   /** @summary Create geometry
     * @private */
   create() {
      return { polygons: this.polygons };
   }

}

// ================= all functions to create geometry ===================================

/** @summary Creates cube geometry
  * @private */
function createCubeBuffer(shape, faces_limit) {
   if (faces_limit < 0)
      return 12;

   const dx = shape.fDX, dy = shape.fDY, dz = shape.fDZ,
         creator = faces_limit ? new PolygonsCreator() : new GeometryCreator(12);

   creator.addFace4(dx, dy, dz, dx, -dy, dz, dx, -dy, -dz, dx, dy, -dz);
   creator.setNormal(1, 0, 0);

   creator.addFace4(-dx, dy, -dz, -dx, -dy, -dz, -dx, -dy, dz, -dx, dy, dz);
   creator.setNormal(-1, 0, 0);

   creator.addFace4(-dx, dy, -dz, -dx, dy, dz, dx, dy, dz, dx, dy, -dz);
   creator.setNormal(0, 1, 0);

   creator.addFace4(-dx, -dy, dz, -dx, -dy, -dz, dx, -dy, -dz, dx, -dy, dz);
   creator.setNormal(0, -1, 0);

   creator.addFace4(-dx, dy, dz, -dx, -dy, dz, dx, -dy, dz, dx, dy, dz);
   creator.setNormal(0, 0, 1);

   creator.addFace4(dx, dy, -dz, dx, -dy, -dz, -dx, -dy, -dz, -dx, dy, -dz);
   creator.setNormal(0, 0, -1);

   return creator.create();
}

/** @summary Creates 8 edges geometry
  * @private */
function create8edgesBuffer(v, faces_limit) {
   const indicies = [4, 7, 6, 5, 0, 3, 7, 4, 4, 5, 1, 0, 6, 2, 1, 5, 7, 3, 2, 6, 1, 2, 3, 0],
         creator = (faces_limit > 0) ? new PolygonsCreator() : new GeometryCreator(12);

   for (let n = 0; n < indicies.length; n += 4) {
      const i1 = indicies[n] * 3,
            i2 = indicies[n + 1] * 3,
            i3 = indicies[n + 2] * 3,
            i4 = indicies[n + 3] * 3;
      creator.addFace4(v[i1], v[i1 + 1], v[i1 + 2], v[i2], v[i2 + 1], v[i2 + 2],
                       v[i3], v[i3 + 1], v[i3 + 2], v[i4], v[i4 + 1], v[i4 + 2]);
      if (n === 0)
         creator.setNormal(0, 0, 1);
      else if (n === 20)
         creator.setNormal(0, 0, -1);
      else
         creator.calcNormal();
   }

   return creator.create();
}

/** @summary Creates PARA geometry
  * @private */
function createParaBuffer(shape, faces_limit) {
   if (faces_limit < 0)
      return 12;

   const txy = shape.fTxy, txz = shape.fTxz, tyz = shape.fTyz, v = [
      -shape.fZ * txz - txy * shape.fY - shape.fX, -shape.fY - shape.fZ * tyz, -shape.fZ,
      -shape.fZ * txz + txy * shape.fY - shape.fX, shape.fY - shape.fZ * tyz, -shape.fZ,
      -shape.fZ * txz + txy * shape.fY + shape.fX, shape.fY - shape.fZ * tyz, -shape.fZ,
      -shape.fZ * txz - txy * shape.fY + shape.fX, -shape.fY - shape.fZ * tyz, -shape.fZ,
      shape.fZ * txz - txy * shape.fY - shape.fX, -shape.fY + shape.fZ * tyz, shape.fZ,
      shape.fZ * txz + txy * shape.fY - shape.fX, shape.fY + shape.fZ * tyz, shape.fZ,
      shape.fZ * txz + txy * shape.fY + shape.fX, shape.fY + shape.fZ * tyz, shape.fZ,
      shape.fZ * txz - txy * shape.fY + shape.fX, -shape.fY + shape.fZ * tyz, shape.fZ];

   return create8edgesBuffer(v, faces_limit);
}

/** @summary Creates trapezoid geometry
  * @private */
function createTrapezoidBuffer(shape, faces_limit) {
   if (faces_limit < 0)
      return 12;

   let y1, y2;
   if (shape._typename === clTGeoTrd1)
      y1 = y2 = shape.fDY;
   else {
      y1 = shape.fDy1;
      y2 = shape.fDy2;
   }

   const v = [
      -shape.fDx1, y1, -shape.fDZ,
      shape.fDx1, y1, -shape.fDZ,
      shape.fDx1, -y1, -shape.fDZ,
      -shape.fDx1, -y1, -shape.fDZ,
      -shape.fDx2, y2, shape.fDZ,
      shape.fDx2, y2, shape.fDZ,
      shape.fDx2, -y2, shape.fDZ,
      -shape.fDx2, -y2, shape.fDZ
   ];

   return create8edgesBuffer(v, faces_limit);
}


/** @summary Creates arb8 geometry
  * @private */
function createArb8Buffer(shape, faces_limit) {
   if (faces_limit < 0)
      return 12;

   const vertices = [
      shape.fXY[0][0], shape.fXY[0][1], -shape.fDZ,
      shape.fXY[1][0], shape.fXY[1][1], -shape.fDZ,
      shape.fXY[2][0], shape.fXY[2][1], -shape.fDZ,
      shape.fXY[3][0], shape.fXY[3][1], -shape.fDZ,
      shape.fXY[4][0], shape.fXY[4][1], shape.fDZ,
      shape.fXY[5][0], shape.fXY[5][1], shape.fDZ,
      shape.fXY[6][0], shape.fXY[6][1], shape.fDZ,
      shape.fXY[7][0], shape.fXY[7][1], shape.fDZ
   ],
         indicies = [
            4, 7, 6, 6, 5, 4, 3, 7, 4, 4, 0, 3,
            5, 1, 0, 0, 4, 5, 6, 2, 1, 1, 5, 6,
            7, 3, 2, 2, 6, 7, 1, 2, 3, 3, 0, 1];

   // detect same vertices on both Z-layers
   for (let side = 0; side < vertices.length; side += vertices.length / 2) {
      for (let n1 = side; n1 < side + vertices.length / 2 - 3; n1 += 3) {
         for (let n2 = n1 + 3; n2 < side + vertices.length / 2; n2 += 3) {
            if ((vertices[n1] === vertices[n2]) &&
                (vertices[n1 + 1] === vertices[n2 + 1]) &&
                (vertices[n1 + 2] === vertices[n2 + 2])) {
               for (let k = 0; k < indicies.length; ++k) {
                  if (indicies[k] === n2 / 3)
                     indicies[k] = n1 / 3;
               }
            }
         }
      }
   }

   const map = []; // list of existing faces (with all rotations)
   let numfaces = 0;

   for (let k = 0; k < indicies.length; k += 3) {
      const id1 = indicies[k] * 100 + indicies[k + 1] * 10 + indicies[k + 2],
            id2 = indicies[k + 1] * 100 + indicies[k + 2] * 10 + indicies[k],
            id3 = indicies[k + 2] * 100 + indicies[k] * 10 + indicies[k + 1];

      if ((indicies[k] === indicies[k + 1]) || (indicies[k] === indicies[k + 2]) || (indicies[k + 1] === indicies[k + 2]) ||
          (map.indexOf(id1) >= 0) || (map.indexOf(id2) >= 0) || (map.indexOf(id3) >= 0))
         indicies[k] = indicies[k + 1] = indicies[k + 2] = -1;
      else {
         map.push(id1, id2, id3);
         numfaces++;
      }
   }

   const creator = faces_limit ? new PolygonsCreator() : new GeometryCreator(numfaces);

   for (let n = 0; n < indicies.length; n += 6) {
      const i1 = indicies[n] * 3,
            i2 = indicies[n + 1] * 3,
            i3 = indicies[n + 2] * 3,
            i4 = indicies[n + 3] * 3,
            i5 = indicies[n + 4] * 3,
            i6 = indicies[n + 5] * 3;
      let norm = null;

      if ((i1 >= 0) && (i4 >= 0) && faces_limit) {
         // try to identify two faces with same normal - very useful if one can create face4
         if (n === 0)
            norm = new THREE.Vector3(0, 0, 1);
         else if (n === 30)
            norm = new THREE.Vector3(0, 0, -1);
         else {
            const norm1 = produceNormal(vertices[i1], vertices[i1 + 1], vertices[i1 + 2],
                                        vertices[i2], vertices[i2 + 1], vertices[i2 + 2],
                                        vertices[i3], vertices[i3 + 1], vertices[i3 + 2]);

            norm1.normalize();

            const norm2 = produceNormal(vertices[i4], vertices[i4 + 1], vertices[i4 + 2],
                                        vertices[i5], vertices[i5 + 1], vertices[i5 + 2],
                                        vertices[i6], vertices[i6 + 1], vertices[i6 + 2]);

            norm2.normalize();

            if (norm1.distanceToSquared(norm2) < 1e-12)
               norm = norm1;
         }
      }

      if (norm !== null) {
         creator.addFace4(vertices[i1], vertices[i1 + 1], vertices[i1 + 2],
                          vertices[i2], vertices[i2 + 1], vertices[i2 + 2],
                          vertices[i3], vertices[i3 + 1], vertices[i3 + 2],
                          vertices[i5], vertices[i5 + 1], vertices[i5 + 2]);
         creator.setNormal(norm.x, norm.y, norm.z);
      } else {
         if (i1 >= 0) {
            creator.addFace3(vertices[i1], vertices[i1 + 1], vertices[i1 + 2],
                             vertices[i2], vertices[i2 + 1], vertices[i2 + 2],
                             vertices[i3], vertices[i3 + 1], vertices[i3 + 2]);
            creator.calcNormal();
         }
         if (i4 >= 0) {
            creator.addFace3(vertices[i4], vertices[i4 + 1], vertices[i4 + 2],
                             vertices[i5], vertices[i5 + 1], vertices[i5 + 2],
                             vertices[i6], vertices[i6 + 1], vertices[i6 + 2]);
            creator.calcNormal();
         }
      }
   }

   return creator.create();
}

/** @summary Creates sphere geometry
  * @private */
function createSphereBuffer(shape, faces_limit) {
   const radius = [shape.fRmax, shape.fRmin],
         phiStart = shape.fPhi1,
         phiLength = shape.fPhi2 - shape.fPhi1,
         thetaStart = shape.fTheta1,
         thetaLength = shape.fTheta2 - shape.fTheta1,
         noInside = (radius[1] <= 0);
   let widthSegments = shape.fNseg,
       heightSegments = shape.fNz;

   if (faces_limit > 0) {
      const fact = (noInside ? 2 : 4) * widthSegments * heightSegments / faces_limit;

      if (fact > 1.0) {
         widthSegments = Math.max(4, Math.floor(widthSegments / Math.sqrt(fact)));
         heightSegments = Math.max(4, Math.floor(heightSegments / Math.sqrt(fact)));
      }
   }

   let numoutside = widthSegments * heightSegments * 2,
       numtop = widthSegments * (noInside ? 1 : 2),
       numbottom = widthSegments * (noInside ? 1 : 2);
   const numcut = (phiLength === 360) ? 0 : heightSegments * (noInside ? 2 : 4),
         epsilon = 1e-10;

   if (faces_limit < 0)
      return numoutside * (noInside ? 1 : 2) + numtop + numbottom + numcut;

   const _sinp = new Float32Array(widthSegments + 1),
         _cosp = new Float32Array(widthSegments + 1),
         _sint = new Float32Array(heightSegments + 1),
         _cost = new Float32Array(heightSegments + 1);

   for (let n = 0; n <= heightSegments; ++n) {
      const theta = (thetaStart + thetaLength / heightSegments * n) * Math.PI / 180;
      _sint[n] = Math.sin(theta);
      _cost[n] = Math.cos(theta);
   }

   for (let n = 0; n <= widthSegments; ++n) {
      const phi = (phiStart + phiLength / widthSegments * n) * Math.PI / 180;
      _sinp[n] = Math.sin(phi);
      _cosp[n] = Math.cos(phi);
   }

   if (Math.abs(_sint[0]) <= epsilon) {
      numoutside -= widthSegments;
      numtop = 0;
   }
   if (Math.abs(_sint[heightSegments]) <= epsilon) {
      numoutside -= widthSegments;
      numbottom = 0;
   }

   const numfaces = numoutside * (noInside ? 1 : 2) + numtop + numbottom + numcut,
         creator = faces_limit ? new PolygonsCreator() : new GeometryCreator(numfaces);

   for (let side = 0; side < 2; ++side) {
      if ((side === 1) && noInside)
         break;

      const r = radius[side],
            s = (side === 0) ? 1 : -1,
            d1 = 1 - side, d2 = 1 - d1;

      // use direct algorithm for the sphere - here normals and position can be calculated directly
      for (let k = 0; k < heightSegments; ++k) {
         const k1 = k + d1, k2 = k + d2;
         let skip = 0;
         if (Math.abs(_sint[k1]) <= epsilon)
            skip = 1;
         else if (Math.abs(_sint[k2]) <= epsilon)
            skip = 2;

         for (let n = 0; n < widthSegments; ++n) {
            creator.addFace4(
               r * _sint[k1] * _cosp[n], r * _sint[k1] * _sinp[n], r * _cost[k1],
               r * _sint[k1] * _cosp[n + 1], r * _sint[k1] * _sinp[n + 1], r * _cost[k1],
               r * _sint[k2] * _cosp[n + 1], r * _sint[k2] * _sinp[n + 1], r * _cost[k2],
               r * _sint[k2] * _cosp[n], r * _sint[k2] * _sinp[n], r * _cost[k2],
               skip);
            creator.setNormal4(
               s * _sint[k1] * _cosp[n], s * _sint[k1] * _sinp[n], s * _cost[k1],
               s * _sint[k1] * _cosp[n + 1], s * _sint[k1] * _sinp[n + 1], s * _cost[k1],
               s * _sint[k2] * _cosp[n + 1], s * _sint[k2] * _sinp[n + 1], s * _cost[k2],
               s * _sint[k2] * _cosp[n], s * _sint[k2] * _sinp[n], s * _cost[k2],
               skip);
         }
      }
   }

   // top/bottom
   for (let side = 0; side <= heightSegments; side += heightSegments) {
      if (Math.abs(_sint[side]) >= epsilon) {
         const ss = _sint[side], cc = _cost[side],
               d1 = (side === 0) ? 0 : 1, d2 = 1 - d1;
         for (let n = 0; n < widthSegments; ++n) {
            creator.addFace4(
               radius[1] * ss * _cosp[n + d1], radius[1] * ss * _sinp[n + d1], radius[1] * cc,
               radius[0] * ss * _cosp[n + d1], radius[0] * ss * _sinp[n + d1], radius[0] * cc,
               radius[0] * ss * _cosp[n + d2], radius[0] * ss * _sinp[n + d2], radius[0] * cc,
               radius[1] * ss * _cosp[n + d2], radius[1] * ss * _sinp[n + d2], radius[1] * cc,
                  noInside ? 2 : 0);
            creator.calcNormal();
         }
      }
   }

   // cut left/right sides
   if (phiLength < 360) {
      for (let side = 0; side <= widthSegments; side += widthSegments) {
         const ss = _sinp[side], cc = _cosp[side],
               d1 = (side === 0) ? 1 : 0, d2 = 1 - d1;

         for (let k = 0; k < heightSegments; ++k) {
            creator.addFace4(
               radius[1] * _sint[k + d1] * cc, radius[1] * _sint[k + d1] * ss, radius[1] * _cost[k + d1],
               radius[0] * _sint[k + d1] * cc, radius[0] * _sint[k + d1] * ss, radius[0] * _cost[k + d1],
               radius[0] * _sint[k + d2] * cc, radius[0] * _sint[k + d2] * ss, radius[0] * _cost[k + d2],
               radius[1] * _sint[k + d2] * cc, radius[1] * _sint[k + d2] * ss, radius[1] * _cost[k + d2],
                  noInside ? 2 : 0);
            creator.calcNormal();
         }
      }
   }

   return creator.create();
}

/** @summary Creates tube geometry
  * @private */
function createTubeBuffer(shape, faces_limit) {
   let outerR, innerR; // inner/outer tube radius
   if ((shape._typename === clTGeoCone) || (shape._typename === clTGeoConeSeg)) {
      outerR = [shape.fRmax2, shape.fRmax1];
      innerR = [shape.fRmin2, shape.fRmin1];
   } else {
      outerR = [shape.fRmax, shape.fRmax];
      innerR = [shape.fRmin, shape.fRmin];
   }

   const hasrmin = (innerR[0] > 0) || (innerR[1] > 0);
   let thetaStart = 0, thetaLength = 360;

   if ((shape._typename === clTGeoConeSeg) || (shape._typename === clTGeoTubeSeg) || (shape._typename === clTGeoCtub)) {
      thetaStart = shape.fPhi1;
      thetaLength = shape.fPhi2 - shape.fPhi1;
   }

   const radiusSegments = Math.max(4, Math.round(thetaLength / _cfg.GradPerSegm));

   // external surface
   let numfaces = radiusSegments * (((outerR[0] <= 0) || (outerR[1] <= 0)) ? 1 : 2);

   // internal surface
   if (hasrmin)
      numfaces += radiusSegments * (((innerR[0] <= 0) || (innerR[1] <= 0)) ? 1 : 2);

   // upper cap
   if (outerR[0] > 0)
      numfaces += radiusSegments * ((innerR[0] > 0) ? 2 : 1);
   // bottom cup
   if (outerR[1] > 0)
      numfaces += radiusSegments * ((innerR[1] > 0) ? 2 : 1);

   if (thetaLength < 360)
      numfaces += ((outerR[0] > innerR[0]) ? 2 : 0) + ((outerR[1] > innerR[1]) ? 2 : 0);

   if (faces_limit < 0)
      return numfaces;

   const phi0 = thetaStart * Math.PI / 180,
         dphi = thetaLength / radiusSegments * Math.PI / 180,
         _sin = new Float32Array(radiusSegments + 1),
         _cos = new Float32Array(radiusSegments + 1);

   for (let seg = 0; seg <= radiusSegments; ++seg) {
      _cos[seg] = Math.cos(phi0 + seg * dphi);
      _sin[seg] = Math.sin(phi0 + seg * dphi);
   }

   const creator = faces_limit ? new PolygonsCreator() : new GeometryCreator(numfaces),
         calcZ = (shape._typename !== clTGeoCtub) ? null : (x, y, z) => {
            const arr = (z < 0) ? shape.fNlow : shape.fNhigh;
            return ((z < 0) ? -shape.fDz : shape.fDz) - (x * arr[0] + y * arr[1]) / arr[2];
         };

   // create outer/inner tube
   for (let side = 0; side < 2; ++side) {
      if ((side === 1) && !hasrmin)
         break;

      const R = (side === 0) ? outerR : innerR, d1 = side, d2 = 1 - side;
      let nxy = 1, nz = 0;

      if (R[0] !== R[1]) {
         const angle = Math.atan2((R[1] - R[0]), 2 * shape.fDZ);
         nxy = Math.cos(angle);
         nz = Math.sin(angle);
      }

      if (side === 1) {
         nxy *= -1;
         nz *= -1;
      }

      const reduce = (R[0] <= 0) ? 2 : ((R[1] <= 0) ? 1 : 0);

      for (let seg = 0; seg < radiusSegments; ++seg) {
         creator.addFace4(
            R[0] * _cos[seg + d1], R[0] * _sin[seg + d1], shape.fDZ,
            R[1] * _cos[seg + d1], R[1] * _sin[seg + d1], -shape.fDZ,
            R[1] * _cos[seg + d2], R[1] * _sin[seg + d2], -shape.fDZ,
            R[0] * _cos[seg + d2], R[0] * _sin[seg + d2], shape.fDZ,
            reduce);

         if (calcZ)
            creator.recalcZ(calcZ);

         creator.setNormal_12_34(nxy * _cos[seg + d1], nxy * _sin[seg + d1], nz,
                                 nxy * _cos[seg + d2], nxy * _sin[seg + d2], nz,
                                 reduce);
      }
   }

   // create upper/bottom part
   for (let side = 0; side < 2; ++side) {
      if (outerR[side] <= 0)
         continue;

      const d1 = side, d2 = 1 - side,
            sign = (side === 0) ? 1 : -1,
            reduce = (innerR[side] <= 0) ? 2 : 0;
      if ((reduce === 2) && (thetaLength === 360) && !calcZ)
         creator.startPolygon(side === 0);
      for (let seg = 0; seg < radiusSegments; ++seg) {
         creator.addFace4(
            innerR[side] * _cos[seg + d1], innerR[side] * _sin[seg + d1], sign * shape.fDZ,
            outerR[side] * _cos[seg + d1], outerR[side] * _sin[seg + d1], sign * shape.fDZ,
            outerR[side] * _cos[seg + d2], outerR[side] * _sin[seg + d2], sign * shape.fDZ,
            innerR[side] * _cos[seg + d2], innerR[side] * _sin[seg + d2], sign * shape.fDZ,
            reduce);
         if (calcZ) {
            creator.recalcZ(calcZ);
            creator.calcNormal();
         } else
            creator.setNormal(0, 0, sign);
      }

      creator.stopPolygon();
   }

   // create cut surfaces
   if (thetaLength < 360) {
      creator.addFace4(innerR[1] * _cos[0], innerR[1] * _sin[0], -shape.fDZ,
                       outerR[1] * _cos[0], outerR[1] * _sin[0], -shape.fDZ,
                       outerR[0] * _cos[0], outerR[0] * _sin[0], shape.fDZ,
                       innerR[0] * _cos[0], innerR[0] * _sin[0], shape.fDZ,
                       (outerR[0] === innerR[0]) ? 2 : ((innerR[1] === outerR[1]) ? 1 : 0));
      if (calcZ)
         creator.recalcZ(calcZ);
      creator.calcNormal();

      creator.addFace4(innerR[0] * _cos[radiusSegments], innerR[0] * _sin[radiusSegments], shape.fDZ,
                       outerR[0] * _cos[radiusSegments], outerR[0] * _sin[radiusSegments], shape.fDZ,
                       outerR[1] * _cos[radiusSegments], outerR[1] * _sin[radiusSegments], -shape.fDZ,
                       innerR[1] * _cos[radiusSegments], innerR[1] * _sin[radiusSegments], -shape.fDZ,
                       (outerR[0] === innerR[0]) ? 1 : ((innerR[1] === outerR[1]) ? 2 : 0));

      if (calcZ)
         creator.recalcZ(calcZ);
      creator.calcNormal();
   }

   return creator.create();
}

/** @summary Creates eltu geometry
  * @private */
function createEltuBuffer(shape, faces_limit) {
   const radiusSegments = Math.max(4, Math.round(360 / _cfg.GradPerSegm));

   if (faces_limit < 0)
      return radiusSegments * 4;

   // calculate all sin/cos tables in advance
   const x = new Float32Array(radiusSegments + 1),
         y = new Float32Array(radiusSegments + 1);
   for (let seg = 0; seg <= radiusSegments; ++seg) {
      const phi = seg / radiusSegments * 2 * Math.PI;
      x[seg] = shape.fRmin * Math.cos(phi);
      y[seg] = shape.fRmax * Math.sin(phi);
   }

   const creator = faces_limit ? new PolygonsCreator() : new GeometryCreator(radiusSegments * 4);
   let nx1, ny1, nx2 = 1, ny2 = 0;

   // create tube faces
   for (let seg = 0; seg < radiusSegments; ++seg) {
      creator.addFace4(x[seg], y[seg], shape.fDZ,
                       x[seg], y[seg], -shape.fDZ,
                       x[seg + 1], y[seg + 1], -shape.fDZ,
                       x[seg + 1], y[seg + 1], shape.fDZ);

      // calculate normals ourself
      nx1 = nx2;
      ny1 = ny2;
      nx2 = x[seg + 1] * shape.fRmax / shape.fRmin;
      ny2 = y[seg + 1] * shape.fRmin / shape.fRmax;
      const dist = Math.sqrt(nx2 ** 2 + ny2 ** 2);
      nx2 /= dist;
      ny2 /= dist;

      creator.setNormal_12_34(nx1, ny1, 0, nx2, ny2, 0);
   }

   // create top/bottom sides
   for (let side = 0; side < 2; ++side) {
      const sign = (side === 0) ? 1 : -1, d1 = side, d2 = 1 - side;
      for (let seg = 0; seg < radiusSegments; ++seg) {
         creator.addFace3(0, 0, sign * shape.fDZ,
                          x[seg + d1], y[seg + d1], sign * shape.fDZ,
                          x[seg + d2], y[seg + d2], sign * shape.fDZ);
         creator.setNormal(0, 0, sign);
      }
   }

   return creator.create();
}

/** @summary Creates torus geometry
  * @private */
function createTorusBuffer(shape, faces_limit) {
   const radius = shape.fR;
   let radialSegments = Math.max(6, Math.round(360 / _cfg.GradPerSegm)),
       tubularSegments = Math.max(8, Math.round(shape.fDphi / _cfg.GradPerSegm)),
       numfaces = (shape.fRmin > 0 ? 4 : 2) * radialSegments * (tubularSegments + (shape.fDphi !== 360 ? 1 : 0));

   if (faces_limit < 0)
      return numfaces;

   if ((faces_limit > 0) && (numfaces > faces_limit)) {
      radialSegments = Math.floor(radialSegments / Math.sqrt(numfaces / faces_limit));
      tubularSegments = Math.floor(tubularSegments / Math.sqrt(numfaces / faces_limit));
      numfaces = (shape.fRmin > 0 ? 4 : 2) * radialSegments * (tubularSegments + (shape.fDphi !== 360 ? 1 : 0));
   }

   const _sinr = new Float32Array(radialSegments + 1),
         _cosr = new Float32Array(radialSegments + 1),
         _sint = new Float32Array(tubularSegments + 1),
         _cost = new Float32Array(tubularSegments + 1);

   for (let n = 0; n <= radialSegments; ++n) {
      _sinr[n] = Math.sin(n / radialSegments * 2 * Math.PI);
      _cosr[n] = Math.cos(n / radialSegments * 2 * Math.PI);
   }

   for (let t = 0; t <= tubularSegments; ++t) {
      const angle = (shape.fPhi1 + shape.fDphi * t / tubularSegments) / 180 * Math.PI;
      _sint[t] = Math.sin(angle);
      _cost[t] = Math.cos(angle);
   }

   const creator = faces_limit ? new PolygonsCreator() : new GeometryCreator(numfaces),
         // use vectors for normals calculation
         p1 = new THREE.Vector3(), p2 = new THREE.Vector3(), p3 = new THREE.Vector3(), p4 = new THREE.Vector3(),
         n1 = new THREE.Vector3(), n2 = new THREE.Vector3(), n3 = new THREE.Vector3(), n4 = new THREE.Vector3(),
         center1 = new THREE.Vector3(), center2 = new THREE.Vector3();

   for (let side = 0; side < 2; ++side) {
      if ((side > 0) && (shape.fRmin <= 0))
         break;
      const tube = (side > 0) ? shape.fRmin : shape.fRmax,
            d1 = 1 - side, d2 = 1 - d1, ns = side > 0 ? -1 : 1;

      for (let t = 0; t < tubularSegments; ++t) {
         const t1 = t + d1, t2 = t + d2;
         center1.x = radius * _cost[t1];
         center1.y = radius * _sint[t1];
         center2.x = radius * _cost[t2];
         center2.y = radius * _sint[t2];

         for (let n = 0; n < radialSegments; ++n) {
            p1.x = (radius + tube * _cosr[n]) * _cost[t1];
            p1.y = (radius + tube * _cosr[n]) * _sint[t1];
            p1.z = tube * _sinr[n];
            p2.x = (radius + tube * _cosr[n + 1]) * _cost[t1];
            p2.y = (radius + tube * _cosr[n + 1]) * _sint[t1];
            p2.z = tube * _sinr[n + 1];
            p3.x = (radius + tube * _cosr[n + 1]) * _cost[t2];
            p3.y = (radius + tube * _cosr[n + 1]) * _sint[t2];
            p3.z = tube * _sinr[n + 1];
            p4.x = (radius + tube * _cosr[n]) * _cost[t2];
            p4.y = (radius + tube * _cosr[n]) * _sint[t2];
            p4.z = tube * _sinr[n];

            creator.addFace4(p1.x, p1.y, p1.z,
                             p2.x, p2.y, p2.z,
                             p3.x, p3.y, p3.z,
                             p4.x, p4.y, p4.z);

            n1.subVectors(p1, center1).normalize();
            n2.subVectors(p2, center1).normalize();
            n3.subVectors(p3, center2).normalize();
            n4.subVectors(p4, center2).normalize();

            creator.setNormal4(ns * n1.x, ns * n1.y, ns * n1.z,
                               ns * n2.x, ns * n2.y, ns * n2.z,
                               ns * n3.x, ns * n3.y, ns * n3.z,
                               ns * n4.x, ns * n4.y, ns * n4.z);
         }
      }
   }

   if (shape.fDphi !== 360) {
      for (let t = 0; t <= tubularSegments; t += tubularSegments) {
         const tube1 = shape.fRmax, tube2 = shape.fRmin,
               d1 = t > 0 ? 0 : 1, d2 = 1 - d1,
               skip = shape.fRmin > 0 ? 0 : 1,
               nsign = t > 0 ? 1 : -1;
         for (let n = 0; n < radialSegments; ++n) {
            creator.addFace4((radius + tube1 * _cosr[n + d1]) * _cost[t], (radius + tube1 * _cosr[n + d1]) * _sint[t], tube1 * _sinr[n + d1],
                             (radius + tube2 * _cosr[n + d1]) * _cost[t], (radius + tube2 * _cosr[n + d1]) * _sint[t], tube2 * _sinr[n + d1],
                             (radius + tube2 * _cosr[n + d2]) * _cost[t], (radius + tube2 * _cosr[n + d2]) * _sint[t], tube2 * _sinr[n + d2],
                             (radius + tube1 * _cosr[n + d2]) * _cost[t], (radius + tube1 * _cosr[n + d2]) * _sint[t], tube1 * _sinr[n + d2], skip);
            creator.setNormal(-nsign * _sint[t], nsign * _cost[t], 0);
         }
      }
   }

   return creator.create();
}


/** @summary Creates polygon geometry
  * @private */
function createPolygonBuffer(shape, faces_limit) {
   const thetaStart = shape.fPhi1,
         thetaLength = shape.fDphi;
   let radiusSegments, factor;

   if (shape._typename === clTGeoPgon) {
      radiusSegments = shape.fNedges;
      factor = 1.0 / Math.cos(Math.PI / 180 * thetaLength / radiusSegments / 2);
   } else {
      radiusSegments = Math.max(5, Math.round(thetaLength / _cfg.GradPerSegm));
      factor = 1;
   }

   const usage = new Int16Array(2 * shape.fNz);
   let numusedlayers = 0, hasrmin = false;

   for (let layer = 0; layer < shape.fNz; ++layer)
      hasrmin = hasrmin || (shape.fRmin[layer] > 0);

   // return very rough estimation, number of faces may be much less
   if (faces_limit < 0)
      return (hasrmin ? 4 : 2) * radiusSegments * (shape.fNz - 1);

   // coordinate of point on cut edge (x,z)
   const pnts = (thetaLength === 360) ? null : [];

   // first analyze levels - if we need to create all of them
   for (let side = 0; side < 2; ++side) {
      const rside = (side === 0) ? 'fRmax' : 'fRmin';

      for (let layer = 0; layer < shape.fNz; ++layer) {
         // first create points for the layer
         const layerz = shape.fZ[layer], rad = shape[rside][layer];

         usage[layer * 2 + side] = 0;

         if ((layer > 0) && (layer < shape.fNz - 1)) {
            if (((shape.fZ[layer - 1] === layerz) && (shape[rside][layer - 1] === rad)) ||
                ((shape[rside][layer + 1] === rad) && (shape[rside][layer - 1] === rad))) {
               // same Z and R as before - ignore
               // or same R before and after
               continue;
            }
         }

         if ((layer > 0) && ((side === 0) || hasrmin)) {
            usage[layer * 2 + side] = 1;
            numusedlayers++;
         }

         if (pnts !== null) {
            if (side === 0)
               pnts.push(new THREE.Vector2(factor * rad, layerz));
            else if (rad < shape.fRmax[layer])
               pnts.unshift(new THREE.Vector2(factor * rad, layerz));
         }
      }
   }

   let numfaces = numusedlayers * radiusSegments * 2;
   if (shape.fRmin[0] !== shape.fRmax[0])
      numfaces += radiusSegments * (hasrmin ? 2 : 1);
   if (shape.fRmin[shape.fNz - 1] !== shape.fRmax[shape.fNz - 1])
      numfaces += radiusSegments * (hasrmin ? 2 : 1);

   let cut_faces = null;

   if (pnts !== null) {
      if (pnts.length === shape.fNz * 2) {
         // special case - all layers are there, create faces ourself
         cut_faces = [];
         for (let layer = shape.fNz - 1; layer > 0; --layer) {
            if (shape.fZ[layer] === shape.fZ[layer - 1])
               continue;
            const right = 2 * shape.fNz - 1 - layer;
            cut_faces.push([right, layer - 1, layer]);
            cut_faces.push([right, right + 1, layer - 1]);
         }
      } else {
         // let three.js calculate our faces
         cut_faces = THREE.ShapeUtils.triangulateShape(pnts, []);
      }
      numfaces += cut_faces.length * 2;
   }

   const phi0 = thetaStart * Math.PI / 180,
         dphi = thetaLength / radiusSegments * Math.PI / 180,
         // calculate all sin/cos tables in advance
         _sin = new Float32Array(radiusSegments + 1),
         _cos = new Float32Array(radiusSegments + 1);
   for (let seg = 0; seg <= radiusSegments; ++seg) {
      _cos[seg] = Math.cos(phi0 + seg * dphi);
      _sin[seg] = Math.sin(phi0 + seg * dphi);
   }

   const creator = faces_limit ? new PolygonsCreator() : new GeometryCreator(numfaces);

   // add sides
   for (let side = 0; side < 2; ++side) {
      const rside = (side === 0) ? 'fRmax' : 'fRmin',
            d1 = 1 - side, d2 = side;
      let z1 = shape.fZ[0], r1 = factor * shape[rside][0];

      for (let layer = 0; layer < shape.fNz; ++layer) {
         if (usage[layer * 2 + side] === 0)
            continue;

         const z2 = shape.fZ[layer], r2 = factor * shape[rside][layer];
         let nxy = 1, nz = 0;

         if ((r2 !== r1)) {
            const angle = Math.atan2((r2 - r1), (z2 - z1));
            nxy = Math.cos(angle);
            nz = Math.sin(angle);
         }

         if (side > 0) {
            nxy *= -1;
            nz *= -1;
         }

         for (let seg = 0; seg < radiusSegments; ++seg) {
            creator.addFace4(r1 * _cos[seg + d1], r1 * _sin[seg + d1], z1,
                             r2 * _cos[seg + d1], r2 * _sin[seg + d1], z2,
                             r2 * _cos[seg + d2], r2 * _sin[seg + d2], z2,
                             r1 * _cos[seg + d2], r1 * _sin[seg + d2], z1);
            creator.setNormal_12_34(nxy * _cos[seg + d1], nxy * _sin[seg + d1], nz, nxy * _cos[seg + d2], nxy * _sin[seg + d2], nz);
         }

         z1 = z2;
         r1 = r2;
      }
   }

   // add top/bottom
   for (let layer = 0; layer < shape.fNz; layer += (shape.fNz - 1)) {
      const rmin = factor * shape.fRmin[layer], rmax = factor * shape.fRmax[layer];

      if (rmin === rmax)
         continue;

      const layerz = shape.fZ[layer],
            d1 = (layer === 0) ? 1 : 0, d2 = 1 - d1,
            normalz = (layer === 0) ? -1 : 1;

      if (!hasrmin && !cut_faces)
         creator.startPolygon(layer > 0);

      for (let seg = 0; seg < radiusSegments; ++seg) {
         creator.addFace4(rmin * _cos[seg + d1], rmin * _sin[seg + d1], layerz,
                          rmax * _cos[seg + d1], rmax * _sin[seg + d1], layerz,
                          rmax * _cos[seg + d2], rmax * _sin[seg + d2], layerz,
                          rmin * _cos[seg + d2], rmin * _sin[seg + d2], layerz,
                          hasrmin ? 0 : 2);
         creator.setNormal(0, 0, normalz);
      }

      creator.stopPolygon();
   }

   if (cut_faces) {
      for (let seg = 0; seg <= radiusSegments; seg += radiusSegments) {
         const d1 = (seg === 0) ? 1 : 2, d2 = 3 - d1;
         for (let n = 0; n < cut_faces.length; ++n) {
            const a = pnts[cut_faces[n][0]],
                  b = pnts[cut_faces[n][d1]],
                  c = pnts[cut_faces[n][d2]];

            creator.addFace3(a.x * _cos[seg], a.x * _sin[seg], a.y,
                             b.x * _cos[seg], b.x * _sin[seg], b.y,
                             c.x * _cos[seg], c.x * _sin[seg], c.y);

            creator.calcNormal();
         }
      }
   }

   return creator.create();
}

/** @summary Creates xtru geometry
  * @private */
function createXtruBuffer(shape, faces_limit) {
   let nfaces = (shape.fNz - 1) * shape.fNvert * 2;

   if (faces_limit < 0)
      return nfaces + shape.fNvert * 3;

   // create points
   const pnts = [];
   for (let vert = 0; vert < shape.fNvert; ++vert)
      pnts.push(new THREE.Vector2(shape.fX[vert], shape.fY[vert]));

   let faces = THREE.ShapeUtils.triangulateShape(pnts, []);
   if (faces.length < pnts.length - 2) {
      geoWarn(`Problem with XTRU shape ${shape.fName} with ${pnts.length} vertices`);
      faces = [];
   } else
      nfaces += faces.length * 2;

   const creator = faces_limit ? new PolygonsCreator() : new GeometryCreator(nfaces);

   for (let layer = 0; layer < shape.fNz - 1; ++layer) {
      const z1 = shape.fZ[layer], scale1 = shape.fScale[layer],
            z2 = shape.fZ[layer + 1], scale2 = shape.fScale[layer + 1],
            x01 = shape.fX0[layer], x02 = shape.fX0[layer + 1],
            y01 = shape.fY0[layer], y02 = shape.fY0[layer + 1];

      for (let vert1 = 0; vert1 < shape.fNvert; ++vert1) {
         const vert2 = (vert1 + 1) % shape.fNvert;
         creator.addFace4(scale1 * shape.fX[vert1] + x01, scale1 * shape.fY[vert1] + y01, z1,
                          scale2 * shape.fX[vert1] + x02, scale2 * shape.fY[vert1] + y02, z2,
                          scale2 * shape.fX[vert2] + x02, scale2 * shape.fY[vert2] + y02, z2,
                          scale1 * shape.fX[vert2] + x01, scale1 * shape.fY[vert2] + y01, z1);
         creator.calcNormal();
      }
   }

   for (let layer = 0; layer <= shape.fNz - 1; layer += (shape.fNz - 1)) {
      const z = shape.fZ[layer], scale = shape.fScale[layer],
            x0 = shape.fX0[layer], y0 = shape.fY0[layer];

      for (let n = 0; n < faces.length; ++n) {
         const face = faces[n],
               pnt1 = pnts[face[0]],
               pnt2 = pnts[face[layer === 0 ? 2 : 1]],
               pnt3 = pnts[face[layer === 0 ? 1 : 2]];

         creator.addFace3(scale * pnt1.x + x0, scale * pnt1.y + y0, z,
                          scale * pnt2.x + x0, scale * pnt2.y + y0, z,
                          scale * pnt3.x + x0, scale * pnt3.y + y0, z);
         creator.setNormal(0, 0, layer === 0 ? -1 : 1);
      }
   }

   return creator.create();
}

/** @summary Creates para geometry
  * @private */
function createParaboloidBuffer(shape, faces_limit) {
   let radiusSegments = Math.max(4, Math.round(360 / _cfg.GradPerSegm)),
       heightSegments = 30;

   if (faces_limit > 0) {
      const fact = 2 * radiusSegments * (heightSegments + 1) / faces_limit;
      if (fact > 1.0) {
         radiusSegments = Math.max(5, Math.floor(radiusSegments / Math.sqrt(fact)));
         heightSegments = Math.max(5, Math.floor(heightSegments / Math.sqrt(fact)));
      }
   }

   const rmin = shape.fRlo, rmax = shape.fRhi;
   let numfaces = (heightSegments + 1) * radiusSegments * 2;

   if (rmin === 0)
      numfaces -= radiusSegments * 2; // complete layer
   if (rmax === 0)
      numfaces -= radiusSegments * 2; // complete layer

   if (faces_limit < 0)
      return numfaces;

   let zmin = -shape.fDZ, zmax = shape.fDZ;

   // if no radius at -z, find intersection
   if (shape.fA >= 0)
      zmin = Math.max(zmin, shape.fB);
   else
      zmax = Math.min(shape.fB, zmax);

   const ttmin = Math.atan2(zmin, rmin),
         ttmax = Math.atan2(zmax, rmax),
         // calculate all sin/cos tables in advance
         _sin = new Float32Array(radiusSegments + 1),
         _cos = new Float32Array(radiusSegments + 1);
   for (let seg = 0; seg <= radiusSegments; ++seg) {
      _cos[seg] = Math.cos(seg / radiusSegments * 2 * Math.PI);
      _sin[seg] = Math.sin(seg / radiusSegments * 2 * Math.PI);
   }

   const creator = faces_limit ? new PolygonsCreator() : new GeometryCreator(numfaces);
   let lastz = zmin, lastr = 0, lastnxy = 0, lastnz = -1;

   for (let layer = 0; layer <= heightSegments + 1; ++layer) {
      if ((layer === 0) && (rmin === 0))
         continue;

      if ((layer === heightSegments + 1) && (lastr === 0))
         break;

      let layerz, radius;

      switch (layer) {
         case 0:
            layerz = zmin;
            radius = rmin;
            break;
         case heightSegments:
            layerz = zmax;
            radius = rmax;
            break;
         case heightSegments + 1:
            layerz = zmax;
            radius = 0;
            break;
         default: {
            const tt = Math.tan(ttmin + (ttmax - ttmin) * layer / heightSegments),
                  delta = tt ** 2 - 4 * shape.fA * shape.fB; // should be always positive (a*b < 0)
            radius = 0.5 * (tt + Math.sqrt(delta)) / shape.fA;
            if (radius < 1e-6)
               radius = 0;
            layerz = radius * tt;
            break;
         }
      }

      const nxy = shape.fA * radius,
            nz = (shape.fA > 0) ? -1 : 1,
            skip = (lastr === 0) ? 1 : ((radius === 0) ? 2 : 0);

      for (let seg = 0; seg < radiusSegments; ++seg) {
         creator.addFace4(radius * _cos[seg], radius * _sin[seg], layerz,
                          lastr * _cos[seg], lastr * _sin[seg], lastz,
                          lastr * _cos[seg + 1], lastr * _sin[seg + 1], lastz,
                          radius * _cos[seg + 1], radius * _sin[seg + 1], layerz, skip);

         // use analytic normal values when open/closing paraboloid around 0
         // cut faces (top or bottom) set with simple normal
         if ((skip === 0) || ((layer === 1) && (rmin === 0)) || ((layer === heightSegments + 1) && (rmax === 0))) {
            creator.setNormal4(nxy * _cos[seg], nxy * _sin[seg], nz,
                               lastnxy * _cos[seg], lastnxy * _sin[seg], lastnz,
                               lastnxy * _cos[seg + 1], lastnxy * _sin[seg + 1], lastnz,
                               nxy * _cos[seg + 1], nxy * _sin[seg + 1], nz, skip);
         } else
            creator.setNormal(0, 0, (layer < heightSegments) ? -1 : 1);
      }

      lastz = layerz;
      lastr = radius;
      lastnxy = nxy;
      lastnz = nz;
   }

   return creator.create();
}

/** @summary Creates hype geometry
  * @private */
function createHypeBuffer(shape, faces_limit) {
   if ((shape.fTin === 0) && (shape.fTout === 0))
      return createTubeBuffer(shape, faces_limit);

   let radiusSegments = Math.max(4, Math.round(360 / _cfg.GradPerSegm)),
       heightSegments = 30,
       numfaces = radiusSegments * (heightSegments + 1) * ((shape.fRmin > 0) ? 4 : 2);

   if (faces_limit < 0)
      return numfaces;

   if ((faces_limit > 0) && (faces_limit > numfaces)) {
      radiusSegments = Math.max(4, Math.floor(radiusSegments / Math.sqrt(numfaces / faces_limit)));
      heightSegments = Math.max(4, Math.floor(heightSegments / Math.sqrt(numfaces / faces_limit)));
      numfaces = radiusSegments * (heightSegments + 1) * ((shape.fRmin > 0) ? 4 : 2);
   }

   // calculate all sin/cos tables in advance
   const _sin = new Float32Array(radiusSegments + 1),
         _cos = new Float32Array(radiusSegments + 1);
   for (let seg = 0; seg <= radiusSegments; ++seg) {
      _cos[seg] = Math.cos(seg / radiusSegments * 2 * Math.PI);
      _sin[seg] = Math.sin(seg / radiusSegments * 2 * Math.PI);
   }

   const creator = faces_limit ? new PolygonsCreator() : new GeometryCreator(numfaces);

   // in-out side
   for (let side = 0; side < 2; ++side) {
      if ((side > 0) && (shape.fRmin <= 0))
         break;

      const r0 = (side > 0) ? shape.fRmin : shape.fRmax,
            tsq = (side > 0) ? shape.fTinsq : shape.fToutsq,
            d1 = 1 - side, d2 = 1 - d1;

      // vertical layers
      for (let layer = 0; layer < heightSegments; ++layer) {
         const z1 = -shape.fDz + layer / heightSegments * 2 * shape.fDz,
               z2 = -shape.fDz + (layer + 1) / heightSegments * 2 * shape.fDz,
               r1 = Math.sqrt(r0 ** 2 + tsq * z1 ** 2),
               r2 = Math.sqrt(r0 ** 2 + tsq * z2 ** 2);

         for (let seg = 0; seg < radiusSegments; ++seg) {
            creator.addFace4(r1 * _cos[seg + d1], r1 * _sin[seg + d1], z1,
                             r2 * _cos[seg + d1], r2 * _sin[seg + d1], z2,
                             r2 * _cos[seg + d2], r2 * _sin[seg + d2], z2,
                             r1 * _cos[seg + d2], r1 * _sin[seg + d2], z1);
            creator.calcNormal();
         }
      }
   }

   // add caps
   for (let layer = 0; layer < 2; ++layer) {
      const z = (layer === 0) ? shape.fDz : -shape.fDz,
            r1 = Math.sqrt(shape.fRmax ** 2 + shape.fToutsq * z ** 2),
            r2 = (shape.fRmin > 0) ? Math.sqrt(shape.fRmin ** 2 + shape.fTinsq * z ** 2) : 0,
            skip = (shape.fRmin > 0) ? 0 : 1,
            d1 = 1 - layer, d2 = 1 - d1;
      for (let seg = 0; seg < radiusSegments; ++seg) {
         creator.addFace4(r1 * _cos[seg + d1], r1 * _sin[seg + d1], z,
                          r2 * _cos[seg + d1], r2 * _sin[seg + d1], z,
                          r2 * _cos[seg + d2], r2 * _sin[seg + d2], z,
                          r1 * _cos[seg + d2], r1 * _sin[seg + d2], z, skip);
         creator.setNormal(0, 0, (layer === 0) ? 1 : -1);
      }
   }

   return creator.create();
}

/** @summary Creates tessellated geometry
  * @private */
function createTessellatedBuffer(shape, faces_limit) {
   let numfaces = 0;
   for (let i = 0; i < shape.fFacets.length; ++i)
      numfaces += (shape.fFacets[i].fNvert === 4) ? 2 : 1;
   if (faces_limit < 0)
      return numfaces;

   const creator = faces_limit ? new PolygonsCreator() : new GeometryCreator(numfaces);

   for (let i = 0; i < shape.fFacets.length; ++i) {
      const f = shape.fFacets[i],
            v0 = shape.fVertices[f.fIvert[0]].fVec,
            v1 = shape.fVertices[f.fIvert[1]].fVec,
            v2 = shape.fVertices[f.fIvert[2]].fVec;

      if (f.fNvert === 4) {
         const v3 = shape.fVertices[f.fIvert[3]].fVec;
         creator.addFace4(v0[0], v0[1], v0[2], v1[0], v1[1], v1[2], v2[0], v2[1], v2[2], v3[0], v3[1], v3[2]);
         creator.calcNormal();
      } else {
         creator.addFace3(v0[0], v0[1], v0[2], v1[0], v1[1], v1[2], v2[0], v2[1], v2[2]);
         creator.calcNormal();
      }
   }

   return creator.create();
}

/** @summary Creates Matrix4 from TGeoMatrix
  * @private */
function createMatrix(matrix) {
   if (!matrix)
      return null;

   let translation, rotation, scale;

   switch (matrix._typename) {
      case 'TGeoTranslation':
         translation = matrix.fTranslation;
         break;
      case 'TGeoRotation':
         rotation = matrix.fRotationMatrix;
         break;
      case 'TGeoScale':
         scale = matrix.fScale;
         break;
      case 'TGeoGenTrans':
         scale = matrix.fScale; // no break, translation and rotation follows
      // eslint-disable-next-line  no-fallthrough
      case 'TGeoCombiTrans':
         translation = matrix.fTranslation;
         rotation = matrix.fRotation?.fRotationMatrix;
         break;
      case 'TGeoHMatrix':
         translation = matrix.fTranslation;
         rotation = matrix.fRotationMatrix;
         scale = matrix.fScale;
         break;
      case 'TGeoIdentity':
         break;
      default:
         console.warn(`unsupported matrix ${matrix._typename}`);
   }

   if (!translation && !rotation && !scale)
      return null;

   const res = new THREE.Matrix4();

   if (rotation) {
      res.set(rotation[0], rotation[1], rotation[2], 0,
              rotation[3], rotation[4], rotation[5], 0,
              rotation[6], rotation[7], rotation[8], 0,
              0, 0, 0, 1);
   }

   if (translation)
      res.setPosition(translation[0], translation[1], translation[2]);

   if (scale)
      res.scale(new THREE.Vector3(scale[0], scale[1], scale[2]));

   return res;
}

/** @summary Creates transformation matrix for TGeoNode
  * @desc created after node visibility flag is checked and volume cut is performed
  * @private */
function getNodeMatrix(kind, node) {
   let matrix = null;

   if (kind === kindEve) {
      // special handling for EVE nodes

      matrix = new THREE.Matrix4();
      if (node.fTrans) {
         matrix.set(node.fTrans[0], node.fTrans[4], node.fTrans[8], 0,
                    node.fTrans[1], node.fTrans[5], node.fTrans[9], 0,
                    node.fTrans[2], node.fTrans[6], node.fTrans[10], 0,
                    0, 0, 0, 1);
         // second - set position with proper sign
         matrix.setPosition(node.fTrans[12], node.fTrans[13], node.fTrans[14]);
      }
   } else if (node.fMatrix)
      matrix = createMatrix(node.fMatrix);
   else if ((node._typename === 'TGeoNodeOffset') && node.fFinder) {
      const kPatternReflected = BIT(14),
            finder = node.fFinder,
            typ = finder._typename;
      if (finder.fBits & kPatternReflected)
         geoWarn(`Unsupported reflected pattern ${typ}`);
      if (typ.indexOf('TGeoPattern'))
         geoWarn(`Abnormal pattern type ${typ}`);
      const part = typ.slice(11);
      matrix = new THREE.Matrix4();
      switch (part) {
         case 'X':
         case 'Y':
         case 'Z':
         case 'ParaX':
         case 'ParaY':
         case 'ParaZ': {
            const _shift = finder.fStart + (node.fIndex + 0.5) * finder.fStep;
            switch (part.at(-1)) {
               case 'X': matrix.setPosition(_shift, 0, 0); break;
               case 'Y': matrix.setPosition(0, _shift, 0); break;
               case 'Z': matrix.setPosition(0, 0, _shift); break;
            }
            break;
         }
         case 'CylPhi': {
            const phi = (Math.PI / 180) * (finder.fStart + (node.fIndex + 0.5) * finder.fStep),
                  _cos = Math.cos(phi), _sin = Math.sin(phi);
            matrix.set(_cos, -_sin, 0, 0,
                       _sin, _cos, 0, 0,
                       0, 0, 1, 0,
                       0, 0, 0, 1);
            break;
         }
         case 'CylR':
            // seems to be, require no transformation
            break;
         case 'TrapZ': {
            const dz = finder.fStart + (node.fIndex + 0.5) * finder.fStep;
            matrix.setPosition(finder.fTxz * dz, finder.fTyz * dz, dz);
            break;
         }
         // case 'CylR': break;
         // case 'SphR': break;
         // case 'SphTheta': break;
         // case 'SphPhi': break;
         // case 'Honeycomb': break;
         default:
            geoWarn(`Unsupported pattern type ${typ}`);
            break;
      }
   }

   return matrix;
}

/** @summary Returns number of faces for provided geometry
  * @param geom  - can be BufferGeometry, CsgGeometry or interim array of polygons
  * @private */
function numGeometryFaces(geom) {
   if (!geom)
      return 0;

   if (geom instanceof Geometry)
      return geom.tree.numPolygons();

   // special array of polygons
   if (geom.polygons)
      return geom.polygons.length;

   const attr = geom.getAttribute('position');
   return attr?.count ? Math.round(attr.count / 3) : 0;
}

/** @summary Returns geometry bounding box
  * @private */
function geomBoundingBox(geom) {
   if (!geom)
      return null;

   let polygons = null;

   if (geom instanceof Geometry)
      polygons = geom.tree.collectPolygons();
   else if (geom.polygons)
      polygons = geom.polygons;

   if (polygons !== null) {
      const box = new THREE.Box3();
      for (let n = 0; n < polygons.length; ++n) {
         const polygon = polygons[n], nvert = polygon.vertices.length;
         for (let k = 0; k < nvert; ++k)
            box.expandByPoint(polygon.vertices[k]);
      }
      return box;
   }

   if (!geom.boundingBox)
      geom.computeBoundingBox();

   return geom.boundingBox.clone();
}

/** @summary Creates half-space geometry for given shape
  * @desc Just big-enough triangle to make BSP calculations
  * @private */
function createHalfSpace(shape, geom) {
   if (!shape?.fN || !shape?.fP)
      return null;

   const vertex = new THREE.Vector3(shape.fP[0], shape.fP[1], shape.fP[2]),
         normal = new THREE.Vector3(shape.fN[0], shape.fN[1], shape.fN[2]);

   normal.normalize();

   let sz = 1e10;
   if (geom) {
      // using real size of other geometry, we probably improve precision
      const box = geomBoundingBox(geom);
      if (box)
         sz = box.getSize(new THREE.Vector3()).length() * 1000;
   }

   const v0 = new THREE.Vector3(-sz, -sz / 2, 0),
         v1 = new THREE.Vector3(0, sz, 0),
         v2 = new THREE.Vector3(sz, -sz / 2, 0),
         v3 = new THREE.Vector3(0, 0, -sz),
         geometry = new THREE.BufferGeometry(),
         positions = new Float32Array([v0.x, v0.y, v0.z, v2.x, v2.y, v2.z, v1.x, v1.y, v1.z,
                                       v0.x, v0.y, v0.z, v1.x, v1.y, v1.z, v3.x, v3.y, v3.z,
                                       v1.x, v1.y, v1.z, v2.x, v2.y, v2.z, v3.x, v3.y, v3.z,
                                       v2.x, v2.y, v2.z, v0.x, v0.y, v0.z, v3.x, v3.y, v3.z]);
   geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
   geometry.computeVertexNormals();

   geometry.lookAt(normal);
   geometry.computeVertexNormals();

   for (let k = 0; k < positions.length; k += 3) {
      positions[k] += vertex.x;
      positions[k + 1] += vertex.y;
      positions[k + 2] += vertex.z;
   }

   return geometry;
}

let createGeometry = null;

/** @summary Creates geometry for composite shape
  * @private */
function createComposite(shape, faces_limit) {
   if (faces_limit < 0) {
      return createGeometry(shape.fNode.fLeft, -1) +
             createGeometry(shape.fNode.fRight, -1);
   }

   let geom1, geom2, return_bsp = false;
   const matrix1 = createMatrix(shape.fNode.fLeftMat),
         matrix2 = createMatrix(shape.fNode.fRightMat);

   if (faces_limit === 0)
      faces_limit = 4000;
   else
      return_bsp = true;

   if (matrix1 && (matrix1.determinant() < -0.9))
      geoWarn('Axis reflection in left composite shape - not supported');

   if (matrix2 && (matrix2.determinant() < -0.9))
      geoWarn('Axis reflections in right composite shape - not supported');

   if (shape.fNode.fLeft._typename === clTGeoHalfSpace)
      geom1 = createHalfSpace(shape.fNode.fLeft);
   else
      geom1 = createGeometry(shape.fNode.fLeft, faces_limit);

   if (!geom1)
      return null;

   let n1 = numGeometryFaces(geom1), n2 = 0;
   if (geom1._exceed_limit)
      n1 += faces_limit;

   if (n1 < faces_limit) {
      if (shape.fNode.fRight._typename === clTGeoHalfSpace)
         geom2 = createHalfSpace(shape.fNode.fRight, geom1);
      else
         geom2 = createGeometry(shape.fNode.fRight, faces_limit);


      n2 = numGeometryFaces(geom2);
   }

   if ((n1 + n2 >= faces_limit) || !geom2) {
      if (geom1.polygons)
         geom1 = createBufferGeometry(geom1.polygons);
      if (matrix1)
         geom1.applyMatrix4(matrix1);
      geom1._exceed_limit = true;
      return geom1;
   }

   let bsp1 = new Geometry(geom1, matrix1, _cfg.CompressComp ? 0 : undefined);

   const bsp2 = new Geometry(geom2, matrix2, bsp1.maxid);

   // take over maxid from both geometries
   bsp1.maxid = bsp2.maxid;

   switch (shape.fNode._typename) {
      case 'TGeoIntersection': // '*'
         bsp1.direct_intersect(bsp2);
         break;
      case 'TGeoUnion':  // '+'
         bsp1.direct_union(bsp2);
         break;
      case 'TGeoSubtraction': // '/'
         bsp1.direct_subtract(bsp2);
         break;
      default:
         geoWarn(`unsupported bool operation ${shape.fNode._typename}, use first geom`);
         break;
   }

   if (numGeometryFaces(bsp1) === 0) {
      geoWarn('Zero faces in comp shape' +
             ` left: ${shape.fNode.fLeft._typename} ${numGeometryFaces(geom1)} faces` +
             ` right: ${shape.fNode.fRight._typename} ${numGeometryFaces(geom2)} faces` +
             '  use first');
      bsp1 = new Geometry(geom1, matrix1);
   }

   return return_bsp ? { polygons: bsp1.toPolygons() } : bsp1.toBufferGeometry();
}

/** @summary Try to create projected geometry
  * @private */
function projectGeometry(geom, matrix, projection, position = 0, flippedMesh = false) {
   if (!geom.boundingBox)
      geom.computeBoundingBox();

   const box = geom.boundingBox.clone();

   box.applyMatrix4(matrix);

   if (((box.min[projection] >= position) && (box.max[projection] >= position)) ||
       ((box.min[projection] <= position) && (box.max[projection] <= position)))
      return null; // not interesting


   const bsp1 = new Geometry(geom, matrix, 0, flippedMesh),
         sizex = 2 * Math.max(Math.abs(box.min.x), Math.abs(box.max.x)),
         sizey = 2 * Math.max(Math.abs(box.min.y), Math.abs(box.max.y)),
         sizez = 2 * Math.max(Math.abs(box.min.z), Math.abs(box.max.z));
   let size = 10000;

   switch (projection) {
      case 'x': size = Math.max(sizey, sizez); break;
      case 'y': size = Math.max(sizex, sizez); break;
      case 'z': size = Math.max(sizex, sizey); break;
   }

   const bsp2 = createNormal(projection, position, size);

   bsp1.cut_from_plane(bsp2);

   return bsp2.toBufferGeometry();
}

/** @summary Creates geometry model for the provided shape
  * @param {Object} shape - instance of TGeoShape object
  * @param {Number} limit - defines return value, see details
  * @desc
  *  - if limit === 0 returns BufferGeometry
  *  - if limit < 0 just returns estimated number of faces
  *  - if limit > 0 return list of CsgPolygons (used only for composite shapes)
  * @private */
createGeometry = function(shape, limit = 0) {
   try {
      switch (shape._typename) {
         case clTGeoBBox: return createCubeBuffer(shape, limit);
         case clTGeoPara: return createParaBuffer(shape, limit);
         case clTGeoTrd1:
         case clTGeoTrd2: return createTrapezoidBuffer(shape, limit);
         case clTGeoArb8:
         case clTGeoTrap:
         case clTGeoGtra: return createArb8Buffer(shape, limit);
         case clTGeoSphere: return createSphereBuffer(shape, limit);
         case clTGeoCone:
         case clTGeoConeSeg:
         case clTGeoTube:
         case clTGeoTubeSeg:
         case clTGeoCtub: return createTubeBuffer(shape, limit);
         case clTGeoEltu: return createEltuBuffer(shape, limit);
         case clTGeoTorus: return createTorusBuffer(shape, limit);
         case clTGeoPcon:
         case clTGeoPgon: return createPolygonBuffer(shape, limit);
         case clTGeoXtru: return createXtruBuffer(shape, limit);
         case clTGeoParaboloid: return createParaboloidBuffer(shape, limit);
         case clTGeoHype: return createHypeBuffer(shape, limit);
         case 'TGeoTessellated': return createTessellatedBuffer(shape, limit);
         case clTGeoCompositeShape: return createComposite(shape, limit);
         case clTGeoShapeAssembly: break;
         case clTGeoScaledShape: {
            const res = createGeometry(shape.fShape, limit);
            if (shape.fScale && (limit >= 0) && isFunc(res?.scale))
               res.scale(shape.fScale.fScale[0], shape.fScale.fScale[1], shape.fScale.fScale[2]);
            return res;
         }
         case clTGeoHalfSpace:
            if (limit < 0)
               return 1; // half space if just plane used in composite
         // eslint-disable-next-line  no-fallthrough
         default:
            geoWarn(`unsupported shape type ${shape._typename}`);
      }
   } catch (e) {
      let place = '';
      if (e.stack !== undefined) {
         place = e.stack.split('\n')[0];
         place = place.indexOf(e.message) >= 0 ? e.stack.split('\n')[1] : 'at: ' + place;
      }
      geoWarn(`${shape._typename} err: ${e.message} ${place}`);
   }

   return limit < 0 ? 0 : null;
};


/** @summary Create single shape from EVE7 render date
  * @private */
function makeEveGeometry(rd) {
   let off = 0;

   if (rd.sz[0]) {
      rd.vtxBuff = new Float32Array(rd.raw.buffer, off, rd.sz[0]);
      off += rd.sz[0] * 4;
   }

   if (rd.sz[1]) {
      // normals were not used
      // rd.nrmBuff = new Float32Array(rd.raw.buffer, off, rd.sz[1]);
      off += rd.sz[1] * 4;
   }

   if (rd.sz[2]) {
      // these are special values in the buffer begin
      rd.prefixBuf = new Uint32Array(rd.raw.buffer, off, 2);
      off += 2 * 4;
      rd.idxBuff = new Uint32Array(rd.raw.buffer, off, rd.sz[2] - 2);
      // off += (rd.sz[2]-2)*4;
   }

   const GL_TRIANGLES = 4; // same as in EVE7

   if (rd.prefixBuf[0] !== GL_TRIANGLES)
      throw Error('Expect triangles first.');

   const nVert = 3 * rd.prefixBuf[1]; // number of vertices to draw

   if (rd.idxBuff.length !== nVert)
      throw Error('Expect single list of triangles in index buffer.');

   const body = new THREE.BufferGeometry();
   body.setAttribute('position', new THREE.BufferAttribute(rd.vtxBuff, 3));
   body.setIndex(new THREE.BufferAttribute(rd.idxBuff, 1));
   body.computeVertexNormals();

   return body;
}

/** @summary Create single shape from geometry viewer render date
  * @private */
function makeViewerGeometry(rd) {
   const vtxBuff = new Float32Array(rd.raw.buffer, 0, rd.raw.buffer.byteLength / 4),
         body = new THREE.BufferGeometry();
   body.setAttribute('position', new THREE.BufferAttribute(vtxBuff, 3));
   body.setIndex(new THREE.BufferAttribute(new Uint32Array(rd.idx), 1));
   body.computeVertexNormals();
   return body;
}

/** @summary Create single shape from provided raw data from web viewer.
  * @desc If nsegm changed, shape will be recreated
  * @private */
function createServerGeometry(rd, nsegm) {
   if (rd.server_shape && ((rd.nsegm === nsegm) || !rd.shape))
      return rd.server_shape;

   rd.nsegm = nsegm;

   let geom;

   if (rd.shape) {
      // case when TGeoShape provided as is
      geom = createGeometry(rd.shape);
   } else {
      if (!rd.raw?.buffer) {
         console.error('No raw data at all');
         return null;
      }

      geom = rd.sz ? makeEveGeometry(rd) : makeViewerGeometry(rd);
   }

   // shape handle is similar to created in TGeoPainter
   return {
      _typename: kShapeType, // indicate that shape can be used as is
      ready: true,
      geom,
      nfaces: numGeometryFaces(geom)
   };
}

/** @summary Provides info about geo object, used for tooltip info
  * @param {Object} obj - any kind of TGeo-related object like shape or node or volume
  * @private */
function provideObjectInfo(obj) {
   let info = [], shape = null;

   if (obj.fVolume !== undefined)
      shape = obj.fVolume.fShape;
   else if (obj.fShape !== undefined)
      shape = obj.fShape;
   else if ((obj.fShapeBits !== undefined) && (obj.fShapeId !== undefined))
      shape = obj;

   if (!shape) {
      info.push(obj._typename);
      return info;
   }

   const sz = Math.max(shape.fDX, shape.fDY, shape.fDZ),
         useexp = (sz > 1e7) || (sz < 1e-7),
         conv = v => {
            if (v === undefined)
               return '???';
            if ((v === Math.round(v) && v < 1e7))
               return Math.round(v);
            return useexp ? v.toExponential(4) : v.toPrecision(7);
         };

   info.push(shape._typename);

   info.push(`DX=${conv(shape.fDX)} DY=${conv(shape.fDY)} DZ=${conv(shape.fDZ)}`);

   switch (shape._typename) {
      case clTGeoPara:
         info.push(`Alpha=${shape.fAlpha} Phi=${shape.fPhi} Theta=${shape.fTheta}`);
         break;
      case clTGeoTrd2:
         info.push(`Dy1=${conv(shape.fDy1)} Dy2=${conv(shape.fDy1)}`); // no break
      // eslint-disable-next-line  no-fallthrough
      case clTGeoTrd1:
         info.push(`Dx1=${conv(shape.fDx1)} Dx2=${conv(shape.fDx1)}`);
         break;
      case clTGeoSphere:
         info.push(`Rmin=${conv(shape.fRmin)} Rmax=${conv(shape.fRmax)}`,
                   `Phi1=${shape.fPhi1} Phi2=${shape.fPhi2}`,
                   `Theta1=${shape.fTheta1} Theta2=${shape.fTheta2}`);
         break;
      case clTGeoConeSeg:
         info.push(`Phi1=${shape.fPhi1} Phi2=${shape.fPhi2}`);
      // eslint-disable-next-line  no-fallthrough
      case clTGeoCone:
         info.push(`Rmin1=${conv(shape.fRmin1)} Rmax1=${conv(shape.fRmax1)}`,
                   `Rmin2=${conv(shape.fRmin2)} Rmax2=${conv(shape.fRmax2)}`);
         break;
      case clTGeoCtub:
      case clTGeoTubeSeg:
         info.push(`Phi1=${shape.fPhi1} Phi2=${shape.fPhi2}`);
      // eslint-disable-next-line  no-fallthrough
      case clTGeoEltu:
      case clTGeoTube:
         info.push(`Rmin=${conv(shape.fRmin)} Rmax=${conv(shape.fRmax)}`);
         break;
      case clTGeoTorus:
         info.push(`Rmin=${conv(shape.fRmin)} Rmax=${conv(shape.fRmax)}`,
                   `Phi1=${shape.fPhi1} Dphi=${shape.fDphi}`);
         break;
      case clTGeoParaboloid:
         info.push(`Rlo=${conv(shape.fRlo)} Rhi=${conv(shape.fRhi)}`,
                   `A=${conv(shape.fA)} B=${conv(shape.fB)}`);
         break;
      case clTGeoHype:
         info.push(`Rmin=${conv(shape.fRmin)} Rmax=${conv(shape.fRmax)}`,
                   `StIn=${conv(shape.fStIn)} StOut=${conv(shape.fStOut)}`);
         break;
      case clTGeoScaledShape:
         info = provideObjectInfo(shape.fShape);
         if (shape.fScale)
            info.unshift(`Scale X=${shape.fScale.fScale[0]} Y=${shape.fScale.fScale[1]} Z=${shape.fScale.fScale[2]}`);
         break;
      case clTGeoPcon:
      case clTGeoPgon:
         info.push(`Phi1=${conv(shape.fPhi1)} Dphi=${conv(shape.fDphi)}`,
                   `Nz=${shape.fNz}`);
         break;
      case clTGeoXtru:
         info.push(`Nz=${shape.fNz} Nvert=${shape.fNvert}`);
         break;
      case clTGeoCompositeShape:
         info.push(`Type ${shape.fNode?._typename}`,
                   `Left=${shape.fNode?.fLeft?._typename} Right=${shape.fNode?.fRight?._typename}`);
         break;
      case clTGeoBBox:
         info.push(`Origin=[${conv(shape.fOrigin[0])},${conv(shape.fOrigin[1])},${conv(shape.fOrigin[2])}]`);
         break;
      case clTGeoArb8:
         break;
      case clTGeoGtra:
         info.push(`TwistAngle=${conv(shape.fTwistAngle)}`);
      // eslint-disable-next-line  no-fallthrough
      case clTGeoTrap:
         info.push(`Phi=${conv(shape.fPhi)} Theta=${conv(shape.fTheta)}`);
         break;
   }

   return info;
}

/** @summary Creates projection matrix for the camera
  * @private */
function createProjectionMatrix(camera) {
   const cameraProjectionMatrix = new THREE.Matrix4();

   camera.updateMatrixWorld();

   camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
   cameraProjectionMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);

   return cameraProjectionMatrix;
}

/** @summary Creates frustum
  * @private */
function createFrustum(source) {
   if (!source)
      return null;

   if (source instanceof THREE.PerspectiveCamera)
      source = createProjectionMatrix(source);

   const frustum = new THREE.Frustum();
   frustum.setFromProjectionMatrix(source);

   frustum.corners = new Float32Array([
      1, 1, 1,
      1, 1, -1,
      1, -1, 1,
      1, -1, -1,
      -1, 1, 1,
      -1, 1, -1,
      -1, -1, 1,
      -1, -1, -1,
      0, 0, 0 // also check center of the shape
   ]);

   frustum.test = new THREE.Vector3(0, 0, 0);

   frustum.CheckShape = function(matrix, shape) {
      const pnt = this.test, len = this.corners.length, corners = this.corners;

      for (let i = 0; i < len; i += 3) {
         pnt.x = corners[i] * shape.fDX;
         pnt.y = corners[i + 1] * shape.fDY;
         pnt.z = corners[i + 2] * shape.fDZ;
         if (this.containsPoint(pnt.applyMatrix4(matrix)))
            return true;
      }

      return false;
   };

   frustum.CheckPoint = function(x, y, z) {
      return this.containsPoint(this.test.set(x, y, z)) ? 1 : 0;
   };

   frustum.CheckBox = function(box) {
      // only if 6 edges and more are seen, we think that box is fully visible
      return this.CheckPoint(box.min.x, box.min.y, box.min.z) +
             this.CheckPoint(box.min.x, box.min.y, box.max.z) +
             this.CheckPoint(box.min.x, box.max.y, box.min.z) +
             this.CheckPoint(box.min.x, box.max.y, box.max.z) +
             this.CheckPoint(box.max.x, box.max.y, box.max.z) +
             this.CheckPoint(box.max.x, box.min.y, box.max.z) +
             this.CheckPoint(box.max.x, box.max.y, box.min.z) +
             this.CheckPoint(box.max.x, box.max.y, box.max.z) > 5;
   };

   return frustum;
}

/** @summary Create node material
  * @private */
function createMaterial(cfg, args0) {
   if (!cfg)
      cfg = { material_kind: 'lambert' };

   const args = Object.assign({}, args0);

   if (args.opacity === undefined)
      args.opacity = 1;

   if (cfg.transparency)
      args.opacity = Math.min(1 - cfg.transparency, args.opacity);

   args.wireframe = cfg.wireframe ?? false;
   if (!args.color)
      args.color = 'red';
   args.side = THREE.FrontSide;
   args.transparent = args.opacity < 1;
   args.depthWrite = args.opactity === 1;

   let material;

   if (cfg.material_kind === 'basic')
      material = new THREE.MeshBasicMaterial(args);
   else if (cfg.material_kind === 'depth') {
      delete args.color;
      material = new THREE.MeshDepthMaterial(args);
   } else if (cfg.material_kind === 'toon')
      material = new THREE.MeshToonMaterial(args);
   else if (cfg.material_kind === 'matcap') {
      delete args.wireframe;
      material = new THREE.MeshMatcapMaterial(args);
   } else if (cfg.material_kind === 'standard') {
      args.metalness = cfg.metalness ?? 0.5;
      args.roughness = cfg.roughness ?? 0.1;
      material = new THREE.MeshStandardMaterial(args);
   } else if (cfg.material_kind === 'normal') {
      delete args.color;
      material = new THREE.MeshNormalMaterial(args);
   } else if (cfg.material_kind === 'physical') {
      args.metalness = cfg.metalness ?? 0.5;
      args.roughness = cfg.roughness ?? 0.1;
      args.reflectivity = cfg.reflectivity ?? 0.5;
      args.emissive = args.color;
      material = new THREE.MeshPhysicalMaterial(args);
   } else if (cfg.material_kind === 'phong') {
      args.shininess = cfg.shininess ?? 0.9;
      material = new THREE.MeshPhongMaterial(args);
   } else {
      args.vertexColors = false;
      material = new THREE.MeshLambertMaterial(args);
   }

   if ((material.flatShading !== undefined) && (cfg.flatShading !== undefined))
      material.flatShading = cfg.flatShading;
   material.inherentOpacity = args0.opacity ?? 1;
   material.inherentArgs = args0;

   return material;
}

/** @summary Compares two stacks.
  * @return {Number} 0 if same, -1 when stack1 < stack2, +1 when stack1 > stack2
  * @private */
function compare_stacks(stack1, stack2) {
   if (stack1 === stack2)
      return 0;
   const len1 = stack1?.length ?? 0,
         len2 = stack2?.length ?? 0,
         len = (len1 < len2) ? len1 : len2;
   let indx = 0;
   while (indx < len) {
      if (stack1[indx] < stack2[indx])
         return -1;
      if (stack1[indx] > stack2[indx])
         return 1;
      ++indx;
   }

   return (len1 < len2) ? -1 : ((len1 > len2) ? 1 : 0);
}

/** @summary Checks if two stack arrays are identical
  * @private */
function isSameStack(stack1, stack2) {
   if (!stack1 || !stack2)
      return false;
   if (stack1 === stack2)
      return true;
   if (stack1.length !== stack2.length)
      return false;
   for (let k = 0; k < stack1.length; ++k) {
      if (stack1[k] !== stack2[k])
         return false;
   }
   return true;
}


function createFlippedGeom(geom) {
   let pos = geom.getAttribute('position').array,
       norm = geom.getAttribute('normal').array;
   const index = geom.getIndex();

   if (index) {
      // we need to unfold all points to
      const arr = index.array,
            i0 = geom.drawRange.start;
      let ilen = geom.drawRange.count;
      if (i0 + ilen > arr.length)
         ilen = arr.length - i0;

      const dpos = new Float32Array(ilen * 3),
            dnorm = new Float32Array(ilen * 3);
      for (let ii = 0; ii < ilen; ++ii) {
         const k = arr[i0 + ii];
         if ((k < 0) || (k * 3 >= pos.length))
            console.log(`strange index ${k * 3} totallen = ${pos.length}`);
         dpos[ii * 3] = pos[k * 3];
         dpos[ii * 3 + 1] = pos[k * 3 + 1];
         dpos[ii * 3 + 2] = pos[k * 3 + 2];
         dnorm[ii * 3] = norm[k * 3];
         dnorm[ii * 3 + 1] = norm[k * 3 + 1];
         dnorm[ii * 3 + 2] = norm[k * 3 + 2];
      }

      pos = dpos;
      norm = dnorm;
   }

   const len = pos.length,
         newpos = new Float32Array(len),
         newnorm = new Float32Array(len);

   // we should swap second and third point in each face
   for (let n = 0, shift = 0; n < len; n += 3) {
      newpos[n] = pos[n + shift];
      newpos[n + 1] = pos[n + 1 + shift];
      newpos[n + 2] = -pos[n + 2 + shift];

      newnorm[n] = norm[n + shift];
      newnorm[n + 1] = norm[n + 1 + shift];
      newnorm[n + 2] = -norm[n + 2 + shift];

      shift += 3;
      if (shift === 6)
         shift = -3; // values 0,3,-3
   }

   const geomZ = new THREE.BufferGeometry();
   geomZ.setAttribute('position', new THREE.BufferAttribute(newpos, 3));
   geomZ.setAttribute('normal', new THREE.BufferAttribute(newnorm, 3));

   return geomZ;
}


/** @summary Create flipped mesh for the shape
  * @desc When transformation matrix includes one or several inversion of axis,
  * one should inverse geometry object, otherwise three.js cannot correctly draw it
  * @param {Object} shape - TGeoShape object
  * @param {Object} material - material
  * @private */
function createFlippedMesh(shape, material) {
   if (shape.geomZ === undefined)
      shape.geomZ = createFlippedGeom(shape.geom);

   const mesh = new THREE.Mesh(shape.geomZ, material);
   mesh.scale.copy(new THREE.Vector3(1, 1, -1));
   mesh.updateMatrix();

   mesh._flippedMesh = true;

   return mesh;
}


/**
  * @summary class for working with cloned nodes
  *
  * @private
  */

class ClonedNodes {

   /** @summary Constructor */
   constructor(obj, clones) {
      this.toplevel = true; // indicate if object creates top-level structure with Nodes and Volumes folder
      this.name_prefix = ''; // name prefix used for nodes names
      this.maxdepth = 1;  // maximal hierarchy depth, required for transparency
      this.vislevel = 4;  // maximal depth of nodes visibility aka gGeoManager->SetVisLevel, same default
      this.maxnodes = 10000; // maximal number of visible nodes aka gGeoManager->fMaxVisNodes

      if (obj) {
         if (obj.$geoh)
            this.toplevel = false;
         this.createClones(obj);
      } else if (clones)
         this.nodes = clones;
   }

   /** @summary Set maximal depth for nodes visibility */
   setVisLevel(lvl) {
      this.vislevel = lvl && Number.isInteger(lvl) ? lvl : 4;
   }

   /** @summary Returns maximal depth for nodes visibility */
   getVisLevel() {
      return this.vislevel;
   }

   /** @summary Set maximal number of visible nodes
    * @desc By default 10000 nodes will be visualized */
   setMaxVisNodes(v, more) {
      this.maxnodes = (v === Infinity) ? 1e9 : (Number.isFinite(v) ? v : 10000);
      if (more && Number.isFinite(more))
         this.maxnodes *= more;
   }

   /** @summary Returns configured maximal number of visible nodes */
   getMaxVisNodes() {
      return this.maxnodes;
   }

   /** @summary Set geo painter configuration - used for material creation */
   setConfig(cfg) {
      this._cfg = cfg;
   }

   /** @summary Insert node into existing array */
   updateNode(node) {
      if (node && Number.isInteger(node.id) && (node.id < this.nodes.length))
         this.nodes[node.id] = node;
   }

   /** @summary Returns TGeoShape for element with given indx */
   getNodeShape(indx) {
      if (!this.origin || !this.nodes)
         return null;
      const obj = this.origin[indx], clone = this.nodes[indx];
      if (!obj || !clone)
         return null;
      return clone.kind === kindGeo ? obj.fVolume?.fShape : obj.fShape;
   }

   /** @summary function to cleanup as much as possible structures
     * @desc Provided parameters drawnodes and drawshapes are arrays created during building of geometry */
   cleanup(drawnodes, drawshapes) {
      if (drawnodes) {
         for (let n = 0; n < drawnodes.length; ++n) {
            delete drawnodes[n].stack;
            drawnodes[n] = undefined;
         }
      }

      if (drawshapes) {
         for (let n = 0; n < drawshapes.length; ++n) {
            delete drawshapes[n].geom;
            drawshapes[n] = undefined;
         }
      }

      if (this.nodes) {
         for (let n = 0; n < this.nodes.length; ++n) {
            if (this.nodes[n])
               delete this.nodes[n].chlds;
         }
      }

      delete this.nodes;
      delete this.origin;

      delete this.sortmap;
   }

   /** @summary Create complete description for provided Geo object */
   createClones(obj, sublevel, kind) {
      if (!sublevel) {
         if (obj?._typename === kShapeType)
            return this.createClonesForShape(obj);

         this.origin = [];
         sublevel = 1;
         kind = getNodeKind(obj);
      }

      if ((kind < 0) || !obj || ('_refid' in obj))
         return;

      obj._refid = this.origin.length;
      this.origin.push(obj);
      this.maxdepth = Math.max(this.maxdepth, sublevel);

      let chlds;
      if (kind === kindGeo)
         chlds = obj.fVolume?.fNodes?.arr || null;
      else
         chlds = obj.fElements?.arr || null;

      if (chlds !== null) {
         checkDuplicates(obj, chlds);
         for (let i = 0; i < chlds.length; ++i)
            this.createClones(chlds[i], sublevel + 1, kind);
      }

      if (sublevel > 1)
         return;

      this.nodes = [];

      const sortarr = [];

      // first create nodes objects
      for (let id = 0; id < this.origin.length; ++id) {
         // let obj = this.origin[id];
         const node = { id, kind, vol: 0, nfaces: 0 };
         this.nodes.push(node);
         sortarr.push(node); // array use to produce sortmap
      }

      // than fill children lists
      for (let n = 0; n < this.origin.length; ++n) {
         const obj2 = this.origin[n],
               clone = this.nodes[n],
               shape = kind === kindEve ? obj2.fShape : obj2.fVolume.fShape,
               chlds2 = kind === kindEve ? obj2.fElements?.arr : obj2.fVolume.fNodes?.arr,
               matrix = getNodeMatrix(kind, obj2);

         if (matrix) {
            clone.matrix = matrix.elements; // take only matrix elements, matrix will be constructed in worker
            if (clone.matrix && (clone.matrix[0] === 1)) {
               let issimple = true;
               for (let k = 1; (k < clone.matrix.length) && issimple; ++k)
                  issimple = (clone.matrix[k] === ((k === 5) || (k === 10) || (k === 15) ? 1 : 0));
               if (issimple)
                  delete clone.matrix;
            }
            if (clone.matrix && (kind === kindEve))
               clone.abs_matrix = true;
         }
         if (shape) {
            clone.fDX = shape.fDX;
            clone.fDY = shape.fDY;
            clone.fDZ = shape.fDZ;
            clone.vol = Math.sqrt(shape.fDX ** 2 + shape.fDY ** 2 + shape.fDZ ** 2);
            if (shape.$nfaces === undefined)
               shape.$nfaces = createGeometry(shape, -1);
            clone.nfaces = shape.$nfaces;
            if (clone.nfaces <= 0)
               clone.vol = 0;
         }

         if (chlds2) {
            // in cloned object children is only list of ids
            clone.chlds = new Array(chlds2.length);
            for (let k = 0; k < chlds2.length; ++k)
               clone.chlds[k] = chlds2[k]._refid;
         }
      }

      // remove _refid identifiers from original objects
      for (let n = 0; n < this.origin.length; ++n)
         delete this.origin[n]._refid;

      // do sorting once
      sortarr.sort((a, b) => b.vol - a.vol);

      // remember sort map and also sortid
      this.sortmap = new Array(this.nodes.length);
      for (let n = 0; n < this.nodes.length; ++n) {
         this.sortmap[n] = sortarr[n].id;
         sortarr[n].sortid = n;
      }
   }

   /** @summary Create elementary item with single already existing shape
     * @desc used by details view of geometry shape */
   createClonesForShape(obj) {
      this.origin = [];

      // indicate that just plain shape is used
      this.plain_shape = obj;

      this.nodes = [{
         id: 0, sortid: 0, kind: kindShape,
         name: 'Shape',
         nfaces: obj.nfaces,
         fDX: 1, fDY: 1, fDZ: 1, vol: 1,
         vis: true
      }];
   }

   /** @summary Count all visible nodes */
   countVisibles() {
      const len = this.nodes?.length || 0;
      let cnt = 0;
      for (let k = 0; k < len; ++k) {
         if (this.nodes[k].vis)
            cnt++;
      }
      return cnt;
   }

   /** @summary Mark visible nodes.
     * @desc Set only basic flags, actual visibility depends from hierarchy */
   markVisibles(on_screen, copy_bits, hide_top_volume) {
      if (this.plain_shape)
         return 1;
      if (!this.origin || !this.nodes)
         return 0;

      let res = 0;

      for (let n = 0; n < this.nodes.length; ++n) {
         const clone = this.nodes[n], obj = this.origin[n];

         clone.vis = 0; // 1 - only with last level
         delete clone.nochlds;

         if (clone.kind === kindGeo) {
            if (obj.fVolume) {
               if (on_screen) {
                  // on screen bits used always, childs always checked
                  clone.vis = testGeoBit(obj.fVolume, geoBITS.kVisOnScreen) ? 99 : 0;

                  if ((n === 0) && clone.vis && hide_top_volume)
                     clone.vis = 0;

                  if (copy_bits) {
                     setGeoBit(obj.fVolume, geoBITS.kVisNone, false);
                     setGeoBit(obj.fVolume, geoBITS.kVisThis, (clone.vis > 0));
                     setGeoBit(obj.fVolume, geoBITS.kVisDaughters, true);
                     setGeoBit(obj, geoBITS.kVisDaughters, true);
                  }
               } else {
                  clone.vis = !testGeoBit(obj.fVolume, geoBITS.kVisNone) && testGeoBit(obj.fVolume, geoBITS.kVisThis) ? 99 : 0;

                  if (!testGeoBit(obj, geoBITS.kVisDaughters) || !testGeoBit(obj.fVolume, geoBITS.kVisDaughters))
                     clone.nochlds = true;

                  // node with childs only shown in case if it is last level in hierarchy
                  if ((clone.vis > 0) && clone.chlds && !clone.nochlds)
                     clone.vis = 1;

                  // special handling for top node
                  if (n === 0) {
                     if (hide_top_volume)
                        clone.vis = 0;
                     delete clone.nochlds;
                  }
               }
            }
         } else {
            clone.vis = obj.fRnrSelf ? 99 : 0;

            // when the only node is selected, draw it
            if ((n === 0) && (this.nodes.length === 1))
               clone.vis = 99;

            this.vislevel = 9999; // automatically take all volumes
         }

         // shape with zero volume or without faces will not be observed
         if ((clone.vol <= 0) || (clone.nfaces <= 0))
            clone.vis = 0;

         if (clone.vis)
            res++;
      }

      return res;
   }

   /** @summary After visibility flags is set, produce id shifts for all nodes as it would be maximum level */
   produceIdShifts() {
      for (let k = 0; k < this.nodes.length; ++k)
         this.nodes[k].idshift = -1;

      function scan_func(nodes, node) {
         if (node.idshift < 0) {
            node.idshift = 0;
            if (node.chlds) {
               for (let k = 0; k < node.chlds.length; ++k)
                  node.idshift += scan_func(nodes, nodes[node.chlds[k]]);
            }
         }

         return node.idshift + 1;
      }

      scan_func(this.nodes, this.nodes[0]);
   }

   /** @summary Extract only visibility flags
     * @desc Used to transfer them to the worker */
   getVisibleFlags() {
      const res = new Array(this.nodes.length);
      for (let n = 0; n < this.nodes.length; ++n)
         res[n] = { vis: this.nodes[n].vis, nochlds: this.nodes[n].nochlds };
      return res;
   }

   /** @summary Assign only visibility flags, extracted with getVisibleFlags */
   setVisibleFlags(flags) {
      if (!this.nodes || !flags || !flags.length !== this.nodes.length)
         return 0;

      let res = 0;
      for (let n = 0; n < this.nodes.length; ++n) {
         const clone = this.nodes[n];
         clone.vis = flags[n].vis;
         clone.nochlds = flags[n].nochlds;
         if (clone.vis)
            res++;
      }

      return res;
   }

   /** @summary Set visibility flag for physical node
     * @desc Trying to reimplement functionality in the RGeomViewer */
   setPhysNodeVisibility(stack, on) {
      let do_clear = false;
      if (on === 'clearall') {
         delete this.fVisibility;
         return;
      } else if (on === 'clear') {
         do_clear = true;
         if (!this.fVisibility)
            return;
      } else
         on = Boolean(on);
      if (!stack)
         return;

      if (!this.fVisibility)
         this.fVisibility = [];

      for (let indx = 0; indx < this.fVisibility.length; ++indx) {
         const item = this.fVisibility[indx],
               res = compare_stacks(item.stack, stack);

         if (res === 0) {
            if (do_clear) {
               this.fVisibility.splice(indx, 1);
               if (!this.fVisibility.length)
                  delete this.fVisibility;
            } else
               item.visible = on;

            return;
         }

         if (res > 0) {
            if (!do_clear)
               this.fVisibility.splice(indx, 0, { visible: on, stack });
            return;
         }
      }

      if (!do_clear)
         this.fVisibility.push({ visible: on, stack });
   }

   /** @summary Get visibility item for physical node */
   getPhysNodeVisibility(stack) {
      if (!stack || !this.fVisibility)
         return null;
      for (let indx = 0; indx < this.fVisibility.length; ++indx) {
         const item = this.fVisibility[indx],
               res = compare_stacks(item.stack, stack);
         if (res === 0)
            return item;
         if (res > 0)
            return null;
      }

      return null;
   }

   /** @summary Scan visible nodes in hierarchy, starting from nodeid
     * @desc Each entry in hierarchy get its unique id, which is not changed with visibility flags */
   scanVisible(arg, vislvl) {
      if (!this.nodes)
         return 0;

      if (vislvl === undefined) {
         if (!arg)
            arg = {};

         vislvl = arg.vislvl || this.vislevel || 4; // default 3 in ROOT
         if (vislvl > 88)
            vislvl = 88;

         arg.stack = new Array(100); // current stack
         arg.nodeid = 0;
         arg.counter = 0; // sequence ID of the node, used to identify it later
         arg.last = 0;
         arg.copyStack = function(factor) {
            const entry = { nodeid: this.nodeid, seqid: this.counter, stack: new Array(this.last) };
            if (factor)
               entry.factor = factor; // factor used to indicate importance of entry, will be built as first
            for (let n = 0; n < this.last; ++n)
               entry.stack[n] = this.stack[n + 1]; // copy stack
            return entry;
         };

         if (arg.domatrix) {
            arg.matrices = [];
            arg.mpool = [new THREE.Matrix4()]; // pool of Matrix objects to avoid permanent creation
            arg.getmatrix = function() { return this.matrices[this.last]; };
         }

         if (this.fVisibility?.length) {
            arg.vindx = 0;
            arg.varray = this.fVisibility;
            arg.vstack = arg.varray[arg.vindx].stack;
            arg.testPhysVis = function() {
               if (!this.vstack || (this.vstack?.length !== this.last))
                  return undefined;
               for (let n = 0; n < this.last; ++n) {
                  if (this.vstack[n] !== this.stack[n + 1])
                     return undefined;
               }
               const res = this.varray[this.vindx++].visible;
               this.vstack = this.vindx < this.varray.length ? this.varray[this.vindx].stack : null;
               return res;
            };
         }
      }

      const node = this.nodes[arg.nodeid];
      let res = 0;

      if (arg.domatrix) {
         if (!arg.mpool[arg.last + 1])
            arg.mpool[arg.last + 1] = new THREE.Matrix4();

         const prnt = (arg.last > 0) ? arg.matrices[arg.last - 1] : new THREE.Matrix4();
         if (node.matrix) {
            arg.matrices[arg.last] = arg.mpool[arg.last].fromArray(prnt.elements);
            arg.matrices[arg.last].multiply(arg.mpool[arg.last + 1].fromArray(node.matrix));
         } else
            arg.matrices[arg.last] = prnt;
      }

      let node_vis = node.vis, node_nochlds = node.nochlds;

      if ((arg.nodeid === 0) && arg.main_visible)
         node_vis = vislvl + 1;
      else if (arg.testPhysVis) {
         const res2 = arg.testPhysVis();
         if (res2 !== undefined) {
            node_vis = res2 && !node.chlds ? vislvl + 1 : 0;
            node_nochlds = !res2;
         }
      }

      if (node_nochlds)
         vislvl = 0;

      if (node_vis > vislvl) {
         if (!arg.func || arg.func(node))
            res++;
      }

      arg.counter++;

      if ((vislvl > 0) && node.chlds) {
         arg.last++;
         for (let i = 0; i < node.chlds.length; ++i) {
            arg.nodeid = node.chlds[i];
            arg.stack[arg.last] = i; // in the stack one store index of child, it is path in the hierarchy
            res += this.scanVisible(arg, vislvl - 1);
         }
         arg.last--;
      } else
         arg.counter += (node.idshift || 0);


      if (arg.last === 0) {
         delete arg.last;
         delete arg.stack;
         delete arg.copyStack;
         delete arg.counter;
         delete arg.matrices;
         delete arg.mpool;
         delete arg.getmatrix;
         delete arg.vindx;
         delete arg.varray;
         delete arg.vstack;
         delete arg.testPhysVis;
      }

      return res;
   }

   /** @summary Return node name with given id.
    * @desc Either original object or description is used */
   getNodeName(nodeid) {
      if (this.origin) {
         const obj = this.origin[nodeid];
         return obj ? getObjectName(obj) : '';
      }
      const node = this.nodes[nodeid];
      return node ? node.name : '';
   }

   /** @summary Returns description for provided stack
     * @desc If specified, absolute matrix is also calculated */
   resolveStack(stack, withmatrix) {
      const res = { id: 0, obj: null, node: this.nodes[0], name: this.name_prefix || '' };

      if (withmatrix) {
         res.matrix = new THREE.Matrix4();
         if (res.node.matrix)
            res.matrix.fromArray(res.node.matrix);
      }

      if (this.origin)
         res.obj = this.origin[0];

      // if (!res.name)
      //   res.name = this.getNodeName(0);

      if (stack) {
         for (let lvl = 0; lvl < stack.length; ++lvl) {
            res.id = res.node.chlds[stack[lvl]];
            res.node = this.nodes[res.id];

            if (this.origin)
               res.obj = this.origin[res.id];

            const subname = this.getNodeName(res.id);
            if (subname) {
               if (res.name)
                  res.name += '/';
               res.name += subname;
            }

            if (withmatrix && res.node.matrix)
               res.matrix.multiply(new THREE.Matrix4().fromArray(res.node.matrix));
         }
      }

      return res;
   }

   /** @summary Provide stack name
     * @desc Stack name includes full path to the physical node which is identified by stack  */
   getStackName(stack) {
      return this.resolveStack(stack).name;
   }

   /** @summary Create stack array based on nodes ids array.
    * @desc Ids list should correspond to existing nodes hierarchy */
   buildStackByIds(ids) {
      if (!ids)
         return null;

      if (ids[0]) {
         console.error('wrong ids - first should be 0');
         return null;
      }

      let node = this.nodes[0];
      const stack = [];

      for (let k = 1; k < ids.length; ++k) {
         const nodeid = ids[k];
         if (!node)
            return null;
         const chindx = node.chlds.indexOf(nodeid);
         if (chindx < 0) {
            console.error(`wrong nodes ids ${ids[k]} is not child of ${ids[k - 1]}`);
            return null;
         }

         stack.push(chindx);
         node = this.nodes[nodeid];
      }

      return stack;
   }

   /** @summary Returns ids array which correspond to the stack */
   buildIdsByStack(stack) {
      if (!stack)
         return null;
      let node = this.nodes[0];
      const ids = [0];
      for (let k = 0; k < stack.length; ++k) {
         const id = node.chlds[stack[k]];
         ids.push(id);
         node = this.nodes[id];
      }
      return ids;
   }

   /** @summary Returns node id by stack */
   getNodeIdByStack(stack) {
      if (!stack || !this.nodes)
         return -1;
      let node = this.nodes[0], id = 0;
      for (let k = 0; k < stack.length; ++k) {
         id = node.chlds[stack[k]];
         node = this.nodes[id];
      }
      return id;
   }

   /** @summary Returns true if stack includes at any place provided nodeid */
   isIdInStack(nodeid, stack) {
      if (!nodeid)
         return true;

      let node = this.nodes[0];

      for (let lvl = 0; lvl < stack.length; ++lvl) {
         const id = node.chlds[stack[lvl]];
         if (id === nodeid)
            return true;
         node = this.nodes[id];
      }

      return false;
   }

   /** @summary Find stack by name which include names of all parents */
   findStackByName(fullname) {
      const names = fullname.split('/'), stack = [];
      let currid = 0;

      if (this.getNodeName(currid) !== names[0])
         return null;

      for (let n = 1; n < names.length; ++n) {
         const node = this.nodes[currid];
         if (!node.chlds)
            return null;

         for (let k = 0; k < node.chlds.length; ++k) {
            const chldid = node.chlds[k];
            if (this.getNodeName(chldid) === names[n]) {
               stack.push(k);
               currid = chldid;
               break;
            }
         }

         // no new entry - not found stack
         if (stack.length === n - 1)
            return null;
      }

      return stack;
   }

   /** @summary Set usage of default ROOT colors */
   setDefaultColors(on) {
      this.use_dflt_colors = on;
      if (this.use_dflt_colors && !this.dflt_table) {
         const nmax = 110, col = [], dflt = { kGray: 920,
            kRed: 632, kGreen: 416, kBlue: 600, kYellow: 400, kMagenta: 616, kOrange: 800};
         for (let i = 0; i < nmax; i++)
            col.push(dflt.kGray);

         //  here we should create a new TColor with the same rgb as in the default
         //  ROOT colors used below
         col[3] = dflt.kYellow - 10;
         col[4] = col[5] = dflt.kGreen - 10;
         col[6] = col[7] = dflt.kBlue - 7;
         col[8] = col[9] = dflt.kMagenta - 3;
         col[10] = col[11] = dflt.kRed - 10;
         col[12] = dflt.kGray + 1;
         col[13] = dflt.kBlue - 10;
         col[14] = dflt.kOrange + 7;
         col[16] = dflt.kYellow + 1;
         col[20] = dflt.kYellow - 10;
         col[24] = col[25] = col[26] = dflt.kBlue - 8;
         col[29] = dflt.kOrange + 9;
         col[79] = dflt.kOrange - 2;

         this.dflt_table = col;
      }
   }

   /** @summary Provide different properties of draw entry nodeid
     * @desc Only if node visible, material will be created */
   getDrawEntryProperties(entry, root_colors) {
      const clone = this.nodes[entry.nodeid];

      if (clone.kind === kindShape) {
         const prop = { name: clone.name, nname: clone.name, shape: null, material: null, chlds: null },
               opacity = entry.opacity || 1, col = entry.color || '#0000FF';
         prop.fillcolor = new THREE.Color(col[0] === '#' ? col : `rgb(${col})`);
         prop.material = createMaterial(this._cfg, { opacity, color: prop.fillcolor });
         return prop;
      }

      if (!this.origin) {
         console.error(`origin not there - kind ${clone.kind} id ${entry.nodeid}`);
         return null;
      }

      const node = this.origin[entry.nodeid];

      if (clone.kind === kindEve) {
         // special handling for EVE nodes

         const prop = { name: getObjectName(node), nname: getObjectName(node), shape: node.fShape, material: null, chlds: null };

         if (node.fElements !== null)
            prop.chlds = node.fElements.arr;

         {
            const opacity = Math.min(1, node.fRGBA[3]);
            prop.fillcolor = new THREE.Color(node.fRGBA[0], node.fRGBA[1], node.fRGBA[2]);
            prop.material = createMaterial(this._cfg, { opacity, color: prop.fillcolor });
         }

         return prop;
      }

      const volume = node.fVolume, prop = {
         name: getObjectName(volume), nname: getObjectName(node), volume, shape: volume.fShape, material: null,
         chlds: volume.fNodes?.arr, linewidth: volume.fLineWidth
      };

      {
         let opacity = 1.0;
         if (!root_colors)
            root_colors = getRootColors();

         if (entry.custom_color)
            prop.fillcolor = entry.custom_color;
         else if ((volume.fFillColor > 1) && (volume.fLineColor === 1))
            prop.fillcolor = root_colors[volume.fFillColor];
         else if (volume.fLineColor >= 0)
            prop.fillcolor = root_colors[volume.fLineColor];

         const mat = volume.fMedium?.fMaterial;

         if (mat) {
            const fillstyle = mat.fFillStyle;
            let transparency = (fillstyle >= 3000 && fillstyle <= 3100) ? fillstyle - 3000 : 0;

            if (this.use_dflt_colors) {
               const matZ = Math.round(mat.fZ), icol = this.dflt_table[matZ];
               prop.fillcolor = root_colors[icol];
               if (mat.fDensity < 0.1)
                  transparency = 60;
            }

            if (transparency > 0)
               opacity = (100 - transparency) / 100;
            if (prop.fillcolor === undefined)
               prop.fillcolor = root_colors[mat.fFillColor];
         }
         if (prop.fillcolor === undefined)
            prop.fillcolor = 'lightgrey';

         prop.material = createMaterial(this._cfg, { opacity, color: prop.fillcolor });
      }

      return prop;
   }

   /** @summary Creates hierarchy of Object3D for given stack entry
     * @desc Such hierarchy repeats hierarchy of TGeoNodes and set matrix for the objects drawing
     * also set renderOrder, required to handle transparency */
   createObject3D(stack, toplevel, options) {
      let node = this.nodes[0], three_prnt = toplevel, draw_depth = 0;
      const force = isObject(options) || (options === 'force');

      for (let lvl = 0; lvl <= stack.length; ++lvl) {
         const nchld = (lvl > 0) ? stack[lvl - 1] : 0,
               // extract current node
               child = (lvl > 0) ? this.nodes[node.chlds[nchld]] : node;
         if (!child) {
            console.error(`Wrong stack ${JSON.stringify(stack)} for nodes at level ${lvl}, node.id ${node.id}, numnodes ${this.nodes.length}, nchld ${nchld}, numchilds ${node.chlds.length}, chldid ${node.chlds[nchld]}`);
            return null;
         }

         node = child;

         let obj3d;

         if (three_prnt.children) {
            for (let i = 0; i < three_prnt.children.length; ++i) {
               if (three_prnt.children[i].nchld === nchld) {
                  obj3d = three_prnt.children[i];
                  break;
               }
            }
         }

         if (obj3d) {
            three_prnt = obj3d;
            if (obj3d.$jsroot_drawable)
               draw_depth++;
            continue;
         }

         if (!force)
            return null;

         obj3d = new THREE.Object3D();

         if (this._cfg?.set_names)
            obj3d.name = this.getNodeName(node.id);

         if (this._cfg?.set_origin && this.origin)
            obj3d.userData = this.origin[node.id];

         if (node.abs_matrix) {
            obj3d.absMatrix = new THREE.Matrix4();
            obj3d.absMatrix.fromArray(node.matrix);
         } else if (node.matrix) {
            obj3d.matrix.fromArray(node.matrix);
            obj3d.matrix.decompose(obj3d.position, obj3d.quaternion, obj3d.scale);
         }

         // this.accountNodes(obj3d);
         obj3d.nchld = nchld; // mark index to find it again later

         // add the mesh to the scene
         three_prnt.add(obj3d);

         // this is only for debugging - test inversion of whole geometry
         if ((lvl === 0) && isObject(options) && options.scale) {
            if ((options.scale.x < 0) || (options.scale.y < 0) || (options.scale.z < 0)) {
               obj3d.scale.copy(options.scale);
               obj3d.updateMatrix();
            }
         }

         obj3d.updateMatrixWorld();

         three_prnt = obj3d;
      }

      if ((options === kGetMesh) || (options === kDeleteMesh)) {
         let mesh = null;
         if (three_prnt) {
            for (let n = 0; (n < three_prnt.children.length) && !mesh; ++n) {
               const chld = three_prnt.children[n];
               if ((chld.type === 'Mesh') && (chld.nchld === undefined))
                  mesh = chld;
            }
         }

         if ((options === kGetMesh) || !mesh)
            return mesh;

         const res = three_prnt;
         while (mesh && (mesh !== toplevel)) {
            three_prnt = mesh.parent;
            three_prnt.remove(mesh);
            mesh = !three_prnt.children.length ? three_prnt : null;
         }

         return res;
      }

      if (three_prnt) {
         three_prnt.$jsroot_drawable = true;
         three_prnt.$jsroot_depth = draw_depth;
      }

      return three_prnt;
   }

   /** @summary Create mesh for single physical node */
   createEntryMesh(ctrl, toplevel, entry, shape, colors) {
      if (!shape || !shape.ready)
         return null;

      entry.done = true; // mark entry is created
      shape.used = true; // indicate that shape was used in building

      if (!shape.geom || !shape.nfaces) {
         // node is visible, but shape does not created
         this.createObject3D(entry.stack, toplevel, kDeleteMesh);
         return null;
      }

      const prop = this.getDrawEntryProperties(entry, colors),
            obj3d = this.createObject3D(entry.stack, toplevel, ctrl),
            matrix = obj3d.absMatrix || obj3d.matrixWorld;

      prop.material.wireframe = ctrl.wireframe;

      prop.material.side = ctrl.doubleside ? THREE.DoubleSide : THREE.FrontSide;

      let mesh;
      if (matrix.determinant() > -0.9)
         mesh = new THREE.Mesh(shape.geom, prop.material);
      else
         mesh = createFlippedMesh(shape, prop.material);

      obj3d.add(mesh);

      if (obj3d.absMatrix) {
         mesh.matrix.copy(obj3d.absMatrix);
         mesh.matrix.decompose(mesh.position, mesh.quaternion, mesh.scale);
         mesh.updateMatrixWorld();
      }

      // keep full stack of nodes
      mesh.stack = entry.stack;
      mesh.renderOrder = this.maxdepth - entry.stack.length; // order of transparency handling

      if (ctrl.set_names)
         mesh.name = this.getNodeName(entry.nodeid);

      if (ctrl.set_origin)
         mesh.userData = prop.volume;

      // keep hierarchy level
      mesh.$jsroot_order = obj3d.$jsroot_depth;

      if (ctrl.info?.num_meshes !== undefined) {
         ctrl.info.num_meshes++;
         ctrl.info.num_faces += shape.nfaces;
      }

      // set initial render order, when camera moves, one must refine it
      // mesh.$jsroot_order = mesh.renderOrder =
      //   this._clones.maxdepth - ((obj3d.$jsroot_depth !== undefined) ? obj3d.$jsroot_depth : entry.stack.length);

      return mesh;
   }

   /** @summary Check if instancing can be used for the nodes */
   createInstancedMeshes(ctrl, toplevel, draw_nodes, build_shapes, colors) {
      if (ctrl.instancing < 0)
         return false;

      // first delete previous data
      const used_shapes = [];
      let max_entries = 1;

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

         // shape can be provided with entry itself
         const shape = entry.server_shape || build_shapes[entry.shapeid];
         if (!shape || !shape.ready) {
            console.warn(`Problem with shape id ${entry.shapeid} when building`);
            return false;
         }

         // ignore shape without geometry
         if (!shape.geom || !shape.nfaces)
            continue;

         if (shape.instances === undefined) {
            shape.instances = [];
            used_shapes.push(shape);
         }

         const instance = shape.instances.find(i => i.nodeid === entry.nodeid);

         if (instance) {
            instance.entries.push(entry);
            max_entries = Math.max(max_entries, instance.entries.length);
         } else
            shape.instances.push({ nodeid: entry.nodeid, entries: [entry] });
      }

      const make_sense = ctrl.instancing > 0 ? (max_entries > 2) : (draw_nodes.length > 10000) && (max_entries > 10);

      if (!make_sense) {
         used_shapes.forEach(shape => { delete shape.instances; });
         return false;
      }

      used_shapes.forEach(shape => {
         shape.used = true;
         shape.instances.forEach(instance => {
            const entry0 = instance.entries[0],
                  prop = this.getDrawEntryProperties(entry0, colors);

            prop.material.wireframe = ctrl.wireframe;

            prop.material.side = ctrl.doubleside ? THREE.DoubleSide : THREE.FrontSide;

            if (instance.entries.length === 1)
               this.createEntryMesh(ctrl, toplevel, entry0, shape, colors);
            else {
               const arr1 = [], arr2 = [], stacks1 = [], stacks2 = [], names1 = [], names2 = [];

               instance.entries.forEach(entry => {
                  const info = this.resolveStack(entry.stack, true);
                  if (info.matrix.determinant() > -0.9) {
                     arr1.push(info.matrix);
                     stacks1.push(entry.stack);
                     names1.push(this.getNodeName(entry.nodeid));
                  } else {
                     arr2.push(info.matrix);
                     stacks2.push(entry.stack);
                     names2.push(this.getNodeName(entry.nodeid));
                  }
                  entry.done = true;
               });

               if (arr1.length) {
                  const mesh1 = new THREE.InstancedMesh(shape.geom, prop.material, arr1.length);

                  mesh1.stacks = stacks1;
                  arr1.forEach((matrix, i) => mesh1.setMatrixAt(i, matrix));

                  toplevel.add(mesh1);

                  mesh1.renderOrder = 1;

                  if (ctrl.set_names) {
                     mesh1.name = names1[0];
                     mesh1.names = names1;
                  }

                  if (ctrl.set_origin)
                     mesh1.userData = prop.volume;

                  mesh1.$jsroot_order = 1;
                  ctrl.info.num_meshes++;
                  ctrl.info.num_faces += shape.nfaces * arr1.length;
               }

               if (arr2.length) {
                  if (shape.geomZ === undefined)
                     shape.geomZ = createFlippedGeom(shape.geom);

                  const mesh2 = new THREE.InstancedMesh(shape.geomZ, prop.material, arr2.length);

                  mesh2.stacks = stacks2;
                  const m = new THREE.Matrix4().makeScale(1, 1, -1);
                  arr2.forEach((matrix, i) => {
                     mesh2.setMatrixAt(i, matrix.multiply(m));
                  });
                  mesh2._flippedMesh = true;

                  toplevel.add(mesh2);

                  mesh2.renderOrder = 1;
                  if (ctrl.set_names) {
                     mesh2.name = names2[0];
                     mesh2.names = names2;
                  }
                  if (ctrl.set_origin)
                     mesh2.userData = prop.volume;

                  mesh2.$jsroot_order = 1;
                  ctrl.info.num_meshes++;
                  ctrl.info.num_faces += shape.nfaces * arr2.length;
               }
            }
         });

         delete shape.instances;
      });

      return true;
   }

   /** @summary Get volume boundary */
   getVolumeBoundary(viscnt, facelimit, nodeslimit) {
      const result = { min: 0, max: 1, sortidcut: 0 };

      if (!this.sortmap) {
         console.error('sorting map do not exist');
         return result;
      }

      let maxNode, currNode, cnt = 0, facecnt = 0;

      for (let n = 0; (n < this.sortmap.length) && (cnt < nodeslimit) && (facecnt < facelimit); ++n) {
         const id = this.sortmap[n];
         if (viscnt[id] === 0)
            continue;
         currNode = this.nodes[id];
         if (!maxNode)
            maxNode = currNode;
         cnt += viscnt[id];
         facecnt += viscnt[id] * currNode.nfaces;
      }

      if (!currNode) {
         console.error('no volumes selected');
         return result;
      }

      result.max = maxNode.vol;
      result.min = currNode.vol;
      result.sortidcut = currNode.sortid; // latest node is not included
      return result;
   }

   /** @summary Collects visible nodes, using maxlimit
     * @desc One can use map to define cut based on the volume or serious of cuts */
   collectVisibles(maxnumfaces, frustum) {
      // in simple case shape as it is
      if (this.plain_shape)
         return { lst: [{ nodeid: 0, seqid: 0, stack: [], factor: 1, shapeid: 0, server_shape: this.plain_shape }], complete: true };

      const arg = {
         facecnt: 0,
         viscnt: new Array(this.nodes.length), // counter for each node
         vislvl: this.getVisLevel(),
         reset() {
            this.total = 0;
            this.facecnt = 0;
            this.viscnt.fill(0);
         },
         func(node) {
            this.total++;
            this.facecnt += node.nfaces;
            this.viscnt[node.id]++;
            return true;
         }
      };

      arg.reset();

      let total = this.scanVisible(arg);
      if ((total === 0) && (this.nodes[0].vis < 2) && !this.nodes[0].nochlds) {
         // try to draw only main node by default
         arg.reset();
         arg.main_visible = true;
         total = this.scanVisible(arg);
      }

      const maxnumnodes = this.getMaxVisNodes();

      if (maxnumnodes > 0) {
         while ((total > maxnumnodes) && (arg.vislvl > 1)) {
            arg.vislvl--;
            arg.reset();
            total = this.scanVisible(arg);
         }
      }

      this.actual_level = arg.vislvl; // not used, can be shown somewhere in the gui

      let minVol = 0, maxVol, camVol = -1, camFact = 10, sortidcut = this.nodes.length + 1;

      if (arg.facecnt > maxnumfaces) {
         const bignumfaces = maxnumfaces * (frustum ? 0.8 : 1.0),
               bignumnodes = maxnumnodes * (frustum ? 0.8 : 1.0),
               // define minimal volume, which always to shown
               boundary = this.getVolumeBoundary(arg.viscnt, bignumfaces, bignumnodes);

         minVol = boundary.min;
         maxVol = boundary.max;
         sortidcut = boundary.sortidcut;

         if (frustum) {
            arg.domatrix = true;
            arg.frustum = frustum;
            arg.totalcam = 0;
            arg.func = function(node) {
               if (node.vol <= minVol) {
                  // only small volumes are interesting
                  if (this.frustum.CheckShape(this.getmatrix(), node)) {
                     this.viscnt[node.id]++;
                     this.totalcam += node.nfaces;
                  }
               }

               return true;
            };

            for (let n = 0; n < arg.viscnt.length; ++n)
               arg.viscnt[n] = 0;

            this.scanVisible(arg);

            if (arg.totalcam > maxnumfaces * 0.2)
               camVol = this.getVolumeBoundary(arg.viscnt, maxnumfaces * 0.2, maxnumnodes * 0.2).min;
            else
               camVol = 0;

            camFact = maxVol / ((camVol > 0) ? (camVol > 0) : minVol);
         }
      }

      arg.items = [];

      arg.func = function(node) {
         if (node.sortid < sortidcut)
            this.items.push(this.copyStack());
         else if ((camVol >= 0) && (node.vol > camVol)) {
            if (this.frustum.CheckShape(this.getmatrix(), node))
               this.items.push(this.copyStack(camFact));
         }
         return true;
      };

      this.scanVisible(arg);

      return { lst: arg.items, complete: minVol === 0 };
   }

   /** @summary Merge list of drawn objects
     * @desc In current list we should mark if object already exists
     * from previous list we should collect objects which are not there */
   mergeVisibles(current, prev) {
      let indx2 = 0;
      const del = [];
      for (let indx1 = 0; (indx1 < current.length) && (indx2 < prev.length); ++indx1) {
         while ((indx2 < prev.length) && (prev[indx2].seqid < current[indx1].seqid))
            del.push(prev[indx2++]); // this entry should be removed


         if ((indx2 < prev.length) && (prev[indx2].seqid === current[indx1].seqid)) {
            if (prev[indx2].done)
               current[indx1].done = true; // copy ready flag
            indx2++;
         }
      }

      // remove rest
      while (indx2 < prev.length)
         del.push(prev[indx2++]);

      return del;
   }

   /** @summary Collect all uniques shapes which should be built
    *  @desc Check if same shape used many times for drawing */
   collectShapes(lst) {
      // nothing else - just that single shape
      if (this.plain_shape)
         return [this.plain_shape];

      const shapes = [];

      for (let i = 0; i < lst.length; ++i) {
         const entry = lst[i],
               shape = this.getNodeShape(entry.nodeid);

         if (!shape)
            continue; // strange, but avoid misleading

         if (shape._id === undefined) {
            shape._id = shapes.length;

            shapes.push({ id: shape._id, shape, vol: this.nodes[entry.nodeid].vol, refcnt: 1, factor: 1, ready: false });

            // shapes.push( { obj: shape, vol: this.nodes[entry.nodeid].vol });
         } else
            shapes[shape._id].refcnt++;


         entry.shape = shapes[shape._id]; // remember shape used

         // use maximal importance factor to push element to the front
         if (entry.factor && (entry.factor > entry.shape.factor))
            entry.shape.factor = entry.factor;
      }

      // now sort shapes in volume decrease order
      shapes.sort((a, b) => b.vol * b.factor - a.vol * a.factor);

      // now set new shape ids according to the sorted order and delete temporary field
      for (let n = 0; n < shapes.length; ++n) {
         const item = shapes[n];
         item.id = n; // set new ID
         delete item.shape._id; // remove temporary field
      }

      // as last action set current shape id to each entry
      for (let i = 0; i < lst.length; ++i) {
         const entry = lst[i];
         if (entry.shape) {
            entry.shapeid = entry.shape.id; // keep only id for the entry
            delete entry.shape; // remove direct references
         }
      }

      return shapes;
   }

   /** @summary Merge shape lists */
   mergeShapesLists(oldlst, newlst) {
      if (!oldlst)
         return newlst;

      // set geometry to shape object itself
      for (let n = 0; n < oldlst.length; ++n) {
         const item = oldlst[n];

         item.shape._geom = item.geom;
         delete item.geom;

         if (item.geomZ !== undefined) {
            item.shape._geomZ = item.geomZ;
            delete item.geomZ;
         }
      }

      // take from shape (if match)
      for (let n = 0; n < newlst.length; ++n) {
         const item = newlst[n];

         if (item.shape._geom !== undefined) {
            item.geom = item.shape._geom;
            delete item.shape._geom;
         }

         if (item.shape._geomZ !== undefined) {
            item.geomZ = item.shape._geomZ;
            delete item.shape._geomZ;
         }
      }

      // now delete all unused geometries
      for (let n = 0; n < oldlst.length; ++n) {
         const item = oldlst[n];
         delete item.shape._geom;
         delete item.shape._geomZ;
      }

      return newlst;
   }

   /** @summary Build shapes */
   buildShapes(lst, limit, timelimit) {
      let created = 0;
      const tm1 = new Date().getTime(),
            res = { done: false, shapes: 0, faces: 0, notusedshapes: 0 };

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

         // if enough faces are produced, nothing else is required
         if (res.done) {
            item.ready = true;
            continue;
         }

         if (!item.ready) {
            item._typename = kShapeType; // let reuse item for direct drawing
            item.ready = true;
            if (item.geom === undefined) {
               item.geom = createGeometry(item.shape);
               if (item.geom)
                  created++; // indicate that at least one shape was created
            }
            item.nfaces = numGeometryFaces(item.geom);
         }

         res.shapes++;
         if (!item.used)
            res.notusedshapes++;
         res.faces += item.nfaces * item.refcnt;

         if (res.faces >= limit)
            res.done = true;
         else if ((created > 0.01 * lst.length) && (timelimit !== undefined)) {
            const tm2 = new Date().getTime();
            if (tm2 - tm1 > timelimit)
               return res;
         }
      }

      res.done = true;

      return res;
   }

   /** @summary Format REveGeomNode data to be able use it in list of clones
     * @private */
   static formatServerElement(elem) {
      elem.kind = 2; // special element for geom viewer, used in TGeoPainter
      elem.vis = 2; // visibility is always on
      const m = elem.matr;
      delete elem.matr;
      if (!m?.length)
         return elem;

      if (m.length === 16)
         elem.matrix = m;
      else {
         const nm = elem.matrix = new Array(16);
         nm.fill(0);
         nm[0] = nm[5] = nm[10] = nm[15] = 1;

         if (m.length === 3) {
            // translation matrix
            nm[12] = m[0];
            nm[13] = m[1];
            nm[14] = m[2];
         } else if (m.length === 4) {
            // scale matrix
            nm[0] = m[0];
            nm[5] = m[1];
            nm[10] = m[2];
            nm[15] = m[3];
         } else if (m.length === 9) {
            // rotation matrix
            nm[0] = m[0];
            nm[4] = m[1];
            nm[8] = m[2];
            nm[1] = m[3];
            nm[5] = m[4];
            nm[9] = m[5];
            nm[2] = m[6];
            nm[6] = m[7];
            nm[10] = m[8];
         } else
            console.error(`wrong number of elements ${m.length} in the matrix`);
      }
      return elem;
   }

} // class ClonedNodes

/** @summary extract code of Box3.expandByObject
  * @desc Major difference - do not traverse hierarchy, support InstancedMesh
  * @private */
function getBoundingBox(node, box3, local_coordinates) {
   if (!node?.geometry)
      return box3;

   if (!box3)
      box3 = new THREE.Box3().makeEmpty();

   if (node.isInstancedMesh) {
      const m = new THREE.Matrix4(), b = new THREE.Box3().makeEmpty();

      node.geometry.computeBoundingBox();

      for (let i = 0; i < node.count; i++) {
         node.getMatrixAt(i, m);
         b.copy(node.geometry.boundingBox).applyMatrix4(m);
         box3.union(b);
      }
      return box3;
   }

   if (!local_coordinates)
      node.updateWorldMatrix(false, false);

   const v1 = new THREE.Vector3(), attribute = node.geometry.attributes?.position;

   if (attribute !== undefined) {
      for (let i = 0, l = attribute.count; i < l; i++) {
         // v1.fromAttribute( attribute, i ).applyMatrix4( node.matrixWorld );
         v1.fromBufferAttribute(attribute, i);
         if (!local_coordinates)
            v1.applyMatrix4(node.matrixWorld);
         box3.expandByPoint(v1);
      }
   }

   return box3;
}

/** @summary Cleanup shape entity
  * @private */
function cleanupShape(shape) {
   if (!shape)
      return;

   if (isFunc(shape.geom?.dispose))
      shape.geom.dispose();

   if (isFunc(shape.geomZ?.dispose))
      shape.geomZ.dispose();

   delete shape.geom;
   delete shape.geomZ;
}

/** @summary Set rendering order for created hierarchy
  * @desc depending from provided method sort differently objects
  * @param toplevel - top element
  * @param origin - camera position used to provide sorting
  * @param method - name of sorting method like 'pnt', 'ray', 'size', 'dflt'  */
function produceRenderOrder(toplevel, origin, method, clones) {
   const raycast = new THREE.Raycaster();

   function setdefaults(top) {
      top?.traverse(obj => {
         obj.renderOrder = obj.defaultOrder || 0;
         if (obj.material)
            obj.material.depthWrite = true; // by default depthWriting enabled
      });
   }

   function traverse(obj, lvl, arr) {
      // traverse hierarchy and extract all children of given level
      // if (obj.$jsroot_depth === undefined)
      //    return;

      if (!obj.children)
         return;

      for (let k = 0; k < obj.children.length; ++k) {
         const chld = obj.children[k];
         if (chld.$jsroot_order === lvl) {
            if (chld.material) {
               if (chld.material.transparent) {
                  chld.material.depthWrite = false; // disable depth writing for transparent
                  arr.push(chld);
               } else
                  setdefaults(chld);
            }
         } else if ((obj.$jsroot_depth === undefined) || (obj.$jsroot_depth < lvl))
            traverse(chld, lvl, arr);
      }
   }

   function sort(arr, minorder, maxorder) {
      // resort meshes using ray caster and camera position
      // idea to identify meshes which are in front or behind

      if (arr.length > 1000) {
         // too many of them, just set basic level and exit
         for (let i = 0; i < arr.length; ++i)
            arr[i].renderOrder = (minorder + maxorder) / 2;
         return false;
      }

      const tmp_vect = new THREE.Vector3();

      // first calculate distance to the camera
      // it gives preliminary order of volumes
      for (let i = 0; i < arr.length; ++i) {
         const mesh = arr[i];
         let box3 = mesh.$jsroot_box3;

         if (!box3)
            mesh.$jsroot_box3 = box3 = getBoundingBox(mesh);

         if (method === 'size') {
            const sz = box3.getSize(new THREE.Vector3());
            mesh.$jsroot_distance = sz.x * sz.y * sz.z;
            continue;
         }

         if (method === 'pnt') {
            mesh.$jsroot_distance = origin.distanceTo(box3.getCenter(tmp_vect));
            continue;
         }

         let dist = Math.min(origin.distanceTo(box3.min), origin.distanceTo(box3.max));
         const pnt = new THREE.Vector3(box3.min.x, box3.min.y, box3.max.z);

         dist = Math.min(dist, origin.distanceTo(pnt));
         pnt.set(box3.min.x, box3.max.y, box3.min.z);
         dist = Math.min(dist, origin.distanceTo(pnt));
         pnt.set(box3.max.x, box3.min.y, box3.min.z);
         dist = Math.min(dist, origin.distanceTo(pnt));
         pnt.set(box3.max.x, box3.max.y, box3.min.z);
         dist = Math.min(dist, origin.distanceTo(pnt));
         pnt.set(box3.max.x, box3.min.y, box3.max.z);
         dist = Math.min(dist, origin.distanceTo(pnt));
         pnt.set(box3.min.x, box3.max.y, box3.max.z);
         dist = Math.min(dist, origin.distanceTo(pnt));

         mesh.$jsroot_distance = dist;
      }

      arr.sort((a, b) => a.$jsroot_distance - b.$jsroot_distance);

      const resort = new Array(arr.length);

      for (let i = 0; i < arr.length; ++i) {
         arr[i].$jsroot_index = i;
         resort[i] = arr[i];
      }

      if (method === 'ray') {
         for (let i = arr.length - 1; i >= 0; --i) {
            const mesh = arr[i], box3 = mesh.$jsroot_box3;
            let intersects, direction = box3.getCenter(tmp_vect);

            for (let ntry = 0; ntry < 2; ++ntry) {
               direction.sub(origin).normalize();

               raycast.set(origin, direction);

               intersects = raycast.intersectObjects(arr, false) || []; // only plain array
               const unique = [];

               for (let k1 = 0; k1 < intersects.length; ++k1) {
                  if (unique.indexOf(intersects[k1].object) < 0)
                     unique.push(intersects[k1].object);
               }

               intersects = unique;

               if ((intersects.indexOf(mesh) < 0) && (ntry > 0))
                  console.log(`MISS ${clones?.resolveStack(mesh.stack)?.name}`);

               if ((intersects.indexOf(mesh) >= 0) || (ntry > 0))
                  break;

               const pos = mesh.geometry.attributes.position.array;

               direction = new THREE.Vector3((pos[0] + pos[3] + pos[6]) / 3, (pos[1] + pos[4] + pos[7]) / 3, (pos[2] + pos[5] + pos[8]) / 3);

               direction.applyMatrix4(mesh.matrixWorld);
            }

            // now push first object in intersects to the front
            for (let k1 = 0; k1 < intersects.length - 1; ++k1) {
               const mesh1 = intersects[k1], mesh2 = intersects[k1 + 1],
                     i1 = mesh1.$jsroot_index, i2 = mesh2.$jsroot_index;
               if (i1 < i2)
                  continue;
               for (let ii = i2; ii < i1; ++ii) {
                  resort[ii] = resort[ii + 1];
                  resort[ii].$jsroot_index = ii;
               }
               resort[i1] = mesh2;
               mesh2.$jsroot_index = i1;
            }
         }
      }

      for (let i = 0; i < resort.length; ++i) {
         resort[i].renderOrder = Math.round(maxorder - (i + 1) / (resort.length + 1) * (maxorder - minorder));
         delete resort[i].$jsroot_index;
         delete resort[i].$jsroot_distance;
      }

      return true;
   }

   function process(obj, lvl, minorder, maxorder) {
      const arr = [];
      let did_sort = false;

      traverse(obj, lvl, arr);

      if (!arr.length)
         return;

      if (minorder === maxorder) {
         for (let k = 0; k < arr.length; ++k)
            arr[k].renderOrder = minorder;
      } else {
         did_sort = sort(arr, minorder, maxorder);
         if (!did_sort)
            minorder = maxorder = (minorder + maxorder) / 2;
      }

      for (let k = 0; k < arr.length; ++k) {
         const next = arr[k].parent;
         let min = minorder, max = maxorder;

         if (did_sort) {
            max = arr[k].renderOrder;
            min = max - (maxorder - minorder) / (arr.length + 2);
         }

         process(next, lvl + 1, min, max);
      }
   }

   if (!method || (method === 'dflt'))
      setdefaults(toplevel);
   else
      process(toplevel, 0, 1, 1000000);
}

/** @summary provide icon name for the shape
  * @private */
function getShapeIcon(shape) {
   switch (shape._typename) {
      case clTGeoArb8: return 'img_geoarb8';
      case clTGeoCone: return 'img_geocone';
      case clTGeoConeSeg: return 'img_geoconeseg';
      case clTGeoCompositeShape: return 'img_geocomposite';
      case clTGeoTube: return 'img_geotube';
      case clTGeoTubeSeg: return 'img_geotubeseg';
      case clTGeoPara: return 'img_geopara';
      case clTGeoParaboloid: return 'img_geoparab';
      case clTGeoPcon: return 'img_geopcon';
      case clTGeoPgon: return 'img_geopgon';
      case clTGeoShapeAssembly: return 'img_geoassembly';
      case clTGeoSphere: return 'img_geosphere';
      case clTGeoTorus: return 'img_geotorus';
      case clTGeoTrd1: return 'img_geotrd1';
      case clTGeoTrd2: return 'img_geotrd2';
      case clTGeoXtru: return 'img_geoxtru';
      case clTGeoTrap: return 'img_geotrap';
      case clTGeoGtra: return 'img_geogtra';
      case clTGeoEltu: return 'img_geoeltu';
      case clTGeoHype: return 'img_geohype';
      case clTGeoCtub: return 'img_geoctub';
   }
   return 'img_geotube';
}

/**
 * lil-gui
 * https://lil-gui.georgealways.com
 * @version 0.20.0
 * @author George Michael Brower
 * @license MIT
 */

/**
 * Base class for all controllers.
 */
class Controller {

	constructor( parent, object, property, className, elementType = 'div' ) {

		/**
		 * The GUI that contains this controller.
		 * @type {GUI}
		 */
		this.parent = parent;

		/**
		 * The object this controller will modify.
		 * @type {object}
		 */
		this.object = object;

		/**
		 * The name of the property to control.
		 * @type {string}
		 */
		this.property = property;

		/**
		 * Used to determine if the controller is disabled.
		 * Use `controller.disable( true|false )` to modify this value.
		 * @type {boolean}
		 */
		this._disabled = false;

		/**
		 * Used to determine if the Controller is hidden.
		 * Use `controller.show()` or `controller.hide()` to change this.
		 * @type {boolean}
		 */
		this._hidden = false;

		/**
		 * The value of `object[ property ]` when the controller was created.
		 * @type {any}
		 */
		this.initialValue = this.getValue();

		/**
		 * The outermost container DOM element for this controller.
		 * @type {HTMLElement}
		 */
		this.domElement = document.createElement( elementType );
		this.domElement.classList.add( 'controller' );
		this.domElement.classList.add( className );

		/**
		 * The DOM element that contains the controller's name.
		 * @type {HTMLElement}
		 */
		this.$name = document.createElement( 'div' );
		this.$name.classList.add( 'name' );

		Controller.nextNameID = Controller.nextNameID || 0;
		this.$name.id = `lil-gui-name-${++Controller.nextNameID}`;

		/**
		 * The DOM element that contains the controller's "widget" (which differs by controller type).
		 * @type {HTMLElement}
		 */
		this.$widget = document.createElement( 'div' );
		this.$widget.classList.add( 'widget' );

		/**
		 * The DOM element that receives the disabled attribute when using disable().
		 * @type {HTMLElement}
		 */
		this.$disable = this.$widget;

		this.domElement.appendChild( this.$name );
		this.domElement.appendChild( this.$widget );

		// Don't fire global key events while typing in a controller
		this.domElement.addEventListener( 'keydown', e => e.stopPropagation() );
		this.domElement.addEventListener( 'keyup', e => e.stopPropagation() );

		this.parent.children.push( this );
		this.parent.controllers.push( this );

		this.parent.$children.appendChild( this.domElement );

		this._listenCallback = this._listenCallback.bind( this );

		this.name( property );

	}

	/**
	 * Sets the name of the controller and its label in the GUI.
	 * @param {string} name
	 * @returns {this}
	 */
	name( name ) {
		/**
		 * The controller's name. Use `controller.name( 'Name' )` to modify this value.
		 * @type {string}
		 */
		this._name = name;
		this.$name.textContent = name;
		return this;
	}

	/**
	 * Pass a function to be called whenever the value is modified by this controller.
	 * The function receives the new value as its first parameter. The value of `this` will be the
	 * controller.
	 *
	 * For function controllers, the `onChange` callback will be fired on click, after the function
	 * executes.
	 * @param {Function} callback
	 * @returns {this}
	 * @example
	 * const controller = gui.add( object, 'property' );
	 *
	 * controller.onChange( function( v ) {
	 * 	console.log( 'The value is now ' + v );
	 * 	console.assert( this === controller );
	 * } );
	 */
	onChange( callback ) {
		/**
		 * Used to access the function bound to `onChange` events. Don't modify this value directly.
		 * Use the `controller.onChange( callback )` method instead.
		 * @type {Function}
		 */
		this._onChange = callback;
		return this;
	}

	/**
	 * Calls the onChange methods of this controller and its parent GUI.
	 * @protected
	 */
	_callOnChange() {

		this.parent._callOnChange( this );

		if ( this._onChange !== undefined ) {
			this._onChange.call( this, this.getValue() );
		}

		this._changed = true;

	}

	/**
	 * Pass a function to be called after this controller has been modified and loses focus.
	 * @param {Function} callback
	 * @returns {this}
	 * @example
	 * const controller = gui.add( object, 'property' );
	 *
	 * controller.onFinishChange( function( v ) {
	 * 	console.log( 'Changes complete: ' + v );
	 * 	console.assert( this === controller );
	 * } );
	 */
	onFinishChange( callback ) {
		/**
		 * Used to access the function bound to `onFinishChange` events. Don't modify this value
		 * directly. Use the `controller.onFinishChange( callback )` method instead.
		 * @type {Function}
		 */
		this._onFinishChange = callback;
		return this;
	}

	/**
	 * Should be called by Controller when its widgets lose focus.
	 * @protected
	 */
	_callOnFinishChange() {

		if ( this._changed ) {

			this.parent._callOnFinishChange( this );

			if ( this._onFinishChange !== undefined ) {
				this._onFinishChange.call( this, this.getValue() );
			}

		}

		this._changed = false;

	}

	/**
	 * Sets the controller back to its initial value.
	 * @returns {this}
	 */
	reset() {
		this.setValue( this.initialValue );
		this._callOnFinishChange();
		return this;
	}

	/**
	 * Enables this controller.
	 * @param {boolean} enabled
	 * @returns {this}
	 * @example
	 * controller.enable();
	 * controller.enable( false ); // disable
	 * controller.enable( controller._disabled ); // toggle
	 */
	enable( enabled = true ) {
		return this.disable( !enabled );
	}

	/**
	 * Disables this controller.
	 * @param {boolean} disabled
	 * @returns {this}
	 * @example
	 * controller.disable();
	 * controller.disable( false ); // enable
	 * controller.disable( !controller._disabled ); // toggle
	 */
	disable( disabled = true ) {

		if ( disabled === this._disabled ) return this;

		this._disabled = disabled;

		this.domElement.classList.toggle( 'disabled', disabled );
		this.$disable.toggleAttribute( 'disabled', disabled );

		return this;

	}

	/**
	 * Shows the Controller after it's been hidden.
	 * @param {boolean} show
	 * @returns {this}
	 * @example
	 * controller.show();
	 * controller.show( false ); // hide
	 * controller.show( controller._hidden ); // toggle
	 */
	show( show = true ) {

		this._hidden = !show;

		this.domElement.style.display = this._hidden ? 'none' : '';

		return this;

	}

	/**
	 * Hides the Controller.
	 * @returns {this}
	 */
	hide() {
		return this.show( false );
	}

	/**
	 * Changes this controller into a dropdown of options.
	 *
	 * Calling this method on an option controller will simply update the options. However, if this
	 * controller was not already an option controller, old references to this controller are
	 * destroyed, and a new controller is added to the end of the GUI.
	 * @example
	 * // safe usage
	 *
	 * gui.add( obj, 'prop1' ).options( [ 'a', 'b', 'c' ] );
	 * gui.add( obj, 'prop2' ).options( { Big: 10, Small: 1 } );
	 * gui.add( obj, 'prop3' );
	 *
	 * // danger
	 *
	 * const ctrl1 = gui.add( obj, 'prop1' );
	 * gui.add( obj, 'prop2' );
	 *
	 * // calling options out of order adds a new controller to the end...
	 * const ctrl2 = ctrl1.options( [ 'a', 'b', 'c' ] );
	 *
	 * // ...and ctrl1 now references a controller that doesn't exist
	 * assert( ctrl2 !== ctrl1 )
	 * @param {object|Array} options
	 * @returns {Controller}
	 */
	options( options ) {
		const controller = this.parent.add( this.object, this.property, options );
		controller.name( this._name );
		this.destroy();
		return controller;
	}

	/**
	 * Sets the minimum value. Only works on number controllers.
	 * @param {number} min
	 * @returns {this}
	 */
	min( min ) {
		return this;
	}

	/**
	 * Sets the maximum value. Only works on number controllers.
	 * @param {number} max
	 * @returns {this}
	 */
	max( max ) {
		return this;
	}

	/**
	 * Values set by this controller will be rounded to multiples of `step`. Only works on number
	 * controllers.
	 * @param {number} step
	 * @returns {this}
	 */
	step( step ) {
		return this;
	}

	/**
	 * Rounds the displayed value to a fixed number of decimals, without affecting the actual value
	 * like `step()`. Only works on number controllers.
	 * @example
	 * gui.add( object, 'property' ).listen().decimals( 4 );
	 * @param {number} decimals
	 * @returns {this}
	 */
	decimals( decimals ) {
		return this;
	}

	/**
	 * Calls `updateDisplay()` every animation frame. Pass `false` to stop listening.
	 * @param {boolean} listen
	 * @returns {this}
	 */
	listen( listen = true ) {

		/**
		 * Used to determine if the controller is currently listening. Don't modify this value
		 * directly. Use the `controller.listen( true|false )` method instead.
		 * @type {boolean}
		 */
		this._listening = listen;

		if ( this._listenCallbackID !== undefined ) {
			cancelAnimationFrame( this._listenCallbackID );
			this._listenCallbackID = undefined;
		}

		if ( this._listening ) {
			this._listenCallback();
		}

		return this;

	}

	_listenCallback() {

		this._listenCallbackID = requestAnimationFrame( this._listenCallback );

		// To prevent framerate loss, make sure the value has changed before updating the display.
		// Note: save() is used here instead of getValue() only because of ColorController. The !== operator
		// won't work for color objects or arrays, but ColorController.save() always returns a string.

		const curValue = this.save();

		if ( curValue !== this._listenPrevValue ) {
			this.updateDisplay();
		}

		this._listenPrevValue = curValue;

	}

	/**
	 * Returns `object[ property ]`.
	 * @returns {any}
	 */
	getValue() {
		return this.object[ this.property ];
	}

	/**
	 * Sets the value of `object[ property ]`, invokes any `onChange` handlers and updates the display.
	 * @param {any} value
	 * @returns {this}
	 */
	setValue( value ) {

		if ( this.getValue() !== value ) {

			this.object[ this.property ] = value;
			this._callOnChange();
			this.updateDisplay();

		}

		return this;

	}

	/**
	 * Updates the display to keep it in sync with the current value. Useful for updating your
	 * controllers when their values have been modified outside of the GUI.
	 * @returns {this}
	 */
	updateDisplay() {
		return this;
	}

	load( value ) {
		this.setValue( value );
		this._callOnFinishChange();
		return this;
	}

	save() {
		return this.getValue();
	}

	/**
	 * Destroys this controller and removes it from the parent GUI.
	 */
	destroy() {
		this.listen( false );
		this.parent.children.splice( this.parent.children.indexOf( this ), 1 );
		this.parent.controllers.splice( this.parent.controllers.indexOf( this ), 1 );
		this.parent.$children.removeChild( this.domElement );
	}

}

class BooleanController extends Controller {

	constructor( parent, object, property ) {

		super( parent, object, property, 'boolean', 'label' );

		this.$input = document.createElement( 'input' );
		this.$input.setAttribute( 'type', 'checkbox' );
		this.$input.setAttribute( 'aria-labelledby', this.$name.id );

		this.$widget.appendChild( this.$input );

		this.$input.addEventListener( 'change', () => {
			this.setValue( this.$input.checked );
			this._callOnFinishChange();
		} );

		this.$disable = this.$input;

		this.updateDisplay();

	}

	updateDisplay() {
		this.$input.checked = this.getValue();
		return this;
	}

}

function normalizeColorString( string ) {

	let match, result;

	if ( match = string.match( /(#|0x)?([a-f0-9]{6})/i ) ) {

		result = match[ 2 ];

	} else if ( match = string.match( /rgb\(\s*(\d*)\s*,\s*(\d*)\s*,\s*(\d*)\s*\)/ ) ) {

		result = parseInt( match[ 1 ] ).toString( 16 ).padStart( 2, 0 )
			+ parseInt( match[ 2 ] ).toString( 16 ).padStart( 2, 0 )
			+ parseInt( match[ 3 ] ).toString( 16 ).padStart( 2, 0 );

	} else if ( match = string.match( /^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i ) ) {

		result = match[ 1 ] + match[ 1 ] + match[ 2 ] + match[ 2 ] + match[ 3 ] + match[ 3 ];

	}

	if ( result ) {
		return '#' + result;
	}

	return false;

}

const STRING = {
	isPrimitive: true,
	match: v => typeof v === 'string',
	fromHexString: normalizeColorString,
	toHexString: normalizeColorString
};

const INT = {
	isPrimitive: true,
	match: v => typeof v === 'number',
	fromHexString: string => parseInt( string.substring( 1 ), 16 ),
	toHexString: value => '#' + value.toString( 16 ).padStart( 6, 0 )
};

const ARRAY = {
	isPrimitive: false,

	// The arrow function is here to appease tree shakers like esbuild or webpack.
	// See https://esbuild.github.io/api/#tree-shaking
	match: v => Array.isArray( v ),

	fromHexString( string, target, rgbScale = 1 ) {

		const int = INT.fromHexString( string );

		target[ 0 ] = ( int >> 16 & 255 ) / 255 * rgbScale;
		target[ 1 ] = ( int >> 8 & 255 ) / 255 * rgbScale;
		target[ 2 ] = ( int & 255 ) / 255 * rgbScale;

	},
	toHexString( [ r, g, b ], rgbScale = 1 ) {

		rgbScale = 255 / rgbScale;

		const int = ( r * rgbScale ) << 16 ^
			( g * rgbScale ) << 8 ^
			( b * rgbScale ) << 0;

		return INT.toHexString( int );

	}
};

const OBJECT = {
	isPrimitive: false,
	match: v => Object( v ) === v,
	fromHexString( string, target, rgbScale = 1 ) {

		const int = INT.fromHexString( string );

		target.r = ( int >> 16 & 255 ) / 255 * rgbScale;
		target.g = ( int >> 8 & 255 ) / 255 * rgbScale;
		target.b = ( int & 255 ) / 255 * rgbScale;

	},
	toHexString( { r, g, b }, rgbScale = 1 ) {

		rgbScale = 255 / rgbScale;

		const int = ( r * rgbScale ) << 16 ^
			( g * rgbScale ) << 8 ^
			( b * rgbScale ) << 0;

		return INT.toHexString( int );

	}
};

const FORMATS = [ STRING, INT, ARRAY, OBJECT ];

function getColorFormat( value ) {
	return FORMATS.find( format => format.match( value ) );
}

class ColorController extends Controller {

	constructor( parent, object, property, rgbScale ) {

		super( parent, object, property, 'color' );

		this.$input = document.createElement( 'input' );
		this.$input.setAttribute( 'type', 'color' );
		this.$input.setAttribute( 'tabindex', -1 );
		this.$input.setAttribute( 'aria-labelledby', this.$name.id );

		this.$text = document.createElement( 'input' );
		this.$text.setAttribute( 'type', 'text' );
		this.$text.setAttribute( 'spellcheck', 'false' );
		this.$text.setAttribute( 'aria-labelledby', this.$name.id );

		this.$display = document.createElement( 'div' );
		this.$display.classList.add( 'display' );

		this.$display.appendChild( this.$input );
		this.$widget.appendChild( this.$display );
		this.$widget.appendChild( this.$text );

		this._format = getColorFormat( this.initialValue );
		this._rgbScale = rgbScale;

		this._initialValueHexString = this.save();
		this._textFocused = false;

		this.$input.addEventListener( 'input', () => {
			this._setValueFromHexString( this.$input.value );
		} );

		this.$input.addEventListener( 'blur', () => {
			this._callOnFinishChange();
		} );

		this.$text.addEventListener( 'input', () => {
			const tryParse = normalizeColorString( this.$text.value );
			if ( tryParse ) {
				this._setValueFromHexString( tryParse );
			}
		} );

		this.$text.addEventListener( 'focus', () => {
			this._textFocused = true;
			this.$text.select();
		} );

		this.$text.addEventListener( 'blur', () => {
			this._textFocused = false;
			this.updateDisplay();
			this._callOnFinishChange();
		} );

		this.$disable = this.$text;

		this.updateDisplay();

	}

	reset() {
		this._setValueFromHexString( this._initialValueHexString );
		return this;
	}

	_setValueFromHexString( value ) {

		if ( this._format.isPrimitive ) {

			const newValue = this._format.fromHexString( value );
			this.setValue( newValue );

		} else {

			this._format.fromHexString( value, this.getValue(), this._rgbScale );
			this._callOnChange();
			this.updateDisplay();

		}

	}

	save() {
		return this._format.toHexString( this.getValue(), this._rgbScale );
	}

	load( value ) {
		this._setValueFromHexString( value );
		this._callOnFinishChange();
		return this;
	}

	updateDisplay() {
		this.$input.value = this._format.toHexString( this.getValue(), this._rgbScale );
		if ( !this._textFocused ) {
			this.$text.value = this.$input.value.substring( 1 );
		}
		this.$display.style.backgroundColor = this.$input.value;
		return this;
	}

}

class FunctionController extends Controller {

	constructor( parent, object, property ) {

		super( parent, object, property, 'function' );

		// Buttons are the only case where widget contains name
		this.$button = document.createElement( 'button' );
		this.$button.appendChild( this.$name );
		this.$widget.appendChild( this.$button );

		this.$button.addEventListener( 'click', e => {
			e.preventDefault();
			this.getValue().call( this.object );
			this._callOnChange();
		} );

		// enables :active pseudo class on mobile
		this.$button.addEventListener( 'touchstart', () => {}, { passive: true } );

		this.$disable = this.$button;

	}

}

class NumberController extends Controller {

	constructor( parent, object, property, min, max, step ) {

		super( parent, object, property, 'number' );

		this._initInput();

		this.min( min );
		this.max( max );

		const stepExplicit = step !== undefined;
		this.step( stepExplicit ? step : this._getImplicitStep(), stepExplicit );

		this.updateDisplay();

	}

	decimals( decimals ) {
		this._decimals = decimals;
		this.updateDisplay();
		return this;
	}

	min( min ) {
		this._min = min;
		this._onUpdateMinMax();
		return this;
	}

	max( max ) {
		this._max = max;
		this._onUpdateMinMax();
		return this;
	}

	step( step, explicit = true ) {
		this._step = step;
		this._stepExplicit = explicit;
		return this;
	}

	updateDisplay() {

		const value = this.getValue();

		if ( this._hasSlider ) {

			let percent = ( value - this._min ) / ( this._max - this._min );
			percent = Math.max( 0, Math.min( percent, 1 ) );

			this.$fill.style.width = percent * 100 + '%';

		}

		if ( !this._inputFocused ) {
			this.$input.value = this._decimals === undefined ? value : value.toFixed( this._decimals );
		}

		return this;

	}

	_initInput() {

		this.$input = document.createElement( 'input' );
		this.$input.setAttribute( 'type', 'text' );
		this.$input.setAttribute( 'aria-labelledby', this.$name.id );

		// On touch devices only, use input[type=number] to force a numeric keyboard.
		// Ideally we could use one input type everywhere, but [type=number] has quirks
		// on desktop, and [inputmode=decimal] has quirks on iOS.
		// See https://github.com/georgealways/lil-gui/pull/16

		const isTouch = window.matchMedia( '(pointer: coarse)' ).matches;

		if ( isTouch ) {
			this.$input.setAttribute( 'type', 'number' );
			this.$input.setAttribute( 'step', 'any' );
		}

		this.$widget.appendChild( this.$input );

		this.$disable = this.$input;

		const onInput = () => {

			let value = parseFloat( this.$input.value );

			if ( isNaN( value ) ) return;

			if ( this._stepExplicit ) {
				value = this._snap( value );
			}

			this.setValue( this._clamp( value ) );

		};

		// Keys & mouse wheel
		// ---------------------------------------------------------------------

		const increment = delta => {

			const value = parseFloat( this.$input.value );

			if ( isNaN( value ) ) return;

			this._snapClampSetValue( value + delta );

			// Force the input to updateDisplay when it's focused
			this.$input.value = this.getValue();

		};

		const onKeyDown = e => {
			// Using `e.key` instead of `e.code` also catches NumpadEnter
			if ( e.key === 'Enter' ) {
				this.$input.blur();
			}
			if ( e.code === 'ArrowUp' ) {
				e.preventDefault();
				increment( this._step * this._arrowKeyMultiplier( e ) );
			}
			if ( e.code === 'ArrowDown' ) {
				e.preventDefault();
				increment( this._step * this._arrowKeyMultiplier( e ) * -1 );
			}
		};

		const onWheel = e => {
			if ( this._inputFocused ) {
				e.preventDefault();
				increment( this._step * this._normalizeMouseWheel( e ) );
			}
		};

		// Vertical drag
		// ---------------------------------------------------------------------

		let testingForVerticalDrag = false,
			initClientX,
			initClientY,
			prevClientY,
			initValue,
			dragDelta;

		// Once the mouse is dragged more than DRAG_THRESH px on any axis, we decide
		// on the user's intent: horizontal means highlight, vertical means drag.
		const DRAG_THRESH = 5;

		const onMouseDown = e => {

			initClientX = e.clientX;
			initClientY = prevClientY = e.clientY;
			testingForVerticalDrag = true;

			initValue = this.getValue();
			dragDelta = 0;

			window.addEventListener( 'mousemove', onMouseMove );
			window.addEventListener( 'mouseup', onMouseUp );

		};

		const onMouseMove = e => {

			if ( testingForVerticalDrag ) {

				const dx = e.clientX - initClientX;
				const dy = e.clientY - initClientY;

				if ( Math.abs( dy ) > DRAG_THRESH ) {

					e.preventDefault();
					this.$input.blur();
					testingForVerticalDrag = false;
					this._setDraggingStyle( true, 'vertical' );

				} else if ( Math.abs( dx ) > DRAG_THRESH ) {

					onMouseUp();

				}

			}

			// This isn't an else so that the first move counts towards dragDelta
			if ( !testingForVerticalDrag ) {

				const dy = e.clientY - prevClientY;

				dragDelta -= dy * this._step * this._arrowKeyMultiplier( e );

				// Clamp dragDelta so we don't have 'dead space' after dragging past bounds.
				// We're okay with the fact that bounds can be undefined here.
				if ( initValue + dragDelta > this._max ) {
					dragDelta = this._max - initValue;
				} else if ( initValue + dragDelta < this._min ) {
					dragDelta = this._min - initValue;
				}

				this._snapClampSetValue( initValue + dragDelta );

			}

			prevClientY = e.clientY;

		};

		const onMouseUp = () => {
			this._setDraggingStyle( false, 'vertical' );
			this._callOnFinishChange();
			window.removeEventListener( 'mousemove', onMouseMove );
			window.removeEventListener( 'mouseup', onMouseUp );
		};

		// Focus state & onFinishChange
		// ---------------------------------------------------------------------

		const onFocus = () => {
			this._inputFocused = true;
		};

		const onBlur = () => {
			this._inputFocused = false;
			this.updateDisplay();
			this._callOnFinishChange();
		};

		this.$input.addEventListener( 'input', onInput );
		this.$input.addEventListener( 'keydown', onKeyDown );
		this.$input.addEventListener( 'wheel', onWheel, { passive: false } );
		this.$input.addEventListener( 'mousedown', onMouseDown );
		this.$input.addEventListener( 'focus', onFocus );
		this.$input.addEventListener( 'blur', onBlur );

	}

	_initSlider() {

		this._hasSlider = true;

		// Build DOM
		// ---------------------------------------------------------------------

		this.$slider = document.createElement( 'div' );
		this.$slider.classList.add( 'slider' );

		this.$fill = document.createElement( 'div' );
		this.$fill.classList.add( 'fill' );

		this.$slider.appendChild( this.$fill );
		this.$widget.insertBefore( this.$slider, this.$input );

		this.domElement.classList.add( 'hasSlider' );

		// Map clientX to value
		// ---------------------------------------------------------------------

		const map = ( v, a, b, c, d ) => {
			return ( v - a ) / ( b - a ) * ( d - c ) + c;
		};

		const setValueFromX = clientX => {
			const rect = this.$slider.getBoundingClientRect();
			let value = map( clientX, rect.left, rect.right, this._min, this._max );
			this._snapClampSetValue( value );
		};

		// Mouse drag
		// ---------------------------------------------------------------------

		const mouseDown = e => {
			this._setDraggingStyle( true );
			setValueFromX( e.clientX );
			window.addEventListener( 'mousemove', mouseMove );
			window.addEventListener( 'mouseup', mouseUp );
		};

		const mouseMove = e => {
			setValueFromX( e.clientX );
		};

		const mouseUp = () => {
			this._callOnFinishChange();
			this._setDraggingStyle( false );
			window.removeEventListener( 'mousemove', mouseMove );
			window.removeEventListener( 'mouseup', mouseUp );
		};

		// Touch drag
		// ---------------------------------------------------------------------

		let testingForScroll = false, prevClientX, prevClientY;

		const beginTouchDrag = e => {
			e.preventDefault();
			this._setDraggingStyle( true );
			setValueFromX( e.touches[ 0 ].clientX );
			testingForScroll = false;
		};

		const onTouchStart = e => {

			if ( e.touches.length > 1 ) return;

			// If we're in a scrollable container, we should wait for the first
			// touchmove to see if the user is trying to slide or scroll.
			if ( this._hasScrollBar ) {

				prevClientX = e.touches[ 0 ].clientX;
				prevClientY = e.touches[ 0 ].clientY;
				testingForScroll = true;

			} else {

				// Otherwise, we can set the value straight away on touchstart.
				beginTouchDrag( e );

			}

			window.addEventListener( 'touchmove', onTouchMove, { passive: false } );
			window.addEventListener( 'touchend', onTouchEnd );

		};

		const onTouchMove = e => {

			if ( testingForScroll ) {

				const dx = e.touches[ 0 ].clientX - prevClientX;
				const dy = e.touches[ 0 ].clientY - prevClientY;

				if ( Math.abs( dx ) > Math.abs( dy ) ) {

					// We moved horizontally, set the value and stop checking.
					beginTouchDrag( e );

				} else {

					// This was, in fact, an attempt to scroll. Abort.
					window.removeEventListener( 'touchmove', onTouchMove );
					window.removeEventListener( 'touchend', onTouchEnd );

				}

			} else {

				e.preventDefault();
				setValueFromX( e.touches[ 0 ].clientX );

			}

		};

		const onTouchEnd = () => {
			this._callOnFinishChange();
			this._setDraggingStyle( false );
			window.removeEventListener( 'touchmove', onTouchMove );
			window.removeEventListener( 'touchend', onTouchEnd );
		};

		// Mouse wheel
		// ---------------------------------------------------------------------

		// We have to use a debounced function to call onFinishChange because
		// there's no way to tell when the user is "done" mouse-wheeling.
		const callOnFinishChange = this._callOnFinishChange.bind( this );
		const WHEEL_DEBOUNCE_TIME = 400;
		let wheelFinishChangeTimeout;

		const onWheel = e => {

			// ignore vertical wheels if there's a scrollbar
			const isVertical = Math.abs( e.deltaX ) < Math.abs( e.deltaY );
			if ( isVertical && this._hasScrollBar ) return;

			e.preventDefault();

			// set value
			const delta = this._normalizeMouseWheel( e ) * this._step;
			this._snapClampSetValue( this.getValue() + delta );

			// force the input to updateDisplay when it's focused
			this.$input.value = this.getValue();

			// debounce onFinishChange
			clearTimeout( wheelFinishChangeTimeout );
			wheelFinishChangeTimeout = setTimeout( callOnFinishChange, WHEEL_DEBOUNCE_TIME );

		};

		this.$slider.addEventListener( 'mousedown', mouseDown );
		this.$slider.addEventListener( 'touchstart', onTouchStart, { passive: false } );
		this.$slider.addEventListener( 'wheel', onWheel, { passive: false } );

	}

	_setDraggingStyle( active, axis = 'horizontal' ) {
		if ( this.$slider ) {
			this.$slider.classList.toggle( 'active', active );
		}
		document.body.classList.toggle( 'lil-gui-dragging', active );
		document.body.classList.toggle( `lil-gui-${axis}`, active );
	}

	_getImplicitStep() {

		if ( this._hasMin && this._hasMax ) {
			return ( this._max - this._min ) / 1000;
		}

		return 0.1;

	}

	_onUpdateMinMax() {

		if ( !this._hasSlider && this._hasMin && this._hasMax ) {

			// If this is the first time we're hearing about min and max
			// and we haven't explicitly stated what our step is, let's
			// update that too.
			if ( !this._stepExplicit ) {
				this.step( this._getImplicitStep(), false );
			}

			this._initSlider();
			this.updateDisplay();

		}

	}

	_normalizeMouseWheel( e ) {

		let { deltaX, deltaY } = e;

		// Safari and Chrome report weird non-integral values for a notched wheel,
		// but still expose actual lines scrolled via wheelDelta. Notched wheels
		// should behave the same way as arrow keys.
		if ( Math.floor( e.deltaY ) !== e.deltaY && e.wheelDelta ) {
			deltaX = 0;
			deltaY = -e.wheelDelta / 120;
			deltaY *= this._stepExplicit ? 1 : 10;
		}

		const wheel = deltaX + -deltaY;

		return wheel;

	}

	_arrowKeyMultiplier( e ) {

		let mult = this._stepExplicit ? 1 : 10;

		if ( e.shiftKey ) {
			mult *= 10;
		} else if ( e.altKey ) {
			mult /= 10;
		}

		return mult;

	}

	_snap( value ) {

		// Make the steps "start" at min or max.
		let offset = 0;
		if ( this._hasMin ) {
			offset = this._min;
		} else if ( this._hasMax ) {
			offset = this._max;
		}

		value -= offset;

		value = Math.round( value / this._step ) * this._step;

		value += offset;

		// Used to prevent "flyaway" decimals like 1.00000000000001
		value = parseFloat( value.toPrecision( 15 ) );

		return value;

	}

	_clamp( value ) {
		// either condition is false if min or max is undefined
		if ( value < this._min ) value = this._min;
		if ( value > this._max ) value = this._max;
		return value;
	}

	_snapClampSetValue( value ) {
		this.setValue( this._clamp( this._snap( value ) ) );
	}

	get _hasScrollBar() {
		const root = this.parent.root.$children;
		return root.scrollHeight > root.clientHeight;
	}

	get _hasMin() {
		return this._min !== undefined;
	}

	get _hasMax() {
		return this._max !== undefined;
	}

}

class OptionController extends Controller {

	constructor( parent, object, property, options ) {

		super( parent, object, property, 'option' );

		this.$select = document.createElement( 'select' );
		this.$select.setAttribute( 'aria-labelledby', this.$name.id );

		this.$display = document.createElement( 'div' );
		this.$display.classList.add( 'display' );

		this.$select.addEventListener( 'change', () => {
			this.setValue( this._values[ this.$select.selectedIndex ] );
			this._callOnFinishChange();
		} );

		this.$select.addEventListener( 'focus', () => {
			this.$display.classList.add( 'focus' );
		} );

		this.$select.addEventListener( 'blur', () => {
			this.$display.classList.remove( 'focus' );
		} );

		this.$widget.appendChild( this.$select );
		this.$widget.appendChild( this.$display );

		this.$disable = this.$select;

		this.options( options );

	}

	options( options ) {

		this._values = Array.isArray( options ) ? options : Object.values( options );
		this._names = Array.isArray( options ) ? options : Object.keys( options );

		this.$select.replaceChildren();

		this._names.forEach( name => {
			const $option = document.createElement( 'option' );
			$option.textContent = name;
			this.$select.appendChild( $option );
		} );

		this.updateDisplay();

		return this;

	}

	updateDisplay() {
		const value = this.getValue();
		const index = this._values.indexOf( value );
		this.$select.selectedIndex = index;
		this.$display.textContent = index === -1 ? value : this._names[ index ];
		return this;
	}

}

class StringController extends Controller {

	constructor( parent, object, property ) {

		super( parent, object, property, 'string' );

		this.$input = document.createElement( 'input' );
		this.$input.setAttribute( 'type', 'text' );
		this.$input.setAttribute( 'spellcheck', 'false' );
		this.$input.setAttribute( 'aria-labelledby', this.$name.id );

		this.$input.addEventListener( 'input', () => {
			this.setValue( this.$input.value );
		} );

		this.$input.addEventListener( 'keydown', e => {
			if ( e.code === 'Enter' ) {
				this.$input.blur();
			}
		} );

		this.$input.addEventListener( 'blur', () => {
			this._callOnFinishChange();
		} );

		this.$widget.appendChild( this.$input );

		this.$disable = this.$input;

		this.updateDisplay();

	}

	updateDisplay() {
		this.$input.value = this.getValue();
		return this;
	}

}

var stylesheet = `.lil-gui {
  font-family: var(--font-family);
  font-size: var(--font-size);
  line-height: 1;
  font-weight: normal;
  font-style: normal;
  text-align: left;
  color: var(--text-color);
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
  --background-color: #1f1f1f;
  --text-color: #ebebeb;
  --title-background-color: #111111;
  --title-text-color: #ebebeb;
  --widget-color: #424242;
  --hover-color: #4f4f4f;
  --focus-color: #595959;
  --number-color: #2cc9ff;
  --string-color: #a2db3c;
  --font-size: 11px;
  --input-font-size: 11px;
  --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  --font-family-mono: Menlo, Monaco, Consolas, "Droid Sans Mono", monospace;
  --padding: 4px;
  --spacing: 4px;
  --widget-height: 20px;
  --title-height: calc(var(--widget-height) + var(--spacing) * 1.25);
  --name-width: 45%;
  --slider-knob-width: 2px;
  --slider-input-width: 27%;
  --color-input-width: 27%;
  --slider-input-min-width: 45px;
  --color-input-min-width: 45px;
  --folder-indent: 7px;
  --widget-padding: 0 0 0 3px;
  --widget-border-radius: 2px;
  --checkbox-size: calc(0.75 * var(--widget-height));
  --scrollbar-width: 5px;
}
.lil-gui, .lil-gui * {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
.lil-gui.root {
  width: var(--width, 245px);
  display: flex;
  flex-direction: column;
  background: var(--background-color);
}
.lil-gui.root > .title {
  background: var(--title-background-color);
  color: var(--title-text-color);
}
.lil-gui.root > .children {
  overflow-x: hidden;
  overflow-y: auto;
}
.lil-gui.root > .children::-webkit-scrollbar {
  width: var(--scrollbar-width);
  height: var(--scrollbar-width);
  background: var(--background-color);
}
.lil-gui.root > .children::-webkit-scrollbar-thumb {
  border-radius: var(--scrollbar-width);
  background: var(--focus-color);
}
@media (pointer: coarse) {
  .lil-gui.allow-touch-styles, .lil-gui.allow-touch-styles .lil-gui {
    --widget-height: 28px;
    --padding: 6px;
    --spacing: 6px;
    --font-size: 13px;
    --input-font-size: 16px;
    --folder-indent: 10px;
    --scrollbar-width: 7px;
    --slider-input-min-width: 50px;
    --color-input-min-width: 65px;
  }
}
.lil-gui.force-touch-styles, .lil-gui.force-touch-styles .lil-gui {
  --widget-height: 28px;
  --padding: 6px;
  --spacing: 6px;
  --font-size: 13px;
  --input-font-size: 16px;
  --folder-indent: 10px;
  --scrollbar-width: 7px;
  --slider-input-min-width: 50px;
  --color-input-min-width: 65px;
}
.lil-gui.autoPlace {
  max-height: 100%;
  position: fixed;
  top: 0;
  right: 15px;
  z-index: 1001;
}

.lil-gui .controller {
  display: flex;
  align-items: center;
  padding: 0 var(--padding);
  margin: var(--spacing) 0;
}
.lil-gui .controller.disabled {
  opacity: 0.5;
}
.lil-gui .controller.disabled, .lil-gui .controller.disabled * {
  pointer-events: none !important;
}
.lil-gui .controller > .name {
  min-width: var(--name-width);
  flex-shrink: 0;
  white-space: pre;
  padding-right: var(--spacing);
  line-height: var(--widget-height);
}
.lil-gui .controller .widget {
  position: relative;
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--widget-height);
}
.lil-gui .controller.string input {
  color: var(--string-color);
}
.lil-gui .controller.boolean {
  cursor: pointer;
}
.lil-gui .controller.color .display {
  width: 100%;
  height: var(--widget-height);
  border-radius: var(--widget-border-radius);
  position: relative;
}
@media (hover: hover) {
  .lil-gui .controller.color .display:hover:before {
    content: " ";
    display: block;
    position: absolute;
    border-radius: var(--widget-border-radius);
    border: 1px solid #fff9;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
  }
}
.lil-gui .controller.color input[type=color] {
  opacity: 0;
  width: 100%;
  height: 100%;
  cursor: pointer;
}
.lil-gui .controller.color input[type=text] {
  margin-left: var(--spacing);
  font-family: var(--font-family-mono);
  min-width: var(--color-input-min-width);
  width: var(--color-input-width);
  flex-shrink: 0;
}
.lil-gui .controller.option select {
  opacity: 0;
  position: absolute;
  width: 100%;
  max-width: 100%;
}
.lil-gui .controller.option .display {
  position: relative;
  pointer-events: none;
  border-radius: var(--widget-border-radius);
  height: var(--widget-height);
  line-height: var(--widget-height);
  max-width: 100%;
  overflow: hidden;
  word-break: break-all;
  padding-left: 0.55em;
  padding-right: 1.75em;
  background: var(--widget-color);
}
@media (hover: hover) {
  .lil-gui .controller.option .display.focus {
    background: var(--focus-color);
  }
}
.lil-gui .controller.option .display.active {
  background: var(--focus-color);
}
.lil-gui .controller.option .display:after {
  font-family: "lil-gui";
  content: "↕";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  padding-right: 0.375em;
}
.lil-gui .controller.option .widget,
.lil-gui .controller.option select {
  cursor: pointer;
}
@media (hover: hover) {
  .lil-gui .controller.option .widget:hover .display {
    background: var(--hover-color);
  }
}
.lil-gui .controller.number input {
  color: var(--number-color);
}
.lil-gui .controller.number.hasSlider input {
  margin-left: var(--spacing);
  width: var(--slider-input-width);
  min-width: var(--slider-input-min-width);
  flex-shrink: 0;
}
.lil-gui .controller.number .slider {
  width: 100%;
  height: var(--widget-height);
  background: var(--widget-color);
  border-radius: var(--widget-border-radius);
  padding-right: var(--slider-knob-width);
  overflow: hidden;
  cursor: ew-resize;
  touch-action: pan-y;
}
@media (hover: hover) {
  .lil-gui .controller.number .slider:hover {
    background: var(--hover-color);
  }
}
.lil-gui .controller.number .slider.active {
  background: var(--focus-color);
}
.lil-gui .controller.number .slider.active .fill {
  opacity: 0.95;
}
.lil-gui .controller.number .fill {
  height: 100%;
  border-right: var(--slider-knob-width) solid var(--number-color);
  box-sizing: content-box;
}

.lil-gui-dragging .lil-gui {
  --hover-color: var(--widget-color);
}
.lil-gui-dragging * {
  cursor: ew-resize !important;
}

.lil-gui-dragging.lil-gui-vertical * {
  cursor: ns-resize !important;
}

.lil-gui .title {
  height: var(--title-height);
  font-weight: 600;
  padding: 0 var(--padding);
  width: 100%;
  text-align: left;
  background: none;
  text-decoration-skip: objects;
}
.lil-gui .title:before {
  font-family: "lil-gui";
  content: "▾";
  padding-right: 2px;
  display: inline-block;
}
.lil-gui .title:active {
  background: var(--title-background-color);
  opacity: 0.75;
}
@media (hover: hover) {
  body:not(.lil-gui-dragging) .lil-gui .title:hover {
    background: var(--title-background-color);
    opacity: 0.85;
  }
  .lil-gui .title:focus {
    text-decoration: underline var(--focus-color);
  }
}
.lil-gui.root > .title:focus {
  text-decoration: none !important;
}
.lil-gui.closed > .title:before {
  content: "▸";
}
.lil-gui.closed > .children {
  transform: translateY(-7px);
  opacity: 0;
}
.lil-gui.closed:not(.transition) > .children {
  display: none;
}
.lil-gui.transition > .children {
  transition-duration: 300ms;
  transition-property: height, opacity, transform;
  transition-timing-function: cubic-bezier(0.2, 0.6, 0.35, 1);
  overflow: hidden;
  pointer-events: none;
}
.lil-gui .children:empty:before {
  content: "Empty";
  padding: 0 var(--padding);
  margin: var(--spacing) 0;
  display: block;
  height: var(--widget-height);
  font-style: italic;
  line-height: var(--widget-height);
  opacity: 0.5;
}
.lil-gui.root > .children > .lil-gui > .title {
  border: 0 solid var(--widget-color);
  border-width: 1px 0;
  transition: border-color 300ms;
}
.lil-gui.root > .children > .lil-gui.closed > .title {
  border-bottom-color: transparent;
}
.lil-gui + .controller {
  border-top: 1px solid var(--widget-color);
  margin-top: 0;
  padding-top: var(--spacing);
}
.lil-gui .lil-gui .lil-gui > .title {
  border: none;
}
.lil-gui .lil-gui .lil-gui > .children {
  border: none;
  margin-left: var(--folder-indent);
  border-left: 2px solid var(--widget-color);
}
.lil-gui .lil-gui .controller {
  border: none;
}

.lil-gui label, .lil-gui input, .lil-gui button {
  -webkit-tap-highlight-color: transparent;
}
.lil-gui input {
  border: 0;
  outline: none;
  font-family: var(--font-family);
  font-size: var(--input-font-size);
  border-radius: var(--widget-border-radius);
  height: var(--widget-height);
  background: var(--widget-color);
  color: var(--text-color);
  width: 100%;
}
@media (hover: hover) {
  .lil-gui input:hover {
    background: var(--hover-color);
  }
  .lil-gui input:active {
    background: var(--focus-color);
  }
}
.lil-gui input:disabled {
  opacity: 1;
}
.lil-gui input[type=text],
.lil-gui input[type=number] {
  padding: var(--widget-padding);
  -moz-appearance: textfield;
}
.lil-gui input[type=text]:focus,
.lil-gui input[type=number]:focus {
  background: var(--focus-color);
}
.lil-gui input[type=checkbox] {
  appearance: none;
  width: var(--checkbox-size);
  height: var(--checkbox-size);
  border-radius: var(--widget-border-radius);
  text-align: center;
  cursor: pointer;
}
.lil-gui input[type=checkbox]:checked:before {
  font-family: "lil-gui";
  content: "✓";
  font-size: var(--checkbox-size);
  line-height: var(--checkbox-size);
}
@media (hover: hover) {
  .lil-gui input[type=checkbox]:focus {
    box-shadow: inset 0 0 0 1px var(--focus-color);
  }
}
.lil-gui button {
  outline: none;
  cursor: pointer;
  font-family: var(--font-family);
  font-size: var(--font-size);
  color: var(--text-color);
  width: 100%;
  border: none;
}
.lil-gui .controller button {
  height: var(--widget-height);
  text-transform: none;
  background: var(--widget-color);
  border-radius: var(--widget-border-radius);
}
@media (hover: hover) {
  .lil-gui .controller button:hover {
    background: var(--hover-color);
  }
  .lil-gui .controller button:focus {
    box-shadow: inset 0 0 0 1px var(--focus-color);
  }
}
.lil-gui .controller button:active {
  background: var(--focus-color);
}

@font-face {
  font-family: "lil-gui";
  src: url("data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAUsAAsAAAAACJwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAAH4AAADAImwmYE9TLzIAAAGIAAAAPwAAAGBKqH5SY21hcAAAAcgAAAD0AAACrukyyJBnbHlmAAACvAAAAF8AAACEIZpWH2hlYWQAAAMcAAAAJwAAADZfcj2zaGhlYQAAA0QAAAAYAAAAJAC5AHhobXR4AAADXAAAABAAAABMAZAAAGxvY2EAAANsAAAAFAAAACgCEgIybWF4cAAAA4AAAAAeAAAAIAEfABJuYW1lAAADoAAAASIAAAIK9SUU/XBvc3QAAATEAAAAZgAAAJCTcMc2eJxVjbEOgjAURU+hFRBK1dGRL+ALnAiToyMLEzFpnPz/eAshwSa97517c/MwwJmeB9kwPl+0cf5+uGPZXsqPu4nvZabcSZldZ6kfyWnomFY/eScKqZNWupKJO6kXN3K9uCVoL7iInPr1X5baXs3tjuMqCtzEuagm/AAlzQgPAAB4nGNgYRBlnMDAysDAYM/gBiT5oLQBAwuDJAMDEwMrMwNWEJDmmsJwgCFeXZghBcjlZMgFCzOiKOIFAB71Bb8AeJy1kjFuwkAQRZ+DwRAwBtNQRUGKQ8OdKCAWUhAgKLhIuAsVSpWz5Bbkj3dEgYiUIszqWdpZe+Z7/wB1oCYmIoboiwiLT2WjKl/jscrHfGg/pKdMkyklC5Zs2LEfHYpjcRoPzme9MWWmk3dWbK9ObkWkikOetJ554fWyoEsmdSlt+uR0pCJR34b6t/TVg1SY3sYvdf8vuiKrpyaDXDISiegp17p7579Gp3p++y7HPAiY9pmTibljrr85qSidtlg4+l25GLCaS8e6rRxNBmsnERunKbaOObRz7N72ju5vdAjYpBXHgJylOAVsMseDAPEP8LYoUHicY2BiAAEfhiAGJgZWBgZ7RnFRdnVJELCQlBSRlATJMoLV2DK4glSYs6ubq5vbKrJLSbGrgEmovDuDJVhe3VzcXFwNLCOILB/C4IuQ1xTn5FPilBTj5FPmBAB4WwoqAHicY2BkYGAA4sk1sR/j+W2+MnAzpDBgAyEMQUCSg4EJxAEAwUgFHgB4nGNgZGBgSGFggJMhDIwMqEAYAByHATJ4nGNgAIIUNEwmAABl3AGReJxjYAACIQYlBiMGJ3wQAEcQBEV4nGNgZGBgEGZgY2BiAAEQyQWEDAz/wXwGAAsPATIAAHicXdBNSsNAHAXwl35iA0UQXYnMShfS9GPZA7T7LgIu03SSpkwzYTIt1BN4Ak/gKTyAeCxfw39jZkjymzcvAwmAW/wgwHUEGDb36+jQQ3GXGot79L24jxCP4gHzF/EIr4jEIe7wxhOC3g2TMYy4Q7+Lu/SHuEd/ivt4wJd4wPxbPEKMX3GI5+DJFGaSn4qNzk8mcbKSR6xdXdhSzaOZJGtdapd4vVPbi6rP+cL7TGXOHtXKll4bY1Xl7EGnPtp7Xy2n00zyKLVHfkHBa4IcJ2oD3cgggWvt/V/FbDrUlEUJhTn/0azVWbNTNr0Ens8de1tceK9xZmfB1CPjOmPH4kitmvOubcNpmVTN3oFJyjzCvnmrwhJTzqzVj9jiSX911FjeAAB4nG3HMRKCMBBA0f0giiKi4DU8k0V2GWbIZDOh4PoWWvq6J5V8If9NVNQcaDhyouXMhY4rPTcG7jwYmXhKq8Wz+p762aNaeYXom2n3m2dLTVgsrCgFJ7OTmIkYbwIbC6vIB7WmFfAAAA==") format("woff");
}`;

function _injectStyles( cssContent ) {
	const injected = document.createElement( 'style' );
	injected.innerHTML = cssContent;
	const before = document.querySelector( 'head link[rel=stylesheet], head style' );
	if ( before ) {
		document.head.insertBefore( injected, before );
	} else {
		document.head.appendChild( injected );
	}
}


let stylesInjected = false;

class GUI {

	/**
	 * Creates a panel that holds controllers.
	 * @example
	 * new GUI();
	 * new GUI( { container: document.getElementById( 'custom' ) } );
	 *
	 * @param {object} [options]
	 * @param {boolean} [options.autoPlace=true]
	 * Adds the GUI to `document.body` and fixes it to the top right of the page.
	 *
	 * @param {HTMLElement} [options.container]
	 * Adds the GUI to this DOM element. Overrides `autoPlace`.
	 *
	 * @param {number} [options.width=245]
	 * Width of the GUI in pixels, usually set when name labels become too long. Note that you can make
	 * name labels wider in CSS with `.lil‑gui { ‑‑name‑width: 55% }`.
	 *
	 * @param {string} [options.title=Controls]
	 * Name to display in the title bar.
	 *
	 * @param {boolean} [options.closeFolders=false]
	 * Pass `true` to close all folders in this GUI by default.
	 *
	 * @param {boolean} [options.injectStyles=true]
	 * Injects the default stylesheet into the page if this is the first GUI.
	 * Pass `false` to use your own stylesheet.
	 *
	 * @param {number} [options.touchStyles=true]
	 * Makes controllers larger on touch devices. Pass `false` to disable touch styles.
	 *
	 * @param {GUI} [options.parent]
	 * Adds this GUI as a child in another GUI. Usually this is done for you by `addFolder()`.
	 */
	constructor( {
		parent,
		autoPlace = parent === undefined,
		container,
		width,
		title = 'Controls',
		closeFolders = false,
		injectStyles = true,
		touchStyles = true
	} = {} ) {

		/**
		 * The GUI containing this folder, or `undefined` if this is the root GUI.
		 * @type {GUI}
		 */
		this.parent = parent;

		/**
		 * The top level GUI containing this folder, or `this` if this is the root GUI.
		 * @type {GUI}
		 */
		this.root = parent ? parent.root : this;

		/**
		 * The list of controllers and folders contained by this GUI.
		 * @type {Array<GUI|Controller>}
		 */
		this.children = [];

		/**
		 * The list of controllers contained by this GUI.
		 * @type {Array<Controller>}
		 */
		this.controllers = [];

		/**
		 * The list of folders contained by this GUI.
		 * @type {Array<GUI>}
		 */
		this.folders = [];

		/**
		 * Used to determine if the GUI is closed. Use `gui.open()` or `gui.close()` to change this.
		 * @type {boolean}
		 */
		this._closed = false;

		/**
		 * Used to determine if the GUI is hidden. Use `gui.show()` or `gui.hide()` to change this.
		 * @type {boolean}
		 */
		this._hidden = false;

		/**
		 * The outermost container element.
		 * @type {HTMLElement}
		 */
		this.domElement = document.createElement( 'div' );
		this.domElement.classList.add( 'lil-gui' );

		/**
		 * The DOM element that contains the title.
		 * @type {HTMLElement}
		 */
		this.$title = document.createElement( 'button' );
		this.$title.classList.add( 'title' );
		this.$title.setAttribute( 'aria-expanded', true );

		this.$title.addEventListener( 'click', () => this.openAnimated( this._closed ) );

		// enables :active pseudo class on mobile
		this.$title.addEventListener( 'touchstart', () => {}, { passive: true } );

		/**
		 * The DOM element that contains children.
		 * @type {HTMLElement}
		 */
		this.$children = document.createElement( 'div' );
		this.$children.classList.add( 'children' );

		this.domElement.appendChild( this.$title );
		this.domElement.appendChild( this.$children );

		this.title( title );

		if ( this.parent ) {

			this.parent.children.push( this );
			this.parent.folders.push( this );

			this.parent.$children.appendChild( this.domElement );

			// Stop the constructor early, everything onward only applies to root GUI's
			return;

		}

		this.domElement.classList.add( 'root' );

		if ( touchStyles ) {
			this.domElement.classList.add( 'allow-touch-styles' );
		}

		// Inject stylesheet if we haven't done that yet
		if ( !stylesInjected && injectStyles ) {
			_injectStyles( stylesheet );
			stylesInjected = true;
		}

		if ( container ) {

			container.appendChild( this.domElement );

		} else if ( autoPlace ) {

			this.domElement.classList.add( 'autoPlace' );
			document.body.appendChild( this.domElement );

		}

		if ( width ) {
			this.domElement.style.setProperty( '--width', width + 'px' );
		}

		this._closeFolders = closeFolders;

	}

	/**
	 * Adds a controller to the GUI, inferring controller type using the `typeof` operator.
	 * @example
	 * gui.add( object, 'property' );
	 * gui.add( object, 'number', 0, 100, 1 );
	 * gui.add( object, 'options', [ 1, 2, 3 ] );
	 *
	 * @param {object} object The object the controller will modify.
	 * @param {string} property Name of the property to control.
	 * @param {number|object|Array} [$1] Minimum value for number controllers, or the set of
	 * selectable values for a dropdown.
	 * @param {number} [max] Maximum value for number controllers.
	 * @param {number} [step] Step value for number controllers.
	 * @returns {Controller}
	 */
	add( object, property, $1, max, step ) {

		if ( Object( $1 ) === $1 ) {

			return new OptionController( this, object, property, $1 );

		}

		const initialValue = object[ property ];

		switch ( typeof initialValue ) {

			case 'number':

				return new NumberController( this, object, property, $1, max, step );

			case 'boolean':

				return new BooleanController( this, object, property );

			case 'string':

				return new StringController( this, object, property );

			case 'function':

				return new FunctionController( this, object, property );

		}

		console.error( `gui.add failed
	property:`, property, `
	object:`, object, `
	value:`, initialValue );

	}

	/**
	 * Adds a color controller to the GUI.
	 * @example
	 * params = {
	 * 	cssColor: '#ff00ff',
	 * 	rgbColor: { r: 0, g: 0.2, b: 0.4 },
	 * 	customRange: [ 0, 127, 255 ],
	 * };
	 *
	 * gui.addColor( params, 'cssColor' );
	 * gui.addColor( params, 'rgbColor' );
	 * gui.addColor( params, 'customRange', 255 );
	 *
	 * @param {object} object The object the controller will modify.
	 * @param {string} property Name of the property to control.
	 * @param {number} rgbScale Maximum value for a color channel when using an RGB color. You may
	 * need to set this to 255 if your colors are too bright.
	 * @returns {Controller}
	 */
	addColor( object, property, rgbScale = 1 ) {
		return new ColorController( this, object, property, rgbScale );
	}

	/**
	 * Adds a folder to the GUI, which is just another GUI. This method returns
	 * the nested GUI so you can add controllers to it.
	 * @example
	 * const folder = gui.addFolder( 'Position' );
	 * folder.add( position, 'x' );
	 * folder.add( position, 'y' );
	 * folder.add( position, 'z' );
	 *
	 * @param {string} title Name to display in the folder's title bar.
	 * @returns {GUI}
	 */
	addFolder( title ) {
		const folder = new GUI( { parent: this, title } );
		if ( this.root._closeFolders ) folder.close();
		return folder;
	}

	/**
	 * Recalls values that were saved with `gui.save()`.
	 * @param {object} obj
	 * @param {boolean} recursive Pass false to exclude folders descending from this GUI.
	 * @returns {this}
	 */
	load( obj, recursive = true ) {

		if ( obj.controllers ) {

			this.controllers.forEach( c => {

				if ( c instanceof FunctionController ) return;

				if ( c._name in obj.controllers ) {
					c.load( obj.controllers[ c._name ] );
				}

			} );

		}

		if ( recursive && obj.folders ) {

			this.folders.forEach( f => {

				if ( f._title in obj.folders ) {
					f.load( obj.folders[ f._title ] );
				}

			} );

		}

		return this;

	}

	/**
	 * Returns an object mapping controller names to values. The object can be passed to `gui.load()` to
	 * recall these values.
	 * @example
	 * {
	 * 	controllers: {
	 * 		prop1: 1,
	 * 		prop2: 'value',
	 * 		...
	 * 	},
	 * 	folders: {
	 * 		folderName1: { controllers, folders },
	 * 		folderName2: { controllers, folders }
	 * 		...
	 * 	}
	 * }
	 *
	 * @param {boolean} recursive Pass false to exclude folders descending from this GUI.
	 * @returns {object}
	 */
	save( recursive = true ) {

		const obj = {
			controllers: {},
			folders: {}
		};

		this.controllers.forEach( c => {

			if ( c instanceof FunctionController ) return;

			if ( c._name in obj.controllers ) {
				throw new Error( `Cannot save GUI with duplicate property "${c._name}"` );
			}

			obj.controllers[ c._name ] = c.save();

		} );

		if ( recursive ) {

			this.folders.forEach( f => {

				if ( f._title in obj.folders ) {
					throw new Error( `Cannot save GUI with duplicate folder "${f._title}"` );
				}

				obj.folders[ f._title ] = f.save();

			} );

		}

		return obj;

	}

	/**
	 * Opens a GUI or folder. GUI and folders are open by default.
	 * @param {boolean} open Pass false to close.
	 * @returns {this}
	 * @example
	 * gui.open(); // open
	 * gui.open( false ); // close
	 * gui.open( gui._closed ); // toggle
	 */
	open( open = true ) {

		this._setClosed( !open );

		this.$title.setAttribute( 'aria-expanded', !this._closed );
		this.domElement.classList.toggle( 'closed', this._closed );

		return this;

	}

	/**
	 * Closes the GUI.
	 * @returns {this}
	 */
	close() {
		return this.open( false );
	}

	_setClosed( closed ) {
		if ( this._closed === closed ) return;
		this._closed = closed;
		this._callOnOpenClose( this );
	}

	/**
	 * Shows the GUI after it's been hidden.
	 * @param {boolean} show
	 * @returns {this}
	 * @example
	 * gui.show();
	 * gui.show( false ); // hide
	 * gui.show( gui._hidden ); // toggle
	 */
	show( show = true ) {

		this._hidden = !show;

		this.domElement.style.display = this._hidden ? 'none' : '';

		return this;

	}

	/**
	 * Hides the GUI.
	 * @returns {this}
	 */
	hide() {
		return this.show( false );
	}

	openAnimated( open = true ) {

		// set state immediately
		this._setClosed( !open );

		this.$title.setAttribute( 'aria-expanded', !this._closed );

		// wait for next frame to measure $children
		requestAnimationFrame( () => {

			// explicitly set initial height for transition
			const initialHeight = this.$children.clientHeight;
			this.$children.style.height = initialHeight + 'px';

			this.domElement.classList.add( 'transition' );

			const onTransitionEnd = e => {
				if ( e.target !== this.$children ) return;
				this.$children.style.height = '';
				this.domElement.classList.remove( 'transition' );
				this.$children.removeEventListener( 'transitionend', onTransitionEnd );
			};

			this.$children.addEventListener( 'transitionend', onTransitionEnd );

			// todo: this is wrong if children's scrollHeight makes for a gui taller than maxHeight
			const targetHeight = !open ? 0 : this.$children.scrollHeight;

			this.domElement.classList.toggle( 'closed', !open );

			requestAnimationFrame( () => {
				this.$children.style.height = targetHeight + 'px';
			} );

		} );

		return this;

	}

	/**
	 * Change the title of this GUI.
	 * @param {string} title
	 * @returns {this}
	 */
	title( title ) {
		/**
		 * Current title of the GUI. Use `gui.title( 'Title' )` to modify this value.
		 * @type {string}
		 */
		this._title = title;
		this.$title.textContent = title;
		return this;
	}

	/**
	 * Resets all controllers to their initial values.
	 * @param {boolean} recursive Pass false to exclude folders descending from this GUI.
	 * @returns {this}
	 */
	reset( recursive = true ) {
		const controllers = recursive ? this.controllersRecursive() : this.controllers;
		controllers.forEach( c => c.reset() );
		return this;
	}

	/**
	 * Pass a function to be called whenever a controller in this GUI changes.
	 * @param {function({object:object, property:string, value:any, controller:Controller})} callback
	 * @returns {this}
	 * @example
	 * gui.onChange( event => {
	 * 	event.object     // object that was modified
	 * 	event.property   // string, name of property
	 * 	event.value      // new value of controller
	 * 	event.controller // controller that was modified
	 * } );
	 */
	onChange( callback ) {
		/**
		 * Used to access the function bound to `onChange` events. Don't modify this value
		 * directly. Use the `gui.onChange( callback )` method instead.
		 * @type {Function}
		 */
		this._onChange = callback;
		return this;
	}

	_callOnChange( controller ) {

		if ( this.parent ) {
			this.parent._callOnChange( controller );
		}

		if ( this._onChange !== undefined ) {
			this._onChange.call( this, {
				object: controller.object,
				property: controller.property,
				value: controller.getValue(),
				controller
			} );
		}
	}

	/**
	 * Pass a function to be called whenever a controller in this GUI has finished changing.
	 * @param {function({object:object, property:string, value:any, controller:Controller})} callback
	 * @returns {this}
	 * @example
	 * gui.onFinishChange( event => {
	 * 	event.object     // object that was modified
	 * 	event.property   // string, name of property
	 * 	event.value      // new value of controller
	 * 	event.controller // controller that was modified
	 * } );
	 */
	onFinishChange( callback ) {
		/**
		 * Used to access the function bound to `onFinishChange` events. Don't modify this value
		 * directly. Use the `gui.onFinishChange( callback )` method instead.
		 * @type {Function}
		 */
		this._onFinishChange = callback;
		return this;
	}

	_callOnFinishChange( controller ) {

		if ( this.parent ) {
			this.parent._callOnFinishChange( controller );
		}

		if ( this._onFinishChange !== undefined ) {
			this._onFinishChange.call( this, {
				object: controller.object,
				property: controller.property,
				value: controller.getValue(),
				controller
			} );
		}
	}

	/**
	 * Pass a function to be called when this GUI or its descendants are opened or closed.
	 * @param {function(GUI)} callback
	 * @returns {this}
	 * @example
	 * gui.onOpenClose( changedGUI => {
	 * 	console.log( changedGUI._closed );
	 * } );
	 */
	onOpenClose( callback ) {
		this._onOpenClose = callback;
		return this;
	}

	_callOnOpenClose( changedGUI ) {
		if ( this.parent ) {
			this.parent._callOnOpenClose( changedGUI );
		}

		if ( this._onOpenClose !== undefined ) {
			this._onOpenClose.call( this, changedGUI );
		}
	}

	/**
	 * Destroys all DOM elements and event listeners associated with this GUI.
	 */
	destroy() {

		if ( this.parent ) {
			this.parent.children.splice( this.parent.children.indexOf( this ), 1 );
			this.parent.folders.splice( this.parent.folders.indexOf( this ), 1 );
		}

		if ( this.domElement.parentElement ) {
			this.domElement.parentElement.removeChild( this.domElement );
		}

		Array.from( this.children ).forEach( c => c.destroy() );

	}

	/**
	 * Returns an array of controllers contained by this GUI and its descendents.
	 * @returns {Controller[]}
	 */
	controllersRecursive() {
		let controllers = Array.from( this.controllers );
		this.folders.forEach( f => {
			controllers = controllers.concat( f.controllersRecursive() );
		} );
		return controllers;
	}

	/**
	 * Returns an array of folders contained by this GUI and its descendents.
	 * @returns {GUI[]}
	 */
	foldersRecursive() {
		let folders = Array.from( this.folders );
		this.folders.forEach( f => {
			folders = folders.concat( f.foldersRecursive() );
		} );
		return folders;
	}

}

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

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

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

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

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

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

   return vol;
}

let $comp_col_cnt = 0;

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

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

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

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

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

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

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

   if (!side) $comp_col_cnt = 0;

   return vol;
}


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

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

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

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

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


/** @summary find item with geometry painter
  * @private */
function findItemWithGeoPainter(hitem, test_changes) {
   while (hitem) {
      if (isFunc(hitem._painter?.testGeomChanges)) {
         if (test_changes)
            hitem._painter.testGeomChanges();
         return hitem;
      }
      hitem = hitem._parent;
   }
   return null;
}

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

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

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

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


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

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


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

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

class Toolbar {

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

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

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

         buttonsNames.push(buttonName);

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

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

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

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

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

} // class ToolBar


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

class GeoDrawingControl extends InteractiveControl {

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

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

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

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

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

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

            h.jsroot_special = true; // exclude from intersections

            c._highlight_mesh = h;
         }
         return true;
      }

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

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

} // class GeoDrawingControl


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

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

class TGeoPainter extends ObjectPainter {

   #geo_manager;  // TGeoManager instance - if assigned in drawing
   #superimpose;  // set when superimposed with other 3D drawings
   #complete_handler; // callback, assigned by ROOT GeomViewer
   #geom_viewer;  // true when processing drawing from ROOT GeomViewer
   #extra_objects; // TList with extra objects configured for drawing
   #webgl;         // true when normal WebGL drawing is enabled
   #worker;       // extra Worker to run different calculations
   #worker_ready; // is worker started and initialized
   #worker_jobs;  // number of submitted to worker jobs
   #clones;       // instance of ClonedNodes
   #clones_owner; // is instance managed by the painter
   #draw_nodes;   // drawn nodes from geometry
   #build_shapes; // build shapes required for drawings
   #current_face_limit; // current face limit
   #draw_all_nodes; // flag using in drawing
   #draw_nodes_again; // flag set if drawing should be started again when completed
   #drawing_ready; // if drawing completed
   #drawing_log;   // current drawing log information
   #draw_resolveFuncs; // resolve call-backs for start drawing calls
   #start_drawing_time; // time when drawing started
   #start_render_tm; // time when rendering started
   #first_render_tm; // first time when rendering was performed
   #last_render_tm;  // last time when rendering was performed
   #last_render_meshes; // last value of rendered meshes
   #render_resolveFuncs; // resolve call-backs from render calls
   #render_tmout;    // timeout used in Render3D()
   #first_drawing;   // true when very first drawing is performed
   #full_redrawing;  // if full redraw must be performed
   #last_clip_cfg;   // last config of clip panels
   #redraw_timer;    // timer used in redraw
   #did_update;     // flag used in update
   #custom_bounding_box; // custom bounding box for extra objects
   #clip_planes;     // clip planes
   #central_painter; // pointer on central painter
   #subordinate_painters; // list of subordinate painters
   #effectComposer;  // extra composer for special effects, used in EVE
   #bloomComposer;   // extra composer for bloom highlight
   #new_draw_nodes; // temporary list of new draw nodes
   #new_append_nodes; // temporary list of new append nodes
   #more_nodes;      // more nodes from ROOT geometry viewer
   #provided_more_nodes; // staged more nodes from ROOT geometry viewer
   #on_pad;         // true when drawing done on TPad
   #last_manifest;  // last node which was "manifest" via menu
   #last_hidden;    // last node which was "hidden" via menu
   #gui;            // dat.GUI instance
   #toolbar;        // tool buttons
   #controls;       // orbit control
   #highlight_handlers; // highlight handlers
   #animating;      // set when animation started
   #last_camera_position; // last camera position
   #fullgeom_proj;  // full geometry to produce projection
   #selected_mesh;  // used in highlight
   #fog;            // fog for the scene
   #lookat;         // position to which camera looks at
   #scene_size;     // stored scene size to control changes
   #scene_width;    // actual scene width
   #scene_height;   // actual scene width
   #fit_main_area;  // true if drawing must fit to DOM
   #overall_size;   // overall size
   #scene;          // three.js Scene object
   #camera;         // three.js camera object
   #renderer;       // three.js renderer object
   #toplevel;       // three.js top level Object3D
   #camera0pos;     // camera on opposite side
   #vrControllers;  // used in VR display
   #controllersMeshes; // used in VR display
   #dolly;          // used in VR display
   #vrDisplay;      // used in VR display
   #vr_camera_position; // used in VR display
   #vr_camera_rotation; // used in VR display
   #vr_camera_near;  // used in VR display
   #standingMatrix;  // used in VR display
   #raycasterEnd;    // used in VR display
   #raycasterOrigin;  // used in VR display
   #adjust_camera_with_render; // flag to readjust camera when rendering
   #did_cleanup;    // flag set after cleanup

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

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

      super(dom, obj);

      this.#superimpose = Boolean(getHistPainter3DCfg(this.getMainPainter()));
      this.#geo_manager = gm;

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

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

      this.#draw_resolveFuncs = [];
      this.#render_resolveFuncs = [];

      this.cleanup(true);
   }

   /** @summary Returns scene */
   getScene() { return this.#scene; }

   /** @summary Returns camera */
   getCamera() { return this.#camera; }

   /** @summary Returns renderer */
   getRenderer() { return this.#renderer; }

   /** @summary Returns top Object3D instance */
   getTopObject3D() { return this.#toplevel; }

   /** @summary Assign geometry viewer mode
    * @private */
   setGeomViewer(on) { this.#geom_viewer = on; }

   /** @summary Assign or remove subordinate painter */
   assignSubordinate(painter, do_assign = true) {
      if (this.#subordinate_painters === undefined)
         this.#subordinate_painters = [];
      const pos = this.#subordinate_painters.indexOf(painter);
      if (do_assign && pos < 0)
         this.#subordinate_painters.push(painter);
      else if (!do_assign && pos >= 0)
         this.#subordinate_painters.splice(pos, 1);
   }

   /** @summary Return list of subordinate painters */
   getSubordinates() { return this.#subordinate_painters ?? []; }

   /** @summary Assign or cleanup central painter */
   assignCentral(painter, do_assign = true) {
      if (do_assign)
         this.#central_painter = painter;
      else if (this.#central_painter === painter)
         this.#central_painter = undefined;
   }

   /** @summary Returns central painter */
   getCentral() { return this.#central_painter; }

   /** @summary Returns extra objects configured for drawing */
   getExtraObjects() { return this.#extra_objects; }

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

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

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

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

   getControls() { return this.#controls; }

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

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

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

               if (closeMenu())
                  return;

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

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

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

      this.#toolbar.createButtons();
   }

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

      // Raycaster temp variables to avoid one per frame allocation.
      this.#raycasterEnd = new THREE.Vector3();
      this.#raycasterOrigin = new THREE.Vector3();

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

         this.initVRControllersGeometry();
      });
   }

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

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

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

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

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

   /** @summary Toggle VR mode
     * @private */
   toggleVRMode() {
      if (!this.#vrDisplay)
         return;
      // Toggle VR mode off
      if (this.#vrDisplay.isPresenting) {
         this.exitVRMode();
         return;
      }
      this.#vr_camera_position = this.#camera.position.clone();
      this.#vr_camera_rotation = this.#camera.rotation.clone();
      this.#vrDisplay.requestPresent([{ source: this.#renderer.domElement }]).then(() => {
         this.#vr_camera_near = this.#camera.near;
         this.#dolly.position.set(this.#camera.position.x / 4, -this.#camera.position.y / 8, -this.#camera.position.z / 4);
         this.#camera.position.set(0, 0, 0);
         this.#dolly.add(this.#camera);
         this.#camera.near = 0.1;
         this.#camera.updateProjectionMatrix();
         this.#renderer.vr.enabled = true;
         this.#renderer.setAnimationLoop(() => {
            this.updateVRControllers();
            this.render3D(0);
         });
      });
      this.#renderer.vr.enabled = true;

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

   /** @summary Exit VR mode
     * @private */
   exitVRMode() {
      if (!this.#vrDisplay.isPresenting)
         return;
      this.#renderer.vr.enabled = false;
      this.#dolly.remove(this.#camera);
      this.#scene.add(this.#camera);
      // Restore Camera pose
      this.#camera.position.copy(this.#vr_camera_position);
      this.#vr_camera_position = undefined;
      this.#camera.rotation.copy(this.#vr_camera_rotation);
      this.#vr_camera_rotation = undefined;
      this.#camera.near = this.#vr_camera_near;
      this.#camera.updateProjectionMatrix();
      this.#vrDisplay.exitPresent();
   }

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

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

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

      let regexp, exact = false;

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

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

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

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

      const res = this.ctrl, macro = opt.indexOf('macro:');

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

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

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

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

         this.modifyVisisbility(name, sign);
      }

      const d = new DrawOptions(opt);

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

      if (d.check('DUMMY'))
         res.dummy = true;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      // let reuse for storing origin options
      this.setOptions(res, true);
   }

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

      const h = this.getHPainter();

      if (h) {
         // show browser if it not visible

         h.activateItems(names, force);

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

   /** @summary  Return ClonedNodes instance from the painter */
   getClones() { return this.#clones; }

   /** @summary Assign clones, created outside.
    * @desc Used by ROOT geometry painter, where clones are handled by the server */
   assignClones(clones, owner = true) {
      if (this.#clones_owner)
         this.#clones?.cleanup(this.#draw_nodes, this.#build_shapes);
      this.#draw_nodes = undefined;
      this.#build_shapes = undefined;
      this.#drawing_ready = false;

      this.#clones = clones;
      this.#clones_owner = owner;
   }

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

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

            totalcnt++;

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

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

            totalmax = Math.max(max, totalmax);

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

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

            errcnt++;

            return false;
         }
      }, tm1 = new Date().getTime();

      this.#clones.scanVisible(arg);

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

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

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

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

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

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

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

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

      menu.sub('Highlight');

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

      menu.separator();

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

      menu.endsub();

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

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

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

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

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

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

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

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

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

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

      this.render3D();
   }

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

      this.render3D(-1);
   }

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

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

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

      this.render3D();
   }

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

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

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

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

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

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

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

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

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

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

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

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

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

               mesh.geometry.computeBoundingBox();

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

                  mesh.trans[i] = item;

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

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

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

            const mm = new THREE.Matrix4();

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

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

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

      this.#toplevel.updateMatrixWorld();

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

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

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

      this.drawAxesAndOverlay();
   }

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

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

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

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

      this.ctrl.show_controls = on;

      if (this.#gui) {
         if (!on) {
            this.#gui.destroy();
            this.#gui = undefined;
         }
         return;
      }

      if (!on || !this.#renderer)
         return;

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

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

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

      this.#gui.painter = this;

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

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

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

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

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

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

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

         const clipFolder = this.#gui.addFolder('Clipping');

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

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

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

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

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

      // Scene Options

      const scene = this.#gui.addFolder('Scene');
      // following items used in handlers and cannot be constants
      let light_pnts = null, strength = null, hcolor = null, overlay = null;

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

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

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

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

      // Appearance Options

      const appearance = this.#gui.addFolder('Appearance');

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

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

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

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

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

      // Material options

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

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

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

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

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

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

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

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

      addMaterialProp();


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

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

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

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

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

      // Advanced Options
      if (this.#webgl) {
         const advanced = this.#gui.addFolder('Advanced');

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

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

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

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

         transform.add(this, 'resetTransformation').name('Reset');
         if (this.ctrl.trans_z || this.ctrl.trans_radial)
            transform.open();
      }
   }

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

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

      this.ensureBloom();

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

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

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

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

      this.render3D();
   }

   /** @summary Handle change of camera kind */
   changeCamera() {
      // force control recreation
      if (this.#controls) {
         this.#controls.cleanup();
         this.#controls = undefined;
      }

      this.ensureBloom(false);

      // recreate camera
      this.createCamera();

      this.createSpecialEffects();

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

      // this.drawOverlay();

      this.addOrbitControls();

      this.render3D();

      // this.#scene_size = undefined; // ensure reassign of camera position

      // this.#first_drawing = true;
      // this.startDrawGeometry(true);
   }

   /** @summary create bloom effect */
   ensureBloom(on) {
      if (on === undefined) {
         if (this.ctrl.highlight_bloom === 0)
            this.ctrl.highlight_bloom = this.#webgl && !browser.android;

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

      if (on && !this.#bloomComposer) {
         this.#camera.layers.enable(_BLOOM_SCENE);
         this.#bloomComposer = new THREE.EffectComposer(this.#renderer);
         this.#bloomComposer.addPass(new THREE.RenderPass(this.#scene, this.#camera));
         const pass = new THREE.UnrealBloomPass(new THREE.Vector2(this.#scene_width, this.#scene_height), 1.5, 0.4, 0.85);
         pass.threshold = 0;
         pass.radius = 0;
         pass.renderToScreen = true;
         this.#bloomComposer.addPass(pass);
         this.#renderer.autoClear = false;
      } else if (!on && this.#bloomComposer) {
         this.#bloomComposer.dispose();
         this.#bloomComposer = undefined;
         if (this.#renderer)
            this.#renderer.autoClear = true;
         this.#camera?.layers.disable(_BLOOM_SCENE);
         this.#camera?.layers.set(_ENTIRE_SCENE);
      }

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


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

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

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

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

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


               cnt++;

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

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

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

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

                  if (many)
                     menu.endsub();

                  continue;
               }

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

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

                     this.#last_hidden?.forEach(obj2 => { obj2.visible = true; });

                     this.#last_hidden = [];

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

                     this.#last_hidden.forEach(obj2 => { obj2.visible = false; });

                     this.#last_manifest = intersects[indx].object.material;

                     this.#last_manifest.wireframe = !this.#last_manifest.wireframe;

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

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

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

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

               if (many)
                  menu.endsub();
            }
         }
         menu.show();
      });
   }

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

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

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

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

         if (obj.jsroot_special)
            unique = false;

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

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

      const clip = this.ctrl.clip;

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

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

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

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

         intersects = clippedIntersects;
      }

      return intersects;
   }

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

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

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

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

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

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

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

      if (!active_mesh.length)
         active_mesh = null;

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

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

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

         const lst = this.#highlight_handlers || this.getCentral()?.getSubordinates().concat([this.getCentral()]) || this.getSubordinates();

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

      const curr_mesh = this.#selected_mesh;
      if (!curr_mesh && !active_mesh)
         return false;

      const get_ctrl = mesh => { return mesh.get_ctrl ? mesh.get_ctrl() : new GeoDrawingControl(mesh, this.ctrl.highlight_bloom && this.#bloomComposer); };

      let same = false;

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

      curr_mesh?.forEach(mesh => get_ctrl(mesh).setHighlight());

      this.#selected_mesh = active_mesh;

      active_mesh?.forEach(mesh => get_ctrl(mesh).setHighlight(color || new THREE.Color(this.ctrl.highlight_color), geo_index));

      this.render3D(0);

      return Boolean(active_mesh);
   }

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

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

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

      ctrl.evnt = evnt;

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

      ctrl.evnt = null;
   }

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

      if (this.#controls)
         this.#controls.mouse_tmout = val;
   }

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

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

   /** @summary Add orbit control */
   addOrbitControls() {
      if (this.#controls || !this.#webgl || this.isBatchMode() || this.#superimpose || isNodeJs())
         return;

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

      this.#controls = createOrbitControl(this, this.#camera, this.#scene, this.#renderer, this.#lookat);

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

      if (!this.canRotateCamera())
         this.#controls.enableRotate = false;

      this.#controls.contextMenu = this.orbitContext.bind(this);

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

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

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

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

            names.push(info);

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

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

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

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

         if (!resolve?.obj)
            return tooltip;

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

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

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

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

         if (this.#last_manifest) {
            this.#last_manifest.wireframe = !this.#last_manifest.wireframe;
            if (this.#last_hidden)
               this.#last_hidden.forEach(obj => { obj.visible = true; });
            this.#last_hidden = undefined;
            this.#last_manifest = undefined;
         } else
            this.adjustCameraPosition(true);

         this.render3D();
      };
   }

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

      if (this.isStage(stageCollect)) {
         if (this.#geom_viewer) {
            this.#draw_all_nodes = false;
            this.changeStage(stageAnalyze);
            return true;
         }

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

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

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

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

            matrix = createProjectionMatrix(this.#camera);

            frustum = createFrustum(matrix);

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

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

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

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

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

         if (!need_worker || !this.#worker_ready) {
            const res = this.#clones.collectVisibles(this.#current_face_limit, frustum);
            this.#new_draw_nodes = res.lst;
            this.#draw_all_nodes = res.complete;
            this.changeStage(stageAnalyze);
            return true;
         }

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

         this.submitToWorker(job);

         this.changeStage(stageWorkerCollect);

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

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

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

         if (this.#new_append_nodes) {
            this.#new_draw_nodes = this.#draw_nodes.concat(this.#new_append_nodes);

            this.#new_append_nodes = undefined;
         } else if (this.#draw_nodes) {
            let del;
            if (this.#geom_viewer)
               del = this.#draw_nodes;
            else
               del = this.#clones.mergeVisibles(this.#new_draw_nodes, this.#draw_nodes);

            // remove should be fast, do it here
            for (let n = 0; n < del.length; ++n)
               this.#clones.createObject3D(del[n].stack, this.#toplevel, kDeleteMesh);

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

         this.#draw_nodes = this.#new_draw_nodes;
         this.#new_draw_nodes = undefined;
         this.changeStage(stageCollShapes);
         return true;
      }

      if (this.isStage(stageCollShapes)) {
         // collect shapes
         const shapes = this.#clones.collectShapes(this.#draw_nodes);

         // merge old and new list with produced shapes
         this.#build_shapes = this.#clones.mergeShapesLists(this.#build_shapes, shapes);

         this.changeStage(stageStartBuild);
         return true;
      }

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

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

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

         this.changeStage(stageBuild);
      }

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

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

            const res = this.#clones.buildShapes(this.#build_shapes, this.#current_face_limit, 500);
            if (res.done) {
               this.ctrl.info.num_shapes = this.#build_shapes.length;
               this.changeStage(stageBuildReady);
            } else {
               this.ctrl.info.num_shapes = res.shapes;
               this.#drawing_log = `Creating: ${res.shapes} / ${this.#build_shapes.length} shapes,  ${res.faces} faces`;
               return true;
            }
         }

         // final stage, create all meshes

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

         if (!this.ctrl.project)
            build_instanced = this.#clones.createInstancedMeshes(this.ctrl, toplevel, this.#draw_nodes, this.#build_shapes, getRootColors());

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

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

               this.createEntryMesh(entry, shape, toplevel);

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

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

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

      if (this.isStage(stageWaitMain)) {
         // wait for main painter to be ready
         if (!this.getCentral()) {
            this.changeStage(stageInit, 'Lost main painter');
            return false;
         }
         if (!this.getCentral().isDrawingReady())
            return 1;

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

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

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

      return false;
   }

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

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

      return true;
   }

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

      // delete old nodes
      if (this.#more_nodes) {
         for (let n = 0; n < this.#more_nodes.length; ++n) {
            const entry = this.#more_nodes[n],
                  obj3d = this.#clones.createObject3D(entry.stack, this.#toplevel, kDeleteMesh);
            disposeThreejsObject(obj3d);
            cleanupShape(entry.server_shape);
            delete entry.server_shape;
         }
         this.#more_nodes = undefined;
      }

      if (!nodes)
         return;

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

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

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

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

   /** @summary Returns hierarchy of 3D objects used to produce projection.
     * @desc Typically external master painter is used, but also internal data can be used */
   getProjectionSource() {
      if (this.#clones_owner)
         return this.#fullgeom_proj;
      if (!this.getCentral()?.isDrawingReady()) {
         console.warn('MAIN PAINTER NOT READY WHEN DO PROJECTION');
         return null;
      }
      return this.getCentral().#toplevel;
   }

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

      const origin = this.#custom_bounding_box.clone();
      this.#custom_bounding_box.union(box);

      if (!this.#custom_bounding_box.equals(origin))
         this.#adjust_camera_with_render = true;
   }

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

      box3.makeEmpty();

      if (this.#custom_bounding_box && (topitem === this.#toplevel)) {
         box3.union(this.#custom_bounding_box);
         return box3;
      }

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

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

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

      return box3;
   }

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

      if (!toplevel)
         return false;

      disposeThreejsObject(this.#toplevel, true);

      // let axis = this.ctrl.project;

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

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

         this.ctrl.projectPos = mean;
      }

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

         const geom2 = projectGeometry(mesh.geometry, mesh.parent.absMatrix || mesh.parent.matrixWorld, this.ctrl.project, this.ctrl.projectPos, mesh._flippedMesh);
         if (geom2) {
            const mesh2 = new THREE.Mesh(geom2, mesh.material.clone());
            this.#toplevel.add(mesh2);
            mesh2.stack = mesh.stack;
         }
      });

      return true;
   }

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

      const need_render = !box;

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

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

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

         this.#camera._lights = this.ctrl.light.kind;

         switch (this.#camera._lights) {
            case 'ambient':
               this.#camera.add(new THREE.AmbientLight(0xefefef, p));
               break;
            case 'hemisphere':
               this.#camera.add(new THREE.HemisphereLight(0xffffbb, 0x080820, p));
               break;
            case 'mix':
               this.#camera.add(new THREE.AmbientLight(0xefefef, p));
            // eslint-disable-next-line  no-fallthrough
            default: // 6 point lights
               for (let n = 0; n < 6; ++n) {
                  const l = new THREE.DirectionalLight(0xefefef, p);
                  this.#camera.add(l);
                  l._id = n;
               }
         }
      }

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

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

      if (need_render)
         this.render3D();
   }

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

   /** @summary Create configured camera */
   createCamera() {
      if (this.#camera) {
         this.#scene.remove(this.#camera);
         disposeThreejsObject(this.#camera);
         this.#camera = undefined;
      }

      if (this.isOrthoCamera())
         this.#camera = new THREE.OrthographicCamera(-this.#scene_width / 2, this.#scene_width / 2, this.#scene_height / 2, -this.#scene_height / 2, 1, 10000);
      else {
         this.#camera = new THREE.PerspectiveCamera(25, this.#scene_width / this.#scene_height, 1, 10000);
         this.#camera.up = this.ctrl._yup ? new THREE.Vector3(0, 1, 0) : new THREE.Vector3(0, 0, 1);
      }

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

      this.#scene.add(this.#camera);
   }

   /** @summary Create special effects */
   createSpecialEffects() {
      if (this.#webgl && this.ctrl.outline && isFunc(this.createOutline)) {
         // code used with jsroot-based geometry drawing in EVE7, not important any longer
         this.#effectComposer = new THREE.EffectComposer(this.#renderer);
         this.#effectComposer.addPass(new THREE.RenderPass(this.#scene, this.#camera));
         this.createOutline(this.#scene, this.#camera, this.#scene_width, this.#scene_height);
      }

      this.ensureBloom();
   }

   /** @summary Return current effect composer */
   getEffectComposer() { return this.#effectComposer; }

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

         if (cfg?.renderer) {
            this.#scene = cfg.scene;
            this.#scene_width = cfg.scene_width;
            this.#scene_height = cfg.scene_height;
            this.#renderer = cfg.renderer;
            this.#webgl = (this.#renderer.jsroot_render3d === constants$1.Render3D.WebGL);

            this.#toplevel = new THREE.Object3D();
            this.#scene.add(this.#toplevel);

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

            this.#camera = cfg.camera;
         }

         return this.#renderer?.jsroot_dom;
      }

      return importThreeJs().then(() => {
         // three.js 3D drawing
         this.#scene = new THREE.Scene();
         this.#fog = new THREE.Fog(0xffffff, 1, 10000);
         this.#scene.fog = this.ctrl.use_fog ? this.#fog : null;

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

         this.#scene_width = w;
         this.#scene_height = h;

         this.createCamera();

         this.#selected_mesh = null;

         this.#overall_size = 10;

         this.#toplevel = new THREE.Object3D();

         this.#scene.add(this.#toplevel);

         this.#scene.background = new THREE.Color(this.ctrl.background);

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

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

         this.#webgl = (r.jsroot_render3d === constants$1.Render3D.WebGL);

         if (isFunc(r.setPixelRatio) && !isNodeJs() && !browser.android)
            r.setPixelRatio(window.devicePixelRatio);
         r.setSize(w, h, !this.#fit_main_area);
         r.localClippingEnabled = true;

         r.setClearColor(this.#scene.background, 1);

         if (this.#fit_main_area && this.#webgl) {
            r.domElement.style.width = '100%';
            r.domElement.style.height = '100%';
            const main = this.selectDom();
            if (main.style('position') === 'static')
               main.style('position', 'relative');
         }

         this.#animating = false;

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

         if (this.#fit_main_area && !this.#webgl) {
            // create top-most SVG for geometry drawings
            const doc = getDocument(),
                  svg = doc.createElementNS(nsSVG, 'svg');
            svg.setAttribute('width', w);
            svg.setAttribute('height', h);
            svg.appendChild(this.#renderer.jsroot_dom);
            return svg;
         }

         return this.#renderer.jsroot_dom;
      });
   }

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

      if (this.#clones_owner)
         this.#clones?.setDefaultColors(this.ctrl.dflt_colors);

      this.#last_render_tm = this.#start_render_tm = new Date().getTime();
      this.#last_render_meshes = 0;
      this.changeStage(stageCollect);
      this.#drawing_ready = false;
      this.ctrl.info.num_meshes = 0;
      this.ctrl.info.num_faces = 0;
      this.ctrl.info.num_shapes = 0;
      this.#selected_mesh = null;

      if (this.ctrl.project) {
         if (this.#clones_owner) {
            if (this.#fullgeom_proj)
               this.changeStage(stageBuildProj);
            else
               this.#fullgeom_proj = new THREE.Object3D();
         } else
            this.changeStage(stageWaitMain);
      }

      this.#last_manifest = undefined;
      this.#last_hidden = undefined; // clear list of hidden objects

      this.#draw_nodes_again = undefined; // forget about such flag

      this.continueDraw();
   }

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

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

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

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

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

      return this.#overall_size;
   }

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

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

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

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

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

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

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

      if (!this.#lookat || !this.#camera0pos)
         return '';

      const pos1 = new THREE.Vector3().add(this.#camera0pos).sub(this.#lookat),
            pos2 = new THREE.Vector3().add(this.#camera.position).sub(this.#lookat),
            zoom = Math.min(10000, Math.max(1, this.ctrl.zoom * pos2.length() / pos1.length() * 100));

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

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

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

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

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

   /** @summary Calculates current zoom factor */
   calculateZoom() {
      if (this.#camera0pos && this.#camera && this.#lookat) {
         const pos1 = new THREE.Vector3().add(this.#camera0pos).sub(this.#lookat),
               pos2 = new THREE.Vector3().add(this.#camera.position).sub(this.#lookat);
         return pos2.length() / pos1.length();
      }

      return 0;
   }

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

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

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

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

      const sizex = box.max.x - box.min.x,
            sizey = box.max.y - box.min.y,
            sizez = box.max.z - box.min.z,
            midx = (box.max.x + box.min.x) / 2,
            midy = (box.max.y + box.min.y) / 2,
            midz = (box.max.z + box.min.z) / 2,
            more = this.ctrl._axis || (this.ctrl.camera_overlay === 'bar') ? 0.2 : 0.1;

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

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

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

      this.#camera.near = this.#overall_size / 350;
      this.#camera.far = this.#overall_size * 100;
      this.#fog.near = this.#overall_size * 0.5;
      this.#fog.far = this.#overall_size * 5;

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

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

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

      this.#lookat = new THREE.Vector3(midx, midy, midz);
      this.#camera0pos = new THREE.Vector3(-2 * max_all, 0, 0); // virtual 0 position, where rotation starts

      this.#camera.updateMatrixWorld();
      this.#camera.updateProjectionMatrix();

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

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

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

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

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

      if (this.#camera.isOrthographicCamera && this.isOrthoCamera() && this.#scene_width && this.#scene_height) {
         const screen_ratio = this.#scene_width / this.#scene_height,
               szx = this.#camera.right - this.#camera.left, szy = this.#camera.top - this.#camera.bottom;

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

      this.#camera.lookAt(this.#lookat);
      this.#camera.updateProjectionMatrix();

      this.changedLight(box);

      if (this.#controls) {
         this.#controls.target.copy(this.#lookat);
         if (!only_set)
            this.#controls.update();
      }

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

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

      this.adjustCameraPosition(false, preserve_zoom);
   }

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

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

      const stack = this.#clones.findStackByName(itemname);

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

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

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

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

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

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

      autoClip = autoClip && this.#webgl;

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

      this.#animating = true;

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

         if (this.#animating)
            requestAnimationFrame(animate);
         else if (!this.#geom_viewer)
            this.startDrawGeometry();
         const smoothFactor = -Math.cos((2.0 * Math.PI * step) / frames) + 1.0;
         this.#camera.position.add(posIncrement.clone().multiplyScalar(smoothFactor));
         oldTarget.add(targetIncrement.clone().multiplyScalar(smoothFactor));
         this.#lookat = oldTarget;
         this.#camera.lookAt(this.#lookat);
         this.#camera.updateProjectionMatrix();

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

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

      animate();

   //   this.#controls.update();
   }

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

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

         const current = new Date();

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

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

      if (this.#webgl)
         animate();
   }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

         let funcname = 'extract_geo_tracks';

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

         const func = findFunction(funcname);

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

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

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

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

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

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

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

      const obj = hitem._obj;

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

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

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

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

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

      this.#extra_objects.Add(obj, itemname);

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

      return true;
   }

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

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

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

      if (indx < 0)
         return;

      const obj = this.#extra_objects.arr[indx];
      let res = Boolean(obj.$hidden_via_menu);

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

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

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

      return res;
   }

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

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


      let promise = false;

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

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

         this.updateClipping(true);

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

         return not_wait_render ? this : pr;
      });
   }

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

      if (!name)
         name = 'tracks';

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

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

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

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

      return extras;
   }

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

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

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

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

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

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

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

      this.addToExtrasContainer(line);

      return true;
   }

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

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

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

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

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

      this.addToExtrasContainer(line3d);

      return true;
   }

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

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

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

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

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

      this.addToExtrasContainer(line);

      return true;
   }

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

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

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

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

   /** @summary Draw extra shape on the geometry */
   drawExtraShape(obj, itemname) {
      // eslint-disable-next-line no-use-before-define
      const mesh = build(obj);
      if (!mesh)
         return false;

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

      this.addToExtrasContainer(mesh);
      return true;
   }

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

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

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

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

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

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

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

      return res;
   }

   /** @summary Search for created shape for nodeid
    * @desc Used in ROOT geometry painter */
   findNodeShape(nodeid) {
      if ((nodeid !== undefined) && !this.#draw_nodes) {
         for (let k = 0; k < this.#draw_nodes.length; ++k) {
            const item = this.geo_painter._draw_nodes[k];
            if ((item.nodeid === nodeid) && item.server_shape)
               return item.server_shape;
         }
      }
   }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      return true;
   }

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

      if (name_prefix === '__geom_viewer_append__') {
         this.#new_append_nodes = draw_obj;
         this.ctrl.use_worker = 0;
         this.setGeomViewer(true); // indicate that working with geom viewer
      } else if ((name_prefix === '__geom_viewer_selection__') && this.#clones) {
         // these are selection done from geom viewer
         this.#new_draw_nodes = draw_obj;
         this.ctrl.use_worker = 0;
         this.setGeomViewer(true); // indicate that working with geom viewer
      } else if (this.getCentral())
         this.assignClones(this.getCentral().getClones(), false);
      else if (!draw_obj)
         this.assignClones(undefined, undefined);
      else {
         this.#start_drawing_time = new Date().getTime();
         this.assignClones(new ClonedNodes(draw_obj), true);
         let lvl = this.ctrl.vislevel, maxnodes = this.ctrl.maxnodes;
         if (this.#geo_manager) {
            if (!lvl && this.#geo_manager.fVisLevel)
               lvl = this.#geo_manager.fVisLevel;
            if (!maxnodes)
               maxnodes = this.#geo_manager.fMaxVisNodes;
         }

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

         this.#clones.name_prefix = name_prefix;

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

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

         this.#clones.produceIdShifts();

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

         if (!this.#scene && (settings.Debug || spent > 1000))
            console.log(`Creating clones ${this.#clones.nodes.length} takes ${spent} ms uniquevis ${uniquevis}`);

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

      let promise = Promise.resolve(true);

      if (!this.#scene) {
         this.#first_drawing = true;

         const pp = this.getPadPainter();

         this.#on_pad = Boolean(pp);

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

               this.batch_mode = pp.isBatchMode();

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

               size = fp.getSizeFor3d(undefined, render3d);

               this.#fit_main_area = (size.can3d === -1);

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

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

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

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

            assign3DHandler(this);

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

            this.#fit_main_area = (size.can3d === -1);

            promise = this.createScene(size.width, size.height, render3d)
                          .then(dom2 => this.add3dCanvas(size, dom2, this.#webgl));
         }
      }

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

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

         this.createToolbar();

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

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

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

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

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

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

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

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

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

         now = new Date().getTime();

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

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

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

            this.showDrawInfo(this.#drawing_log);

            if (this.#first_drawing && this.#webgl && (this.ctrl.info.num_meshes - this.#last_render_meshes > 100) && (now - this.#last_render_tm > 2.5 * interval)) {
               this.adjustCameraPosition();
               this.render3D(-1);
               this.#last_render_meshes = this.ctrl.info.num_meshes;
            }
            if (res !== 2)
               setTimeout(() => this.continueDraw(), (res === 1) ? 100 : 1);

            return;
         }
      }

      const take_time = now - this.#start_render_tm;

      if ((this.#first_drawing || this.#full_redrawing) && (settings.Debug || take_time > 1000))
         console.log(`Create tm = ${take_time} meshes ${this.ctrl.info.num_meshes} faces ${this.ctrl.info.num_faces}`);

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

      this.completeDraw(true);
   }

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

      this.drawOverlay();

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

      this.#last_camera_position = origin; // remember current camera position

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

      if (!this.ctrl.project)
         produceRenderOrder(this.#toplevel, origin, this.ctrl.depthMethod, this.#clones);
   }

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

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

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

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

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

      if (this.#render_tmout) {
         clearTimeout(this.#render_tmout);
         this.#render_tmout = undefined;
      }

      beforeRender3D(this.#renderer);

      const tm1 = new Date();

      if (this.#adjust_camera_with_render) {
         this.adjustCameraPosition('only_set');
         this.#adjust_camera_with_render = undefined;
      }

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

      // its needed for outlinePass - do rendering, most consuming time
      if (this.#webgl && this.#effectComposer?.passes)
         this.#effectComposer.render();
      else if (this.#webgl && this.#bloomComposer?.passes) {
         this.#renderer.clear();
         this.#camera.layers.set(_BLOOM_SCENE);
         this.#bloomComposer.render();
         this.#renderer.clearDepth();
         this.#camera.layers.set(_ENTIRE_SCENE);
         this.#renderer.render(this.#scene, this.#camera);
      } else
         this.#renderer.render(this.#scene, this.#camera);

      const tm2 = new Date();

      this.#last_render_tm = tm2.getTime();

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

      afterRender3D(this.#renderer);

      const arr = this.#render_resolveFuncs;
      this.#render_resolveFuncs = [];
      arr?.forEach(func => func(this));
   }

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

      this.#worker_ready = false;
      this.#worker_jobs = 0; // counter how many requests send to worker

      let pr;

      if (isNodeJs()) {
         pr = Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(h => {
            const wrk = new h.Worker(exports.source_dir.slice(7) + 'modules/geom/nodeworker.mjs', { type: 'module' });
            wrk.on('message', msg => this.processWorkerReply(msg));
            return wrk;
         });
      } else {
         // Finally use ES6 module, see https://www.codedread.com/blog/archives/2017/10/19/web-workers-can-be-es6-modules-too/
         const wrk = new Worker(exports.source_dir + 'modules/geom/geoworker.mjs', { type: 'module' });
         wrk.onmessage = e => this.processWorkerReply(e?.data);
         pr = Promise.resolve(wrk);
      }

      return pr.then(wrk => {
         this.#worker = wrk;
         // send initialization message with clones
         wrk.postMessage({
            init: true,   // indicate init command for worker
            tm0: new Date().getTime(),
            vislevel: this.#clones.getVisLevel(),
            maxvisnodes: this.#clones.getMaxVisNodes(),
            clones: this.#clones.nodes,
            sortmap: this.#clones.sortmap
         });
      });
   }

   /** @summary check if one can submit request to worker
     * @private */
   canSubmitToWorker(force) {
      return this.#worker ? this.#worker_ready && ((this.#worker_jobs === 0) || force) : false;
   }

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

      this.#worker_jobs++;
      job.tm0 = new Date().getTime();
      this.#worker.postMessage(job);
   }

   /** @summary process reply from worker
     * @private */
   processWorkerReply(job) {
      if (!job || !isObject(job))
         return;

      if (job.log)
         return console.log(`geo: ${job.log}`);

      if (job.progress)
         return showProgress(job.progress);

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

      if (job.init) {
         this.#worker_ready = true;
         return;
      }

      this.#worker_jobs--;

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

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

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

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

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

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

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

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

   /** @summary start draw geometries on central and all subordinate painters
     * @private */
   testGeomChanges() {
      if (this.getCentral()) {
         console.warn('Get testGeomChanges call for subordinate painter');
         return this.getCentral().testGeomChanges();
      }
      this.startDrawGeometry();
      this.getSubordinates()?.forEach(p => p.startDrawGeometry());
   }

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

      if (!res1 && !res2)
         return norender ? null : this.render3D();

      return this.changedDepthMethod(norender ? 'norender' : undefined);
   }

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

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

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

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

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

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

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

         if (isFunc(this.#camera.orthoRotation))
            this.#camera.orthoRotation(text3d);

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

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

         let ox1 = xmin * 0.15 + xmax * 0.85,
             ox2 = xmin * 0.05 + xmax * 0.95;
         const oy1 = ymax * 0.9 + ymin * 0.1,
               oy2 = ymax * 0.86 + ymin * 0.14,
               ticks = x_handle.createTicks();

         if (ticks.major?.length > 1) {
            ox1 = ticks.major.at(-2);
            ox2 = ticks.major.at(-1);
         }

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

         addPoint(ox1, oy1, midZ);
         addPoint(ox1, oy2, midZ);

         addPoint(ox1, (oy1 + oy2) / 2, midZ);
         addPoint(ox2, (oy1 + oy2) / 2, midZ);

         addPoint(ox2, oy1, midZ);
         addPoint(ox2, oy2, midZ);

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

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

         const text3d = createText(x_handle.format(ox2 - ox1, true), Math.abs(oy2 - oy1));

         container.add(createTextMesh(text3d, textMaterial, (ox2 + ox1) / 2, (oy1 + oy2) / 2 + text3d._height * 0.8, midZ));
         return true;
      }

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

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

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

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

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

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

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

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

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

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

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

         const yticks = y_handle.createTicks();

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

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

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

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

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

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

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

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

         return true;
      }

      return false;
   }

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

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

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

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

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

         const buf = new Float32Array(6),
               color = colors[naxis],
               name = names[naxis],
               valueToString = val => {
                  if (!val)
                     return '0';
                  const lg = Math.log10(Math.abs(val));
                  if (lg < 0) {
                     if (lg > -1)
                        return val.toFixed(2);
                     if (lg > -2)
                        return val.toFixed(3);
                  } else {
                     if (lg < 2)
                        return val.toFixed(1);
                     if (lg < 4)
                        return val.toFixed(0);
                  }
                  return val.toExponential(2);
               },
               lbl = valueToString(box.max[name]) + ' ' + labels[naxis];

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

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

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

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

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

         mesh._no_clip = true; // skip from clipping

         container.add(mesh);

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

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

         let text3d = createTextGeometry(lbl, text_size);
         mesh = new THREE.Mesh(text3d, textMaterial);
         mesh._no_clip = true; // skip from clipping

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

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

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

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

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

         mesh._axis_name = name;

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

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

         container.add(mesh);

         text3d = createTextGeometry(valueToString(box.min[name]), text_size);

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

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

         mesh._axis_name = name;

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

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

         container.add(mesh);
      }

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

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

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

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

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

   /** @summary Specify showtop draw options, relevant only for TGeoManager */
   setShowTop(on) {
      this.ctrl.showtop = Boolean(on);
      return this.startRedraw();
   }

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

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

      this.render3D(0);
   }

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

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

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

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

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

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

      if (this.#last_clip_cfg !== clip_cfg)
         changed = true;

      this.#last_clip_cfg = clip_cfg;

      const any_clipping = Boolean(panels), ci = this.ctrl.clipIntersect,
            material_side = any_clipping ? THREE.DoubleSide : THREE.FrontSide;

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

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

      this.ctrl.doubleside = any_clipping;

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

      return changed;
   }

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

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

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

      let promise;

      if (!this.#clones) {
         check_extras = false;
         // if extra object where append, redraw them at the end
         this.getExtrasContainer('delete'); // delete old container
         promise = this.drawExtras(this.getCentral()?.getExtraObjects() || this.getExtraObjects(), '', false);
      } else if ((this.#first_drawing || this.#full_redrawing) && this.ctrl.tracks && this.#geo_manager)
         promise = this.drawExtras(this.#geo_manager.fTracks, '<prnt>/Tracks');
      else
         promise = Promise.resolve(true);

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

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

         if (first_time)
            this.completeScene();

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

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

         if (this.#provided_more_nodes !== undefined) {
            this.appendMoreNodes(this.#provided_more_nodes, true);
            this.#provided_more_nodes = undefined;
         }

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

         this.render3D(0, true);

         if (close_progress)
            showProgress();

         this.addOrbitControls();

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

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

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

         this.setAsMainPainter();

         const arr = this.#draw_resolveFuncs;
         this.#draw_resolveFuncs = [];
         arr?.forEach(func => func(this));

         if (isFunc(this.#complete_handler))
            this.#complete_handler(this);

         if (this.#draw_nodes_again)
            this.startDrawGeometry(); // relaunch drawing
         else
            this.#drawing_ready = true; // indicate that drawing is completed

         return this;
      });
   }

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

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

      const new_nodes = [];

      for (let n = 0; n < this.#draw_nodes.length; ++n) {
         const entry = this.#draw_nodes[n];
         if ((entry.nodeid === nodeid) || this.#clones.isIdInStack(nodeid, entry.stack))
            this.#clones.createObject3D(entry.stack, this.#toplevel, kDeleteMesh);
         else
            new_nodes.push(entry);
      }

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

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

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

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

            disposeThreejsObject(this.#scene);
         }

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

         disposeThreejsObject(this.#fullgeom_proj);

         this.#controls?.cleanup();

         this.#gui?.destroy();

         this.#worker?.terminate();

         this.#animating = undefined;

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

         this.#central_painter?.assignSubordinate(this, false);
         this.#subordinate_painters?.forEach(p => {
            p.assignCentral(this, false);
            if (p.getClones() === this.getClones())
               p.assignClones(undefined, undefined);
         });

         this.#geo_manager = undefined;
         this.#highlight_handlers = undefined;

         super.cleanup();

         delete this.ctrl;

         this.#did_cleanup = true;

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

      this.#central_painter = undefined;
      this.#subordinate_painters = [];

      const arr = this.#render_resolveFuncs;
      this.#render_resolveFuncs = [];
      arr?.forEach(func => func(this));

      if (!this.#superimpose)
         cleanupRender3D(this.#renderer);

      this.ensureBloom(false);
      this.#effectComposer = undefined;

      this.#scene = undefined;
      this.#scene_size = undefined;
      this.#scene_width = 0;
      this.#scene_height = 0;
      this.#renderer = null;
      this.#toplevel = null;
      this.#fullgeom_proj = undefined;
      this.#fog = undefined;
      this.#camera = undefined;
      this.#camera0pos = undefined;
      this.#lookat = undefined;
      this.#selected_mesh = undefined;
      this.#custom_bounding_box = undefined;

      this.assignClones(undefined, undefined);
      this.#new_draw_nodes = undefined;
      this.#new_append_nodes = undefined;
      this.#last_camera_position = undefined;

      this.#on_pad = undefined;

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

      this.changeStage(stageInit, 'cleanup');
      this.#drawing_log = undefined;

      this.#gui = undefined;
      this.#controls = undefined;
      this.#toolbar = undefined;

      this.#worker = undefined;
   }

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

      this.#scene_width = width;
      this.#scene_height = height;

      if (this.#camera && this.#renderer) {
         if (this.#camera.isPerspectiveCamera)
            this.#camera.aspect = this.#scene_width / this.#scene_height;
         else if (this.#camera.isOrthographicCamera)
            this.adjustCameraPosition(true, true);
         this.#camera.updateProjectionMatrix();
         this.#renderer.setSize(this.#scene_width, this.#scene_height, !this.#fit_main_area);
         this.#effectComposer?.setSize(this.#scene_width, this.#scene_height);
         this.#bloomComposer?.setSize(this.#scene_width, this.#scene_height);

         if (this.isStage(stageInit))
            this.render3D();
      }

      return true;
   }

   /** @summary Check if HTML element was resized and drawing need to be adjusted */
   checkResize(arg) {
      const cp = this.getCanvPainter();

      // firefox is the only browser which correctly supports resize of embedded canvas,
      // for others we should force canvas redrawing at every step
      if (cp && !cp.checkCanvasResize(arg))
         return false;

      const sz = this.getSizeFor3d();
      return this.performResize(sz.width, sz.height);
   }

   /** @summary Toggle enlarge state */
   toggleEnlarge() {
      if (this.enlargeMain('toggle'))
         this.checkResize();
   }

   /** @summary either change mesh wireframe or return current value
     * @return undefined when wireframe cannot be accessed
     * @private */
   accessObjectWireFrame(obj, on) {
      if (!obj?.material)
         return;

      if ((on !== undefined) && obj.stack)
         obj.material.wireframe = on;

      return obj.material.wireframe;
   }

   /** @summary handle wireframe flag change in GUI
     * @private */
   changedWireFrame() {
      this.#scene?.traverse(obj => this.accessObjectWireFrame(obj, this.ctrl.wireframe));

      this.render3D();
   }

   /** @summary Update object in geo painter */
   updateObject(obj) {
      if ((obj === 'same') || !obj?._typename)
         return false;
      if (obj === this.getObject())
         return true;

      let gm;
      if (obj._typename === clTGeoManager) {
         gm = obj;
         obj = obj.fMasterVolume;
      }

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

      if (this.#geo_manager && gm) {
         this.#geo_manager = gm;
         this.assignObject(obj);
         this.#did_update = true;
         return true;
      }

      if (!this.matchObjectType(obj._typename))
         return false;

      this.assignObject(obj);
      this.#did_update = true;
      return true;
   }

   /** @summary Cleanup TGeo drawings */
   clearDrawings() {
      this.assignClones(undefined, undefined);

      this.#extra_objects = undefined;
      this.#last_clip_cfg = undefined;

      // only remove all childs from top level object
      disposeThreejsObject(this.#toplevel, true);

      this.#full_redrawing = true;
   }

   /** @summary Redraw TGeo object inside TPad */
   redraw() {
      if (this.#superimpose) {
         const cfg = getHistPainter3DCfg(this.getMainPainter());

         if (cfg) {
            this.#toplevel.scale.set(cfg.scale_x ?? 1, cfg.scale_y ?? 1, cfg.scale_z ?? 1);
            this.#toplevel.position.set(cfg.offset_x ?? 0, cfg.offset_y ?? 0, cfg.offset_z ?? 0);
            this.#toplevel.updateMatrix();
            this.#toplevel.updateMatrixWorld();
         }
      }

      if (this.#did_update)
         return this.startRedraw();

      const fp = this.#on_pad ? this.getFramePainter() : null;
      if (!fp)
         return Promise.resolve(false);
      const sz = fp.getSizeFor3d(fp.access3dKind());
      fp.apply3dSize(sz);
      return this.performResize(sz.width, sz.height);
   }

   /** @summary Redraw TGeo object */
   redrawObject(obj, opt) {
      if (!this.updateObject(obj, opt))
         return false;

      return this.startRedraw();
   }

   /** @summary Start geometry redraw */
   startRedraw(tmout) {
      if (this.#redraw_timer) {
         clearTimeout(this.#redraw_timer);
         this.#redraw_timer = undefined;
      }

      if (tmout) {
         this.#redraw_timer = setTimeout(() => this.startRedraw(), tmout);
         return;
      }

      this.#did_update = undefined;

      this.clearDrawings();
      const draw_obj = this.getGeometry(),
            name_prefix = this.#geo_manager ? draw_obj.fName : '';
      return this.prepareObjectDraw(draw_obj, name_prefix);
   }

   /** @summary Build three.js object */
   static build3d(obj, sopt) {
      if (!obj)
         return null;

      let opt = sopt || {};
      if (isStr(sopt)) {
         const painter = new TGeoPainter(null, obj);
         painter.decodeOptions(sopt);
         opt = painter.ctrl;
         opt.numfaces = opt.maxfaces;
         opt.numnodes = opt.maxnodes;
      }

      if (!opt.numfaces)
         opt.numfaces = 100000;
      if (!opt.numnodes)
         opt.numnodes = 1000;
      if (!opt.frustum)
         opt.frustum = null;

      opt.res_mesh = opt.res_faces = 0;

      if (opt.instancing === undefined)
         opt.instancing = -1;

      opt.info = { num_meshes: 0, num_faces: 0 };

      let clones, visibles;

      if (obj.visibles && obj.nodes && obj.numnodes) {
         // case of draw message from geometry viewer

         const nodes = obj.numnodes > 1e6 ? { length: obj.numnodes } : new Array(obj.numnodes);

         obj.nodes.forEach(node => {
            nodes[node.id] = ClonedNodes.formatServerElement(node);
         });

         clones = new ClonedNodes(null, nodes);
         clones.name_prefix = clones.getNodeName(0);

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

         const nsegm = obj.cfg?.nsegm || 30;

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

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

         visibles = obj.visibles;
      } else {
         let shape = null, hide_top = false;

         if (('fShapeBits' in obj) && ('fShapeId' in obj)) {
            shape = obj;
            obj = null;
         } else if ((obj._typename === clTGeoVolumeAssembly) || (obj._typename === clTGeoVolume))
            shape = obj.fShape;
         else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract))
            shape = obj.fShape;
         else if (obj._typename === clTGeoManager) {
            obj = obj.fMasterVolume;
            hide_top = !opt.showtop;
            shape = obj.fShape;
         } else if (obj.fVolume)
            shape = obj.fVolume.fShape;
         else
            obj = null;

         if (opt.composite && shape && (shape._typename === clTGeoCompositeShape) && shape.fNode)
            obj = buildCompositeVolume(shape);

         if (!obj && shape)
            obj = Object.assign(create$1(clTNamed), { _typename: clTEveGeoShapeExtract, fTrans: null, fShape: shape, fRGBA: [0, 1, 0, 1], fElements: null, fRnrSelf: true });

         if (!obj)
            return null;

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

         clones = new ClonedNodes(obj);
         clones.setVisLevel(opt.vislevel);
         clones.setMaxVisNodes(opt.numnodes);

         if (opt.dflt_colors)
            clones.setDefaultColors(true);

         const uniquevis = opt.no_screen ? 0 : clones.markVisibles(true);
         if (uniquevis <= 0)
            clones.markVisibles(false, false, hide_top);
         else
            clones.markVisibles(true, true, hide_top); // copy bits once and use normal visibility bits

         clones.produceIdShifts();

         // collect visible nodes
         const res = clones.collectVisibles(opt.numfaces, opt.frustum);

         visibles = res.lst;
      }

      if (!opt.material_kind)
         opt.material_kind = 'lambert';
      if (opt.set_names === undefined)
         opt.set_names = true;

      clones.setConfig(opt);

      // collect shapes
      const shapes = clones.collectShapes(visibles);

      clones.buildShapes(shapes, opt.numfaces);

      const toplevel = new THREE.Object3D();
      toplevel.clones = clones; // keep reference on JSROOT data

      const colors = getRootColors();

      if (clones.createInstancedMeshes(opt, toplevel, visibles, shapes, colors))
         return toplevel;

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

         const shape = entry.server_shape || shapes[entry.shapeid];
         if (!shape.ready) {
            console.warn('shape marked as not ready when it should');
            break;
         }

         clones.createEntryMesh(opt, toplevel, entry, shape, colors);
      }

      return toplevel;
   }

   /** @summary draw TGeo object */
   static async draw(dom, obj, opt) {
      if (!obj)
         return null;

      let shape = null, extras = null, extras_path = '', is_eve = false;

      if (('fShapeBits' in obj) && ('fShapeId' in obj)) {
         shape = obj;
         obj = null;
      } else if ((obj._typename === clTGeoVolumeAssembly) || (obj._typename === clTGeoVolume))
         shape = obj.fShape;
      else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) {
         shape = obj.fShape;
         is_eve = true;
      } else if (obj._typename === clTGeoManager)
         shape = obj.fMasterVolume.fShape;
      else if (obj._typename === clTGeoOverlap) {
         extras = obj.fMarker;
         extras_path = '<prnt>/Marker';
         obj = buildOverlapVolume(obj);
         if (!opt)
            opt = 'wire';
      } else if ('fVolume' in obj)
         shape = obj.fVolume?.fShape;
      else
         obj = null;

      if (isStr(opt) && opt.indexOf('comp') === 0 && shape && (shape._typename === clTGeoCompositeShape) && shape.fNode) {
         let maxlvl = 1;
         opt = opt.slice(4);
         if (opt[0] === 'x') {
            maxlvl = 999;
            opt = opt.slice(1) + '_vislvl999';
         }
         obj = buildCompositeVolume(shape, maxlvl);
      }

      if (!obj && shape)
         obj = Object.assign(create$1(clTNamed), { _typename: clTEveGeoShapeExtract, fTrans: null, fShape: shape, fRGBA: [0, 1, 0, 1], fElements: null, fRnrSelf: true });

      if (!obj)
         return null;

      // eslint-disable-next-line no-use-before-define
      const painter = createGeoPainter(dom, obj, opt);

      if (painter.ctrl.is_main && !obj.$geo_painter)
         obj.$geo_painter = painter;

      if (!painter.ctrl.is_main && painter.ctrl.project && obj.$geo_painter) {
         painter.assignCentral(obj.$geo_painter);
         obj.$geo_painter.assignSubordinate(painter);
      }

      if (is_eve && (!painter.ctrl.vislevel || (painter.ctrl.vislevel < 9)))
         painter.ctrl.vislevel = 9;

      if (extras) {
         painter.ctrl.split_colors = true;
         painter.addExtra(extras, extras_path);
      }

      return painter.loadMacro(painter.ctrl.script_name).then(arg => painter.prepareObjectDraw(arg.obj, arg.prefix));
   }

} // class TGeoPainter


let add_settings = false;

/** @summary Get icon for the browser
  * @private */
function getBrowserIcon(hitem, hpainter) {
   let icon = '';
   switch (hitem._kind) {
      case getKindForType(clTEveTrack): icon = 'img_evetrack'; break;
      case getKindForType(clTEvePointSet): icon = 'img_evepoints'; break;
      case getKindForType(clTPolyMarker3D): icon = 'img_evepoints'; break;
   }
   if (icon) {
      const drawitem = findItemWithGeoPainter(hitem);
      if (drawitem?._painter?.extraObjectVisible(hpainter, hitem))
         icon += ' geovis_this';
   }
   return icon;
}

/** @summary handle click on browser icon
  * @private */
function browserIconClick(hitem, hpainter) {
   if (hitem._volume) {
      if (hitem._more && hitem._volume.fNodes?.arr?.length)
         toggleGeoBit(hitem._volume, geoBITS.kVisDaughters);
      else
         toggleGeoBit(hitem._volume, geoBITS.kVisThis);

      updateBrowserIcons(hitem._volume, hpainter);

      findItemWithGeoPainter(hitem, true);
      return false; // no need to update icon - we did it ourself
   }

   if (hitem._geoobj && ((hitem._geoobj._typename === clTEveGeoShapeExtract) || (hitem._geoobj._typename === clREveGeoShapeExtract))) {
      hitem._geoobj.fRnrSelf = !hitem._geoobj.fRnrSelf;

      updateBrowserIcons(hitem._geoobj, hpainter);
      findItemWithGeoPainter(hitem, true);
      return false; // no need to update icon - we did it ourself
   }

   // first check that geo painter assigned with the item
   const drawitem = findItemWithGeoPainter(hitem),
         newstate = drawitem?._painter?.extraObjectVisible(hpainter, hitem, true);

   // return true means browser should update icon for the item
   return newstate !== undefined;
}


/** @summary Create geo-related css entries
  * @private */
function injectGeoStyle() {
   if (!add_settings && isFunc(internals.addDrawFunc)) {
      add_settings = true;
      // indication that draw and hierarchy is loaded, create css
      internals.addDrawFunc({ name: clTEvePointSet, icon_get: getBrowserIcon, icon_click: browserIconClick });
      internals.addDrawFunc({ name: clTEveTrack, icon_get: getBrowserIcon, icon_click: browserIconClick });
   }

   function img(name, code) {
      return `.jsroot .img_${name} { display: inline-block; height: 16px; width: 16px; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQ${code}'); }`;
   }

   injectStyle(`
${img('geoarb8', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB1SURBVBjTdY6rEYAwEETTy6lzK8/Fo+Jj18dTAjUgaQGfGiggtRDE8RtY93Zu514If2nzk2ux9c5TZkwXbiWTUavzws69oBfpYBrMT4r0Jhsw+QfRgQSw+CaKRsKsnV+SaF8MN49RBSgPUxO85PMl5n4tfGUH2gghs2uPAeQAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geocombi', 'CAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAABIAAAASABGyWs+AAAAlUlEQVQoz5VQMQ4CMQyzEUNnBqT7Bo+4nZUH8gj+welWJsQDkHoCEYakTXMHSFiq2jqu4xRAEl2A7w4myWzpzCSZRZ658ldKu1hPnFsequBIc/hcLli3l52MAIANtpWrDsv8waGTW6BPuFtsdZArXyFuj33TQpazGEQF38phipnLgItxRcAoOeNpzv4PTXnC42fb//AGI5YqfQAU8dkAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geocone', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACRSURBVBjTdY+xDcNACEVvEm/ggo6Olva37IB0C3iEzJABvAHFTXBDeJRwthMnUvylk44vPjxK+afeokX0flQhJO7L4pafSOMxzaxIKc/Tc7SIjNLyieyZSjBzc4DqMZI0HTMonWPBNlogOLeuewbg9c0hOiIqH7DKmTCuFykjHe4XOzQ58XVMGxzt575tKzd6AX9yMkcWyPlsAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geogtra', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACCSURBVBjTVc+hDQMxDAVQD1FyqCQk0MwsCwQEG3+eCW6B0FvheDboFMGepTlVitPP/Cz5y0S/mNkw8pySU9INJDDH4vM4Usm5OrQXasXtkA+tQF+zxfcDY8EVwgNeiwmA37TEccK5oLOwQtuCj7BM2Fq7iGrxVqJbSsH+GzXs+798AThwKMh3/6jDAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geomedium', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAABVQTFRFAAAAAAAAMDAww8PDWKj/////gICAG0/C4AAAAAF0Uk5TAEDm2GYAAAABYktHRAX4b+nHAAAACXBIWXMAAABIAAAASABGyWs+AAAAXElEQVQI102MwRGAMAgEuQ6IDwvQCjQdhAl/H7ED038JHhkd3dcOLAgESFARaAqnEB3yrj6QSEym1RbbOKinN+8q2Esui1GaX7VXSi4RUbxHRbER8X6O5Pg/fLgBBzMN8HfXD3AAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geopara', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABtSURBVBjTY2DADq5MT7+CzD9kaKjp+QhJYIWqublhMbKAgpOnZxWSQJdsVJTndCSBKoWoAM/VSALpqlEBAYeQBKJAAsi2BGgCBZDdEWUYFZCOLFBlGOWJ7AyGFeaotjIccopageK3R12PGHABACTYHWd0tGw6AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('georotation', 'CAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAABIAAAASABGyWs+AAAAiklEQVQoz2NgYGBgYGDg+A/BmIAFIvyDEbs0AwMTAwHACLPiB5QVBTdpGSOSCZjScDcgc4z+32BgYGBgEGIQw3QDLkdCTZD8/xJFeBfDVxQT/j9n/MeIrMCNIRBJwX8GRuzGM/yHKMAljeILNFOuMTyEisEUMKIqucrwB2oyIhyQpH8y/MZrLWkAAHFzIHIc0Q5yAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geotranslation', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABESURBVBjTY2DgYGAAYzjgAAIQgSLAgSwAAcrWUUCAJBAVhSpgBAQumALGCJPAAsriHIS0IAQ4UAU4cGphQBWwZSAOAADGJBKdZk/rHQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geotrd2', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABsSURBVBjTbY+xDcAwCARZx6UraiaAmpoRvIIb75PWI2QITxIiRQKk0CCO/xcA/NZ9LRs7RkJEYg3QxczUwoGsXiMAoe8lAelqRWFNKpiNXZLAalRDd0f3TMgeMckABKsCDmu+442RddeHz9cf9jUkW8smGn8AAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geovolume', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAB5QTFRFAAAAMDAw///Ay8uc/7+Q/4BgmJh4gIDgAAD/////CZb2ugAAAAF0Uk5TAEDm2GYAAAABYktHRAnx2aXsAAAACXBIWXMAAABIAAAASABGyWs+AAAAR0lEQVQI12NggAEBIBAEQgYGQUYQAyIGIhgwAZMSGCgwMJuEKimFOhswsKWAGG4JDGxJIBk1EEO9o6NIDVkEpgauC24ODAAASQ8Pkj/retYAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geoassembly', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAA9QTFRFAAAAMDAw/wAAAAD/////jEo0BQAAAAF0Uk5TAEDm2GYAAAABYktHRASPaNlRAAAACXBIWXMAAABIAAAASABGyWs+AAAAOklEQVQI12NggAFGRgEgEBRgEBSAMhgYGQQEgAR+oARGDIwCIAYjUL0A2DQQg9nY2ABVBKoGrgsDAADxzgNboMz8zQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geocomposite', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABuSURBVBjTY2AgF2hqgQCCr+0V4O7hFmgCF7CJyKysKkmxhfGNLaw9SppqAi2gfMuY5Agrl+ZaC6iAUXRJZX6Ic0klTMA5urapPFY5NRcmYKFqWl8S5RobBRNg0PbNT3a1dDGH8RlM3LysTRjIBwAG6xrzJt11BAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geoctub', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACESURBVBjTdc+xDcMwDARA7cKKHTuWX37LHaw+vQbQAJomA7j2DB7FhCMFCZB8pxPwJEv5kQcZW+3HencRBekak4aaMQIi8YJdAQ1CMeE0UBkuaLMETklQ9Alhka0JzzXWqLVBuQYPpWcVuBbZjZafNRYcDk9o/b07bvhINz+/zxu1/M0FSRcmAk/HaIcAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geohype', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACKSURBVBjTbU+rFQQhDKQSDDISEYuMREfHx6eHKMpYuf5qoIQt5bgDblfcuJk3nySEhSvceDV3c/ejT66lspopE9pXyIlkCrHMBACpu1DClekQAREi/loviCnF/NhRwJLaQ6hVhPjB8bOCsjlnNnNl0FWJVWxAqGzHONRHpu5Ml+nQ+8GzNW9n+Is3eg80Nk0iiwoAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geomixture', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAACFQTFRFAAAAAAAAKysrVVUA//8B//8AgICAqqpV398gv79A////VYJtlwAAAAF0Uk5TAEDm2GYAAAABYktHRApo0PRWAAAACXBIWXMAAABIAAAASABGyWs+AAAAXklEQVQI12NgwASCQsJCgoZAhoADq1tKIJAhEpDGxpYIZKgxsLElgBhibAkOCY4gKTaGkPRGIEPUIYEBrEaAIY0tDawmgYWNgREkkjCVjRWkWCUhLY0FJCIIBljsBgCZTAykgaRiRwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geopcon', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACJSURBVBjTdc+hGcQwCIZhhjl/rkgWiECj8XgGyAbZoD5LdIRMkEnKkV575n75Pp8AgLU54dmh6mauelyAL2Qzxfe2sklioq6FacFAcRFXYhwJHdU5rDD2hEYB/CmoJVRMiIJqgtENuoqA8ltAlYAqRH4d1tGkwzTqN2gA7Nv+fUwkgZ/3mg34txM+szzATJS1HQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geosphere', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACFSURBVBjTdY+xEcQwCAQp5QNFjpQ5vZACFBFTADFFfKYCXINzlUAJruXll2ekxDAEt9zcANFbXb2mqm56dxsymAH0yccAJaeNi0h5QGyfxGJmivMPjj0nmLsbRmyFCss3rlbpcUjfS8wLUNRcJyCF6uqg2IvYCnoKC7f1kSbA6riTz7evfwj3Ml+H3KBqAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geotrap', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB5SURBVBjTbY+hFYAwDETZB1OJi4yNPp0JqjtAZ2AELL5DdABmIS2PtLxHXH7u7l2W5W+uHMHpGiCHLYR1yw4SCZMIXBOJWVSjK7QDDAu4g8OBmAKK4sAEDdR3rw8YmcUcrEijKKhl7lN1IQPn9ExlgU6/WEyc75+5AYK0KY5oHBDfAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geotubeseg', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACBSURBVBjTdc+hEcQwDARA12P6QFBQ9LDwcXEVkA7SQTr4BlJBakgpsWdsh/wfux3NSCrlV86Mlrxmz1pBWq3bAHwETohxABVmDZADQp1BE+wDNnGywzHgmHDOreJNTDH3Xn3CVX0dpu2MHcIFBkYp/gKsQ8SCQ72V+36/+2aWf3kAQfgshnpXF0wAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geoxtru', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABcSURBVBjTY2AgEmhpeZV56vmWwQW00QUYwAJlSAI6XmVqukh8PT1bT03PchhXX09Pr9wQIQDiJ+ZowgWAXD3bck+QQDlCQTkDQgCoxA/ERBKwhbDglgA1lDMQDwCc/Rvq8nYsWgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geobbox', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB/SURBVBjTVc+hEYAwDAXQLlNRF1tVGxn9NRswQiSSCdgDyQBM0FlIIb2WuL77uf6E8E0N02wKYRwDciTKREVvB04GuZSyOMCABRB1WGzF3uDNQTvs/RcDtJXT4fSEXA5XoiQt0ttVSm8Co2psIOvoimjAOqBmFtH5wEP2373TPIvTK1nrpULXAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geoconeseg', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB4SURBVBjTdc6hEcAgDAXQbFNZXHQkFlkd/30myAIMwAws0gmYpVzvoFyv/S5P/B+izzQ387ZA2pkDnvsU1SQLVIFrOM4JFmEaYp2gCQbmPEGODhJ8jt7Am47hwgrzInGAifa/elUZnQLY00iU30BZAV+BWi2VfnIBv1osbHH8jX0AAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geoeltu', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACGSURBVBjTdY+hFYUwDEU7xq9CIXC4uNjY6KczQXeoYgVMR2ABRmCGjvIp/6dgiEruueedvBDuOR57LQnKyc8CJmKO+N8bieIUPtmBWjIIx8XDBHYCipsnql1g2D0UP2OoDqwBncf+RdZmzFMHizRjog7KZYzawd4Ay93lEAPWR7WAvNbwMl/XwSxBV8qCjgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geomaterial', 'CAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAABIAAAASABGyWs+AAAAbElEQVQoz62QMRbAIAhDP319Xon7j54qHSyCtaMZFCUkRjgDIdRU9yZUCfg8ut5aAHdcxtoNurmgA3ABNKIR9KimhSukPe2qxcCYC0pfFXx/aFWo7i42KKItOpopqvvnLzJmtlZTS7EfGAfwAM4EQbLIGV0sAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geoparab', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB/SURBVBjTbY+xDYAwDAQ9UAp3X7p0m9o9dUZgA9oMwAjpMwMzMAnYBAQSX9mn9+tN9KOtzsWsLOvYCziUGNX3nnCLJRzKPgeYrhPW7FJNLUB3YJazYKQKTnBaxgXRzNmJcrt7XCHQp9kEB1wfELEir/KGj4Foh8A+/zW1nf51AFabKZuWK+mNAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geopgon', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABwSURBVBjTY2AgDlwAAzh3sX1sPRDEeuwDc+8V2dsHgQQ8LCzq74HkLSzs7Yva2tLt7S3sN4MNiDUGKQmysCi6BzWkzcI+PdY+aDPCljZlj1iFOUjW1tvHLjYuQhJIt5/DcAFZYLH9YnSn7iPST9gAACbsJth21haFAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geotorus', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACGSURBVBjTjY+hFcMwDEQ9SkFggXGIoejhw+LiGkBDlHoAr+AhgjNL5byChuXeE7gvPelUyjOds/f5Zw0ggfj5KVCPMBWeyx+SbQ1XUriAC2XfpWWxjQQEZasRtRHiCUAj3qN4JaolUJppzh4q7dUTdHFXW/tH9OuswWm3nI7tc08+/eGLl758ey9KpKrNOQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geotrd1', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB/SURBVBjTbc6xDQMhDAVQ9qH6lUtal65/zQ5IDMAMmYAZrmKGm4FJzlEQQUo+bvwkG4fwm9lbodV7w40Y4WGfSxQiXiJlQfZOjWRb8Ioi3tKuBQMCo7+9N72BzPsfAuoTdUP9QN8wgOQwvsfWmHzpeT5BKydMNW0nhJGvGf7mAc5WKO9e5N2dAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geotube', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACGSURBVBjTRc+tEcAwCAXgLFNbWeSzSDQazw5doWNUZIOM0BEyS/NHy10E30HyklKvWnJ+0le3sJoKn3X2z7GRuvG++YRyMMDt0IIKUXMzxbnugJi5m9K1gNnGBOUFElAWGMaKIKI4xoQggl00gT+A9hXWgDwnfqgsHRAx2m+8bfjfdyrx5AtsSjpwu+M2RgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('evepoints', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAABJQTFRF////n4mJcEdKRDMzcEdH////lLE/CwAAAAF0Uk5TAEDm2GYAAAABYktHRACIBR1IAAAACXBIWXMAAABIAAAASABGyWs+AAAAI0lEQVQI12NgIAowIpgKEJIZLiAgAKWZGQzQ9UGlWIizBQgAN4IAvGtVrTcAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTYtMDktMDJUMTU6MDQ6MzgrMDI6MDDPyc7hAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE2LTA5LTAyVDE1OjA0OjM4KzAyOjAwvpR2XQAAAABJRU5ErkJggg==')}
${img('evetrack', 'CAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAABIAAAASABGyWs+AAAAqElEQVQoz32RMQrCQBBFf4IgSMB0IpGkMpVHCFh7BbHIGTyVhU0K8QYewEKsbVJZaCUiPAsXV8Puzhaz7H8zs5+JUDjikLilQr5zpCRl5xMXZNScQE5gSMGaz70jjUAJcw5c3UBMTsUe+9Kzf065SbropeLXimWfDIgoab/tOyPGzOhz53+oSWcSGh7UdB2ZNKXBZdgAuUdEKJYmrEILyVgG6pE2tEHgDfe42rbjYzSHAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE2LTA5LTAyVDE1OjA0OjQ3KzAyOjAwM0S3EQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNi0wOS0wMlQxNTowNDo0NyswMjowMEIZD60AAAAASUVORK5CYII=')}
.jsroot .geovis_this { background-color: lightgreen; }
.jsroot .geovis_daughters { background-color: lightblue; }
.jsroot .geovis_all { background-color: yellow; }`);
}


/** @summary Create geo painter
  * @private */
function createGeoPainter(dom, obj, opt) {
   injectGeoStyle();

   geoCfg('GradPerSegm', settings.GeoGradPerSegm);
   geoCfg('CompressComp', settings.GeoCompressComp);

   const painter = new TGeoPainter(dom, obj);

   painter.decodeOptions(opt); // indicator of initialization

   return painter;
}


/** @summary provide menu for geo object
  * @private */
function provideMenu(menu, item, hpainter) {
   if (!item._geoobj)
      return false;

   const obj = item._geoobj, vol = item._volume,
         iseve = ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract));

   if (!vol && !iseve)
      return false;

   menu.separator();

   const scanEveVisible = (obj2, arg, skip_this) => {
      if (!arg)
         arg = { visible: 0, hidden: 0 };

      if (!skip_this) {
         if (arg.assign !== undefined)
            obj2.fRnrSelf = arg.assign;
         else if (obj2.fRnrSelf)
            arg.vis++;
         else
            arg.hidden++;
      }

      if (obj2.fElements) {
         for (let n = 0; n < obj2.fElements.arr.length; ++n)
            scanEveVisible(obj2.fElements.arr[n], arg, false);
      }

      return arg;
   }, toggleEveVisibility = arg => {
      if (arg === 'self') {
         obj.fRnrSelf = !obj.fRnrSelf;
         item._icon = item._icon.split(' ')[0] + provideVisStyle(obj);
         hpainter.updateTreeNode(item);
      } else {
         scanEveVisible(obj, { assign: (arg === 'true') }, true);
         hpainter.forEachItem(m => {
            // update all child items
            if (m._geoobj && m._icon) {
               m._icon = item._icon.split(' ')[0] + provideVisStyle(m._geoobj);
               hpainter.updateTreeNode(m);
            }
         }, item);
      }

      findItemWithGeoPainter(item, true);
   }, toggleMenuBit = arg => {
      toggleGeoBit(vol, arg);
      const newname = item._icon.split(' ')[0] + provideVisStyle(vol);
      hpainter.forEachItem(m => {
         // update all items with that volume
         if (item._volume === m._volume) {
            m._icon = newname;
            hpainter.updateTreeNode(m);
         }
      });

      hpainter.updateTreeNode(item);
      findItemWithGeoPainter(item, true);
   }, drawitem = findItemWithGeoPainter(item),
         fullname = drawitem ? hpainter.itemFullName(item, drawitem) : '';

   if ((item._geoobj._typename.indexOf(clTGeoNode) === 0) && drawitem) {
      menu.add('Focus', () => {
         if (drawitem && isFunc(drawitem._painter?.focusOnItem))
            drawitem._painter.focusOnItem(fullname);
      });
   }

   if (iseve) {
      menu.addchk(obj.fRnrSelf, 'Visible', 'self', toggleEveVisibility);
      const res = scanEveVisible(obj, undefined, true);
      if (res.hidden + res.visible > 0)
         menu.addchk((res.hidden === 0), 'Daughters', res.hidden ? 'true' : 'false', toggleEveVisibility);
   } else {
      const stack = drawitem?._painter?.getClones()?.findStackByName(fullname),
            phys_vis = stack ? drawitem._painter.getClones().getPhysNodeVisibility(stack) : null,
            is_visible = testGeoBit(vol, geoBITS.kVisThis);

      menu.addchk(testGeoBit(vol, geoBITS.kVisNone), 'Invisible', geoBITS.kVisNone, toggleMenuBit);
      if (stack) {
         const changePhysVis = arg => {
            drawitem._painter.getClones().setPhysNodeVisibility(stack, (arg === 'off') ? false : arg);
            findItemWithGeoPainter(item, true);
         };

         menu.sub('Physical vis', 'Physical node visibility - only for this instance');
         menu.addchk(phys_vis?.visible, 'on', 'on', changePhysVis, 'Enable visibility of phys node');
         menu.addchk(phys_vis && !phys_vis.visible, 'off', 'off', changePhysVis, 'Disable visibility of physical node');
         menu.add('reset', 'clear', changePhysVis, 'Reset custom visibility of physical node');
         menu.add('reset all', 'clearall', changePhysVis, 'Reset all custom settings for all nodes');
         menu.endsub();
      }

      menu.addchk(is_visible, 'Logical vis',
                  geoBITS.kVisThis, toggleMenuBit, 'Logical node visibility - all instances');
      menu.addchk(testGeoBit(vol, geoBITS.kVisDaughters), 'Daughters',
                  geoBITS.kVisDaughters, toggleMenuBit, 'Logical node daugthers visibility');
   }

   return true;
}

let createItem = null;

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

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

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

         if (!lst2.arr)
            return false;

         node._childs = [];

         checkDuplicates(null, lst2.arr);

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

         return true;
      }
   };

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


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

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

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

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

   if (parent._childs)
      return true;

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

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

   let volume, subnodes, shape;

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

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

      return true;
   }

   if (!subnodes)
      return false;

   checkDuplicates(obj, subnodes);

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

   return true;
}


/** @summary create hierarchy item for geo object
  * @private */
createItem = function(node, obj, name) {
   const sub = {
      _kind: getKindForType(obj._typename),
      _name: name || getObjectName(obj),
      _title: obj.fTitle,
      _parent: node,
      _geoobj: obj,
      _get(item /* ,itemname */) {
         // mark object as belong to the hierarchy, require to
         if (item._geoobj)
            item._geoobj.$geoh = true;
         return Promise.resolve(item._geoobj);
      }
   };
   let volume, shape, subnodes, iseve = false;

   if (obj._typename === 'TGeoMaterial')
      sub._icon = 'img_geomaterial';
   else if (obj._typename === 'TGeoMedium')
      sub._icon = 'img_geomedium';
   else if (obj._typename === 'TGeoMixture')
      sub._icon = 'img_geomixture';
   else if ((obj._typename.indexOf(clTGeoNode) === 0) && obj.fVolume) {
      sub._title = 'node:' + obj._typename;
      if (obj.fTitle)
         sub._title += ' ' + obj.fTitle;
      volume = obj.fVolume;
   } else if (obj._typename.indexOf(clTGeoVolume) === 0)
      volume = obj;
   else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) {
      iseve = true;
      shape = obj.fShape;
      subnodes = obj.fElements ? obj.fElements.arr : null;
   } else if ((obj.fShapeBits !== undefined) && (obj.fShapeId !== undefined))
      shape = obj;

   if (volume) {
      shape = volume.fShape;
      subnodes = volume.fNodes ? volume.fNodes.arr : null;
   }

   if (volume || shape || subnodes) {
      if (volume)
         sub._volume = volume;

      if (subnodes) {
         sub._more = true;
         sub._expand = expandGeoObject;
      } else if (shape && (shape._typename === clTGeoCompositeShape) && shape.fNode) {
         sub._more = true;
         sub._shape = shape;
         sub._expand = function(node2 /* , obj */) {
            createItem(node2, node2._shape.fNode.fLeft, 'Left');
            createItem(node2, node2._shape.fNode.fRight, 'Right');
            return true;
         };
      }

      if (!sub._title && (obj._typename !== clTGeoVolume))
         sub._title = obj._typename;

      if (shape) {
         if (sub._title === '')
            sub._title = shape._typename;

         sub._icon = getShapeIcon(shape);
      } else
         sub._icon = sub._more ? 'img_geocombi' : 'img_geobbox';

      if (volume)
         sub._icon += provideVisStyle(volume);
      else if (iseve)
         sub._icon += provideVisStyle(obj);

      sub._menu = provideMenu;
      sub._icon_click = browserIconClick;
   }

   if (!node._childs)
      node._childs = [];

   if (!sub._name) {
      if (isStr(node._name)) {
         sub._name = node._name;
         if (sub._name.at(-1) === 's')
            sub._name = sub._name.slice(0, sub._name.length - 1);
         sub._name += '_' + node._childs.length;
      } else
         sub._name = 'item_' + node._childs.length;
   }

   node._childs.push(sub);

   return sub;
};

/** @summary Draw dummy geometry
  * @private */
async function drawDummy3DGeom(painter) {
   const shape = create$1(clTNamed);
   shape._typename = clTGeoBBox;
   shape.fDX = 1e-10;
   shape.fDY = 1e-10;
   shape.fDZ = 1e-10;
   shape.fShapeId = 1;
   shape.fShapeBits = 0;
   shape.fOrigin = [0, 0, 0];

   const obj = Object.assign(create$1(clTNamed), {
      _typename: clTEveGeoShapeExtract,
      fTrans: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
      fShape: shape, fRGBA: [0, 0, 0, 0], fElements: null, fRnrSelf: false
   }),
         pp = painter.getPadPainter(),
         pad = pp?.getRootPad(true),
         opt = 'dummy;' + (pad?.fFillColor && (pad?.fFillStyle > 1000) ? 'bkgr_' + pad.fFillColor : '');

   return TGeoPainter.draw(pp || painter.getDom(), obj, opt);
}

/** @summary Direct draw function for TAxis3D
  * @private */
function drawAxis3D() {
   const main = this.getMainPainter();

   if (isFunc(main?.setAxesDraw))
      return main.setAxesDraw(true);

   console.error('no geometry painter found to toggle TAxis3D drawing');
}

/** @summary Build three.js model for given geometry object
  * @param {Object} obj - TGeo-related object
  * @param {Object} [opt] - options
  * @param {Number} [opt.vislevel] - visibility level like TGeoManager, when not specified - show all
  * @param {Number} [opt.numnodes=1000] - maximal number of visible nodes
  * @param {Number} [opt.numfaces=100000] - approx maximal number of created triangles
  * @param {Number} [opt.instancing=-1] - <0 disable use of InstancedMesh, =0 only for large geometries, >0 enforce usage of InstancedMesh
  * @param {boolean} [opt.doubleside=false] - use double-side material
  * @param {boolean} [opt.wireframe=false] - show wireframe for created shapes
  * @param {boolean} [opt.transparency=0] - make nodes transparent
  * @param {boolean} [opt.dflt_colors=false] - use default ROOT colors
  * @param {boolean} [opt.set_names=true] - set names to all Object3D instances
  * @param {boolean} [opt.set_origin=false] - set TGeoNode/TGeoVolume as Object3D.userData
  * @return {object} Object3D with created model
  * @example
  * import { build } from 'https://root.cern/js/latest/modules/geom/TGeoPainter.mjs';
  * let obj3d = build(obj);
  * // this is three.js object and can be now inserted in the scene
  */
function build(obj, opt) {
   return TGeoPainter.build3d(obj, opt);
}

var TGeoPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
ClonedNodes: ClonedNodes,
GeoDrawingControl: GeoDrawingControl,
TGeoPainter: TGeoPainter,
build: build,
createGeoPainter: createGeoPainter,
drawAxis3D: drawAxis3D,
drawDummy3DGeom: drawDummy3DGeom,
expandGeoObject: expandGeoObject,
produceRenderOrder: produceRenderOrder
});

const clTStreamerElement = 'TStreamerElement', clTStreamerObject = 'TStreamerObject',
      clTStreamerSTL = 'TStreamerSTL', clTStreamerInfoList = 'TStreamerInfoList',
      clTDirectory = 'TDirectory', clTDirectoryFile = 'TDirectoryFile',
      clTQObject = 'TQObject', clTBasket = 'TBasket', clTDatime = 'TDatime',
      nameStreamerInfo = 'StreamerInfo',

      kChar = 1, kShort = 2, kInt = 3, kLong = 4, kFloat = 5, kCounter = 6,
      kCharStar = 7, kDouble = 8, kDouble32 = 9, kLegacyChar = 10,
      kUChar = 11, kUShort = 12, kUInt = 13, kULong = 14, kBits = 15,
      kLong64 = 16, kULong64 = 17, kBool = 18, kFloat16 = 19,

      kBase = 0, kOffsetL = 20, kOffsetP = 40,
      kObject = 61, kAny = 62, kObjectp = 63, kObjectP = 64, kTString = 65,
      kTObject = 66, kTNamed = 67, kAnyp = 68, kAnyP = 69,

      /* kAnyPnoVT: 70, */
      kSTLp = 71,
      /* kSkip = 100, kSkipL = 120, kSkipP = 140, kConv = 200, kConvL = 220, kConvP = 240, */
      kSTL = 300, /* kSTLstring = 365, */

      kStreamer = 500, kStreamLoop = 501,

      kMapOffset = 2, kByteCountMask = 0x40000000, kNewClassTag = 0xFFFFFFFF, kClassMask = 0x80000000,

      // constants of bits in version
      kStreamedMemberWise = BIT(14),

      // constants used for coding type of STL container
      kNotSTL = 0, kSTLvector = 1, kSTLlist = 2, kSTLdeque = 3, kSTLmap = 4, kSTLmultimap = 5,
      kSTLset = 6, kSTLmultiset = 7, kSTLbitset = 8,
      // kSTLforwardlist = 9, kSTLunorderedset = 10, kSTLunorderedmultiset = 11, kSTLunorderedmap = 12,
      // kSTLunorderedmultimap = 13, kSTLend = 14
      kBaseClass = 'BASE',

      // name of base IO types
      BasicTypeNames = [kBaseClass, 'char', 'short', 'int', 'long', 'float', 'int', 'const char*', 'double', 'Double32_t',
                        'char', 'unsigned  char', 'unsigned short', 'unsigned', 'unsigned long', 'unsigned', 'Long64_t', 'ULong64_t', 'bool', 'Float16_t'],

      // names of STL containers
      StlNames = ['', 'vector', 'list', 'deque', 'map', 'multimap', 'set', 'multiset', 'bitset'],

      // TObject bits
      kIsReferenced = BIT(4), kHasUUID = BIT(5);


/** @summary Custom streamers for root classes
  * @desc map of user-streamer function like func(buf,obj)
  * or alias (classname) which can be used to read that function
  * or list of read functions
  * @private */
/* eslint-disable one-var */
const CustomStreamers = {
   TObject(buf, obj) {
      obj.fUniqueID = buf.ntou4();
      obj.fBits = buf.ntou4();
      if (obj.fBits & kIsReferenced)
         buf.ntou2(); // skip pid
   },

   TNamed: [
      {
         basename: clTObject, base: 1,
         func(buf, obj) {
            if (!obj._typename)
               obj._typename = clTNamed;
            buf.classStreamer(obj, clTObject);
         }
      },
      { name: 'fName', func(buf, obj) { obj.fName = buf.readTString(); } },
      { name: 'fTitle', func(buf, obj) { obj.fTitle = buf.readTString(); } }
   ],

   TObjString: [
      {
         basename: clTObject, base: 1,
         func(buf, obj) {
            if (!obj._typename)
               obj._typename = clTObjString;
            buf.classStreamer(obj, clTObject);
         }
      },
      { name: 'fString', func(buf, obj) { obj.fString = buf.readTString(); } }
   ],

   TClonesArray(buf, list) {
      if (!list._typename)
         list._typename = clTClonesArray;
      list.$kind = clTClonesArray;
      list.name = '';
      const ver = buf.last_read_version;
      if (ver > 2)
         buf.classStreamer(list, clTObject);
      if (ver > 1)
         list.name = buf.readTString();
      let classv = buf.readTString(), clv = 0;
      const pos = classv.lastIndexOf(';');

      if (pos > 0) {
         clv = Number.parseInt(classv.slice(pos + 1));
         classv = classv.slice(0, pos);
      }

      let nobjects = buf.ntou4();
      if (nobjects < 0)
         nobjects = -nobjects;  // for backward compatibility

      list.arr = new Array(nobjects);
      list.fLast = nobjects - 1;
      list.fLowerBound = buf.ntou4();

      let streamer = buf.fFile.getStreamer(classv, { val: clv });
      streamer = buf.fFile.getSplittedStreamer(streamer);

      if (!streamer)
         console.log(`Cannot get member-wise streamer for ${classv}:${clv}`);
      else {
         // create objects
         for (let n = 0; n < nobjects; ++n)
            list.arr[n] = { _typename: classv };

         // call streamer for all objects member-wise
         for (let k = 0; k < streamer.length; ++k) {
            for (let n = 0; n < nobjects; ++n)
               streamer[k].func(buf, list.arr[n]);
         }
      }
   },

   TMap(buf, map) {
      if (!map._typename)
         map._typename = clTMap;
      map.name = '';
      map.arr = [];
      const ver = buf.last_read_version;
      if (ver > 2)
         buf.classStreamer(map, clTObject);
      if (ver > 1)
         map.name = buf.readTString();

      const nobjects = buf.ntou4();
      // create objects
      for (let n = 0; n < nobjects; ++n) {
         const obj = { _typename: 'TPair' };
         obj.first = buf.readObjectAny();
         obj.second = buf.readObjectAny();
         if (obj.first)
            map.arr.push(obj);
      }
   },

   TTreeIndex(buf, obj) {
      const ver = buf.last_read_version;
      obj._typename = 'TTreeIndex';
      buf.classStreamer(obj, 'TVirtualIndex');
      obj.fMajorName = buf.readTString();
      obj.fMinorName = buf.readTString();
      obj.fN = buf.ntoi8();
      obj.fIndexValues = buf.readFastArray(obj.fN, kLong64);
      if (ver > 1)
         obj.fIndexValuesMinor = buf.readFastArray(obj.fN, kLong64);
      obj.fIndex = buf.readFastArray(obj.fN, kLong64);
   },

   TRefArray(buf, obj) {
      obj._typename = 'TRefArray';
      buf.classStreamer(obj, clTObject);
      obj.name = buf.readTString();
      const nobj = buf.ntoi4();
      obj.fLast = nobj - 1;
      obj.fLowerBound = buf.ntoi4();
      /* const pidf = */ buf.ntou2();
      obj.fUIDs = buf.readFastArray(nobj, kUInt);
   },

   TCanvas(buf, obj) {
      obj._typename = clTCanvas;
      buf.classStreamer(obj, clTPad);
      obj.fDISPLAY = buf.readTString();
      obj.fDoubleBuffer = buf.ntoi4();
      obj.fRetained = buf.ntobool();
      obj.fXsizeUser = buf.ntoi4();
      obj.fYsizeUser = buf.ntoi4();
      obj.fXsizeReal = buf.ntoi4();
      obj.fYsizeReal = buf.ntoi4();
      obj.fWindowTopX = buf.ntoi4();
      obj.fWindowTopY = buf.ntoi4();
      obj.fWindowWidth = buf.ntoi4();
      obj.fWindowHeight = buf.ntoi4();
      obj.fCw = buf.ntou4();
      obj.fCh = buf.ntou4();
      obj.fCatt = buf.classStreamer({}, clTAttCanvas);
      buf.ntou1(); // ignore b << TestBit(kMoveOpaque);
      buf.ntou1(); // ignore b << TestBit(kResizeOpaque);
      obj.fHighLightColor = buf.ntoi2();
      obj.fBatch = buf.ntobool();
      buf.ntou1();   // ignore b << TestBit(kShowEventStatus);
      buf.ntou1();   // ignore b << TestBit(kAutoExec);
      buf.ntou1();   // ignore b << TestBit(kMenuBar);
   },

   TObjArray(buf, list) {
      if (!list._typename)
         list._typename = clTObjArray;
      list.$kind = clTObjArray;
      list.name = '';
      const ver = buf.last_read_version;
      if (ver > 2)
         buf.classStreamer(list, clTObject);
      if (ver > 1)
         list.name = buf.readTString();
      const nobjects = buf.ntou4();
      let i = 0;
      list.arr = new Array(nobjects);
      list.fLast = nobjects - 1;
      list.fLowerBound = buf.ntou4();
      while (i < nobjects)
         list.arr[i++] = buf.readObjectAny();
   },

   TPolyMarker3D(buf, marker) {
      const ver = buf.last_read_version;
      buf.classStreamer(marker, clTObject);
      buf.classStreamer(marker, clTAttMarker);
      marker.fN = buf.ntoi4();
      marker.fP = buf.readFastArray(marker.fN * 3, kFloat);
      marker.fOption = buf.readTString();
      marker.fName = (ver > 1) ? buf.readTString() : clTPolyMarker3D;
   },

   TPolyLine3D(buf, obj) {
      buf.classStreamer(obj, clTObject);
      buf.classStreamer(obj, clTAttLine);
      obj.fN = buf.ntoi4();
      obj.fP = buf.readFastArray(obj.fN * 3, kFloat);
      obj.fOption = buf.readTString();
   },

   TStreamerInfo(buf, obj) {
      buf.classStreamer(obj, clTNamed);
      obj.fCheckSum = buf.ntou4();
      obj.fClassVersion = buf.ntou4();
      obj.fElements = buf.readObjectAny();
   },

   TStreamerElement(buf, element) {
      const ver = buf.last_read_version;
      buf.classStreamer(element, clTNamed);
      element.fType = buf.ntou4();
      element.fSize = buf.ntou4();
      element.fArrayLength = buf.ntou4();
      element.fArrayDim = buf.ntou4();
      element.fMaxIndex = buf.readFastArray((ver === 1) ? buf.ntou4() : 5, kUInt);
      element.fTypeName = buf.readTString();

      if ((element.fType === kUChar) && ((element.fTypeName === 'Bool_t') || (element.fTypeName === 'bool')))
         element.fType = kBool;

      element.fXmin = element.fXmax = element.fFactor = 0;
      if (ver === 3) {
         element.fXmin = buf.ntod();
         element.fXmax = buf.ntod();
         element.fFactor = buf.ntod();
      } else if ((ver > 3) && (element.fBits & BIT(6))) { // kHasRange
         let p1 = element.fTitle.indexOf('[');
         if ((p1 >= 0) && (element.fType > kOffsetP))
            p1 = element.fTitle.indexOf('[', p1 + 1);
         const p2 = element.fTitle.indexOf(']', p1 + 1);

         if ((p1 >= 0) && (p2 >= p1 + 2)) {
            const arr = element.fTitle.slice(p1 + 1, p2).split(',');
            let nbits = 32;
            if (!arr || arr.length < 2)
               throw new Error(`Problem to decode range setting from streamer element title ${element.fTitle}`);

            if (arr.length === 3)
               nbits = parseInt(arr[2]);
            if (!Number.isInteger(nbits) || (nbits < 2) || (nbits > 32))
               nbits = 32;

            const parse_range = val => {
               if (!val)
                  return 0;
               if (val.indexOf('pi') < 0)
                  return parseFloat(val);
               val = val.trim();
               let sign = 1;
               if (val[0] === '-') {
                  sign = -1;
                  val = val.slice(1);
               }
               switch (val) {
                  case '2pi':
                  case '2*pi':
                  case 'twopi': return sign * 2 * Math.PI;
                  case 'pi/2': return sign * Math.PI / 2;
                  case 'pi/4': return sign * Math.PI / 4;
               }
               return sign * Math.PI;
            };

            element.fXmin = parse_range(arr[0]);
            element.fXmax = parse_range(arr[1]);

            // avoid usage of 1 << nbits, while only works up to 32 bits
            const bigint = ((nbits >= 0) && (nbits < 32)) ? Math.pow(2, nbits) : 0xffffffff;
            if (element.fXmin < element.fXmax)
               element.fFactor = bigint / (element.fXmax - element.fXmin);
            else if (nbits < 15)
               element.fXmin = nbits;
         }
      }
   },

   TStreamerBase(buf, elem) {
      const ver = buf.last_read_version;
      buf.classStreamer(elem, clTStreamerElement);
      if (ver > 2)
         elem.fBaseVersion = buf.ntou4();
   },

   TStreamerSTL(buf, elem) {
      buf.classStreamer(elem, clTStreamerElement);
      elem.fSTLtype = buf.ntou4();
      elem.fCtype = buf.ntou4();

      if ((elem.fSTLtype === kSTLmultimap) &&
          ((elem.fTypeName.indexOf('std::set') === 0) || (elem.fTypeName.indexOf('set') === 0)))
         elem.fSTLtype = kSTLset;

      if ((elem.fSTLtype === kSTLset) &&
          ((elem.fTypeName.indexOf('std::multimap') === 0) || (elem.fTypeName.indexOf('multimap') === 0)))
         elem.fSTLtype = kSTLmultimap;
   },

   TStreamerSTLstring(buf, elem) {
      if (buf.last_read_version > 0)
         buf.classStreamer(elem, clTStreamerSTL);
   },

   TList(buf, obj) {
      // stream all objects in the list from the I/O buffer
      if (!obj._typename)
         obj._typename = this.typename;
      obj.$kind = clTList; // all derived classes will be marked as well
      if (buf.last_read_version > 3) {
         buf.classStreamer(obj, clTObject);
         obj.name = buf.readTString();
         const nobjects = buf.ntou4();
         obj.arr = new Array(nobjects);
         obj.opt = new Array(nobjects);
         for (let i = 0; i < nobjects; ++i) {
            obj.arr[i] = buf.readObjectAny();
            obj.opt[i] = buf.readTString();
         }
      } else {
         obj.name = '';
         obj.arr = [];
         obj.opt = [];
      }
   },

   THashList: clTList,

   TStreamerLoop(buf, elem) {
      if (buf.last_read_version > 1) {
         buf.classStreamer(elem, clTStreamerElement);
         elem.fCountVersion = buf.ntou4();
         elem.fCountName = buf.readTString();
         elem.fCountClass = buf.readTString();
      }
   },

   TStreamerBasicPointer: 'TStreamerLoop',

   TStreamerObject(buf, elem) {
      if (buf.last_read_version > 1)
         buf.classStreamer(elem, clTStreamerElement);
   },

   TStreamerBasicType: clTStreamerObject,
   TStreamerObjectAny: clTStreamerObject,
   TStreamerString: clTStreamerObject,
   TStreamerObjectPointer: clTStreamerObject,

   TStreamerObjectAnyPointer(buf, elem) {
      if (buf.last_read_version > 0)
         buf.classStreamer(elem, clTStreamerElement);
   },

   TTree: {
      name: '$file',
      func(buf, obj) {
         obj.$kind = clTTree;
         obj.$file = buf.fFile;
      }
   },

   TBranch(buf, obj) {
      const v = buf.last_read_version;
      if (v > 9)
         buf.streamClassMembers(obj, 'TBranch', v);
      else {
         buf.classStreamer(obj, clTNamed);
         if (v > 7)
            buf.classStreamer(obj, clTAttFill);
         obj.fCompress = buf.ntoi4();
         obj.fBasketSize = buf.ntoi4();
         obj.fEntryOffsetLen = buf.ntoi4();
         obj.fWriteBasket = buf.ntoi4();
         obj.fEntryNumber = buf.ntoi4();
         obj.fOffset = buf.ntoi4();
         obj.fMaxBaskets = buf.ntoi4();
         if (v > 6)
            obj.fSplitLevel = buf.ntoi4();
         obj.fEntries = buf.ntod();
         obj.fTotBytes = buf.ntod();
         obj.fZipBytes = buf.ntod();
         obj.fBranches = buf.classStreamer({}, clTObjArray);
         obj.fLeaves = buf.classStreamer({}, clTObjArray);
         obj.fBaskets = buf.classStreamer({}, clTObjArray);
         buf.ntoi1(); // isArray
         obj.fBasketBytes = buf.readFastArray(obj.fMaxBaskets, kInt);
         buf.ntoi1(); // isArray
         obj.fBasketEntry = buf.readFastArray(obj.fMaxBaskets, kInt);
         const isArray = buf.ntoi1();
         obj.fBasketSeek = buf.readFastArray(obj.fMaxBaskets, isArray === 2 ? kLong64 : kInt);
         obj.fFileName = buf.readTString();
      }
   },

   'ROOT::RNTuple': {
      name: '$file',
      func(buf, obj) {
         obj.$kind = 'ROOT::RNTuple';
         obj.$file = buf.fFile;
      }
   },

   RooRealVar(buf, obj) {
      const v = buf.last_read_version;
      buf.classStreamer(obj, 'RooAbsRealLValue');
      if (v === 1) {
         // skip fitMin, fitMax, fitBins
         buf.ntod();
         buf.ntod();
         buf.ntoi4();
      }
      obj._error = buf.ntod();
      obj._asymErrLo = buf.ntod();
      obj._asymErrHi = buf.ntod();
      if (v >= 2)
         obj._binning = buf.readObjectAny();
      if (v === 3)
         obj._sharedProp = buf.readObjectAny();
      if (v >= 4)
         obj._sharedProp = buf.classStreamer({}, 'RooRealVarSharedProperties');
   },

   RooAbsBinning(buf, obj) {
      buf.classStreamer(obj, (buf.last_read_version === 1) ? clTObject : clTNamed);
      buf.classStreamer(obj, 'RooPrintable');
   },

   RooCategory(buf, obj) {
      const v = buf.last_read_version;
      buf.classStreamer(obj, 'RooAbsCategoryLValue');
      obj._sharedProp = (v === 1) ? buf.readObjectAny() : buf.classStreamer({}, 'RooCategorySharedProperties');
   },

   'RooWorkspace::CodeRepo': (buf /* , obj */) => {
      const sz = (buf.last_read_version === 2) ? 3 : 2;
      for (let i = 0; i < sz; ++i) {
         let cnt = buf.ntoi4() * ((i === 0) ? 4 : 3);
         while (cnt--)
            buf.readTString();
      }
   },

   RooLinkedList(buf, obj) {
      const v = buf.last_read_version;
      buf.classStreamer(obj, clTObject);
      let size = buf.ntoi4();
      obj.arr = create$1(clTList);
      while (size--)
         obj.arr.Add(buf.readObjectAny());
      if (v > 1)
         obj._name = buf.readTString();
   },

   TImagePalette: [
      {
         basename: clTObject, base: 1, func(buf, obj) {
            if (!obj._typename)
               obj._typename = clTImagePalette;
            buf.classStreamer(obj, clTObject);
         }
      },
      { name: 'fNumPoints', func(buf, obj) { obj.fNumPoints = buf.ntou4(); } },
      { name: 'fPoints', func(buf, obj) { obj.fPoints = buf.readFastArray(obj.fNumPoints, kDouble); } },
      { name: 'fColorRed', func(buf, obj) { obj.fColorRed = buf.readFastArray(obj.fNumPoints, kUShort); } },
      { name: 'fColorGreen', func(buf, obj) { obj.fColorGreen = buf.readFastArray(obj.fNumPoints, kUShort); } },
      { name: 'fColorBlue', func(buf, obj) { obj.fColorBlue = buf.readFastArray(obj.fNumPoints, kUShort); } },
      { name: 'fColorAlpha', func(buf, obj) { obj.fColorAlpha = buf.readFastArray(obj.fNumPoints, kUShort); } }
   ],

   TAttImage: [
      { name: 'fImageQuality', func(buf, obj) { obj.fImageQuality = buf.ntoi4(); } },
      { name: 'fImageCompression', func(buf, obj) { obj.fImageCompression = buf.ntou4(); } },
      { name: 'fConstRatio', func(buf, obj) { obj.fConstRatio = buf.ntobool(); } },
      { name: 'fPalette', func(buf, obj) { obj.fPalette = buf.classStreamer({}, clTImagePalette); } }
   ],

   TASImage(buf, obj) {
      if ((buf.last_read_version === 1) && (buf.fFile.fVersion > 0) && (buf.fFile.fVersion < 50000))
         return console.warn('old TASImage version - not yet supported');

      buf.classStreamer(obj, clTNamed);

      if (buf.ntobool()) {
         const size = buf.ntoi4();
         obj.fPngBuf = buf.readFastArray(size, kUChar);
      } else {
         buf.classStreamer(obj, 'TAttImage');
         obj.fWidth = buf.ntoi4();
         obj.fHeight = buf.ntoi4();
         obj.fImgBuf = buf.readFastArray(obj.fWidth * obj.fHeight, kDouble);
      }
   },

   TMaterial(buf, obj) {
      const v = buf.last_read_version;
      buf.classStreamer(obj, clTNamed);
      obj.fNumber = buf.ntoi4();
      obj.fA = buf.ntof();
      obj.fZ = buf.ntof();
      obj.fDensity = buf.ntof();
      if (v > 2) {
         buf.classStreamer(obj, clTAttFill);
         obj.fRadLength = buf.ntof();
         obj.fInterLength = buf.ntof();
      } else
         obj.fRadLength = obj.fInterLength = 0;
   },

   TMixture(buf, obj) {
      buf.classStreamer(obj, 'TMaterial');
      obj.fNmixt = buf.ntoi4();
      obj.fAmixt = buf.readFastArray(buf.ntoi4(), kFloat);
      obj.fZmixt = buf.readFastArray(buf.ntoi4(), kFloat);
      obj.fWmixt = buf.readFastArray(buf.ntoi4(), kFloat);
   },

   TVirtualPerfStats: clTObject, // use directly TObject streamer

   TMethodCall: clTObject
};


/** @summary Add custom streamer
  * @public */
function addUserStreamer(type, user_streamer) {
   CustomStreamers[type] = user_streamer;
}

/** @summary these are streamers which do not handle version regularly
  * @desc used for special classes like TRef or TBasket
  * @private */
const DirectStreamers = {
   // do nothing for these classes
   TQObject() {},
   TGraphStruct() {},
   TGraphNode() {},
   TGraphEdge() {},

   TDatime(buf, obj) {
      obj.fDatime = buf.ntou4();
   },

   TKey(buf, key) {
      key.fNbytes = buf.ntoi4();
      key.fVersion = buf.ntoi2();
      key.fObjlen = buf.ntou4();
      key.fDatime = buf.classStreamer({}, clTDatime);
      key.fKeylen = buf.ntou2();
      key.fCycle = buf.ntou2();
      if (key.fVersion > 1000) {
         key.fSeekKey = buf.ntou8();
         buf.shift(8); // skip seekPdir
      } else {
         key.fSeekKey = buf.ntou4();
         buf.shift(4); // skip seekPdir
      }
      key.fClassName = buf.readTString();
      key.fName = buf.readTString();
      key.fTitle = buf.readTString();
   },

   TDirectory(buf, dir) {
      const version = buf.ntou2();
      dir.fDatimeC = buf.classStreamer({}, clTDatime);
      dir.fDatimeM = buf.classStreamer({}, clTDatime);
      dir.fNbytesKeys = buf.ntou4();
      dir.fNbytesName = buf.ntou4();
      dir.fSeekDir = (version > 1000) ? buf.ntou8() : buf.ntou4();
      dir.fSeekParent = (version > 1000) ? buf.ntou8() : buf.ntou4();
      dir.fSeekKeys = (version > 1000) ? buf.ntou8() : buf.ntou4();
      // if ((version % 1000) > 2) buf.shift(18); // skip fUUID
   },

   TRef(buf, obj) {
      buf.classStreamer(obj, clTObject);
      if (obj.fBits & kHasUUID)
         obj.fUUID = buf.readTString();
      else
         obj.fPID = buf.ntou2();
   },

   'TMatrixTSym<float>': (buf, obj) => {
      buf.classStreamer(obj, 'TMatrixTBase<float>');
      obj.fElements = new Float32Array(obj.fNelems);
      const arr = buf.readFastArray((obj.fNrows * (obj.fNcols + 1)) / 2, kFloat);
      for (let i = 0, cnt = 0; i < obj.fNrows; ++i) {
         for (let j = i; j < obj.fNcols; ++j)
            obj.fElements[j * obj.fNcols + i] = obj.fElements[i * obj.fNcols + j] = arr[cnt++];
      }
   },

   'TMatrixTSym<double>': (buf, obj) => {
      buf.classStreamer(obj, 'TMatrixTBase<double>');
      obj.fElements = new Float64Array(obj.fNelems);
      const arr = buf.readFastArray((obj.fNrows * (obj.fNcols + 1)) / 2, kDouble);
      for (let i = 0, cnt = 0; i < obj.fNrows; ++i) {
         for (let j = i; j < obj.fNcols; ++j)
            obj.fElements[j * obj.fNcols + i] = obj.fElements[i * obj.fNcols + j] = arr[cnt++];
      }
   }
};

/** @summary Returns type id by its name
  * @private */
function getTypeId(typname, norecursion) {
   switch (typname) {
      case 'bool':
      case 'Bool_t': return kBool;
      case 'char':
      case 'signed char':
      case 'Char_t': return kChar;
      case 'Color_t':
      case 'Style_t':
      case 'Width_t':
      case 'short':
      case 'Short_t': return kShort;
      case 'int':
      case 'EErrorType':
      case 'Int_t': return kInt;
      case 'long':
      case 'Long_t': return kLong;
      case 'float':
      case 'Float_t': return kFloat;
      case 'double':
      case 'Double_t': return kDouble;
      case 'unsigned char':
      case 'UChar_t': return kUChar;
      case 'unsigned short':
      case 'UShort_t': return kUShort;
      case 'unsigned':
      case 'unsigned int':
      case 'UInt_t': return kUInt;
      case 'unsigned long':
      case 'ULong_t': return kULong;
      case 'int64_t':
      case 'long long':
      case 'Long64_t': return kLong64;
      case 'uint64_t':
      case 'unsigned long long':
      case 'ULong64_t': return kULong64;
      case 'Double32_t': return kDouble32;
      case 'Float16_t': return kFloat16;
      case 'char*':
      case 'const char*':
      case 'const Char_t*': return kCharStar;
   }

   if (!norecursion) {
      const replace = CustomStreamers[typname];
      if (isStr(replace))
         return getTypeId(replace, true);
   }

   return -1;
}

/** @summary Analyze and returns arrays kind
  * @return 0 if TString (or equivalent), positive value - some basic type, -1 - any other kind
  * @private */
function getArrayKind(type_name) {
   if ((type_name === clTString) || (type_name === 'string') ||
      (CustomStreamers[type_name] === clTString))
      return 0;
   if ((type_name.length < 7) || type_name.indexOf('TArray'))
      return -1;
   if (type_name.length === 7) {
      switch (type_name[6]) {
         case 'I': return kInt;
         case 'D': return kDouble;
         case 'F': return kFloat;
         case 'S': return kShort;
         case 'C': return kChar;
         case 'L': return kLong;
         default: return -1;
      }
   }

   return type_name === 'TArrayL64' ? kLong64 : -1;
}

// eslint-disable-next-line  prefer-const
let createPairStreamer;

/** @summary create element of the streamer
  * @private  */
function createStreamerElement(name, typename, file) {
   const elem = {
      _typename: clTStreamerElement, fName: name, fTypeName: typename,
      fType: 0, fSize: 0, fArrayLength: 0, fArrayDim: 0, fMaxIndex: [0, 0, 0, 0, 0],
      fXmin: 0, fXmax: 0, fFactor: 0
   };

   if (isStr(typename)) {
      elem.fType = getTypeId(typename);
      if ((elem.fType < 0) && file && file.fBasicTypes[typename])
         elem.fType = file.fBasicTypes[typename];
   } else {
      elem.fType = typename;
      typename = elem.fTypeName = BasicTypeNames[elem.fType] || 'int';
   }

   if (elem.fType > 0)
      return elem; // basic type

   // check if there are STL containers
   const pos = typename.indexOf('<');
   let stltype = kNotSTL;
   if ((pos > 0) && (typename.indexOf('>') > pos + 2)) {
      for (let stl = 1; stl < StlNames.length; ++stl) {
         if (typename.slice(0, pos) === StlNames[stl]) {
            stltype = stl;
            break;
         }
      }
   }

   if (stltype !== kNotSTL) {
      elem._typename = clTStreamerSTL;
      elem.fType = kStreamer;
      elem.fSTLtype = stltype;
      elem.fCtype = 0;
      return elem;
   }

   if ((pos > 0) && (typename.slice(0, pos) === 'pair') && file && isFunc(createPairStreamer))
      createPairStreamer(typename, file);

   const isptr = typename.at(-1) === '*';

   if (isptr)
      elem.fTypeName = typename = typename.slice(0, typename.length - 1);

   if (getArrayKind(typename) === 0) {
      elem.fType = kTString;
      return elem;
   }

   elem.fType = isptr ? kAnyP : kAny;

   return elem;
}

/** @summary Function to read vector element in the streamer
  * @private */
function readVectorElement(buf) {
   if (this.member_wise) {
      const n = buf.ntou4(), ver = this.stl_version;

      if (n === 0)
         return []; // for empty vector no need to search split streamers

      if (n > 1000000)
         throw new Error(`member-wise streaming of ${this.conttype} num ${n} member ${this.name}`);

      let streamer;

      if ((ver.val === this.member_ver) && (ver.checksum === this.member_checksum))
         streamer = this.member_streamer;
      else {
         streamer = buf.fFile.getStreamer(this.conttype, ver);

         this.member_streamer = streamer = buf.fFile.getSplittedStreamer(streamer);
         this.member_ver = ver.val;
         this.member_checksum = ver.checksum;
      }

      const res = new Array(n);
      let i, k, member;

      for (i = 0; i < n; ++i)
         res[i] = { _typename: this.conttype }; // create objects
      if (!streamer)
         console.error(`Fail to create split streamer for ${this.conttype} need to read ${n} objects version ${ver}`);
      else {
         for (k = 0; k < streamer.length; ++k) {
            member = streamer[k];
            if (member.split_func)
               member.split_func(buf, res, n);
            else {
               for (i = 0; i < n; ++i)
                  member.func(buf, res[i]);
            }
         }
      }
      return res;
   }

   const n = buf.ntou4(), res = new Array(n);
   let i = 0;

   if (n > 200000) {
      console.error(`vector streaming for ${this.conttype} at ${n}`);
      return res;
   }

   if (this.arrkind > 0) {
      while (i < n)
         res[i++] = buf.readFastArray(buf.ntou4(), this.arrkind);
   } else if (this.arrkind === 0) {
      while (i < n)
         res[i++] = buf.readTString();
   } else if (this.isptr) {
      while (i < n)
         res[i++] = buf.readObjectAny();
   } else if (this.submember) {
      while (i < n)
         res[i++] = this.submember.readelem(buf);
   } else {
      while (i < n)
         res[i++] = buf.classStreamer({}, this.conttype);
   }

   return res;
}

/** @summary Create streamer info for pair object
  * @private */
createPairStreamer = function(typename, file) {
   let si = file.findStreamerInfo(typename);
   if (si)
      return si;
   let p1 = typename.indexOf('<');
   const p2 = typename.lastIndexOf('>');
   function getNextName() {
      let res = '', p = p1 + 1, cnt = 0;
      while ((p < p2) && (cnt >= 0)) {
         switch (typename[p]) {
            case '<':
               cnt++;
               break;
            case ',':
               if (cnt === 0)
                  cnt--;
               break;
            case '>':
               cnt--;
               break;
         }
         if (cnt >= 0)
            res += typename[p];
         p++;
      }
      p1 = p - 1;
      return res.trim();
   }
   si = { _typename: 'TStreamerInfo', fClassVersion: 0, fName: typename, fElements: create$1(clTList) };
   si.fElements.Add(createStreamerElement('first', getNextName(), file));
   si.fElements.Add(createStreamerElement('second', getNextName(), file));
   file.fStreamerInfos.arr.push(si);
   return si;
};

/** @summary Function creates streamer for std::pair object
  * @private */
function getPairStreamer(si, typname, file) {
   if (!si)
      si = createPairStreamer(typname, file);

   const streamer = file.getStreamer(typname, null, si);
   if (!streamer)
      return null;

   if (streamer.length !== 2) {
      console.error(`Streamer for pair class contains ${streamer.length} elements`);
      return null;
   }

   for (let nn = 0; nn < 2; ++nn) {
      if (streamer[nn].readelem && !streamer[nn].pair_name) {
         streamer[nn].pair_name = (nn === 0) ? 'first' : 'second';
         streamer[nn].func = function(buf, obj) {
            obj[this.pair_name] = this.readelem(buf);
         };
      }
   }

   return streamer;
}

/** @summary Function used in streamer to read std::map object
  * @private */
function readMapElement(buf) {
   let streamer = this.streamer;

   if (this.member_wise) {
      // when member-wise streaming is used, version is written
      const si = buf.fFile.findStreamerInfo(this.pairtype, this.stl_version.val, this.stl_version.checksum);

      if (si && (this.si !== si)) {
         streamer = getPairStreamer(si, this.pairtype, buf.fFile);
         if (streamer?.length !== 2) {
            console.log(`Fail to produce streamer for ${this.pairtype}`);
            return null;
         }
      }
   }

   const n = buf.ntoi4(), res = new Array(n);

   // no extra data written for empty map
   if (n === 0)
      return res;

   if (this.member_wise && (buf.remain() >= 6)) {
      if (buf.ntoi2() === kStreamedMemberWise)
         buf.shift(4); // skip checksum
      else
         buf.shift(-2); // rewind
   }

   for (let i = 0; i < n; ++i) {
      res[i] = { _typename: this.pairtype };
      streamer[0].func(buf, res[i]);
      if (!this.member_wise)
         streamer[1].func(buf, res[i]);
   }

   // due-to member-wise streaming second element read after first is completed
   if (this.member_wise) {
      if (buf.remain() >= 6) {
         if (buf.ntoi2() === kStreamedMemberWise)
            buf.shift(4);  // skip checksum
         else
            buf.shift(-2);  // rewind
      }
      for (let i = 0; i < n; ++i)
         streamer[1].func(buf, res[i]);
   }

   return res;
}


/** @summary create member entry for streamer element
  * @desc used for reading of data
  * @private */
function createMemberStreamer(element, file, no_string) {
   const member = {
      name: element.fName, type: element.fType,
      fArrayLength: element.fArrayLength,
      fArrayDim: element.fArrayDim,
      fMaxIndex: element.fMaxIndex
   };

   if (element.fTypeName === kBaseClass) {
      if (getArrayKind(member.name) > 0) {
         // this is workaround for arrays as base class
         // we create 'fArray' member, which read as any other data member
         member.name = 'fArray';
         member.type = kAny;
      } else {
         // create streamer for base class
         member.type = kBase;
         // this.getStreamer(element.fName);
      }
   }

   switch (member.type) {
      case kBase:
         member.base = element.fBaseVersion; // indicate base class
         member.basename = element.fName; // keep class name
         member.func = function(buf, obj) { buf.classStreamer(obj, this.basename); };
         break;
      case kShort:
         member.func = function(buf, obj) { obj[this.name] = buf.ntoi2(); };
         break;
      case kInt:
      case kCounter:
         member.func = function(buf, obj) { obj[this.name] = buf.ntoi4(); };
         break;
      case kLong:
      case kLong64:
         member.func = function(buf, obj) { obj[this.name] = buf.ntoi8(); };
         break;
      case kDouble:
         member.func = function(buf, obj) { obj[this.name] = buf.ntod(); };
         break;
      case kFloat:
         member.func = function(buf, obj) { obj[this.name] = buf.ntof(); };
         break;
      case kLegacyChar:
      case kUChar:
         member.func = function(buf, obj) { obj[this.name] = buf.ntou1(); };
         break;
      case kUShort:
         member.func = function(buf, obj) { obj[this.name] = buf.ntou2(); };
         break;
      case kBits:
      case kUInt:
         member.func = function(buf, obj) { obj[this.name] = buf.ntou4(); };
         break;
      case kULong64:
      case kULong:
         member.func = function(buf, obj) { obj[this.name] = buf.ntou8(); };
         break;
      case kBool:
         member.func = function(buf, obj) { obj[this.name] = buf.ntobool(); };
         break;
      case kOffsetL + kBool:
      case kOffsetL + kInt:
      case kOffsetL + kCounter:
      case kOffsetL + kDouble:
      case kOffsetL + kChar:
      case kOffsetL + kUChar:
      case kOffsetL + kShort:
      case kOffsetL + kUShort:
      case kOffsetL + kBits:
      case kOffsetL + kUInt:
      case kOffsetL + kULong:
      case kOffsetL + kULong64:
      case kOffsetL + kLong:
      case kOffsetL + kLong64:
      case kOffsetL + kFloat:
         if (element.fArrayDim < 2) {
            member.arrlength = element.fArrayLength;
            if ((member.type !== kOffsetL + kChar) || no_string) {
               member.func = function(buf, obj) {
                  obj[this.name] = buf.readFastArray(this.arrlength, this.type - kOffsetL);
               };
            } else {
               member.func = function(buf, obj) {
                  obj[this.name] = buf.readFastString(this.arrlength);
               };
            }
         } else {
            member.minus1 = true;
            member.arrlength = element.fMaxIndex[element.fArrayDim - 1];
            if ((member.type !== kOffsetL + kChar) || no_string) {
               member.func = function(buf, obj) {
                  obj[this.name] = buf.readNdimArray(this, (buf2, handle) =>
                     buf2.readFastArray(handle.arrlength, handle.type - kOffsetL));
               };
            } else {
               member.func = function(buf, obj) {
                  obj[this.name] = buf.readNdimArray(this, (buf2, handle) =>
                     buf2.readFastString(handle.arrlength));
               };
            }
         }
         break;
      case kOffsetP + kBool:
      case kOffsetP + kInt:
      case kOffsetP + kDouble:
      case kOffsetP + kChar:
      case kOffsetP + kUChar:
      case kOffsetP + kShort:
      case kOffsetP + kUShort:
      case kOffsetP + kBits:
      case kOffsetP + kUInt:
      case kOffsetP + kULong:
      case kOffsetP + kULong64:
      case kOffsetP + kLong:
      case kOffsetP + kLong64:
      case kOffsetP + kFloat:
         member.cntname = element.fCountName;
         if ((member.type !== kOffsetP + kChar) || no_string) {
            member.func = function(buf, obj) {
               obj[this.name] = (buf.ntou1() === 1) ? buf.readFastArray(obj[this.cntname], this.type - kOffsetP) : [];
            };
         } else {
            member.func = function(buf, obj) {
               obj[this.name] = (buf.ntou1() === 1) ? buf.readFastString(obj[this.cntname]) : null;
            };
         }
         break;
      case kDouble32:
      case kOffsetL + kDouble32:
      case kOffsetP + kDouble32:
         member.double32 = true;
      // eslint-disable-next-line  no-fallthrough
      case kFloat16:
      case kOffsetL + kFloat16:
      case kOffsetP + kFloat16:
         if (element.fFactor) {
            member.factor = 1 / element.fFactor;
            member.min = element.fXmin;
            member.read = function(buf) { return buf.ntou4() * this.factor + this.min; };
         } else
            if ((element.fXmin === 0) && member.double32)
               member.read = function(buf) { return buf.ntof(); };
            else {
               member.nbits = Math.round(element.fXmin);
               if (member.nbits === 0)
                  member.nbits = 12;
               member.dv = new DataView(new ArrayBuffer(8), 0); // used to cast from uint32 to float32
               member.read = function(buf) {
                  const theExp = buf.ntou1(), theMan = buf.ntou2();
                  this.dv.setUint32(0, (theExp << 23) | ((theMan & ((1 << (this.nbits + 1)) - 1)) << (23 - this.nbits)));
                  return ((1 << (this.nbits + 1) & theMan) ? -1 : 1) * this.dv.getFloat32(0);
               };
            }

         member.readarr = function(buf, len) {
            const arr = this.double32 ? new Float64Array(len) : new Float32Array(len);
            for (let n = 0; n < len; ++n)
               arr[n] = this.read(buf);
            return arr;
         };

         if (member.type < kOffsetL)
            member.func = function(buf, obj) { obj[this.name] = this.read(buf); };
         else
            if (member.type > kOffsetP) {
               member.cntname = element.fCountName;
               member.func = function(buf, obj) {
                  obj[this.name] = (buf.ntou1() === 1) ? this.readarr(buf, obj[this.cntname]) : null;
               };
            } else
               if (element.fArrayDim < 2) {
                  member.arrlength = element.fArrayLength;
                  member.func = function(buf, obj) { obj[this.name] = this.readarr(buf, this.arrlength); };
               } else {
                  member.arrlength = element.fMaxIndex[element.fArrayDim - 1];
                  member.minus1 = true;
                  member.func = function(buf, obj) {
                     obj[this.name] = buf.readNdimArray(this, (buf2, handle) => handle.readarr(buf2, handle.arrlength));
                  };
               }
         break;

      case kAnyP:
      case kObjectP:
         member.func = function(buf, obj) {
            obj[this.name] = buf.readNdimArray(this, buf2 => buf2.readObjectAny());
         };
         break;

      case kAny:
      case kAnyp:
      case kObjectp:
      case kObject: {
         let classname = (element.fTypeName === kBaseClass) ? element.fName : element.fTypeName;
         if (classname.at(-1) === '*')
            classname = classname.slice(0, classname.length - 1);

         const arrkind = getArrayKind(classname);

         if (arrkind > 0) {
            member.arrkind = arrkind;
            member.func = function(buf, obj) { obj[this.name] = buf.readFastArray(buf.ntou4(), this.arrkind); };
         } else if (arrkind === 0)
            member.func = function(buf, obj) { obj[this.name] = buf.readTString(); };
         else {
            member.classname = classname;

            if (element.fArrayLength > 1) {
               member.func = function(buf, obj) {
                  obj[this.name] = buf.readNdimArray(this, (buf2, handle) => buf2.classStreamer({}, handle.classname));
               };
            } else {
               member.func = function(buf, obj) {
                  obj[this.name] = buf.classStreamer({}, this.classname);
               };
            }
         }
         break;
      }
      case kOffsetL + kObject:
      case kOffsetL + kAny:
      case kOffsetL + kAnyp:
      case kOffsetL + kObjectp: {
         let classname = element.fTypeName;
         if (classname.at(-1) === '*')
            classname = classname.slice(0, classname.length - 1);

         member.arrkind = getArrayKind(classname);
         if (member.arrkind < 0)
            member.classname = classname;
         member.func = function(buf, obj) {
            obj[this.name] = buf.readNdimArray(this, (buf2, handle) => {
               if (handle.arrkind > 0)
                  return buf2.readFastArray(buf.ntou4(), handle.arrkind);
               if (handle.arrkind === 0)
                  return buf2.readTString();
               return buf2.classStreamer({}, handle.classname);
            });
         };
         break;
      }
      case kChar:
         member.func = function(buf, obj) { obj[this.name] = buf.ntoi1(); };
         break;
      case kCharStar:
         member.func = function(buf, obj) {
            const len = buf.ntoi4();
            obj[this.name] = buf.substring(buf.o, buf.o + len);
            buf.o += len;
         };
         break;
      case kTString:
         member.func = function(buf, obj) { obj[this.name] = buf.readTString(); };
         break;
      case kTObject:
      case kTNamed:
         member.typename = element.fTypeName;
         member.func = function(buf, obj) { obj[this.name] = buf.classStreamer({}, this.typename); };
         break;
      case kOffsetL + kTString:
      case kOffsetL + kTObject:
      case kOffsetL + kTNamed:
         member.typename = element.fTypeName;
         member.func = function(buf, obj) {
            const ver = buf.readVersion();
            obj[this.name] = buf.readNdimArray(this, (buf2, handle) => {
               if (handle.typename === clTString)
                  return buf2.readTString();
               return buf2.classStreamer({}, handle.typename);
            });
            buf.checkByteCount(ver, this.typename + '[]');
         };
         break;
      case kStreamLoop:
      case kOffsetL + kStreamLoop:
         member.typename = element.fTypeName;
         member.cntname = element.fCountName;

         if (member.typename.lastIndexOf('**') > 0) {
            member.typename = member.typename.slice(0, member.typename.lastIndexOf('**'));
            member.isptrptr = true;
         } else {
            member.typename = member.typename.slice(0, member.typename.lastIndexOf('*'));
            member.isptrptr = false;
         }

         if (member.isptrptr)
            member.readitem = function(buf) { return buf.readObjectAny(); };
         else {
            member.arrkind = getArrayKind(member.typename);
            if (member.arrkind > 0)
               member.readitem = function(buf) { return buf.readFastArray(buf.ntou4(), this.arrkind); };
            else if (member.arrkind === 0)
               member.readitem = function(buf) { return buf.readTString(); };
            else
               member.readitem = function(buf) { return buf.classStreamer({}, this.typename); };
         }

         if (member.readitem !== undefined) {
            member.read_loop = function(buf, cnt) {
               return buf.readNdimArray(this, (buf2, member2) => {
                  const itemarr = new Array(cnt);
                  for (let i = 0; i < cnt; ++i)
                     itemarr[i] = member2.readitem(buf2);
                  return itemarr;
               });
            };

            member.func = function(buf, obj) {
               const ver = buf.readVersion(),
                     res = this.read_loop(buf, obj[this.cntname]);
               obj[this.name] = buf.checkByteCount(ver, this.typename) ? res : null;
            };
            member.branch_func = function(buf, obj) {
               // this is special functions, used by branch in the STL container
               const ver = buf.readVersion(), sz0 = obj[this.stl_size], res = new Array(sz0);

               for (let loop0 = 0; loop0 < sz0; ++loop0) {
                  const cnt = obj[this.cntname][loop0];
                  res[loop0] = this.read_loop(buf, cnt);
               }
               obj[this.name] = buf.checkByteCount(ver, this.typename) ? res : null;
            };

            member.objs_branch_func = function(buf, obj) {
               // special function when branch read as part of complete object
               // objects already preallocated and only appropriate member must be set
               // see code in JSRoot.tree.js for reference

               const ver = buf.readVersion(),
                     arr = obj[this.name0]; // objects array where reading is done

               for (let loop0 = 0; loop0 < arr.length; ++loop0) {
                  const obj1 = this.get(arr, loop0), cnt = obj1[this.cntname];
                  obj1[this.name] = this.read_loop(buf, cnt);
               }

               buf.checkByteCount(ver, this.typename);
            };
         } else {
            console.error(`fail to provide function for ${element.fName} (${element.fTypeName})  typ = ${element.fType}`);
            member.func = function(buf, obj) {
               const ver = buf.readVersion();
               buf.checkByteCount(ver);
               obj[this.name] = null;
            };
         }

         break;

      case kStreamer: {
         member.typename = element.fTypeName;

         const stl = (element.fSTLtype || 0) % 40;
         if ((element._typename === 'TStreamerSTLstring') ||
            (member.typename === 'string') || (member.typename === 'string*'))
            member.readelem = buf => buf.readTString();
         else if ((stl === kSTLvector) || (stl === kSTLlist) ||
                    (stl === kSTLdeque) || (stl === kSTLset) || (stl === kSTLmultiset)) {
            const p1 = member.typename.indexOf('<'),
                  p2 = member.typename.lastIndexOf('>');

            member.conttype = member.typename.slice(p1 + 1, p2).trim();
            member.typeid = getTypeId(member.conttype);
            if ((member.typeid < 0) && file.fBasicTypes[member.conttype]) {
               member.typeid = file.fBasicTypes[member.conttype];
               console.log(`Reuse basic type ${member.conttype} from file streamer infos`);
            }

            // check
            if (element.fCtype && (element.fCtype < 20) && (element.fCtype !== member.typeid)) {
               console.warn(`Contained type ${member.conttype} not recognized as basic type ${element.fCtype} FORCE`);
               member.typeid = element.fCtype;
            }

            if (member.typeid > 0) {
               member.readelem = function(buf) {
                  return buf.readFastArray(buf.ntoi4(), this.typeid);
               };
            } else {
               member.isptr = false;

               if (member.conttype.at(-1) === '*') {
                  member.isptr = true;
                  member.conttype = member.conttype.slice(0, member.conttype.length - 1);
               }

               if (element.fCtype === kObjectp)
                  member.isptr = true;

               member.arrkind = getArrayKind(member.conttype);

               member.readelem = readVectorElement;

               if (!member.isptr && (member.arrkind < 0)) {
                  const subelem = createStreamerElement('temp', member.conttype, file);
                  if (subelem.fType === kStreamer) {
                     subelem.$fictional = true;
                     member.submember = createMemberStreamer(subelem, file);
                  }
               }
            }
         } else if ((stl === kSTLmap) || (stl === kSTLmultimap)) {
            const p1 = member.typename.indexOf('<'),
                  p2 = member.typename.lastIndexOf('>');

            member.pairtype = 'pair<' + member.typename.slice(p1 + 1, p2) + '>';

            // remember found streamer info from the file -
            // most probably it is the only one which should be used
            member.si = file.findStreamerInfo(member.pairtype);

            member.streamer = getPairStreamer(member.si, member.pairtype, file);

            if (!member.streamer || (member.streamer.length !== 2)) {
               console.error(`Fail to build streamer for pair ${member.pairtype}`);
               delete member.streamer;
            }

            if (member.streamer)
               member.readelem = readMapElement;
         } else if (stl === kSTLbitset)
            member.readelem = (buf /* , obj */) => buf.readFastArray(buf.ntou4(), kBool);

         if (!member.readelem) {
            console.error(`failed to create streamer for element ${member.typename} ${member.name} element ${element._typename} STL type ${element.fSTLtype}`);
            member.func = function(buf, obj) {
               const ver = buf.readVersion();
               buf.checkByteCount(ver);
               obj[this.name] = null;
            };
         } else if (!element.$fictional) {
            member.read_version = function(buf, cnt) {
               if (cnt === 0)
                  return null;
               const ver = buf.readVersion();
               this.member_wise = Boolean(ver.val & kStreamedMemberWise);

               this.stl_version = undefined;
               if (this.member_wise) {
                  ver.val &= ~kStreamedMemberWise;
                  this.stl_version = { val: buf.ntoi2() };
                  if (this.stl_version.val <= 0)
                     this.stl_version.checksum = buf.ntou4();
               }
               return ver;
            };
            member.func = function(buf, obj) {
               const ver = this.read_version(buf),
                     res = buf.readNdimArray(this, (buf2, member2) => member2.readelem(buf2));
               obj[this.name] = buf.checkByteCount(ver, this.typename) ? res : null;
            };
            member.branch_func = function(buf, obj) {
               // special function to read data from STL branch
               const cnt = obj[this.stl_size],
                     ver = this.read_version(buf, cnt),
                     arr = new Array(cnt);

               for (let n = 0; n < cnt; ++n)
                  arr[n] = buf.readNdimArray(this, (buf2, member2) => member2.readelem(buf2));

               if (ver)
                  buf.checkByteCount(ver, `branch ${this.typename}`);

               obj[this.name] = arr;
            };
            member.split_func = function(buf, arr, n) {
               // function to read array from member-wise streaming
               const ver = this.read_version(buf);
               for (let i = 0; i < n; ++i)
                  arr[i][this.name] = buf.readNdimArray(this, (buf2, member2) => member2.readelem(buf2));
               buf.checkByteCount(ver, this.typename);
            };
            member.objs_branch_func = function(buf, obj) {
               // special function when branch read as part of complete object
               // objects already preallocated and only appropriate member must be set
               // see code in JSRoot.tree.js for reference

               const arr = obj[this.name0], // objects array where reading is done
                     ver = this.read_version(buf, arr.length);

               for (let n = 0; n < arr.length; ++n) {
                  const obj1 = this.get(arr, n);
                  obj1[this.name] = buf.readNdimArray(this, (buf2, member2) => member2.readelem(buf2));
               }

               if (ver)
                  buf.checkByteCount(ver, `branch ${this.typename}`);
            };
         }
         break;
      }

      default:
         console.error(`fail to provide function for ${element.fName} (${element.fTypeName})  typ = ${element.fType}`);

         member.func = function(/* buf, obj */) {};  // do nothing, fix in the future
   }

   return member;
}


/** @summary Let directly assign methods when doing I/O
  * @private */
function addClassMethods(clname, streamer) {
   if (streamer === null)
      return streamer;

   const methods = getMethods(clname);

   if (methods) {
      for (const key in methods) {
         if (isFunc(methods[key]) || (key.indexOf('_') === 0))
            streamer.push({ name: key, method: methods[key], func(_buf, obj) { obj[this.name] = this.method; } });
      }
   }

   return streamer;
}


/* Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp>
 * Version: 1.0.0.1
 * LastModified: Dec 25 1999
 * original: http://www.onicos.com/staff/iz/amuse/javascript/expert/inflate.txt
 */

/* constant parameters */
const
   zip_WSIZE = 32768,       // Sliding Window size

   /* constant tables (inflate) */
   zip_MASK_BITS = [0x0000, 0x0001, 0x0003, 0x0007, 0x000f, 0x001f, 0x003f, 0x007f, 0x00ff,
                    0x01ff, 0x03ff, 0x07ff, 0x0fff, 0x1fff, 0x3fff, 0x7fff, 0xffff],

   // Tables for deflate from PKZIP's appnote.txt.
   // Copy lengths for literal codes 257..285
   zip_cplens = [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
                 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0],

   /* note: see note #13 above about the 258 in this list. */
   // Extra bits for literal codes 257..285
   zip_cplext = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2,
                 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 99, 99], // 99==invalid

   // Copy offsets for distance codes 0..29
   zip_cpdist = [1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
                 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
                 8193, 12289, 16385, 24577],

   // Extra bits for distance codes
   zip_cpdext = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
                 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13],

   // Order of the bit length code lengths
   zip_border = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15];

function ZIP_inflate(arr, tgt) {
   /* variables (inflate) */
   const zip_slide = new Array(2 * zip_WSIZE),
         zip_inflate_data = arr,
         zip_inflate_datalen = arr.byteLength;
   let zip_wp = 0,                // current position in slide
       zip_fixed_tl = null,      // inflate static
       zip_fixed_td,             // inflate static
       zip_fixed_bl, zip_fixed_bd,   // inflate static
       zip_bit_buf = 0,            // bit buffer
       zip_bit_len = 0,           // bits in bit buffer
       zip_method = -1,
       zip_eof = false,
       zip_copy_leng = 0,
       zip_copy_dist = 0,
       zip_tl = null, zip_td,    // literal/length and distance decoder tables
       zip_bl, zip_bd,           // number of bits decoded by tl and td
       zip_inflate_pos = 0;

   function zip_NEEDBITS(n) {
      while (zip_bit_len < n) {
         if (zip_inflate_pos < zip_inflate_datalen)
            zip_bit_buf |= zip_inflate_data[zip_inflate_pos++] << zip_bit_len;
         zip_bit_len += 8;
      }
   }

   function zip_GETBITS(n) {
      return zip_bit_buf & zip_MASK_BITS[n];
   }

   function zip_DUMPBITS(n) {
      zip_bit_buf >>= n;
      zip_bit_len -= n;
   }

   /* objects (inflate) */
   function zip_HuftBuild(b,     // code lengths in bits (all assumed <= BMAX)
                          n,     // number of codes (assumed <= N_MAX)
                          s,     // number of simple-valued codes (0..s-1)
                          d,     // list of base values for non-simple codes
                          e,     // list of extra bits for non-simple codes
                          mm) {  // maximum lookup bits
      const BMAX = 16,      // maximum bit length of any code
            N_MAX = 288,    // maximum number of codes in any set
            c = Array(BMAX + 1).fill(0),  // bit length count table
            lx = Array(BMAX + 1).fill(0), // stack of bits per table
            u = Array(BMAX).fill(null), // zip_HuftNode[BMAX][]  table stack
            v = Array(N_MAX).fill(0), // values in order of bit length
            x = Array(BMAX + 1).fill(0), // bit offsets, then code stack
            r = { e: 0, b: 0, n: 0, t: null }, // new zip_HuftNode(), // table entry for structure assignment
            el = (n > 256) ? b[256] : BMAX, // set length of EOB code, if any
            res = {
               status: 0,    // 0: success, 1: incomplete table, 2: bad input
               root: null,   // (zip_HuftList) starting table
               m: 0          // maximum lookup bits, returns actual
            };
      let rr,        // temporary variable, use in assignment
          a,         // counter for codes of length k
          f,         // i repeats in table every f entries
          h,         // table level
          j,         // counter
          k,         // number of bits in current code
          p = b,     // pointer into c[], b[], or v[]
          pidx = 0,  // index of p
          q,         // (zip_HuftNode) points to current table
          w,
          xp,        // pointer into x or c
          y,         // number of dummy codes added
          z,         // number of entries in current table
          o,
          tail = null,   // (zip_HuftList)
          i = n;         // counter, current code

      // Generate counts for each bit length
      do
         c[p[pidx++]]++; // assume all entries <= BMAX
      while (--i > 0);

      if (c[0] === n)    // null input--all zero length codes
         return res;

      // Find minimum and maximum length, bound *m by those
      for (j = 1; j <= BMAX && !c[j]; ++j);

      k = j;         // minimum code length
      if (mm < j)
         mm = j;
      for (i = BMAX; i && !c[i]; --i);

      const g = i;         // maximum code length
      if (mm > i)
         mm = i;

      // Adjust last length count to fill out codes, if needed
      for (y = 1 << j; j < i; ++j, y <<= 1) {
         if ((y -= c[j]) < 0) {
            res.status = 2;  // bad input: more codes than bits
            res.m = mm;
            return res;
         }
      }
      if ((y -= c[i]) < 0) {
         res.status = 2;
         res.m = mm;
         return res;
      }
      c[i] += y;

      // Generate starting offsets into the value table for each length
      x[1] = j = 0;
      p = c;
      pidx = 1;
      xp = 2;
      while (--i > 0)    // note that i == g from above
         x[xp++] = (j += p[pidx++]);

      // Make a table of values in order of bit lengths
      p = b;
      pidx = 0;
      i = 0;
      do {
         if ((j = p[pidx++]))
            v[x[j]++] = i;
      } while (++i < n);
      n = x[g];         // set n to length of v

      // Generate the Huffman codes and for each, make the table entries
      x[0] = i = 0;     // first Huffman code is zero
      p = v;
      pidx = 0;     // grab values in bit order
      h = -1;        // no tables yet--level -1
      w = lx[0] = 0;    // no bits decoded yet
      q = null;         // ditto
      z = 0;         // ditto

      // go through the bit lengths (k already is bits in shortest code)
      for (; k <= g; ++k) {
         a = c[k];
         while (a-- > 0) {
            // here i is the Huffman code of length k bits for value p[pidx]
            // make tables up to required level
            while (k > w + lx[1 + h]) {
               w += lx[1 + h++]; // add bits already decoded

               // compute minimum size table less than or equal to *m bits
               z = (z = g - w) > mm ? mm : z; // upper limit
               if ((f = 1 << (j = k - w)) > a + 1) { // try a k-w bit table
                  // too few codes for k-w bit table
                  f -= a + 1; // deduct codes from patterns left
                  xp = k;
                  while (++j < z) { // try smaller tables up to z bits
                     if ((f <<= 1) <= c[++xp])
                        break;   // enough codes to use up j bits
                     f -= c[xp];   // else deduct codes from patterns
                  }
               }
               if (w + j > el && w < el)
                  j = el - w; // make EOB code end at table
               z = 1 << j;   // table entries for j-bit table
               lx[1 + h] = j; // set table size in stack

               // allocate and link in new table
               q = new Array(z);
               for (o = 0; o < z; ++o)
                  q[o] = { e: 0, b: 0, n: 0, t: null }; // new zip_HuftNode

               if (tail === null)
                  tail = res.root = { next: null, list: null }; // new zip_HuftList();
               else
                  tail = tail.next = { next: null, list: null }; // new zip_HuftList();
               tail.next = null;
               tail.list = q;
               u[h] = q;  // table starts after link

               /* connect to last table, if there is one */
               if (h > 0) {
                  x[h] = i;      // save pattern for backing up
                  r.b = lx[h];   // bits to dump before this table
                  r.e = 16 + j;  // bits in this table
                  r.t = q;    // pointer to this table
                  j = (i & ((1 << w) - 1)) >> (w - lx[h]);
                  rr = u[h - 1][j];
                  rr.e = r.e;
                  rr.b = r.b;
                  rr.n = r.n;
                  rr.t = r.t;
               }
            }

            // set up table entry in r
            r.b = k - w;
            if (pidx >= n)
               r.e = 99;     // out of values--invalid code
            else if (p[pidx] < s) {
               r.e = (p[pidx] < 256 ? 16 : 15); // 256 is end-of-block code
               r.n = p[pidx++]; // simple code is just the value
            } else {
               r.e = e[p[pidx] - s];  // non-simple--look up in lists
               r.n = d[p[pidx++] - s];
            }

            // fill code-like entries with r //
            f = 1 << (k - w);
            for (j = i >> w; j < z; j += f) {
               rr = q[j];
               rr.e = r.e;
               rr.b = r.b;
               rr.n = r.n;
               rr.t = r.t;
            }

            // backwards increment the k-bit code i
            for (j = 1 << (k - 1); (i & j); j >>= 1)
               i ^= j;
            i ^= j;

            // backup over finished tables
            while ((i & ((1 << w) - 1)) !== x[h])
               w -= lx[h--];      // don't need to update q
         }
      }

      /* return actual size of base table */
      res.m = lx[1];

      /* Return true (1) if we were given an incomplete table */
      res.status = (y && g !== 1) ? 1 : 0;

      return res;
   }

   /* routines (inflate) */

   function zip_inflate_codes(buff, off, size) {
      if (size === 0)
         return 0;

      /* inflate (decompress) the codes in a deflated (compressed) block.
         Return an error code or zero if it all goes ok. */

      let e,     // table entry flag/number of extra bits
          t,     // (zip_HuftNode) pointer to table entry
          n = 0;

      // inflate the coded data
      for (;;) {        // do until end of block
         zip_NEEDBITS(zip_bl);
         t = zip_tl.list[zip_GETBITS(zip_bl)];
         e = t.e;
         while (e > 16) {
            if (e === 99)
               return -1;
            zip_DUMPBITS(t.b);
            e -= 16;
            zip_NEEDBITS(e);
            t = t.t[zip_GETBITS(e)];
            e = t.e;
         }
         zip_DUMPBITS(t.b);

         if (e === 16) {     // then it's a literal
            zip_wp &= zip_WSIZE - 1;
            buff[off + n++] = zip_slide[zip_wp++] = t.n;
            if (n === size)
               return size;
            continue;
         }

         // exit if end of block
         if (e === 15)
            break;

         // it's an EOB or a length

         // get length of block to copy
         zip_NEEDBITS(e);
         zip_copy_leng = t.n + zip_GETBITS(e);
         zip_DUMPBITS(e);

         // decode distance of block to copy
         zip_NEEDBITS(zip_bd);
         t = zip_td.list[zip_GETBITS(zip_bd)];
         e = t.e;

         while (e > 16) {
            if (e === 99)
               return -1;
            zip_DUMPBITS(t.b);
            e -= 16;
            zip_NEEDBITS(e);
            t = t.t[zip_GETBITS(e)];
            e = t.e;
         }
         zip_DUMPBITS(t.b);
         zip_NEEDBITS(e);
         zip_copy_dist = zip_wp - t.n - zip_GETBITS(e);
         zip_DUMPBITS(e);

         // do the copy
         while (zip_copy_leng > 0 && n < size) {
            --zip_copy_leng;
            zip_copy_dist &= zip_WSIZE - 1;
            zip_wp &= zip_WSIZE - 1;
            buff[off + n++] = zip_slide[zip_wp++] = zip_slide[zip_copy_dist++];
         }

         if (n === size)
            return size;
      }

      zip_method = -1; // done
      return n;
   }

   function zip_inflate_stored(buff, off, size) {
      /* 'decompress' an inflated type 0 (stored) block. */

      // go to byte boundary
      let n = zip_bit_len & 7;
      zip_DUMPBITS(n);

      // get the length and its complement
      zip_NEEDBITS(16);
      n = zip_GETBITS(16);
      zip_DUMPBITS(16);
      zip_NEEDBITS(16);
      if (n !== ((~zip_bit_buf) & 0xffff))
         return -1;        // error in compressed data
      zip_DUMPBITS(16);

      // read and output the compressed data
      zip_copy_leng = n;

      n = 0;
      while (zip_copy_leng > 0 && n < size) {
         --zip_copy_leng;
         zip_wp &= zip_WSIZE - 1;
         zip_NEEDBITS(8);
         buff[off + n++] = zip_slide[zip_wp++] = zip_GETBITS(8);
         zip_DUMPBITS(8);
      }

      if (zip_copy_leng === 0)
         zip_method = -1; // done
      return n;
   }

   function zip_inflate_fixed(buff, off, size) {
      /* decompress an inflated type 1 (fixed Huffman codes) block.  We should
         either replace this with a custom decoder, or at least pre-compute the
         Huffman tables. */

      // if first time, set up tables for fixed blocks
      if (zip_fixed_tl === null) {
         // literal table
         const l = Array(288).fill(8, 0, 144).fill(9, 144, 256).fill(7, 256, 280).fill(8, 280, 288);
         // make a complete, but wrong code set
         zip_fixed_bl = 7;

         let h = zip_HuftBuild(l, 288, 257, zip_cplens, zip_cplext, zip_fixed_bl);
         if (h.status)
            throw new Error('HufBuild error: ' + h.status);
         zip_fixed_tl = h.root;
         zip_fixed_bl = h.m;

         // distance table
         l.fill(5, 0, 30); // make an incomplete code set
         zip_fixed_bd = 5;

         h = zip_HuftBuild(l, 30, 0, zip_cpdist, zip_cpdext, zip_fixed_bd);
         if (h.status > 1) {
            zip_fixed_tl = null;
            throw new Error('HufBuild error: ' + h.status);
         }
         zip_fixed_td = h.root;
         zip_fixed_bd = h.m;
      }

      zip_tl = zip_fixed_tl;
      zip_td = zip_fixed_td;
      zip_bl = zip_fixed_bl;
      zip_bd = zip_fixed_bd;
      return zip_inflate_codes(buff, off, size);
   }

   function zip_inflate_dynamic(buff, off, size) {
      // decompress an inflated type 2 (dynamic Huffman codes) block.
      let i, j,  // temporary variables
          l,     // last length
          t,     // (zip_HuftNode) literal/length code table
          h;     // (zip_HuftBuild)
      const ll = new Array(286 + 30).fill(0); // literal/length and distance code lengths

      // read in table lengths
      zip_NEEDBITS(5);
      const nl = 257 + zip_GETBITS(5);   // number of literal/length codes
      zip_DUMPBITS(5);
      zip_NEEDBITS(5);
      const nd = 1 + zip_GETBITS(5);  // number of distance codes
      zip_DUMPBITS(5);
      zip_NEEDBITS(4);
      const nb = 4 + zip_GETBITS(4);  // number of bit length codes
      zip_DUMPBITS(4);
      if (nl > 286 || nd > 30)
         return -1;     // bad lengths

      // read in bit-length-code lengths
      for (j = 0; j < nb; ++j) {
         zip_NEEDBITS(3);
         ll[zip_border[j]] = zip_GETBITS(3);
         zip_DUMPBITS(3);
      }
      for (; j < 19; ++j)
         ll[zip_border[j]] = 0;

      // build decoding table for trees--single level, 7 bit lookup
      zip_bl = 7;
      h = zip_HuftBuild(ll, 19, 19, null, null, zip_bl);
      if (h.status)
         return -1;  // incomplete code set

      zip_tl = h.root;
      zip_bl = h.m;

      // read in literal and distance code lengths
      const n = nl + nd;   // number of lengths to get
      i = l = 0;
      while (i < n) {
         zip_NEEDBITS(zip_bl);
         t = zip_tl.list[zip_GETBITS(zip_bl)];
         j = t.b;
         zip_DUMPBITS(j);
         j = t.n;
         if (j < 16) // length of code in bits (0..15)
            ll[i++] = l = j; // save last length in l
         else if (j === 16) {   // repeat last length 3 to 6 times
            zip_NEEDBITS(2);
            j = 3 + zip_GETBITS(2);
            zip_DUMPBITS(2);
            if (i + j > n)
               return -1;
            while (j-- > 0)
               ll[i++] = l;
         } else if (j === 17) { // 3 to 10 zero length codes
            zip_NEEDBITS(3);
            j = 3 + zip_GETBITS(3);
            zip_DUMPBITS(3);
            if (i + j > n)
               return -1;
            while (j-- > 0)
               ll[i++] = 0;
            l = 0;
         } else {    // j == 18: 11 to 138 zero length codes
            zip_NEEDBITS(7);
            j = 11 + zip_GETBITS(7);
            zip_DUMPBITS(7);
            if (i + j > n)
               return -1;
            while (j-- > 0)
               ll[i++] = 0;
            l = 0;
         }
      }

      // build the decoding tables for literal/length and distance codes
      zip_bl = 9; // zip_lbits;
      h = zip_HuftBuild(ll, nl, 257, zip_cplens, zip_cplext, zip_bl);
      if (zip_bl === 0)  // no literals or lengths
         h.status = 1;
      if (h.status)
         return -1;     // incomplete code set
      zip_tl = h.root;
      zip_bl = h.m;

      for (i = 0; i < nd; ++i)
         ll[i] = ll[i + nl];
      zip_bd = 6; // zip_dbits;
      h = zip_HuftBuild(ll, nd, 0, zip_cpdist, zip_cpdext, zip_bd);
      zip_td = h.root;
      zip_bd = h.m;

      // incomplete distance tree
      if ((zip_bd === 0 && nl > 257) || h.status)   // lengths but no distances
         return -1;

      // decompress until an end-of-block code
      return zip_inflate_codes(buff, off, size);
   }

   function zip_inflate_internal(buff, off, size) {
      // decompress an inflated entry
      let n = 0, i;

      while (n < size) {
         if (zip_eof && zip_method === -1)
            return n;

         if (zip_copy_leng > 0) {
            if (zip_method /* zip_STORED_BLOCK */) {
               // STATIC_TREES or DYN_TREES
               while (zip_copy_leng > 0 && n < size) {
                  --zip_copy_leng;
                  zip_copy_dist &= zip_WSIZE - 1;
                  zip_wp &= zip_WSIZE - 1;
                  buff[off + n++] = zip_slide[zip_wp++] =
                  zip_slide[zip_copy_dist++];
               }
            } else {
               while (zip_copy_leng > 0 && n < size) {
                  --zip_copy_leng;
                  zip_wp &= zip_WSIZE - 1;
                  zip_NEEDBITS(8);
                  buff[off + n++] = zip_slide[zip_wp++] = zip_GETBITS(8);
                  zip_DUMPBITS(8);
               }
               if (zip_copy_leng === 0)
                  zip_method = -1; // done
            }
            if (n === size)
               return n;
         }

         if (zip_method === -1) {
            if (zip_eof)
               break;

            // read in last block bit
            zip_NEEDBITS(1);
            if (zip_GETBITS(1))
               zip_eof = true;
            zip_DUMPBITS(1);

            // read in block type
            zip_NEEDBITS(2);
            zip_method = zip_GETBITS(2);
            zip_DUMPBITS(2);
            zip_tl = null;
            zip_copy_leng = 0;
         }

         switch (zip_method) {
            case 0: // zip_STORED_BLOCK
               i = zip_inflate_stored(buff, off + n, size - n);
               break;

            case 1: // zip_STATIC_TREES
               if (zip_tl !== null)
                  i = zip_inflate_codes(buff, off + n, size - n);
               else
                  i = zip_inflate_fixed(buff, off + n, size - n);
               break;

            case 2: // zip_DYN_TREES
               if (zip_tl !== null)
                  i = zip_inflate_codes(buff, off + n, size - n);
               else
                  i = zip_inflate_dynamic(buff, off + n, size - n);
               break;

            default: // error
               i = -1;
               break;
         }

         if (i === -1)
            return zip_eof ? 0 : -1;
         n += i;
      }
      return n;
   }

   let i, cnt = 0;
   while ((i = zip_inflate_internal(tgt, cnt, Math.min(1024, tgt.byteLength - cnt))) > 0)
      cnt += i;

   return cnt;
} // function ZIP_inflate

/**
 * https://github.com/pierrec/node-lz4/blob/master/lib/binding.js
 *
 * LZ4 based compression and decompression
 * Copyright (c) 2014 Pierre Curto
 * MIT Licensed
 */

/**
 * Decode a block. Assumptions: input contains all sequences of a
 * chunk, output is large enough to receive the decoded data.
 * If the output buffer is too small, an error will be thrown.
 * If the returned value is negative, an error occurred at the returned offset.
 *
 * @param input {Buffer} input data
 * @param output {Buffer} output data
 * @return {Number} number of decoded bytes
 * @private */
function LZ4_uncompress(input, output, sIdx, eIdx) {
   sIdx = sIdx || 0;
   eIdx = eIdx || (input.length - sIdx);
   // Process each sequence in the incoming data
   let j = 0;
   for (let i = sIdx, n = eIdx; i < n;) {
      const token = input[i++];

      // Literals
      let literals_length = (token >> 4);
      if (literals_length > 0) {
         // length of literals
         let l = literals_length + 240;
         while (l === 255) {
            l = input[i++];
            literals_length += l;
         }

         // Copy the literals
         const end = i + literals_length;
         while (i < end)
            output[j++] = input[i++];

         // End of buffer?
         if (i === n)
            return j;
      }

      // Match copy
      // 2 bytes offset (little endian)
      const offset = input[i++] | (input[i++] << 8);

      // 0 is an invalid offset value
      if (offset === 0 || offset > j)
         return -(i - 2);

      // length of match copy
      let match_length = (token & 0xf),
          l = match_length + 240;
      while (l === 255) {
         l = input[i++];
         match_length += l;
      }

      // Copy the match
      let pos = j - offset; // position of the match copy in the current output
      const end = j + match_length + 4; // minmatch = 4;
      while (j < end)
         output[j++] = output[pos++];
   }

   return j;
}


/** @summary Reads header envelope, determines zipped size and unzip content
  * @return {Promise} with unzipped content
  * @private */
async function R__unzip(arr, tgtsize, noalert, src_shift) {
   const HDRSIZE = 9, totallen = arr.byteLength;

   let curr = src_shift || 0, fullres = 0, tgtbuf = null;

   const nextPortion = () => {
      while (fullres < tgtsize) {
         let fmt = 'unknown', off = 0, CHKSUM = 0;

         if (curr + HDRSIZE >= totallen) {
            if (!noalert)
               console.error('Error R__unzip: header size exceeds buffer size');
            return Promise.resolve(null);
         }

         const getCode = o => arr.getUint8(o),
               checkChar = (o, symb) => { return getCode(o) === symb.charCodeAt(0); },
               checkFmt = (a, b, c) => { return checkChar(curr, a) && checkChar(curr + 1, b) && (getCode(curr + 2) === c); };

         if (checkFmt('Z', 'L', 8)) {
            fmt = 'new';
            off = 2;
         } else if (checkFmt('C', 'S', 8))
            fmt = 'old';
         else if (checkFmt('X', 'Z', 0))
            fmt = 'LZMA';
         else if (checkFmt('Z', 'S', 1))
            fmt = 'ZSTD';
         else if (checkChar(curr, 'L') && checkChar(curr + 1, '4')) {
            fmt = 'LZ4';
            CHKSUM = 8;
         }

         /*   C H E C K   H E A D E R   */
         if ((fmt !== 'new') && (fmt !== 'old') && (fmt !== 'LZ4') && (fmt !== 'ZSTD') && (fmt !== 'LZMA')) {
            if (!noalert)
               console.error(`R__unzip: ${fmt} format is not supported!`);
            return Promise.resolve(null);
         }

         const srcsize = HDRSIZE + ((getCode(curr + 3) & 0xff) | ((getCode(curr + 4) & 0xff) << 8) | ((getCode(curr + 5) & 0xff) << 16)),
               uint8arr = new Uint8Array(arr.buffer, arr.byteOffset + curr + HDRSIZE + off + CHKSUM, Math.min(arr.byteLength - curr - HDRSIZE - off - CHKSUM, srcsize - HDRSIZE - CHKSUM));

         if (!tgtbuf)
            tgtbuf = new ArrayBuffer(tgtsize);
         const tgt8arr = new Uint8Array(tgtbuf, fullres);

         if (fmt === 'ZSTD') {
            let promise;
            if (internals._ZstdStream)
               promise = Promise.resolve(internals._ZstdStream);
            else if (internals._ZstdInit !== undefined)
               promise = new Promise(resolveFunc => { internals._ZstdInit.push(resolveFunc); });
            else {
               internals._ZstdInit = [];
               promise = (isNodeJs() ? Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }) : Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }))
                  .then(({ ZstdInit }) => ZstdInit())
                  .then(({ ZstdStream }) => {
                     internals._ZstdStream = ZstdStream;
                     internals._ZstdInit.forEach(func => func(ZstdStream));
                     delete internals._ZstdInit;
                     return ZstdStream;
                  });
            }

            return promise.then(ZstdStream => {
               const data2 = ZstdStream.decompress(uint8arr),
                     reslen = data2.length;

               for (let i = 0; i < reslen; ++i)
                  tgt8arr[i] = data2[i];

               fullres += reslen;
               curr += srcsize;
               return nextPortion();
            });
         } else if (fmt === 'LZMA') {
            return Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(lzma => {
               const expected_len = (getCode(curr + 6) & 0xff) | ((getCode(curr + 7) & 0xff) << 8) | ((getCode(curr + 8) & 0xff) << 16),
                     reslen = lzma.decompress(uint8arr, tgt8arr, expected_len);
               fullres += reslen;
               curr += srcsize;
               return nextPortion();
            });
         }

         const reslen = (fmt === 'LZ4') ? LZ4_uncompress(uint8arr, tgt8arr) : ZIP_inflate(uint8arr, tgt8arr);

         if (reslen <= 0)
            break;
         fullres += reslen;
         curr += srcsize;
      }

      if (fullres !== tgtsize) {
         if (!noalert)
            console.error(`R__unzip: fail to unzip data expects ${tgtsize}, got ${fullres}`);
         return Promise.resolve(null);
      }

      return Promise.resolve(new DataView(tgtbuf));
   };

   return nextPortion();
}


/**
  * @summary Buffer object to read data from TFile
  *
  * @private
  */

class TBuffer {

   constructor(arr, pos, file, length) {
      this._typename = 'TBuffer';
      this.arr = arr;
      this.o = pos || 0;
      this.fFile = file;
      this.length = length || (arr ? arr.byteLength : 0); // use size of array view, blob buffer can be much bigger
      this.clearObjectMap();
      this.fTagOffset = 0;
      this.last_read_version = 0;
   }

   /** @summary locate position in the buffer  */
   locate(pos) { this.o = pos; }

   /** @summary shift position in the buffer  */
   shift(cnt) { this.o += cnt; }

   /** @summary Returns remaining place in the buffer */
   remain() { return this.length - this.o; }

   /** @summary Get mapped object with provided tag */
   getMappedObject(tag) { return this.fObjectMap[tag]; }

   /** @summary Map object */
   mapObject(tag, obj) {
      if (obj !== null)
         this.fObjectMap[tag] = obj;
   }

   /** @summary Map class */
   mapClass(tag, classname) { this.fClassMap[tag] = classname; }

   /** @summary Get mapped class with provided tag */
   getMappedClass(tag) { return (tag in this.fClassMap) ? this.fClassMap[tag] : -1; }

   /** @summary Clear objects map */
   clearObjectMap() {
      this.fObjectMap = {};
      this.fClassMap = {};
      this.fObjectMap[0] = null;
      this.fDisplacement = 0;
   }

   /** @summary  read class version from I/O buffer */
   readVersion() {
      const ver = {}, bytecnt = this.ntou4(); // byte count

      if (bytecnt & kByteCountMask)
         ver.bytecnt = bytecnt - kByteCountMask - 2; // one can check between Read version and end of streamer
      else
         this.o -= 4; // rollback read bytes, this is old buffer without byte count

      this.last_read_version = ver.val = this.ntoi2();
      this.last_read_checksum = 0;
      ver.off = this.o;

      if ((ver.val <= 0) && ver.bytecnt && (ver.bytecnt >= 4)) {
         ver.checksum = this.ntou4();
         if (!this.fFile.findStreamerInfo(undefined, undefined, ver.checksum)) {
            // console.error(`Fail to find streamer info with check sum ${ver.checksum} version ${ver.val}`);
            this.o -= 4; // not found checksum in the list
            delete ver.checksum; // remove checksum
         } else
            this.last_read_checksum = ver.checksum;
      }
      return ver;
   }

   /** @summary Check bytecount after object streaming */
   checkByteCount(ver, where) {
      if ((ver.bytecnt !== undefined) && (ver.off + ver.bytecnt !== this.o)) {
         if (where)
            console.log(`Missmatch in ${where}:${ver.val} bytecount expected = ${ver.bytecnt}  got = ${this.o - ver.off}`);
         this.o = ver.off + ver.bytecnt;
         return false;
      }
      return true;
   }

   /** @summary Read TString object (or equivalent)
     * @desc std::string uses similar binary format */
   readTString() {
      let len = this.ntou1();
      // large strings
      if (len === 255)
         len = this.ntou4();
      if (!len)
         return '';

      const pos = this.o;
      this.o += len;

      return (this.codeAt(pos) === 0) ? '' : this.substring(pos, pos + len);
   }

   /** @summary read Char_t array as string
     * @desc stops when 0 is found */
   readNullTerminatedString() {
      let res = '', code;
      while ((code = this.ntou1()))
         res += String.fromCharCode(code);
      return res;
   }

   /** @summary read Char_t array as string */
   readFastString(n) {
      let res = '', reading = true;
      for (let i = 0; i < n; ++i) {
         const code = this.ntou1();
         if (code === 0)
            reading = false;
         if (reading)
            res += String.fromCharCode(code);
      }

      return res;
   }

   /** @summary read uint8_t */
   ntou1() { return this.arr.getUint8(this.o++); }

   /** @summary read boolean */
   ntobool() { return Boolean(this.arr.getUint8(this.o++)); }

   /** @summary read uint16_t */
   ntou2() {
      const o = this.o;
      this.o += 2;
      return this.arr.getUint16(o);
   }

   /** @summary read uint32_t */
   ntou4() {
      const o = this.o;
      this.o += 4;
      return this.arr.getUint32(o);
   }

   /** @summary read uint64_t */
   ntou8() {
      const high = this.arr.getUint32(this.o);
      this.o += 4;
      const low = this.arr.getUint32(this.o);
      this.o += 4;
      return (high < 0x200000) ? (high * 0x100000000 + low) : (BigInt(high) * BigInt(0x100000000) + BigInt(low));
   }

   /** @summary read int8_t */
   ntoi1() { return this.arr.getInt8(this.o++); }

   /** @summary read int16_t */
   ntoi2() {
      const o = this.o;
      this.o += 2;
      return this.arr.getInt16(o);
   }

   /** @summary read int32_t */
   ntoi4() {
      const o = this.o;
      this.o += 4;
      return this.arr.getInt32(o);
   }

   /** @summary read int64_t */
   ntoi8() {
      const high = this.arr.getUint32(this.o);
      this.o += 4;
      const low = this.arr.getUint32(this.o);
      this.o += 4;
      if (high < 0x80000000)
         return (high < 0x200000) ? (high * 0x100000000 + low) : (BigInt(high) * BigInt(0x100000000) + BigInt(low));
      return (~high < 0x200000) ? (-1 - ((~high) * 0x100000000 + ~low)) : (BigInt(-1) - (BigInt(~high) * BigInt(0x100000000) + BigInt(~low)));
   }

   /** @summary read float */
   ntof() {
      const o = this.o;
      this.o += 4;
      return this.arr.getFloat32(o);
   }

   /** @summary read double */
   ntod() {
      const o = this.o;
      this.o += 8;
      return this.arr.getFloat64(o);
   }

   /** @summary Reads array of n values from the I/O buffer */
   readFastArray(n, array_type) {
      let array, i = 0, o = this.o;
      const view = this.arr;
      switch (array_type) {
         case kDouble:
            array = new Float64Array(n);
            for (; i < n; ++i, o += 8)
               array[i] = view.getFloat64(o);
            break;
         case kFloat:
            array = new Float32Array(n);
            for (; i < n; ++i, o += 4)
               array[i] = view.getFloat32(o);
            break;
         case kLong:
         case kLong64:
            array = new Array(n);
            for (; i < n; ++i)
               array[i] = this.ntoi8();
            return array; // exit here to avoid conflicts
         case kULong:
         case kULong64:
            array = new Array(n);
            for (; i < n; ++i)
               array[i] = this.ntou8();
            return array; // exit here to avoid conflicts
         case kInt:
         case kCounter:
            array = new Int32Array(n);
            for (; i < n; ++i, o += 4)
               array[i] = view.getInt32(o);
            break;
         case kShort:
            array = new Int16Array(n);
            for (; i < n; ++i, o += 2)
               array[i] = view.getInt16(o);
            break;
         case kUShort:
            array = new Uint16Array(n);
            for (; i < n; ++i, o += 2)
               array[i] = view.getUint16(o);
            break;
         case kChar:
            array = new Int8Array(n);
            for (; i < n; ++i)
               array[i] = view.getInt8(o++);
            break;
         case kBool:
         case kUChar:
            array = new Uint8Array(n);
            for (; i < n; ++i)
               array[i] = view.getUint8(o++);
            break;
         case kTString:
            array = new Array(n);
            for (; i < n; ++i)
               array[i] = this.readTString();
            return array; // exit here to avoid conflicts
         case kDouble32:
            throw new Error('kDouble32 should not be used in readFastArray');
         case kFloat16:
            throw new Error('kFloat16 should not be used in readFastArray');
         // case kBits:
         // case kUInt:
         default:
            array = new Uint32Array(n);
            for (; i < n; ++i, o += 4)
               array[i] = view.getUint32(o);
            break;
      }

      this.o = o;
      return array;
   }

   /** @summary Check if provided regions can be extracted from the buffer */
   canExtract(place) {
      for (let n = 0; n < place.length; n += 2) {
         if (place[n] + place[n + 1] > this.length)
            return false;
      }
      return true;
   }

   /** @summary Extract area */
   extract(place) {
      if (!this.arr?.buffer || !this.canExtract(place))
         return null;
      if (place.length === 2)
         return new DataView(this.arr.buffer, this.arr.byteOffset + place[0], place[1]);

      const res = new Array(place.length / 2);
      for (let n = 0; n < place.length; n += 2)
         res[n / 2] = new DataView(this.arr.buffer, this.arr.byteOffset + place[n], place[n + 1]);
      return res; // return array of buffers
   }

   /** @summary Get code at buffer position */
   codeAt(pos) { return this.arr.getUint8(pos); }

   /** @summary Get part of buffer as string */
   substring(beg, end) {
      let res = '';
      for (let n = beg; n < end; ++n)
         res += String.fromCharCode(this.arr.getUint8(n));
      return res;
   }

   /** @summary Read buffer as N-dim array */
   readNdimArray(handle, func) {
      let ndim = handle.fArrayDim, maxindx = handle.fMaxIndex, res;
      if ((ndim < 1) && (handle.fArrayLength > 0)) {
         ndim = 1;
         maxindx = [handle.fArrayLength];
      }
      if (handle.minus1)
         --ndim;

      if (ndim < 1)
         return func(this, handle);

      if (ndim === 1) {
         res = new Array(maxindx[0]);
         for (let n = 0; n < maxindx[0]; ++n)
            res[n] = func(this, handle);
      } else if (ndim === 2) {
         res = new Array(maxindx[0]);
         for (let n = 0; n < maxindx[0]; ++n) {
            const res2 = new Array(maxindx[1]);
            for (let k = 0; k < maxindx[1]; ++k)
               res2[k] = func(this, handle);
            res[n] = res2;
         }
      } else {
         const indx = new Array(ndim).fill(0), arr = new Array(ndim);
         for (let k = 0; k < ndim; ++k)
            arr[k] = [];
         res = arr[0];
         while (indx[0] < maxindx[0]) {
            let k = ndim - 1;
            arr[k].push(func(this, handle));
            ++indx[k];
            while ((indx[k] === maxindx[k]) && (k > 0)) {
               indx[k] = 0;
               arr[k - 1].push(arr[k]);
               arr[k] = [];
               ++indx[--k];
            }
         }
      }

      return res;
   }

   /** @summary read TKey data */
   readTKey(key) {
      if (!key)
         key = {};
      this.classStreamer(key, clTKey);
      const name = key.fName.replace(/['"]/g, '');
      if (name !== key.fName) {
         key.fRealName = key.fName;
         key.fName = name;
      }
      return key;
   }

   /** @summary reading basket data
     * @desc this is remaining part of TBasket streamer to decode fEntryOffset
     * after unzipping of the TBasket data */
   readBasketEntryOffset(basket, offset) {
      this.locate(basket.fLast - offset);

      if (this.remain() <= 0) {
         if (!basket.fEntryOffset && (basket.fNevBuf <= 1))
            basket.fEntryOffset = [basket.fKeylen];
         if (!basket.fEntryOffset)
            console.warn(`No fEntryOffset when expected for basket with ${basket.fNevBuf} entries`);
         return;
      }

      const nentries = this.ntoi4();
      // there is error in file=reco_103.root&item=Events;2/PCaloHits_g4SimHits_EcalHitsEE_Sim.&opt=dump;num:10;first:101
      // it is workaround, but normally I/O should fail here
      if ((nentries < 0) || (nentries > this.remain() * 4)) {
         console.error(`Error when reading entries offset from basket fNevBuf ${basket.fNevBuf} remains ${this.remain()} want to read ${nentries}`);
         if (basket.fNevBuf <= 1)
            basket.fEntryOffset = [basket.fKeylen];
         return;
      }

      basket.fEntryOffset = this.readFastArray(nentries, kInt);
      if (!basket.fEntryOffset)
         basket.fEntryOffset = [basket.fKeylen];

      if (this.remain() > 0)
         basket.fDisplacement = this.readFastArray(this.ntoi4(), kInt);
      else
         basket.fDisplacement = undefined;
   }

   /** @summary read class definition from I/O buffer */
   readClass() {
      const classInfo = { name: -1 },
            bcnt = this.ntou4(),
            startpos = this.o,
            tag = !(bcnt & kByteCountMask) || (bcnt === kNewClassTag) ? bcnt : this.ntou4();

      if (!(tag & kClassMask)) {
         classInfo.objtag = tag + this.fDisplacement; // indicate that we have deal with objects tag
         return classInfo;
      }
      if (tag === kNewClassTag) {
         // got a new class description followed by a new object
         classInfo.name = this.readNullTerminatedString();

         if (this.getMappedClass(this.fTagOffset + startpos + kMapOffset) === -1)
            this.mapClass(this.fTagOffset + startpos + kMapOffset, classInfo.name);
      } else {
         // got a tag to an already seen class
         const clTag = (tag & ~kClassMask) + this.fDisplacement;
         classInfo.name = this.getMappedClass(clTag);

         if (classInfo.name === -1)
            console.error(`Did not found class with tag ${clTag}`);
      }

      return classInfo;
   }

   /** @summary Read any object from buffer data */
   readObjectAny() {
      const objtag = this.fTagOffset + this.o + kMapOffset,
            clRef = this.readClass();

      // class identified as object and should be handled so
      if (clRef.objtag !== undefined)
         return this.getMappedObject(clRef.objtag);

      if (clRef.name === -1)
         return null;

      const arrkind = getArrayKind(clRef.name);
      let obj;

      if (arrkind === 0)
         obj = this.readTString();
      else if (arrkind > 0) {
         // reading array, can map array only afterwards
         obj = this.readFastArray(this.ntou4(), arrkind);
         this.mapObject(objtag, obj);
      } else {
         // reading normal object, should map before to
         obj = {};
         this.mapObject(objtag, obj);
         this.classStreamer(obj, clRef.name);
      }

      return obj;
   }

   /** @summary Invoke streamer for specified class  */
   classStreamer(obj, classname) {
      if (obj._typename === undefined)
         obj._typename = classname;

      const direct = DirectStreamers[classname];
      if (direct) {
         direct(this, obj);
         return obj;
      }

      const ver = this.readVersion(),
            streamer = this.fFile.getStreamer(classname, ver);

      if (streamer !== null) {
         const len = streamer.length;
         for (let n = 0; n < len; ++n)
            streamer[n].func(this, obj);
      } else {
         // just skip bytes belonging to not-recognized object
         // console.warn(`skip object ${classname}`);
         exports.addMethods(obj);
      }

      this.checkByteCount(ver, classname);

      return obj;
   }

   /** @summary Stream class members using normal streamer */
   streamClassMembers(obj, classname, version) {
      const streamer = this.fFile.getStreamer(classname, { val: version }, undefined, true);
      if (streamer !== null) {
         const len = streamer.length;
         for (let n = 0; n < len; ++n)
            streamer[n].func(this, obj);
      }
      return obj;
   }

} // class TBuffer

// ==============================================================================

/** @summary Direct streamer for TBasket,
  * @desc uses TBuffer therefore defined later
  * @private */
DirectStreamers[clTBasket] = function(buf, obj) {
   buf.classStreamer(obj, clTKey);
   const ver = buf.readVersion();
   obj.fBufferSize = buf.ntoi4();
   obj.fNevBufSize = buf.ntoi4();

   if (obj.fNevBufSize < 0) {
      obj.fNevBufSize = -obj.fNevBufSize;
      obj.fIOBits = buf.ntoi1();
   }

   obj.fNevBuf = buf.ntoi4();
   obj.fLast = buf.ntoi4();

   if (obj.fLast > obj.fBufferSize)
      obj.fBufferSize = obj.fLast;
   const flag = buf.ntoi1();

   if (flag === 0)
      return;

   if ((flag % 10) !== 2) {
      if (obj.fNevBuf) {
         obj.fEntryOffset = buf.readFastArray(buf.ntoi4(), kInt);
         if ((flag > 20) && (flag < 40)) {
            for (let i = 0, kDisplacementMask = 0xFF000000; i < obj.fNevBuf; ++i)
               obj.fEntryOffset[i] &= ~kDisplacementMask;
         }
      }

      if (flag > 40)
         obj.fDisplacement = buf.readFastArray(buf.ntoi4(), kInt);
   }

   if ((flag === 1) || (flag > 10)) {
      // here is reading of raw data
      const sz = (ver.val <= 1) ? buf.ntoi4() : obj.fLast;

      if (sz > obj.fKeylen) {
         // buffer includes again complete TKey data - exclude it
         const blob = buf.extract([buf.o + obj.fKeylen, sz - obj.fKeylen]);
         obj.fBufferRef = new TBuffer(blob, 0, buf.fFile, sz - obj.fKeylen);
         obj.fBufferRef.fTagOffset = obj.fKeylen;
      }

      buf.shift(sz);
   }
};

// ==============================================================================

/**
  * @summary A class that reads a TDirectory from a buffer.
  *
  * @private
  */

class TDirectory {

   /** @summary constructor */
   constructor(file, dirname, cycle) {
      this.fFile = file;
      this._typename = clTDirectory;
      this.dir_name = dirname;
      this.dir_cycle = cycle;
      this.fKeys = [];
   }

   /** @summary retrieve a key by its name and cycle in the list of keys */
   getKey(keyname, cycle, only_direct) {
      if (typeof cycle !== 'number')
         cycle = -1;
      let bestkey = null;
      for (let i = 0; i < this.fKeys.length; ++i) {
         const key = this.fKeys[i];
         if (!key || (key.fName !== keyname))
            continue;
         if (key.fCycle === cycle) {
            bestkey = key;
            break;
         }
         if ((cycle < 0) && (!bestkey || (key.fCycle > bestkey.fCycle)))
            bestkey = key;
      }
      if (bestkey)
         return only_direct ? bestkey : Promise.resolve(bestkey);

      let pos = keyname.lastIndexOf('/');
      // try to handle situation when object name contains slashed (bad practice anyway)
      while (pos > 0) {
         const dirname = keyname.slice(0, pos),
               subname = keyname.slice(pos + 1),
               dirkey = this.getKey(dirname, undefined, true);

         if (dirkey && !only_direct && (dirkey.fClassName.indexOf(clTDirectory) === 0)) {
            return this.fFile.readObject(this.dir_name + '/' + dirname, 1)
                             .then(newdir => newdir.getKey(subname, cycle));
         }

         pos = keyname.lastIndexOf('/', pos - 1);
      }

      return only_direct ? null : Promise.reject(Error(`Key not found ${keyname}`));
   }

   /** @summary Read object from the directory
     * @param {string} name - object name
     * @param {number} [cycle] - cycle number
     * @return {Promise} with read object */
   readObject(obj_name, cycle) {
      return this.fFile.readObject(this.dir_name + '/' + obj_name, cycle);
   }

   /** @summary Read list of keys in directory
     * @return {Promise} with TDirectory object */
   async readKeys(objbuf) {
      objbuf.classStreamer(this, clTDirectory);

      if ((this.fSeekKeys <= 0) || (this.fNbytesKeys <= 0))
         return this;

      return this.fFile.readBuffer([this.fSeekKeys, this.fNbytesKeys]).then(blob => {
         // Read keys of the top directory

         const buf = new TBuffer(blob, 0, this.fFile);

         buf.readTKey();
         const nkeys = buf.ntoi4();

         for (let i = 0; i < nkeys; ++i)
            this.fKeys.push(buf.readTKey());

         this.fFile.fDirectories.push(this);

         return this;
      });
   }

} // class TDirectory

/**
  * @summary Interface to read objects from ROOT files
  *
  * @desc Use {@link openFile} to create instance of the class
  */

class TFile {

   constructor(url) {
      this._typename = clTFile;
      this.fEND = 0;
      this.fFullURL = url;
      this.fURL = url;
      // when disabled ('+' at the end of file name), complete file content read with single operation
      this.fAcceptRanges = true;
      // use additional time stamp parameter for file name to avoid browser caching problem
      this.fUseStampPar = settings.UseStamp ? 'stamp=' + (new Date()).getTime() : false;
      this.fFileContent = null; // this can be full or partial content of the file (if ranges are not supported or if 1K header read from file)
      // stored as TBuffer instance
      this.fMaxRanges = settings.MaxRanges || 200; // maximal number of file ranges requested at once
      this.fDirectories = [];
      this.fKeys = [];
      this.fSeekInfo = 0;
      this.fNbytesInfo = 0;
      this.fTagOffset = 0;
      this.fStreamers = 0;
      this.fStreamerInfos = null;
      this.fFileName = '';
      this.fTimeout = settings.FilesTimeout ?? 0;
      this.fStreamers = [];
      this.fBasicTypes = {}; // custom basic types, in most case enumerations

      if (!isStr(this.fURL))
         return;

      if (this.fURL.at(-1) === '+') {
         this.fURL = this.fURL.slice(0, this.fURL.length - 1);
         this.fAcceptRanges = false;
      }

      if (this.fURL.at(-1) === '^') {
         this.fURL = this.fURL.slice(0, this.fURL.length - 1);
         this.fSkipHeadRequest = true;
      }

      if (this.fURL.at(-1) === '-') {
         this.fURL = this.fURL.slice(0, this.fURL.length - 1);
         this.fUseStampPar = false;
      }

      if (this.fURL.indexOf('file://') === 0) {
         this.fUseStampPar = false;
         this.fAcceptRanges = false;
      }

      const pos = Math.max(this.fURL.lastIndexOf('/'), this.fURL.lastIndexOf('\\'));
      this.fFileName = pos >= 0 ? this.fURL.slice(pos + 1) : this.fURL;
   }

   /** @summary Set timeout for File instance
    * @desc Timeout used when submitting http requests to the server */
   setTimeout(v) {
      this.fTimeout = v;
   }

   /** @summary Assign remap for web servers
    * @desc Allows to specify fallback server if main server fails
    * @param {Object} remap - looks like { 'https://original.server/': 'https://fallback.server/' } */
   assignRemap(remap) {
      if (!remap && !isObject(remap))
         return;

      for (const key in remap) {
         if (this.fURL.indexOf(key) === 0) {
            this.fURL2 = remap[key] + this.fURL.slice(key.length);
            if (!this.fTimeout)
               this.fTimeout = 10000;
         }
      }
   }

   /** @summary Assign BufferArray with file contentOpen file
     * @private */
   assignFileContent(bufArray) {
      this.fFileContent = new TBuffer(new DataView(bufArray));
      this.fAcceptRanges = false;
      this.fUseStampPar = false;
      this.fEND = this.fFileContent.length;
   }

   /** @summary Actual file open
     * @return {Promise} when file keys are read
     * @private */
   async _open() { return this.readKeys(); }

   /** @summary read buffer(s) from the file
    * @return {Promise} with read buffers
    * @private */
   async readBuffer(place, filename, progress_callback) {
      if ((this.fFileContent !== null) && !filename && (!this.fAcceptRanges || this.fFileContent.canExtract(place)))
         return this.fFileContent.extract(place);

      let resolveFunc, rejectFunc;

      const file = this, first_block = (place[0] === 0) && (place.length === 2),
            blobs = [], // array of requested segments
            promise = new Promise((resolve, reject) => {
               resolveFunc = resolve;
               rejectFunc = reject;
            });

      let fileurl, first = 0, last = 0,
          // eslint-disable-next-line prefer-const
          read_callback, first_req,
          first_block_retry = false;

      function setFileUrl(use_second) {
         if (use_second) {
            console.log('Failure - try to repair with URL2', file.fURL2);
            internals.RemapCounter = (internals.RemapCounter ?? 0) + 1;
            file.fURL = file.fURL2;
            delete file.fURL2;
         }

         fileurl = file.fURL;
         if (isStr(filename) && filename) {
            const pos = fileurl.lastIndexOf('/');
            fileurl = (pos < 0) ? filename : fileurl.slice(0, pos + 1) + filename;
         }
      }

      function send_new_request(increment) {
         if (increment) {
            first = last;
            last = Math.min(first + file.fMaxRanges * 2, place.length);
            if (first >= place.length)
               return resolveFunc(blobs);
         }

         let fullurl = fileurl, ranges = 'bytes', totalsz = 0;
         // try to avoid browser caching by adding stamp parameter to URL
         if (file.fUseStampPar)
            fullurl += ((fullurl.indexOf('?') < 0) ? '?' : '&') + file.fUseStampPar;

         for (let n = first; n < last; n += 2) {
            ranges += (n > first ? ',' : '=') + `${place[n]}-${place[n] + place[n + 1] - 1}`;
            totalsz += place[n + 1]; // accumulated total size
         }
         if (last - first > 2)
            totalsz += (last - first) * 60; // for multi-range ~100 bytes/per request

         // when read first block, allow to read more - maybe ranges are not supported and full file content will be returned
         if (file.fAcceptRanges && first_block)
            totalsz = Math.max(totalsz, 1e7);

         return createHttpRequest(fullurl, 'buf', read_callback, undefined, true).then(xhr => {
            if (file.fAcceptRanges) {
               xhr.setRequestHeader('Range', ranges);
               xhr.expected_size = Math.max(Math.round(1.1 * totalsz), totalsz + 200); // 200 if offset for the potential gzip
            }

            if (file.fTimeout)
               xhr.timeout = file.fTimeout;

            if (isFunc(progress_callback) && isFunc(xhr.addEventListener)) {
               let sum1 = 0, sum2 = 0, sum_total = 0;
               for (let n = 1; n < place.length; n += 2) {
                  sum_total += place[n];
                  if (n < first)
                     sum1 += place[n];
                  if (n < last)
                     sum2 += place[n];
               }
               if (!sum_total)
                  sum_total = 1;

               const progress_offest = sum1 / sum_total, progress_this = (sum2 - sum1) / sum_total;
               xhr.addEventListener('progress', oEvent => {
                  if (oEvent.lengthComputable) {
                     if (progress_callback(progress_offest + progress_this * oEvent.loaded / oEvent.total) === 'break') {
                        xhr.did_abort = true;
                        xhr.abort();
                     }
                  }
               });
            } else if (first_block_retry && isFunc(xhr.addEventListener)) {
               xhr.addEventListener('progress', oEvent => {
                  if (!oEvent.total)
                     console.warn('Fail to get file size information');
                  else if (oEvent.total > 5e7) {
                     console.error(`Try to load very large file ${oEvent.total} at once - abort`);
                     xhr.did_abort = 'large';
                     xhr.abort();
                  }
               });
            }

            first_req = first_block ? xhr : null;
            xhr.send(null);
         });
      }

      read_callback = function(res) {
         if (!res && first_block) {
            // if fail to read file with stamp parameter, try once again without it
            if (file.fUseStampPar) {
               file.fUseStampPar = false;
               return send_new_request();
            }
            if (file.fURL2 && (this.did_abort !== 'large')) {
               setFileUrl(true);
               return send_new_request();
            }
            if (file.fAcceptRanges) {
               file.fAcceptRanges = false;
               first_block_retry = true;
               return send_new_request();
            }
         }

         if (res && first_req) {
            // special workaround for servers like cernbox blocking access to some response headers
            // as result, it is not possible to parse multipart responses
            if (file.fAcceptRanges && (first_req.status === 206) && (res?.byteLength === place[1]) && !first_req.getResponseHeader('Content-Range') && (file.fMaxRanges > 1)) {
               console.warn('Server response with 206 code but browser does not provide access to Content-Range header - setting fMaxRanges = 1, consider to load full file with "filename.root+" argument or adjust server configurations');
               file.fMaxRanges = 1;
            }

            // workaround for simpleHTTP
            const kind = browser.isFirefox ? first_req.getResponseHeader('Server') : '';
            if (isStr(kind) && kind.indexOf('SimpleHTTP') === 0) {
               file.fMaxRanges = 1;
               file.fUseStampPar = false;
            }
         }

         if (res && first_block && !file.fFileContent) {
            // special case - keep content of first request (could be complete file) in memory
            file.fFileContent = new TBuffer(isStr(res) ? res : new DataView(res));

            if (!file.fAcceptRanges)
               file.fEND = file.fFileContent.length;

            return resolveFunc(file.fFileContent.extract(place));
         }

         if (!res) {
            if (file.fURL2 && (this.did_abort !== 'large')) {
               setFileUrl(true);
               return send_new_request();
            }
            if ((first === 0) && (last > 2) && (file.fMaxRanges > 1)) {
               // server return no response with multi request - try to decrease ranges count or fail
               if (last / 2 > 200)
                  file.fMaxRanges = 200;
               else if (last / 2 > 50)
                  file.fMaxRanges = 50;
               else if (last / 2 > 20)
                  file.fMaxRanges = 20;
               else if (last / 2 > 5)
                  file.fMaxRanges = 5;
               else
                  file.fMaxRanges = 1;
               last = Math.min(last, file.fMaxRanges * 2);
               return send_new_request();
            }
            return rejectFunc(Error(`Fail to read with ${place.length / 2} ranges max = ${file.fMaxRanges}`));
         }

         // if only single segment requested, return result as is
         if (last - first === 2) {
            const b = new DataView(res);
            if (place.length === 2)
               return resolveFunc(b);
            blobs.push(b);
            return send_new_request(true);
         }

         // object to access response data
         const hdr = this.getResponseHeader('Content-Type'),
               ismulti = isStr(hdr) && (hdr.indexOf('multipart') >= 0),
               view = new DataView(res);

         if (!ismulti) {
            // server may returns simple buffer, which combines all segments together

            const hdr_range = this.getResponseHeader('Content-Range');
            let segm_start = 0, segm_last = -1;

            if (isStr(hdr_range) && hdr_range.indexOf('bytes') >= 0) {
               const parts = hdr_range.slice(hdr_range.indexOf('bytes') + 6).split(/[\s-/]+/);
               if (parts.length === 3) {
                  segm_start = Number.parseInt(parts[0]);
                  segm_last = Number.parseInt(parts[1]);
                  if (!Number.isInteger(segm_start) || !Number.isInteger(segm_last) || (segm_start > segm_last)) {
                     segm_start = 0;
                     segm_last = -1;
                  }
               }
            }

            let canbe_single_segment = (segm_start <= segm_last);
            for (let n = first; n < last; n += 2) {
               if ((place[n] < segm_start) || (place[n] + place[n + 1] - 1 > segm_last))
                  canbe_single_segment = false;
            }

            if (canbe_single_segment) {
               for (let n = first; n < last; n += 2)
                  blobs.push(new DataView(res, place[n] - segm_start, place[n + 1]));
               return send_new_request(true);
            }

            if ((file.fMaxRanges === 1) || first)
               return rejectFunc(Error('Server returns normal response when multipart was requested, disable multirange support'));

            file.fMaxRanges = 1;
            last = Math.min(last, file.fMaxRanges * 2);

            return send_new_request();
         }

         // multipart messages requires special handling

         const indx = hdr.indexOf('boundary=');
         let boundary = '', n = first, o = 0, normal_order = true;
         if (indx > 0) {
            boundary = hdr.slice(indx + 9);
            if ((boundary[0] === '"') && (boundary.at(-1) === '"'))
               boundary = boundary.slice(1, boundary.length - 1);
            boundary = '--' + boundary;
         } else
            console.error('Did not found boundary id in the response header');

         while (n < last) {
            let code1, code2 = view.getUint8(o), nline = 0, line = '',
                finish_header = false, segm_start = 0, segm_last = -1;

            while ((o < view.byteLength - 1) && !finish_header && (nline < 5)) {
               code1 = code2;
               code2 = view.getUint8(o + 1);

               if (((code1 === 13) && (code2 === 10)) || (code1 === 10)) {
                  if ((line.length > 2) && (line.slice(0, 2) === '--') && (line !== boundary))
                     return rejectFunc(Error(`Decode multipart message, expect boundary ${boundary} got ${line}`));

                  line = line.toLowerCase();

                  if ((line.indexOf('content-range') >= 0) && (line.indexOf('bytes') > 0)) {
                     const parts = line.slice(line.indexOf('bytes') + 6).split(/[\s-/]+/);
                     if (parts.length === 3) {
                        segm_start = Number.parseInt(parts[0]);
                        segm_last = Number.parseInt(parts[1]);
                        if (!Number.isInteger(segm_start) || !Number.isInteger(segm_last) || (segm_start > segm_last)) {
                           segm_start = 0;
                           segm_last = -1;
                        }
                     } else
                        console.error(`Fail to decode content-range ${line} ${parts}`);
                  }

                  if ((nline > 1) && !line)
                     finish_header = true;

                  nline++;
                  line = '';
                  if (code1 !== 10) {
                     o++;
                     code2 = view.getUint8(o + 1);
                  }
               } else
                  line += String.fromCharCode(code1);
               o++;
            }

            if (!finish_header)
               return rejectFunc(Error('Cannot decode header in multipart message'));

            if (segm_start > segm_last) {
               // fall-back solution, believe that segments same as requested
               blobs.push(new DataView(res, o, place[n + 1]));
               o += place[n + 1];
               n += 2;
            } else if (normal_order) {
               const n0 = n;
               while ((n < last) && (place[n] >= segm_start) && (place[n] + place[n + 1] - 1 <= segm_last)) {
                  blobs.push(new DataView(res, o + place[n] - segm_start, place[n + 1]));
                  n += 2;
               }

               if (n > n0)
                  o += (segm_last - segm_start + 1);
               else
                  normal_order = false;
            }

            if (!normal_order) {
               // special situation when server reorder segments in the reply
               let isany = false;
               for (let n1 = n; n1 < last; n1 += 2) {
                  if ((place[n1] >= segm_start) && (place[n1] + place[n1 + 1] - 1 <= segm_last)) {
                     blobs[n1 / 2] = new DataView(res, o + place[n1] - segm_start, place[n1 + 1]);
                     isany = true;
                  }
               }
               if (!isany)
                  return rejectFunc(Error(`Provided fragment ${segm_start} - ${segm_last} out of requested multi-range request`));

               while (blobs[n / 2])
                  n += 2;

               o += (segm_last - segm_start + 1);
            }
         }

         send_new_request(true);
      };

      setFileUrl();

      return send_new_request(true).then(() => promise);
   }

   /** @summary Returns file name */
   getFileName() { return this.fFileName; }

   /** @summary Get directory with given name and cycle
    * @desc Function only can be used for already read directories, which are preserved in the memory
    * @private */
   getDir(dirname, cycle) {
      if ((cycle === undefined) && isStr(dirname)) {
         const pos = dirname.lastIndexOf(';');
         if (pos > 0) {
            cycle = Number.parseInt(dirname.slice(pos + 1));
            dirname = dirname.slice(0, pos);
         }
      }

      for (let j = 0; j < this.fDirectories.length; ++j) {
         const dir = this.fDirectories[j];
         if (dir.dir_name !== dirname)
            continue;
         if ((cycle !== undefined) && (dir.dir_cycle !== cycle))
            continue;
         return dir;
      }
      return null;
   }

   /** @summary Retrieve a key by its name and cycle in the list of keys
    * @desc If only_direct not specified, returns Promise while key keys must be read first from the directory
    * @private */
   getKey(keyname, cycle, only_direct) {
      if (typeof cycle !== 'number')
         cycle = -1;
      let bestkey = null;
      for (let i = 0; i < this.fKeys.length; ++i) {
         const key = this.fKeys[i];
         if (!key || (key.fName !== keyname))
            continue;
         if (key.fCycle === cycle) {
            bestkey = key;
            break;
         }
         if ((cycle < 0) && (!bestkey || (key.fCycle > bestkey.fCycle)))
            bestkey = key;
      }
      if (bestkey)
         return only_direct ? bestkey : Promise.resolve(bestkey);

      let pos = keyname.lastIndexOf('/');
      // try to handle situation when object name contains slashes (bad practice anyway)
      while (pos > 0) {
         const dirname = keyname.slice(0, pos),
               subname = keyname.slice(pos + 1),
               dir = this.getDir(dirname);

         if (dir)
            return dir.getKey(subname, cycle, only_direct);

         const dirkey = this.getKey(dirname, undefined, true);
         if (dirkey && !only_direct && (dirkey.fClassName.indexOf(clTDirectory) === 0))
            return this.readObject(dirname).then(newdir => newdir.getKey(subname, cycle));

         pos = keyname.lastIndexOf('/', pos - 1);
      }

      return only_direct ? null : Promise.reject(Error(`Key not found ${keyname}`));
   }

   /** @summary Read and inflate object buffer described by its key
    * @private */
   async readObjBuffer(key) {
      return this.readBuffer([key.fSeekKey + key.fKeylen, key.fNbytes - key.fKeylen]).then(blob1 => {
         if (key.fObjlen <= key.fNbytes - key.fKeylen) {
            const buf = new TBuffer(blob1, 0, this);
            buf.fTagOffset = key.fKeylen;
            return buf;
         }

         return R__unzip(blob1, key.fObjlen).then(objbuf => {
            if (!objbuf)
               return Promise.reject(Error(`Fail to UNZIP buffer for ${key.fName}`));

            const buf = new TBuffer(objbuf, 0, this);
            buf.fTagOffset = key.fKeylen;
            return buf;
         });
      });
   }

   /** @summary Read any object from a root file
     * @desc One could specify cycle number in the object name or as separate argument
     * @param {string} obj_name - name of object, may include cycle number like 'hpxpy;1'
     * @param {number} [cycle] - cycle number, also can be included in obj_name
     * @return {Promise} promise with object read
     * @example
     * import { openFile } from 'https://root.cern/js/latest/modules/io.mjs';
     * let f = await openFile('https://root.cern/js/files/hsimple.root');
     * let obj = await f.readObject('hpxpy;1');
     * console.log(`Read object of type ${obj._typename}`); */
   async readObject(obj_name, cycle, only_dir) {
      const pos = obj_name.lastIndexOf(';');
      if (pos >= 0) {
         cycle = Number.parseInt(obj_name.slice(pos + 1));
         obj_name = obj_name.slice(0, pos);
      }

      if (typeof cycle !== 'number')
         cycle = -1;
      // remove leading slashes
      while (obj_name.length && (obj_name[0] === '/'))
         obj_name = obj_name.slice(1);

      // one uses Promises while in some cases we need to
      // read sub-directory to get list of keys
      // in such situation calls are asynchronous
      return this.getKey(obj_name, cycle).then(key => {
         if ((obj_name === nameStreamerInfo) && (key.fClassName === clTList))
            return this.fStreamerInfos;

         let isdir = false;

         if ((key.fClassName === clTDirectory || key.fClassName === clTDirectoryFile)) {
            const dir = this.getDir(obj_name, cycle);
            if (dir)
               return dir;
            isdir = true;
         }

         if (!isdir && only_dir)
            return Promise.reject(Error(`Key ${obj_name} is not directory}`));

         return this.readObjBuffer(key).then(buf => {
            if (isdir) {
               const dir = new TDirectory(this, obj_name, cycle);
               dir.fTitle = key.fTitle;
               return dir.readKeys(buf);
            }

            const obj = {};
            buf.mapObject(1, obj); // tag object itself with id == 1
            buf.classStreamer(obj, key.fClassName);

            if ((key.fClassName === clTF1) || (key.fClassName === clTF12) || (key.fClassName === clTF2) || (key.fClassName === clTF3))
               return this.#readFormulas(obj);

            return obj;
         });
      });
   }

   /** @summary read formulas from the file and add them to TF1/TF2 objects
     * @private */
   async #readFormulas(tf1) {
      const arr = [];
      for (let indx = 0; indx < this.fKeys.length; ++indx) {
         if (this.fKeys[indx].fClassName === 'TFormula')
            arr.push(this.readObject(this.fKeys[indx].fName, this.fKeys[indx].fCycle));
      }

      return Promise.all(arr).then(formulas => {
         formulas.forEach(obj => tf1.addFormula(obj));
         return tf1;
      });
   }

   /** @summary extract streamer infos from the buffer
     * @private */
   extractStreamerInfos(buf) {
      if (!buf)
         return;

      const lst = {};
      buf.mapObject(1, lst);

      try {
         buf.classStreamer(lst, clTList);
      } catch (err) {
         console.error('Fail extract streamer infos', err);
         return;
      }

      lst._typename = clTStreamerInfoList;

      this.fStreamerInfos = lst;

      if (isFunc(internals.addStreamerInfosForPainter))
         internals.addStreamerInfosForPainter(lst);

      for (let k = 0; k < lst.arr.length; ++k) {
         const si = lst.arr[k];
         if (!si.fElements)
            continue;
         for (let l = 0; l < si.fElements.arr.length; ++l) {
            const elem = si.fElements.arr[l];
            if (!elem.fTypeName || !elem.fType)
               continue;

            let typ = elem.fType, typname = elem.fTypeName;

            if (typ >= 60) {
               if ((typ === kStreamer) && (elem._typename === clTStreamerSTL) && elem.fSTLtype && elem.fCtype && (elem.fCtype < 20)) {
                  const prefix = (StlNames[elem.fSTLtype] || 'undef') + '<';
                  if ((typname.indexOf(prefix) === 0) && (typname.at(-1) === '>')) {
                     typ = elem.fCtype;
                     typname = typname.slice(prefix.length, typname.length - 1).trim();

                     if ((elem.fSTLtype === kSTLmap) || (elem.fSTLtype === kSTLmultimap)) {
                        if (typname.indexOf(',') > 0)
                           typname = typname.slice(0, typname.indexOf(',')).trim();
                        else
                           continue;
                     }
                  }
               }
               if (typ >= 60)
                  continue;
            } else {
               if ((typ > 20) && (typname.at(-1) === '*'))
                  typname = typname.slice(0, typname.length - 1);
               typ %= 20;
            }

            const kind = getTypeId(typname);
            if ((kind === typ) ||
                ((typ === kBits) && (kind === kUInt)) ||
                ((typ === kCounter) && (kind === kInt)))
               continue;

            if (typname && typ && (this.fBasicTypes[typname] !== typ))
               this.fBasicTypes[typname] = typ;
         }
      }
   }

   /** @summary Read file keys
     * @private */
   async readKeys() {
      // with the first readbuffer we read bigger amount to create header cache
      return this.readBuffer([0, 400]).then(blob => {
         const buf = new TBuffer(blob, 0, this);
         if (buf.substring(0, 4) !== 'root')
            return Promise.reject(Error(`Not a ROOT file ${this.fURL}`));

         buf.shift(4);

         this.fVersion = buf.ntou4();
         this.fBEGIN = buf.ntou4();
         if (this.fVersion < 1000000) { // small file
            this.fEND = buf.ntou4();
            this.fSeekFree = buf.ntou4();
            this.fNbytesFree = buf.ntou4();
            buf.shift(4); // const nfree = buf.ntoi4();
            this.fNbytesName = buf.ntou4();
            this.fUnits = buf.ntou1();
            this.fCompress = buf.ntou4();
            this.fSeekInfo = buf.ntou4();
            this.fNbytesInfo = buf.ntou4();
         } else { // new format to support large files
            this.fEND = buf.ntou8();
            this.fSeekFree = buf.ntou8();
            this.fNbytesFree = buf.ntou4();
            buf.shift(4); // const nfree = buf.ntou4();
            this.fNbytesName = buf.ntou4();
            this.fUnits = buf.ntou1();
            this.fCompress = buf.ntou4();
            this.fSeekInfo = buf.ntou8();
            this.fNbytesInfo = buf.ntou4();
         }

         // empty file
         if (!this.fSeekInfo || !this.fNbytesInfo)
            return Promise.reject(Error(`File ${this.fURL} does not provide streamer infos`));

         // extra check to prevent reading of corrupted data
         if (!this.fNbytesName || this.fNbytesName > 100000)
            return Promise.reject(Error(`Cannot read directory info of the file ${this.fURL}`));

         // *-*-------------Read directory info
         let nbytes = this.fNbytesName + 22;
         nbytes += 4;  // fDatimeC.Sizeof();
         nbytes += 4;  // fDatimeM.Sizeof();
         nbytes += 18; // fUUID.Sizeof();
         // assume that the file may be above 2 Gbytes if file version is > 4
         if (this.fVersion >= 40000)
            nbytes += 12;

         // this part typically read from the header, no need to optimize
         return this.readBuffer([this.fBEGIN, Math.max(300, nbytes)]);
      }).then(blob3 => {
         const buf3 = new TBuffer(blob3, 0, this);

         // keep only title from TKey data
         this.fTitle = buf3.readTKey().fTitle;

         buf3.locate(this.fNbytesName);

         // we read TDirectory part of TFile
         buf3.classStreamer(this, clTDirectory);

         if (!this.fSeekKeys)
            return Promise.reject(Error(`Empty keys list in ${this.fURL}`));

         // read with same request keys and streamer infos
         return this.readBuffer([this.fSeekKeys, this.fNbytesKeys, this.fSeekInfo, this.fNbytesInfo]);
      }).then(blobs => {
         const buf4 = new TBuffer(blobs[0], 0, this);

         buf4.readTKey();
         const nkeys = buf4.ntoi4();
         for (let i = 0; i < nkeys; ++i)
            this.fKeys.push(buf4.readTKey());

         const buf5 = new TBuffer(blobs[1], 0, this),
               si_key = buf5.readTKey();
         if (!si_key)
            return Promise.reject(Error(`Fail to read StreamerInfo data in ${this.fURL}`));

         this.fKeys.push(si_key);
         return this.readObjBuffer(si_key);
      }).then(blob6 => {
         this.extractStreamerInfos(blob6);
         return this;
      });
   }

   /** @summary Read the directory content from  a root file
     * @desc If directory was already read - return previously read object
     * Same functionality as {@link TFile#readObject}
     * @param {string} dir_name - directory name
     * @param {number} [cycle] - directory cycle
     * @return {Promise} - promise with read directory */
   async readDirectory(dir_name, cycle) {
      return this.readObject(dir_name, cycle, true);
   }

   /** @summary Search streamer info
     * @param {string} clanme - class name
     * @param {number} [clversion] - class version
     * @param {number} [checksum] - streamer info checksum, have to match when specified
     * @private */
   findStreamerInfo(clname, clversion, checksum) {
      if (!this.fStreamerInfos)
         return null;

      const arr = this.fStreamerInfos.arr, len = arr.length;

      if (checksum !== undefined) {
         let cache = this.fStreamerInfos.cache;
         if (!cache)
            cache = this.fStreamerInfos.cache = {};
         let si = cache[checksum];
         if (si && (!clname || (si.fName === clname)))
            return si;

         for (let i = 0; i < len; ++i) {
            si = arr[i];
            if (si.fCheckSum === checksum) {
               cache[checksum] = si;
               if (!clname || (si.fName === clname))
                  return si;
            }
         }
         cache[checksum] = null; // checksum did not found, do not try again
      }

      if (clname) {
         for (let i = 0; i < len; ++i) {
            const si = arr[i];
            if ((si.fName === clname) && ((si.fClassVersion === clversion) || (clversion === undefined)))
               return si;
         }
      }

      return null;
   }

   /** @summary Returns streamer for the class 'clname',
     * @desc From the list of streamers or generate it from the streamer infos and add it to the list
     * @private */
   getStreamer(clname, ver, s_i, only_plain) {
      // these are special cases, which are handled separately
      if (clname === clTQObject || clname === clTBasket)
         return null;

      let fullname = clname;

      if (ver) {
         fullname += (ver.checksum ? `$chksum${ver.checksum}` : `$ver${ver.val}`);
         const streamer = this.fStreamers[fullname];
         if (streamer !== undefined)
            return streamer;
      }

      const custom = only_plain ? null : CustomStreamers[clname];

      // one can define in the user streamers just aliases
      if (isStr(custom))
         return this.getStreamer(custom, ver, s_i);

      // streamer is just separate function
      if (isFunc(custom))
         return addClassMethods(clname, [{ typename: clname, func: custom }]);

      const streamer = [];

      if (isObject(custom)) {
         if (!custom.name && !custom.func)
            return custom;
         streamer.push(custom); // special read entry, add in the beginning of streamer
      }

      // check element in streamer infos, one can have special cases
      if (!s_i)
         s_i = this.findStreamerInfo(clname, ver.val, ver.checksum);

      if (!s_i) {
         delete this.fStreamers[fullname];
         if (!ver.nowarning)
            console.warn(`Not found streamer for ${clname} ver ${ver.val} checksum ${ver.checksum} full ${fullname}`);
         return null;
      }

      // special handling for TStyle which has duplicated member name fLineStyle
      if (s_i.fName === clTStyle) {
         s_i.fElements?.arr.forEach(elem => {
            if (elem.fName === 'fLineStyle')
               elem.fName = 'fLineStyles'; // like in ROOT JSON now
         });
      }

      // for each entry in streamer info produce member function
      s_i.fElements?.arr.forEach(elem => streamer.push(createMemberStreamer(elem, this)));

      this.fStreamers[fullname] = streamer;

      return addClassMethods(clname, streamer);
   }

   /** @summary Here we produce list of members, resolving all base classes
     * @private */
   getSplittedStreamer(streamer, tgt) {
      if (!streamer)
         return tgt;

      if (!tgt)
         tgt = [];

      for (let n = 0; n < streamer.length; ++n) {
         const elem = streamer[n];

         if (elem.base === undefined) {
            tgt.push(elem);
            continue;
         }

         if (elem.basename === clTObject) {
            tgt.push({
               func(buf, obj) {
                  buf.ntoi2(); // read version, why it here??
                  obj.fUniqueID = buf.ntou4();
                  obj.fBits = buf.ntou4();
                  if (obj.fBits & kIsReferenced)
                     buf.ntou2(); // skip pid
               }
            });
            continue;
         }

         const ver = { val: elem.base };

         if (ver.val === 4294967295) {
            // this is -1 and indicates foreign class, need more workarounds
            ver.val = 1; // need to search version 1 - that happens when several versions of foreign class exists ???
         }

         const parent = this.getStreamer(elem.basename, ver);
         if (parent)
            this.getSplittedStreamer(parent, tgt);
      }

      return tgt;
   }

   /** @summary Fully cleanup TFile data
     * @private */
   delete() {
      this.fDirectories = null;
      this.fKeys = null;
      this.fStreamers = null;
      this.fSeekInfo = 0;
      this.fNbytesInfo = 0;
      this.fTagOffset = 0;
   }

} // class TFile

// =============================================================

/**
  * @summary Interface to read local file in the browser
  *
  * @hideconstructor
  * @desc Use {@link openFile} to create instance of the class
  * @private
  */

class TLocalFile extends TFile {

   constructor(file) {
      super(null);
      this.fUseStampPar = false;
      this.fLocalFile = file;
      this.fEND = file.size;
      this.fFullURL = file.name;
      this.fURL = file.name;
      this.fFileName = file.name;
   }

   /** @summary Open local file
     * @return {Promise} after file keys are read */
   async _open() { return this.readKeys(); }

   /** @summary read buffer from local file
     * @return {Promise} with read data */
   async readBuffer(place, filename /* , progress_callback */) {
      const file = this.fLocalFile;

      return new Promise((resolve, reject) => {
         if (filename) {
            reject(Error(`Cannot access other local file ${filename}`));
            return;
         }

         const reader = new FileReader(), blobs = [];
         let cnt = 0;

         reader.onload = function(evnt) {
            const res = new DataView(evnt.target.result);
            if (place.length === 2) {
               resolve(res);
               return;
            }

            blobs.push(res);
            cnt += 2;
            if (cnt >= place.length) {
               resolve(blobs);
               return;
            }
            reader.readAsArrayBuffer(file.slice(place[cnt], place[cnt] + place[cnt + 1]));
         };

         reader.readAsArrayBuffer(file.slice(place[0], place[0] + place[1]));
      });
   }

} // class TLocalFile

/**
  * @summary Interface to read file in node.js
  *
  * @hideconstructor
  * @desc Use {@link openFile} to create instance of the class
  * @private
  */

class TNodejsFile extends TFile {

   constructor(filename) {
      super(null);
      this.fUseStampPar = false;
      this.fEND = 0;
      this.fFullURL = filename;
      this.fURL = filename;
      this.fFileName = filename;
   }

   /** @summary Open file in node.js
     * @return {Promise} after file keys are read */
   async _open() {
      return Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(fs => {
         this.fs = fs;

         return new Promise((resolve, reject) => {
            this.fs.open(this.fFileName, 'r', (status, fd) => {
               if (status) {
                  console.log(status.message);
                  reject(Error(`Not possible to open ${this.fFileName} inside node.js`));
               } else {
                  const stats = this.fs.fstatSync(fd);
                  this.fEND = stats.size;
                  this.fd = fd;
                  this.readKeys().then(resolve).catch(reject);
               }
            });
         });
      });
   }

   /** @summary Read buffer from node.js file
     * @return {Promise} with requested blocks */
   async readBuffer(place, filename /* , progress_callback */) {
      return new Promise((resolve, reject) => {
         if (filename) {
            reject(Error(`Cannot access other local file ${filename}`));
            return;
         }

         if (!this.fs || !this.fd) {
            reject(Error(`File is not opened ${this.fFileName}`));
            return;
         }

         const blobs = [];
         let cnt = 0;

         const readfunc = (_err, _bytesRead, buf) => {
            const res = new DataView(buf.buffer, buf.byteOffset, place[cnt + 1]);
            if (place.length === 2)
               return resolve(res);
            blobs.push(res);
            cnt += 2;
            if (cnt >= place.length)
               return resolve(blobs);
            this.fs.read(this.fd, Buffer.alloc(place[cnt + 1]), 0, place[cnt + 1], place[cnt], readfunc);
         };

         this.fs.read(this.fd, Buffer.alloc(place[1]), 0, place[1], place[0], readfunc);
      });
   }

} // class TNodejsFile

/**
  * @summary Proxy to read file content
  *
  * @desc Should implement following methods:
  *
  * - openFile() - return Promise with true when file can be open normally
  * - getFileName() - returns string with file name
  * - getFileSize() - returns size of file
  * - readBuffer(pos, len) - return promise with DataView for requested position and length
  *
  * @private
  */

class FileProxy {

   async openFile() { return false; }
   getFileName() { return ''; }
   getFileSize() { return 0; }
   async readBuffer(/* pos, sz */) { return null; }

} // class FileProxy

/**
  * @summary File to use file context via FileProxy
  *
  * @hideconstructor
  * @desc Use {@link openFile} to create instance of the class, providing FileProxy as argument
  * @private
  */

class TProxyFile extends TFile {

   constructor(proxy) {
      super(null);
      this.fUseStampPar = false;
      this.proxy = proxy;
   }

   /** @summary Open file
     * @return {Promise} after file keys are read */
   async _open() {
      return this.proxy.openFile().then(res => {
         if (!res)
            return false;
         this.fEND = this.proxy.getFileSize();
         this.fFullURL = this.fURL = this.fFileName = this.proxy.getFileName();
         if (isStr(this.fFileName)) {
            const p = this.fFileName.lastIndexOf('/');
            if ((p > 0) && (p < this.fFileName.length - 4))
               this.fFileName = this.fFileName.slice(p + 1);
         }
         return this.readKeys();
      });
   }

   /** @summary Read buffer from FileProxy
     * @return {Promise} with requested blocks */
   async readBuffer(place, filename /* , progress_callback */) {
      if (filename)
         return Promise.reject(Error(`Cannot access other file ${filename}`));

      if (!this.proxy)
         return Promise.reject(Error(`File is not opened ${this.fFileName}`));

      if (isFunc(this.proxy.readBuffers)) {
         return this.proxy.readBuffers(place).then(arr => {
            return arr?.length === 1 ? arr[0] : arr;
         });
      }

      if (place.length === 2)
         return this.proxy.readBuffer(place[0], place[1]);

      const arr = [];
      for (let k = 0; k < place.length; k += 2)
         arr.push(this.proxy.readBuffer(place[k], place[k + 1]));
      return Promise.all(arr);
   }

} // class TProxyFile


/** @summary Open ROOT file for reading
  * @desc Generic method to open ROOT file for reading
  * Following kind of arguments can be provided:
  *  - string with file URL (see example). In node.js environment local file like 'file://hsimple.root' can be specified
  *  - [File]{@link https://developer.mozilla.org/en-US/docs/Web/API/File} instance which let read local files from browser
  *  - [ArrayBuffer]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer} instance with complete file content
  *  - [FileProxy]{@link FileProxy} let access arbitrary files via tiny proxy API
  * @param {string|object} arg - argument for file open like url, see details
  * @param {object} [opts] - extra arguments
  * @param {Number} [opts.timeout=0] - read timeout for http requests in ms
  * @param {Object} [opts.remap={}] - http server remap to fallback when main server fails, like { 'https://original.server/': 'https://fallback.server/' }
  * @return {object} - Promise with {@link TFile} instance when file is opened
  * @example
  *
  * import { openFile } from 'https://root.cern/js/latest/modules/io.mjs';
  * let f = await openFile('https://root.cern/js/files/hsimple.root');
  * console.log(`Open file ${f.getFileName()}`); */
function openFile(arg, opts) {
   let file, plain_file;

   if (isNodeJs() && isStr(arg)) {
      if (!arg.indexOf('file://'))
         file = new TNodejsFile(arg.slice(7));
      else if (arg.indexOf('http'))
         file = new TNodejsFile(arg);
   }

   if (!file && isObject(arg) && (arg instanceof FileProxy))
      file = new TProxyFile(arg);

   if (!file && isObject(arg) && (arg instanceof ArrayBuffer)) {
      file = new TFile('localfile.root');
      file.assignFileContent(arg);
   }

   if (!file && isObject(arg) && arg.size && arg.name)
      file = new TLocalFile(arg);

   if (!file) {
      file = new TFile(arg);
      plain_file = true;
      file.assignRemap(settings.FilesRemap);
   }

   if (opts && isObject(opts)) {
      if (opts.timeout)
         file.setTimeout(opts.timeout);
      if (plain_file && opts.remap)
         file.assignRemap(opts.remap);
   }

   return file._open();
}

/** @summary Unzip JSON string
 * @desc Should be used for buffer produced with TBufferJSON::zipJSON() method
 * @param tgtsize - original length of json string
 * @param src - string with data returned by TBufferJSON::zipJSON
 * @return {Promise} with unzipped string */

async function unzipJSON(tgtsize, src) {
   const bindata = atob_func(src),
         buf = new ArrayBuffer(bindata.length),
         bufView = new DataView(buf);
   for (let i = 0; i < bindata.length; i++)
      bufView.setUint8(i, bindata.charCodeAt(i));

   return R__unzip(bufView, tgtsize).then(resView => {
      let resstr = '';
      for (let i = 0; i < tgtsize; i++)
         resstr += String.fromCharCode(resView.getUint8(i));
      return resstr;
   });
}

// special way to assign methods when streaming objects
addClassMethods(clTNamed, CustomStreamers[clTNamed]);
addClassMethods(clTObjString, CustomStreamers[clTObjString]);

// branch types
const kLeafNode = 0, kBaseClassNode = 1, kObjectNode = 2, kClonesNode = 3,
      kSTLNode = 4, kClonesMemberNode = 31, kSTLMemberNode = 41,
      // branch bits
      // kDoNotProcess = BIT(10), // Active bit for branches
      // kIsClone = BIT(11), // to indicate a TBranchClones
      // kBranchObject = BIT(12), // branch is a TObject*
      // kBranchAny = BIT(17), // branch is an object*
      // kAutoDelete = BIT(15),
      kDoNotUseBufferMap = BIT(22), // If set, at least one of the entry in the branch will use the buffer's map of classname and objects.
      clTBranchElement = 'TBranchElement', clTBranchFunc = 'TBranchFunc';

/**
 * @summary Class to read data from TTree
 *
 * @desc Instance of TSelector can be used to access TTree data
 */

class TSelector {

   /** @summary constructor */
   constructor() {
      this._branches = []; // list of branches to read
      this._names = []; // list of member names for each branch in tgtobj
      this._directs = []; // indication if only branch without any children should be read
      this._break = 0;
      this.tgtobj = {};
   }

   /** @summary Add branch to the selector
    * @desc Either branch name or branch itself should be specified
    * Second parameter defines member name in the tgtobj
    * If selector.addBranch('px', 'read_px') is called,
    * branch will be read into selector.tgtobj.read_px member
    * If second parameter not specified, branch name (here 'px') will be used
    * If branch object specified as first parameter and second parameter missing,
    * then member like 'br0', 'br1' and so on will be assigned
    * @param {string|Object} branch - name of branch (or branch object itself}
    * @param {string} [name] - member name in tgtobj where data will be read
    * @param {boolean} [direct] - if only branch without any children should be read */
   addBranch(branch, name, direct) {
      if (!name)
         name = isStr(branch) ? branch : `br${this._branches.length}`;
      this._branches.push(branch);
      this._names.push(name);
      this._directs.push(direct);
      return this._branches.length - 1;
   }

   /** @summary returns number of branches used in selector */
   numBranches() { return this._branches.length; }

   /** @summary returns branch by index used in selector */
   getBranch(indx) { return this._branches[indx]; }

   /** @summary returns index of branch
     * @private */
   indexOfBranch(branch) { return this._branches.indexOf(branch); }

   /** @summary returns name of branch
     * @private */
   nameOfBranch(indx) { return this._names[indx]; }

   /** @summary function called during TTree processing
    * @abstract
    * @param {number} progress - current value between 0 and 1 */
   ShowProgress(/* progress */) {}

   /** @summary call this function to abort processing */
   Abort() { this._break = -1111; }

   /** @summary function called before start processing
    * @abstract
    * @param {object} tree - tree object */
   Begin(/* tree */) {}

   /** @summary function called when next entry extracted from the tree
    * @abstract
    * @param {number} entry - read entry number */
   Process(/* entry */) {}

   /** @summary function called at the very end of processing
    * @abstract
    * @param {boolean} res - true if all data were correctly processed */
   Terminate(/* res */) {}

} // class TSelector

// =================================================================

/** @summary Checks array kind
  * @desc return 0 when not array
  * 1 - when arbitrary array
  * 2 - when plain (1-dim) array with same-type content
  * @private */
function checkArrayPrototype(arr, check_content) {
   if (!isObject(arr))
      return 0;

   const arr_kind = isArrayProto(Object.prototype.toString.apply(arr));
   if (!check_content || (arr_kind !== 1))
      return arr_kind;

   let typ, plain = true;
   for (let k = 0; k < arr.length; ++k) {
      const sub = typeof arr[k];
      if (!typ)
         typ = sub;
      if ((sub !== typ) || (isObject(sub) && checkArrayPrototype(arr[k]))) {
         plain = false;
         break;
      }
   }

   return plain ? 2 : 1;
}

/**
 * @summary Class to iterate over array elements
 *
 * @private
 */

class ArrayIterator {

   /** @summary constructor */
   constructor(arr, select, tgtobj) {
      this.object = arr;
      this.value = 0; // value always used in iterator
      this.arr = []; // all arrays
      this.indx = []; // all indexes
      this.cnt = -1; // current index counter
      this.tgtobj = tgtobj;

      if (isObject(select))
         this.select = select; // remember indexes for selection
      else
         this.select = []; // empty array, undefined for each dimension means iterate over all indexes
   }

   /** @summary next element */
   next() {
      let obj, typ, cnt = this.cnt;

      if (cnt >= 0) {
         if (++this.fastindx < this.fastlimit) {
            this.value = this.fastarr[this.fastindx];
            return true;
         }

         while (--cnt >= 0) {
            if ((this.select[cnt] === undefined) && (++this.indx[cnt] < this.arr[cnt].length))
               break;
         }
         if (cnt < 0)
            return false;
      }

      while (true) {
         obj = (cnt < 0) ? this.object : (this.arr[cnt])[this.indx[cnt]];

         typ = obj ? typeof obj : 'any';

         if (typ === 'object') {
            if (obj._typename !== undefined) {
               if (isRootCollection(obj)) {
                  obj = obj.arr;
                  typ = 'array';
               } else
                  typ = 'any';
            } else if (Number.isInteger(obj.length) && (checkArrayPrototype(obj) > 0))
               typ = 'array';
            else
               typ = 'any';
         }

         if (this.select[cnt + 1] === '$self$') {
            this.value = obj;
            this.fastindx = this.fastlimit = 0;
            this.cnt = cnt + 1;
            return true;
         }

         if ((typ === 'any') && isStr(this.select[cnt + 1])) {
            // this is extraction of the member from arbitrary class
            this.arr[++cnt] = obj;
            this.indx[cnt] = this.select[cnt]; // use member name as index
            continue;
         }

         if ((typ === 'array') && (obj.length || (this.select[cnt + 1] === '$size$'))) {
            this.arr[++cnt] = obj;
            switch (this.select[cnt]) {
               case undefined:
                  this.indx[cnt] = 0;
                  break;
               case '$last$':
                  this.indx[cnt] = obj.length - 1;
                  break;
               case '$size$':
                  this.value = obj.length;
                  this.fastindx = this.fastlimit = 0;
                  this.cnt = cnt;
                  return true;
               default:
                  if (Number.isInteger(this.select[cnt])) {
                     this.indx[cnt] = this.select[cnt];
                     if (this.indx[cnt] < 0)
                        this.indx[cnt] = obj.length - 1;
                  } else {
                     // this is compile variable as array index - can be any expression
                     this.select[cnt].produce(this.tgtobj);
                     this.indx[cnt] = Math.round(this.select[cnt].get(0));
                  }
            }
         } else {
            if (cnt < 0)
               return false;

            this.value = obj;
            if (this.select[cnt] === undefined) {
               this.fastarr = this.arr[cnt];
               this.fastindx = this.indx[cnt];
               this.fastlimit = this.fastarr.length;
            } else
               this.fastindx = this.fastlimit = 0; // no any iteration on that level

            this.cnt = cnt;
            return true;
         }
      }

      // unreachable code
      // return false;
   }

   /** @summary reset iterator */
   reset() {
      this.arr = [];
      this.indx = [];
      delete this.fastarr;
      this.cnt = -1;
      this.value = 0;
   }

} // class ArrayIterator


/** @summary return TStreamerElement associated with the branch - if any
  * @desc unfortunately, branch.fID is not number of element in streamer info
  * @private */
function findBrachStreamerElement(branch, file) {
   if (!branch || !file || (branch._typename !== clTBranchElement) || (branch.fID < 0) || (branch.fStreamerType < 0))
      return null;

   const s_i = file.findStreamerInfo(branch.fClassName, branch.fClassVersion, branch.fCheckSum),
         arr = (s_i && s_i.fElements) ? s_i.fElements.arr : null;
   if (!arr)
      return null;

   let match_name = branch.fName,
       pos = match_name.indexOf('[');
   if (pos > 0)
      match_name = match_name.slice(0, pos);
   pos = match_name.lastIndexOf('.');
   if (pos > 0)
      match_name = match_name.slice(pos + 1);

   function match_elem(elem) {
      if (!elem)
         return false;
      if (elem.fName !== match_name)
         return false;
      if (elem.fType === branch.fStreamerType)
         return true;
      if ((elem.fType === kBool) && (branch.fStreamerType === kUChar))
         return true;
      if (((branch.fStreamerType === kSTL) || (branch.fStreamerType === kSTL + kOffsetL) ||
           (branch.fStreamerType === kSTLp) || (branch.fStreamerType === kSTLp + kOffsetL)) &&
          (elem.fType === kStreamer))
         return true;
      console.warn(`Should match element ${elem.fType} with branch ${branch.fStreamerType}`);
      return false;
   }

   // first check branch fID - in many cases gut guess
   if (match_elem(arr[branch.fID]))
      return arr[branch.fID];

   for (let k = 0; k < arr.length; ++k) {
      if ((k !== branch.fID) && match_elem(arr[k]))
         return arr[k];
   }

   console.error(`Did not found/match element for branch ${branch.fName} class ${branch.fClassName}`);

   return null;
}


/** @summary return class name of the object, stored in the branch
  * @private */
function getBranchObjectClass(branch, tree, with_clones = false, with_leafs = false) {
   if (!branch || (branch._typename !== clTBranchElement))
      return '';

   if ((branch.fType === kLeafNode) && (branch.fID === -2) && (branch.fStreamerType === -1)) {
      // object where all sub-branches will be collected
      return branch.fClassName;
   }

   if (with_clones && branch.fClonesName && ((branch.fType === kClonesNode) || (branch.fType === kSTLNode)))
      return branch.fClonesName;

   const s_elem = findBrachStreamerElement(branch, tree.$file);
   if ((branch.fType === kBaseClassNode) && s_elem && (s_elem.fTypeName === kBaseClass))
      return s_elem.fName;

   if (branch.fType === kObjectNode) {
      if (s_elem && ((s_elem.fType === kObject) || (s_elem.fType === kAny)))
         return s_elem.fTypeName;
      return clTObject;
   }

   if ((branch.fType === kLeafNode) && s_elem && with_leafs) {
      if ((s_elem.fType === kObject) || (s_elem.fType === kAny))
         return s_elem.fTypeName;
      if (s_elem.fType === kObjectp)
         return s_elem.fTypeName.slice(0, s_elem.fTypeName.length - 1);
   }

   return '';
}


/** @summary Get branch with specified id
  * @desc All sub-branches checked as well
  * @return {Object} branch
  * @private */
function getTreeBranch(tree, id) {
   if (!Number.isInteger(id))
      return;
   let res, seq = 0;
   function scan(obj) {
      obj?.fBranches?.arr.forEach(br => {
         if (seq++ === id)
            res = br;
         if (!res)
            scan(br);
      });
   }

   scan(tree);
   return res;
}


/** @summary Special branch search
  * @desc Name can include extra part, which will be returned in the result
  * @param {string} name - name of the branch
  * @return {Object} with 'branch' and 'rest' members
  * @private */
function findBranchComplex(tree, name, lst = undefined, only_search = false) {
   let top_search = false, search = name, res = null;

   if (!lst) {
      top_search = true;
      lst = tree.fBranches;
      const pos = search.indexOf('[');
      if (pos > 0)
         search = search.slice(0, pos);
   }

   if (!lst?.arr.length)
      return null;

   for (let n = 0; n < lst.arr.length; ++n) {
      let brname = lst.arr[n].fName;
      if (brname.at(-1) === ']')
         brname = brname.slice(0, brname.indexOf('['));

      // special case when branch name includes STL map name
      if (search.indexOf(brname) && (brname.indexOf('<') > 0)) {
         const p1 = brname.indexOf('<'), p2 = brname.lastIndexOf('>');
         brname = brname.slice(0, p1) + brname.slice(p2 + 1);
      }

      if (brname === search) {
         res = { branch: lst.arr[n], rest: '' };
         break;
      }

      if (search.indexOf(brname))
         continue;

      // this is a case when branch name is in the begin of the search string

      // check where point is
      let pnt = brname.length;
      if (brname[pnt - 1] === '.')
         pnt--;
      if (search[pnt] !== '.')
         continue;

      res = findBranchComplex(tree, search, lst.arr[n].fBranches) ||
            findBranchComplex(tree, search.slice(pnt + 1), lst.arr[n].fBranches) ||
            { branch: lst.arr[n], rest: search.slice(pnt) };

      break;
   }

   if (top_search && !only_search && !res && (search.indexOf('br_') === 0)) {
      let p = 3;
      while ((p < search.length) && (search[p] >= '0') && (search[p] <= '9'))
         ++p;
      const br = (p > 3) ? getTreeBranch(tree, parseInt(search.slice(3, p))) : null;
      if (br)
         res = { branch: br, rest: search.slice(p) };
   }

   if (!top_search || !res)
      return res;

   if (name.length > search.length)
      res.rest += name.slice(search.length);

   return res;
}


/** @summary Search branch with specified name
  * @param {string} name - name of the branch
  * @return {Object} found branch
  * @private */
function findBranch(tree, name) {
   const res = findBranchComplex(tree, name, tree.fBranches, true);
   return (!res || res.rest) ? null : res.branch;
}


/** summary Returns number of branches in the TTree
  * desc Checks also sub-branches in the branches
  * return {number} number of branches
  * private
function getNumBranches(tree) {
   function count(obj) {
      if (!obj?.fBranches)
         return 0;
      let nchld = 0;
      obj.fBranches.arr.forEach(sub => { nchld += count(sub); });
      return obj.fBranches.arr.length + nchld;
   }

   return count(tree);
}
*/

/**
 * @summary object with single variable in TTree::Draw expression
 *
 * @private
 */

class TDrawVariable {

   /** @summary constructor */
   constructor(globals) {
      this.globals = globals;

      this.code = '';
      this.brindex = []; // index of used branches from selector
      this.branches = []; // names of branches in target object
      this.brarray = []; // array specifier for each branch
      this.func = null; // generic function for variable calculation

      this.kind = undefined;
      this.buf = []; // buffer accumulates temporary values
   }

   /** @summary Parse variable
     * @desc when only_branch specified, its placed in the front of the expression */
   parse(tree, selector, code, only_branch, branch_mode) {
      const is_start_symbol = symb => {
         if ((symb >= 'A') && (symb <= 'Z'))
            return true;
         if ((symb >= 'a') && (symb <= 'z'))
            return true;
         return (symb === '_');
      }, is_next_symbol = symb => {
         if (is_start_symbol(symb))
            return true;
         if ((symb >= '0') && (symb <= '9'))
            return true;
         return false;
      };

      if (!code)
         code = ''; // should be empty string at least

      this.code = (only_branch?.fName ?? '') + code;

      let pos = 0, pos2 = 0, br;
      while ((pos < code.length) || only_branch) {
         let arriter = [];

         if (only_branch) {
            br = only_branch;
            only_branch = undefined;
         } else {
            // first try to find branch
            pos2 = pos;
            while ((pos2 < code.length) && (is_next_symbol(code[pos2]) || code[pos2] === '.'))
               pos2++;
            if (code[pos2] === '$') {
               let repl = '';
               switch (code.slice(pos, pos2)) {
                  case 'LocalEntry':
                  case 'Entry': repl = 'arg.$globals.entry'; break;
                  case 'Entries': repl = 'arg.$globals.entries'; break;
               }
               if (repl) {
                  code = code.slice(0, pos) + repl + code.slice(pos2 + 1);
                  pos += repl.length;
                  continue;
               }
            }

            br = selector.findBranch(tree, code.slice(pos, pos2));
            if (!br) {
               pos = pos2 + 1;
               continue;
            }

            // when full id includes branch name, replace only part of extracted expression
            if (br.branch && (br.rest !== undefined)) {
               pos2 -= br.rest.length;
               branch_mode = undefined; // maybe selection of the sub-object done
               br = br.branch;
            }

            // when code ends with the point - means object itself will be accessed
            // sometime branch name itself ends with the point
            if ((pos2 >= code.length - 1) && (code.at(-1) === '.')) {
               arriter.push('$self$');
               pos2 = code.length;
            }
         }

         // now extract all levels of iterators
         while (pos2 < code.length) {
            if ((code[pos2] === '@') && (code.slice(pos2, pos2 + 5) === '@size') && !arriter.length) {
               pos2 += 5;
               branch_mode = true;
               break;
            }

            if (code[pos2] === '.') {
               // this is object member
               const prev = ++pos2;

               if ((code[prev] === '@') && (code.slice(prev, prev + 5) === '@size')) {
                  arriter.push('$size$');
                  pos2 += 5;
                  break;
               }

               if (!is_start_symbol(code[prev])) {
                  arriter.push('$self$'); // last point means extraction of object itself
                  break;
               }

               while ((pos2 < code.length) && is_next_symbol(code[pos2]))
                  pos2++;

               // this is looks like function call - do not need to extract member with
               if (code[pos2] === '(') {
                  pos2 = prev - 1;
                  break;
               }

               // this is selection of member, but probably we need to activate iterator for ROOT collection
               // TODO: if selected member is simple data type - no need to make other checks - just break here
               if (!arriter.length && selector.isArrayBranch(tree, br))
                  arriter.push(undefined);
               arriter.push(code.slice(prev, pos2));
               continue;
            }

            if (code[pos2] !== '[')
               break;

            // simple []
            if (code[pos2 + 1] === ']') {
               arriter.push(undefined);
               pos2 += 2;
               continue;
            }

            const prev = pos2++;
            let cnt = 0;
            while ((pos2 < code.length) && ((code[pos2] !== ']') || (cnt > 0))) {
               if (code[pos2] === '[')
                  cnt++;
               else if (code[pos2] === ']')
                  cnt--;
               pos2++;
            }
            const sub = code.slice(prev + 1, pos2);
            switch (sub) {
               case '':
               case '$all$': arriter.push(undefined); break;
               case '$last$': arriter.push('$last$'); break;
               case '$size$': arriter.push('$size$'); break;
               case '$first$': arriter.push(0); break;
               default:
                  if (Number.isInteger(parseInt(sub)))
                     arriter.push(parseInt(sub));
                  else {
                     // try to compile code as draw variable
                     const subvar = new TDrawVariable(this.globals);
                     if (!subvar.parse(tree, selector, sub))
                        return false;
                     arriter.push(subvar);
                  }
            }
            pos2++;
         }

         if (!arriter.length)
            arriter = undefined;
         else if ((arriter.length === 1) && (arriter[0] === undefined))
            arriter = true;

         let indx = selector.indexOfBranch(br);
         if (indx < 0)
            indx = selector.addBranch(br, undefined, branch_mode);

         branch_mode = undefined;

         this.brindex.push(indx);
         this.branches.push(selector.nameOfBranch(indx));
         this.brarray.push(arriter);

         // this is simple case of direct usage of the branch
         if ((pos === 0) && (pos2 === code.length) && (this.branches.length === 1)) {
            this.direct_branch = true; // remember that branch read as is
            return true;
         }

         const replace = `arg.var${this.branches.length - 1}`;
         code = code.slice(0, pos) + replace + code.slice(pos2);
         pos += replace.length;
      }

      // support usage of some standard TMath functions
      code = code.replace(/TMath::Exp\(/g, 'Math.exp(')
                 .replace(/TMath::Abs\(/g, 'Math.abs(')
                 .replace(/TMath::Prob\(/g, 'arg.$math.Prob(')
                 .replace(/TMath::Gaus\(/g, 'arg.$math.Gaus(');

      this.func = new Function('arg', `return (${code})`);

      return true;
   }

   /** @summary Check if it is dummy variable */
   is_dummy() { return !this.branches.length && !this.func; }

   /** @summary Produce variable
     * @desc after reading tree branches into the object, calculate variable value */
   produce(obj) {
      this.length = 1;
      this.isarray = false;

      if (this.is_dummy()) {
         this.value = 1; // used as dummy weight variable
         this.kind = 'number';
         return;
      }

      const arg = { $globals: this.globals, $math: jsroot_math }, arrs = [];
      let usearrlen = -1;
      for (let n = 0; n < this.branches.length; ++n) {
         const name = `var${n}`;
         arg[name] = obj[this.branches[n]];

         // try to check if branch is array and need to be iterated
         if (this.brarray[n] === undefined)
            this.brarray[n] = (checkArrayPrototype(arg[name]) > 0) || isRootCollection(arg[name]);

         // no array - no pain
         if (this.brarray[n] === false)
            continue;

         // check if array can be used as is - one dimension and normal values
         if ((this.brarray[n] === true) && (checkArrayPrototype(arg[name], true) === 2)) {
            // plain array, can be used as is
            arrs[n] = arg[name];
         } else {
            const iter = new ArrayIterator(arg[name], this.brarray[n], obj);
            arrs[n] = [];
            while (iter.next())
               arrs[n].push(iter.value);
         }
         if ((usearrlen < 0) || (usearrlen < arrs[n].length))
            usearrlen = arrs[n].length;
      }

      if (usearrlen < 0) {
         this.value = this.direct_branch ? arg.var0 : this.func(arg);
         if (!this.kind)
            this.kind = typeof this.value;
         return;
      }

      if (usearrlen === 0) {
         // empty array - no any histogram should be filled
         this.length = 0;
         this.value = 0;
         return;
      }

      this.length = usearrlen;
      this.isarray = true;

      if (this.direct_branch)
         this.value = arrs[0]; // just use array
      else {
         this.value = new Array(usearrlen);

         for (let k = 0; k < usearrlen; ++k) {
            for (let n = 0; n < this.branches.length; ++n) {
               if (arrs[n])
                  arg[`var${n}`] = arrs[n][k];
            }
            this.value[k] = this.func(arg);
         }
      }

      if (!this.kind)
         this.kind = typeof this.value[0];
   }

   /** @summary Get variable */
   get(indx) { return this.isarray ? this.value[indx] : this.value; }

   /** @summary Append array to the buffer */
   appendArray(tgtarr) { this.buf = this.buf.concat(tgtarr[this.branches[0]]); }

} // class TDrawVariable


/**
 * @summary Selector class for TTree::Draw function
 *
 * @private
 */

class TDrawSelector extends TSelector {

   /** @summary constructor */
   constructor() {
      super();

      this.ndim = 0;
      this.vars = []; // array of expression variables
      this.cut = null; // cut variable
      this.hist = null;
      this.drawopt = '';
      this.hist_name = '$htemp';
      this.draw_title = 'Result of TTree::Draw';
      this.graph = false;
      this.hist_args = []; // arguments for histogram creation
      this.arr_limit = 1000;  // number of accumulated items before create histogram
      this.htype = 'F';
      this.monitoring = 0;
      this.globals = {}; // object with global parameters, which could be used in any draw expression
      this.last_progress = 0;
      this.aver_diff = 0;
   }

   /** @summary Return number of entries in the tree */
   getNumEntries(tree) { return tree?.fEntries || 0; }

   /** @summary Find branch in the tree */
   findBranch(tree, name) { return findBranchComplex(tree, name); }

   /** @summary Returns true if one can use branch as array */
   isArrayBranch(tree, br) {
      if ((br.fType === kClonesNode) || (br.fType === kSTLNode))
         return true;
      const objclass = getBranchObjectClass(br, tree, false, true);
      if (objclass && isRootCollection(null, objclass))
         return true;
   }

   /** @summary Set draw selector callbacks */
   setCallback(result_callback, progress_callback) {
      this.result_callback = result_callback;
      this.progress_callback = progress_callback;
   }

   /** @summary Parse parameters */
   parseParameters(tree, args, expr) {
      if (!expr || !isStr(expr))
         return '';

      // parse parameters which defined at the end as expression;par1name:par1value;par2name:par2value
      let pos = expr.lastIndexOf(';');
      while (pos >= 0) {
         let parname = expr.slice(pos + 1), parvalue;
         expr = expr.slice(0, pos);
         pos = expr.lastIndexOf(';');

         const separ = parname.indexOf(':');
         if (separ > 0) {
            parvalue = parname.slice(separ + 1);
            parname = parname.slice(0, separ);
         }

         let intvalue = parseInt(parvalue);
         if (!parvalue || !Number.isInteger(intvalue))
            intvalue = undefined;

         switch (parname) {
            case 'elist':
               if ((parvalue.at(0) === '[') && (parvalue.at(-1) === ']')) {
                  parvalue = parvalue.slice(1, parvalue.length - 1).replaceAll(/\s/g, '');
                  args.elist = [];
                  let p = 0, last_v = -1;
                  const getInt = () => {
                     const p0 = p;
                     while ((p < parvalue.length) && (parvalue.charCodeAt(p) >= 48) && (parvalue.charCodeAt(p) < 58))
                        p++;
                     return parseInt(parvalue.slice(p0, p));
                  };

                  while (p < parvalue.length) {
                     const v1 = getInt();
                     if (v1 <= last_v) {
                        console.log('position', p);
                        throw Error(`Wrong entry id ${v1} in elist last ${last_v}`);
                     }
                     let v2 = v1;
                     if (parvalue[p] === '.' && parvalue[p + 1] === '.') {
                        p += 2;
                        v2 = getInt();
                        if (v2 < v1)
                           throw Error(`Wrong entry id ${v2} in range ${v1}`);
                     }
                     if (parvalue[p] === ',' || p === parvalue.length) {
                        for (let v = v1; v <= v2; ++v) {
                           args.elist.push(v);
                           last_v = v;
                        }
                        p++;
                     } else
                        throw Error('Wrong syntax for elist');
                  }
               }
               break;
            case 'entries':
            case 'num':
            case 'numentries':
               if (parvalue === 'all')
                  args.numentries = this.getNumEntries(tree);
               else if (parvalue === 'half')
                  args.numentries = Math.round(this.getNumEntries(tree) / 2);
               else if (intvalue !== undefined)
                  args.numentries = intvalue;
               break;
            case 'first':
               if (intvalue !== undefined)
                  args.firstentry = intvalue;
               break;
            case 'nmatch':
               if (intvalue !== undefined)
                  this.nmatch = intvalue;
               break;
            case 'mon':
            case 'monitor':
               args.monitoring = intvalue ?? 5000;
               break;
            case 'player':
               args.player = true;
               break;
            case 'dump':
               args.dump = true;
               break;
            case 'staged':
               args.staged = true;
               break;
            case 'maxseg':
            case 'maxrange':
               if (intvalue)
                  tree.$file.fMaxRanges = intvalue;
               break;
            case 'accum':
               if (intvalue)
                  this.arr_limit = intvalue;
               break;
            case 'htype':
               if (parvalue && (parvalue.length === 1)) {
                  this.htype = parvalue.toUpperCase();
                  if (['C', 'S', 'I', 'F', 'L', 'D'].indexOf(this.htype) < 0)
                     this.htype = 'F';
               }
               break;
            case 'hbins':
               this.hist_nbins = parseInt(parvalue);
               if (!Number.isInteger(this.hist_nbins) || (this.hist_nbins <= 3))
                  delete this.hist_nbins;
               else
                  this.want_hist = true;
               break;
            case 'drawopt':
               args.drawopt = parvalue;
               break;
            case 'graph':
               args.graph = intvalue || true;
               break;
         }
      }

      pos = expr.lastIndexOf('>>');
      if (pos >= 0) {
         let harg = expr.slice(pos + 2).trim();
         expr = expr.slice(0, pos).trim();
         pos = harg.indexOf('(');
         if (pos > 0) {
            this.hist_name = harg.slice(0, pos);
            harg = harg.slice(pos);
         }
         if (harg === 'dump')
            args.dump = true;
         else if (harg === 'elist')
            args.dump_entries = true;
         else if (harg.indexOf('Graph') === 0)
            args.graph = true;
         else if (pos < 0) {
            this.want_hist = true;
            this.hist_name = harg;
         } else if ((harg[0] === '(') && (harg.at(-1) === ')')) {
            this.want_hist = true;
            harg = harg.slice(1, harg.length - 1).split(',');
            let isok = true;
            for (let n = 0; n < harg.length; ++n) {
               harg[n] = (n % 3 === 0) ? parseInt(harg[n]) : parseFloat(harg[n]);
               if (!Number.isFinite(harg[n]))
                  isok = false;
            }
            if (isok)
               this.hist_args = harg;
         }
      }

      if (args.dump) {
         this.dump_values = true;
         args.reallocate_objects = true;
         if ((args.numentries === undefined) && !args.elist) {
            args.numentries = 10;
            args._dflt_entries = true;
         }
      }

      return expr;
   }

   /** @summary Create draw expression for N-dim with cut */
   createDrawExpression(tree, names, cut, args) {
      if (args.dump && names.length === 1 && names[0] === 'Entry$') {
         args.dump_entries = true;
         args.dump = false;
      }

      if (args.dump_entries) {
         this.dump_entries = true;
         this.hist = [];
         if (args._dflt_entries) {
            delete args._dflt_entries;
            delete args.numentries;
         }
      }

      let is_direct = !cut && !this.dump_entries;

      this.ndim = names.length;

      for (let n = 0; n < this.ndim; ++n) {
         this.vars[n] = new TDrawVariable(this.globals);
         if (!this.vars[n].parse(tree, this, names[n]))
            return false;
         if (!this.vars[n].direct_branch)
            is_direct = false;
      }

      this.cut = new TDrawVariable(this.globals);
      if (cut && !this.cut.parse(tree, this, cut))
         return false;

      if (!this.numBranches()) {
         console.warn('no any branch is selected');
         return false;
      }

      if (is_direct)
         this.ProcessArrays = this.ProcessArraysFunc;

      this.monitoring = args.monitoring;

      // force TPolyMarker3D drawing for 3D case
      if ((this.ndim === 3) && !this.want_hist && !args.dump)
         args.graph = true;

      this.graph = args.graph;

      if (args.drawopt !== undefined)
         this.drawopt = args.drawopt;
      else
         args.drawopt = this.drawopt = this.graph ? 'P' : '';

      return true;
   }

   /** @summary Parse draw expression */
   parseDrawExpression(tree, args) {
      // parse complete expression
      let expr = this.parseParameters(tree, args, args.expr), cut = '';

      // parse option for histogram creation
      this.draw_title = `drawing '${expr}'`;
      if (tree?.fName)
         this.draw_title += ` from ${tree.fName}`;

      let pos;
      if (args.cut)
         cut = args.cut;
      else {
         pos = expr.replace(/TMath::/g, 'TMath__').lastIndexOf('::'); // avoid confusion due-to :: in the namespace
         if (pos >= 0) {
            cut = expr.slice(pos + 2).trim();
            expr = expr.slice(0, pos).trim();
         }
      }

      args.parse_expr = expr;
      args.parse_cut = cut;

      // let names = expr.split(':'); // to allow usage of ? operator, we need to handle : as well
      let names = [], nbr1 = 0, nbr2 = 0, prev = 0;
      for (pos = 0; pos < expr.length; ++pos) {
         switch (expr[pos]) {
            case '(':
               nbr1++;
               break;
            case ')':
               nbr1--;
               break;
            case '[':
               nbr2++;
               break;
            case ']':
               nbr2--;
               break;
            case ':':
               if (expr[pos + 1] === ':') {
                  pos++;
                  continue;
               }
               if (!nbr1 && !nbr2 && (pos > prev))
                  names.push(expr.slice(prev, pos));
               prev = pos + 1;
               break;
         }
      }
      if (!nbr1 && !nbr2 && (pos > prev))
         names.push(expr.slice(prev, pos));

      if (args.staged) {
         args.staged_names = names;
         names = ['Entry$'];
         args.dump_entries = true;
      } else if (cut && args.dump_entries)
         names = ['Entry$'];
      else if ((names.length < 1) || (names.length > 3))
         return false;

      return this.createDrawExpression(tree, names, cut, args);
   }

   /** @summary Draw only specified branch */
   drawOnlyBranch(tree, branch, expr, args) {
      this.ndim = 1;

      if (expr.indexOf('dump') === 0)
         expr = ';' + expr;

      expr = this.parseParameters(tree, args, expr);

      this.monitoring = args.monitoring;

      if (args.dump) {
         this.dump_values = true;
         args.reallocate_objects = true;
      }

      if (this.dump_values) {
         this.hist = []; // array of dump objects
         this.leaf = args.leaf;

         // branch object remains, therefore we need to copy fields to see them all
         this.copy_fields = ((args.branch.fLeaves?.arr.length > 1) || args.branch.fBranches?.arr.length) && !args.leaf;

         this.addBranch(branch, 'br0', args.direct_branch); // add branch

         this.Process = this.ProcessDump;

         return true;
      }

      this.vars[0] = new TDrawVariable(this.globals);
      if (!this.vars[0].parse(tree, this, expr, branch, args.direct_branch))
         return false;
      this.draw_title = `drawing branch ${branch.fName} ${expr ? ' expr:' + expr : ''} from ${tree.fName ?? ''}`;

      this.cut = new TDrawVariable(this.globals);

      if (this.vars[0].direct_branch)
         this.ProcessArrays = this.ProcessArraysFunc;

      return true;
   }

   /** @summary Begin processing */
   Begin(tree) {
      this.globals.entries = this.getNumEntries(tree);

      if (this.monitoring)
         this.lasttm = new Date().getTime();
   }

   /** @summary Show progress */
   ShowProgress(/* value */) {}

   /** @summary Get bins for bits histogram */
   getBitsBins(nbits, res) {
      res.nbins = res.max = nbits;
      res.fLabels = create$1(clTHashList);
      for (let k = 0; k < nbits; ++k) {
         const s = create$1(clTObjString);
         s.fString = k.toString();
         s.fUniqueID = k + 1;
         res.fLabels.Add(s);
      }
      return res;
   }

   /** @summary Get min.max bins */
   getMinMaxBins(axisid, nbins) {
      const res = { min: 0, max: 0, nbins, k: 1, fLabels: null, title: '' };
      if (axisid >= this.ndim)
         return res;

      const arr = this.vars[axisid].buf;
      res.title = this.vars[axisid].code || '';

      if (this.vars[axisid].kind === 'object') {
         // this is any object type
         let typename, similar = true, maxbits = 8;
         for (let k = 0; k < arr.length; ++k) {
            if (!arr[k])
               continue;
            if (!typename)
               typename = arr[k]._typename;
            if (typename !== arr[k]._typename)
               similar = false; // check all object types
            if (arr[k].fNbits)
               maxbits = Math.max(maxbits, arr[k].fNbits + 1);
         }

         if (typename && similar) {
            if ((typename === 'TBits') && (axisid === 0)) {
               this.fill1DHistogram = this.fillTBitsHistogram;
               if (maxbits % 8)
                  maxbits = (maxbits & 0xfff0) + 8;

               if ((this.hist_name === 'bits') && (this.hist_args.length === 1) && this.hist_args[0])
                  maxbits = this.hist_args[0];

               return this.getBitsBins(maxbits, res);
            }
         }
      }

      if (this.vars[axisid].kind === 'boolean') {
         res.lbls = ['false', 'true'];
         this.fill1DHistogram = this.fillBooleanHistogram;
      } else if (this.vars[axisid].kind === 'string') {
         res.lbls = []; // all labels
         for (let k = 0; k < arr.length; ++k) {
            if (res.lbls.indexOf(arr[k]) < 0)
               res.lbls.push(arr[k]);
         }
         res.lbls.sort();
      } else if ((axisid === 0) && (this.hist_name === 'bits') && (this.hist_args.length <= 1)) {
         this.fill1DHistogram = this.fillBitsHistogram;
         return this.getBitsBins(this.hist_args[0] || 32, res);
      } else if (axisid * 3 + 2 < this.hist_args.length) {
         res.nbins = this.hist_args[axisid * 3];
         res.min = this.hist_args[axisid * 3 + 1];
         res.max = this.hist_args[axisid * 3 + 2];
      } else {
         let is_any = false;
         for (let i = 1; i < arr.length; ++i) {
            const v = arr[i];
            if (!Number.isFinite(v))
               continue;
            if (is_any) {
               res.min = Math.min(res.min, v);
               res.max = Math.max(res.max, v);
            } else {
               res.min = res.max = v;
               is_any = true;
            }
         }
         if (!is_any) {
            res.min = 0;
            res.max = 1;
         }

         if (this.hist_nbins)
            nbins = res.nbins = this.hist_nbins;

         res.isinteger = (Math.round(res.min) === res.min) && (Math.round(res.max) === res.max);
         if (res.isinteger) {
            for (let k = 0; k < arr.length; ++k) {
               if (arr[k] !== Math.round(arr[k])) {
                  res.isinteger = false;
                  break;
               }
            }
         }

         if (res.isinteger) {
            res.min = Math.round(res.min);
            res.max = Math.round(res.max);
            if (res.max - res.min < nbins * 5) {
               res.min -= 1;
               res.max += 2;
               res.nbins = Math.round(res.max - res.min);
            } else {
               const range = (res.max - res.min + 2);
               let step = Math.floor(range / nbins);
               while (step * nbins < range)
                  step++;
               res.max = res.min + nbins * step;
            }
         } else if (res.min >= res.max) {
            res.max = res.min;
            if (Math.abs(res.min) < 100) {
               res.min -= 1;
               res.max += 1;
            } else if (res.min > 0) {
               res.min *= 0.9;
               res.max *= 1.1;
            } else {
               res.min *= 1.1;
               res.max *= 0.9;
            }
         } else
            res.max += (res.max - res.min) / res.nbins;
      }

      if (res.lbls) {
         res.max = res.nbins = res.lbls.length;

         res.fLabels = create$1(clTHashList);
         for (let k = 0; k < res.lbls.length; ++k) {
            const s = create$1(clTObjString);
            s.fString = res.lbls[k];
            s.fUniqueID = k + 1;
            if (s.fString === '')
               s.fString = '<empty>';
            res.fLabels.Add(s);
         }
      }

      res.k = res.nbins / (res.max - res.min);

      res.GetBin = function(value) {
         const bin = this.lbls?.indexOf(value) ?? (Number.isFinite(value) ? Math.floor((value - this.min) * this.k) : this.nbins + 1);
         return bin < 0 ? 0 : ((bin > this.nbins) ? this.nbins + 1 : bin + 1);
      };

      return res;
   }

   /** @summary Create histogram which matches value in dimensions */
   createHistogram(nbins, set_hist = false) {
      if (!nbins)
         nbins = 20;

      const x = this.getMinMaxBins(0, nbins),
            y = this.getMinMaxBins(1, nbins),
            z = this.getMinMaxBins(2, nbins);
      let hist = null;

      switch (this.ndim) {
         case 1: hist = createHistogram(clTH1 + this.htype, x.nbins); break;
         case 2: hist = createHistogram(clTH2 + this.htype, x.nbins, y.nbins); break;
         case 3: hist = createHistogram(clTH3 + this.htype, x.nbins, y.nbins, z.nbins); break;
      }

      hist.fXaxis.fTitle = x.title;
      hist.fXaxis.fXmin = x.min;
      hist.fXaxis.fXmax = x.max;
      hist.fXaxis.fLabels = x.fLabels;

      if (this.ndim > 1)
         hist.fYaxis.fTitle = y.title;
      hist.fYaxis.fXmin = y.min;
      hist.fYaxis.fXmax = y.max;
      hist.fYaxis.fLabels = y.fLabels;

      if (this.ndim > 2)
         hist.fZaxis.fTitle = z.title;
      hist.fZaxis.fXmin = z.min;
      hist.fZaxis.fXmax = z.max;
      hist.fZaxis.fLabels = z.fLabels;

      hist.fName = this.hist_name;
      hist.fTitle = this.draw_title;
      hist.fOption = this.drawopt;
      hist.$custom_stat = (this.hist_name === '$htemp') ? 111110 : 111111;

      if (set_hist) {
         this.hist = hist;
         this.x = x;
         this.y = y;
         this.z = z;
      } else
         hist.fBits |= kNoStats;

      return hist;
   }

   /** @summary Create output object - histogram, graph, dump array */
   createOutputObject() {
      if (this.hist || !this.vars[0].buf)
         return;

      if (this.dump_values) {
         // just create array where dumped values will be collected
         this.hist = [];

         // reassign fill method
         this.fill1DHistogram = this.fill2DHistogram = this.fill3DHistogram = this.dumpValues;
      } else if (this.graph) {
         const N = this.vars[0].buf.length;
         let res = null;

         if (this.ndim === 1) {
            // A 1-dimensional graph will just have the x axis as an index
            res = createTGraph(N, Array.from(Array(N).keys()), this.vars[0].buf);
            res.fName = 'Graph';
            res.fTitle = this.draw_title;
         } else if (this.ndim === 2) {
            res = createTGraph(N, this.vars[0].buf, this.vars[1].buf);
            res.fName = 'Graph';
            res.fTitle = this.draw_title;
            delete this.vars[1].buf;
         } else if (this.ndim === 3) {
            res = create$1(clTPolyMarker3D);
            res.fN = N;
            res.fLastPoint = N - 1;
            const arr = new Array(N * 3);
            for (let k = 0; k < N; ++k) {
               arr[k * 3] = this.vars[0].buf[k];
               arr[k * 3 + 1] = this.vars[1].buf[k];
               arr[k * 3 + 2] = this.vars[2].buf[k];
            }
            res.fP = arr;
            res.$hist = this.createHistogram(10);
            delete this.vars[1].buf;
            delete this.vars[2].buf;
            res.fName = 'Points';
         }

         this.hist = res;
      } else {
         const nbins = [200, 50, 20];
         this.createHistogram(nbins[this.ndim], true);
      }

      const var0 = this.vars[0].buf, cut = this.cut.buf, len = var0.length;

      if (!this.graph) {
         switch (this.ndim) {
            case 1: {
               for (let n = 0; n < len; ++n)
                  this.fill1DHistogram(var0[n], cut ? cut[n] : 1);
               break;
            }
            case 2: {
               const var1 = this.vars[1].buf;
               for (let n = 0; n < len; ++n)
                  this.fill2DHistogram(var0[n], var1[n], cut ? cut[n] : 1);
               delete this.vars[1].buf;
               break;
            }
            case 3: {
               const var1 = this.vars[1].buf, var2 = this.vars[2].buf;
               for (let n = 0; n < len; ++n)
                  this.fill3DHistogram(var0[n], var1[n], var2[n], cut ? cut[n] : 1);
               delete this.vars[1].buf;
               delete this.vars[2].buf;
               break;
            }
         }
      }

      delete this.vars[0].buf;
      delete this.cut.buf;
   }

   /** @summary Fill TBits histogram */
   fillTBitsHistogram(xvalue, weight) {
      if (!weight || !xvalue || !xvalue.fNbits || !xvalue.fAllBits)
         return;

      const sz = Math.min(xvalue.fNbits + 1, xvalue.fNbytes * 8);

      for (let bit = 0, mask = 1, b = 0; bit < sz; ++bit) {
         if (xvalue.fAllBits[b] && mask) {
            if (bit <= this.x.nbins)
               this.hist.fArray[bit + 1] += weight;
            else
               this.hist.fArray[this.x.nbins + 1] += weight;
         }

         mask *= 2;
         if (mask >= 0x100) {
            mask = 1;
            ++b;
         }
      }
   }

   /** @summary Fill bits histogram */
   fillBitsHistogram(xvalue, weight) {
      if (!weight)
         return;

      for (let bit = 0, mask = 1; bit < this.x.nbins; ++bit) {
         if (xvalue & mask)
            this.hist.fArray[bit + 1] += weight;
         mask *= 2;
      }
   }

   /** @summary Fill boolean histogram */
   fillBooleanHistogram(boolvalue, weight) {
      if (!weight)
         return;
      const xvalue = boolvalue ? 1 : 0;
      this.hist.fArray[xvalue + 1] += weight;
      this.hist.fTsumw += weight;
      this.hist.fTsumwx += weight * xvalue;
      this.hist.fTsumwx2 += weight * xvalue * xvalue;
   }

   /** @summary Fill 1D histogram */
   fill1DHistogram(xvalue, weight) {
      const bin = this.x.GetBin(xvalue);
      this.hist.fArray[bin] += weight;

      if (!this.x.lbls && Number.isFinite(xvalue)) {
         this.hist.fTsumw += weight;
         this.hist.fTsumwx += weight * xvalue;
         this.hist.fTsumwx2 += weight * xvalue * xvalue;
      }
   }

   /** @summary Fill 2D histogram */
   fill2DHistogram(xvalue, yvalue, weight) {
      const xbin = this.x.GetBin(xvalue),
            ybin = this.y.GetBin(yvalue);

      this.hist.fArray[xbin + (this.x.nbins + 2) * ybin] += weight;
      if (!this.x.lbls && !this.y.lbls && Number.isFinite(xvalue) && Number.isFinite(yvalue)) {
         this.hist.fTsumw += weight;
         this.hist.fTsumwx += weight * xvalue;
         this.hist.fTsumwy += weight * yvalue;
         this.hist.fTsumwx2 += weight * xvalue * xvalue;
         this.hist.fTsumwxy += weight * xvalue * yvalue;
         this.hist.fTsumwy2 += weight * yvalue * yvalue;
      }
   }

   /** @summary Fill 3D histogram */
   fill3DHistogram(xvalue, yvalue, zvalue, weight) {
      const xbin = this.x.GetBin(xvalue),
            ybin = this.y.GetBin(yvalue),
            zbin = this.z.GetBin(zvalue);

      this.hist.fArray[xbin + (this.x.nbins + 2) * (ybin + (this.y.nbins + 2) * zbin)] += weight;
      if (!this.x.lbls && !this.y.lbls && !this.z.lbls && Number.isFinite(xvalue) && Number.isFinite(yvalue) && Number.isFinite(zvalue)) {
         this.hist.fTsumw += weight;
         this.hist.fTsumwx += weight * xvalue;
         this.hist.fTsumwy += weight * yvalue;
         this.hist.fTsumwz += weight * zvalue;
         this.hist.fTsumwx2 += weight * xvalue * xvalue;
         this.hist.fTsumwy2 += weight * yvalue * yvalue;
         this.hist.fTsumwz2 += weight * zvalue * zvalue;
         this.hist.fTsumwxy += weight * xvalue * yvalue;
         this.hist.fTsumwxz += weight * xvalue * zvalue;
         this.hist.fTsumwyz += weight * yvalue * zvalue;
      }
   }

   /** @summary Dump values */
   dumpValues(v1, v2, v3, v4) {
      let obj;
      switch (this.ndim) {
         case 1: obj = { x: v1, weight: v2 }; break;
         case 2: obj = { x: v1, y: v2, weight: v3 }; break;
         case 3: obj = { x: v1, y: v2, z: v3, weight: v4 }; break;
      }

      if (this.cut.is_dummy()) {
         if (this.ndim === 1)
            obj = v1;
         else
            delete obj.weight;
      }

      this.hist.push(obj);
   }

   /** @summary function used when all branches can be read as array
     * @desc most typical usage - histogram filling of single branch */
   ProcessArraysFunc(/* entry */) {
      if (this.arr_limit || this.graph) {
         const var0 = this.vars[0],
               var1 = this.vars[1],
               var2 = this.vars[2],
               len = this.tgtarr.br0.length;
         if (!var0.buf.length && (len >= this.arr_limit) && !this.graph) {
            // special use case - first array large enough to create histogram directly base on it
            var0.buf = this.tgtarr.br0;
            if (var1)
               var1.buf = this.tgtarr.br1;
            if (var2)
               var2.buf = this.tgtarr.br2;
         } else {
            for (let k = 0; k < len; ++k) {
               var0.buf.push(this.tgtarr.br0[k]);
               if (var1)
                  var1.buf.push(this.tgtarr.br1[k]);
               if (var2)
                  var2.buf.push(this.tgtarr.br2[k]);
            }
         }
         var0.kind = 'number';
         if (var1)
            var1.kind = 'number';
         if (var2)
            var2.kind = 'number';
         this.cut.buf = null; // do not create buffer for cuts
         if (!this.graph && (var0.buf.length >= this.arr_limit)) {
            this.createOutputObject();
            this.arr_limit = 0;
         }
      } else {
         const br0 = this.tgtarr.br0, len = br0.length;
         switch (this.ndim) {
            case 1: {
               for (let k = 0; k < len; ++k)
                  this.fill1DHistogram(br0[k], 1);
               break;
            }
            case 2: {
               const br1 = this.tgtarr.br1;
               for (let k = 0; k < len; ++k)
                  this.fill2DHistogram(br0[k], br1[k], 1);
               break;
            }
            case 3: {
               const br1 = this.tgtarr.br1, br2 = this.tgtarr.br2;
               for (let k = 0; k < len; ++k)
                  this.fill3DHistogram(br0[k], br1[k], br2[k], 1);
               break;
            }
         }
      }
   }

   /** @summary simple dump of the branch - no need to analyze something */
   ProcessDump(/* entry */) {
      const res = this.leaf ? this.tgtobj.br0[this.leaf] : this.tgtobj.br0;

      if (res && this.copy_fields) {
         if (checkArrayPrototype(res) === 0)
            this.hist.push(Object.assign({}, res));
         else
            this.hist.push(res);
      } else
         this.hist.push(res);
   }

   /** @summary Normal TSelector Process handler */
   Process(entry) {
      this.globals.entry = entry; // can be used in any expression

      this.cut.produce(this.tgtobj);
      if (!this.dump_values && !this.cut.value)
         return;

      for (let n = 0; n < this.ndim; ++n)
         this.vars[n].produce(this.tgtobj);

      const var0 = this.vars[0], var1 = this.vars[1], var2 = this.vars[2], cut = this.cut;

      if (this.dump_entries)
         this.hist.push(entry);
      else if (this.graph || this.arr_limit) {
         switch (this.ndim) {
            case 1:
               for (let n0 = 0; n0 < var0.length; ++n0) {
                  var0.buf.push(var0.get(n0));
                  cut.buf?.push(cut.value);
               }
               break;
            case 2:
               for (let n0 = 0; n0 < var0.length; ++n0) {
                  for (let n1 = 0; n1 < var1.length; ++n1) {
                     var0.buf.push(var0.get(n0));
                     var1.buf.push(var1.get(n1));
                     cut.buf?.push(cut.value);
                  }
               }
               break;
            case 3:
               for (let n0 = 0; n0 < var0.length; ++n0) {
                  for (let n1 = 0; n1 < var1.length; ++n1) {
                     for (let n2 = 0; n2 < var2.length; ++n2) {
                        var0.buf.push(var0.get(n0));
                        var1.buf.push(var1.get(n1));
                        var2.buf.push(var2.get(n2));
                        cut.buf?.push(cut.value);
                     }
                  }
               }
               break;
         }
         if (!this.graph && (var0.buf.length >= this.arr_limit)) {
            this.createOutputObject();
            this.arr_limit = 0;
         }
      } else if (this.hist) {
         switch (this.ndim) {
            case 1:
               for (let n0 = 0; n0 < var0.length; ++n0)
                  this.fill1DHistogram(var0.get(n0), cut.value);
               break;
            case 2:
               for (let n0 = 0; n0 < var0.length; ++n0) {
                  for (let n1 = 0; n1 < var1.length; ++n1)
                     this.fill2DHistogram(var0.get(n0), var1.get(n1), cut.value);
               }
               break;
            case 3:
               for (let n0 = 0; n0 < var0.length; ++n0) {
                  for (let n1 = 0; n1 < var1.length; ++n1) {
                     for (let n2 = 0; n2 < var2.length; ++n2)
                        this.fill3DHistogram(var0.get(n0), var1.get(n1), var2.get(n2), cut.value);
                  }
               }
               break;
         }
      }

      if (this.monitoring && this.hist && !this.dump_values) {
         const now = new Date().getTime();
         if (now - this.lasttm > this.monitoring) {
            this.lasttm = now;
            if (isFunc(this.progress_callback))
               this.progress_callback(this.hist);
         }
      }

      if ((this.nmatch !== undefined) && (--this.nmatch <= 0)) {
         if (!this.hist)
            this.createOutputObject();
         this.Abort();
      }
   }

   /** @summary Normal TSelector Terminate handler */
   Terminate(res) {
      if (res && !this.hist)
         this.createOutputObject();

      this.ShowProgress();

      if (isFunc(this.result_callback))
         this.result_callback(this.hist);
   }

} // class TDrawSelector


/** @summary return type name of given member in the class
  * @private */
function defineMemberTypeName(file, parent_class, member_name) {
   const s_i = file.findStreamerInfo(parent_class),
         arr = s_i?.fElements?.arr;
   if (!arr)
      return '';

   let elem = null;
   for (let k = 0; k < arr.length; ++k) {
      if (arr[k].fTypeName === kBaseClass) {
         const res = defineMemberTypeName(file, arr[k].fName, member_name);
         if (res)
            return res;
      } else if (arr[k].fName === member_name) {
         elem = arr[k];
         break;
      }
   }

   if (!elem)
      return '';

   const clname = elem.fTypeName;
   return clname.at(-1) === '*' ? clname.slice(0, clname.length - 1) : clname;
}

/** @summary create fast list to assign all methods to the object
  * @private */
function makeMethodsList(typename) {
   const methods = getMethods(typename), res = {
      names: [],
      values: [],
      Create() {
         const obj = {};
         for (let n = 0; n < this.names.length; ++n)
            obj[this.names[n]] = this.values[n];
         return obj;
      }
   };

   res.names.push('_typename');
   res.values.push(typename);
   for (const key in methods) {
      res.names.push(key);
      res.values.push(methods[key]);
   }
   return res;
}

/** @summary try to define classname for the branch member, scanning list of branches
  * @private */
function detectBranchMemberClass(brlst, prefix, start) {
   let clname = '';
   for (let kk = (start || 0); kk < brlst.arr.length; ++kk) {
      if ((brlst.arr[kk].fName.indexOf(prefix) === 0) && brlst.arr[kk].fClassName)
         clname = brlst.arr[kk].fClassName;
   }
   return clname;
}

/** @summary Process selector for the tree
  * @desc function similar to the TTree::Process
  * @param {object} tree - instance of TTree class
  * @param {object} selector - instance of {@link TSelector} class
  * @param {object} [args] - different arguments
  * @param {number} [args.firstentry] - first entry to process, 0 when not specified
  * @param {number} [args.numentries] - number of entries to process, all when not specified
  * @param {Array} [args.elist] - arrays of entries id to process
  * @return {Promise} with TSelector instance */
async function treeProcess(tree, selector, args) {
   if (!args)
      args = {};

   if (!selector || !tree.$file || !selector.numBranches()) {
      selector?.Terminate(false);
      return Promise.reject(Error('required parameter missing for TTree::Process'));
   }

   // central handle with all information required for reading
   const handle = {
      tree, // keep tree reference
      file: tree.$file, // keep file reference
      selector, // reference on selector
      arr: [], // list of branches
      current_entry: -1, // current processed entry
      simple_read: true, // all baskets in all used branches are in sync,
      process_arrays: true // one can process all branches as arrays
   }, createLeafElem = (leaf, name) => {
      // function creates TStreamerElement which corresponds to the elementary leaf
      let datakind;
      switch (leaf._typename) {
         case 'TLeafF': datakind = kFloat; break;
         case 'TLeafD': datakind = kDouble; break;
         case 'TLeafO': datakind = kBool; break;
         case 'TLeafB': datakind = leaf.fIsUnsigned ? kUChar : kChar; break;
         case 'TLeafS': datakind = leaf.fIsUnsigned ? kUShort : kShort; break;
         case 'TLeafI': datakind = leaf.fIsUnsigned ? kUInt : kInt; break;
         case 'TLeafL': datakind = leaf.fIsUnsigned ? kULong64 : kLong64; break;
         case 'TLeafG': datakind = leaf.fIsUnsigned ? kULong : kLong; break;
         case 'TLeafC': datakind = kTString; break;
         default: return null;
      }
      const elem = createStreamerElement(name || leaf.fName, datakind);
      if ((leaf.fLen > 1) && (datakind !== kTString)) {
         elem.fType += kOffsetL;
         elem.fArrayLength = leaf.fLen;
      }
      return elem;
   }, findInHandle = branch => {
      for (let k = 0; k < handle.arr.length; ++k) {
         if (handle.arr[k].branch === branch)
            return handle.arr[k];
      }
      return null;
   };

   let namecnt = 0;

   function addBranchForReading(branch, target_object, target_name, read_mode) {
      // central method to add branch for reading
      // read_mode == true - read only this branch
      // read_mode == '$child$' is just member of object from for STL or clones array
      // read_mode == '<any class name>' is sub-object from STL or clones array, happens when such new object need to be created
      // read_mode == '.member_name' select only reading of member_name instead of complete object

      if (isStr(branch))
         branch = findBranch(handle.tree, branch);

      if (!branch) {
         console.error('Did not found branch');
         return null;
      }

      let item = findInHandle(branch);

      if (item) {
         console.error(`Branch ${branch.fName} already configured for reading`);
         if (item.tgt !== target_object)
            console.error('Target object differs');
         return null;
      }

      if (!branch.fEntries) {
         console.warn(`Branch ${branch.fName} does not have entries`);
         return null;
      }

      item = {
         branch,
         tgt: target_object, // used target object - can be differ for object members
         name: target_name,
         index: -1, // index in the list of read branches
         member: null, // member to read branch
         type: 0, // keep type identifier
         curr_entry: -1, // last processed entry
         raw: null, // raw buffer for reading
         basket: null, // current basket object
         curr_basket: 0,  // number of basket used for processing
         read_entry: -1,  // last entry which is already read
         staged_entry: -1, // entry which is staged for reading
         first_readentry: -1, // first entry to read
         staged_basket: 0,  // last basket staged for reading
         eindx: 0, // index of last checked entry when selecting baskets
         selected_baskets: [],   // array of selected baskets, used when specific events are selected
         numentries: branch.fEntries,
         numbaskets: branch.fWriteBasket, // number of baskets which can be read from the file
         counters: null, // branch indexes used as counters
         ascounter: [], // list of other branches using that branch as counter
         baskets: [], // array for read baskets,
         staged_prev: 0, // entry limit of previous I/O request
         staged_now: 0, // entry limit of current I/O request
         progress_showtm: 0, // last time when progress was showed
         getBasketEntry(k) {
            if (!this.branch || (k > this.branch.fMaxBaskets))
               return 0;
            const res = (k < this.branch.fMaxBaskets) ? this.branch.fBasketEntry[k] : 0;
            if (res)
               return res;
            const bskt = (k > 0) ? this.branch.fBaskets.arr[k - 1] : null;
            return bskt ? (this.branch.fBasketEntry[k - 1] + bskt.fNevBuf) : 0;
         },
         getTarget(tgtobj) {
            // returns target object which should be used for the branch reading
            if (!this.tgt)
               return tgtobj;
            for (let k = 0; k < this.tgt.length; ++k) {
               const sub = this.tgt[k];
               if (!tgtobj[sub.name])
                  tgtobj[sub.name] = sub.lst.Create();
               tgtobj = tgtobj[sub.name];
            }
            return tgtobj;
         },
         getEntry(entry) {
            // This should be equivalent to TBranch::GetEntry() method
            const shift = entry - this.first_entry;
            let off;
            if (!this.branch.TestBit(kDoNotUseBufferMap))
               this.raw.clearObjectMap();
            if (this.basket.fEntryOffset) {
               off = this.basket.fEntryOffset[shift];
               if (this.basket.fDisplacement)
                  this.raw.fDisplacement = this.basket.fDisplacement[shift];
            } else
               off = this.basket.fKeylen + this.basket.fNevBufSize * shift;
            this.raw.locate(off - this.raw.raw_shift);

            // this.member.func(this.raw, this.getTarget(tgtobj));
         }
      };

      // last basket can be stored directly with the branch
      while (item.getBasketEntry(item.numbaskets + 1))
         item.numbaskets++;

      // check all counters if we
      const nb_leaves = branch.fLeaves?.arr?.length ?? 0,
            leaf = (nb_leaves > 0) ? branch.fLeaves.arr[0] : null,
            is_brelem = (branch._typename === clTBranchElement);
      let elem = null, // TStreamerElement used to create reader
          member = null, // member for actual reading of the branch
          child_scan = 0, // scan child branches after main branch is appended
          item_cnt = null, item_cnt2 = null, object_class;

      if (branch.fBranchCount) {
         item_cnt = findInHandle(branch.fBranchCount);

         if (!item_cnt)
            item_cnt = addBranchForReading(branch.fBranchCount, target_object, '$counter' + namecnt++, true);

         if (!item_cnt) {
            console.error(`Cannot add counter branch ${branch.fBranchCount.fName}`);
            return null;
         }

         let BranchCount2 = branch.fBranchCount2;

         if (!BranchCount2 && (branch.fBranchCount.fStreamerType === kSTL) &&
            ((branch.fStreamerType === kStreamLoop) || (branch.fStreamerType === kOffsetL + kStreamLoop))) {
            // special case when count member from kStreamLoop not assigned as fBranchCount2
            const elemd = findBrachStreamerElement(branch, handle.file),
                  arrd = branch.fBranchCount.fBranches.arr;

            if (elemd?.fCountName && arrd) {
               for (let k = 0; k < arrd.length; ++k) {
                  if (arrd[k].fName === branch.fBranchCount.fName + '.' + elemd.fCountName) {
                     BranchCount2 = arrd[k];
                     break;
                  }
               }
            }

            if (!BranchCount2)
               console.error('Did not found branch for second counter of kStreamLoop element');
         }

         if (BranchCount2) {
            item_cnt2 = findInHandle(BranchCount2);

            if (!item_cnt2)
               item_cnt2 = addBranchForReading(BranchCount2, target_object, '$counter' + namecnt++, true);

            if (!item_cnt2) {
               console.error(`Cannot add counter branch2 ${BranchCount2.fName}`);
               return null;
            }
         }
      } else if (nb_leaves === 1 && leaf?.fLeafCount) {
         const br_cnt = findBranch(handle.tree, leaf.fLeafCount.fName);

         if (br_cnt) {
            item_cnt = findInHandle(br_cnt);

            if (!item_cnt)
               item_cnt = addBranchForReading(br_cnt, target_object, '$counter' + namecnt++, true);

            if (!item_cnt) {
               console.error(`Cannot add counter branch ${br_cnt.fName}`);
               return null;
            }
         }
      }

      function scanBranches(lst, master_target, chld_kind) {
         if (!lst?.arr.length)
            return true;

         let match_prefix = branch.fName;
         if (match_prefix.at(-1) === '.')
            match_prefix = match_prefix.slice(0, match_prefix.length - 1);
         if (isStr(read_mode) && (read_mode[0] === '.'))
            match_prefix += read_mode;
         match_prefix += '.';

         for (let k = 0; k < lst.arr.length; ++k) {
            const br = lst.arr[k];
            if ((chld_kind > 0) && (br.fType !== chld_kind))
               continue;

            if (br.fType === kBaseClassNode) {
               if (!scanBranches(br.fBranches, master_target, chld_kind))
                  return false;
               continue;
            }

            const elem2 = findBrachStreamerElement(br, handle.file);
            if (elem2?.fTypeName === kBaseClass) {
               // if branch is data of base class, map it to original target
               if (br.fTotBytes && !addBranchForReading(br, target_object, target_name, read_mode))
                  return false;
               if (!scanBranches(br.fBranches, master_target, chld_kind))
                  return false;
               continue;
            }

            let subname = br.fName, chld_direct = 1;

            if (br.fName.indexOf(match_prefix) === 0)
               subname = subname.slice(match_prefix.length);
            else if (chld_kind > 0)
               continue; // for defined children names prefix must be present

            let p = subname.indexOf('[');
            if (p > 0)
               subname = subname.slice(0, p);
            p = subname.indexOf('<');
            if (p > 0)
               subname = subname.slice(0, p);

            if (chld_kind > 0) {
               chld_direct = '$child$';
               const pp = subname.indexOf('.');
               if (pp > 0)
                  chld_direct = detectBranchMemberClass(lst, branch.fName + '.' + subname.slice(0, pp + 1), k) || clTObject;
            }

            if (!addBranchForReading(br, master_target, subname, chld_direct))
               return false;
         }

         return true;
      }

      if (branch._typename === 'TBranchObject') {
         member = {
            name: target_name,
            typename: branch.fClassName,
            virtual: leaf.fVirtual,
            func(buf, obj) {
               const clname = this.virtual ? buf.readFastString(buf.ntou1() + 1) : this.typename;
               obj[this.name] = buf.classStreamer({}, clname);
            }
         };
      } else if ((branch.fType === kClonesNode) || (branch.fType === kSTLNode)) {
         elem = createStreamerElement(target_name, kInt);

         if (!read_mode || (isStr(read_mode) && (read_mode[0] === '.')) || (read_mode === 1)) {
            handle.process_arrays = false;

            member = {
               name: target_name,
               conttype: branch.fClonesName || clTObject,
               reallocate: args.reallocate_objects,
               func(buf, obj) {
                  const size = buf.ntoi4();
                  let n = 0, arr = obj[this.name];
                  if (!arr || this.reallocate)
                     arr = obj[this.name] = new Array(size);
                  else {
                     n = arr.length;
                     arr.length = size; // reallocate array
                  }

                  while (n < size)
                     arr[n++] = this.methods.Create(); // create new objects
               }
            };

            if (isStr(read_mode) && (read_mode[0] === '.')) {
               member.conttype = detectBranchMemberClass(branch.fBranches, branch.fName + read_mode);
               if (!member.conttype) {
                  console.error(`Cannot select object ${read_mode} in the branch ${branch.fName}`);
                  return null;
               }
            }

            member.methods = makeMethodsList(member.conttype);

            child_scan = (branch.fType === kClonesNode) ? kClonesMemberNode : kSTLMemberNode;
         }
      } else if ((object_class = getBranchObjectClass(branch, handle.tree))) {
         if (read_mode === true) {
            console.warn(`Object branch ${object_class} can not have data to be read directly`);
            return null;
         }

         handle.process_arrays = false;

         const newtgt = new Array((target_object?.length || 0) + 1);
         for (let l = 0; l < newtgt.length - 1; ++l)
            newtgt[l] = target_object[l];
         newtgt[newtgt.length - 1] = { name: target_name, lst: makeMethodsList(object_class) };

         // this kind of branch does not have baskets and not need to be read
         return scanBranches(branch.fBranches, newtgt, 0) ? item : null;
      } else if (is_brelem && (nb_leaves === 1) && (leaf.fName === branch.fName) && (branch.fID === -1)) {
         elem = createStreamerElement(target_name, branch.fClassName);

         if (elem.fType === kAny) {
            const streamer = handle.file.getStreamer(branch.fClassName, { val: branch.fClassVersion, checksum: branch.fCheckSum });
            if (!streamer) {
               elem = null;
               console.warn('not found streamer!');
            } else {
               member = {
                  name: target_name,
                  typename: branch.fClassName,
                  streamer,
                  func(buf, obj) {
                     const res = { _typename: this.typename };
                     for (let n = 0; n < this.streamer.length; ++n)
                        this.streamer[n].func(buf, res);
                     obj[this.name] = res;
                  }
               };
            }
         }

         // elem.fType = kAnyP;

         // only STL containers here
         // if (!elem.fSTLtype) elem = null;
      } else if (is_brelem && (nb_leaves <= 1)) {
         elem = findBrachStreamerElement(branch, handle.file);

         // this is basic type - can try to solve problem differently
         if (!elem && branch.fStreamerType && (branch.fStreamerType < 20))
            elem = createStreamerElement(target_name, branch.fStreamerType);
      } else if (nb_leaves === 1) {
         // no special constrains for the leaf names

         elem = createLeafElem(leaf, target_name);
      } else if ((branch._typename === 'TBranch') && (nb_leaves > 1)) {
         // branch with many elementary leaves

         const leaves = new Array(nb_leaves);
         let isok = true;
         for (let l = 0; l < nb_leaves; ++l) {
            leaves[l] = createMemberStreamer(createLeafElem(branch.fLeaves.arr[l]), handle.file);
            if (!leaves[l])
               isok = false;
         }

         if (isok) {
            member = {
               name: target_name,
               leaves,
               func(buf, obj) {
                  let tgt = obj[this.name], l = 0;
                  if (!tgt)
                     obj[this.name] = tgt = {};
                  while (l < this.leaves.length)
                     this.leaves[l++].func(buf, tgt);
               }
            };
         }
      }

      if (!elem && !member) {
         console.warn(`Not supported branch ${branch.fName} type ${branch._typename}`);
         return null;
      }

      if (!member) {
         member = createMemberStreamer(elem, handle.file);

         if ((member.base !== undefined) && member.basename) {
            // when element represent base class, we need handling which differ from normal IO
            member.func = function(buf, obj) {
               if (!obj[this.name])
                  obj[this.name] = { _typename: this.basename };
               buf.classStreamer(obj[this.name], this.basename);
            };
         }
      }

      if (item_cnt && isStr(read_mode)) {
         member.name0 = item_cnt.name;

         const snames = target_name.split('.');

         if (snames.length === 1) {
            // no point in the name - just plain array of objects
            member.get = (arr, n) => arr[n];
         } else if (read_mode === '$child$') {
            console.error(`target name ${target_name} contains point, but suppose to be direct child`);
            return null;
         } else if (snames.length === 2) {
            target_name = member.name = snames[1];
            member.name1 = snames[0];
            member.subtype1 = read_mode;
            member.methods1 = makeMethodsList(member.subtype1);
            member.get = function(arr, n) {
               let obj1 = arr[n][this.name1];
               if (!obj1)
                  obj1 = arr[n][this.name1] = this.methods1.Create();
               return obj1;
            };
         } else {
            // very complex task - we need to reconstruct several embedded members with their types
            // try our best - but not all data types can be reconstructed correctly
            // while classname is not enough - there can be different versions

            if (!branch.fParentName) {
               console.error(`Not possible to provide more than 2 parts in the target name ${target_name}`);
               return null;
            }

            target_name = member.name = snames.pop(); // use last element
            member.snames = snames; // remember all sub-names
            member.smethods = []; // and special handles to create missing objects

            let parent_class = branch.fParentName; // unfortunately, without version

            for (let k = 0; k < snames.length; ++k) {
               const chld_class = defineMemberTypeName(handle.file, parent_class, snames[k]);
               member.smethods[k] = makeMethodsList(chld_class || 'AbstractClass');
               parent_class = chld_class;
            }
            member.get = function(arr, n) {
               let obj1 = arr[n][this.snames[0]];
               if (!obj1)
                  obj1 = arr[n][this.snames[0]] = this.smethods[0].Create();
               for (let k = 1; k < this.snames.length; ++k) {
                  let obj2 = obj1[this.snames[k]];
                  if (!obj2)
                     obj2 = obj1[this.snames[k]] = this.smethods[k].Create();
                  obj1 = obj2;
               }
               return obj1;
            };
         }

         // case when target is sub-object and need to be created before

         if (member.objs_branch_func) {
            // STL branch provides special function for the reading
            member.func = member.objs_branch_func;
         } else {
            member.func0 = member.func;

            member.func = function(buf, obj) {
               const arr = obj[this.name0]; // objects array where reading is done
               let n = 0;
               while (n < arr.length)
                  this.func0(buf, this.get(arr, n++)); // read all individual object with standard functions
            };
         }
      } else if (item_cnt) {
         handle.process_arrays = false;

         if ((elem.fType === kDouble32) || (elem.fType === kFloat16)) {
            // special handling for compressed floats

            member.stl_size = item_cnt.name;
            member.func = function(buf, obj) {
               obj[this.name] = this.readarr(buf, obj[this.stl_size]);
            };
         } else if (((elem.fType === kOffsetP + kDouble32) || (elem.fType === kOffsetP + kFloat16)) && branch.fBranchCount2) {
            // special handling for variable arrays of compressed floats in branch - not tested

            member.stl_size = item_cnt.name;
            member.arr_size = item_cnt2.name;
            member.func = function(buf, obj) {
               const sz0 = obj[this.stl_size], sz1 = obj[this.arr_size], arr = new Array(sz0);
               for (let n = 0; n < sz0; ++n)
                  arr[n] = (buf.ntou1() === 1) ? this.readarr(buf, sz1[n]) : [];
               obj[this.name] = arr;
            };
         } else if (((elem.fType > 0) && (elem.fType < kOffsetL)) || (elem.fType === kTString) ||
                    (((elem.fType > kOffsetP) && (elem.fType < kOffsetP + kOffsetL)) && branch.fBranchCount2)) {
            // special handling of simple arrays
            member = {
               name: target_name,
               stl_size: item_cnt.name,
               type: elem.fType,
               func(buf, obj) {
                  obj[this.name] = buf.readFastArray(obj[this.stl_size], this.type);
               }
            };

            if (branch.fBranchCount2) {
               member.type -= kOffsetP;
               member.arr_size = item_cnt2.name;
               member.func = function(buf, obj) {
                  const sz0 = obj[this.stl_size], sz1 = obj[this.arr_size], arr = new Array(sz0);
                  for (let n = 0; n < sz0; ++n)
                     arr[n] = (buf.ntou1() === 1) ? buf.readFastArray(sz1[n], this.type) : [];
                  obj[this.name] = arr;
               };
            }
         } else if ((elem.fType > kOffsetP) && (elem.fType < kOffsetP + kOffsetL) && member.cntname)
            member.cntname = item_cnt.name;
         else if (elem.fType === kStreamer) {
            // with streamers one need to extend existing array

            if (item_cnt2)
               throw new Error('Second branch counter not supported yet with kStreamer');

            // function provided by normal I/O
            member.func = member.branch_func;
            member.stl_size = item_cnt.name;
         } else if ((elem.fType === kStreamLoop) || (elem.fType === kOffsetL + kStreamLoop)) {
            if (item_cnt2) {
               // special solution for kStreamLoop
               member.stl_size = item_cnt.name;
               member.cntname = item_cnt2.name;
               member.func = member.branch_func; // this is special function, provided by base I/O
            } else
               member.cntname = item_cnt.name;
         } else {
            member.name = '$stl_member';

            let loop_size_name;
            if (item_cnt2) {
               if (member.cntname) {
                  loop_size_name = item_cnt2.name;
                  member.cntname = '$loop_size';
               } else
                  throw new Error('Second branch counter not used - very BAD');
            }

            const stlmember = {
               name: target_name,
               stl_size: item_cnt.name,
               loop_size: loop_size_name,
               member0: member,
               func(buf, obj) {
                  const cnt = obj[this.stl_size], arr = new Array(cnt);
                  for (let n = 0; n < cnt; ++n) {
                     if (this.loop_size)
                        obj.$loop_size = obj[this.loop_size][n];
                     this.member0.func(buf, obj);
                     arr[n] = obj.$stl_member;
                  }
                  delete obj.$stl_member;
                  delete obj.$loop_size;
                  obj[this.name] = arr;
               }
            };

            member = stlmember;
         }
      } // if (item_cnt)

      // set name used to store result
      member.name = target_name;

      item.member = member; // member for reading
      if (elem)
         item.type = elem.fType;
      item.index = handle.arr.length; // index in the global list of branches

      if (item_cnt) {
         item.counters = [item_cnt.index];
         item_cnt.ascounter.push(item.index);

         if (item_cnt2) {
            item.counters.push(item_cnt2.index);
            item_cnt2.ascounter.push(item.index);
         }
      }

      handle.arr.push(item);

      // now one should add all other child branches
      if (child_scan && !scanBranches(branch.fBranches, target_object, child_scan))
         return null;

      return item;
   }

   // main loop to add all branches from selector for reading
   for (let nn = 0; nn < selector.numBranches(); ++nn) {
      const item = addBranchForReading(selector.getBranch(nn), undefined, selector.nameOfBranch(nn), selector._directs[nn]);

      if (!item) {
         selector.Terminate(false);
         return Promise.reject(Error(`Fail to add branch ${selector.nameOfBranch(nn)}`));
      }
   }

   // check if simple reading can be performed and there are direct data in branch

   for (let h = 1; (h < handle.arr.length) && handle.simple_read; ++h) {
      const item = handle.arr[h], item0 = handle.arr[0];

      if ((item.numentries !== item0.numentries) || (item.numbaskets !== item0.numbaskets))
         handle.simple_read = false;
      for (let n = 0; n < item.numbaskets; ++n) {
         if (item.getBasketEntry(n) !== item0.getBasketEntry(n))
            handle.simple_read = false;
      }
   }

   // now calculate entries range

   handle.firstentry = handle.lastentry = 0;
   for (let nn = 0; nn < handle.arr.length; ++nn) {
      const branch = handle.arr[nn].branch,
            e1 = branch.fFirstEntry ?? (branch.fBasketBytes[0] ? branch.fBasketEntry[0] : 0);
      handle.firstentry = Math.max(handle.firstentry, e1);
      handle.lastentry = (nn === 0) ? (e1 + branch.fEntries) : Math.min(handle.lastentry, e1 + branch.fEntries);
   }

   if (handle.firstentry >= handle.lastentry) {
      selector.Terminate(false);
      return Promise.reject(Error('No any common events for selected branches'));
   }

   handle.process_min = handle.firstentry;
   handle.process_max = handle.lastentry;

   let resolveFunc, rejectFunc; // Promise methods

   if (args.elist) {
      args.firstentry = args.elist.at(0);
      args.numentries = args.elist.at(-1) - args.elist.at(0) + 1;
      handle.process_entries = args.elist;
      handle.process_entries_indx = 0;
      handle.process_arrays = false; // do not use arrays process for selected entries
   }

   if (Number.isInteger(args.firstentry) && (args.firstentry > handle.firstentry) && (args.firstentry < handle.lastentry))
      handle.process_min = args.firstentry;

   handle.current_entry = handle.staged_now = handle.process_min;

   if (Number.isInteger(args.numentries) && (args.numentries > 0))
      handle.process_max = Math.min(handle.process_max, handle.process_min + args.numentries);

   if (isFunc(selector.ProcessArrays) && handle.simple_read) {
      // this is indication that selector can process arrays of values
      // only strictly-matched tree structure can be used for that

      for (let k = 0; k < handle.arr.length; ++k) {
         const elem = handle.arr[k];
         if ((elem.type <= 0) || (elem.type >= kOffsetL) || (elem.type === kCharStar) || (elem.type === kFloat16) || (elem.type === kDouble32))
            handle.process_arrays = false;
      }

      if (handle.process_arrays) {
         // create other members for fast processing

         selector.tgtarr = {}; // object with arrays

         for (let nn = 0; nn < handle.arr.length; ++nn) {
            const item = handle.arr[nn],
                  elem = createStreamerElement(item.name, item.type);

            elem.fType = item.type + kOffsetL;
            elem.fArrayLength = 10;
            elem.fArrayDim = 1;
            elem.fMaxIndex[0] = 10; // 10 if artificial number, will be replaced during reading

            item.arrmember = createMemberStreamer(elem, handle.file, true);
         }
      }
   } else
      handle.process_arrays = false;

   /** read basket with tree data, selecting different files */
   function readBaskets(bitems) {
      function extractPlaces() {
         // extract places to read and define file name

         const places = [];
         let filename = '';

         for (let n = 0; n < bitems.length; ++n) {
            if (bitems[n].done)
               continue;

            const branch = bitems[n].branch;

            if (!places.length)
               filename = branch.fFileName;
            else if (filename !== branch.fFileName)
               continue;

            bitems[n].selected = true; // mark which item was selected for reading

            places.push(branch.fBasketSeek[bitems[n].basket], branch.fBasketBytes[bitems[n].basket]);
         }

         return places.length ? { places, filename } : null;
      }

      function readProgress(value) {
         if ((handle.staged_prev === handle.staged_now) || (handle.process_max <= handle.process_min))
            return;

         const tm = new Date().getTime();
         if (tm - handle.progress_showtm < 500)
            return; // no need to show very often
         handle.progress_showtm = tm;

         const portion = (handle.staged_prev + value * (handle.staged_now - handle.staged_prev)) /
                         (handle.process_max - handle.process_min);
         return handle.selector.ShowProgress(portion);
      }

      function processBlobs(blobs, places) {
         if (!blobs || ((places.length > 2) && (blobs.length * 2 !== places.length)))
            return Promise.resolve(null);

         if (places.length === 2)
            blobs = [blobs];

         function doProcessing(k) {
            for (; k < bitems.length; ++k) {
               if (!bitems[k].selected)
                  continue;

               bitems[k].selected = false;
               bitems[k].done = true;

               const blob = blobs.shift();
               let buf = new TBuffer(blob, 0, handle.file);
               const basket = buf.classStreamer({}, clTBasket);

               if (basket.fNbytes !== bitems[k].branch.fBasketBytes[bitems[k].basket])
                  console.error(`mismatch in read basket sizes ${basket.fNbytes} != ${bitems[k].branch.fBasketBytes[bitems[k].basket]}`);

               // items[k].obj = basket; // keep basket object itself if necessary

               bitems[k].bskt_obj = basket; // only number of entries in the basket are relevant for the moment

               if (basket.fKeylen + basket.fObjlen === basket.fNbytes) {
                  // use data from original blob
                  buf.raw_shift = 0;

                  bitems[k].raw = buf; // here already unpacked buffer

                  if (bitems[k].branch.fEntryOffsetLen > 0)
                     buf.readBasketEntryOffset(basket, buf.raw_shift);

                  continue;
               }

               // unpack data and create new blob
               return R__unzip(blob, basket.fObjlen, false, buf.o).then(objblob => {
                  if (objblob) {
                     buf = new TBuffer(objblob, 0, handle.file);
                     buf.raw_shift = basket.fKeylen;
                     buf.fTagOffset = basket.fKeylen;
                  } else
                     throw new Error('FAIL TO UNPACK');

                  bitems[k].raw = buf; // here already unpacked buffer

                  if (bitems[k].branch.fEntryOffsetLen > 0)
                     buf.readBasketEntryOffset(basket, buf.raw_shift);

                  return doProcessing(k + 1);  // continue processing
               });
            }

            const req = extractPlaces();
            if (req)
               return handle.file.readBuffer(req.places, req.filename, readProgress).then(blobs2 => processBlobs(blobs2)).catch(() => null);

            return Promise.resolve(bitems);
         }

         return doProcessing(0);
      }

      const req = extractPlaces();

      // extract places where to read
      if (req)
         return handle.file.readBuffer(req.places, req.filename, readProgress).then(blobs => processBlobs(blobs, req.places)).catch(() => { return null; });

      return Promise.resolve(null);
   }

   let processBaskets = null;

   function readNextBaskets() {
      const bitems = [], max_ranges = tree.$file?.fMaxRanges || settings.MaxRanges,
            select_entries = handle.process_entries !== undefined;
      let total_size = 0, total_nsegm = 0, isany = true, is_direct = false, min_staged = handle.process_max;

      while (isany && (total_size < settings.TreeReadBunchSize) && (!total_nsegm || (total_nsegm + handle.arr.length <= max_ranges))) {
         isany = false;
         // very important, loop over branches in reverse order
         // let check counter branch after reading of normal branch is prepared
         for (let n = handle.arr.length - 1; n >= 0; --n) {
            const elem = handle.arr[n];

            while (elem.staged_basket < elem.numbaskets) {
               const k = elem.staged_basket++,
                     bskt_emin = elem.getBasketEntry(k),
                     bskt_emax = k < elem.numbaskets - 1 ? elem.getBasketEntry(k + 1) : bskt_emin + 1e6;

               // first baskets can be ignored
               if (bskt_emax <= handle.process_min)
                  continue;

               // no need to read more baskets, process_max is not included
               if (bskt_emin >= handle.process_max)
                  break;

               if (elem.first_readentry < 0) {
                  // basket where reading will start
                  elem.curr_basket = k;

                  elem.first_readentry = elem.getBasketEntry(k); // remember which entry will be read first
               } else if (select_entries) {
                  // all entries from process entries are analyzed
                  if (elem.eindx >= handle.process_entries.length)
                     break;

                  // check if this basket required
                  if ((handle.process_entries[elem.eindx] < bskt_emin) || (handle.process_entries[elem.eindx] >= bskt_emax))
                     continue;

                  // when all previous baskets were processed, continue with selected
                  if (elem.curr_basket < 0)
                     elem.curr_basket = k;
               }

               if (select_entries) {
                  // also check next entries which may belong to this basket
                  do
                     elem.eindx++;
                  while ((elem.eindx < handle.process_entries.length) && (handle.process_entries[elem.eindx] >= bskt_emin) && (handle.process_entries[elem.eindx] < bskt_emax));

                  // remember which baskets are required
                  elem.selected_baskets.push(k);
               }

               // check if basket already loaded in the branch

               const bitem = {
                  id: n, // to find which element we are reading
                  branch: elem.branch,
                  basket: k,
                  raw: null // here should be result
               }, bskt = elem.branch.fBaskets.arr[k];
               if (bskt) {
                  bitem.raw = bskt.fBufferRef;
                  if (bitem.raw)
                     bitem.raw.locate(0); // reset pointer - same branch may be read several times
                  else
                     bitem.raw = new TBuffer(null, 0, handle.file); // create dummy buffer - basket has no data
                  bitem.raw.raw_shift = bskt.fKeylen;

                  if (bskt.fBufferRef && (elem.branch.fEntryOffsetLen > 0))
                     bitem.raw.readBasketEntryOffset(bskt, bitem.raw.raw_shift);

                  bitem.bskt_obj = bskt;
                  is_direct = true;
                  elem.baskets[k] = bitem;
               } else {
                  bitems.push(bitem);
                  total_size += elem.branch.fBasketBytes[k];
                  total_nsegm++;
                  isany = true;
               }

               elem.staged_entry = elem.getBasketEntry(k + 1);

               min_staged = Math.min(min_staged, elem.staged_entry);

               break;
            }
         }
      }

      if ((total_size === 0) && !is_direct) {
         handle.selector.Terminate(true);
         return resolveFunc(handle.selector);
      }

      handle.staged_prev = handle.staged_now;
      handle.staged_now = min_staged;

      let portion = 0;
      if (handle.process_max > handle.process_min)
         portion = (handle.staged_prev - handle.process_min) / (handle.process_max - handle.process_min);

      if (handle.selector.ShowProgress(portion) === 'break') {
         handle.selector.Terminate(true);
         return resolveFunc(handle.selector);
      }

      handle.progress_showtm = new Date().getTime();

      if (total_size > 0)
         return readBaskets(bitems).then(processBaskets);

      if (is_direct)
         return processBaskets([]); // directly process baskets

      throw new Error('No any data is requested - never come here');
   }

   processBaskets = function(bitems) {
      // this is call-back when next baskets are read

      if (handle.selector._break || (bitems === null)) {
         handle.selector.Terminate(false);
         return resolveFunc(handle.selector);
      }

      // redistribute read baskets over branches
      for (let n = 0; n < bitems.length; ++n)
         handle.arr[bitems[n].id].baskets[bitems[n].basket] = bitems[n];

      // now process baskets

      let isanyprocessed = false;

      while (true) {
         let loopentries = 100000000, n, elem;

         // first loop used to check if all required data exists
         for (n = 0; n < handle.arr.length; ++n) {
            elem = handle.arr[n];

            if (!elem.raw || !elem.basket || (elem.first_entry + elem.basket.fNevBuf <= handle.current_entry)) {
               delete elem.raw;
               delete elem.basket;

               if ((elem.curr_basket >= elem.numbaskets)) {
                  if (n === 0) {
                     handle.selector.Terminate(true);
                     return resolveFunc(handle.selector);
                  }
                  continue; // ignore non-master branch
               }

               // this is single response from the tree, includes branch, basket number, raw data
               const bitem = elem.baskets[elem.curr_basket];

               // basket not read
               if (!bitem) {
                  // no data, but no any event processed - problem
                  if (!isanyprocessed) {
                     handle.selector.Terminate(false);
                     return rejectFunc(Error(`no data for ${elem.branch.fName} basket ${elem.curr_basket}`));
                  }

                  // try to read next portion of tree data
                  return readNextBaskets();
               }

               elem.raw = bitem.raw;
               elem.basket = bitem.bskt_obj;
               // elem.nev = bitem.fNevBuf; // number of entries in raw buffer
               elem.first_entry = elem.getBasketEntry(bitem.basket);

               bitem.raw = null; // remove reference on raw buffer
               bitem.branch = null; // remove reference on the branch
               bitem.bskt_obj = null; // remove reference on the branch
               elem.baskets[elem.curr_basket++] = undefined; // remove from array

               if (handle.process_entries !== undefined) {
                  elem.selected_baskets.shift();
                  // -1 means that basket is not yet found for following entries
                  elem.curr_basket = elem.selected_baskets.length ? elem.selected_baskets[0] : -1;
               }
            }

            // define how much entries can be processed before next raw buffer will be finished
            loopentries = Math.min(loopentries, elem.first_entry + elem.basket.fNevBuf - handle.current_entry);
         }

         // second loop extracts all required data

         // do not read too much
         if (handle.current_entry + loopentries > handle.process_max)
            loopentries = handle.process_max - handle.current_entry;

         if (handle.process_arrays && (loopentries > 1)) {
            // special case - read all data from baskets as arrays

            for (n = 0; n < handle.arr.length; ++n) {
               elem = handle.arr[n];

               elem.getEntry(handle.current_entry);

               elem.arrmember.arrlength = loopentries;
               elem.arrmember.func(elem.raw, handle.selector.tgtarr);

               elem.raw = null;
            }

            handle.selector.ProcessArrays(handle.current_entry);

            handle.current_entry += loopentries;

            isanyprocessed = true;
         } else {
            // main processing loop
            while (loopentries > 0) {
               for (n = 0; n < handle.arr.length; ++n) {
                  elem = handle.arr[n];

                  // locate buffer offset at proper place
                  elem.getEntry(handle.current_entry);

                  elem.member.func(elem.raw, elem.getTarget(handle.selector.tgtobj));
               }

               handle.selector.Process(handle.current_entry);

               isanyprocessed = true;

               if (handle.process_entries) {
                  handle.process_entries_indx++;
                  if (handle.process_entries_indx >= handle.process_entries.length) {
                     handle.current_entry++;
                     loopentries = 0;
                  } else {
                     const next_entry = handle.process_entries[handle.process_entries_indx],
                           diff = next_entry - handle.current_entry;
                     handle.current_entry = next_entry;
                     loopentries -= diff;
                  }
               } else {
                  handle.current_entry++;
                  loopentries--;
               }
            }
         }

         if (handle.current_entry >= handle.process_max) {
            handle.selector.Terminate(true);
            return resolveFunc(handle.selector);
         }
      }
   };

   return new Promise((resolve, reject) => {
      resolveFunc = resolve;
      rejectFunc = reject;

      // call begin before first entry is read
      handle.selector.Begin(tree);

      readNextBaskets();
   });
}

/** @summary implementation of TTree::Draw
  * @param {object|string} args - different setting or simply draw expression
  * @param {string} args.expr - draw expression
  * @param {string} [args.cut=undefined] - cut expression (also can be part of 'expr' after '::')
  * @param {string} [args.drawopt=undefined] - draw options for result histogram
  * @param {number} [args.firstentry=0] - first entry to process
  * @param {number} [args.numentries=undefined] - number of entries to process, all by default
  * @param {Array} [args.elist=undefined] - array of entries id to process, all by default
  * @param {Boolean} [args.dump] - dump values of expression instead creating histogram
  * @param {Boolean} [args.dump_entries] - if array of entries should be return which match cut condition
  * @param {Boolean} [args.staged] - staged processing, first apply cut to select entries and then perform drawing for selected entries
  * @param {object} [args.branch=undefined] - TBranch object from TTree itself for the direct drawing
  * @param {function} [args.progress=undefined] - function called during histogram accumulation with obj argument
  * @return {Promise} with produced object */
async function treeDraw(tree, args) {
   if (isStr(args))
      args = { expr: args };

   if (!isStr(args.expr))
      args.expr = '';

   if (!args.SelectorClass)
      args.SelectorClass = TDrawSelector;
   if (!args.processFunction)
      args.processFunction = treeProcess;

   const selector = new args.SelectorClass();

   if (args.branch) {
      if (!selector.drawOnlyBranch(tree, args.branch, args.expr, args))
         return Promise.reject(Error(`Fail to create draw expression ${args.expr} for branch ${args.branch.fName}`));
   } else if (!selector.parseDrawExpression(tree, args))
      return Promise.reject(Error(`Fail to create draw expression ${args.expr}`));

   selector.setCallback(null, args.progress);

   return args.processFunction(tree, selector, args).then(sel => {
      if (!args.staged)
         return sel;

      delete args.dump_entries;

      const selector2 = new args.SelectorClass(),
            args2 = Object.assign({}, args);
      args2.staged = false;
      args2.elist = sel.hist; // assign entries found in first selection
      if (!selector2.createDrawExpression(tree, args.staged_names, '', args2))
         return Promise.reject(Error(`Fail to create final draw expression ${args.expr}`));
      ['arr_limit', 'htype', 'nmatch', 'want_hist', 'hist_nbins', 'hist_name', 'hist_args', 'draw_title']
        .forEach(name => { selector2[name] = selector[name]; });
      return args.processFunction(tree, selector2, args2);
   }).then(sel => sel.hist);
}

/** @summary Performs generic I/O test for all branches in the TTree
  * @desc Used when 'testio' draw option for TTree is specified
  * @private */
function treeIOTest(tree, args) {
   const branches = [], names = [], nchilds = [];

   function collectBranches(obj, prntname = '') {
      if (!obj?.fBranches)
         return 0;

      let cnt = 0;

      for (let n = 0; n < obj.fBranches.arr.length; ++n) {
         const br = obj.fBranches.arr[n],
               name = (prntname ? prntname + '/' : '') + br.fName;
         branches.push(br);
         names.push(name);
         nchilds.push(0);
         const pos = nchilds.length - 1;
         cnt += (br.fLeaves?.arr?.length ?? 0);
         const nchld = collectBranches(br, name);

         cnt += nchld;
         nchilds[pos] = nchld;
      }
      return cnt;
   }

   const numleaves = collectBranches(tree);
   let selector;

   names.push(`Total are ${branches.length} branches with ${numleaves} leaves`);

   function testBranch(nbr) {
      if (nbr >= branches.length)
         return Promise.resolve(true);

      if (selector?._break || args._break)
         return Promise.resolve(true);

      selector = new TSelector();

      selector.addBranch(branches[nbr], 'br0');

      selector.Process = function() {
         if (this.tgtobj.br0 === undefined)
            this.fail = true;
      };

      selector.Terminate = function(res) {
         if (!isStr(res))
            res = (!res || this.fails) ? 'FAIL' : 'ok';

         names[nbr] = res + ' ' + names[nbr];
      };

      const br = branches[nbr],
            object_class = getBranchObjectClass(br, tree),
            num = br.fEntries,
            skip_branch = object_class ? (nchilds[nbr] > 100) : !br.fLeaves?.arr?.length;

      if (skip_branch || (num <= 0))
         return testBranch(nbr + 1);

      const drawargs = { numentries: 10 },
            first = br.fFirstEntry || 0,
            last = br.fEntryNumber || (first + num);

      if (num < drawargs.numentries)
         drawargs.numentries = num;
      else
         drawargs.firstentry = first + Math.round((last - first - drawargs.numentries) * Math.random()); // select randomly

      // keep console output for debug purposes
      console.log(`test branch ${br.fName} first ${drawargs.firstentry || 0} num ${drawargs.numentries}`);

      if (isFunc(args.showProgress))
         args.showProgress(`br ${nbr}/${branches.length} ${br.fName}`);

      return treeProcess(tree, selector, drawargs).then(() => testBranch(nbr + 1));
   }

   return testBranch(0).then(() => {
      if (isFunc(args.showProgress))
         args.showProgress();

      return names;
   });
}

/** @summary Create hierarchy of TTree object
  * @private */
function treeHierarchy(tree_node, obj) {
   function createBranchItem(node, branch, tree, parent_branch) {
      if (!node || !branch)
         return false;

      const nb_branches = branch.fBranches?.arr?.length ?? 0,
            nb_leaves = branch.fLeaves?.arr?.length ?? 0;

      function ClearName(arg) {
         const pos = arg.indexOf('[');
         if (pos > 0)
            arg = arg.slice(0, pos);
         if (parent_branch && arg.indexOf(parent_branch.fName) === 0) {
            arg = arg.slice(parent_branch.fName.length);
            if (arg[0] === '.')
               arg = arg.slice(1);
         }
         return arg;
      }

      branch.$tree = tree; // keep tree pointer, later do it more smart

      const subitem = {
         _name: ClearName(branch.fName),
         _kind: getKindForType(branch._typename),
         _title: branch.fTitle,
         _obj: branch
      };

      if (!node._childs)
         node._childs = [];

      node._childs.push(subitem);

      if (branch._typename === clTBranchElement)
         subitem._title += ` from ${branch.fClassName};${branch.fClassVersion}`;

      if (nb_branches > 0) {
         subitem._more = true;
         subitem._expand = function(bnode, bobj) {
            // really create all sub-branch items
            if (!bobj)
               return false;

            if (!bnode._childs)
               bnode._childs = [];

            if ((bobj.fLeaves?.arr?.length === 1) &&
                ((bobj.fType === kClonesNode) || (bobj.fType === kSTLNode))) {
               bobj.fLeaves.arr[0].$branch = bobj;
               bnode._childs.push({
                  _name: '@size',
                  _title: 'container size',
                  _kind: getKindForType('TLeafElement'),
                  _icon: 'img_leaf',
                  _obj: bobj.fLeaves.arr[0],
                  _more: false
               });
            }

            for (let i = 0; i < bobj.fBranches.arr.length; ++i)
               createBranchItem(bnode, bobj.fBranches.arr[i], bobj.$tree, bobj);

            const object_class = getBranchObjectClass(bobj, bobj.$tree, true),
                  methods = object_class ? getMethods(object_class) : null;

            if (methods && bobj.fBranches.arr.length) {
               for (const key in methods) {
                  if (!isFunc(methods[key]))
                     continue;
                  const s = methods[key].toString();
                  if ((s.indexOf('return') > 0) && (s.indexOf('function ()') === 0)) {
                     bnode._childs.push({
                        _name: key + '()',
                        _title: `function ${key} of class ${object_class}`,
                        _kind: getKindForType(clTBranchFunc), // fictional class, only for drawing
                        _obj: { _typename: clTBranchFunc, branch: bobj, func: key },
                        _more: false
                     });
                  }
               }
            }

            return true;
         };
         return true;
      } else if (nb_leaves === 1) {
         subitem._icon = 'img_leaf';
         subitem._more = false;
      } else if (nb_leaves > 1) {
         subitem._childs = [];
         for (let j = 0; j < nb_leaves; ++j) {
            branch.fLeaves.arr[j].$branch = branch; // keep branch pointer for drawing
            const leafitem = {
               _name: ClearName(branch.fLeaves.arr[j].fName),
               _kind: getKindForType(branch.fLeaves.arr[j]._typename),
               _obj: branch.fLeaves.arr[j]
            };
            subitem._childs.push(leafitem);
         }
      }

      return true;
   }

   // protect against corrupted TTree objects
   if (obj.fBranches === undefined)
      return false;

   tree_node._childs = [];
   tree_node._tree = obj;  // set reference, will be used later by TTree::Draw

   obj.fBranches?.arr.forEach(branch => createBranchItem(tree_node, branch, obj));

   return true;
}

var tree = /*#__PURE__*/Object.freeze({
__proto__: null,
TDrawSelector: TDrawSelector,
TDrawVariable: TDrawVariable,
TSelector: TSelector,
clTBranchFunc: clTBranchFunc,
kClonesNode: kClonesNode,
kSTLNode: kSTLNode,
treeDraw: treeDraw,
treeHierarchy: treeHierarchy,
treeIOTest: treeIOTest,
treeProcess: treeProcess
});

/** @license
 *
 * jsPDF - PDF Document creation from JavaScript
 * Version 3.0.3
 *
 * Copyright (c) 2010-2025 James Hall <james@parall.ax>, https://github.com/MrRio/jsPDF
 *               2015-2025 yWorks GmbH, http://www.yworks.com
 *               2015-2025 Lukas Holländer <lukas.hollaender@yworks.com>, https://github.com/HackbrettXXX
 *               2016-2018 Aras Abbasi <aras.abbasi@gmail.com>
 *               2010 Aaron Spike, https://github.com/acspike
 *               2012 Willow Systems Corporation, https://github.com/willowsystems
 *               2012 Pablo Hess, https://github.com/pablohess
 *               2012 Florian Jenett, https://github.com/fjenett
 *               2013 Warren Weckesser, https://github.com/warrenweckesser
 *               2013 Youssef Beddad, https://github.com/lifof
 *               2013 Lee Driscoll, https://github.com/lsdriscoll
 *               2013 Stefan Slonevskiy, https://github.com/stefslon
 *               2013 Jeremy Morel, https://github.com/jmorel
 *               2013 Christoph Hartmann, https://github.com/chris-rock
 *               2014 Juan Pablo Gaviria, https://github.com/juanpgaviria
 *               2014 James Makes, https://github.com/dollaruw
 *               2014 Diego Casorran, https://github.com/diegocr
 *               2014 Steven Spungin, https://github.com/Flamenco
 *               2014 Kenneth Glassey, https://github.com/Gavvers
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * Contributor(s):
 *    siefkenj, ahwolf, rickygu, Midnith, saintclair, eaparango,
 *    kim3er, mfo, alnorth, Flamenco
 */

const globalObject = globalThis;

/**
 * A class to parse color values
 * @author Stoyan Stefanov <sstoo@gmail.com>
 * {@link   http://www.phpied.com/rgb-color-parser-in-javascript/}
 * @license Use it if you like it
 */

function RGBColor$1(color_string) {
  color_string = color_string || "";
  this.ok = false;

  // strip any leading #
  if (color_string.charAt(0) == "#") {
    // remove # if any
    color_string = color_string.substr(1, 6);
  }

  color_string = color_string.replace(/ /g, "");
  color_string = color_string.toLowerCase();

  var channels;

  // before getting into regexps, try simple matches
  // and overwrite the input
  var simple_colors = {
    aliceblue: "f0f8ff",
    antiquewhite: "faebd7",
    aqua: "00ffff",
    aquamarine: "7fffd4",
    azure: "f0ffff",
    beige: "f5f5dc",
    bisque: "ffe4c4",
    black: "000000",
    blanchedalmond: "ffebcd",
    blue: "0000ff",
    blueviolet: "8a2be2",
    brown: "a52a2a",
    burlywood: "deb887",
    cadetblue: "5f9ea0",
    chartreuse: "7fff00",
    chocolate: "d2691e",
    coral: "ff7f50",
    cornflowerblue: "6495ed",
    cornsilk: "fff8dc",
    crimson: "dc143c",
    cyan: "00ffff",
    darkblue: "00008b",
    darkcyan: "008b8b",
    darkgoldenrod: "b8860b",
    darkgray: "a9a9a9",
    darkgreen: "006400",
    darkkhaki: "bdb76b",
    darkmagenta: "8b008b",
    darkolivegreen: "556b2f",
    darkorange: "ff8c00",
    darkorchid: "9932cc",
    darkred: "8b0000",
    darksalmon: "e9967a",
    darkseagreen: "8fbc8f",
    darkslateblue: "483d8b",
    darkslategray: "2f4f4f",
    darkturquoise: "00ced1",
    darkviolet: "9400d3",
    deeppink: "ff1493",
    deepskyblue: "00bfff",
    dimgray: "696969",
    dodgerblue: "1e90ff",
    feldspar: "d19275",
    firebrick: "b22222",
    floralwhite: "fffaf0",
    forestgreen: "228b22",
    fuchsia: "ff00ff",
    gainsboro: "dcdcdc",
    ghostwhite: "f8f8ff",
    gold: "ffd700",
    goldenrod: "daa520",
    gray: "808080",
    green: "008000",
    greenyellow: "adff2f",
    honeydew: "f0fff0",
    hotpink: "ff69b4",
    indianred: "cd5c5c",
    indigo: "4b0082",
    ivory: "fffff0",
    khaki: "f0e68c",
    lavender: "e6e6fa",
    lavenderblush: "fff0f5",
    lawngreen: "7cfc00",
    lemonchiffon: "fffacd",
    lightblue: "add8e6",
    lightcoral: "f08080",
    lightcyan: "e0ffff",
    lightgoldenrodyellow: "fafad2",
    lightgrey: "d3d3d3",
    lightgreen: "90ee90",
    lightpink: "ffb6c1",
    lightsalmon: "ffa07a",
    lightseagreen: "20b2aa",
    lightskyblue: "87cefa",
    lightslateblue: "8470ff",
    lightslategray: "778899",
    lightsteelblue: "b0c4de",
    lightyellow: "ffffe0",
    lime: "00ff00",
    limegreen: "32cd32",
    linen: "faf0e6",
    magenta: "ff00ff",
    maroon: "800000",
    mediumaquamarine: "66cdaa",
    mediumblue: "0000cd",
    mediumorchid: "ba55d3",
    mediumpurple: "9370d8",
    mediumseagreen: "3cb371",
    mediumslateblue: "7b68ee",
    mediumspringgreen: "00fa9a",
    mediumturquoise: "48d1cc",
    mediumvioletred: "c71585",
    midnightblue: "191970",
    mintcream: "f5fffa",
    mistyrose: "ffe4e1",
    moccasin: "ffe4b5",
    navajowhite: "ffdead",
    navy: "000080",
    oldlace: "fdf5e6",
    olive: "808000",
    olivedrab: "6b8e23",
    orange: "ffa500",
    orangered: "ff4500",
    orchid: "da70d6",
    palegoldenrod: "eee8aa",
    palegreen: "98fb98",
    paleturquoise: "afeeee",
    palevioletred: "d87093",
    papayawhip: "ffefd5",
    peachpuff: "ffdab9",
    peru: "cd853f",
    pink: "ffc0cb",
    plum: "dda0dd",
    powderblue: "b0e0e6",
    purple: "800080",
    red: "ff0000",
    rosybrown: "bc8f8f",
    royalblue: "4169e1",
    saddlebrown: "8b4513",
    salmon: "fa8072",
    sandybrown: "f4a460",
    seagreen: "2e8b57",
    seashell: "fff5ee",
    sienna: "a0522d",
    silver: "c0c0c0",
    skyblue: "87ceeb",
    slateblue: "6a5acd",
    slategray: "708090",
    snow: "fffafa",
    springgreen: "00ff7f",
    steelblue: "4682b4",
    tan: "d2b48c",
    teal: "008080",
    thistle: "d8bfd8",
    tomato: "ff6347",
    turquoise: "40e0d0",
    violet: "ee82ee",
    violetred: "d02090",
    wheat: "f5deb3",
    white: "ffffff",
    whitesmoke: "f5f5f5",
    yellow: "ffff00",
    yellowgreen: "9acd32"
  };
  color_string = simple_colors[color_string] || color_string;

  // array of color definition objects
  var color_defs = [
    {
      re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
      example: ["rgb(123, 234, 45)", "rgb(255,234,245)"],
      process: function(bits) {
        return [parseInt(bits[1]), parseInt(bits[2]), parseInt(bits[3])];
      }
    },
    {
      re: /^(\w{2})(\w{2})(\w{2})$/,
      example: ["#00ff00", "336699"],
      process: function(bits) {
        return [
          parseInt(bits[1], 16),
          parseInt(bits[2], 16),
          parseInt(bits[3], 16)
        ];
      }
    },
    {
      re: /^(\w{1})(\w{1})(\w{1})$/,
      example: ["#fb0", "f0f"],
      process: function(bits) {
        return [
          parseInt(bits[1] + bits[1], 16),
          parseInt(bits[2] + bits[2], 16),
          parseInt(bits[3] + bits[3], 16)
        ];
      }
    }
  ];

  // search through the definitions to find a match
  for (var i = 0; i < color_defs.length; i++) {
    var re = color_defs[i].re;
    var processor = color_defs[i].process;
    var bits = re.exec(color_string);
    if (bits) {
      channels = processor(bits);
      this.r = channels[0];
      this.g = channels[1];
      this.b = channels[2];
      this.ok = true;
    }
  }

  // validate/cleanup values
  this.r = this.r < 0 || isNaN(this.r) ? 0 : this.r > 255 ? 255 : this.r;
  this.g = this.g < 0 || isNaN(this.g) ? 0 : this.g > 255 ? 255 : this.g;
  this.b = this.b < 0 || isNaN(this.b) ? 0 : this.b > 255 ? 255 : this.b;

  // some getters
  this.toRGB = function() {
    return "rgb(" + this.r + ", " + this.g + ", " + this.b + ")";
  };
  this.toHex = function() {
    var r = this.r.toString(16);
    var g = this.g.toString(16);
    var b = this.b.toString(16);
    if (r.length == 1) r = "0" + r;
    if (g.length == 1) g = "0" + g;
    if (b.length == 1) b = "0" + b;
    return "#" + r + g + b;
  };
}

let atob$1, btoa$1;

if ((typeof process === 'object') && (typeof process.versions === 'object') && process.versions.node && process.versions.v8) {
   atob$1 = str => Buffer.from(str, 'base64').toString('latin1');
   btoa$1 = str => Buffer.from(str, 'latin1').toString('base64');
} else {
   atob$1 = globalThis.atob;
   btoa$1 = globalThis.btoa;
}

function consoleLog() {
  if (globalObject.console && typeof globalObject.console.log === "function") {
    globalObject.console.log.apply(globalObject.console, arguments);
  }
}

function consoleWarn(str) {
  if (globalObject.console) {
    if (typeof globalObject.console.warn === "function") {
      globalObject.console.warn.apply(globalObject.console, arguments);
    } else {
      consoleLog.call(null, arguments);
    }
  }
}

function consoleError(str) {
  if (globalObject.console) {
    if (typeof globalObject.console.error === "function") {
      globalObject.console.error.apply(globalObject.console, arguments);
    } else {
      consoleLog(str);
    }
  }
}
var console$1 = {
  log: consoleLog,
  warn: consoleWarn,
  error: consoleError
};

/**
 * @license
 * Joseph Myers does not specify a particular license for his work.
 *
 * Author: Joseph Myers
 * Accessed from: http://www.myersdaily.org/joseph/javascript/md5.js
 *
 * Modified by: Owen Leong
 */

function md5cycle(x, k) {
  var a = x[0],
    b = x[1],
    c = x[2],
    d = x[3];

  a = ff(a, b, c, d, k[0], 7, -680876936);
  d = ff(d, a, b, c, k[1], 12, -389564586);
  c = ff(c, d, a, b, k[2], 17, 606105819);
  b = ff(b, c, d, a, k[3], 22, -1044525330);
  a = ff(a, b, c, d, k[4], 7, -176418897);
  d = ff(d, a, b, c, k[5], 12, 1200080426);
  c = ff(c, d, a, b, k[6], 17, -1473231341);
  b = ff(b, c, d, a, k[7], 22, -45705983);
  a = ff(a, b, c, d, k[8], 7, 1770035416);
  d = ff(d, a, b, c, k[9], 12, -1958414417);
  c = ff(c, d, a, b, k[10], 17, -42063);
  b = ff(b, c, d, a, k[11], 22, -1990404162);
  a = ff(a, b, c, d, k[12], 7, 1804603682);
  d = ff(d, a, b, c, k[13], 12, -40341101);
  c = ff(c, d, a, b, k[14], 17, -1502002290);
  b = ff(b, c, d, a, k[15], 22, 1236535329);

  a = gg(a, b, c, d, k[1], 5, -165796510);
  d = gg(d, a, b, c, k[6], 9, -1069501632);
  c = gg(c, d, a, b, k[11], 14, 643717713);
  b = gg(b, c, d, a, k[0], 20, -373897302);
  a = gg(a, b, c, d, k[5], 5, -701558691);
  d = gg(d, a, b, c, k[10], 9, 38016083);
  c = gg(c, d, a, b, k[15], 14, -660478335);
  b = gg(b, c, d, a, k[4], 20, -405537848);
  a = gg(a, b, c, d, k[9], 5, 568446438);
  d = gg(d, a, b, c, k[14], 9, -1019803690);
  c = gg(c, d, a, b, k[3], 14, -187363961);
  b = gg(b, c, d, a, k[8], 20, 1163531501);
  a = gg(a, b, c, d, k[13], 5, -1444681467);
  d = gg(d, a, b, c, k[2], 9, -51403784);
  c = gg(c, d, a, b, k[7], 14, 1735328473);
  b = gg(b, c, d, a, k[12], 20, -1926607734);

  a = hh(a, b, c, d, k[5], 4, -378558);
  d = hh(d, a, b, c, k[8], 11, -2022574463);
  c = hh(c, d, a, b, k[11], 16, 1839030562);
  b = hh(b, c, d, a, k[14], 23, -35309556);
  a = hh(a, b, c, d, k[1], 4, -1530992060);
  d = hh(d, a, b, c, k[4], 11, 1272893353);
  c = hh(c, d, a, b, k[7], 16, -155497632);
  b = hh(b, c, d, a, k[10], 23, -1094730640);
  a = hh(a, b, c, d, k[13], 4, 681279174);
  d = hh(d, a, b, c, k[0], 11, -358537222);
  c = hh(c, d, a, b, k[3], 16, -722521979);
  b = hh(b, c, d, a, k[6], 23, 76029189);
  a = hh(a, b, c, d, k[9], 4, -640364487);
  d = hh(d, a, b, c, k[12], 11, -421815835);
  c = hh(c, d, a, b, k[15], 16, 530742520);
  b = hh(b, c, d, a, k[2], 23, -995338651);

  a = ii(a, b, c, d, k[0], 6, -198630844);
  d = ii(d, a, b, c, k[7], 10, 1126891415);
  c = ii(c, d, a, b, k[14], 15, -1416354905);
  b = ii(b, c, d, a, k[5], 21, -57434055);
  a = ii(a, b, c, d, k[12], 6, 1700485571);
  d = ii(d, a, b, c, k[3], 10, -1894986606);
  c = ii(c, d, a, b, k[10], 15, -1051523);
  b = ii(b, c, d, a, k[1], 21, -2054922799);
  a = ii(a, b, c, d, k[8], 6, 1873313359);
  d = ii(d, a, b, c, k[15], 10, -30611744);
  c = ii(c, d, a, b, k[6], 15, -1560198380);
  b = ii(b, c, d, a, k[13], 21, 1309151649);
  a = ii(a, b, c, d, k[4], 6, -145523070);
  d = ii(d, a, b, c, k[11], 10, -1120210379);
  c = ii(c, d, a, b, k[2], 15, 718787259);
  b = ii(b, c, d, a, k[9], 21, -343485551);

  x[0] = add32(a, x[0]);
  x[1] = add32(b, x[1]);
  x[2] = add32(c, x[2]);
  x[3] = add32(d, x[3]);
}

function cmn(q, a, b, x, s, t) {
  a = add32(add32(a, q), add32(x, t));
  return add32((a << s) | (a >>> (32 - s)), b);
}

function ff(a, b, c, d, x, s, t) {
  return cmn((b & c) | (~b & d), a, b, x, s, t);
}

function gg(a, b, c, d, x, s, t) {
  return cmn((b & d) | (c & ~d), a, b, x, s, t);
}

function hh(a, b, c, d, x, s, t) {
  return cmn(b ^ c ^ d, a, b, x, s, t);
}

function ii(a, b, c, d, x, s, t) {
  return cmn(c ^ (b | ~d), a, b, x, s, t);
}

function md51(s) {
  // txt = '';
  var n = s.length,
    state = [1732584193, -271733879, -1732584194, 271733878],
    i;
  for (i = 64; i <= s.length; i += 64) {
    md5cycle(state, md5blk(s.substring(i - 64, i)));
  }
  s = s.substring(i - 64);
  var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
  for (i = 0; i < s.length; i++)
    tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3);
  tail[i >> 2] |= 0x80 << (i % 4 << 3);
  if (i > 55) {
    md5cycle(state, tail);
    for (i = 0; i < 16; i++) tail[i] = 0;
  }
  tail[14] = n * 8;
  md5cycle(state, tail);
  return state;
}

/* there needs to be support for Unicode here,
 * unless we pretend that we can redefine the MD-5
 * algorithm for multi-byte characters (perhaps
 * by adding every four 16-bit characters and
 * shortening the sum to 32 bits). Otherwise
 * I suggest performing MD-5 as if every character
 * was two bytes--e.g., 0040 0025 = @%--but then
 * how will an ordinary MD-5 sum be matched?
 * There is no way to standardize text to something
 * like UTF-8 before transformation; speed cost is
 * utterly prohibitive. The JavaScript standard
 * itself needs to look at this: it should start
 * providing access to strings as preformed UTF-8
 * 8-bit unsigned value arrays.
 */
function md5blk(s) {
  /* I figured global was faster.   */
  var md5blks = [],
    i; /* Andy King said do it this way. */
  for (i = 0; i < 64; i += 4) {
    md5blks[i >> 2] =
      s.charCodeAt(i) +
      (s.charCodeAt(i + 1) << 8) +
      (s.charCodeAt(i + 2) << 16) +
      (s.charCodeAt(i + 3) << 24);
  }
  return md5blks;
}

var hex_chr = "0123456789abcdef".split("");

function rhex(n) {
  var s = "",
    j = 0;
  for (; j < 4; j++)
    s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f];
  return s;
}

function hex(x) {
  for (var i = 0; i < x.length; i++) x[i] = rhex(x[i]);
  return x.join("");
}

// Converts a 4-byte number to byte string
function singleToByteString(n) {
  return String.fromCharCode(
    (n & 0xff) >> 0,
    (n & 0xff00) >> 8,
    (n & 0xff0000) >> 16,
    (n & 0xff000000) >> 24
  );
}

// Converts an array of numbers to a byte string
function toByteString(x) {
  return x.map(singleToByteString).join("");
}

// Returns the MD5 hash as a byte string
function md5Bin(s) {
  return toByteString(md51(s));
}

// Returns MD5 hash as a hex string
function md5(s) {
  return hex(md51(s));
}

var md5Check = md5("hello") != "5d41402abc4b2a76b9719d911017c592";

function add32(a, b) {
  if (md5Check) {
    /* if the md5Check does not match
     the expected value, we're dealing
     with an old browser and need
     this function. */
    var lsw = (a & 0xffff) + (b & 0xffff),
      msw = (a >> 16) + (b >> 16) + (lsw >> 16);
    return (msw << 16) | (lsw & 0xffff);
  } else {
    /* this function is much faster,
    so if possible we use it. Some IEs
    are the only ones I know of that
    need the idiotic second function,
    generated by an if clause.  */
    return (a + b) & 0xffffffff;
  }
}

/**
 * @license
 * FPDF is released under a permissive license: there is no usage restriction.
 * You may embed it freely in your application (commercial or not), with or
 * without modifications.
 *
 * Reference: http://www.fpdf.org/en/script/script37.php
 */

function repeat(str, num) {
  return new Array(num + 1).join(str);
}

/**
 * Converts a byte string to a hex string
 *
 * @name rc4
 * @function
 * @param {string} key Byte string of encryption key
 * @param {string} data Byte string of data to be encrypted
 * @returns {string} Encrypted string
 */
function rc4(key, data) {
  var lastKey, lastState;
  if (key !== lastKey) {
    var k = repeat(key, ((256 / key.length) >> 0) + 1);
    var state = [];
    for (var i = 0; i < 256; i++) {
      state[i] = i;
    }
    var j = 0;
    for (var i = 0; i < 256; i++) {
      var t = state[i];
      j = (j + t + k.charCodeAt(i)) % 256;
      state[i] = state[j];
      state[j] = t;
    }
    lastKey = key;
    lastState = state;
  } else {
    state = lastState;
  }
  var length = data.length;
  var a = 0;
  var b = 0;
  var out = "";
  for (var i = 0; i < length; i++) {
    a = (a + 1) % 256;
    t = state[a];
    b = (b + t) % 256;
    state[a] = state[b];
    state[b] = t;
    k = state[(state[a] + state[b]) % 256];
    out += String.fromCharCode(data.charCodeAt(i) ^ k);
  }
  return out;
}

/**
 * @license
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 * Author: Owen Leong (@owenl131)
 * Date: 15 Oct 2020
 * References:
 * https://www.cs.cmu.edu/~dst/Adobe/Gallery/anon21jul01-pdf-encryption.txt
 * https://github.com/foliojs/pdfkit/blob/master/lib/security.js
 * http://www.fpdf.org/en/script/script37.php
 */

var permissionOptions = {
  print: 4,
  modify: 8,
  copy: 16,
  "annot-forms": 32
};

/**
 * Initializes encryption settings
 *
 * @name constructor
 * @function
 * @param {Array} permissions Permissions allowed for user, "print", "modify", "copy" and "annot-forms".
 * @param {String} userPassword Permissions apply to this user. Leaving this empty means the document
 *                              is not password protected but viewer has the above permissions.
 * @param {String} ownerPassword Owner has full functionalities to the file.
 * @param {String} fileId As hex string, should be same as the file ID in the trailer.
 * @example
 * var security = new PDFSecurity(["print"])
 */
function PDFSecurity(permissions, userPassword, ownerPassword, fileId) {
  this.v = 1; // algorithm 1, future work can add in more recent encryption schemes
  this.r = 2; // revision 2

  // set flags for what functionalities the user can access
  let protection = 192;
  permissions.forEach(function(perm) {
    if (typeof permissionOptions.perm !== "undefined") {
      throw new Error("Invalid permission: " + perm);
    }
    protection += permissionOptions[perm];
  });

  // padding is used to pad the passwords to 32 bytes, also is hashed and stored in the final PDF
  this.padding =
    "\x28\xBF\x4E\x5E\x4E\x75\x8A\x41\x64\x00\x4E\x56\xFF\xFA\x01\x08" +
    "\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A";
  let paddedUserPassword = (userPassword + this.padding).substr(0, 32);
  let paddedOwnerPassword = (ownerPassword + this.padding).substr(0, 32);

  this.O = this.processOwnerPassword(paddedUserPassword, paddedOwnerPassword);
  this.P = -((protection ^ 255) + 1);
  this.encryptionKey = md5Bin(
    paddedUserPassword +
      this.O +
      this.lsbFirstWord(this.P) +
      this.hexToBytes(fileId)
  ).substr(0, 5);
  this.U = rc4(this.encryptionKey, this.padding);
}

/**
 * Breaks down a 4-byte number into its individual bytes, with the least significant bit first
 *
 * @name lsbFirstWord
 * @function
 * @param {number} data 32-bit number
 * @returns {Array}
 */
PDFSecurity.prototype.lsbFirstWord = function(data) {
  return String.fromCharCode(
    (data >> 0) & 0xff,
    (data >> 8) & 0xff,
    (data >> 16) & 0xff,
    (data >> 24) & 0xff
  );
};

/**
 * Converts a byte string to a hex string
 *
 * @name toHexString
 * @function
 * @param {String} byteString Byte string
 * @returns {String}
 */
PDFSecurity.prototype.toHexString = function(byteString) {
  return byteString
    .split("")
    .map(function(byte) {
      return ("0" + (byte.charCodeAt(0) & 0xff).toString(16)).slice(-2);
    })
    .join("");
};

/**
 * Converts a hex string to a byte string
 *
 * @name hexToBytes
 * @function
 * @param {String} hex Hex string
 * @returns {String}
 */
PDFSecurity.prototype.hexToBytes = function(hex) {
  for (var bytes = [], c = 0; c < hex.length; c += 2)
    bytes.push(String.fromCharCode(parseInt(hex.substr(c, 2), 16)));
  return bytes.join("");
};

/**
 * Computes the 'O' field in the encryption dictionary
 *
 * @name processOwnerPassword
 * @function
 * @param {String} paddedUserPassword Byte string of padded user password
 * @param {String} paddedOwnerPassword Byte string of padded owner password
 * @returns {String}
 */
PDFSecurity.prototype.processOwnerPassword = function(
  paddedUserPassword,
  paddedOwnerPassword
) {
  let key = md5Bin(paddedOwnerPassword).substr(0, 5);
  return rc4(key, paddedUserPassword);
};

/**
 * Returns an encryptor function which can take in a byte string and returns the encrypted version
 *
 * @name encryptor
 * @function
 * @param {number} objectId
 * @param {number} generation Not sure what this is for, you can set it to 0
 * @returns {Function}
 * @example
 * out("stream");
 * encryptor = security.encryptor(object.id, 0);
 * out(encryptor(data));
 * out("endstream");
 */
PDFSecurity.prototype.encryptor = function(objectId, generation) {
  let key = md5Bin(
    this.encryptionKey +
      String.fromCharCode(
        objectId & 0xff,
        (objectId >> 8) & 0xff,
        (objectId >> 16) & 0xff,
        generation & 0xff,
        (generation >> 8) & 0xff
      )
  ).substr(0, 10);
  return function(data) {
    return rc4(key, data);
  };
};

/**
 * Convert string to `PDF Name Object`.
 * Detail: PDF Reference 1.3 - Chapter 3.2.4 Name Object
 * @param str
 */
function toPDFName(str) {
  // eslint-disable-next-line no-control-regex
  if (/[^\u0000-\u00ff]/.test(str)) {
    // non ascii string
    throw new Error(
      "Invalid PDF Name Object: " + str + ", Only accept ASCII characters."
    );
  }
  var result = "",
    strLength = str.length;
  for (var i = 0; i < strLength; i++) {
    var charCode = str.charCodeAt(i);
    if (
      charCode < 0x21 ||
      charCode === 0x23 /* # */ ||
      charCode === 0x25 /* % */ ||
      charCode === 0x28 /* ( */ ||
      charCode === 0x29 /* ) */ ||
      charCode === 0x2f /* / */ ||
      charCode === 0x3c /* < */ ||
      charCode === 0x3e /* > */ ||
      charCode === 0x5b /* [ */ ||
      charCode === 0x5d /* ] */ ||
      charCode === 0x7b /* { */ ||
      charCode === 0x7d /* } */ ||
      charCode > 0x7e
    ) {
      // Char    CharCode    hexStr   paddingHexStr    Result
      // "\t"    9           9        09               #09
      // " "     32          20       20               #20
      // "©"     169         a9       a9               #a9
      var hexStr = charCode.toString(16),
        paddingHexStr = ("0" + hexStr).slice(-2);

      result += "#" + paddingHexStr;
    } else {
      // Other ASCII printable characters between 0x21 <= X <= 0x7e
      result += str[i];
    }
  }
  return result;
}

/* eslint-disable no-console */
/**
 * jsPDF's Internal PubSub Implementation.
 * Backward compatible rewritten on 2014 by
 * Diego Casorran, https://github.com/diegocr
 *
 * @class
 * @name PubSub
 * @ignore
 */
function PubSub(context) {
  if (typeof context !== "object") {
    throw new Error(
      "Invalid Context passed to initialize PubSub (jsPDF-module)"
    );
  }
  var topics = {};

  this.subscribe = function(topic, callback, once) {
    once = once || false;
    if (
      typeof topic !== "string" ||
      typeof callback !== "function" ||
      typeof once !== "boolean"
    ) {
      throw new Error(
        "Invalid arguments passed to PubSub.subscribe (jsPDF-module)"
      );
    }

    if (!topics.hasOwnProperty(topic)) {
      topics[topic] = {};
    }

    var token = Math.random().toString(35);
    topics[topic][token] = [callback, !!once];

    return token;
  };

  this.unsubscribe = function(token) {
    for (var topic in topics) {
      if (topics[topic][token]) {
        delete topics[topic][token];
        if (Object.keys(topics[topic]).length === 0) {
          delete topics[topic];
        }
        return true;
      }
    }
    return false;
  };

  this.publish = function(topic) {
    if (topics.hasOwnProperty(topic)) {
      var args = Array.prototype.slice.call(arguments, 1),
        tokens = [];

      for (var token in topics[topic]) {
        var sub = topics[topic][token];
        try {
          sub[0].apply(context, args);
        } catch (ex) {
          if (globalObject.console) {
            console$1.error("jsPDF PubSub Error", ex.message, ex);
          }
        }
        if (sub[1]) tokens.push(token);
      }
      if (tokens.length) tokens.forEach(this.unsubscribe);
    }
  };

  this.getTopics = function() {
    return topics;
  };
}

function GState(parameters) {
  if (!(this instanceof GState)) {
    return new GState(parameters);
  }

  /**
   * @name GState#opacity
   * @type {any}
   */
  /**
   * @name GState#stroke-opacity
   * @type {any}
   */
  var supported = "opacity,stroke-opacity".split(",");
  for (var p in parameters) {
    if (parameters.hasOwnProperty(p) && supported.indexOf(p) >= 0) {
      this[p] = parameters[p];
    }
  }
  /**
   * @name GState#id
   * @type {string}
   */
  this.id = ""; // set by addGState()
  /**
   * @name GState#objectNumber
   * @type {number}
   */
  this.objectNumber = -1; // will be set by putGState()
}

GState.prototype.equals = function equals(other) {
  var ignore = "id,objectNumber,equals";
  var p;
  if (!other || typeof other !== typeof this) return false;
  var count = 0;
  for (p in this) {
    if (ignore.indexOf(p) >= 0) continue;
    if (this.hasOwnProperty(p) && !other.hasOwnProperty(p)) return false;
    if (this[p] !== other[p]) return false;
    count++;
  }
  for (p in other) {
    if (other.hasOwnProperty(p) && ignore.indexOf(p) < 0) count--;
  }
  return count === 0;
};

function Pattern$1(gState, matrix) {
  this.gState = gState;
  this.matrix = matrix;

  this.id = ""; // set by addPattern()
  this.objectNumber = -1; // will be set by putPattern()
}

function ShadingPattern(type, coords, colors, gState, matrix) {
  if (!(this instanceof ShadingPattern)) {
    return new ShadingPattern(type, coords, colors, gState, matrix);
  }

  // see putPattern() for information how they are realized
  this.type = type === "axial" ? 2 : 3;
  this.coords = coords;
  this.colors = colors;

  Pattern$1.call(this, gState, matrix);
}

function TilingPattern(boundingBox, xStep, yStep, gState, matrix) {
  if (!(this instanceof TilingPattern)) {
    return new TilingPattern(boundingBox, xStep, yStep, gState, matrix);
  }

  this.boundingBox = boundingBox;
  this.xStep = xStep;
  this.yStep = yStep;

  this.stream = ""; // set by endTilingPattern();

  this.cloneIndex = 0;

  Pattern$1.call(this, gState, matrix);
}

/**
 * Creates new jsPDF document object instance.
 * @name jsPDF
 * @class
 * @param {Object} [options] - Collection of settings initializing the jsPDF-instance
 * @param {string} [options.orientation=portrait] - Orientation of the first page. Possible values are "portrait" or "landscape" (or shortcuts "p" or "l").<br />
 * @param {string} [options.unit=mm] Measurement unit (base unit) to be used when coordinates are specified.<br />
 * Possible values are "pt" (points), "mm", "cm", "in", "px", "pc", "em" or "ex". Note that in order to get the correct scaling for "px"
 * units, you need to enable the hotfix "px_scaling" by setting options.hotfixes = ["px_scaling"].
 * @param {string/Array} [options.format=a4] The format of the first page. Can be:<ul><li>a0 - a10</li><li>b0 - b10</li><li>c0 - c10</li><li>dl</li><li>letter</li><li>government-letter</li><li>legal</li><li>junior-legal</li><li>ledger</li><li>tabloid</li><li>credit-card</li></ul><br />
 * Default is "a4". If you want to use your own format just pass instead of one of the above predefined formats the size as an number-array, e.g. [595.28, 841.89]
 * @param {boolean} [options.putOnlyUsedFonts=false] Only put fonts into the PDF, which were used.
 * @param {boolean} [options.compress=false] Compress the generated PDF.
 * @param {number} [options.precision=16] Precision of the element-positions.
 * @param {number} [options.userUnit=1.0] Not to be confused with the base unit. Please inform yourself before you use it.
 * @param {string[]} [options.hotfixes] An array of strings to enable hotfixes such as correct pixel scaling.
 * @param {Object} [options.encryption]
 * @param {string} [options.encryption.userPassword] Password for the user bound by the given permissions list.
 * @param {string} [options.encryption.ownerPassword] Both userPassword and ownerPassword should be set for proper authentication.
 * @param {string[]} [options.encryption.userPermissions] Array of permissions "print", "modify", "copy", "annot-forms", accessible by the user.
 * @param {number|"smart"} [options.floatPrecision=16]
 * @returns {jsPDF} jsPDF-instance
 * @description
 * ```
 * {
 *  orientation: 'p',
 *  unit: 'mm',
 *  format: 'a4',
 *  putOnlyUsedFonts:true,
 *  floatPrecision: 16 // or "smart", default is 16
 * }
 * ```
 *
 * @constructor
 */
function jsPDF(options) {
  var orientation = typeof arguments[0] === "string" ? arguments[0] : "p";
  var unit = arguments[1];
  var format = arguments[2];
  var compressPdf = arguments[3];
  var filters = [];
  var userUnit = 1.0;
  var precision;
  var floatPrecision = 16;
  var defaultPathOperation = "S";
  var encryptionOptions = null;

  options = options || {};

  if (typeof options === "object") {
    orientation = options.orientation;
    unit = options.unit || unit;
    format = options.format || format;
    compressPdf = options.compress || options.compressPdf || compressPdf;
    encryptionOptions = options.encryption || null;
    if (encryptionOptions !== null) {
      encryptionOptions.userPassword = encryptionOptions.userPassword || "";
      encryptionOptions.ownerPassword = encryptionOptions.ownerPassword || "";
      encryptionOptions.userPermissions =
        encryptionOptions.userPermissions || [];
    }
    userUnit =
      typeof options.userUnit === "number" ? Math.abs(options.userUnit) : 1.0;
    if (typeof options.precision !== "undefined") {
      precision = options.precision;
    }
    if (typeof options.floatPrecision !== "undefined") {
      floatPrecision = options.floatPrecision;
    }
    defaultPathOperation = options.defaultPathOperation || "S";
  }

  filters =
    options.filters || (compressPdf === true ? ["FlateEncode"] : filters);

  unit = unit || "mm";
  orientation = ("" + (orientation || "P")).toLowerCase();
  var putOnlyUsedFonts = options.putOnlyUsedFonts || false;
  var usedFonts = {};

  var API = {
    internal: {},
    __private__: {}
  };

  API.__private__.PubSub = PubSub;

  var pdfVersion = "1.3";
  var getPdfVersion = (API.__private__.getPdfVersion = function() {
    return pdfVersion;
  });

  API.__private__.setPdfVersion = function(value) {
    pdfVersion = value;
  };

  // Size in pt of various paper formats
  var pageFormats = {
    a0: [2383.94, 3370.39],
    a1: [1683.78, 2383.94],
    a2: [1190.55, 1683.78],
    a3: [841.89, 1190.55],
    a4: [595.28, 841.89],
    a5: [419.53, 595.28],
    a6: [297.64, 419.53],
    a7: [209.76, 297.64],
    a8: [147.4, 209.76],
    a9: [104.88, 147.4],
    a10: [73.7, 104.88],
    b0: [2834.65, 4008.19],
    b1: [2004.09, 2834.65],
    b2: [1417.32, 2004.09],
    b3: [1000.63, 1417.32],
    b4: [708.66, 1000.63],
    b5: [498.9, 708.66],
    b6: [354.33, 498.9],
    b7: [249.45, 354.33],
    b8: [175.75, 249.45],
    b9: [124.72, 175.75],
    b10: [87.87, 124.72],
    c0: [2599.37, 3676.54],
    c1: [1836.85, 2599.37],
    c2: [1298.27, 1836.85],
    c3: [918.43, 1298.27],
    c4: [649.13, 918.43],
    c5: [459.21, 649.13],
    c6: [323.15, 459.21],
    c7: [229.61, 323.15],
    c8: [161.57, 229.61],
    c9: [113.39, 161.57],
    c10: [79.37, 113.39],
    dl: [311.81, 623.62],
    letter: [612, 792],
    "government-letter": [576, 756],
    legal: [612, 1008],
    "junior-legal": [576, 360],
    ledger: [1224, 792],
    tabloid: [792, 1224],
    "credit-card": [153, 243]
  };

  API.__private__.getPageFormats = function() {
    return pageFormats;
  };

  var getPageFormat = (API.__private__.getPageFormat = function(value) {
    return pageFormats[value];
  });

  format = format || "a4";

  var ApiMode = {
    COMPAT: "compat",
    ADVANCED: "advanced"
  };
  var apiMode = ApiMode.COMPAT;

  function advancedAPI() {
    // prepend global change of basis matrix
    // (Now, instead of converting every coordinate to the pdf coordinate system, we apply a matrix
    // that does this job for us (however, texts, images and similar objects must be drawn bottom up))
    this.saveGraphicsState();
    out(
      new Matrix(
        scaleFactor,
        0,
        0,
        -scaleFactor,
        0,
        getPageHeight() * scaleFactor
      ).toString() + " cm"
    );
    this.setFontSize(this.getFontSize() / scaleFactor);

    // The default in MrRio's implementation is "S" (stroke), whereas the default in the yWorks implementation
    // was "n" (none). Although this has nothing to do with transforms, we should use the API switch here.
    defaultPathOperation = "n";

    apiMode = ApiMode.ADVANCED;
  }

  function compatAPI() {
    this.restoreGraphicsState();
    defaultPathOperation = "S";
    apiMode = ApiMode.COMPAT;
  }

  /**
   * @function combineFontStyleAndFontWeight
   * @param {string} fontStyle Fontstyle or variant. Example: "italic".
   * @param {number | string} fontWeight Weight of the Font. Example: "normal" | 400
   * @returns {string}
   * @private
   */
  var combineFontStyleAndFontWeight = (API.__private__.combineFontStyleAndFontWeight = function(
    fontStyle,
    fontWeight
  ) {
    if (
      (fontStyle == "bold" && fontWeight == "normal") ||
      (fontStyle == "bold" && fontWeight == 400) ||
      (fontStyle == "normal" && fontWeight == "italic") ||
      (fontStyle == "bold" && fontWeight == "italic")
    ) {
      throw new Error("Invalid Combination of fontweight and fontstyle");
    }
    if (fontWeight) {
      fontStyle =
        fontWeight == 400 || fontWeight === "normal"
          ? fontStyle === "italic"
            ? "italic"
            : "normal"
          : (fontWeight == 700 || fontWeight === "bold") &&
            fontStyle === "normal"
          ? "bold"
          : (fontWeight == 700 ? "bold" : fontWeight) + "" + fontStyle;
    }
    return fontStyle;
  });

  /**
   * @callback ApiSwitchBody
   * @param {jsPDF} pdf
   */

  /**
   * For compatibility reasons jsPDF offers two API modes which differ in the way they convert between the the usual
   * screen coordinates and the PDF coordinate system.
   *   - "compat": Offers full compatibility across all plugins but does not allow arbitrary transforms
   *   - "advanced": Allows arbitrary transforms and more advanced features like pattern fills. Some plugins might
   *     not support this mode, though.
   * Initial mode is "compat".
   *
   * You can either provide a callback to the body argument, which means that jsPDF will automatically switch back to
   * the original API mode afterwards; or you can omit the callback and switch back manually using {@link compatAPI}.
   *
   * Note, that the calls to {@link saveGraphicsState} and {@link restoreGraphicsState} need to be balanced within the
   * callback or between calls of this method and its counterpart {@link compatAPI}. Calls to {@link beginFormObject}
   * or {@link beginTilingPattern} need to be closed by their counterparts before switching back to "compat" API mode.
   *
   * @param {ApiSwitchBody=} body When provided, this callback will be called after the API mode has been switched.
   * The API mode will be switched back automatically afterwards.
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name advancedAPI
   */
  API.advancedAPI = function(body) {
    var doSwitch = apiMode === ApiMode.COMPAT;

    if (doSwitch) {
      advancedAPI.call(this);
    }

    if (typeof body !== "function") {
      return this;
    }

    body(this);

    if (doSwitch) {
      compatAPI.call(this);
    }

    return this;
  };

  /**
   * Switches to "compat" API mode. See {@link advancedAPI} for more details.
   *
   * @param {ApiSwitchBody=} body When provided, this callback will be called after the API mode has been switched.
   * The API mode will be switched back automatically afterwards.
   * @return {jsPDF}
   * @memberof jsPDF#
   * @name compatApi
   */
  API.compatAPI = function(body) {
    var doSwitch = apiMode === ApiMode.ADVANCED;

    if (doSwitch) {
      compatAPI.call(this);
    }

    if (typeof body !== "function") {
      return this;
    }

    body(this);

    if (doSwitch) {
      advancedAPI.call(this);
    }

    return this;
  };

  /**
   * @return {boolean} True iff the current API mode is "advanced". See {@link advancedAPI}.
   * @memberof jsPDF#
   * @name isAdvancedAPI
   */
  API.isAdvancedAPI = function() {
    return apiMode === ApiMode.ADVANCED;
  };

  var advancedApiModeTrap = function(methodName) {
    if (apiMode !== ApiMode.ADVANCED) {
      throw new Error(
        methodName +
          " is only available in 'advanced' API mode. " +
          "You need to call advancedAPI() first."
      );
    }
  };

  var roundToPrecision = (API.roundToPrecision = API.__private__.roundToPrecision = function(
    number,
    parmPrecision
  ) {
    var tmpPrecision = precision || parmPrecision;
    if (isNaN(number) || isNaN(tmpPrecision)) {
      throw new Error("Invalid argument passed to jsPDF.roundToPrecision");
    }
    return number.toFixed(tmpPrecision).replace(/0+$/, "");
  });

  // high precision float
  var hpf;
  if (typeof floatPrecision === "number") {
    hpf = API.hpf = API.__private__.hpf = function(number) {
      if (isNaN(number)) {
        throw new Error("Invalid argument passed to jsPDF.hpf");
      }
      return roundToPrecision(number, floatPrecision);
    };
  } else if (floatPrecision === "smart") {
    hpf = API.hpf = API.__private__.hpf = function(number) {
      if (isNaN(number)) {
        throw new Error("Invalid argument passed to jsPDF.hpf");
      }
      if (number > -1 && number < 1) {
        return roundToPrecision(number, 16);
      } else {
        return roundToPrecision(number, 5);
      }
    };
  } else {
    hpf = API.hpf = API.__private__.hpf = function(number) {
      if (isNaN(number)) {
        throw new Error("Invalid argument passed to jsPDF.hpf");
      }
      return roundToPrecision(number, 16);
    };
  }
  var f2 = (API.f2 = API.__private__.f2 = function(number) {
    if (isNaN(number)) {
      throw new Error("Invalid argument passed to jsPDF.f2");
    }
    return roundToPrecision(number, 2);
  });

  var f3 = (API.__private__.f3 = function(number) {
    if (isNaN(number)) {
      throw new Error("Invalid argument passed to jsPDF.f3");
    }
    return roundToPrecision(number, 3);
  });

  var scale = (API.scale = API.__private__.scale = function(number) {
    if (isNaN(number)) {
      throw new Error("Invalid argument passed to jsPDF.scale");
    }
    if (apiMode === ApiMode.COMPAT) {
      return number * scaleFactor;
    } else if (apiMode === ApiMode.ADVANCED) {
      return number;
    }
  });

  var transformY = function(y) {
    if (apiMode === ApiMode.COMPAT) {
      return getPageHeight() - y;
    } else if (apiMode === ApiMode.ADVANCED) {
      return y;
    }
  };

  var transformScaleY = function(y) {
    return scale(transformY(y));
  };

  /**
   * @name setPrecision
   * @memberof jsPDF#
   * @function
   * @instance
   * @param {string} precision
   * @returns {jsPDF}
   */
  API.__private__.setPrecision = API.setPrecision = function(value) {
    if (typeof parseInt(value, 10) === "number") {
      precision = parseInt(value, 10);
    }
  };

  var fileId = "00000000000000000000000000000000";

  var getFileId = (API.__private__.getFileId = function() {
    return fileId;
  });

  var setFileId = (API.__private__.setFileId = function(value) {
    if (typeof value !== "undefined" && /^[a-fA-F0-9]{32}$/.test(value)) {
      fileId = value.toUpperCase();
    } else {
      fileId = fileId
        .split("")
        .map(function() {
          return "ABCDEF0123456789".charAt(Math.floor(Math.random() * 16));
        })
        .join("");
    }

    if (encryptionOptions !== null) {
      encryption = new PDFSecurity(
        encryptionOptions.userPermissions,
        encryptionOptions.userPassword,
        encryptionOptions.ownerPassword,
        fileId
      );
    }
    return fileId;
  });

  /**
   * @name setFileId
   * @memberof jsPDF#
   * @function
   * @instance
   * @param {string} value GUID.
   * @returns {jsPDF}
   */
  API.setFileId = function(value) {
    setFileId(value);
    return this;
  };

  /**
   * @name getFileId
   * @memberof jsPDF#
   * @function
   * @instance
   *
   * @returns {string} GUID.
   */
  API.getFileId = function() {
    return getFileId();
  };

  var creationDate;

  var convertDateToPDFDate = (API.__private__.convertDateToPDFDate = function(
    parmDate
  ) {
    var result = "";
    var tzoffset = parmDate.getTimezoneOffset(),
      tzsign = tzoffset < 0 ? "+" : "-",
      tzhour = Math.floor(Math.abs(tzoffset / 60)),
      tzmin = Math.abs(tzoffset % 60),
      timeZoneString = [tzsign, padd2(tzhour), "'", padd2(tzmin), "'"].join("");

    result = [
      "D:",
      parmDate.getFullYear(),
      padd2(parmDate.getMonth() + 1),
      padd2(parmDate.getDate()),
      padd2(parmDate.getHours()),
      padd2(parmDate.getMinutes()),
      padd2(parmDate.getSeconds()),
      timeZoneString
    ].join("");
    return result;
  });

  var convertPDFDateToDate = (API.__private__.convertPDFDateToDate = function(
    parmPDFDate
  ) {
    var year = parseInt(parmPDFDate.substr(2, 4), 10);
    var month = parseInt(parmPDFDate.substr(6, 2), 10) - 1;
    var date = parseInt(parmPDFDate.substr(8, 2), 10);
    var hour = parseInt(parmPDFDate.substr(10, 2), 10);
    var minutes = parseInt(parmPDFDate.substr(12, 2), 10);
    var seconds = parseInt(parmPDFDate.substr(14, 2), 10);
    // var timeZoneHour = parseInt(parmPDFDate.substr(16, 2), 10);
    // var timeZoneMinutes = parseInt(parmPDFDate.substr(20, 2), 10);

    var resultingDate = new Date(year, month, date, hour, minutes, seconds, 0);
    return resultingDate;
  });

  var setCreationDate = (API.__private__.setCreationDate = function(date) {
    var tmpCreationDateString;
    var regexPDFCreationDate = /^D:(20[0-2][0-9]|203[0-7]|19[7-9][0-9])(0[0-9]|1[0-2])([0-2][0-9]|3[0-1])(0[0-9]|1[0-9]|2[0-3])(0[0-9]|[1-5][0-9])(0[0-9]|[1-5][0-9])(\+0[0-9]|\+1[0-4]|-0[0-9]|-1[0-1])'(0[0-9]|[1-5][0-9])'?$/;
    if (typeof date === "undefined") {
      date = new Date();
    }

    if (date instanceof Date) {
      tmpCreationDateString = convertDateToPDFDate(date);
    } else if (regexPDFCreationDate.test(date)) {
      tmpCreationDateString = date;
    } else {
      throw new Error("Invalid argument passed to jsPDF.setCreationDate");
    }
    creationDate = tmpCreationDateString;
    return creationDate;
  });

  var getCreationDate = (API.__private__.getCreationDate = function(type) {
    var result = creationDate;
    if (type === "jsDate") {
      result = convertPDFDateToDate(creationDate);
    }
    return result;
  });

  /**
   * @name setCreationDate
   * @memberof jsPDF#
   * @function
   * @instance
   * @param {Object} date
   * @returns {jsPDF}
   */
  API.setCreationDate = function(date) {
    setCreationDate(date);
    return this;
  };

  /**
   * @name getCreationDate
   * @memberof jsPDF#
   * @function
   * @instance
   * @param {Object} type
   * @returns {Object}
   */
  API.getCreationDate = function(type) {
    return getCreationDate(type);
  };

  var padd2 = (API.__private__.padd2 = function(number) {
    return ("0" + parseInt(number)).slice(-2);
  });

  var padd2Hex = (API.__private__.padd2Hex = function(hexString) {
    hexString = hexString.toString();
    return ("00" + hexString).substr(hexString.length);
  });

  var objectNumber = 0; // 'n' Current object number
  var offsets = []; // List of offsets. Activated and reset by buildDocument(). Pupulated by various calls buildDocument makes.
  var content = [];
  var contentLength = 0;
  var additionalObjects = [];

  var pages = [];
  var currentPage;
  var hasCustomDestination = false;
  var outputDestination = content;

  var resetDocument = function() {
    //reset fields relevant for objectNumber generation and xref.
    objectNumber = 0;
    contentLength = 0;
    content = [];
    offsets = [];
    additionalObjects = [];

    rootDictionaryObjId = newObjectDeferred();
    resourceDictionaryObjId = newObjectDeferred();
  };

  API.__private__.setCustomOutputDestination = function(destination) {
    hasCustomDestination = true;
    outputDestination = destination;
  };
  var setOutputDestination = function(destination) {
    if (!hasCustomDestination) {
      outputDestination = destination;
    }
  };

  API.__private__.resetCustomOutputDestination = function() {
    hasCustomDestination = false;
    outputDestination = content;
  };

  var out = (API.__private__.out = function(string) {
    string = string.toString();
    contentLength += string.length + 1;
    outputDestination.push(string);

    return outputDestination;
  });

  var write = (API.__private__.write = function(value) {
    return out(
      arguments.length === 1
        ? value.toString()
        : Array.prototype.join.call(arguments, " ")
    );
  });

  var getArrayBuffer = (API.__private__.getArrayBuffer = function(data) {
    var len = data.length,
      ab = new ArrayBuffer(len),
      u8 = new Uint8Array(ab);

    while (len--) u8[len] = data.charCodeAt(len);
    return ab;
  });

  var standardFonts = [
    ["Helvetica", "helvetica", "normal", "WinAnsiEncoding"],
    ["Helvetica-Bold", "helvetica", "bold", "WinAnsiEncoding"],
    ["Helvetica-Oblique", "helvetica", "italic", "WinAnsiEncoding"],
    ["Helvetica-BoldOblique", "helvetica", "bolditalic", "WinAnsiEncoding"],
    ["Courier", "courier", "normal", "WinAnsiEncoding"],
    ["Courier-Bold", "courier", "bold", "WinAnsiEncoding"],
    ["Courier-Oblique", "courier", "italic", "WinAnsiEncoding"],
    ["Courier-BoldOblique", "courier", "bolditalic", "WinAnsiEncoding"],
    ["Times-Roman", "times", "normal", "WinAnsiEncoding"],
    ["Times-Bold", "times", "bold", "WinAnsiEncoding"],
    ["Times-Italic", "times", "italic", "WinAnsiEncoding"],
    ["Times-BoldItalic", "times", "bolditalic", "WinAnsiEncoding"],
    ["ZapfDingbats", "zapfdingbats", "normal", null],
    ["Symbol", "symbol", "normal", null]
  ];

  API.__private__.getStandardFonts = function() {
    return standardFonts;
  };

  var activeFontSize = options.fontSize || 16;

  /**
   * Sets font size for upcoming text elements.
   *
   * @param {number} size Font size in points.
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setFontSize
   */
  API.__private__.setFontSize = API.setFontSize = function(size) {
    if (apiMode === ApiMode.ADVANCED) {
      activeFontSize = size / scaleFactor;
    } else {
      activeFontSize = size;
    }
    return this;
  };

  /**
   * Gets the fontsize for upcoming text elements.
   *
   * @function
   * @instance
   * @returns {number}
   * @memberof jsPDF#
   * @name getFontSize
   */
  var getFontSize = (API.__private__.getFontSize = API.getFontSize = function() {
    if (apiMode === ApiMode.COMPAT) {
      return activeFontSize;
    } else {
      return activeFontSize * scaleFactor;
    }
  });

  var R2L = options.R2L || false;

  /**
   * Set value of R2L functionality.
   *
   * @param {boolean} value
   * @function
   * @instance
   * @returns {jsPDF} jsPDF-instance
   * @memberof jsPDF#
   * @name setR2L
   */
  API.__private__.setR2L = API.setR2L = function(value) {
    R2L = value;
    return this;
  };

  /**
   * Get value of R2L functionality.
   *
   * @function
   * @instance
   * @returns {boolean} jsPDF-instance
   * @memberof jsPDF#
   * @name getR2L
   */
  API.__private__.getR2L = API.getR2L = function() {
    return R2L;
  };

  var zoomMode; // default: 1;

  var setZoomMode = (API.__private__.setZoomMode = function(zoom) {
    var validZoomModes = [
      undefined,
      null,
      "fullwidth",
      "fullheight",
      "fullpage",
      "original"
    ];

    if (/^(?:\d+\.\d*|\d*\.\d+|\d+)%$/.test(zoom)) {
      zoomMode = zoom;
    } else if (!isNaN(zoom)) {
      zoomMode = parseInt(zoom, 10);
    } else if (validZoomModes.indexOf(zoom) !== -1) {
      zoomMode = zoom;
    } else {
      throw new Error(
        'zoom must be Integer (e.g. 2), a percentage Value (e.g. 300%) or fullwidth, fullheight, fullpage, original. "' +
          zoom +
          '" is not recognized.'
      );
    }
  });

  API.__private__.getZoomMode = function() {
    return zoomMode;
  };

  var pageMode; // default: 'UseOutlines';
  var setPageMode = (API.__private__.setPageMode = function(pmode) {
    var validPageModes = [
      undefined,
      null,
      "UseNone",
      "UseOutlines",
      "UseThumbs",
      "FullScreen"
    ];

    if (validPageModes.indexOf(pmode) == -1) {
      throw new Error(
        'Page mode must be one of UseNone, UseOutlines, UseThumbs, or FullScreen. "' +
          pmode +
          '" is not recognized.'
      );
    }
    pageMode = pmode;
  });

  API.__private__.getPageMode = function() {
    return pageMode;
  };

  var layoutMode; // default: 'continuous';
  var setLayoutMode = (API.__private__.setLayoutMode = function(layout) {
    var validLayoutModes = [
      undefined,
      null,
      "continuous",
      "single",
      "twoleft",
      "tworight",
      "two"
    ];

    if (validLayoutModes.indexOf(layout) == -1) {
      throw new Error(
        'Layout mode must be one of continuous, single, twoleft, tworight. "' +
          layout +
          '" is not recognized.'
      );
    }
    layoutMode = layout;
  });

  API.__private__.getLayoutMode = function() {
    return layoutMode;
  };

  /**
   * Set the display mode options of the page like zoom and layout.
   *
   * @name setDisplayMode
   * @memberof jsPDF#
   * @function
   * @instance
   * @param {integer|String} zoom   You can pass an integer or percentage as
   * a string. 2 will scale the document up 2x, '200%' will scale up by the
   * same amount. You can also set it to 'fullwidth', 'fullheight',
   * 'fullpage', or 'original'.
   *
   * Only certain PDF readers support this, such as Adobe Acrobat.
   *
   * @param {string} layout Layout mode can be: 'continuous' - this is the
   * default continuous scroll. 'single' - the single page mode only shows one
   * page at a time. 'twoleft' - two column left mode, first page starts on
   * the left, and 'tworight' - pages are laid out in two columns, with the
   * first page on the right. This would be used for books.
   * @param {string} pmode 'UseOutlines' - it shows the
   * outline of the document on the left. 'UseThumbs' - shows thumbnails along
   * the left. 'FullScreen' - prompts the user to enter fullscreen mode.
   *
   * @returns {jsPDF}
   */
  API.__private__.setDisplayMode = API.setDisplayMode = function(
    zoom,
    layout,
    pmode
  ) {
    setZoomMode(zoom);
    setLayoutMode(layout);
    setPageMode(pmode);
    return this;
  };

  var documentProperties = {
    title: "",
    subject: "",
    author: "",
    keywords: "",
    creator: ""
  };

  API.__private__.getDocumentProperty = function(key) {
    if (Object.keys(documentProperties).indexOf(key) === -1) {
      throw new Error("Invalid argument passed to jsPDF.getDocumentProperty");
    }
    return documentProperties[key];
  };

  API.__private__.getDocumentProperties = function() {
    return documentProperties;
  };

  /**
   * Adds a properties to the PDF document.
   *
   * @param {Object} A property_name-to-property_value object structure.
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setDocumentProperties
   */
  API.__private__.setDocumentProperties = API.setProperties = API.setDocumentProperties = function(
    properties
  ) {
    // copying only those properties we can render.
    for (var property in documentProperties) {
      if (documentProperties.hasOwnProperty(property) && properties[property]) {
        documentProperties[property] = properties[property];
      }
    }
    return this;
  };

  API.__private__.setDocumentProperty = function(key, value) {
    if (Object.keys(documentProperties).indexOf(key) === -1) {
      throw new Error("Invalid arguments passed to jsPDF.setDocumentProperty");
    }
    return (documentProperties[key] = value);
  };

  var fonts = {}; // collection of font objects, where key is fontKey - a dynamically created label for a given font.
  var fontmap = {}; // mapping structure fontName > fontStyle > font key - performance layer. See addFont()
  var activeFontKey; // will be string representing the KEY of the font as combination of fontName + fontStyle
  var fontStateStack = []; //
  var patterns = {}; // collection of pattern objects
  var patternMap = {}; // see fonts
  var gStates = {}; // collection of graphic state objects
  var gStatesMap = {}; // see fonts
  var activeGState = null;
  var scaleFactor; // Scale factor
  var page = 0;
  var pagesContext = [];
  var events = new PubSub(API);
  var hotfixes = options.hotfixes || [];

  var renderTargets = {};
  var renderTargetMap = {};
  var renderTargetStack = [];
  var pageX;
  var pageY;
  var pageMatrix; // only used for FormObjects

  /**
   * A matrix object for 2D homogenous transformations: <br>
   * | a b 0 | <br>
   * | c d 0 | <br>
   * | e f 1 | <br>
   * pdf multiplies matrices righthand: v' = v x m1 x m2 x ...
   *
   * @class
   * @name Matrix
   * @param {number} sx
   * @param {number} shy
   * @param {number} shx
   * @param {number} sy
   * @param {number} tx
   * @param {number} ty
   * @constructor
   */
  var Matrix = function(sx, shy, shx, sy, tx, ty) {
    if (!(this instanceof Matrix)) {
      return new Matrix(sx, shy, shx, sy, tx, ty);
    }

    if (isNaN(sx)) sx = 1;
    if (isNaN(shy)) shy = 0;
    if (isNaN(shx)) shx = 0;
    if (isNaN(sy)) sy = 1;
    if (isNaN(tx)) tx = 0;
    if (isNaN(ty)) ty = 0;

    this._matrix = [sx, shy, shx, sy, tx, ty];
  };

  /**
   * @name sx
   * @memberof Matrix#
   */
  Object.defineProperty(Matrix.prototype, "sx", {
    get: function() {
      return this._matrix[0];
    },
    set: function(value) {
      this._matrix[0] = value;
    }
  });

  /**
   * @name shy
   * @memberof Matrix#
   */
  Object.defineProperty(Matrix.prototype, "shy", {
    get: function() {
      return this._matrix[1];
    },
    set: function(value) {
      this._matrix[1] = value;
    }
  });

  /**
   * @name shx
   * @memberof Matrix#
   */
  Object.defineProperty(Matrix.prototype, "shx", {
    get: function() {
      return this._matrix[2];
    },
    set: function(value) {
      this._matrix[2] = value;
    }
  });

  /**
   * @name sy
   * @memberof Matrix#
   */
  Object.defineProperty(Matrix.prototype, "sy", {
    get: function() {
      return this._matrix[3];
    },
    set: function(value) {
      this._matrix[3] = value;
    }
  });

  /**
   * @name tx
   * @memberof Matrix#
   */
  Object.defineProperty(Matrix.prototype, "tx", {
    get: function() {
      return this._matrix[4];
    },
    set: function(value) {
      this._matrix[4] = value;
    }
  });

  /**
   * @name ty
   * @memberof Matrix#
   */
  Object.defineProperty(Matrix.prototype, "ty", {
    get: function() {
      return this._matrix[5];
    },
    set: function(value) {
      this._matrix[5] = value;
    }
  });

  Object.defineProperty(Matrix.prototype, "a", {
    get: function() {
      return this._matrix[0];
    },
    set: function(value) {
      this._matrix[0] = value;
    }
  });

  Object.defineProperty(Matrix.prototype, "b", {
    get: function() {
      return this._matrix[1];
    },
    set: function(value) {
      this._matrix[1] = value;
    }
  });

  Object.defineProperty(Matrix.prototype, "c", {
    get: function() {
      return this._matrix[2];
    },
    set: function(value) {
      this._matrix[2] = value;
    }
  });

  Object.defineProperty(Matrix.prototype, "d", {
    get: function() {
      return this._matrix[3];
    },
    set: function(value) {
      this._matrix[3] = value;
    }
  });

  Object.defineProperty(Matrix.prototype, "e", {
    get: function() {
      return this._matrix[4];
    },
    set: function(value) {
      this._matrix[4] = value;
    }
  });

  Object.defineProperty(Matrix.prototype, "f", {
    get: function() {
      return this._matrix[5];
    },
    set: function(value) {
      this._matrix[5] = value;
    }
  });

  /**
   * @name rotation
   * @memberof Matrix#
   */
  Object.defineProperty(Matrix.prototype, "rotation", {
    get: function() {
      return Math.atan2(this.shx, this.sx);
    }
  });

  /**
   * @name scaleX
   * @memberof Matrix#
   */
  Object.defineProperty(Matrix.prototype, "scaleX", {
    get: function() {
      return this.decompose().scale.sx;
    }
  });

  /**
   * @name scaleY
   * @memberof Matrix#
   */
  Object.defineProperty(Matrix.prototype, "scaleY", {
    get: function() {
      return this.decompose().scale.sy;
    }
  });

  /**
   * @name isIdentity
   * @memberof Matrix#
   */
  Object.defineProperty(Matrix.prototype, "isIdentity", {
    get: function() {
      if (this.sx !== 1) {
        return false;
      }
      if (this.shy !== 0) {
        return false;
      }
      if (this.shx !== 0) {
        return false;
      }
      if (this.sy !== 1) {
        return false;
      }
      if (this.tx !== 0) {
        return false;
      }
      if (this.ty !== 0) {
        return false;
      }
      return true;
    }
  });

  /**
   * Join the Matrix Values to a String
   *
   * @function join
   * @param {string} separator Specifies a string to separate each pair of adjacent elements of the array. The separator is converted to a string if necessary. If omitted, the array elements are separated with a comma (","). If separator is an empty string, all elements are joined without any characters in between them.
   * @returns {string} A string with all array elements joined.
   * @memberof Matrix#
   */
  Matrix.prototype.join = function(separator) {
    return [this.sx, this.shy, this.shx, this.sy, this.tx, this.ty]
      .map(hpf)
      .join(separator);
  };

  /**
   * Multiply the matrix with given Matrix
   *
   * @function multiply
   * @param matrix
   * @returns {Matrix}
   * @memberof Matrix#
   */
  Matrix.prototype.multiply = function(matrix) {
    var sx = matrix.sx * this.sx + matrix.shy * this.shx;
    var shy = matrix.sx * this.shy + matrix.shy * this.sy;
    var shx = matrix.shx * this.sx + matrix.sy * this.shx;
    var sy = matrix.shx * this.shy + matrix.sy * this.sy;
    var tx = matrix.tx * this.sx + matrix.ty * this.shx + this.tx;
    var ty = matrix.tx * this.shy + matrix.ty * this.sy + this.ty;

    return new Matrix(sx, shy, shx, sy, tx, ty);
  };

  /**
   * @function decompose
   * @memberof Matrix#
   */
  Matrix.prototype.decompose = function() {
    var a = this.sx;
    var b = this.shy;
    var c = this.shx;
    var d = this.sy;
    var e = this.tx;
    var f = this.ty;

    var scaleX = Math.sqrt(a * a + b * b);
    a /= scaleX;
    b /= scaleX;

    var shear = a * c + b * d;
    c -= a * shear;
    d -= b * shear;

    var scaleY = Math.sqrt(c * c + d * d);
    c /= scaleY;
    d /= scaleY;
    shear /= scaleY;

    if (a * d < b * c) {
      a = -a;
      b = -b;
      shear = -shear;
      scaleX = -scaleX;
    }

    return {
      scale: new Matrix(scaleX, 0, 0, scaleY, 0, 0),
      translate: new Matrix(1, 0, 0, 1, e, f),
      rotate: new Matrix(a, b, -b, a, 0, 0),
      skew: new Matrix(1, 0, shear, 1, 0, 0)
    };
  };

  /**
   * @function toString
   * @memberof Matrix#
   */
  Matrix.prototype.toString = function(parmPrecision) {
    return this.join(" ");
  };

  /**
   * @function inversed
   * @memberof Matrix#
   */
  Matrix.prototype.inversed = function() {
    var a = this.sx,
      b = this.shy,
      c = this.shx,
      d = this.sy,
      e = this.tx,
      f = this.ty;

    var quot = 1 / (a * d - b * c);

    var aInv = d * quot;
    var bInv = -b * quot;
    var cInv = -c * quot;
    var dInv = a * quot;
    var eInv = -aInv * e - cInv * f;
    var fInv = -bInv * e - dInv * f;

    return new Matrix(aInv, bInv, cInv, dInv, eInv, fInv);
  };

  /**
   * @function applyToPoint
   * @memberof Matrix#
   */
  Matrix.prototype.applyToPoint = function(pt) {
    var x = pt.x * this.sx + pt.y * this.shx + this.tx;
    var y = pt.x * this.shy + pt.y * this.sy + this.ty;
    return new Point(x, y);
  };

  /**
   * @function applyToRectangle
   * @memberof Matrix#
   */
  Matrix.prototype.applyToRectangle = function(rect) {
    var pt1 = this.applyToPoint(rect);
    var pt2 = this.applyToPoint(new Point(rect.x + rect.w, rect.y + rect.h));
    return new Rectangle(pt1.x, pt1.y, pt2.x - pt1.x, pt2.y - pt1.y);
  };

  /**
   * Clone the Matrix
   *
   * @function clone
   * @memberof Matrix#
   * @name clone
   * @instance
   */
  Matrix.prototype.clone = function() {
    var sx = this.sx;
    var shy = this.shy;
    var shx = this.shx;
    var sy = this.sy;
    var tx = this.tx;
    var ty = this.ty;

    return new Matrix(sx, shy, shx, sy, tx, ty);
  };

  API.Matrix = Matrix;

  /**
   * Multiplies two matrices. (see {@link Matrix})
   * @param {Matrix} m1
   * @param {Matrix} m2
   * @memberof jsPDF#
   * @name matrixMult
   */
  var matrixMult = (API.matrixMult = function(m1, m2) {
    return m2.multiply(m1);
  });

  /**
   * The identity matrix (equivalent to new Matrix(1, 0, 0, 1, 0, 0)).
   * @type {Matrix}
   * @memberof! jsPDF#
   * @name identityMatrix
   */
  var identityMatrix = new Matrix(1, 0, 0, 1, 0, 0);
  API.unitMatrix = API.identityMatrix = identityMatrix;

  /**
   * Adds a new pattern for later use.
   * @param {String} key The key by it can be referenced later. The keys must be unique!
   * @param {API.Pattern} pattern The pattern
   */
  var addPattern = function(key, pattern) {
    // only add it if it is not already present (the keys provided by the user must be unique!)
    if (patternMap[key]) return;

    var prefix = pattern instanceof ShadingPattern ? "Sh" : "P";
    var patternKey = prefix + (Object.keys(patterns).length + 1).toString(10);
    pattern.id = patternKey;

    patternMap[key] = patternKey;
    patterns[patternKey] = pattern;

    events.publish("addPattern", pattern);
  };

  /**
   * A pattern describing a shading pattern.
   *
   * Only available in "advanced" API mode.
   *
   * @param {String} type One of "axial" or "radial"
   * @param {Array<Number>} coords Either [x1, y1, x2, y2] for "axial" type describing the two interpolation points
   * or [x1, y1, r, x2, y2, r2] for "radial" describing inner and the outer circle.
   * @param {Array<Object>} colors An array of objects with the fields "offset" and "color". "offset" describes
   * the offset in parameter space [0, 1]. "color" is an array of length 3 describing RGB values in [0, 255].
   * @param {GState=} gState An additional graphics state that gets applied to the pattern (optional).
   * @param {Matrix=} matrix A matrix that describes the transformation between the pattern coordinate system
   * and the use coordinate system (optional).
   * @constructor
   * @extends API.Pattern
   */
  API.ShadingPattern = ShadingPattern;

  /**
   * A PDF Tiling pattern.
   *
   * Only available in "advanced" API mode.
   *
   * @param {Array.<Number>} boundingBox The bounding box at which one pattern cell gets clipped.
   * @param {Number} xStep Horizontal spacing between pattern cells.
   * @param {Number} yStep Vertical spacing between pattern cells.
   * @param {API.GState=} gState An additional graphics state that gets applied to the pattern (optional).
   * @param {Matrix=} matrix A matrix that describes the transformation between the pattern coordinate system
   * and the use coordinate system (optional).
   * @constructor
   * @extends API.Pattern
   */
  API.TilingPattern = TilingPattern;

  /**
   * Adds a new {@link API.ShadingPattern} for later use. Only available in "advanced" API mode.
   * @param {String} key
   * @param {Pattern} pattern
   * @function
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name addPattern
   */
  API.addShadingPattern = function(key, pattern) {
    advancedApiModeTrap("addShadingPattern()");

    addPattern(key, pattern);
    return this;
  };

  /**
   * Begins a new tiling pattern. All subsequent render calls are drawn to this pattern until {@link API.endTilingPattern}
   * gets called. Only available in "advanced" API mode.
   * @param {API.Pattern} pattern
   * @memberof jsPDF#
   * @name beginTilingPattern
   */
  API.beginTilingPattern = function(pattern) {
    advancedApiModeTrap("beginTilingPattern()");

    beginNewRenderTarget(
      pattern.boundingBox[0],
      pattern.boundingBox[1],
      pattern.boundingBox[2] - pattern.boundingBox[0],
      pattern.boundingBox[3] - pattern.boundingBox[1],
      pattern.matrix
    );
  };

  /**
   * Ends a tiling pattern and sets the render target to the one active before {@link API.beginTilingPattern} has been called.
   *
   * Only available in "advanced" API mode.
   *
   * @param {string} key A unique key that is used to reference this pattern at later use.
   * @param {API.Pattern} pattern The pattern to end.
   * @memberof jsPDF#
   * @name endTilingPattern
   */
  API.endTilingPattern = function(key, pattern) {
    advancedApiModeTrap("endTilingPattern()");

    // retrieve the stream
    pattern.stream = pages[currentPage].join("\n");

    addPattern(key, pattern);

    events.publish("endTilingPattern", pattern);

    // restore state from stack
    renderTargetStack.pop().restore();
  };

  var newObject = (API.__private__.newObject = function() {
    var oid = newObjectDeferred();
    newObjectDeferredBegin(oid, true);
    return oid;
  });

  // Does not output the object.  The caller must call newObjectDeferredBegin(oid) before outputing any data
  var newObjectDeferred = (API.__private__.newObjectDeferred = function() {
    objectNumber++;
    offsets[objectNumber] = function() {
      return contentLength;
    };
    return objectNumber;
  });

  var newObjectDeferredBegin = function(oid, doOutput) {
    doOutput = typeof doOutput === "boolean" ? doOutput : false;
    offsets[oid] = contentLength;
    if (doOutput) {
      out(oid + " 0 obj");
    }
    return oid;
  };
  // Does not output the object until after the pages have been output.
  // Returns an object containing the objectId and content.
  // All pages have been added so the object ID can be estimated to start right after.
  // This does not modify the current objectNumber;  It must be updated after the newObjects are output.
  var newAdditionalObject = (API.__private__.newAdditionalObject = function() {
    var objId = newObjectDeferred();
    var obj = {
      objId: objId,
      content: ""
    };
    additionalObjects.push(obj);
    return obj;
  });

  var rootDictionaryObjId = newObjectDeferred();
  var resourceDictionaryObjId = newObjectDeferred();

  /////////////////////
  // Private functions
  /////////////////////

  var decodeColorString = (API.__private__.decodeColorString = function(color) {
    var colorEncoded = color.split(" ");
    if (
      colorEncoded.length === 2 &&
      (colorEncoded[1] === "g" || colorEncoded[1] === "G")
    ) {
      // convert grayscale value to rgb so that it can be converted to hex for consistency
      var floatVal = parseFloat(colorEncoded[0]);
      colorEncoded = [floatVal, floatVal, floatVal, "r"];
    } else if (
      colorEncoded.length === 5 &&
      (colorEncoded[4] === "k" || colorEncoded[4] === "K")
    ) {
      // convert CMYK values to rbg so that it can be converted to hex for consistency
      var red = (1.0 - colorEncoded[0]) * (1.0 - colorEncoded[3]);
      var green = (1.0 - colorEncoded[1]) * (1.0 - colorEncoded[3]);
      var blue = (1.0 - colorEncoded[2]) * (1.0 - colorEncoded[3]);

      colorEncoded = [red, green, blue, "r"];
    }
    var colorAsRGB = "#";
    for (var i = 0; i < 3; i++) {
      colorAsRGB += (
        "0" + Math.floor(parseFloat(colorEncoded[i]) * 255).toString(16)
      ).slice(-2);
    }
    return colorAsRGB;
  });

  var encodeColorString = (API.__private__.encodeColorString = function(
    options
  ) {
    var color;

    if (typeof options === "string") {
      options = {
        ch1: options
      };
    }
    var ch1 = options.ch1;
    var ch2 = options.ch2;
    var ch3 = options.ch3;
    var ch4 = options.ch4;
    var letterArray =
      options.pdfColorType === "draw" ? ["G", "RG", "K"] : ["g", "rg", "k"];

    if (typeof ch1 === "string" && ch1.charAt(0) !== "#") {
      var rgbColor = new RGBColor$1(ch1);
      if (rgbColor.ok) {
        ch1 = rgbColor.toHex();
      } else if (!/^\d*\.?\d*$/.test(ch1)) {
        throw new Error(
          'Invalid color "' + ch1 + '" passed to jsPDF.encodeColorString.'
        );
      }
    }
    //convert short rgb to long form
    if (typeof ch1 === "string" && /^#[0-9A-Fa-f]{3}$/.test(ch1)) {
      ch1 = "#" + ch1[1] + ch1[1] + ch1[2] + ch1[2] + ch1[3] + ch1[3];
    }

    if (typeof ch1 === "string" && /^#[0-9A-Fa-f]{6}$/.test(ch1)) {
      var hex = parseInt(ch1.substr(1), 16);
      ch1 = (hex >> 16) & 255;
      ch2 = (hex >> 8) & 255;
      ch3 = hex & 255;
    }

    if (
      typeof ch2 === "undefined" ||
      (typeof ch4 === "undefined" && ch1 === ch2 && ch2 === ch3)
    ) {
      // Gray color space.
      if (typeof ch1 === "string") {
        color = ch1 + " " + letterArray[0];
      } else {
        switch (options.precision) {
          case 2:
            color = f2(ch1 / 255) + " " + letterArray[0];
            break;
          case 3:
          default:
            color = f3(ch1 / 255) + " " + letterArray[0];
        }
      }
    } else if (typeof ch4 === "undefined" || typeof ch4 === "object") {
      // assume RGBA
      if (ch4 && !isNaN(ch4.a)) {
        //TODO Implement transparency.
        //WORKAROUND use white for now, if transparent, otherwise handle as rgb
        if (ch4.a === 0) {
          color = ["1.", "1.", "1.", letterArray[1]].join(" ");
          return color;
        }
      }
      // assume RGB
      if (typeof ch1 === "string") {
        color = [ch1, ch2, ch3, letterArray[1]].join(" ");
      } else {
        switch (options.precision) {
          case 2:
            color = [
              f2(ch1 / 255),
              f2(ch2 / 255),
              f2(ch3 / 255),
              letterArray[1]
            ].join(" ");
            break;
          default:
          case 3:
            color = [
              f3(ch1 / 255),
              f3(ch2 / 255),
              f3(ch3 / 255),
              letterArray[1]
            ].join(" ");
        }
      }
    } else {
      // assume CMYK
      if (typeof ch1 === "string") {
        color = [ch1, ch2, ch3, ch4, letterArray[2]].join(" ");
      } else {
        switch (options.precision) {
          case 2:
            color = [f2(ch1), f2(ch2), f2(ch3), f2(ch4), letterArray[2]].join(
              " "
            );
            break;
          case 3:
          default:
            color = [f3(ch1), f3(ch2), f3(ch3), f3(ch4), letterArray[2]].join(
              " "
            );
        }
      }
    }
    return color;
  });

  var getFilters = (API.__private__.getFilters = function() {
    return filters;
  });

  var putStream = (API.__private__.putStream = function(options) {
    options = options || {};
    var data = options.data || "";
    var filters = options.filters || getFilters();
    var alreadyAppliedFilters = options.alreadyAppliedFilters || [];
    var addLength1 = options.addLength1 || false;
    var valueOfLength1 = data.length;
    var objectId = options.objectId;
    var encryptor = function(data) {
      return data;
    };
    if (encryptionOptions !== null && typeof objectId == "undefined") {
      throw new Error(
        "ObjectId must be passed to putStream for file encryption"
      );
    }
    if (encryptionOptions !== null) {
      encryptor = encryption.encryptor(objectId, 0);
    }

    var processedData = {};
    if (filters === true) {
      filters = ["FlateEncode"];
    }
    var keyValues = options.additionalKeyValues || [];
    if (typeof jsPDF.API.processDataByFilters !== "undefined") {
      processedData = jsPDF.API.processDataByFilters(data, filters);
    } else {
      processedData = { data: data, reverseChain: [] };
    }
    var filterAsString =
      processedData.reverseChain +
      (Array.isArray(alreadyAppliedFilters)
        ? alreadyAppliedFilters.join(" ")
        : alreadyAppliedFilters.toString());

    if (processedData.data.length !== 0) {
      keyValues.push({
        key: "Length",
        value: processedData.data.length
      });
      if (addLength1 === true) {
        keyValues.push({
          key: "Length1",
          value: valueOfLength1
        });
      }
    }

    if (filterAsString.length != 0) {
      if (filterAsString.split("/").length - 1 === 1) {
        keyValues.push({
          key: "Filter",
          value: filterAsString
        });
      } else {
        keyValues.push({
          key: "Filter",
          value: "[" + filterAsString + "]"
        });

        for (var j = 0; j < keyValues.length; j += 1) {
          if (keyValues[j].key === "DecodeParms") {
            var decodeParmsArray = [];

            for (
              var i = 0;
              i < processedData.reverseChain.split("/").length - 1;
              i += 1
            ) {
              decodeParmsArray.push("null");
            }

            decodeParmsArray.push(keyValues[j].value);
            keyValues[j].value = "[" + decodeParmsArray.join(" ") + "]";
          }
        }
      }
    }

    out("<<");
    for (var k = 0; k < keyValues.length; k++) {
      out("/" + keyValues[k].key + " " + keyValues[k].value);
    }
    out(">>");
    if (processedData.data.length !== 0) {
      out("stream");
      out(encryptor(processedData.data));
      out("endstream");
    }
  });

  var putPage = (API.__private__.putPage = function(page) {
    var pageNumber = page.number;
    var data = page.data;
    var pageObjectNumber = page.objId;
    var pageContentsObjId = page.contentsObjId;

    newObjectDeferredBegin(pageObjectNumber, true);
    out("<</Type /Page");
    out("/Parent " + page.rootDictionaryObjId + " 0 R");
    out("/Resources " + page.resourceDictionaryObjId + " 0 R");
    out(
      "/MediaBox [" +
        parseFloat(hpf(page.mediaBox.bottomLeftX)) +
        " " +
        parseFloat(hpf(page.mediaBox.bottomLeftY)) +
        " " +
        hpf(page.mediaBox.topRightX) +
        " " +
        hpf(page.mediaBox.topRightY) +
        "]"
    );
    if (page.cropBox !== null) {
      out(
        "/CropBox [" +
          hpf(page.cropBox.bottomLeftX) +
          " " +
          hpf(page.cropBox.bottomLeftY) +
          " " +
          hpf(page.cropBox.topRightX) +
          " " +
          hpf(page.cropBox.topRightY) +
          "]"
      );
    }

    if (page.bleedBox !== null) {
      out(
        "/BleedBox [" +
          hpf(page.bleedBox.bottomLeftX) +
          " " +
          hpf(page.bleedBox.bottomLeftY) +
          " " +
          hpf(page.bleedBox.topRightX) +
          " " +
          hpf(page.bleedBox.topRightY) +
          "]"
      );
    }

    if (page.trimBox !== null) {
      out(
        "/TrimBox [" +
          hpf(page.trimBox.bottomLeftX) +
          " " +
          hpf(page.trimBox.bottomLeftY) +
          " " +
          hpf(page.trimBox.topRightX) +
          " " +
          hpf(page.trimBox.topRightY) +
          "]"
      );
    }

    if (page.artBox !== null) {
      out(
        "/ArtBox [" +
          hpf(page.artBox.bottomLeftX) +
          " " +
          hpf(page.artBox.bottomLeftY) +
          " " +
          hpf(page.artBox.topRightX) +
          " " +
          hpf(page.artBox.topRightY) +
          "]"
      );
    }

    if (typeof page.userUnit === "number" && page.userUnit !== 1.0) {
      out("/UserUnit " + page.userUnit);
    }

    events.publish("putPage", {
      objId: pageObjectNumber,
      pageContext: pagesContext[pageNumber],
      pageNumber: pageNumber,
      page: data
    });
    out("/Contents " + pageContentsObjId + " 0 R");
    out(">>");
    out("endobj");
    // Page content
    var pageContent = data.join("\n");

    if (apiMode === ApiMode.ADVANCED) {
      // if the user forgot to switch back to COMPAT mode, we must balance the graphics stack again
      pageContent += "\nQ";
    }

    newObjectDeferredBegin(pageContentsObjId, true);
    putStream({
      data: pageContent,
      filters: getFilters(),
      objectId: pageContentsObjId
    });
    out("endobj");
    return pageObjectNumber;
  });

  var putPages = (API.__private__.putPages = function() {
    var n,
      i,
      pageObjectNumbers = [];

    for (n = 1; n <= page; n++) {
      pagesContext[n].objId = newObjectDeferred();
      pagesContext[n].contentsObjId = newObjectDeferred();
    }

    for (n = 1; n <= page; n++) {
      pageObjectNumbers.push(
        putPage({
          number: n,
          data: pages[n],
          objId: pagesContext[n].objId,
          contentsObjId: pagesContext[n].contentsObjId,
          mediaBox: pagesContext[n].mediaBox,
          cropBox: pagesContext[n].cropBox,
          bleedBox: pagesContext[n].bleedBox,
          trimBox: pagesContext[n].trimBox,
          artBox: pagesContext[n].artBox,
          userUnit: pagesContext[n].userUnit,
          rootDictionaryObjId: rootDictionaryObjId,
          resourceDictionaryObjId: resourceDictionaryObjId
        })
      );
    }
    newObjectDeferredBegin(rootDictionaryObjId, true);
    out("<</Type /Pages");
    var kids = "/Kids [";
    for (i = 0; i < page; i++) {
      kids += pageObjectNumbers[i] + " 0 R ";
    }
    out(kids + "]");
    out("/Count " + page);
    out(">>");
    out("endobj");
    events.publish("postPutPages");
  });

  var putFont = function(font) {
    events.publish("putFont", {
      font: font,
      out: out,
      newObject: newObject,
      putStream: putStream
    });

    if (font.isAlreadyPutted !== true) {
      font.objectNumber = newObject();
      out("<<");
      out("/Type /Font");
      out("/BaseFont /" + toPDFName(font.postScriptName));
      out("/Subtype /Type1");
      if (typeof font.encoding === "string") {
        out("/Encoding /" + font.encoding);
      }
      out("/FirstChar 32");
      out("/LastChar 255");
      out(">>");
      out("endobj");
    }
  };

  var putFonts = function() {
    for (var fontKey in fonts) {
      if (fonts.hasOwnProperty(fontKey)) {
        if (
          putOnlyUsedFonts === false ||
          (putOnlyUsedFonts === true && usedFonts.hasOwnProperty(fontKey))
        ) {
          putFont(fonts[fontKey]);
        }
      }
    }
  };

  var putXObject = function(xObject) {
    xObject.objectNumber = newObject();

    var options = [];
    options.push({ key: "Type", value: "/XObject" });
    options.push({ key: "Subtype", value: "/Form" });
    options.push({
      key: "BBox",
      value:
        "[" +
        [
          hpf(xObject.x),
          hpf(xObject.y),
          hpf(xObject.x + xObject.width),
          hpf(xObject.y + xObject.height)
        ].join(" ") +
        "]"
    });
    options.push({
      key: "Matrix",
      value: "[" + xObject.matrix.toString() + "]"
    });
    // TODO: /Resources

    var stream = xObject.pages[1].join("\n");
    putStream({
      data: stream,
      additionalKeyValues: options,
      objectId: xObject.objectNumber
    });
    out("endobj");
  };

  var putXObjects = function() {
    for (var xObjectKey in renderTargets) {
      if (renderTargets.hasOwnProperty(xObjectKey)) {
        putXObject(renderTargets[xObjectKey]);
      }
    }
  };

  var interpolateAndEncodeRGBStream = function(colors, numberSamples) {
    var tValues = [];
    var t;
    var dT = 1.0 / (numberSamples - 1);
    for (t = 0.0; t < 1.0; t += dT) {
      tValues.push(t);
    }
    tValues.push(1.0);
    // add first and last control point if not present
    if (colors[0].offset != 0.0) {
      var c0 = {
        offset: 0.0,
        color: colors[0].color
      };
      colors.unshift(c0);
    }
    if (colors[colors.length - 1].offset != 1.0) {
      var c1 = {
        offset: 1.0,
        color: colors[colors.length - 1].color
      };
      colors.push(c1);
    }
    var out = "";
    var index = 0;

    for (var i = 0; i < tValues.length; i++) {
      t = tValues[i];
      while (t > colors[index + 1].offset) index++;
      var a = colors[index].offset;
      var b = colors[index + 1].offset;
      var d = (t - a) / (b - a);

      var aColor = colors[index].color;
      var bColor = colors[index + 1].color;

      out +=
        padd2Hex(Math.round((1 - d) * aColor[0] + d * bColor[0]).toString(16)) +
        padd2Hex(Math.round((1 - d) * aColor[1] + d * bColor[1]).toString(16)) +
        padd2Hex(Math.round((1 - d) * aColor[2] + d * bColor[2]).toString(16));
    }
    return out.trim();
  };

  var putShadingPattern = function(pattern, numberSamples) {
    /*
       Axial patterns shade between the two points specified in coords, radial patterns between the inner
       and outer circle.
       The user can specify an array (colors) that maps t-Values in [0, 1] to RGB colors. These are now
       interpolated to equidistant samples and written to pdf as a sample (type 0) function.
       */
    // The number of color samples that should be used to describe the shading.
    // The higher, the more accurate the gradient will be.
    numberSamples || (numberSamples = 21);
    var funcObjectNumber = newObject();
    var stream = interpolateAndEncodeRGBStream(pattern.colors, numberSamples);

    var options = [];
    options.push({ key: "FunctionType", value: "0" });
    options.push({ key: "Domain", value: "[0.0 1.0]" });
    options.push({ key: "Size", value: "[" + numberSamples + "]" });
    options.push({ key: "BitsPerSample", value: "8" });
    options.push({ key: "Range", value: "[0.0 1.0 0.0 1.0 0.0 1.0]" });
    options.push({ key: "Decode", value: "[0.0 1.0 0.0 1.0 0.0 1.0]" });

    putStream({
      data: stream,
      additionalKeyValues: options,
      alreadyAppliedFilters: ["/ASCIIHexDecode"],
      objectId: funcObjectNumber
    });
    out("endobj");

    pattern.objectNumber = newObject();
    out("<< /ShadingType " + pattern.type);
    out("/ColorSpace /DeviceRGB");
    var coords =
      "/Coords [" +
      hpf(parseFloat(pattern.coords[0])) +
      " " + // x1
      hpf(parseFloat(pattern.coords[1])) +
      " "; // y1
    if (pattern.type === 2) {
      // axial
      coords +=
        hpf(parseFloat(pattern.coords[2])) +
        " " + // x2
        hpf(parseFloat(pattern.coords[3])); // y2
    } else {
      // radial
      coords +=
        hpf(parseFloat(pattern.coords[2])) +
        " " + // r1
        hpf(parseFloat(pattern.coords[3])) +
        " " + // x2
        hpf(parseFloat(pattern.coords[4])) +
        " " + // y2
        hpf(parseFloat(pattern.coords[5])); // r2
    }
    coords += "]";
    out(coords);

    if (pattern.matrix) {
      out("/Matrix [" + pattern.matrix.toString() + "]");
    }
    out("/Function " + funcObjectNumber + " 0 R");
    out("/Extend [true true]");
    out(">>");
    out("endobj");
  };

  var putTilingPattern = function(pattern, deferredResourceDictionaryIds) {
    var resourcesObjectId = newObjectDeferred();
    var patternObjectId = newObject();

    deferredResourceDictionaryIds.push({
      resourcesOid: resourcesObjectId,
      objectOid: patternObjectId
    });

    pattern.objectNumber = patternObjectId;
    var options = [];
    options.push({ key: "Type", value: "/Pattern" });
    options.push({ key: "PatternType", value: "1" }); // tiling pattern
    options.push({ key: "PaintType", value: "1" }); // colored tiling pattern
    options.push({ key: "TilingType", value: "1" }); // constant spacing
    options.push({
      key: "BBox",
      value: "[" + pattern.boundingBox.map(hpf).join(" ") + "]"
    });
    options.push({ key: "XStep", value: hpf(pattern.xStep) });
    options.push({ key: "YStep", value: hpf(pattern.yStep) });
    options.push({ key: "Resources", value: resourcesObjectId + " 0 R" });
    if (pattern.matrix) {
      options.push({
        key: "Matrix",
        value: "[" + pattern.matrix.toString() + "]"
      });
    }

    putStream({
      data: pattern.stream,
      additionalKeyValues: options,
      objectId: pattern.objectNumber
    });
    out("endobj");
  };

  var putPatterns = function(deferredResourceDictionaryIds) {
    var patternKey;
    for (patternKey in patterns) {
      if (patterns.hasOwnProperty(patternKey)) {
        if (patterns[patternKey] instanceof ShadingPattern) {
          putShadingPattern(patterns[patternKey]);
        } else if (patterns[patternKey] instanceof TilingPattern) {
          putTilingPattern(patterns[patternKey], deferredResourceDictionaryIds);
        }
      }
    }
  };

  var putGState = function(gState) {
    gState.objectNumber = newObject();
    out("<<");
    for (var p in gState) {
      switch (p) {
        case "opacity":
          out("/ca " + f2(gState[p]));
          break;
        case "stroke-opacity":
          out("/CA " + f2(gState[p]));
          break;
      }
    }
    out(">>");
    out("endobj");
  };

  var putGStates = function() {
    var gStateKey;
    for (gStateKey in gStates) {
      if (gStates.hasOwnProperty(gStateKey)) {
        putGState(gStates[gStateKey]);
      }
    }
  };

  var putXobjectDict = function() {
    out("/XObject <<");
    for (var xObjectKey in renderTargets) {
      if (
        renderTargets.hasOwnProperty(xObjectKey) &&
        renderTargets[xObjectKey].objectNumber >= 0
      ) {
        out(
          "/" +
            xObjectKey +
            " " +
            renderTargets[xObjectKey].objectNumber +
            " 0 R"
        );
      }
    }

    // Loop through images, or other data objects
    events.publish("putXobjectDict");
    out(">>");
  };

  var putEncryptionDict = function() {
    encryption.oid = newObject();
    out("<<");
    out("/Filter /Standard");
    out("/V " + encryption.v);
    out("/R " + encryption.r);
    out("/U <" + encryption.toHexString(encryption.U) + ">");
    out("/O <" + encryption.toHexString(encryption.O) + ">");
    out("/P " + encryption.P);
    out(">>");
    out("endobj");
  };

  var putFontDict = function() {
    out("/Font <<");

    for (var fontKey in fonts) {
      if (fonts.hasOwnProperty(fontKey)) {
        if (
          putOnlyUsedFonts === false ||
          (putOnlyUsedFonts === true && usedFonts.hasOwnProperty(fontKey))
        ) {
          out("/" + fontKey + " " + fonts[fontKey].objectNumber + " 0 R");
        }
      }
    }
    out(">>");
  };

  var putShadingPatternDict = function() {
    if (Object.keys(patterns).length > 0) {
      out("/Shading <<");
      for (var patternKey in patterns) {
        if (
          patterns.hasOwnProperty(patternKey) &&
          patterns[patternKey] instanceof ShadingPattern &&
          patterns[patternKey].objectNumber >= 0
        ) {
          out(
            "/" + patternKey + " " + patterns[patternKey].objectNumber + " 0 R"
          );
        }
      }

      events.publish("putShadingPatternDict");
      out(">>");
    }
  };

  var putTilingPatternDict = function(objectOid) {
    if (Object.keys(patterns).length > 0) {
      out("/Pattern <<");
      for (var patternKey in patterns) {
        if (
          patterns.hasOwnProperty(patternKey) &&
          patterns[patternKey] instanceof API.TilingPattern &&
          patterns[patternKey].objectNumber >= 0 &&
          patterns[patternKey].objectNumber < objectOid // prevent cyclic dependencies
        ) {
          out(
            "/" + patternKey + " " + patterns[patternKey].objectNumber + " 0 R"
          );
        }
      }
      events.publish("putTilingPatternDict");
      out(">>");
    }
  };

  var putGStatesDict = function() {
    if (Object.keys(gStates).length > 0) {
      var gStateKey;
      out("/ExtGState <<");
      for (gStateKey in gStates) {
        if (
          gStates.hasOwnProperty(gStateKey) &&
          gStates[gStateKey].objectNumber >= 0
        ) {
          out("/" + gStateKey + " " + gStates[gStateKey].objectNumber + " 0 R");
        }
      }

      events.publish("putGStateDict");
      out(">>");
    }
  };

  var putResourceDictionary = function(objectIds) {
    newObjectDeferredBegin(objectIds.resourcesOid, true);
    out("<<");
    out("/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]");
    putFontDict();
    putShadingPatternDict();
    putTilingPatternDict(objectIds.objectOid);
    putGStatesDict();
    putXobjectDict();
    out(">>");
    out("endobj");
  };

  var putResources = function() {
    // FormObjects, Patterns etc. might use other FormObjects/Patterns/Images
    // which means their resource dictionaries must contain the already resolved
    // object ids. For this reason we defer the serialization of the resource
    // dicts until all objects have been serialized and have object ids.
    //
    // In order to prevent cyclic dependencies (which Adobe Reader doesn't like),
    // we only put all oids that are smaller than the oid of the object the
    // resource dict belongs to. This is correct behavior, since the streams
    // may only use other objects that have already been defined and thus appear
    // earlier in their respective collection.
    // Currently, this only affects tiling patterns, but a (more) correct
    // implementation of FormObjects would also define their own resource dicts.
    var deferredResourceDictionaryIds = [];

    putFonts();
    putGStates();
    putXObjects();
    putPatterns(deferredResourceDictionaryIds);

    events.publish("putResources");
    deferredResourceDictionaryIds.forEach(putResourceDictionary);
    putResourceDictionary({
      resourcesOid: resourceDictionaryObjId,
      objectOid: Number.MAX_SAFE_INTEGER // output all objects
    });
    events.publish("postPutResources");
  };

  var putAdditionalObjects = function() {
    events.publish("putAdditionalObjects");
    for (var i = 0; i < additionalObjects.length; i++) {
      var obj = additionalObjects[i];
      newObjectDeferredBegin(obj.objId, true);
      out(obj.content);
      out("endobj");
    }
    events.publish("postPutAdditionalObjects");
  };

  var addFontToFontDictionary = function(font) {
    fontmap[font.fontName] = fontmap[font.fontName] || {};
    fontmap[font.fontName][font.fontStyle] = font.id;
  };

  var addFont = function(
    postScriptName,
    fontName,
    fontStyle,
    encoding,
    isStandardFont
  ) {
    var font = {
      id: "F" + (Object.keys(fonts).length + 1).toString(10),
      postScriptName: postScriptName,
      fontName: fontName,
      fontStyle: fontStyle,
      encoding: encoding,
      isStandardFont: isStandardFont || false,
      metadata: {}
    };

    events.publish("addFont", {
      font: font,
      instance: this
    });

    fonts[font.id] = font;
    addFontToFontDictionary(font);
    return font.id;
  };

  var addFonts = function(arrayOfFonts) {
    for (var i = 0, l = standardFonts.length; i < l; i++) {
      var fontKey = addFont.call(
        this,
        arrayOfFonts[i][0],
        arrayOfFonts[i][1],
        arrayOfFonts[i][2],
        standardFonts[i][3],
        true
      );

      if (putOnlyUsedFonts === false) {
        usedFonts[fontKey] = true;
      }
      // adding aliases for standard fonts, this time matching the capitalization
      var parts = arrayOfFonts[i][0].split("-");
      addFontToFontDictionary({
        id: fontKey,
        fontName: parts[0],
        fontStyle: parts[1] || ""
      });
    }
    events.publish("addFonts", {
      fonts: fonts,
      dictionary: fontmap
    });
  };

  var SAFE = function __safeCall(fn) {
    fn.foo = function __safeCallWrapper() {
      try {
        return fn.apply(this, arguments);
      } catch (e) {
        var stack = e.stack || "";
        if (~stack.indexOf(" at ")) stack = stack.split(" at ")[1];
        var m =
          "Error in function " +
          stack.split("\n")[0].split("<")[0] +
          ": " +
          e.message;
        if (globalObject.console) {
          globalObject.console.error(m, e);
          if (globalObject.alert) alert(m);
        } else {
          throw new Error(m);
        }
      }
    };
    fn.foo.bar = fn;
    return fn.foo;
  };

  var to8bitStream = function(text, flags) {
    /**
     * PDF 1.3 spec:
     * "For text strings encoded in Unicode, the first two bytes must be 254 followed by
     * 255, representing the Unicode byte order marker, U+FEFF. (This sequence conflicts
     * with the PDFDocEncoding character sequence thorn ydieresis, which is unlikely
     * to be a meaningful beginning of a word or phrase.) The remainder of the
     * string consists of Unicode character codes, according to the UTF-16 encoding
     * specified in the Unicode standard, version 2.0. Commonly used Unicode values
     * are represented as 2 bytes per character, with the high-order byte appearing first
     * in the string."
     *
     * In other words, if there are chars in a string with char code above 255, we
     * recode the string to UCS2 BE - string doubles in length and BOM is prepended.
     *
     * HOWEVER!
     * Actual *content* (body) text (as opposed to strings used in document properties etc)
     * does NOT expect BOM. There, it is treated as a literal GID (Glyph ID)
     *
     * Because of Adobe's focus on "you subset your fonts!" you are not supposed to have
     * a font that maps directly Unicode (UCS2 / UTF16BE) code to font GID, but you could
     * fudge it with "Identity-H" encoding and custom CIDtoGID map that mimics Unicode
     * code page. There, however, all characters in the stream are treated as GIDs,
     * including BOM, which is the reason we need to skip BOM in content text (i.e. that
     * that is tied to a font).
     *
     * To signal this "special" PDFEscape / to8bitStream handling mode,
     * API.text() function sets (unless you overwrite it with manual values
     * given to API.text(.., flags) )
     * flags.autoencode = true
     * flags.noBOM = true
     *
     * ===================================================================================
     * `flags` properties relied upon:
     *   .sourceEncoding = string with encoding label.
     *                     "Unicode" by default. = encoding of the incoming text.
     *                     pass some non-existing encoding name
     *                     (ex: 'Do not touch my strings! I know what I am doing.')
     *                     to make encoding code skip the encoding step.
     *   .outputEncoding = Either valid PDF encoding name
     *                     (must be supported by jsPDF font metrics, otherwise no encoding)
     *                     or a JS object, where key = sourceCharCode, value = outputCharCode
     *                     missing keys will be treated as: sourceCharCode === outputCharCode
     *   .noBOM
     *       See comment higher above for explanation for why this is important
     *   .autoencode
     *       See comment higher above for explanation for why this is important
     */

    var i,
      l,
      sourceEncoding,
      encodingBlock,
      outputEncoding,
      newtext,
      isUnicode,
      ch,
      bch;

    flags = flags || {};
    sourceEncoding = flags.sourceEncoding || "Unicode";
    outputEncoding = flags.outputEncoding;

    // This 'encoding' section relies on font metrics format
    // attached to font objects by, among others,
    // "Willow Systems' standard_font_metrics plugin"
    // see jspdf.plugin.standard_font_metrics.js for format
    // of the font.metadata.encoding Object.
    // It should be something like
    //   .encoding = {'codePages':['WinANSI....'], 'WinANSI...':{code:code, ...}}
    //   .widths = {0:width, code:width, ..., 'fof':divisor}
    //   .kerning = {code:{previous_char_code:shift, ..., 'fof':-divisor},...}
    if (
      (flags.autoencode || outputEncoding) &&
      fonts[activeFontKey].metadata &&
      fonts[activeFontKey].metadata[sourceEncoding] &&
      fonts[activeFontKey].metadata[sourceEncoding].encoding
    ) {
      encodingBlock = fonts[activeFontKey].metadata[sourceEncoding].encoding;

      // each font has default encoding. Some have it clearly defined.
      if (!outputEncoding && fonts[activeFontKey].encoding) {
        outputEncoding = fonts[activeFontKey].encoding;
      }

      // Hmmm, the above did not work? Let's try again, in different place.
      if (!outputEncoding && encodingBlock.codePages) {
        outputEncoding = encodingBlock.codePages[0]; // let's say, first one is the default
      }

      if (typeof outputEncoding === "string") {
        outputEncoding = encodingBlock[outputEncoding];
      }
      // we want output encoding to be a JS Object, where
      // key = sourceEncoding's character code and
      // value = outputEncoding's character code.
      if (outputEncoding) {
        isUnicode = false;
        newtext = [];
        for (i = 0, l = text.length; i < l; i++) {
          ch = outputEncoding[text.charCodeAt(i)];
          if (ch) {
            newtext.push(String.fromCharCode(ch));
          } else {
            newtext.push(text[i]);
          }

          // since we are looping over chars anyway, might as well
          // check for residual unicodeness
          if (newtext[i].charCodeAt(0) >> 8) {
            /* more than 255 */
            isUnicode = true;
          }
        }
        text = newtext.join("");
      }
    }

    i = text.length;
    // isUnicode may be set to false above. Hence the triple-equal to undefined
    while (isUnicode === undefined && i !== 0) {
      if (text.charCodeAt(i - 1) >> 8) {
        /* more than 255 */
        isUnicode = true;
      }
      i--;
    }
    if (!isUnicode) {
      return text;
    }

    newtext = flags.noBOM ? [] : [254, 255];
    for (i = 0, l = text.length; i < l; i++) {
      ch = text.charCodeAt(i);
      bch = ch >> 8; // divide by 256
      if (bch >> 8) {
        /* something left after dividing by 256 second time */
        throw new Error(
          "Character at position " +
            i +
            " of string '" +
            text +
            "' exceeds 16bits. Cannot be encoded into UCS-2 BE"
        );
      }
      newtext.push(bch);
      newtext.push(ch - (bch << 8));
    }
    return String.fromCharCode.apply(undefined, newtext);
  };

  var pdfEscape = (API.__private__.pdfEscape = API.pdfEscape = function(
    text,
    flags
  ) {
    /**
     * Replace '/', '(', and ')' with pdf-safe versions
     *
     * Doing to8bitStream does NOT make this PDF display unicode text. For that
     * we also need to reference a unicode font and embed it - royal pain in the rear.
     *
     * There is still a benefit to to8bitStream - PDF simply cannot handle 16bit chars,
     * which JavaScript Strings are happy to provide. So, while we still cannot display
     * 2-byte characters property, at least CONDITIONALLY converting (entire string containing)
     * 16bit chars to (USC-2-BE) 2-bytes per char + BOM streams we ensure that entire PDF
     * is still parseable.
     * This will allow immediate support for unicode in document properties strings.
     */
    return to8bitStream(text, flags)
      .replace(/\\/g, "\\\\")
      .replace(/\(/g, "\\(")
      .replace(/\)/g, "\\)");
  });

  var beginPage = (API.__private__.beginPage = function(format) {
    pages[++page] = [];
    pagesContext[page] = {
      objId: 0,
      contentsObjId: 0,
      userUnit: Number(userUnit),
      artBox: null,
      bleedBox: null,
      cropBox: null,
      trimBox: null,
      mediaBox: {
        bottomLeftX: 0,
        bottomLeftY: 0,
        topRightX: Number(format[0]),
        topRightY: Number(format[1])
      }
    };
    _setPage(page);
    setOutputDestination(pages[currentPage]);
  });

  var _addPage = function(parmFormat, parmOrientation) {
    var dimensions, width, height;

    orientation = parmOrientation || orientation;

    if (typeof parmFormat === "string") {
      dimensions = getPageFormat(parmFormat.toLowerCase());
      if (Array.isArray(dimensions)) {
        width = dimensions[0];
        height = dimensions[1];
      }
    }

    if (Array.isArray(parmFormat)) {
      width = parmFormat[0] * scaleFactor;
      height = parmFormat[1] * scaleFactor;
    }

    if (isNaN(width)) {
      width = format[0];
      height = format[1];
    }

    if (width > 14400 || height > 14400) {
      console$1.warn(
        "A page in a PDF can not be wider or taller than 14400 userUnit. jsPDF limits the width/height to 14400"
      );
      width = Math.min(14400, width);
      height = Math.min(14400, height);
    }

    format = [width, height];

    switch (orientation.substr(0, 1)) {
      case "l":
        if (height > width) {
          format = [height, width];
        }
        break;
      case "p":
        if (width > height) {
          format = [height, width];
        }
        break;
    }

    beginPage(format);

    // Set line width
    setLineWidth(lineWidth);
    // Set draw color
    out(strokeColor);
    // resurrecting non-default line caps, joins
    if (lineCapID !== 0) {
      out(lineCapID + " J");
    }
    if (lineJoinID !== 0) {
      out(lineJoinID + " j");
    }
    events.publish("addPage", {
      pageNumber: page
    });
  };

  var _deletePage = function(n) {
    if (n > 0 && n <= page) {
      pages.splice(n, 1);
      pagesContext.splice(n, 1);
      page--;
      if (currentPage > page) {
        currentPage = page;
      }
      this.setPage(currentPage);
    }
  };

  var _setPage = function(n) {
    if (n > 0 && n <= page) {
      currentPage = n;
    }
  };

  var getNumberOfPages = (API.__private__.getNumberOfPages = API.getNumberOfPages = function() {
    return pages.length - 1;
  });

  /**
   * Returns a document-specific font key - a label assigned to a
   * font name + font type combination at the time the font was added
   * to the font inventory.
   *
   * Font key is used as label for the desired font for a block of text
   * to be added to the PDF document stream.
   * @private
   * @function
   * @param fontName {string} can be undefined on "falthy" to indicate "use current"
   * @param fontStyle {string} can be undefined on "falthy" to indicate "use current"
   * @returns {string} Font key.
   * @ignore
   */
  var getFont = function(fontName, fontStyle, options) {
    var key = undefined,
      fontNameLowerCase;
    options = options || {};

    fontName =
      fontName !== undefined ? fontName : fonts[activeFontKey].fontName;
    fontStyle =
      fontStyle !== undefined ? fontStyle : fonts[activeFontKey].fontStyle;
    fontNameLowerCase = fontName.toLowerCase();

    if (
      fontmap[fontNameLowerCase] !== undefined &&
      fontmap[fontNameLowerCase][fontStyle] !== undefined
    ) {
      key = fontmap[fontNameLowerCase][fontStyle];
    } else if (
      fontmap[fontName] !== undefined &&
      fontmap[fontName][fontStyle] !== undefined
    ) {
      key = fontmap[fontName][fontStyle];
    } else {
      if (options.disableWarning === false) {
        console$1.warn(
          "Unable to look up font label for font '" +
            fontName +
            "', '" +
            fontStyle +
            "'. Refer to getFontList() for available fonts."
        );
      }
    }

    if (!key && !options.noFallback) {
      key = fontmap["times"][fontStyle];
      if (key == null) {
        key = fontmap["times"]["normal"];
      }
    }
    return key;
  };

  var putInfo = (API.__private__.putInfo = function() {
    var objectId = newObject();
    var encryptor = function(data) {
      return data;
    };
    if (encryptionOptions !== null) {
      encryptor = encryption.encryptor(objectId, 0);
    }
    out("<<");
    out("/Producer (" + pdfEscape(encryptor("jsPDF " + jsPDF.version)) + ")");
    for (var key in documentProperties) {
      if (documentProperties.hasOwnProperty(key) && documentProperties[key]) {
        out(
          "/" +
            key.substr(0, 1).toUpperCase() +
            key.substr(1) +
            " (" +
            pdfEscape(encryptor(documentProperties[key])) +
            ")"
        );
      }
    }
    out("/CreationDate (" + pdfEscape(encryptor(creationDate)) + ")");
    out(">>");
    out("endobj");
  });

  var putCatalog = (API.__private__.putCatalog = function(options) {
    options = options || {};
    var tmpRootDictionaryObjId =
      options.rootDictionaryObjId || rootDictionaryObjId;
    newObject();
    out("<<");
    out("/Type /Catalog");
    out("/Pages " + tmpRootDictionaryObjId + " 0 R");
    // PDF13ref Section 7.2.1
    if (!zoomMode) zoomMode = "fullwidth";
    switch (zoomMode) {
      case "fullwidth":
        out("/OpenAction [3 0 R /FitH null]");
        break;
      case "fullheight":
        out("/OpenAction [3 0 R /FitV null]");
        break;
      case "fullpage":
        out("/OpenAction [3 0 R /Fit]");
        break;
      case "original":
        out("/OpenAction [3 0 R /XYZ null null 1]");
        break;
      default:
        var pcn = "" + zoomMode;
        if (pcn.substr(pcn.length - 1) === "%")
          zoomMode = parseInt(zoomMode) / 100;
        if (typeof zoomMode === "number") {
          out("/OpenAction [3 0 R /XYZ null null " + f2(zoomMode) + "]");
        }
    }
    if (!layoutMode) layoutMode = "continuous";
    switch (layoutMode) {
      case "continuous":
        out("/PageLayout /OneColumn");
        break;
      case "single":
        out("/PageLayout /SinglePage");
        break;
      case "two":
      case "twoleft":
        out("/PageLayout /TwoColumnLeft");
        break;
      case "tworight":
        out("/PageLayout /TwoColumnRight");
        break;
    }
    if (pageMode) {
      /**
       * A name object specifying how the document should be displayed when opened:
       * UseNone      : Neither document outline nor thumbnail images visible -- DEFAULT
       * UseOutlines  : Document outline visible
       * UseThumbs    : Thumbnail images visible
       * FullScreen   : Full-screen mode, with no menu bar, window controls, or any other window visible
       */
      out("/PageMode /" + pageMode);
    }
    events.publish("putCatalog");
    out(">>");
    out("endobj");
  });

  var putTrailer = (API.__private__.putTrailer = function() {
    out("trailer");
    out("<<");
    out("/Size " + (objectNumber + 1));
    // Root and Info must be the last and second last objects written respectively
    out("/Root " + objectNumber + " 0 R");
    out("/Info " + (objectNumber - 1) + " 0 R");
    if (encryptionOptions !== null) {
      out("/Encrypt " + encryption.oid + " 0 R");
    }
    out("/ID [ <" + fileId + "> <" + fileId + "> ]");
    out(">>");
  });

  var putHeader = (API.__private__.putHeader = function() {
    out("%PDF-" + pdfVersion);
    out("%\xBA\xDF\xAC\xE0");
  });

  var putXRef = (API.__private__.putXRef = function() {
    var p = "0000000000";

    out("xref");
    out("0 " + (objectNumber + 1));
    out("0000000000 65535 f ");
    for (var i = 1; i <= objectNumber; i++) {
      var offset = offsets[i];
      if (typeof offset === "function") {
        out((p + offsets[i]()).slice(-10) + " 00000 n ");
      } else {
        if (typeof offsets[i] !== "undefined") {
          out((p + offsets[i]).slice(-10) + " 00000 n ");
        } else {
          out("0000000000 00000 n ");
        }
      }
    }
  });

  var buildDocument = (API.__private__.buildDocument = function() {
    resetDocument();
    setOutputDestination(content);

    events.publish("buildDocument");

    putHeader();
    putPages();
    putAdditionalObjects();
    putResources();
    if (encryptionOptions !== null) putEncryptionDict();
    putInfo();
    putCatalog();

    var offsetOfXRef = contentLength;
    putXRef();
    putTrailer();
    out("startxref");
    out("" + offsetOfXRef);
    out("%%EOF");

    setOutputDestination(pages[currentPage]);

    return content.join("\n");
  });

  var getBlob = (API.__private__.getBlob = function(data) {
    return new Blob([getArrayBuffer(data)], {
      type: "application/pdf"
    });
  });

  /**
   * Generates the PDF document.
   *
   * If `type` argument is undefined, output is raw body of resulting PDF returned as a string.
   *
   * @param {string} type A string identifying one of the possible output types.<br/>
   *                      Possible values are: <br/>
   *                          'arraybuffer' -> (ArrayBuffer)<br/>
   *                          'blob' -> (Blob)<br/>
   *                          'bloburi'/'bloburl' -> (string)<br/>
   *                          'datauristring'/'dataurlstring' -> (string)<br/>
   *                          'datauri'/'dataurl' -> (undefined) -> change location to generated datauristring/dataurlstring<br/>
   * @param {Object|string} options An object providing some additional signalling to PDF generator.<br/>
   *                                Possible options are 'filename'.<br/>
   *                                A string can be passed instead of {filename:string} and defaults to 'generated.pdf'
   * @function
   * @instance
   * @returns {string|window|ArrayBuffer|Blob|jsPDF|null|undefined}
   * @memberof jsPDF#
   * @name output
   */
  var output = (API.output = API.__private__.output = SAFE(function output(
    type,
    options
  ) {
    options = options || {};

    if (typeof options === "string") {
      options = {
        filename: options
      };
    } else {
      options.filename = options.filename || "generated.pdf";
    }

    switch (type) {
      case undefined:
        return buildDocument();
      case "save":
        API.save(options.filename);
        break;
      case "arraybuffer":
        return getArrayBuffer(buildDocument());
      case "blob":
        return getBlob(buildDocument());
      case "bloburi":
      case "bloburl":
        // Developer is responsible of calling revokeObjectURL
        if (
          typeof globalObject.URL !== "undefined" &&
          typeof globalObject.URL.createObjectURL === "function"
        ) {
          return (
            (globalObject.URL &&
              globalObject.URL.createObjectURL(getBlob(buildDocument()))) ||
            void 0
          );
        } else {
          console$1.warn(
            "bloburl is not supported by your system, because URL.createObjectURL is not supported by your browser."
          );
        }
        break;
      case "datauristring":
      case "dataurlstring":
        var dataURI = "";
        var pdfDocument = buildDocument();
        try {
          dataURI = btoa$1(pdfDocument);
        } catch (e) {
          dataURI = btoa$1(unescape(encodeURIComponent(pdfDocument)));
        }
        return (
          "data:application/pdf;filename=" +
          options.filename +
          ";base64," +
          dataURI
        );
      case "datauri":
      case "dataurl":
        return (globalObject.document.location.href = this.output(
          "datauristring",
          options
        ));
      default:
        return null;
    }
  }));

  /**
   * Used to see if a supplied hotfix was requested when the pdf instance was created.
   * @param {string} hotfixName - The name of the hotfix to check.
   * @returns {boolean}
   */
  var hasHotfix = function(hotfixName) {
    return (
      Array.isArray(hotfixes) === true && hotfixes.indexOf(hotfixName) > -1
    );
  };

  switch (unit) {
    case "pt":
      scaleFactor = 1;
      break;
    case "mm":
      scaleFactor = 72 / 25.4;
      break;
    case "cm":
      scaleFactor = 72 / 2.54;
      break;
    case "in":
      scaleFactor = 72;
      break;
    case "px":
      if (hasHotfix("px_scaling") == true) {
        scaleFactor = 72 / 96;
      } else {
        scaleFactor = 96 / 72;
      }
      break;
    case "pc":
      scaleFactor = 12;
      break;
    case "em":
      scaleFactor = 12;
      break;
    case "ex":
      scaleFactor = 6;
      break;
    default:
      if (typeof unit === "number") {
        scaleFactor = unit;
      } else {
        throw new Error("Invalid unit: " + unit);
      }
  }

  var encryption = null;
  setCreationDate();
  setFileId();

  var getEncryptor = function(objectId) {
    if (encryptionOptions !== null) {
      return encryption.encryptor(objectId, 0);
    }
    return function(data) {
      return data;
    };
  };

  //---------------------------------------
  // Public API

  var getPageInfo = (API.__private__.getPageInfo = API.getPageInfo = function(
    pageNumberOneBased
  ) {
    if (isNaN(pageNumberOneBased) || pageNumberOneBased % 1 !== 0) {
      throw new Error("Invalid argument passed to jsPDF.getPageInfo");
    }
    var objId = pagesContext[pageNumberOneBased].objId;
    return {
      objId: objId,
      pageNumber: pageNumberOneBased,
      pageContext: pagesContext[pageNumberOneBased]
    };
  });

  var getPageInfoByObjId = (API.__private__.getPageInfoByObjId = function(
    objId
  ) {
    if (isNaN(objId) || objId % 1 !== 0) {
      throw new Error("Invalid argument passed to jsPDF.getPageInfoByObjId");
    }
    for (var pageNumber in pagesContext) {
      if (pagesContext[pageNumber].objId === objId) {
        break;
      }
    }
    return getPageInfo(pageNumber);
  });

  var getCurrentPageInfo = (API.__private__.getCurrentPageInfo = API.getCurrentPageInfo = function() {
    return {
      objId: pagesContext[currentPage].objId,
      pageNumber: currentPage,
      pageContext: pagesContext[currentPage]
    };
  });

  /**
   * Adds (and transfers the focus to) new page to the PDF document.
   * @param format {String/Array} The format of the new page. Can be: <ul><li>a0 - a10</li><li>b0 - b10</li><li>c0 - c10</li><li>dl</li><li>letter</li><li>government-letter</li><li>legal</li><li>junior-legal</li><li>ledger</li><li>tabloid</li><li>credit-card</li></ul><br />
   * Default is "a4". If you want to use your own format just pass instead of one of the above predefined formats the size as an number-array, e.g. [595.28, 841.89]
   * @param orientation {string} Orientation of the new page. Possible values are "portrait" or "landscape" (or shortcuts "p" (Default), "l").
   * @function
   * @instance
   * @returns {jsPDF}
   *
   * @memberof jsPDF#
   * @name addPage
   */
  API.addPage = function() {
    _addPage.apply(this, arguments);
    return this;
  };
  /**
   * Adds (and transfers the focus to) new page to the PDF document.
   * @function
   * @instance
   * @returns {jsPDF}
   *
   * @memberof jsPDF#
   * @name setPage
   * @param {number} page Switch the active page to the page number specified (indexed starting at 1).
   * @example
   * doc = jsPDF()
   * doc.addPage()
   * doc.addPage()
   * doc.text('I am on page 3', 10, 10)
   * doc.setPage(1)
   * doc.text('I am on page 1', 10, 10)
   */
  API.setPage = function() {
    _setPage.apply(this, arguments);
    setOutputDestination.call(this, pages[currentPage]);
    return this;
  };

  /**
   * @name insertPage
   * @memberof jsPDF#
   *
   * @function
   * @instance
   * @param {Object} beforePage
   * @returns {jsPDF}
   */
  API.insertPage = function(beforePage) {
    this.addPage();
    this.movePage(currentPage, beforePage);
    return this;
  };

  /**
   * @name movePage
   * @memberof jsPDF#
   * @function
   * @instance
   * @param {number} targetPage
   * @param {number} beforePage
   * @returns {jsPDF}
   */
  API.movePage = function(targetPage, beforePage) {
    var tmpPages, tmpPagesContext;
    if (targetPage > beforePage) {
      tmpPages = pages[targetPage];
      tmpPagesContext = pagesContext[targetPage];
      for (var i = targetPage; i > beforePage; i--) {
        pages[i] = pages[i - 1];
        pagesContext[i] = pagesContext[i - 1];
      }
      pages[beforePage] = tmpPages;
      pagesContext[beforePage] = tmpPagesContext;
      this.setPage(beforePage);
    } else if (targetPage < beforePage) {
      tmpPages = pages[targetPage];
      tmpPagesContext = pagesContext[targetPage];
      for (var j = targetPage; j < beforePage; j++) {
        pages[j] = pages[j + 1];
        pagesContext[j] = pagesContext[j + 1];
      }
      pages[beforePage] = tmpPages;
      pagesContext[beforePage] = tmpPagesContext;
      this.setPage(beforePage);
    }
    return this;
  };

  /**
   * Deletes a page from the PDF.
   * @name deletePage
   * @memberof jsPDF#
   * @function
   * @param {number} targetPage
   * @instance
   * @returns {jsPDF}
   */
  API.deletePage = function() {
    _deletePage.apply(this, arguments);
    return this;
  };

  /**
   * Adds text to page. Supports adding multiline text when 'text' argument is an Array of Strings.
   *
   * @function
   * @instance
   * @param {String|Array} text String or array of strings to be added to the page. Each line is shifted one line down per font, spacing settings declared before this call.
   * @param {number} x Coordinate (in units declared at inception of PDF document) against left edge of the page.
   * @param {number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page.
   * @param {Object} [options] - Collection of settings signaling how the text must be encoded.
   * @param {string} [options.align=left] - The alignment of the text, possible values: left, center, right, justify.
   * @param {string} [options.baseline=alphabetic] - Sets text baseline used when drawing the text, possible values: alphabetic, ideographic, bottom, top, middle, hanging
   * @param {number|Matrix} [options.angle=0] - Rotate the text clockwise or counterclockwise. Expects the angle in degree.
   * @param {number} [options.rotationDirection=1] - Direction of the rotation. 0 = clockwise, 1 = counterclockwise.
   * @param {number} [options.charSpace=0] - The space between each letter.
   * @param {number} [options.horizontalScale=1] - Horizontal scale of the text as a factor of the regular size.
   * @param {number} [options.lineHeightFactor=1.15] - The lineheight of each line.
   * @param {Object} [options.flags] - Flags for to8bitStream.
   * @param {boolean} [options.flags.noBOM=true] - Don't add BOM to Unicode-text.
   * @param {boolean} [options.flags.autoencode=true] - Autoencode the Text.
   * @param {number} [options.maxWidth=0] - Split the text by given width, 0 = no split.
   * @param {string} [options.renderingMode=fill] - Set how the text should be rendered, possible values: fill, stroke, fillThenStroke, invisible, fillAndAddForClipping, strokeAndAddPathForClipping, fillThenStrokeAndAddToPathForClipping, addToPathForClipping.
   * @param {boolean} [options.isInputVisual] - Option for the BidiEngine
   * @param {boolean} [options.isOutputVisual] - Option for the BidiEngine
   * @param {boolean} [options.isInputRtl] - Option for the BidiEngine
   * @param {boolean} [options.isOutputRtl] - Option for the BidiEngine
   * @param {boolean} [options.isSymmetricSwapping] - Option for the BidiEngine
   * @param {number|Matrix} transform If transform is a number the text will be rotated by this value around the anchor set by x and y.
   *
   * If it is a Matrix, this matrix gets directly applied to the text, which allows shearing
   * effects etc.; the x and y offsets are then applied AFTER the coordinate system has been established by this
   * matrix. This means passing a rotation matrix that is equivalent to some rotation angle will in general yield a
   * DIFFERENT result. A matrix is only allowed in "advanced" API mode.
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name text
   */
  API.__private__.text = API.text = function(text, x, y, options, transform) {
    /*
     * Inserts something like this into PDF
     *   BT
     *    /F1 16 Tf  % Font name + size
     *    16 TL % How many units down for next line in multiline text
     *    0 g % color
     *    28.35 813.54 Td % position
     *    (line one) Tj
     *    T* (line two) Tj
     *    T* (line three) Tj
     *   ET
     */
    options = options || {};
    var scope = options.scope || this;
    var payload, da, angle, align, charSpace, maxWidth, flags, horizontalScale;

    // Pre-August-2012 the order of arguments was function(x, y, text, flags)
    // in effort to make all calls have similar signature like
    //   function(data, coordinates... , miscellaneous)
    // this method had its args flipped.
    // code below allows backward compatibility with old arg order.
    if (
      typeof text === "number" &&
      typeof x === "number" &&
      (typeof y === "string" || Array.isArray(y))
    ) {
      var tmp = y;
      y = x;
      x = text;
      text = tmp;
    }

    var transformationMatrix;

    if (arguments[3] instanceof Matrix === false) {
      flags = arguments[3];
      angle = arguments[4];
      align = arguments[5];

      if (typeof flags !== "object" || flags === null) {
        if (typeof angle === "string") {
          align = angle;
          angle = null;
        }
        if (typeof flags === "string") {
          align = flags;
          flags = null;
        }
        if (typeof flags === "number") {
          angle = flags;
          flags = null;
        }
        options = {
          flags: flags,
          angle: angle,
          align: align
        };
      }
    } else {
      advancedApiModeTrap(
        "The transform parameter of text() with a Matrix value"
      );
      transformationMatrix = transform;
    }

    if (isNaN(x) || isNaN(y) || typeof text === "undefined" || text === null) {
      throw new Error("Invalid arguments passed to jsPDF.text");
    }

    if (text.length === 0) {
      return scope;
    }

    var xtra = "";
    var isHex = false;
    var lineHeight =
      typeof options.lineHeightFactor === "number"
        ? options.lineHeightFactor
        : lineHeightFactor;
    var scaleFactor = scope.internal.scaleFactor;

    function ESC(s) {
      s = s.split("\t").join(Array(options.TabLen || 9).join(" "));
      return pdfEscape(s, flags);
    }

    function transformTextToSpecialArray(text) {
      //we don't want to destroy original text array, so cloning it
      var sa = text.concat();
      var da = [];
      var len = sa.length;
      var curDa;
      //we do array.join('text that must not be PDFescaped")
      //thus, pdfEscape each component separately
      while (len--) {
        curDa = sa.shift();
        if (typeof curDa === "string") {
          da.push(curDa);
        } else {
          if (
            Array.isArray(text) &&
            (curDa.length === 1 ||
              (curDa[1] === undefined && curDa[2] === undefined))
          ) {
            da.push(curDa[0]);
          } else {
            da.push([curDa[0], curDa[1], curDa[2]]);
          }
        }
      }
      return da;
    }

    function processTextByFunction(text, processingFunction) {
      var result;
      if (typeof text === "string") {
        result = processingFunction(text)[0];
      } else if (Array.isArray(text)) {
        //we don't want to destroy original text array, so cloning it
        var sa = text.concat();
        var da = [];
        var len = sa.length;
        var curDa;
        var tmpResult;
        //we do array.join('text that must not be PDFescaped")
        //thus, pdfEscape each component separately
        while (len--) {
          curDa = sa.shift();
          if (typeof curDa === "string") {
            da.push(processingFunction(curDa)[0]);
          } else if (Array.isArray(curDa) && typeof curDa[0] === "string") {
            tmpResult = processingFunction(curDa[0], curDa[1], curDa[2]);
            da.push([tmpResult[0], tmpResult[1], tmpResult[2]]);
          }
        }
        result = da;
      }
      return result;
    }

    //Check if text is of type String
    var textIsOfTypeString = false;
    var tmpTextIsOfTypeString = true;

    if (typeof text === "string") {
      textIsOfTypeString = true;
    } else if (Array.isArray(text)) {
      //we don't want to destroy original text array, so cloning it
      var sa = text.concat();
      da = [];
      var len = sa.length;
      var curDa;
      //we do array.join('text that must not be PDFescaped")
      //thus, pdfEscape each component separately
      while (len--) {
        curDa = sa.shift();
        if (
          typeof curDa !== "string" ||
          (Array.isArray(curDa) && typeof curDa[0] !== "string")
        ) {
          tmpTextIsOfTypeString = false;
        }
      }
      textIsOfTypeString = tmpTextIsOfTypeString;
    }
    if (textIsOfTypeString === false) {
      throw new Error(
        'Type of text must be string or Array. "' +
          text +
          '" is not recognized.'
      );
    }

    //If there are any newlines in text, we assume
    //the user wanted to print multiple lines, so break the
    //text up into an array. If the text is already an array,
    //we assume the user knows what they are doing.
    //Convert text into an array anyway to simplify
    //later code.

    if (typeof text === "string") {
      if (text.match(/[\r?\n]/)) {
        text = text.split(/\r\n|\r|\n/g);
      } else {
        text = [text];
      }
    }

    //baseline
    var height = activeFontSize / scope.internal.scaleFactor;
    var descent = height * (lineHeight - 1);

    switch (options.baseline) {
      case "bottom":
        y -= descent;
        break;
      case "top":
        y += height - descent;
        break;
      case "hanging":
        y += height - 2 * descent;
        break;
      case "middle":
        y += height / 2 - descent;
        break;
    }

    //multiline
    maxWidth = options.maxWidth || 0;

    if (maxWidth > 0) {
      if (typeof text === "string") {
        text = scope.splitTextToSize(text, maxWidth);
      } else if (Object.prototype.toString.call(text) === "[object Array]") {
        text = text.reduce(function(acc, textLine) {
          return acc.concat(scope.splitTextToSize(textLine, maxWidth));
        }, []);
      }
    }

    //creating Payload-Object to make text byRef
    payload = {
      text: text,
      x: x,
      y: y,
      options: options,
      mutex: {
        pdfEscape: pdfEscape,
        activeFontKey: activeFontKey,
        fonts: fonts,
        activeFontSize: activeFontSize
      }
    };
    events.publish("preProcessText", payload);

    text = payload.text;
    options = payload.options;

    //angle
    angle = options.angle;

    if (
      transformationMatrix instanceof Matrix === false &&
      angle &&
      typeof angle === "number"
    ) {
      angle *= Math.PI / 180;

      if (options.rotationDirection === 0) {
        angle = -angle;
      }

      if (apiMode === ApiMode.ADVANCED) {
        angle = -angle;
      }

      var c = Math.cos(angle);
      var s = Math.sin(angle);
      transformationMatrix = new Matrix(c, s, -s, c, 0, 0);
    } else if (angle && angle instanceof Matrix) {
      transformationMatrix = angle;
    }

    if (apiMode === ApiMode.ADVANCED && !transformationMatrix) {
      transformationMatrix = identityMatrix;
    }

    //charSpace

    charSpace = options.charSpace || activeCharSpace;

    if (typeof charSpace !== "undefined") {
      xtra += hpf(scale(charSpace)) + " Tc\n";
      this.setCharSpace(this.getCharSpace() || 0);
    }

    horizontalScale = options.horizontalScale;
    if (typeof horizontalScale !== "undefined") {
      xtra += hpf(horizontalScale * 100) + " Tz\n";
    }

    //lang

    options.lang;

    //renderingMode
    var renderingMode = -1;
    var parmRenderingMode =
      typeof options.renderingMode !== "undefined"
        ? options.renderingMode
        : options.stroke;
    var pageContext = scope.internal.getCurrentPageInfo().pageContext;

    switch (parmRenderingMode) {
      case 0:
      case false:
      case "fill":
        renderingMode = 0;
        break;
      case 1:
      case true:
      case "stroke":
        renderingMode = 1;
        break;
      case 2:
      case "fillThenStroke":
        renderingMode = 2;
        break;
      case 3:
      case "invisible":
        renderingMode = 3;
        break;
      case 4:
      case "fillAndAddForClipping":
        renderingMode = 4;
        break;
      case 5:
      case "strokeAndAddPathForClipping":
        renderingMode = 5;
        break;
      case 6:
      case "fillThenStrokeAndAddToPathForClipping":
        renderingMode = 6;
        break;
      case 7:
      case "addToPathForClipping":
        renderingMode = 7;
        break;
    }

    var usedRenderingMode =
      typeof pageContext.usedRenderingMode !== "undefined"
        ? pageContext.usedRenderingMode
        : -1;

    //if the coder wrote it explicitly to use a specific
    //renderingMode, then use it
    if (renderingMode !== -1) {
      xtra += renderingMode + " Tr\n";
      //otherwise check if we used the rendering Mode already
      //if so then set the rendering Mode...
    } else if (usedRenderingMode !== -1) {
      xtra += "0 Tr\n";
    }

    if (renderingMode !== -1) {
      pageContext.usedRenderingMode = renderingMode;
    }

    //align
    align = options.align || "left";
    var leading = activeFontSize * lineHeight;
    var pageWidth = scope.internal.pageSize.getWidth();
    var activeFont = fonts[activeFontKey];
    charSpace = options.charSpace || activeCharSpace;
    maxWidth = options.maxWidth || 0;

    var lineWidths;
    flags = Object.assign({ autoencode: true, noBOM: true }, options.flags);

    var wordSpacingPerLine = [];
    var findWidth = function(v) {
      return (
        (scope.getStringUnitWidth(v, {
          font: activeFont,
          charSpace: charSpace,
          fontSize: activeFontSize,
          doKerning: false
        }) *
          activeFontSize) /
        scaleFactor
      );
    };
    if (Object.prototype.toString.call(text) === "[object Array]") {
      da = transformTextToSpecialArray(text);
      var newY;
      if (align !== "left") {
        lineWidths = da.map(findWidth);
      }
      //The first line uses the "main" Td setting,
      //and the subsequent lines are offset by the
      //previous line's x coordinate.
      var prevWidth = 0;
      var newX;
      if (align === "right") {
        //The passed in x coordinate defines the
        //rightmost point of the text.
        x -= lineWidths[0];
        text = [];
        len = da.length;
        for (var i = 0; i < len; i++) {
          if (i === 0) {
            newX = getHorizontalCoordinate(x);
            newY = getVerticalCoordinate(y);
          } else {
            newX = scale(prevWidth - lineWidths[i]);
            newY = -leading;
          }
          text.push([da[i], newX, newY]);
          prevWidth = lineWidths[i];
        }
      } else if (align === "center") {
        //The passed in x coordinate defines
        //the center point.
        x -= lineWidths[0] / 2;
        text = [];
        len = da.length;
        for (var j = 0; j < len; j++) {
          if (j === 0) {
            newX = getHorizontalCoordinate(x);
            newY = getVerticalCoordinate(y);
          } else {
            newX = scale((prevWidth - lineWidths[j]) / 2);
            newY = -leading;
          }
          text.push([da[j], newX, newY]);
          prevWidth = lineWidths[j];
        }
      } else if (align === "left") {
        text = [];
        len = da.length;
        for (var h = 0; h < len; h++) {
          text.push(da[h]);
        }
      } else if (align === "justify" && activeFont.encoding === "Identity-H") {
        // when using unicode fonts, wordSpacePerLine does not apply
        text = [];
        len = da.length;
        maxWidth = maxWidth !== 0 ? maxWidth : pageWidth;
        let backToStartX = 0;
        for (var l = 0; l < len; l++) {
          newY = l === 0 ? getVerticalCoordinate(y) : -leading;
          newX = l === 0 ? getHorizontalCoordinate(x) : backToStartX;
          if (l < len - 1) {
            let spacing = scale(
              (maxWidth - lineWidths[l]) / (da[l].split(" ").length - 1)
            );
            let words = da[l].split(" ");
            text.push([words[0] + " ", newX, newY]);
            backToStartX = 0; // distance to reset back to the left
            for (let i = 1; i < words.length; i++) {
              let shiftAmount =
                (findWidth(words[i - 1] + " " + words[i]) -
                  findWidth(words[i])) *
                  scaleFactor +
                spacing;
              if (i == words.length - 1) text.push([words[i], shiftAmount, 0]);
              else text.push([words[i] + " ", shiftAmount, 0]);
              backToStartX -= shiftAmount;
            }
          } else {
            text.push([da[l], newX, newY]);
          }
        }
        text.push(["", backToStartX, 0]);
      } else if (align === "justify") {
        text = [];
        len = da.length;
        maxWidth = maxWidth !== 0 ? maxWidth : pageWidth;
        for (var l = 0; l < len; l++) {
          newY = l === 0 ? getVerticalCoordinate(y) : -leading;
          newX = l === 0 ? getHorizontalCoordinate(x) : 0;

          const numSpaces = da[l].split(" ").length - 1;
          const spacing =
            numSpaces > 0 ? (maxWidth - lineWidths[l]) / numSpaces : 0;

          if (l < len - 1) {
            wordSpacingPerLine.push(hpf(scale(spacing)));
          } else {
            wordSpacingPerLine.push(0);
          }
          text.push([da[l], newX, newY]);
        }
      } else {
        throw new Error(
          'Unrecognized alignment option, use "left", "center", "right" or "justify".'
        );
      }
    }

    //R2L
    var doReversing = typeof options.R2L === "boolean" ? options.R2L : R2L;
    if (doReversing === true) {
      text = processTextByFunction(text, function(text, posX, posY) {
        return [
          text
            .split("")
            .reverse()
            .join(""),
          posX,
          posY
        ];
      });
    }

    //creating Payload-Object to make text byRef
    payload = {
      text: text,
      x: x,
      y: y,
      options: options,
      mutex: {
        pdfEscape: pdfEscape,
        activeFontKey: activeFontKey,
        fonts: fonts,
        activeFontSize: activeFontSize
      }
    };
    events.publish("postProcessText", payload);

    text = payload.text;
    isHex = payload.mutex.isHex || false;

    //Escaping
    var activeFontEncoding = fonts[activeFontKey].encoding;

    if (
      activeFontEncoding === "WinAnsiEncoding" ||
      activeFontEncoding === "StandardEncoding"
    ) {
      text = processTextByFunction(text, function(text, posX, posY) {
        return [ESC(text), posX, posY];
      });
    }

    da = transformTextToSpecialArray(text);

    text = [];
    var STRING = 0;
    var ARRAY = 1;
    var variant = Array.isArray(da[0]) ? ARRAY : STRING;
    var posX;
    var posY;
    var content;
    var wordSpacing = "";

    var generatePosition = function(
      parmPosX,
      parmPosY,
      parmTransformationMatrix
    ) {
      var position = "";
      if (parmTransformationMatrix instanceof Matrix) {
        // It is kind of more intuitive to apply a plain rotation around the text anchor set by x and y
        // but when the user supplies an arbitrary transformation matrix, the x and y offsets should be applied
        // in the coordinate system established by this matrix
        if (typeof options.angle === "number") {
          parmTransformationMatrix = matrixMult(
            parmTransformationMatrix,
            new Matrix(1, 0, 0, 1, parmPosX, parmPosY)
          );
        } else {
          parmTransformationMatrix = matrixMult(
            new Matrix(1, 0, 0, 1, parmPosX, parmPosY),
            parmTransformationMatrix
          );
        }

        if (apiMode === ApiMode.ADVANCED) {
          parmTransformationMatrix = matrixMult(
            new Matrix(1, 0, 0, -1, 0, 0),
            parmTransformationMatrix
          );
        }

        position = parmTransformationMatrix.join(" ") + " Tm\n";
      } else {
        position = hpf(parmPosX) + " " + hpf(parmPosY) + " Td\n";
      }
      return position;
    };

    for (var lineIndex = 0; lineIndex < da.length; lineIndex++) {
      wordSpacing = "";

      switch (variant) {
        case ARRAY:
          content =
            (isHex ? "<" : "(") + da[lineIndex][0] + (isHex ? ">" : ")");
          posX = parseFloat(da[lineIndex][1]);
          posY = parseFloat(da[lineIndex][2]);
          break;
        case STRING:
          content = (isHex ? "<" : "(") + da[lineIndex] + (isHex ? ">" : ")");
          posX = getHorizontalCoordinate(x);
          posY = getVerticalCoordinate(y);
          break;
      }

      if (
        typeof wordSpacingPerLine !== "undefined" &&
        typeof wordSpacingPerLine[lineIndex] !== "undefined"
      ) {
        wordSpacing = wordSpacingPerLine[lineIndex] + " Tw\n";
      }

      if (lineIndex === 0) {
        text.push(
          wordSpacing +
            generatePosition(posX, posY, transformationMatrix) +
            content
        );
      } else if (variant === STRING) {
        text.push(wordSpacing + content);
      } else if (variant === ARRAY) {
        text.push(
          wordSpacing +
            generatePosition(posX, posY, transformationMatrix) +
            content
        );
      }
    }

    text = variant === STRING ? text.join(" Tj\nT* ") : text.join(" Tj\n");
    text += " Tj\n";

    var result = "BT\n/";
    result += activeFontKey + " " + activeFontSize + " Tf\n"; // font face, style, size
    result += hpf(activeFontSize * lineHeight) + " TL\n"; // line spacing
    result += textColor + "\n";
    result += xtra;
    result += text;
    result += "ET";

    out(result);
    usedFonts[activeFontKey] = true;
    return scope;
  };

  // PDF supports these path painting and clip path operators:
  //
  // S - stroke
  // s - close/stroke
  // f (F) - fill non-zero
  // f* - fill evenodd
  // B - fill stroke nonzero
  // B* - fill stroke evenodd
  // b - close fill stroke nonzero
  // b* - close fill stroke evenodd
  // n - nothing (consume path)
  // W - clip nonzero
  // W* - clip evenodd
  //
  // In order to keep the API small, we omit the close-and-fill/stroke operators and provide a separate close()
  // method.
  /**
   *
   * @name clip
   * @function
   * @instance
   * @param {string} rule Only possible value is 'evenodd'
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @description All .clip() after calling drawing ops with a style argument of null.
   */
  var clip = (API.__private__.clip = API.clip = function(rule) {
    // Call .clip() after calling drawing ops with a style argument of null
    // W is the PDF clipping op
    if ("evenodd" === rule) {
      out("W*");
    } else {
      out("W");
    }
    return this;
  });

  /**
   * @name clipEvenOdd
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @description Modify the current clip path by intersecting it with the current path using the even-odd rule. Note
   * that this will NOT consume the current path. In order to only use this path for clipping call
   * {@link API.discardPath} afterwards.
   */
  API.clipEvenOdd = function() {
    return clip("evenodd");
  };

  /**
   * Consumes the current path without any effect. Mainly used in combination with {@link clip} or
   * {@link clipEvenOdd}. The PDF "n" operator.
   * @name discardPath
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   */
  API.__private__.discardPath = API.discardPath = function() {
    out("n");
    return this;
  };

  var isValidStyle = (API.__private__.isValidStyle = function(style) {
    var validStyleVariants = [
      undefined,
      null,
      "S",
      "D",
      "F",
      "DF",
      "FD",
      "f",
      "f*",
      "B",
      "B*",
      "n"
    ];
    var result = false;
    if (validStyleVariants.indexOf(style) !== -1) {
      result = true;
    }
    return result;
  });

  API.__private__.setDefaultPathOperation = API.setDefaultPathOperation = function(
    operator
  ) {
    if (isValidStyle(operator)) {
      defaultPathOperation = operator;
    }
    return this;
  };

  var getStyle = (API.__private__.getStyle = API.getStyle = function(style) {
    // see path-painting operators in PDF spec
    var op = defaultPathOperation; // stroke

    switch (style) {
      case "D":
      case "S":
        op = "S"; // stroke
        break;
      case "F":
        op = "f"; // fill
        break;
      case "FD":
      case "DF":
        op = "B";
        break;
      case "f":
      case "f*":
      case "B":
      case "B*":
        /*
               Allow direct use of these PDF path-painting operators:
               - f    fill using nonzero winding number rule
               - f*    fill using even-odd rule
               - B    fill then stroke with fill using non-zero winding number rule
               - B*    fill then stroke with fill using even-odd rule
               */
        op = style;
        break;
    }
    return op;
  });

  /**
   * Close the current path. The PDF "h" operator.
   * @name close
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   */
  var close = (API.close = function() {
    out("h");
    return this;
  });

  /**
   * Stroke the path. The PDF "S" operator.
   * @name stroke
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   */
  API.stroke = function() {
    out("S");
    return this;
  };

  /**
   * Fill the current path using the nonzero winding number rule. If a pattern is provided, the path will be filled
   * with this pattern, otherwise with the current fill color. Equivalent to the PDF "f" operator.
   * @name fill
   * @function
   * @instance
   * @param {PatternData=} pattern If provided the path will be filled with this pattern
   * @returns {jsPDF}
   * @memberof jsPDF#
   */
  API.fill = function(pattern) {
    fillWithOptionalPattern("f", pattern);
    return this;
  };

  /**
   * Fill the current path using the even-odd rule. The PDF f* operator.
   * @see API.fill
   * @name fillEvenOdd
   * @function
   * @instance
   * @param {PatternData=} pattern If provided the path will be filled with this pattern
   * @returns {jsPDF}
   * @memberof jsPDF#
   */
  API.fillEvenOdd = function(pattern) {
    fillWithOptionalPattern("f*", pattern);
    return this;
  };

  /**
   * Fill using the nonzero winding number rule and then stroke the current Path. The PDF "B" operator.
   * @see API.fill
   * @name fillStroke
   * @function
   * @instance
   * @param {PatternData=} pattern If provided the path will be stroked with this pattern
   * @returns {jsPDF}
   * @memberof jsPDF#
   */
  API.fillStroke = function(pattern) {
    fillWithOptionalPattern("B", pattern);
    return this;
  };

  /**
   * Fill using the even-odd rule and then stroke the current Path. The PDF "B" operator.
   * @see API.fill
   * @name fillStrokeEvenOdd
   * @function
   * @instance
   * @param {PatternData=} pattern If provided the path will be fill-stroked with this pattern
   * @returns {jsPDF}
   * @memberof jsPDF#
   */
  API.fillStrokeEvenOdd = function(pattern) {
    fillWithOptionalPattern("B*", pattern);
    return this;
  };

  var fillWithOptionalPattern = function(style, pattern) {
    if (typeof pattern === "object") {
      fillWithPattern(pattern, style);
    } else {
      out(style);
    }
  };

  var putStyle = function(style) {
    if (
      style === null ||
      (apiMode === ApiMode.ADVANCED && style === undefined)
    ) {
      return;
    }

    style = getStyle(style);

    // stroking / filling / both the path
    out(style);
  };

  function cloneTilingPattern(patternKey, boundingBox, xStep, yStep, matrix) {
    var clone = new TilingPattern(
      boundingBox || this.boundingBox,
      xStep || this.xStep,
      yStep || this.yStep,
      this.gState,
      matrix || this.matrix
    );
    clone.stream = this.stream;
    var key = patternKey + "$$" + this.cloneIndex++ + "$$";
    addPattern(key, clone);
    return clone;
  }

  var fillWithPattern = function(patternData, style) {
    var patternId = patternMap[patternData.key];
    var pattern = patterns[patternId];

    if (pattern instanceof ShadingPattern) {
      out("q");

      out(clipRuleFromStyle(style));

      if (pattern.gState) {
        API.setGState(pattern.gState);
      }
      out(patternData.matrix.toString() + " cm");
      out("/" + patternId + " sh");
      out("Q");
    } else if (pattern instanceof TilingPattern) {
      // pdf draws patterns starting at the bottom left corner and they are not affected by the global transformation,
      // so we must flip them
      var matrix = new Matrix(1, 0, 0, -1, 0, getPageHeight());

      if (patternData.matrix) {
        matrix = matrix.multiply(patternData.matrix || identityMatrix);
        // we cannot apply a matrix to the pattern on use so we must abuse the pattern matrix and create new instances
        // for each use
        patternId = cloneTilingPattern.call(
          pattern,
          patternData.key,
          patternData.boundingBox,
          patternData.xStep,
          patternData.yStep,
          matrix
        ).id;
      }

      out("q");
      out("/Pattern cs");
      out("/" + patternId + " scn");

      if (pattern.gState) {
        API.setGState(pattern.gState);
      }

      out(style);
      out("Q");
    }
  };

  var clipRuleFromStyle = function(style) {
    switch (style) {
      case "f":
      case "F":
        return "W n";
      case "f*":
        return "W* n";
      case "B":
        return "W S";
      case "B*":
        return "W* S";

      // these two are for compatibility reasons (in the past, calling any primitive method with a shading pattern
      // and "n"/"S" as style would still fill/fill and stroke the path)
      case "S":
        return "W S";
      case "n":
        return "W n";
    }
  };

  /**
   * Begin a new subpath by moving the current point to coordinates (x, y). The PDF "m" operator.
   * @param {number} x
   * @param {number} y
   * @name moveTo
   * @function
   * @instance
   * @memberof jsPDF#
   * @returns {jsPDF}
   */
  var moveTo = (API.moveTo = function(x, y) {
    out(hpf(scale(x)) + " " + hpf(transformScaleY(y)) + " m");
    return this;
  });

  /**
   * Append a straight line segment from the current point to the point (x, y). The PDF "l" operator.
   * @param {number} x
   * @param {number} y
   * @memberof jsPDF#
   * @name lineTo
   * @function
   * @instance
   * @memberof jsPDF#
   * @returns {jsPDF}
   */
  var lineTo = (API.lineTo = function(x, y) {
    out(hpf(scale(x)) + " " + hpf(transformScaleY(y)) + " l");
    return this;
  });

  /**
   * Append a cubic Bézier curve to the current path. The curve shall extend from the current point to the point
   * (x3, y3), using (x1, y1) and (x2, y2) as Bézier control points. The new current point shall be (x3, x3).
   * @param {number} x1
   * @param {number} y1
   * @param {number} x2
   * @param {number} y2
   * @param {number} x3
   * @param {number} y3
   * @memberof jsPDF#
   * @name curveTo
   * @function
   * @instance
   * @memberof jsPDF#
   * @returns {jsPDF}
   */
  var curveTo = (API.curveTo = function(x1, y1, x2, y2, x3, y3) {
    out(
      [
        hpf(scale(x1)),
        hpf(transformScaleY(y1)),
        hpf(scale(x2)),
        hpf(transformScaleY(y2)),
        hpf(scale(x3)),
        hpf(transformScaleY(y3)),
        "c"
      ].join(" ")
    );
    return this;
  });

  /**
   * Draw a line on the current page.
   *
   * @name line
   * @function
   * @instance
   * @param {number} x1
   * @param {number} y1
   * @param {number} x2
   * @param {number} y2
   * @param {string} style A string specifying the painting style or null.  Valid styles include: 'S' [default] - stroke, 'F' - fill,  and 'DF' (or 'FD') -  fill then stroke. A null value postpones setting the style so that a shape may be composed using multiple method calls. The last drawing method call used to define the shape should not have a null style argument. default: 'S'
   * @returns {jsPDF}
   * @memberof jsPDF#
   */
  API.__private__.line = API.line = function(x1, y1, x2, y2, style) {
    if (
      isNaN(x1) ||
      isNaN(y1) ||
      isNaN(x2) ||
      isNaN(y2) ||
      !isValidStyle(style)
    ) {
      throw new Error("Invalid arguments passed to jsPDF.line");
    }
    if (apiMode === ApiMode.COMPAT) {
      return this.lines([[x2 - x1, y2 - y1]], x1, y1, [1, 1], style || "S");
    } else {
      return this.lines([[x2 - x1, y2 - y1]], x1, y1, [1, 1]).stroke();
    }
  };

  /**
   * @typedef {Object} PatternData
   * {Matrix|undefined} matrix
   * {Number|undefined} xStep
   * {Number|undefined} yStep
   * {Array.<Number>|undefined} boundingBox
   */

  /**
   * Adds series of curves (straight lines or cubic bezier curves) to canvas, starting at `x`, `y` coordinates.
   * All data points in `lines` are relative to last line origin.
   * `x`, `y` become x1,y1 for first line / curve in the set.
   * For lines you only need to specify [x2, y2] - (ending point) vector against x1, y1 starting point.
   * For bezier curves you need to specify [x2,y2,x3,y3,x4,y4] - vectors to control points 1, 2, ending point. All vectors are against the start of the curve - x1,y1.
   *
   * @example .lines([[2,2],[-2,2],[1,1,2,2,3,3],[2,1]], 212,110, [1,1], 'F', false) // line, line, bezier curve, line
   * @param {Array} lines Array of *vector* shifts as pairs (lines) or sextets (cubic bezier curves).
   * @param {number} x Coordinate (in units declared at inception of PDF document) against left edge of the page
   * @param {number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page
   * @param {number} scale (Defaults to [1.0,1.0]) x,y Scaling factor for all vectors. Elements can be any floating number Sub-one makes drawing smaller. Over-one grows the drawing. Negative flips the direction.
   * @param {string=} style A string specifying the painting style or null. Valid styles include:
   * 'S' [default] - stroke,
   * 'F' - fill,
   * and 'DF' (or 'FD') -  fill then stroke.
   * In "compat" API mode, a null value postpones setting the style so that a shape may be composed using multiple
   * method calls. The last drawing method call used to define the shape should not have a null style argument.
   *
   * In "advanced" API mode this parameter is deprecated.
   * @param {Boolean=} closed If true, the path is closed with a straight line from the end of the last curve to the starting point.
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name lines
   */
  API.__private__.lines = API.lines = function(
    lines,
    x,
    y,
    scale,
    style,
    closed
  ) {
    var scalex, scaley, i, l, leg, x2, y2, x3, y3, x4, y4, tmp;

    // Pre-August-2012 the order of arguments was function(x, y, lines, scale, style)
    // in effort to make all calls have similar signature like
    //   function(content, coordinateX, coordinateY , miscellaneous)
    // this method had its args flipped.
    // code below allows backward compatibility with old arg order.
    if (typeof lines === "number") {
      tmp = y;
      y = x;
      x = lines;
      lines = tmp;
    }

    scale = scale || [1, 1];
    closed = closed || false;

    if (
      isNaN(x) ||
      isNaN(y) ||
      !Array.isArray(lines) ||
      !Array.isArray(scale) ||
      !isValidStyle(style) ||
      typeof closed !== "boolean"
    ) {
      throw new Error("Invalid arguments passed to jsPDF.lines");
    }

    // starting point
    moveTo(x, y);

    scalex = scale[0];
    scaley = scale[1];
    l = lines.length;
    //, x2, y2 // bezier only. In page default measurement "units", *after* scaling
    //, x3, y3 // bezier only. In page default measurement "units", *after* scaling
    // ending point for all, lines and bezier. . In page default measurement "units", *after* scaling
    x4 = x; // last / ending point = starting point for first item.
    y4 = y; // last / ending point = starting point for first item.

    for (i = 0; i < l; i++) {
      leg = lines[i];
      if (leg.length === 2) {
        // simple line
        x4 = leg[0] * scalex + x4; // here last x4 was prior ending point
        y4 = leg[1] * scaley + y4; // here last y4 was prior ending point
        lineTo(x4, y4);
      } else {
        // bezier curve
        x2 = leg[0] * scalex + x4; // here last x4 is prior ending point
        y2 = leg[1] * scaley + y4; // here last y4 is prior ending point
        x3 = leg[2] * scalex + x4; // here last x4 is prior ending point
        y3 = leg[3] * scaley + y4; // here last y4 is prior ending point
        x4 = leg[4] * scalex + x4; // here last x4 was prior ending point
        y4 = leg[5] * scaley + y4; // here last y4 was prior ending point
        curveTo(x2, y2, x3, y3, x4, y4);
      }
    }

    if (closed) {
      close();
    }

    putStyle(style);
    return this;
  };

  /**
   * Similar to {@link API.lines} but all coordinates are interpreted as absolute coordinates instead of relative.
   * @param {Array<Object>} lines An array of {op: operator, c: coordinates} object, where op is one of "m" (move to), "l" (line to)
   * "c" (cubic bezier curve) and "h" (close (sub)path)). c is an array of coordinates. "m" and "l" expect two, "c"
   * six and "h" an empty array (or undefined).
   * @function
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name path
   */
  API.path = function(lines) {
    for (var i = 0; i < lines.length; i++) {
      var leg = lines[i];
      var coords = leg.c;
      switch (leg.op) {
        case "m":
          moveTo(coords[0], coords[1]);
          break;
        case "l":
          lineTo(coords[0], coords[1]);
          break;
        case "c":
          curveTo.apply(this, coords);
          break;
        case "h":
          close();
          break;
      }
    }

    return this;
  };

  /**
   * Adds a rectangle to PDF.
   *
   * @param {number} x Coordinate (in units declared at inception of PDF document) against left edge of the page
   * @param {number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page
   * @param {number} w Width (in units declared at inception of PDF document)
   * @param {number} h Height (in units declared at inception of PDF document)
   * @param {string=} style A string specifying the painting style or null. Valid styles include:
   * 'S' [default] - stroke,
   * 'F' - fill,
   * and 'DF' (or 'FD') -  fill then stroke.
   * In "compat" API mode, a null value postpones setting the style so that a shape may be composed using multiple
   * method calls. The last drawing method call used to define the shape should not have a null style argument.
   *
   * In "advanced" API mode this parameter is deprecated.
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name rect
   */
  API.__private__.rect = API.rect = function(x, y, w, h, style) {
    if (isNaN(x) || isNaN(y) || isNaN(w) || isNaN(h) || !isValidStyle(style)) {
      throw new Error("Invalid arguments passed to jsPDF.rect");
    }
    if (apiMode === ApiMode.COMPAT) {
      h = -h;
    }

    out(
      [
        hpf(scale(x)),
        hpf(transformScaleY(y)),
        hpf(scale(w)),
        hpf(scale(h)),
        "re"
      ].join(" ")
    );

    putStyle(style);
    return this;
  };

  /**
   * Adds a triangle to PDF.
   *
   * @param {number} x1 Coordinate (in units declared at inception of PDF document) against left edge of the page
   * @param {number} y1 Coordinate (in units declared at inception of PDF document) against upper edge of the page
   * @param {number} x2 Coordinate (in units declared at inception of PDF document) against left edge of the page
   * @param {number} y2 Coordinate (in units declared at inception of PDF document) against upper edge of the page
   * @param {number} x3 Coordinate (in units declared at inception of PDF document) against left edge of the page
   * @param {number} y3 Coordinate (in units declared at inception of PDF document) against upper edge of the page
   * @param {string=} style A string specifying the painting style or null. Valid styles include:
   * 'S' [default] - stroke,
   * 'F' - fill,
   * and 'DF' (or 'FD') -  fill then stroke.
   * In "compat" API mode, a null value postpones setting the style so that a shape may be composed using multiple
   * method calls. The last drawing method call used to define the shape should not have a null style argument.
   *
   * In "advanced" API mode this parameter is deprecated.
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name triangle
   */
  API.__private__.triangle = API.triangle = function(
    x1,
    y1,
    x2,
    y2,
    x3,
    y3,
    style
  ) {
    if (
      isNaN(x1) ||
      isNaN(y1) ||
      isNaN(x2) ||
      isNaN(y2) ||
      isNaN(x3) ||
      isNaN(y3) ||
      !isValidStyle(style)
    ) {
      throw new Error("Invalid arguments passed to jsPDF.triangle");
    }
    this.lines(
      [
        [x2 - x1, y2 - y1], // vector to point 2
        [x3 - x2, y3 - y2], // vector to point 3
        [x1 - x3, y1 - y3] // closing vector back to point 1
      ],
      x1,
      y1, // start of path
      [1, 1],
      style,
      true
    );
    return this;
  };

  /**
   * Adds a rectangle with rounded corners to PDF.
   *
   * @param {number} x Coordinate (in units declared at inception of PDF document) against left edge of the page
   * @param {number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page
   * @param {number} w Width (in units declared at inception of PDF document)
   * @param {number} h Height (in units declared at inception of PDF document)
   * @param {number} rx Radius along x axis (in units declared at inception of PDF document)
   * @param {number} ry Radius along y axis (in units declared at inception of PDF document)
   * @param {string=} style A string specifying the painting style or null. Valid styles include:
   * 'S' [default] - stroke,
   * 'F' - fill,
   * and 'DF' (or 'FD') -  fill then stroke.
   * In "compat" API mode, a null value postpones setting the style so that a shape may be composed using multiple
   * method calls. The last drawing method call used to define the shape should not have a null style argument.
   *
   * In "advanced" API mode this parameter is deprecated.
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name roundedRect
   */
  API.__private__.roundedRect = API.roundedRect = function(
    x,
    y,
    w,
    h,
    rx,
    ry,
    style
  ) {
    if (
      isNaN(x) ||
      isNaN(y) ||
      isNaN(w) ||
      isNaN(h) ||
      isNaN(rx) ||
      isNaN(ry) ||
      !isValidStyle(style)
    ) {
      throw new Error("Invalid arguments passed to jsPDF.roundedRect");
    }
    var MyArc = (4 / 3) * (Math.SQRT2 - 1);

    rx = Math.min(rx, w * 0.5);
    ry = Math.min(ry, h * 0.5);

    this.lines(
      [
        [w - 2 * rx, 0],
        [rx * MyArc, 0, rx, ry - ry * MyArc, rx, ry],
        [0, h - 2 * ry],
        [0, ry * MyArc, -(rx * MyArc), ry, -rx, ry],
        [-w + 2 * rx, 0],
        [-(rx * MyArc), 0, -rx, -(ry * MyArc), -rx, -ry],
        [0, -h + 2 * ry],
        [0, -(ry * MyArc), rx * MyArc, -ry, rx, -ry]
      ],
      x + rx,
      y, // start of path
      [1, 1],
      style,
      true
    );
    return this;
  };

  /**
   * Adds an ellipse to PDF.
   *
   * @param {number} x Coordinate (in units declared at inception of PDF document) against left edge of the page
   * @param {number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page
   * @param {number} rx Radius along x axis (in units declared at inception of PDF document)
   * @param {number} ry Radius along y axis (in units declared at inception of PDF document)
   * @param {string=} style A string specifying the painting style or null. Valid styles include:
   * 'S' [default] - stroke,
   * 'F' - fill,
   * and 'DF' (or 'FD') -  fill then stroke.
   * In "compat" API mode, a null value postpones setting the style so that a shape may be composed using multiple
   * method calls. The last drawing method call used to define the shape should not have a null style argument.
   *
   * In "advanced" API mode this parameter is deprecated.
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name ellipse
   */
  API.__private__.ellipse = API.ellipse = function(x, y, rx, ry, style) {
    if (
      isNaN(x) ||
      isNaN(y) ||
      isNaN(rx) ||
      isNaN(ry) ||
      !isValidStyle(style)
    ) {
      throw new Error("Invalid arguments passed to jsPDF.ellipse");
    }
    var lx = (4 / 3) * (Math.SQRT2 - 1) * rx,
      ly = (4 / 3) * (Math.SQRT2 - 1) * ry;

    moveTo(x + rx, y);
    curveTo(x + rx, y - ly, x + lx, y - ry, x, y - ry);
    curveTo(x - lx, y - ry, x - rx, y - ly, x - rx, y);
    curveTo(x - rx, y + ly, x - lx, y + ry, x, y + ry);
    curveTo(x + lx, y + ry, x + rx, y + ly, x + rx, y);

    putStyle(style);
    return this;
  };

  /**
   * Adds an circle to PDF.
   *
   * @param {number} x Coordinate (in units declared at inception of PDF document) against left edge of the page
   * @param {number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page
   * @param {number} r Radius (in units declared at inception of PDF document)
   * @param {string=} style A string specifying the painting style or null. Valid styles include:
   * 'S' [default] - stroke,
   * 'F' - fill,
   * and 'DF' (or 'FD') -  fill then stroke.
   * In "compat" API mode, a null value postpones setting the style so that a shape may be composed using multiple
   * method calls. The last drawing method call used to define the shape should not have a null style argument.
   *
   * In "advanced" API mode this parameter is deprecated.
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name circle
   */
  API.__private__.circle = API.circle = function(x, y, r, style) {
    if (isNaN(x) || isNaN(y) || isNaN(r) || !isValidStyle(style)) {
      throw new Error("Invalid arguments passed to jsPDF.circle");
    }
    return this.ellipse(x, y, r, r, style);
  };

  /**
   * Sets text font face, variant for upcoming text elements.
   * See output of jsPDF.getFontList() for possible font names, styles.
   *
   * @param {string} fontName Font name or family. Example: "times".
   * @param {string} fontStyle Font style or variant. Example: "italic".
   * @param {number | string} fontWeight Weight of the Font. Example: "normal" | 400
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setFont
   */
  API.setFont = function(fontName, fontStyle, fontWeight) {
    if (fontWeight) {
      fontStyle = combineFontStyleAndFontWeight(fontStyle, fontWeight);
    }
    activeFontKey = getFont(fontName, fontStyle, {
      disableWarning: false
    });
    return this;
  };

  /**
   * Gets text font face, variant for upcoming text elements.
   *
   * @function
   * @instance
   * @returns {Object}
   * @memberof jsPDF#
   * @name getFont
   */
  var getFontEntry = (API.__private__.getFont = API.getFont = function() {
    return fonts[getFont.apply(API, arguments)];
  });

  /**
   * Returns an object - a tree of fontName to fontStyle relationships available to
   * active PDF document.
   *
   * @public
   * @function
   * @instance
   * @returns {Object} Like {'times':['normal', 'italic', ... ], 'arial':['normal', 'bold', ... ], ... }
   * @memberof jsPDF#
   * @name getFontList
   */
  API.__private__.getFontList = API.getFontList = function() {
    var list = {},
      fontName,
      fontStyle;

    for (fontName in fontmap) {
      if (fontmap.hasOwnProperty(fontName)) {
        list[fontName] = [];
        for (fontStyle in fontmap[fontName]) {
          if (fontmap[fontName].hasOwnProperty(fontStyle)) {
            list[fontName].push(fontStyle);
          }
        }
      }
    }
    return list;
  };

  /**
   * Add a custom font to the current instance.
   *
   * @param {string} postScriptName PDF specification full name for the font.
   * @param {string} id PDF-document-instance-specific label assinged to the font.
   * @param {string} fontStyle Style of the Font.
   * @param {number | string} fontWeight Weight of the Font.
   * @param {Object} encoding Encoding_name-to-Font_metrics_object mapping.
   * @function
   * @instance
   * @memberof jsPDF#
   * @name addFont
   * @returns {string} fontId
   */
  API.addFont = function(
    postScriptName,
    fontName,
    fontStyle,
    fontWeight,
    encoding
  ) {
    var encodingOptions = [
      "StandardEncoding",
      "MacRomanEncoding",
      "Identity-H",
      "WinAnsiEncoding"
    ];
    if (arguments[3] && encodingOptions.indexOf(arguments[3]) !== -1) {
      //IE 11 fix
      encoding = arguments[3];
    } else if (arguments[3] && encodingOptions.indexOf(arguments[3]) == -1) {
      fontStyle = combineFontStyleAndFontWeight(fontStyle, fontWeight);
    }
    encoding = encoding || "Identity-H";
    return addFont.call(this, postScriptName, fontName, fontStyle, encoding);
  };

  var lineWidth = options.lineWidth || 0.200025; // 2mm
  /**
   * Gets the line width, default: 0.200025.
   *
   * @function
   * @instance
   * @returns {number} lineWidth
   * @memberof jsPDF#
   * @name getLineWidth
   */
  var getLineWidth = (API.__private__.getLineWidth = API.getLineWidth = function() {
    return lineWidth;
  });

  /**
   * Sets line width for upcoming lines.
   *
   * @param {number} width Line width (in units declared at inception of PDF document).
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setLineWidth
   */
  var setLineWidth = (API.__private__.setLineWidth = API.setLineWidth = function(
    width
  ) {
    lineWidth = width;
    out(hpf(scale(width)) + " w");
    return this;
  });

  /**
   * Sets the dash pattern for upcoming lines.
   *
   * To reset the settings simply call the method without any parameters.
   * @param {Array<number>} dashArray An array containing 0-2 numbers. The first number sets the length of the
   * dashes, the second number the length of the gaps. If the second number is missing, the gaps are considered
   * to be as long as the dashes. An empty array means solid, unbroken lines.
   * @param {number} dashPhase The phase lines start with.
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setLineDashPattern
   */
  API.__private__.setLineDash = jsPDF.API.setLineDash = jsPDF.API.setLineDashPattern = function(
    dashArray,
    dashPhase
  ) {
    dashArray = dashArray || [];
    dashPhase = dashPhase || 0;

    if (isNaN(dashPhase) || !Array.isArray(dashArray)) {
      throw new Error("Invalid arguments passed to jsPDF.setLineDash");
    }

    dashArray = dashArray
      .map(function(x) {
        return hpf(scale(x));
      })
      .join(" ");
    dashPhase = hpf(scale(dashPhase));

    out("[" + dashArray + "] " + dashPhase + " d");
    return this;
  };

  var lineHeightFactor;

  var getLineHeight = (API.__private__.getLineHeight = API.getLineHeight = function() {
    return activeFontSize * lineHeightFactor;
  });

  API.__private__.getLineHeight = API.getLineHeight = function() {
    return activeFontSize * lineHeightFactor;
  };

  /**
   * Sets the LineHeightFactor of proportion.
   *
   * @param {number} value LineHeightFactor value. Default: 1.15.
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setLineHeightFactor
   */
  var setLineHeightFactor = (API.__private__.setLineHeightFactor = API.setLineHeightFactor = function(
    value
  ) {
    value = value || 1.15;
    if (typeof value === "number") {
      lineHeightFactor = value;
    }
    return this;
  });

  /**
   * Gets the LineHeightFactor, default: 1.15.
   *
   * @function
   * @instance
   * @returns {number} lineHeightFactor
   * @memberof jsPDF#
   * @name getLineHeightFactor
   */
  var getLineHeightFactor = (API.__private__.getLineHeightFactor = API.getLineHeightFactor = function() {
    return lineHeightFactor;
  });

  setLineHeightFactor(options.lineHeight);

  var getHorizontalCoordinate = (API.__private__.getHorizontalCoordinate = function(
    value
  ) {
    return scale(value);
  });

  var getVerticalCoordinate = (API.__private__.getVerticalCoordinate = function(
    value
  ) {
    if (apiMode === ApiMode.ADVANCED) {
      return value;
    } else {
      var pageHeight =
        pagesContext[currentPage].mediaBox.topRightY -
        pagesContext[currentPage].mediaBox.bottomLeftY;
      return pageHeight - scale(value);
    }
  });

  var getHorizontalCoordinateString = (API.__private__.getHorizontalCoordinateString = API.getHorizontalCoordinateString = function(
    value
  ) {
    return hpf(getHorizontalCoordinate(value));
  });

  var getVerticalCoordinateString = (API.__private__.getVerticalCoordinateString = API.getVerticalCoordinateString = function(
    value
  ) {
    return hpf(getVerticalCoordinate(value));
  });

  var strokeColor = options.strokeColor || "0 G";

  /**
   *  Gets the stroke color for upcoming elements.
   *
   * @function
   * @instance
   * @returns {string} colorAsHex
   * @memberof jsPDF#
   * @name getDrawColor
   */
  API.__private__.getStrokeColor = API.getDrawColor = function() {
    return decodeColorString(strokeColor);
  };

  /**
   * Sets the stroke color for upcoming elements.
   *
   * Depending on the number of arguments given, Gray, RGB, or CMYK
   * color space is implied.
   *
   * When only ch1 is given, "Gray" color space is implied and it
   * must be a value in the range from 0.00 (solid black) to to 1.00 (white)
   * if values are communicated as String types, or in range from 0 (black)
   * to 255 (white) if communicated as Number type.
   * The RGB-like 0-255 range is provided for backward compatibility.
   *
   * When only ch1,ch2,ch3 are given, "RGB" color space is implied and each
   * value must be in the range from 0.00 (minimum intensity) to to 1.00
   * (max intensity) if values are communicated as String types, or
   * from 0 (min intensity) to to 255 (max intensity) if values are communicated
   * as Number types.
   * The RGB-like 0-255 range is provided for backward compatibility.
   *
   * When ch1,ch2,ch3,ch4 are given, "CMYK" color space is implied and each
   * value must be a in the range from 0.00 (0% concentration) to to
   * 1.00 (100% concentration)
   *
   * Because JavaScript treats fixed point numbers badly (rounds to
   * floating point nearest to binary representation) it is highly advised to
   * communicate the fractional numbers as String types, not JavaScript Number type.
   *
   * @param {Number|String} ch1 Color channel value or {string} ch1 color value in hexadecimal, example: '#FFFFFF'.
   * @param {Number} ch2 Color channel value.
   * @param {Number} ch3 Color channel value.
   * @param {Number} ch4 Color channel value.
   *
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setDrawColor
   */
  API.__private__.setStrokeColor = API.setDrawColor = function(
    ch1,
    ch2,
    ch3,
    ch4
  ) {
    var options = {
      ch1: ch1,
      ch2: ch2,
      ch3: ch3,
      ch4: ch4,
      pdfColorType: "draw",
      precision: 2
    };

    strokeColor = encodeColorString(options);
    out(strokeColor);
    return this;
  };

  var fillColor = options.fillColor || "0 g";

  /**
   * Gets the fill color for upcoming elements.
   *
   * @function
   * @instance
   * @returns {string} colorAsHex
   * @memberof jsPDF#
   * @name getFillColor
   */
  API.__private__.getFillColor = API.getFillColor = function() {
    return decodeColorString(fillColor);
  };

  /**
   * Sets the fill color for upcoming elements.
   *
   * Depending on the number of arguments given, Gray, RGB, or CMYK
   * color space is implied.
   *
   * When only ch1 is given, "Gray" color space is implied and it
   * must be a value in the range from 0.00 (solid black) to to 1.00 (white)
   * if values are communicated as String types, or in range from 0 (black)
   * to 255 (white) if communicated as Number type.
   * The RGB-like 0-255 range is provided for backward compatibility.
   *
   * When only ch1,ch2,ch3 are given, "RGB" color space is implied and each
   * value must be in the range from 0.00 (minimum intensity) to to 1.00
   * (max intensity) if values are communicated as String types, or
   * from 0 (min intensity) to to 255 (max intensity) if values are communicated
   * as Number types.
   * The RGB-like 0-255 range is provided for backward compatibility.
   *
   * When ch1,ch2,ch3,ch4 are given, "CMYK" color space is implied and each
   * value must be a in the range from 0.00 (0% concentration) to to
   * 1.00 (100% concentration)
   *
   * Because JavaScript treats fixed point numbers badly (rounds to
   * floating point nearest to binary representation) it is highly advised to
   * communicate the fractional numbers as String types, not JavaScript Number type.
   *
   * @param {Number|String} ch1 Color channel value or {string} ch1 color value in hexadecimal, example: '#FFFFFF'.
   * @param {Number} ch2 Color channel value.
   * @param {Number} ch3 Color channel value.
   * @param {Number} ch4 Color channel value.
   *
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setFillColor
   */
  API.__private__.setFillColor = API.setFillColor = function(
    ch1,
    ch2,
    ch3,
    ch4
  ) {
    var options = {
      ch1: ch1,
      ch2: ch2,
      ch3: ch3,
      ch4: ch4,
      pdfColorType: "fill",
      precision: 2
    };

    fillColor = encodeColorString(options);
    out(fillColor);
    return this;
  };

  var textColor = options.textColor || "0 g";
  /**
   * Gets the text color for upcoming elements.
   *
   * @function
   * @instance
   * @returns {string} colorAsHex
   * @memberof jsPDF#
   * @name getTextColor
   */
  var getTextColor = (API.__private__.getTextColor = API.getTextColor = function() {
    return decodeColorString(textColor);
  });
  /**
   * Sets the text color for upcoming elements.
   *
   * Depending on the number of arguments given, Gray, RGB, or CMYK
   * color space is implied.
   *
   * When only ch1 is given, "Gray" color space is implied and it
   * must be a value in the range from 0.00 (solid black) to to 1.00 (white)
   * if values are communicated as String types, or in range from 0 (black)
   * to 255 (white) if communicated as Number type.
   * The RGB-like 0-255 range is provided for backward compatibility.
   *
   * When only ch1,ch2,ch3 are given, "RGB" color space is implied and each
   * value must be in the range from 0.00 (minimum intensity) to to 1.00
   * (max intensity) if values are communicated as String types, or
   * from 0 (min intensity) to to 255 (max intensity) if values are communicated
   * as Number types.
   * The RGB-like 0-255 range is provided for backward compatibility.
   *
   * When ch1,ch2,ch3,ch4 are given, "CMYK" color space is implied and each
   * value must be a in the range from 0.00 (0% concentration) to to
   * 1.00 (100% concentration)
   *
   * Because JavaScript treats fixed point numbers badly (rounds to
   * floating point nearest to binary representation) it is highly advised to
   * communicate the fractional numbers as String types, not JavaScript Number type.
   *
   * @param {Number|String} ch1 Color channel value or {string} ch1 color value in hexadecimal, example: '#FFFFFF'.
   * @param {Number} ch2 Color channel value.
   * @param {Number} ch3 Color channel value.
   * @param {Number} ch4 Color channel value.
   *
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setTextColor
   */
  API.__private__.setTextColor = API.setTextColor = function(
    ch1,
    ch2,
    ch3,
    ch4
  ) {
    var options = {
      ch1: ch1,
      ch2: ch2,
      ch3: ch3,
      ch4: ch4,
      pdfColorType: "text",
      precision: 3
    };
    textColor = encodeColorString(options);

    return this;
  };

  var activeCharSpace = options.charSpace;

  /**
   * Get global value of CharSpace.
   *
   * @function
   * @instance
   * @returns {number} charSpace
   * @memberof jsPDF#
   * @name getCharSpace
   */
  var getCharSpace = (API.__private__.getCharSpace = API.getCharSpace = function() {
    return parseFloat(activeCharSpace || 0);
  });

  /**
   * Set global value of CharSpace.
   *
   * @param {number} charSpace
   * @function
   * @instance
   * @returns {jsPDF} jsPDF-instance
   * @memberof jsPDF#
   * @name setCharSpace
   */
  API.__private__.setCharSpace = API.setCharSpace = function(charSpace) {
    if (isNaN(charSpace)) {
      throw new Error("Invalid argument passed to jsPDF.setCharSpace");
    }
    activeCharSpace = charSpace;
    return this;
  };

  var lineCapID = 0;
  /**
   * Is an Object providing a mapping from human-readable to
   * integer flag values designating the varieties of line cap
   * and join styles.
   *
   * @memberof jsPDF#
   * @name CapJoinStyles
   */
  API.CapJoinStyles = {
    0: 0,
    butt: 0,
    but: 0,
    miter: 0,
    1: 1,
    round: 1,
    rounded: 1,
    circle: 1,
    2: 2,
    projecting: 2,
    project: 2,
    square: 2,
    bevel: 2
  };

  /**
   * Sets the line cap styles.
   * See {jsPDF.CapJoinStyles} for variants.
   *
   * @param {String|Number} style A string or number identifying the type of line cap.
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setLineCap
   */
  API.__private__.setLineCap = API.setLineCap = function(style) {
    var id = API.CapJoinStyles[style];
    if (id === undefined) {
      throw new Error(
        "Line cap style of '" +
          style +
          "' is not recognized. See or extend .CapJoinStyles property for valid styles"
      );
    }
    lineCapID = id;
    out(id + " J");

    return this;
  };

  var lineJoinID = 0;
  /**
   * Sets the line join styles.
   * See {jsPDF.CapJoinStyles} for variants.
   *
   * @param {String|Number} style A string or number identifying the type of line join.
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setLineJoin
   */
  API.__private__.setLineJoin = API.setLineJoin = function(style) {
    var id = API.CapJoinStyles[style];
    if (id === undefined) {
      throw new Error(
        "Line join style of '" +
          style +
          "' is not recognized. See or extend .CapJoinStyles property for valid styles"
      );
    }
    lineJoinID = id;
    out(id + " j");

    return this;
  };
  /**
   * Sets the miterLimit property, which effects the maximum miter length.
   *
   * @param {number} length The length of the miter
   * @function
   * @instance
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setLineMiterLimit
   */
  API.__private__.setLineMiterLimit = API.__private__.setMiterLimit = API.setLineMiterLimit = API.setMiterLimit = function(
    length
  ) {
    length = length || 0;
    if (isNaN(length)) {
      throw new Error("Invalid argument passed to jsPDF.setLineMiterLimit");
    }
    out(hpf(scale(length)) + " M");

    return this;
  };

  /**
   * An object representing a pdf graphics state.
   * @class GState
   */

  /**
   *
   * @param parameters A parameter object that contains all properties this graphics state wants to set.
   * Supported are: opacity, stroke-opacity
   * @constructor
   */
  API.GState = GState;

  /**
   * Sets a either previously added {@link GState} (via {@link addGState}) or a new {@link GState}.
   * @param {String|GState} gState If type is string, a previously added GState is used, if type is GState
   * it will be added before use.
   * @function
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setGState
   */
  API.setGState = function(gState) {
    if (typeof gState === "string") {
      gState = gStates[gStatesMap[gState]];
    } else {
      gState = addGState(null, gState);
    }

    if (!gState.equals(activeGState)) {
      out("/" + gState.id + " gs");
      activeGState = gState;
    }
  };

  /**
   * Adds a new Graphics State. Duplicates are automatically eliminated.
   * @param {String} key Might also be null, if no later reference to this gState is needed
   * @param {Object} gState The gState object
   */
  var addGState = function(key, gState) {
    // only add it if it is not already present (the keys provided by the user must be unique!)
    if (key && gStatesMap[key]) return;
    var duplicate = false;
    for (var s in gStates) {
      if (gStates.hasOwnProperty(s)) {
        if (gStates[s].equals(gState)) {
          duplicate = true;
          break;
        }
      }
    }

    if (duplicate) {
      gState = gStates[s];
    } else {
      var gStateKey = "GS" + (Object.keys(gStates).length + 1).toString(10);
      gStates[gStateKey] = gState;
      gState.id = gStateKey;
    }

    // several user keys may point to the same GState object
    key && (gStatesMap[key] = gState.id);

    events.publish("addGState", gState);

    return gState;
  };

  /**
   * Adds a new {@link GState} for later use. See {@link setGState}.
   * @param {String} key
   * @param {GState} gState
   * @function
   * @instance
   * @returns {jsPDF}
   *
   * @memberof jsPDF#
   * @name addGState
   */
  API.addGState = function(key, gState) {
    addGState(key, gState);
    return this;
  };

  /**
   * Saves the current graphics state ("pushes it on the stack"). It can be restored by {@link restoreGraphicsState}
   * later. Here, the general pdf graphics state is meant, also including the current transformation matrix,
   * fill and stroke colors etc.
   * @function
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name saveGraphicsState
   */
  API.saveGraphicsState = function() {
    out("q");
    // as we cannot set font key and size independently we must keep track of both
    fontStateStack.push({
      key: activeFontKey,
      size: activeFontSize,
      color: textColor
    });
    return this;
  };

  /**
   * Restores a previously saved graphics state saved by {@link saveGraphicsState} ("pops the stack").
   * @function
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name restoreGraphicsState
   */
  API.restoreGraphicsState = function() {
    out("Q");

    // restore previous font state
    var fontState = fontStateStack.pop();
    activeFontKey = fontState.key;
    activeFontSize = fontState.size;
    textColor = fontState.color;

    activeGState = null;

    return this;
  };

  /**
   * Appends this matrix to the left of all previously applied matrices.
   *
   * @param {Matrix} matrix
   * @function
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name setCurrentTransformationMatrix
   */
  API.setCurrentTransformationMatrix = function(matrix) {
    out(matrix.toString() + " cm");
    return this;
  };

  /**
   * Inserts a debug comment into the generated pdf.
   * @function
   * @instance
   * @param {String} text
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name comment
   */
  API.comment = function(text) {
    out("#" + text);
    return this;
  };

  /**
   * Point
   */
  var Point = function(x, y) {
    var _x = x || 0;
    Object.defineProperty(this, "x", {
      enumerable: true,
      get: function() {
        return _x;
      },
      set: function(value) {
        if (!isNaN(value)) {
          _x = parseFloat(value);
        }
      }
    });

    var _y = y || 0;
    Object.defineProperty(this, "y", {
      enumerable: true,
      get: function() {
        return _y;
      },
      set: function(value) {
        if (!isNaN(value)) {
          _y = parseFloat(value);
        }
      }
    });

    var _type = "pt";
    Object.defineProperty(this, "type", {
      enumerable: true,
      get: function() {
        return _type;
      },
      set: function(value) {
        _type = value.toString();
      }
    });
    return this;
  };

  /**
   * Rectangle
   */
  var Rectangle = function(x, y, w, h) {
    Point.call(this, x, y);
    this.type = "rect";

    var _w = w || 0;
    Object.defineProperty(this, "w", {
      enumerable: true,
      get: function() {
        return _w;
      },
      set: function(value) {
        if (!isNaN(value)) {
          _w = parseFloat(value);
        }
      }
    });

    var _h = h || 0;
    Object.defineProperty(this, "h", {
      enumerable: true,
      get: function() {
        return _h;
      },
      set: function(value) {
        if (!isNaN(value)) {
          _h = parseFloat(value);
        }
      }
    });

    return this;
  };

  /**
   * FormObject/RenderTarget
   */

  var RenderTarget = function() {
    this.page = page;
    this.currentPage = currentPage;
    this.pages = pages.slice(0);
    this.pagesContext = pagesContext.slice(0);
    this.x = pageX;
    this.y = pageY;
    this.matrix = pageMatrix;
    this.width = getUnscaledPageWidth(currentPage);
    this.height = getUnscaledPageHeight(currentPage);
    this.outputDestination = outputDestination;

    this.id = ""; // set by endFormObject()
    this.objectNumber = -1; // will be set by putXObject()
  };

  RenderTarget.prototype.restore = function() {
    page = this.page;
    currentPage = this.currentPage;
    pagesContext = this.pagesContext;
    pages = this.pages;
    pageX = this.x;
    pageY = this.y;
    pageMatrix = this.matrix;
    setPageWidthWithoutScaling(currentPage, this.width);
    setPageHeightWithoutScaling(currentPage, this.height);
    outputDestination = this.outputDestination;
  };

  var beginNewRenderTarget = function(x, y, width, height, matrix) {
    // save current state
    renderTargetStack.push(new RenderTarget());

    // clear pages
    page = currentPage = 0;
    pages = [];
    pageX = x;
    pageY = y;

    pageMatrix = matrix;

    beginPage([width, height]);
  };

  var endFormObject = function(key) {
    // only add it if it is not already present (the keys provided by the user must be unique!)
    if (renderTargetMap[key]) {
      renderTargetStack.pop().restore();
      return;
    }

    // save the created xObject
    var newXObject = new RenderTarget();

    var xObjectId = "Xo" + (Object.keys(renderTargets).length + 1).toString(10);
    newXObject.id = xObjectId;

    renderTargetMap[key] = xObjectId;
    renderTargets[xObjectId] = newXObject;

    events.publish("addFormObject", newXObject);

    // restore state from stack
    renderTargetStack.pop().restore();
  };

  /**
   * Starts a new pdf form object, which means that all consequent draw calls target a new independent object
   * until {@link endFormObject} is called. The created object can be referenced and drawn later using
   * {@link doFormObject}. Nested form objects are possible.
   * x, y, width, height set the bounding box that is used to clip the content.
   *
   * @param {number} x
   * @param {number} y
   * @param {number} width
   * @param {number} height
   * @param {Matrix} matrix The matrix that will be applied to convert the form objects coordinate system to
   * the parent's.
   * @function
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name beginFormObject
   */
  API.beginFormObject = function(x, y, width, height, matrix) {
    // The user can set the output target to a new form object. Nested form objects are possible.
    // Currently, they use the resource dictionary of the surrounding stream. This should be changed, as
    // the PDF-Spec states:
    // "In PDF 1.2 and later versions, form XObjects may be independent of the content streams in which
    // they appear, and this is strongly recommended although not requiredIn PDF 1.2 and later versions,
    // form XObjects may be independent of the content streams in which they appear, and this is strongly
    // recommended although not required"
    beginNewRenderTarget(x, y, width, height, matrix);
    return this;
  };

  /**
   * Completes and saves the form object.
   * @param {String} key The key by which this form object can be referenced.
   * @function
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name endFormObject
   */
  API.endFormObject = function(key) {
    endFormObject(key);
    return this;
  };

  /**
   * Draws the specified form object by referencing to the respective pdf XObject created with
   * {@link API.beginFormObject} and {@link endFormObject}.
   * The location is determined by matrix.
   *
   * @param {String} key The key to the form object.
   * @param {Matrix} matrix The matrix applied before drawing the form object.
   * @function
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name doFormObject
   */
  API.doFormObject = function(key, matrix) {
    var xObject = renderTargets[renderTargetMap[key]];
    out("q");
    out(matrix.toString() + " cm");
    out("/" + xObject.id + " Do");
    out("Q");
    return this;
  };

  /**
   * Returns the form object specified by key.
   * @param key {String}
   * @returns {{x: number, y: number, width: number, height: number, matrix: Matrix}}
   * @function
   * @returns {jsPDF}
   * @memberof jsPDF#
   * @name getFormObject
   */
  API.getFormObject = function(key) {
    var xObject = renderTargets[renderTargetMap[key]];
    return {
      x: xObject.x,
      y: xObject.y,
      width: xObject.width,
      height: xObject.height,
      matrix: xObject.matrix
    };
  };

  /**
   * Saves as PDF document. An alias of jsPDF.output('save', 'filename.pdf').
   * Uses FileSaver.js-method saveAs.
   *
   * @memberof jsPDF#
   * @name save
   * @function
   * @instance
   * @param  {string} filename The filename including extension.
   * @param  {Object} options An Object with additional options, possible options: 'returnPromise'.
   * @returns {jsPDF|Promise} jsPDF-instance     */
  API.save = function(filename, options) {

    options = options || {};
    options.returnPromise = options.returnPromise || false;

    return Promise.reject(Error('save function removed'));
  };

  // applying plugins (more methods) ON TOP of built-in API.
  // this is intentional as we allow plugins to override
  // built-ins
  for (var plugin in jsPDF.API) {
    if (jsPDF.API.hasOwnProperty(plugin)) {
      if (plugin === "events" && jsPDF.API.events.length) {
        (function(events, newEvents) {
          // jsPDF.API.events is a JS Array of Arrays
          // where each Array is a pair of event name, handler
          // Events were added by plugins to the jsPDF instantiator.
          // These are always added to the new instance and some ran
          // during instantiation.
          var eventname, handler_and_args, i;

          for (i = newEvents.length - 1; i !== -1; i--) {
            // subscribe takes 3 args: 'topic', function, runonce_flag
            // if undefined, runonce is false.
            // users can attach callback directly,
            // or they can attach an array with [callback, runonce_flag]
            // that's what the "apply" magic is for below.
            eventname = newEvents[i][0];
            handler_and_args = newEvents[i][1];
            events.subscribe.apply(
              events,
              [eventname].concat(
                typeof handler_and_args === "function"
                  ? [handler_and_args]
                  : handler_and_args
              )
            );
          }
        })(events, jsPDF.API.events);
      } else {
        API[plugin] = jsPDF.API[plugin];
      }
    }
  }

  function getUnscaledPageWidth(pageNumber) {
    return (
      pagesContext[pageNumber].mediaBox.topRightX -
      pagesContext[pageNumber].mediaBox.bottomLeftX
    );
  }

  function setPageWidthWithoutScaling(pageNumber, value) {
    pagesContext[pageNumber].mediaBox.topRightX =
      value + pagesContext[pageNumber].mediaBox.bottomLeftX;
  }

  function getUnscaledPageHeight(pageNumber) {
    return (
      pagesContext[pageNumber].mediaBox.topRightY -
      pagesContext[pageNumber].mediaBox.bottomLeftY
    );
  }

  function setPageHeightWithoutScaling(pageNumber, value) {
    pagesContext[pageNumber].mediaBox.topRightY =
      value + pagesContext[pageNumber].mediaBox.bottomLeftY;
  }

  var getPageWidth = (API.getPageWidth = function(pageNumber) {
    pageNumber = pageNumber || currentPage;
    return getUnscaledPageWidth(pageNumber) / scaleFactor;
  });

  var setPageWidth = (API.setPageWidth = function(pageNumber, value) {
    setPageWidthWithoutScaling(pageNumber, value * scaleFactor);
  });

  var getPageHeight = (API.getPageHeight = function(pageNumber) {
    pageNumber = pageNumber || currentPage;
    return getUnscaledPageHeight(pageNumber) / scaleFactor;
  });

  var setPageHeight = (API.setPageHeight = function(pageNumber, value) {
    setPageHeightWithoutScaling(pageNumber, value * scaleFactor);
  });

  /**
   * Object exposing internal API to plugins
   * @public
   * @ignore
   */
  API.internal = {
    pdfEscape: pdfEscape,
    getStyle: getStyle,
    getFont: getFontEntry,
    getFontSize: getFontSize,
    getCharSpace: getCharSpace,
    getTextColor: getTextColor,
    getLineHeight: getLineHeight,
    getLineHeightFactor: getLineHeightFactor,
    getLineWidth: getLineWidth,
    write: write,
    getHorizontalCoordinate: getHorizontalCoordinate,
    getVerticalCoordinate: getVerticalCoordinate,
    getCoordinateString: getHorizontalCoordinateString,
    getVerticalCoordinateString: getVerticalCoordinateString,
    collections: {},
    newObject: newObject,
    newAdditionalObject: newAdditionalObject,
    newObjectDeferred: newObjectDeferred,
    newObjectDeferredBegin: newObjectDeferredBegin,
    getFilters: getFilters,
    putStream: putStream,
    events: events,
    scaleFactor: scaleFactor,
    pageSize: {
      getWidth: function() {
        return getPageWidth(currentPage);
      },
      setWidth: function(value) {
        setPageWidth(currentPage, value);
      },
      getHeight: function() {
        return getPageHeight(currentPage);
      },
      setHeight: function(value) {
        setPageHeight(currentPage, value);
      }
    },
    encryptionOptions: encryptionOptions,
    encryption: encryption,
    getEncryptor: getEncryptor,
    output: output,
    getNumberOfPages: getNumberOfPages,
    pages: pages,
    out: out,
    f2: f2,
    f3: f3,
    getPageInfo: getPageInfo,
    getPageInfoByObjId: getPageInfoByObjId,
    getCurrentPageInfo: getCurrentPageInfo,
    getPDFVersion: getPdfVersion,
    Point: Point,
    Rectangle: Rectangle,
    Matrix: Matrix,
    hasHotfix: hasHotfix //Expose the hasHotfix check so plugins can also check them.
  };

  Object.defineProperty(API.internal.pageSize, "width", {
    get: function() {
      return getPageWidth(currentPage);
    },
    set: function(value) {
      setPageWidth(currentPage, value);
    },
    enumerable: true,
    configurable: true
  });
  Object.defineProperty(API.internal.pageSize, "height", {
    get: function() {
      return getPageHeight(currentPage);
    },
    set: function(value) {
      setPageHeight(currentPage, value);
    },
    enumerable: true,
    configurable: true
  });

  //////////////////////////////////////////////////////
  // continuing initialization of jsPDF Document object
  //////////////////////////////////////////////////////
  // Add the first page automatically
  addFonts.call(API, standardFonts);
  activeFontKey = "F1";
  _addPage(format, orientation);

  events.publish("initialized");
  return API;
}

/**
 * jsPDF.API is a STATIC property of jsPDF class.
 * jsPDF.API is an object you can add methods and properties to.
 * The methods / properties you add will show up in new jsPDF objects.
 *
 * One property is prepopulated. It is the 'events' Object. Plugin authors can add topics,
 * callbacks to this object. These will be reassigned to all new instances of jsPDF.
 *
 * @static
 * @public
 * @memberof jsPDF#
 * @name API
 *
 * @example
 * jsPDF.API.mymethod = function(){
 *   // 'this' will be ref to internal API object. see jsPDF source
 *   // , so you can refer to built-in methods like so:
 *   //     this.line(....)
 *   //     this.text(....)
 * }
 * var pdfdoc = new jsPDF()
 * pdfdoc.mymethod() // <- !!!!!!
 */
jsPDF.API = {
  events: []
};
/**
 * The version of jsPDF.
 * @name version
 * @type {string}
 * @memberof jsPDF#
 */
jsPDF.version = "3.0.3";

/* global jsPDF */

var jsPDFAPI = jsPDF.API;
var scaleFactor = 1;

var pdfEscape = function(value) {
  return value
    .replace(/\\/g, "\\\\")
    .replace(/\(/g, "\\(")
    .replace(/\)/g, "\\)");
};
var pdfUnescape = function(value) {
  return value
    .replace(/\\\\/g, "\\")
    .replace(/\\\(/g, "(")
    .replace(/\\\)/g, ")");
};

var f2 = function(number) {
  return number.toFixed(2); // Ie, %.2f
};

var f5 = function(number) {
  return number.toFixed(5); // Ie, %.2f
};

jsPDFAPI.__acroform__ = {};
var inherit = function(child, parent) {
  child.prototype = Object.create(parent.prototype);
  child.prototype.constructor = child;
};

var scale = function(x) {
  return x * scaleFactor;
};

var createFormXObject = function(formObject) {
  var xobj = new AcroFormXObject();
  var height = AcroFormAppearance.internal.getHeight(formObject) || 0;
  var width = AcroFormAppearance.internal.getWidth(formObject) || 0;
  xobj.BBox = [0, 0, Number(f2(width)), Number(f2(height))];
  return xobj;
};

/**
 * Bit-Operations
 */
var setBit = (jsPDFAPI.__acroform__.setBit = function(number, bitPosition) {
  number = number || 0;
  bitPosition = bitPosition || 0;

  if (isNaN(number) || isNaN(bitPosition)) {
    throw new Error(
      "Invalid arguments passed to jsPDF.API.__acroform__.setBit"
    );
  }
  var bitMask = 1 << bitPosition;

  number |= bitMask;

  return number;
});

var clearBit = (jsPDFAPI.__acroform__.clearBit = function(number, bitPosition) {
  number = number || 0;
  bitPosition = bitPosition || 0;

  if (isNaN(number) || isNaN(bitPosition)) {
    throw new Error(
      "Invalid arguments passed to jsPDF.API.__acroform__.clearBit"
    );
  }
  var bitMask = 1 << bitPosition;

  number &= ~bitMask;

  return number;
});

var getBit = (jsPDFAPI.__acroform__.getBit = function(number, bitPosition) {
  if (isNaN(number) || isNaN(bitPosition)) {
    throw new Error(
      "Invalid arguments passed to jsPDF.API.__acroform__.getBit"
    );
  }
  return (number & (1 << bitPosition)) === 0 ? 0 : 1;
});

/*
 * Ff starts counting the bit position at 1 and not like javascript at 0
 */
var getBitForPdf = (jsPDFAPI.__acroform__.getBitForPdf = function(
  number,
  bitPosition
) {
  if (isNaN(number) || isNaN(bitPosition)) {
    throw new Error(
      "Invalid arguments passed to jsPDF.API.__acroform__.getBitForPdf"
    );
  }
  return getBit(number, bitPosition - 1);
});

var setBitForPdf = (jsPDFAPI.__acroform__.setBitForPdf = function(
  number,
  bitPosition
) {
  if (isNaN(number) || isNaN(bitPosition)) {
    throw new Error(
      "Invalid arguments passed to jsPDF.API.__acroform__.setBitForPdf"
    );
  }
  return setBit(number, bitPosition - 1);
});

var clearBitForPdf = (jsPDFAPI.__acroform__.clearBitForPdf = function(
  number,
  bitPosition
) {
  if (isNaN(number) || isNaN(bitPosition)) {
    throw new Error(
      "Invalid arguments passed to jsPDF.API.__acroform__.clearBitForPdf"
    );
  }
  return clearBit(number, bitPosition - 1);
});

var calculateCoordinates = (jsPDFAPI.__acroform__.calculateCoordinates = function(
  args,
  scope
) {
  var getHorizontalCoordinate = scope.internal.getHorizontalCoordinate;
  var getVerticalCoordinate = scope.internal.getVerticalCoordinate;
  var x = args[0];
  var y = args[1];
  var w = args[2];
  var h = args[3];

  var coordinates = {};

  coordinates.lowerLeft_X = getHorizontalCoordinate(x) || 0;
  coordinates.lowerLeft_Y = getVerticalCoordinate(y + h) || 0;
  coordinates.upperRight_X = getHorizontalCoordinate(x + w) || 0;
  coordinates.upperRight_Y = getVerticalCoordinate(y) || 0;

  return [
    Number(f2(coordinates.lowerLeft_X)),
    Number(f2(coordinates.lowerLeft_Y)),
    Number(f2(coordinates.upperRight_X)),
    Number(f2(coordinates.upperRight_Y))
  ];
});

var calculateAppearanceStream = function(formObject) {
  if (formObject.appearanceStreamContent) {
    return formObject.appearanceStreamContent;
  }

  if (!formObject.V && !formObject.DV) {
    return;
  }

  // else calculate it

  var stream = [];
  var text = formObject._V || formObject.DV;
  var calcRes = calculateX(formObject, text);
  var fontKey = formObject.scope.internal.getFont(
    formObject.fontName,
    formObject.fontStyle
  ).id;

  //PDF 32000-1:2008, page 444
  stream.push("/Tx BMC");
  stream.push("q");
  stream.push("BT"); // Begin Text
  stream.push(formObject.scope.__private__.encodeColorString(formObject.color));
  stream.push("/" + fontKey + " " + f2(calcRes.fontSize) + " Tf");
  stream.push("1 0 0 1 0 0 Tm"); // Transformation Matrix
  stream.push(calcRes.text);
  stream.push("ET"); // End Text
  stream.push("Q");
  stream.push("EMC");

  var appearanceStreamContent = createFormXObject(formObject);
  appearanceStreamContent.scope = formObject.scope;
  appearanceStreamContent.stream = stream.join("\n");
  return appearanceStreamContent;
};

var calculateX = function(formObject, text) {
  var maxFontSize =
    formObject.fontSize === 0 ? formObject.maxFontSize : formObject.fontSize;
  var returnValue = {
    text: "",
    fontSize: ""
  };
  // Remove Brackets
  text = text.substr(0, 1) == "(" ? text.substr(1) : text;
  text =
    text.substr(text.length - 1) == ")"
      ? text.substr(0, text.length - 1)
      : text;
  // split into array of words
  var textSplit = text.split(" ");
  if (formObject.multiline) {
    textSplit = textSplit.map(word => word.split("\n"));
  } else {
    textSplit = textSplit.map(word => [word]);
  }

  var fontSize = maxFontSize; // The Starting fontSize (The Maximum)
  var lineSpacing = 2;
  var borderPadding = 2;

  var height = AcroFormAppearance.internal.getHeight(formObject) || 0;
  height = height < 0 ? -height : height;
  var width = AcroFormAppearance.internal.getWidth(formObject) || 0;
  width = width < 0 ? -width : width;

  var isSmallerThanWidth = function(i, lastLine, fontSize) {
    if (i + 1 < textSplit.length) {
      var tmp = lastLine + " " + textSplit[i + 1][0];
      var TextWidth = calculateFontSpace(tmp, formObject, fontSize).width;
      var FieldWidth = width - 2 * borderPadding;
      return TextWidth <= FieldWidth;
    } else {
      return false;
    }
  };

  fontSize++;
  FontSize: while (fontSize > 0) {
    text = "";
    fontSize--;
    var textHeight = calculateFontSpace("3", formObject, fontSize).height;
    var startY = formObject.multiline
      ? height - fontSize
      : (height - textHeight) / 2;
    startY += lineSpacing;
    var startX;

    var lastY = startY;
    var firstWordInLine = 0,
      lastWordInLine = 0;
    var lastLength;
    var currWord = 0;

    if (fontSize <= 0) {
      // In case, the Text doesn't fit at all
      fontSize = 12;
      text = "(...) Tj\n";
      text +=
        "% Width of Text: " +
        calculateFontSpace(text, formObject, fontSize).width +
        ", FieldWidth:" +
        width +
        "\n";
      break;
    }

    var lastLine = "";
    var lineCount = 0;
    Line: for (var i = 0; i < textSplit.length; i++) {
      if (textSplit.hasOwnProperty(i)) {
        let isWithNewLine = false;
        if (textSplit[i].length !== 1 && currWord !== textSplit[i].length - 1) {
          if (
            (textHeight + lineSpacing) * (lineCount + 2) + lineSpacing >
            height
          ) {
            continue FontSize;
          }

          lastLine += textSplit[i][currWord];
          isWithNewLine = true;
          lastWordInLine = i;
          i--;
        } else {
          lastLine += textSplit[i][currWord] + " ";
          lastLine =
            lastLine.substr(lastLine.length - 1) == " "
              ? lastLine.substr(0, lastLine.length - 1)
              : lastLine;
          var key = parseInt(i);
          var nextLineIsSmaller = isSmallerThanWidth(key, lastLine, fontSize);
          var isLastWord = i >= textSplit.length - 1;

          if (nextLineIsSmaller && !isLastWord) {
            lastLine += " ";
            currWord = 0;
            continue; // Line
          } else if (!nextLineIsSmaller && !isLastWord) {
            if (!formObject.multiline) {
              continue FontSize;
            } else {
              if (
                (textHeight + lineSpacing) * (lineCount + 2) + lineSpacing >
                height
              ) {
                // If the Text is higher than the
                // FieldObject
                continue FontSize;
              }
              lastWordInLine = key;
              // go on
            }
          } else if (isLastWord) {
            lastWordInLine = key;
          } else {
            if (
              formObject.multiline &&
              (textHeight + lineSpacing) * (lineCount + 2) + lineSpacing >
                height
            ) {
              // If the Text is higher than the FieldObject
              continue FontSize;
            }
          }
        }
        // Remove last blank

        var line = "";

        for (var x = firstWordInLine; x <= lastWordInLine; x++) {
          var currLine = textSplit[x];
          if (formObject.multiline) {
            if (x === lastWordInLine) {
              line += currLine[currWord] + " ";
              currWord = (currWord + 1) % currLine.length;
              continue;
            }
            if (x === firstWordInLine) {
              line += currLine[currLine.length - 1] + " ";
              continue;
            }
          }
          line += currLine[0] + " ";
        }

        // Remove last blank
        line =
          line.substr(line.length - 1) == " "
            ? line.substr(0, line.length - 1)
            : line;
        // lastLength -= blankSpace.width;
        lastLength = calculateFontSpace(line, formObject, fontSize).width;

        // Calculate startX
        switch (formObject.textAlign) {
          case "right":
            startX = width - lastLength - borderPadding;
            break;
          case "center":
            startX = (width - lastLength) / 2;
            break;
          case "left":
          default:
            startX = borderPadding;
            break;
        }
        text += f2(startX) + " " + f2(lastY) + " Td\n";
        text += "(" + pdfEscape(line) + ") Tj\n";
        // reset X in PDF
        text += -f2(startX) + " 0 Td\n";

        // After a Line, adjust y position
        lastY = -(fontSize + lineSpacing);

        // Reset for next iteration step
        lastLength = 0;
        firstWordInLine = isWithNewLine ? lastWordInLine : lastWordInLine + 1;
        lineCount++;

        lastLine = "";
        continue Line;
      }
    }
    break;
  }

  returnValue.text = text;
  returnValue.fontSize = fontSize;

  return returnValue;
};

/**
 * Small workaround for calculating the TextMetric approximately.
 *
 * @param text
 * @param fontsize
 * @returns {TextMetrics} (Has Height and Width)
 */
var calculateFontSpace = function(text, formObject, fontSize) {
  var font = formObject.scope.internal.getFont(
    formObject.fontName,
    formObject.fontStyle
  );
  var width =
    formObject.scope.getStringUnitWidth(text, {
      font: font,
      fontSize: parseFloat(fontSize),
      charSpace: 0
    }) * parseFloat(fontSize);
  var height =
    formObject.scope.getStringUnitWidth("3", {
      font: font,
      fontSize: parseFloat(fontSize),
      charSpace: 0
    }) *
    parseFloat(fontSize) *
    1.5;
  return { height: height, width: width };
};

var acroformPluginTemplate = {
  fields: [],
  xForms: [],
  /**
   * acroFormDictionaryRoot contains information about the AcroForm
   * Dictionary 0: The Event-Token, the AcroFormDictionaryCallback has
   * 1: The Object ID of the Root
   */
  acroFormDictionaryRoot: null,
  /**
   * After the PDF gets evaluated, the reference to the root has to be
   * reset, this indicates, whether the root has already been printed
   * out
   */
  printedOut: false,
  internal: null,
  isInitialized: false
};

var annotReferenceCallback = function(scope) {
  //set objId to undefined and force it to get a new objId on buildDocument
  scope.internal.acroformPlugin.acroFormDictionaryRoot.objId = undefined;
  var fields = scope.internal.acroformPlugin.acroFormDictionaryRoot.Fields;
  for (var i in fields) {
    if (fields.hasOwnProperty(i)) {
      var formObject = fields[i];
      //set objId to undefined and force it to get a new objId on buildDocument
      formObject.objId = undefined;
      // add Annot Reference!
      if (formObject.hasAnnotation) {
        // If theres an Annotation Widget in the Form Object, put the
        // Reference in the /Annot array
        createAnnotationReference(formObject, scope);
      }
    }
  }
};

var putForm = function(formObject) {
  if (formObject.scope.internal.acroformPlugin.printedOut) {
    formObject.scope.internal.acroformPlugin.printedOut = false;
    formObject.scope.internal.acroformPlugin.acroFormDictionaryRoot = null;
  }
  formObject.scope.internal.acroformPlugin.acroFormDictionaryRoot.Fields.push(
    formObject
  );
};
/**
 * Create the Reference to the widgetAnnotation, so that it gets referenced
 * in the Annot[] int the+ (Requires the Annotation Plugin)
 */
var createAnnotationReference = function(object, scope) {
  var options = {
    type: "reference",
    object: object
  };
  var findEntry = function(entry) {
    return entry.type === options.type && entry.object === options.object;
  };
  if (
    scope.internal
      .getPageInfo(object.page)
      .pageContext.annotations.find(findEntry) === undefined
  ) {
    scope.internal
      .getPageInfo(object.page)
      .pageContext.annotations.push(options);
  }
};

// Callbacks

var putCatalogCallback = function(scope) {
  // Put reference to AcroForm to DocumentCatalog
  if (
    typeof scope.internal.acroformPlugin.acroFormDictionaryRoot !== "undefined"
  ) {
    // for safety, shouldn't normally be the case
    scope.internal.write(
      "/AcroForm " +
        scope.internal.acroformPlugin.acroFormDictionaryRoot.objId +
        " " +
        0 +
        " R"
    );
  } else {
    throw new Error("putCatalogCallback: Root missing.");
  }
};

/**
 * Adds /Acroform X 0 R to Document Catalog, and creates the AcroForm
 * Dictionary
 */
var AcroFormDictionaryCallback = function(scope) {
  // Remove event
  scope.internal.events.unsubscribe(
    scope.internal.acroformPlugin.acroFormDictionaryRoot._eventID
  );
  delete scope.internal.acroformPlugin.acroFormDictionaryRoot._eventID;
  scope.internal.acroformPlugin.printedOut = true;
};

/**
 * Creates the single Fields and writes them into the Document
 *
 * If fieldArray is set, use the fields that are inside it instead of the
 * fields from the AcroRoot (for the FormXObjects...)
 */
var createFieldCallback = function(fieldArray, scope) {
  var standardFields = !fieldArray;

  if (!fieldArray) {
    // in case there is no fieldArray specified, we want to print out
    // the Fields of the AcroForm
    // Print out Root
    scope.internal.newObjectDeferredBegin(
      scope.internal.acroformPlugin.acroFormDictionaryRoot.objId,
      true
    );
    scope.internal.acroformPlugin.acroFormDictionaryRoot.putStream();
  }

  fieldArray =
    fieldArray || scope.internal.acroformPlugin.acroFormDictionaryRoot.Kids;

  for (var i in fieldArray) {
    if (fieldArray.hasOwnProperty(i)) {
      var fieldObject = fieldArray[i];
      var keyValueList = [];
      var oldRect = fieldObject.Rect;

      if (fieldObject.Rect) {
        fieldObject.Rect = calculateCoordinates(fieldObject.Rect, scope);
      }

      // Start Writing the Object
      scope.internal.newObjectDeferredBegin(fieldObject.objId, true);

      fieldObject.DA = AcroFormAppearance.createDefaultAppearanceStream(
        fieldObject
      );

      if (
        typeof fieldObject === "object" &&
        typeof fieldObject.getKeyValueListForStream === "function"
      ) {
        keyValueList = fieldObject.getKeyValueListForStream();
      }

      fieldObject.Rect = oldRect;

      if (
        fieldObject.hasAppearanceStream &&
        !fieldObject.appearanceStreamContent
      ) {
        // Calculate Appearance
        var appearance = calculateAppearanceStream(fieldObject);
        keyValueList.push({ key: "AP", value: "<</N " + appearance + ">>" });

        scope.internal.acroformPlugin.xForms.push(appearance);
      }

      // Assume AppearanceStreamContent is a Array with N,R,D (at least
      // one of them!)
      if (fieldObject.appearanceStreamContent) {
        var appearanceStreamString = "";
        // Iterate over N,R and D
        for (var k in fieldObject.appearanceStreamContent) {
          if (fieldObject.appearanceStreamContent.hasOwnProperty(k)) {
            var value = fieldObject.appearanceStreamContent[k];
            appearanceStreamString += "/" + k + " ";
            appearanceStreamString += "<<";
            if (Object.keys(value).length >= 1 || Array.isArray(value)) {
              // appearanceStream is an Array or Object!
              for (var i in value) {
                if (value.hasOwnProperty(i)) {
                  var obj = value[i];
                  if (typeof obj === "function") {
                    // if Function is referenced, call it in order
                    // to get the FormXObject
                    obj = obj.call(scope, fieldObject);
                  }
                  appearanceStreamString += "/" + i + " " + obj + " ";

                  // In case the XForm is already used, e.g. OffState
                  // of CheckBoxes, don't add it
                  if (!(scope.internal.acroformPlugin.xForms.indexOf(obj) >= 0))
                    scope.internal.acroformPlugin.xForms.push(obj);
                }
              }
            } else {
              obj = value;
              if (typeof obj === "function") {
                // if Function is referenced, call it in order to
                // get the FormXObject
                obj = obj.call(scope, fieldObject);
              }
              appearanceStreamString += "/" + i + " " + obj;
              if (!(scope.internal.acroformPlugin.xForms.indexOf(obj) >= 0))
                scope.internal.acroformPlugin.xForms.push(obj);
            }
            appearanceStreamString += ">>";
          }
        }

        // appearance stream is a normal Object..
        keyValueList.push({
          key: "AP",
          value: "<<\n" + appearanceStreamString + ">>"
        });
      }

      scope.internal.putStream({
        additionalKeyValues: keyValueList,
        objectId: fieldObject.objId
      });

      scope.internal.out("endobj");
    }
  }
  if (standardFields) {
    createXFormObjectCallback(scope.internal.acroformPlugin.xForms, scope);
  }
};

var createXFormObjectCallback = function(fieldArray, scope) {
  for (var i in fieldArray) {
    if (fieldArray.hasOwnProperty(i)) {
      var key = i;
      var fieldObject = fieldArray[i];
      // Start Writing the Object
      scope.internal.newObjectDeferredBegin(fieldObject.objId, true);

      if (
        typeof fieldObject === "object" &&
        typeof fieldObject.putStream === "function"
      ) {
        fieldObject.putStream();
      }
      delete fieldArray[key];
    }
  }
};

var initializeAcroForm = function(scope, formObject) {
  formObject.scope = scope;
  if (
    scope.internal !== undefined &&
    (scope.internal.acroformPlugin === undefined ||
      scope.internal.acroformPlugin.isInitialized === false)
  ) {
    AcroFormField.FieldNum = 0;
    scope.internal.acroformPlugin = JSON.parse(
      JSON.stringify(acroformPluginTemplate)
    );
    if (scope.internal.acroformPlugin.acroFormDictionaryRoot) {
      throw new Error("Exception while creating AcroformDictionary");
    }
    scaleFactor = scope.internal.scaleFactor;
    // The Object Number of the AcroForm Dictionary
    scope.internal.acroformPlugin.acroFormDictionaryRoot = new AcroFormDictionary();
    scope.internal.acroformPlugin.acroFormDictionaryRoot.scope = scope;

    // add Callback for creating the AcroForm Dictionary
    scope.internal.acroformPlugin.acroFormDictionaryRoot._eventID = scope.internal.events.subscribe(
      "postPutResources",
      function() {
        AcroFormDictionaryCallback(scope);
      }
    );

    scope.internal.events.subscribe("buildDocument", function() {
      annotReferenceCallback(scope);
    }); // buildDocument

    // Register event, that is triggered when the DocumentCatalog is
    // written, in order to add /AcroForm

    scope.internal.events.subscribe("putCatalog", function() {
      putCatalogCallback(scope);
    });

    // Register event, that creates all Fields
    scope.internal.events.subscribe("postPutPages", function(fieldArray) {
      createFieldCallback(fieldArray, scope);
    });

    scope.internal.acroformPlugin.isInitialized = true;
  }
};

//PDF 32000-1:2008, page 26, 7.3.6
var arrayToPdfArray = (jsPDFAPI.__acroform__.arrayToPdfArray = function(
  array,
  objId,
  scope
) {
  var encryptor = function(data) {
    return data;
  };
  if (Array.isArray(array)) {
    var content = "[";
    for (var i = 0; i < array.length; i++) {
      if (i !== 0) {
        content += " ";
      }
      switch (typeof array[i]) {
        case "boolean":
        case "number":
        case "object":
          content += array[i].toString();
          break;
        case "string":
          if (array[i].substr(0, 1) !== "/") {
            if (typeof objId !== "undefined" && scope)
              encryptor = scope.internal.getEncryptor(objId);
            content += "(" + pdfEscape(encryptor(array[i].toString())) + ")";
          } else {
            content += array[i].toString();
          }
          break;
      }
    }
    content += "]";
    return content;
  }
  throw new Error(
    "Invalid argument passed to jsPDF.__acroform__.arrayToPdfArray"
  );
});
function getMatches(string, regex, index) {
  index || (index = 1); // default to the first capturing group
  var matches = [];
  var match;
  while ((match = regex.exec(string))) {
    matches.push(match[index]);
  }
  return matches;
}
var pdfArrayToStringArray = function(array) {
  var result = [];
  if (typeof array === "string") {
    result = getMatches(array, /\((.*?)\)/g);
  }
  return result;
};

var toPdfString = function(string, objId, scope) {
  var encryptor = function(data) {
    return data;
  };
  if (typeof objId !== "undefined" && scope)
    encryptor = scope.internal.getEncryptor(objId);
  string = string || "";
  string.toString();
  string = "(" + pdfEscape(encryptor(string)) + ")";
  return string;
};

// ##########################
// Classes
// ##########################

/**
 * @class AcroFormPDFObject
 * @classdesc A AcroFormPDFObject
 */
var AcroFormPDFObject = function() {
  this._objId = undefined;
  this._scope = undefined;

  /**
   * @name AcroFormPDFObject#objId
   * @type {any}
   */
  Object.defineProperty(this, "objId", {
    get: function() {
      if (typeof this._objId === "undefined") {
        if (typeof this.scope === "undefined") {
          return undefined;
        }
        this._objId = this.scope.internal.newObjectDeferred();
      }
      return this._objId;
    },
    set: function(value) {
      this._objId = value;
    }
  });
  Object.defineProperty(this, "scope", {
    value: this._scope,
    writable: true
  });
};

/**
 * @function AcroFormPDFObject.toString
 */
AcroFormPDFObject.prototype.toString = function() {
  return this.objId + " 0 R";
};

AcroFormPDFObject.prototype.putStream = function() {
  var keyValueList = this.getKeyValueListForStream();
  this.scope.internal.putStream({
    data: this.stream,
    additionalKeyValues: keyValueList,
    objectId: this.objId
  });
  this.scope.internal.out("endobj");
};

/**
 * Returns an key-value-List of all non-configurable Variables from the Object
 *
 * @name getKeyValueListForStream
 * @returns {string}
 */
AcroFormPDFObject.prototype.getKeyValueListForStream = function() {
  var keyValueList = [];
  var keys = Object.getOwnPropertyNames(this).filter(function(key) {
    return (
      key != "content" &&
      key != "appearanceStreamContent" &&
      key != "scope" &&
      key != "objId" &&
      key.substring(0, 1) != "_"
    );
  });

  for (var i in keys) {
    if (Object.getOwnPropertyDescriptor(this, keys[i]).configurable === false) {
      var key = keys[i];
      var value = this[key];

      if (value) {
        if (Array.isArray(value)) {
          keyValueList.push({
            key: key,
            value: arrayToPdfArray(value, this.objId, this.scope)
          });
        } else if (value instanceof AcroFormPDFObject) {
          // In case it is a reference to another PDFObject,
          // take the reference number
          value.scope = this.scope;
          keyValueList.push({ key: key, value: value.objId + " 0 R" });
        } else if (typeof value !== "function") {
          keyValueList.push({ key: key, value: value });
        }
      }
    }
  }
  return keyValueList;
};

var AcroFormXObject = function() {
  AcroFormPDFObject.call(this);

  Object.defineProperty(this, "Type", {
    value: "/XObject",
    configurable: false,
    writable: true
  });

  Object.defineProperty(this, "Subtype", {
    value: "/Form",
    configurable: false,
    writable: true
  });

  Object.defineProperty(this, "FormType", {
    value: 1,
    configurable: false,
    writable: true
  });

  var _BBox = [];
  Object.defineProperty(this, "BBox", {
    configurable: false,
    get: function() {
      return _BBox;
    },
    set: function(value) {
      _BBox = value;
    }
  });

  Object.defineProperty(this, "Resources", {
    value: "2 0 R",
    configurable: false,
    writable: true
  });

  var _stream;
  Object.defineProperty(this, "stream", {
    enumerable: false,
    configurable: true,
    set: function(value) {
      _stream = value.trim();
    },
    get: function() {
      if (_stream) {
        return _stream;
      } else {
        return null;
      }
    }
  });
};

inherit(AcroFormXObject, AcroFormPDFObject);

var AcroFormDictionary = function() {
  AcroFormPDFObject.call(this);

  var _Kids = [];

  Object.defineProperty(this, "Kids", {
    enumerable: false,
    configurable: true,
    get: function() {
      if (_Kids.length > 0) {
        return _Kids;
      } else {
        return undefined;
      }
    }
  });
  Object.defineProperty(this, "Fields", {
    enumerable: false,
    configurable: false,
    get: function() {
      return _Kids;
    }
  });

  // Default Appearance
  var _DA;
  Object.defineProperty(this, "DA", {
    enumerable: false,
    configurable: false,
    get: function() {
      if (!_DA) {
        return undefined;
      }
      var encryptor = function(data) {
        return data;
      };
      if (this.scope) encryptor = this.scope.internal.getEncryptor(this.objId);
      return "(" + pdfEscape(encryptor(_DA)) + ")";
    },
    set: function(value) {
      _DA = value;
    }
  });
};

inherit(AcroFormDictionary, AcroFormPDFObject);

/**
 * The Field Object contains the Variables, that every Field needs
 *
 * @class AcroFormField
 * @classdesc An AcroForm FieldObject
 */
var AcroFormField = function() {
  AcroFormPDFObject.call(this);

  //Annotation-Flag See Table 165
  var _F = 4;
  Object.defineProperty(this, "F", {
    enumerable: false,
    configurable: false,
    get: function() {
      return _F;
    },
    set: function(value) {
      if (!isNaN(value)) {
        _F = value;
      } else {
        throw new Error(
          'Invalid value "' + value + '" for attribute F supplied.'
        );
      }
    }
  });

  /**
   * (PDF 1.2) If set, print the annotation when the page is printed. If clear, never print the annotation, regardless of wether is is displayed on the screen.
   * NOTE 2 This can be useful for annotations representing interactive pushbuttons, which would serve no meaningful purpose on the printed page.
   *
   * @name AcroFormField#showWhenPrinted
   * @default true
   * @type {boolean}
   */
  Object.defineProperty(this, "showWhenPrinted", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(_F, 3));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.F = setBitForPdf(_F, 3);
      } else {
        this.F = clearBitForPdf(_F, 3);
      }
    }
  });

  var _Ff = 0;
  Object.defineProperty(this, "Ff", {
    enumerable: false,
    configurable: false,
    get: function() {
      return _Ff;
    },
    set: function(value) {
      if (!isNaN(value)) {
        _Ff = value;
      } else {
        throw new Error(
          'Invalid value "' + value + '" for attribute Ff supplied.'
        );
      }
    }
  });

  var _Rect = [];
  Object.defineProperty(this, "Rect", {
    enumerable: false,
    configurable: false,
    get: function() {
      if (_Rect.length === 0) {
        return undefined;
      }
      return _Rect;
    },
    set: function(value) {
      if (typeof value !== "undefined") {
        _Rect = value;
      } else {
        _Rect = [];
      }
    }
  });

  /**
   * The x-position of the field.
   *
   * @name AcroFormField#x
   * @default null
   * @type {number}
   */
  Object.defineProperty(this, "x", {
    enumerable: true,
    configurable: true,
    get: function() {
      if (!_Rect || isNaN(_Rect[0])) {
        return 0;
      }
      return _Rect[0];
    },
    set: function(value) {
      _Rect[0] = value;
    }
  });

  /**
   * The y-position of the field.
   *
   * @name AcroFormField#y
   * @default null
   * @type {number}
   */
  Object.defineProperty(this, "y", {
    enumerable: true,
    configurable: true,
    get: function() {
      if (!_Rect || isNaN(_Rect[1])) {
        return 0;
      }
      return _Rect[1];
    },
    set: function(value) {
      _Rect[1] = value;
    }
  });

  /**
   * The width of the field.
   *
   * @name AcroFormField#width
   * @default null
   * @type {number}
   */
  Object.defineProperty(this, "width", {
    enumerable: true,
    configurable: true,
    get: function() {
      if (!_Rect || isNaN(_Rect[2])) {
        return 0;
      }
      return _Rect[2];
    },
    set: function(value) {
      _Rect[2] = value;
    }
  });

  /**
   * The height of the field.
   *
   * @name AcroFormField#height
   * @default null
   * @type {number}
   */
  Object.defineProperty(this, "height", {
    enumerable: true,
    configurable: true,
    get: function() {
      if (!_Rect || isNaN(_Rect[3])) {
        return 0;
      }
      return _Rect[3];
    },
    set: function(value) {
      _Rect[3] = value;
    }
  });

  var _FT = "";
  Object.defineProperty(this, "FT", {
    enumerable: true,
    configurable: false,
    get: function() {
      return _FT;
    },
    set: function(value) {
      switch (value) {
        case "/Btn":
        case "/Tx":
        case "/Ch":
        case "/Sig":
          _FT = value;
          break;
        default:
          throw new Error(
            'Invalid value "' + value + '" for attribute FT supplied.'
          );
      }
    }
  });

  var _T = null;

  Object.defineProperty(this, "T", {
    enumerable: true,
    configurable: false,
    get: function() {
      if (!_T || _T.length < 1) {
        // In case of a Child from a Radio´Group, you don't need a FieldName
        if (this instanceof AcroFormChildClass) {
          return undefined;
        }
        _T = "FieldObject" + AcroFormField.FieldNum++;
      }
      var encryptor = function(data) {
        return data;
      };
      if (this.scope) encryptor = this.scope.internal.getEncryptor(this.objId);
      return "(" + pdfEscape(encryptor(_T)) + ")";
    },
    set: function(value) {
      _T = value.toString();
    }
  });

  /**
   * (Optional) The partial field name (see 12.7.3.2, “Field Names”).
   *
   * @name AcroFormField#fieldName
   * @default null
   * @type {string}
   */
  Object.defineProperty(this, "fieldName", {
    configurable: true,
    enumerable: true,
    get: function() {
      return _T;
    },
    set: function(value) {
      _T = value;
    }
  });

  var _fontName = "helvetica";
  /**
   * The fontName of the font to be used.
   *
   * @name AcroFormField#fontName
   * @default 'helvetica'
   * @type {string}
   */
  Object.defineProperty(this, "fontName", {
    enumerable: true,
    configurable: true,
    get: function() {
      return _fontName;
    },
    set: function(value) {
      _fontName = value;
    }
  });

  var _fontStyle = "normal";
  /**
   * The fontStyle of the font to be used.
   *
   * @name AcroFormField#fontStyle
   * @default 'normal'
   * @type {string}
   */
  Object.defineProperty(this, "fontStyle", {
    enumerable: true,
    configurable: true,
    get: function() {
      return _fontStyle;
    },
    set: function(value) {
      _fontStyle = value;
    }
  });

  var _fontSize = 0;
  /**
   * The fontSize of the font to be used.
   *
   * @name AcroFormField#fontSize
   * @default 0 (for auto)
   * @type {number}
   */
  Object.defineProperty(this, "fontSize", {
    enumerable: true,
    configurable: true,
    get: function() {
      return _fontSize;
    },
    set: function(value) {
      _fontSize = value;
    }
  });

  var _maxFontSize = undefined;
  /**
   * The maximum fontSize of the font to be used.
   *
   * @name AcroFormField#maxFontSize
   * @default 0 (for auto)
   * @type {number}
   */
  Object.defineProperty(this, "maxFontSize", {
    enumerable: true,
    configurable: true,
    get: function() {
      if (_maxFontSize === undefined) {
        // use the old default value here - the value is some kind of random as it depends on the scaleFactor (user unit)
        // ("50" is transformed to the "user space" but then used in "pdf space")
        return 50 / scaleFactor;
      } else {
        return _maxFontSize;
      }
    },
    set: function(value) {
      _maxFontSize = value;
    }
  });

  var _color = "black";
  /**
   * The color of the text
   *
   * @name AcroFormField#color
   * @default 'black'
   * @type {string|rgba}
   */
  Object.defineProperty(this, "color", {
    enumerable: true,
    configurable: true,
    get: function() {
      return _color;
    },
    set: function(value) {
      _color = value;
    }
  });

  var _DA = "/F1 0 Tf 0 g";
  // Defines the default appearance (Needed for variable Text)
  Object.defineProperty(this, "DA", {
    enumerable: true,
    configurable: false,
    get: function() {
      if (
        !_DA ||
        this instanceof AcroFormChildClass ||
        this instanceof AcroFormTextField
      ) {
        return undefined;
      }
      return toPdfString(_DA, this.objId, this.scope);
    },
    set: function(value) {
      value = value.toString();
      _DA = value;
    }
  });

  var _DV = null;
  Object.defineProperty(this, "DV", {
    enumerable: false,
    configurable: false,
    get: function() {
      if (!_DV) {
        return undefined;
      }
      if (this instanceof AcroFormButton === false) {
        return toPdfString(_DV, this.objId, this.scope);
      }
      return _DV;
    },
    set: function(value) {
      value = value.toString();
      if (this instanceof AcroFormButton === false) {
        if (value.substr(0, 1) === "(") {
          _DV = pdfUnescape(value.substr(1, value.length - 2));
        } else {
          _DV = pdfUnescape(value);
        }
      } else {
        _DV = value;
      }
    }
  });

  /**
   * (Optional; inheritable) The default value to which the field reverts when a reset-form action is executed (see 12.7.5.3, “Reset-Form Action”). The format of this value is the same as that of value.
   *
   * @name AcroFormField#defaultValue
   * @default null
   * @type {any}
   */
  Object.defineProperty(this, "defaultValue", {
    enumerable: true,
    configurable: true,
    get: function() {
      if (this instanceof AcroFormButton === true) {
        return pdfUnescape(_DV.substr(1, _DV.length - 1));
      } else {
        return _DV;
      }
    },
    set: function(value) {
      value = value.toString();
      if (this instanceof AcroFormButton === true) {
        _DV = "/" + value;
      } else {
        _DV = value;
      }
    }
  });

  var _V = null;
  Object.defineProperty(this, "_V", {
    enumerable: false,
    configurable: false,
    get: function() {
      if (!_V) {
        return undefined;
      }
      return _V;
    },
    set: function(value) {
      this.V = value;
    }
  });
  Object.defineProperty(this, "V", {
    enumerable: false,
    configurable: false,
    get: function() {
      if (!_V) {
        return undefined;
      }
      if (this instanceof AcroFormButton === false) {
        return toPdfString(_V, this.objId, this.scope);
      }
      return _V;
    },
    set: function(value) {
      value = value.toString();
      if (this instanceof AcroFormButton === false) {
        if (value.substr(0, 1) === "(") {
          _V = pdfUnescape(value.substr(1, value.length - 2));
        } else {
          _V = pdfUnescape(value);
        }
      } else {
        _V = value;
      }
    }
  });

  /**
   * (Optional; inheritable) The field’s value, whose format varies depending on the field type. See the descriptions of individual field types for further information.
   *
   * @name AcroFormField#value
   * @default null
   * @type {any}
   */
  Object.defineProperty(this, "value", {
    enumerable: true,
    configurable: true,
    get: function() {
      if (this instanceof AcroFormButton === true) {
        return pdfUnescape(_V.substr(1, _V.length - 1));
      } else {
        return _V;
      }
    },
    set: function(value) {
      value = value.toString();
      if (this instanceof AcroFormButton === true) {
        _V = "/" + value;
      } else {
        _V = value;
      }
    }
  });

  /**
   * Check if field has annotations
   *
   * @name AcroFormField#hasAnnotation
   * @readonly
   * @type {boolean}
   */
  Object.defineProperty(this, "hasAnnotation", {
    enumerable: true,
    configurable: true,
    get: function() {
      return this.Rect;
    }
  });

  Object.defineProperty(this, "Type", {
    enumerable: true,
    configurable: false,
    get: function() {
      return this.hasAnnotation ? "/Annot" : null;
    }
  });

  Object.defineProperty(this, "Subtype", {
    enumerable: true,
    configurable: false,
    get: function() {
      return this.hasAnnotation ? "/Widget" : null;
    }
  });

  var _hasAppearanceStream = false;
  /**
   * true if field has an appearanceStream
   *
   * @name AcroFormField#hasAppearanceStream
   * @readonly
   * @type {boolean}
   */
  Object.defineProperty(this, "hasAppearanceStream", {
    enumerable: true,
    configurable: true,
    get: function() {
      return _hasAppearanceStream;
    },
    set: function(value) {
      value = Boolean(value);
      _hasAppearanceStream = value;
    }
  });

  /**
   * The page on which the AcroFormField is placed
   *
   * @name AcroFormField#page
   * @type {number}
   */
  var _page;
  Object.defineProperty(this, "page", {
    enumerable: true,
    configurable: true,
    get: function() {
      if (!_page) {
        return undefined;
      }
      return _page;
    },
    set: function(value) {
      _page = value;
    }
  });

  /**
   * If set, the user may not change the value of the field. Any associated widget annotations will not interact with the user; that is, they will not respond to mouse clicks or change their appearance in response to mouse motions. This flag is useful for fields whose values are computed or imported from a database.
   *
   * @name AcroFormField#readOnly
   * @default false
   * @type {boolean}
   */
  Object.defineProperty(this, "readOnly", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 1));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 1);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 1);
      }
    }
  });

  /**
   * If set, the field shall have a value at the time it is exported by a submitform action (see 12.7.5.2, “Submit-Form Action”).
   *
   * @name AcroFormField#required
   * @default false
   * @type {boolean}
   */
  Object.defineProperty(this, "required", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 2));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 2);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 2);
      }
    }
  });

  /**
   * If set, the field shall not be exported by a submit-form action (see 12.7.5.2, “Submit-Form Action”)
   *
   * @name AcroFormField#noExport
   * @default false
   * @type {boolean}
   */
  Object.defineProperty(this, "noExport", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 3));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 3);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 3);
      }
    }
  });

  var _Q = null;
  Object.defineProperty(this, "Q", {
    enumerable: true,
    configurable: false,
    get: function() {
      if (_Q === null) {
        return undefined;
      }
      return _Q;
    },
    set: function(value) {
      if ([0, 1, 2].indexOf(value) !== -1) {
        _Q = value;
      } else {
        throw new Error(
          'Invalid value "' + value + '" for attribute Q supplied.'
        );
      }
    }
  });

  /**
   * (Optional; inheritable) A code specifying the form of quadding (justification) that shall be used in displaying the text:
   * 'left', 'center', 'right'
   *
   * @name AcroFormField#textAlign
   * @default 'left'
   * @type {string}
   */
  Object.defineProperty(this, "textAlign", {
    get: function() {
      var result;
      switch (_Q) {
        case 0:
        default:
          result = "left";
          break;
        case 1:
          result = "center";
          break;
        case 2:
          result = "right";
          break;
      }
      return result;
    },
    configurable: true,
    enumerable: true,
    set: function(value) {
      switch (value) {
        case "right":
        case 2:
          _Q = 2;
          break;
        case "center":
        case 1:
          _Q = 1;
          break;
        case "left":
        case 0:
        default:
          _Q = 0;
      }
    }
  });
};

inherit(AcroFormField, AcroFormPDFObject);

/**
 * @class AcroFormChoiceField
 * @extends AcroFormField
 */
var AcroFormChoiceField = function() {
  AcroFormField.call(this);
  // Field Type = Choice Field
  this.FT = "/Ch";
  // options
  this.V = "()";

  this.fontName = "zapfdingbats";
  // Top Index
  var _TI = 0;

  Object.defineProperty(this, "TI", {
    enumerable: true,
    configurable: false,
    get: function() {
      return _TI;
    },
    set: function(value) {
      _TI = value;
    }
  });

  /**
   * (Optional) For scrollable list boxes, the top index (the index in the Opt array of the first option visible in the list). Default value: 0.
   *
   * @name AcroFormChoiceField#topIndex
   * @default 0
   * @type {number}
   */
  Object.defineProperty(this, "topIndex", {
    enumerable: true,
    configurable: true,
    get: function() {
      return _TI;
    },
    set: function(value) {
      _TI = value;
    }
  });

  var _Opt = [];
  Object.defineProperty(this, "Opt", {
    enumerable: true,
    configurable: false,
    get: function() {
      return arrayToPdfArray(_Opt, this.objId, this.scope);
    },
    set: function(value) {
      _Opt = pdfArrayToStringArray(value);
    }
  });

  /**
   * @memberof AcroFormChoiceField
   * @name getOptions
   * @function
   * @instance
   * @returns {array} array of Options
   */
  this.getOptions = function() {
    return _Opt;
  };

  /**
   * @memberof AcroFormChoiceField
   * @name setOptions
   * @function
   * @instance
   * @param {array} value
   */
  this.setOptions = function(value) {
    _Opt = value;
    if (this.sort) {
      _Opt.sort();
    }
  };

  /**
   * @memberof AcroFormChoiceField
   * @name addOption
   * @function
   * @instance
   * @param {string} value
   */
  this.addOption = function(value) {
    value = value || "";
    value = value.toString();
    _Opt.push(value);
    if (this.sort) {
      _Opt.sort();
    }
  };

  /**
   * @memberof AcroFormChoiceField
   * @name removeOption
   * @function
   * @instance
   * @param {string} value
   * @param {boolean} allEntries (default: false)
   */
  this.removeOption = function(value, allEntries) {
    allEntries = allEntries || false;
    value = value || "";
    value = value.toString();

    while (_Opt.indexOf(value) !== -1) {
      _Opt.splice(_Opt.indexOf(value), 1);
      if (allEntries === false) {
        break;
      }
    }
  };

  /**
   * If set, the field is a combo box; if clear, the field is a list box.
   *
   * @name AcroFormChoiceField#combo
   * @default false
   * @type {boolean}
   */
  Object.defineProperty(this, "combo", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 18));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 18);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 18);
      }
    }
  });

  /**
   * If set, the combo box shall include an editable text box as well as a drop-down list; if clear, it shall include only a drop-down list. This flag shall be used only if the Combo flag is set.
   *
   * @name AcroFormChoiceField#edit
   * @default false
   * @type {boolean}
   */
  Object.defineProperty(this, "edit", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 19));
    },
    set: function(value) {
      //PDF 32000-1:2008, page 444
      if (this.combo === true) {
        if (Boolean(value) === true) {
          this.Ff = setBitForPdf(this.Ff, 19);
        } else {
          this.Ff = clearBitForPdf(this.Ff, 19);
        }
      }
    }
  });

  /**
   * If set, the field’s option items shall be sorted alphabetically. This flag is intended for use by writers, not by readers. Conforming readers shall display the options in the order in which they occur in the Opt array (see Table 231).
   *
   * @name AcroFormChoiceField#sort
   * @default false
   * @type {boolean}
   */
  Object.defineProperty(this, "sort", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 20));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 20);
        _Opt.sort();
      } else {
        this.Ff = clearBitForPdf(this.Ff, 20);
      }
    }
  });

  /**
   * (PDF 1.4) If set, more than one of the field’s option items may be selected simultaneously; if clear, at most one item shall be selected
   *
   * @name AcroFormChoiceField#multiSelect
   * @default false
   * @type {boolean}
   */
  Object.defineProperty(this, "multiSelect", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 22));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 22);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 22);
      }
    }
  });

  /**
   * (PDF 1.4) If set, text entered in the field shall not be spellchecked. This flag shall not be used unless the Combo and Edit flags are both set.
   *
   * @name AcroFormChoiceField#doNotSpellCheck
   * @default false
   * @type {boolean}
   */
  Object.defineProperty(this, "doNotSpellCheck", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 23));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 23);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 23);
      }
    }
  });

  /**
   * (PDF 1.5) If set, the new value shall be committed as soon as a selection is made (commonly with the pointing device). In this case, supplying a value for a field involves three actions: selecting the field for fill-in, selecting a choice for the fill-in value, and leaving that field, which finalizes or “commits” the data choice and triggers any actions associated with the entry or changing of this data. If this flag is on, then processing does not wait for leaving the field action to occur, but immediately proceeds to the third step.
   * This option enables applications to perform an action once a selection is made, without requiring the user to exit the field. If clear, the new value is not committed until the user exits the field.
   *
   * @name AcroFormChoiceField#commitOnSelChange
   * @default false
   * @type {boolean}
   */
  Object.defineProperty(this, "commitOnSelChange", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 27));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 27);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 27);
      }
    }
  });

  this.hasAppearanceStream = false;
};
inherit(AcroFormChoiceField, AcroFormField);

/**
 * @class AcroFormListBox
 * @extends AcroFormChoiceField
 * @extends AcroFormField
 */
var AcroFormListBox = function() {
  AcroFormChoiceField.call(this);
  this.fontName = "helvetica";

  //PDF 32000-1:2008, page 444
  this.combo = false;
};
inherit(AcroFormListBox, AcroFormChoiceField);

/**
 * @class AcroFormComboBox
 * @extends AcroFormListBox
 * @extends AcroFormChoiceField
 * @extends AcroFormField
 */
var AcroFormComboBox = function() {
  AcroFormListBox.call(this);
  this.combo = true;
};
inherit(AcroFormComboBox, AcroFormListBox);

/**
 * @class AcroFormEditBox
 * @extends AcroFormComboBox
 * @extends AcroFormListBox
 * @extends AcroFormChoiceField
 * @extends AcroFormField
 */
var AcroFormEditBox = function() {
  AcroFormComboBox.call(this);
  this.edit = true;
};
inherit(AcroFormEditBox, AcroFormComboBox);

/**
 * @class AcroFormButton
 * @extends AcroFormField
 */
var AcroFormButton = function() {
  AcroFormField.call(this);
  this.FT = "/Btn";

  /**
   * (Radio buttons only) If set, exactly one radio button shall be selected at all times; selecting the currently selected button has no effect. If clear, clicking the selected button deselects it, leaving no button selected.
   *
   * @name AcroFormButton#noToggleToOff
   * @type {boolean}
   */
  Object.defineProperty(this, "noToggleToOff", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 15));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 15);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 15);
      }
    }
  });

  /**
   * If set, the field is a set of radio buttons; if clear, the field is a checkbox. This flag may be set only if the Pushbutton flag is clear.
   *
   * @name AcroFormButton#radio
   * @type {boolean}
   */
  Object.defineProperty(this, "radio", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 16));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 16);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 16);
      }
    }
  });

  /**
   * If set, the field is a pushbutton that does not retain a permanent value.
   *
   * @name AcroFormButton#pushButton
   * @type {boolean}
   */
  Object.defineProperty(this, "pushButton", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 17));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 17);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 17);
      }
    }
  });

  /**
   * (PDF 1.5) If set, a group of radio buttons within a radio button field that use the same value for the on state will turn on and off in unison; that is if one is checked, they are all checked. If clear, the buttons are mutually exclusive (the same behavior as HTML radio buttons).
   *
   * @name AcroFormButton#radioIsUnison
   * @type {boolean}
   */
  Object.defineProperty(this, "radioIsUnison", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 26));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 26);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 26);
      }
    }
  });

  var _MK = {};
  Object.defineProperty(this, "MK", {
    enumerable: false,
    configurable: false,
    get: function() {
      var encryptor = function(data) {
        return data;
      };
      if (this.scope) encryptor = this.scope.internal.getEncryptor(this.objId);
      if (Object.keys(_MK).length !== 0) {
        var result = [];
        result.push("<<");
        var key;
        for (key in _MK) {
          result.push("/" + key + " (" + pdfEscape(encryptor(_MK[key])) + ")");
        }
        result.push(">>");
        return result.join("\n");
      }
      return undefined;
    },
    set: function(value) {
      if (typeof value === "object") {
        _MK = value;
      }
    }
  });

  /**
   * From the PDF reference:
   * (Optional, button fields only) The widget annotation's normal caption which shall be displayed when it is not interacting with the user.
   * Unlike the remaining entries listed in this Table which apply only to widget annotations associated with pushbutton fields (see Pushbuttons in 12.7.4.2, "Button Fields"), the CA entry may be used with any type of button field, including check boxes (see Check Boxes in 12.7.4.2, "Button Fields") and radio buttons (Radio Buttons in 12.7.4.2, "Button Fields").
   *
   * - '8' = Cross,
   * - 'l' =  Circle,
   * - '' = nothing
   * @name AcroFormButton#caption
   * @type {string}
   */
  Object.defineProperty(this, "caption", {
    enumerable: true,
    configurable: true,
    get: function() {
      return _MK.CA || "";
    },
    set: function(value) {
      if (typeof value === "string") {
        _MK.CA = value;
      }
    }
  });

  var _AS;
  Object.defineProperty(this, "AS", {
    enumerable: false,
    configurable: false,
    get: function() {
      return _AS;
    },
    set: function(value) {
      _AS = value;
    }
  });

  /**
   * (Required if the appearance dictionary AP contains one or more subdictionaries; PDF 1.2) The annotation's appearance state, which selects the applicable appearance stream from an appearance subdictionary (see Section 12.5.5, "Appearance Streams")
   *
   * @name AcroFormButton#appearanceState
   * @type {any}
   */
  Object.defineProperty(this, "appearanceState", {
    enumerable: true,
    configurable: true,
    get: function() {
      return _AS.substr(1, _AS.length - 1);
    },
    set: function(value) {
      _AS = "/" + value;
    }
  });
};
inherit(AcroFormButton, AcroFormField);

/**
 * @class AcroFormPushButton
 * @extends AcroFormButton
 * @extends AcroFormField
 */
var AcroFormPushButton = function() {
  AcroFormButton.call(this);
  this.pushButton = true;
};
inherit(AcroFormPushButton, AcroFormButton);

/**
 * @class AcroFormRadioButton
 * @extends AcroFormButton
 * @extends AcroFormField
 */
var AcroFormRadioButton = function() {
  AcroFormButton.call(this);
  this.radio = true;
  this.pushButton = false;

  var _Kids = [];
  Object.defineProperty(this, "Kids", {
    enumerable: true,
    configurable: false,
    get: function() {
      return _Kids;
    },
    set: function(value) {
      if (typeof value !== "undefined") {
        _Kids = value;
      } else {
        _Kids = [];
      }
    }
  });
};
inherit(AcroFormRadioButton, AcroFormButton);

/**
 * The Child class of a RadioButton (the radioGroup) -> The single Buttons
 *
 * @class AcroFormChildClass
 * @extends AcroFormField
 * @ignore
 */
var AcroFormChildClass = function() {
  AcroFormField.call(this);

  var _parent;
  Object.defineProperty(this, "Parent", {
    enumerable: false,
    configurable: false,
    get: function() {
      return _parent;
    },
    set: function(value) {
      _parent = value;
    }
  });

  var _optionName;
  Object.defineProperty(this, "optionName", {
    enumerable: false,
    configurable: true,
    get: function() {
      return _optionName;
    },
    set: function(value) {
      _optionName = value;
    }
  });

  var _MK = {};
  Object.defineProperty(this, "MK", {
    enumerable: false,
    configurable: false,
    get: function() {
      var encryptor = function(data) {
        return data;
      };
      if (this.scope) encryptor = this.scope.internal.getEncryptor(this.objId);
      var result = [];
      result.push("<<");
      var key;
      for (key in _MK) {
        result.push("/" + key + " (" + pdfEscape(encryptor(_MK[key])) + ")");
      }
      result.push(">>");
      return result.join("\n");
    },
    set: function(value) {
      if (typeof value === "object") {
        _MK = value;
      }
    }
  });

  /**
   * From the PDF reference:
   * (Optional, button fields only) The widget annotation's normal caption which shall be displayed when it is not interacting with the user.
   * Unlike the remaining entries listed in this Table which apply only to widget annotations associated with pushbutton fields (see Pushbuttons in 12.7.4.2, "Button Fields"), the CA entry may be used with any type of button field, including check boxes (see Check Boxes in 12.7.4.2, "Button Fields") and radio buttons (Radio Buttons in 12.7.4.2, "Button Fields").
   *
   * - '8' = Cross,
   * - 'l' =  Circle,
   * - '' = nothing
   * @name AcroFormButton#caption
   * @type {string}
   */
  Object.defineProperty(this, "caption", {
    enumerable: true,
    configurable: true,
    get: function() {
      return _MK.CA || "";
    },
    set: function(value) {
      if (typeof value === "string") {
        _MK.CA = value;
      }
    }
  });

  var _AS;
  Object.defineProperty(this, "AS", {
    enumerable: false,
    configurable: false,
    get: function() {
      return _AS;
    },
    set: function(value) {
      _AS = value;
    }
  });

  /**
   * (Required if the appearance dictionary AP contains one or more subdictionaries; PDF 1.2) The annotation's appearance state, which selects the applicable appearance stream from an appearance subdictionary (see Section 12.5.5, "Appearance Streams")
   *
   * @name AcroFormButton#appearanceState
   * @type {any}
   */
  Object.defineProperty(this, "appearanceState", {
    enumerable: true,
    configurable: true,
    get: function() {
      return _AS.substr(1, _AS.length - 1);
    },
    set: function(value) {
      _AS = "/" + value;
    }
  });
  this.caption = "l";
  this.appearanceState = "Off";
  // todo: set AppearanceType as variable that can be set from the
  // outside...
  this._AppearanceType = AcroFormAppearance.RadioButton.Circle;
  // The Default appearanceType is the Circle
  this.appearanceStreamContent = this._AppearanceType.createAppearanceStream(
    this.optionName
  );
};
inherit(AcroFormChildClass, AcroFormField);

AcroFormRadioButton.prototype.setAppearance = function(appearance) {
  if (!("createAppearanceStream" in appearance && "getCA" in appearance)) {
    throw new Error(
      "Couldn't assign Appearance to RadioButton. Appearance was Invalid!"
    );
  }
  for (var objId in this.Kids) {
    if (this.Kids.hasOwnProperty(objId)) {
      var child = this.Kids[objId];
      child.appearanceStreamContent = appearance.createAppearanceStream(
        child.optionName
      );
      child.caption = appearance.getCA();
    }
  }
};

AcroFormRadioButton.prototype.createOption = function(name) {
  // Create new Child for RadioGroup
  var child = new AcroFormChildClass();
  child.Parent = this;
  child.optionName = name;
  // Add to Parent
  this.Kids.push(child);

  addField.call(this.scope, child);

  return child;
};

/**
 * @class AcroFormCheckBox
 * @extends AcroFormButton
 * @extends AcroFormField
 */
var AcroFormCheckBox = function() {
  AcroFormButton.call(this);

  this.fontName = "zapfdingbats";
  this.caption = "3";
  this.appearanceState = "On";
  this.value = "On";
  this.textAlign = "center";
  this.appearanceStreamContent = AcroFormAppearance.CheckBox.createAppearanceStream();
};
inherit(AcroFormCheckBox, AcroFormButton);

/**
 * @class AcroFormTextField
 * @extends AcroFormField
 */
var AcroFormTextField = function() {
  AcroFormField.call(this);
  this.FT = "/Tx";

  /**
   * If set, the field may contain multiple lines of text; if clear, the field’s text shall be restricted to a single line.
   *
   * @name AcroFormTextField#multiline
   * @type {boolean}
   */
  Object.defineProperty(this, "multiline", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 13));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 13);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 13);
      }
    }
  });

  /**
   * (PDF 1.4) If set, the text entered in the field represents the pathname of a file whose contents shall be submitted as the value of the field.
   *
   * @name AcroFormTextField#fileSelect
   * @type {boolean}
   */
  Object.defineProperty(this, "fileSelect", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 21));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 21);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 21);
      }
    }
  });

  /**
   * (PDF 1.4) If set, text entered in the field shall not be spell-checked.
   *
   * @name AcroFormTextField#doNotSpellCheck
   * @type {boolean}
   */
  Object.defineProperty(this, "doNotSpellCheck", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 23));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 23);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 23);
      }
    }
  });

  /**
   * (PDF 1.4) If set, the field shall not scroll (horizontally for single-line fields, vertically for multiple-line fields) to accommodate more text than fits within its annotation rectangle. Once the field is full, no further text shall be accepted for interactive form filling; for noninteractive form filling, the filler should take care not to add more character than will visibly fit in the defined area.
   *
   * @name AcroFormTextField#doNotScroll
   * @type {boolean}
   */
  Object.defineProperty(this, "doNotScroll", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 24));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 24);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 24);
      }
    }
  });

  /**
   * (PDF 1.5) May be set only if the MaxLen entry is present in the text field dictionary (see Table 229) and if the Multiline, Password, and FileSelect flags are clear. If set, the field shall be automatically divided into as many equally spaced positions, or combs, as the value of MaxLen, and the text is laid out into those combs.
   *
   * @name AcroFormTextField#comb
   * @type {boolean}
   */
  Object.defineProperty(this, "comb", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 25));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 25);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 25);
      }
    }
  });

  /**
   * (PDF 1.5) If set, the value of this field shall be a rich text string (see 12.7.3.4, “Rich Text Strings”). If the field has a value, the RV entry of the field dictionary (Table 222) shall specify the rich text string.
   *
   * @name AcroFormTextField#richText
   * @type {boolean}
   */
  Object.defineProperty(this, "richText", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 26));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 26);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 26);
      }
    }
  });

  var _MaxLen = null;
  Object.defineProperty(this, "MaxLen", {
    enumerable: true,
    configurable: false,
    get: function() {
      return _MaxLen;
    },
    set: function(value) {
      _MaxLen = value;
    }
  });

  /**
   * (Optional; inheritable) The maximum length of the field’s text, in characters.
   *
   * @name AcroFormTextField#maxLength
   * @type {number}
   */
  Object.defineProperty(this, "maxLength", {
    enumerable: true,
    configurable: true,
    get: function() {
      return _MaxLen;
    },
    set: function(value) {
      if (Number.isInteger(value)) {
        _MaxLen = value;
      }
    }
  });

  Object.defineProperty(this, "hasAppearanceStream", {
    enumerable: true,
    configurable: true,
    get: function() {
      return this.V || this.DV;
    }
  });
};
inherit(AcroFormTextField, AcroFormField);

/**
 * @class AcroFormPasswordField
 * @extends AcroFormTextField
 * @extends AcroFormField
 */
var AcroFormPasswordField = function() {
  AcroFormTextField.call(this);

  /**
   * If set, the field is intended for entering a secure password that should not be echoed visibly to the screen. Characters typed from the keyboard shall instead be echoed in some unreadable form, such as asterisks or bullet characters.
   * NOTE To protect password confidentiality, readers should never store the value of the text field in the PDF file if this flag is set.
   *
   * @name AcroFormTextField#password
   * @type {boolean}
   */
  Object.defineProperty(this, "password", {
    enumerable: true,
    configurable: true,
    get: function() {
      return Boolean(getBitForPdf(this.Ff, 14));
    },
    set: function(value) {
      if (Boolean(value) === true) {
        this.Ff = setBitForPdf(this.Ff, 14);
      } else {
        this.Ff = clearBitForPdf(this.Ff, 14);
      }
    }
  });
  this.password = true;
};
inherit(AcroFormPasswordField, AcroFormTextField);

// Contains Methods for creating standard appearances
var AcroFormAppearance = {
  CheckBox: {
    createAppearanceStream: function() {
      var appearance = {
        N: {
          On: AcroFormAppearance.CheckBox.YesNormal
        },
        D: {
          On: AcroFormAppearance.CheckBox.YesPushDown,
          Off: AcroFormAppearance.CheckBox.OffPushDown
        }
      };

      return appearance;
    },
    /**
     * Returns the standard On Appearance for a CheckBox
     *
     * @returns {AcroFormXObject}
     */
    YesPushDown: function(formObject) {
      var xobj = createFormXObject(formObject);
      xobj.scope = formObject.scope;
      var stream = [];
      var fontKey = formObject.scope.internal.getFont(
        formObject.fontName,
        formObject.fontStyle
      ).id;
      var encodedColor = formObject.scope.__private__.encodeColorString(
        formObject.color
      );
      var calcRes = calculateX(formObject, formObject.caption);
      stream.push("0.749023 g");
      stream.push(
        "0 0 " +
          f2(AcroFormAppearance.internal.getWidth(formObject)) +
          " " +
          f2(AcroFormAppearance.internal.getHeight(formObject)) +
          " re"
      );
      stream.push("f");
      stream.push("BMC");
      stream.push("q");
      stream.push("0 0 1 rg");
      stream.push(
        "/" + fontKey + " " + f2(calcRes.fontSize) + " Tf " + encodedColor
      );
      stream.push("BT");
      stream.push(calcRes.text);
      stream.push("ET");
      stream.push("Q");
      stream.push("EMC");
      xobj.stream = stream.join("\n");
      return xobj;
    },

    YesNormal: function(formObject) {
      var xobj = createFormXObject(formObject);
      xobj.scope = formObject.scope;
      var fontKey = formObject.scope.internal.getFont(
        formObject.fontName,
        formObject.fontStyle
      ).id;
      var encodedColor = formObject.scope.__private__.encodeColorString(
        formObject.color
      );
      var stream = [];
      var height = AcroFormAppearance.internal.getHeight(formObject);
      var width = AcroFormAppearance.internal.getWidth(formObject);
      var calcRes = calculateX(formObject, formObject.caption);
      stream.push("1 g");
      stream.push("0 0 " + f2(width) + " " + f2(height) + " re");
      stream.push("f");
      stream.push("q");
      stream.push("0 0 1 rg");
      stream.push("0 0 " + f2(width - 1) + " " + f2(height - 1) + " re");
      stream.push("W");
      stream.push("n");
      stream.push("0 g");
      stream.push("BT");
      stream.push(
        "/" + fontKey + " " + f2(calcRes.fontSize) + " Tf " + encodedColor
      );
      stream.push(calcRes.text);
      stream.push("ET");
      stream.push("Q");
      xobj.stream = stream.join("\n");
      return xobj;
    },

    /**
     * Returns the standard Off Appearance for a CheckBox
     *
     * @returns {AcroFormXObject}
     */
    OffPushDown: function(formObject) {
      var xobj = createFormXObject(formObject);
      xobj.scope = formObject.scope;
      var stream = [];
      stream.push("0.749023 g");
      stream.push(
        "0 0 " +
          f2(AcroFormAppearance.internal.getWidth(formObject)) +
          " " +
          f2(AcroFormAppearance.internal.getHeight(formObject)) +
          " re"
      );
      stream.push("f");
      xobj.stream = stream.join("\n");
      return xobj;
    }
  },

  RadioButton: {
    Circle: {
      createAppearanceStream: function(name) {
        var appearanceStreamContent = {
          D: {
            Off: AcroFormAppearance.RadioButton.Circle.OffPushDown
          },
          N: {}
        };
        appearanceStreamContent.N[name] =
          AcroFormAppearance.RadioButton.Circle.YesNormal;
        appearanceStreamContent.D[name] =
          AcroFormAppearance.RadioButton.Circle.YesPushDown;
        return appearanceStreamContent;
      },
      getCA: function() {
        return "l";
      },

      YesNormal: function(formObject) {
        var xobj = createFormXObject(formObject);
        xobj.scope = formObject.scope;
        var stream = [];
        // Make the Radius of the Circle relative to min(height, width) of formObject
        var DotRadius =
          AcroFormAppearance.internal.getWidth(formObject) <=
          AcroFormAppearance.internal.getHeight(formObject)
            ? AcroFormAppearance.internal.getWidth(formObject) / 4
            : AcroFormAppearance.internal.getHeight(formObject) / 4;
        // The Borderpadding...
        DotRadius = Number((DotRadius * 0.9).toFixed(5));
        var c = AcroFormAppearance.internal.Bezier_C;
        var DotRadiusBezier = Number((DotRadius * c).toFixed(5));
        /*
         * The Following is a Circle created with Bezier-Curves.
         */
        stream.push("q");
        stream.push(
          "1 0 0 1 " +
            f5(AcroFormAppearance.internal.getWidth(formObject) / 2) +
            " " +
            f5(AcroFormAppearance.internal.getHeight(formObject) / 2) +
            " cm"
        );
        stream.push(DotRadius + " 0 m");
        stream.push(
          DotRadius +
            " " +
            DotRadiusBezier +
            " " +
            DotRadiusBezier +
            " " +
            DotRadius +
            " 0 " +
            DotRadius +
            " c"
        );
        stream.push(
          "-" +
            DotRadiusBezier +
            " " +
            DotRadius +
            " -" +
            DotRadius +
            " " +
            DotRadiusBezier +
            " -" +
            DotRadius +
            " 0 c"
        );
        stream.push(
          "-" +
            DotRadius +
            " -" +
            DotRadiusBezier +
            " -" +
            DotRadiusBezier +
            " -" +
            DotRadius +
            " 0 -" +
            DotRadius +
            " c"
        );
        stream.push(
          DotRadiusBezier +
            " -" +
            DotRadius +
            " " +
            DotRadius +
            " -" +
            DotRadiusBezier +
            " " +
            DotRadius +
            " 0 c"
        );
        stream.push("f");
        stream.push("Q");
        xobj.stream = stream.join("\n");
        return xobj;
      },
      YesPushDown: function(formObject) {
        var xobj = createFormXObject(formObject);
        xobj.scope = formObject.scope;
        var stream = [];
        var DotRadius =
          AcroFormAppearance.internal.getWidth(formObject) <=
          AcroFormAppearance.internal.getHeight(formObject)
            ? AcroFormAppearance.internal.getWidth(formObject) / 4
            : AcroFormAppearance.internal.getHeight(formObject) / 4;
        // The Borderpadding...
        DotRadius = Number((DotRadius * 0.9).toFixed(5));
        // Save results for later use; no need to waste
        // processor ticks on doing math
        var k = Number((DotRadius * 2).toFixed(5));
        var kc = Number((k * AcroFormAppearance.internal.Bezier_C).toFixed(5));
        var dc = Number(
          (DotRadius * AcroFormAppearance.internal.Bezier_C).toFixed(5)
        );

        stream.push("0.749023 g");
        stream.push("q");
        stream.push(
          "1 0 0 1 " +
            f5(AcroFormAppearance.internal.getWidth(formObject) / 2) +
            " " +
            f5(AcroFormAppearance.internal.getHeight(formObject) / 2) +
            " cm"
        );
        stream.push(k + " 0 m");
        stream.push(k + " " + kc + " " + kc + " " + k + " 0 " + k + " c");
        stream.push(
          "-" + kc + " " + k + " -" + k + " " + kc + " -" + k + " 0 c"
        );
        stream.push(
          "-" + k + " -" + kc + " -" + kc + " -" + k + " 0 -" + k + " c"
        );
        stream.push(kc + " -" + k + " " + k + " -" + kc + " " + k + " 0 c");
        stream.push("f");
        stream.push("Q");
        stream.push("0 g");
        stream.push("q");
        stream.push(
          "1 0 0 1 " +
            f5(AcroFormAppearance.internal.getWidth(formObject) / 2) +
            " " +
            f5(AcroFormAppearance.internal.getHeight(formObject) / 2) +
            " cm"
        );
        stream.push(DotRadius + " 0 m");
        stream.push(
          "" +
            DotRadius +
            " " +
            dc +
            " " +
            dc +
            " " +
            DotRadius +
            " 0 " +
            DotRadius +
            " c"
        );
        stream.push(
          "-" +
            dc +
            " " +
            DotRadius +
            " -" +
            DotRadius +
            " " +
            dc +
            " -" +
            DotRadius +
            " 0 c"
        );
        stream.push(
          "-" +
            DotRadius +
            " -" +
            dc +
            " -" +
            dc +
            " -" +
            DotRadius +
            " 0 -" +
            DotRadius +
            " c"
        );
        stream.push(
          dc +
            " -" +
            DotRadius +
            " " +
            DotRadius +
            " -" +
            dc +
            " " +
            DotRadius +
            " 0 c"
        );
        stream.push("f");
        stream.push("Q");
        xobj.stream = stream.join("\n");
        return xobj;
      },
      OffPushDown: function(formObject) {
        var xobj = createFormXObject(formObject);
        xobj.scope = formObject.scope;
        var stream = [];
        var DotRadius =
          AcroFormAppearance.internal.getWidth(formObject) <=
          AcroFormAppearance.internal.getHeight(formObject)
            ? AcroFormAppearance.internal.getWidth(formObject) / 4
            : AcroFormAppearance.internal.getHeight(formObject) / 4;
        // The Borderpadding...
        DotRadius = Number((DotRadius * 0.9).toFixed(5));
        // Save results for later use; no need to waste
        // processor ticks on doing math
        var k = Number((DotRadius * 2).toFixed(5));
        var kc = Number((k * AcroFormAppearance.internal.Bezier_C).toFixed(5));

        stream.push("0.749023 g");
        stream.push("q");
        stream.push(
          "1 0 0 1 " +
            f5(AcroFormAppearance.internal.getWidth(formObject) / 2) +
            " " +
            f5(AcroFormAppearance.internal.getHeight(formObject) / 2) +
            " cm"
        );
        stream.push(k + " 0 m");
        stream.push(k + " " + kc + " " + kc + " " + k + " 0 " + k + " c");
        stream.push(
          "-" + kc + " " + k + " -" + k + " " + kc + " -" + k + " 0 c"
        );
        stream.push(
          "-" + k + " -" + kc + " -" + kc + " -" + k + " 0 -" + k + " c"
        );
        stream.push(kc + " -" + k + " " + k + " -" + kc + " " + k + " 0 c");
        stream.push("f");
        stream.push("Q");
        xobj.stream = stream.join("\n");
        return xobj;
      }
    },

    Cross: {
      /**
       * Creates the Actual AppearanceDictionary-References
       *
       * @param {string} name
       * @returns {Object}
       * @ignore
       */
      createAppearanceStream: function(name) {
        var appearanceStreamContent = {
          D: {
            Off: AcroFormAppearance.RadioButton.Cross.OffPushDown
          },
          N: {}
        };
        appearanceStreamContent.N[name] =
          AcroFormAppearance.RadioButton.Cross.YesNormal;
        appearanceStreamContent.D[name] =
          AcroFormAppearance.RadioButton.Cross.YesPushDown;
        return appearanceStreamContent;
      },
      getCA: function() {
        return "8";
      },

      YesNormal: function(formObject) {
        var xobj = createFormXObject(formObject);
        xobj.scope = formObject.scope;
        var stream = [];
        var cross = AcroFormAppearance.internal.calculateCross(formObject);
        stream.push("q");
        stream.push(
          "1 1 " +
            f2(AcroFormAppearance.internal.getWidth(formObject) - 2) +
            " " +
            f2(AcroFormAppearance.internal.getHeight(formObject) - 2) +
            " re"
        );
        stream.push("W");
        stream.push("n");
        stream.push(f2(cross.x1.x) + " " + f2(cross.x1.y) + " m");
        stream.push(f2(cross.x2.x) + " " + f2(cross.x2.y) + " l");
        stream.push(f2(cross.x4.x) + " " + f2(cross.x4.y) + " m");
        stream.push(f2(cross.x3.x) + " " + f2(cross.x3.y) + " l");
        stream.push("s");
        stream.push("Q");
        xobj.stream = stream.join("\n");
        return xobj;
      },
      YesPushDown: function(formObject) {
        var xobj = createFormXObject(formObject);
        xobj.scope = formObject.scope;
        var cross = AcroFormAppearance.internal.calculateCross(formObject);
        var stream = [];
        stream.push("0.749023 g");
        stream.push(
          "0 0 " +
            f2(AcroFormAppearance.internal.getWidth(formObject)) +
            " " +
            f2(AcroFormAppearance.internal.getHeight(formObject)) +
            " re"
        );
        stream.push("f");
        stream.push("q");
        stream.push(
          "1 1 " +
            f2(AcroFormAppearance.internal.getWidth(formObject) - 2) +
            " " +
            f2(AcroFormAppearance.internal.getHeight(formObject) - 2) +
            " re"
        );
        stream.push("W");
        stream.push("n");
        stream.push(f2(cross.x1.x) + " " + f2(cross.x1.y) + " m");
        stream.push(f2(cross.x2.x) + " " + f2(cross.x2.y) + " l");
        stream.push(f2(cross.x4.x) + " " + f2(cross.x4.y) + " m");
        stream.push(f2(cross.x3.x) + " " + f2(cross.x3.y) + " l");
        stream.push("s");
        stream.push("Q");
        xobj.stream = stream.join("\n");
        return xobj;
      },
      OffPushDown: function(formObject) {
        var xobj = createFormXObject(formObject);
        xobj.scope = formObject.scope;
        var stream = [];
        stream.push("0.749023 g");
        stream.push(
          "0 0 " +
            f2(AcroFormAppearance.internal.getWidth(formObject)) +
            " " +
            f2(AcroFormAppearance.internal.getHeight(formObject)) +
            " re"
        );
        stream.push("f");
        xobj.stream = stream.join("\n");
        return xobj;
      }
    }
  },

  /**
   * Returns the standard Appearance
   *
   * @returns {AcroFormXObject}
   */
  createDefaultAppearanceStream: function(formObject) {
    // Set Helvetica to Standard Font (size: auto)
    // Color: Black
    var fontKey = formObject.scope.internal.getFont(
      formObject.fontName,
      formObject.fontStyle
    ).id;
    var encodedColor = formObject.scope.__private__.encodeColorString(
      formObject.color
    );
    var fontSize = formObject.fontSize;
    var result = "/" + fontKey + " " + fontSize + " Tf " + encodedColor;
    return result;
  }
};

AcroFormAppearance.internal = {
  Bezier_C: 0.551915024494,

  calculateCross: function(formObject) {
    var width = AcroFormAppearance.internal.getWidth(formObject);
    var height = AcroFormAppearance.internal.getHeight(formObject);
    var a = Math.min(width, height);

    var cross = {
      x1: {
        // upperLeft
        x: (width - a) / 2,
        y: (height - a) / 2 + a // height - borderPadding
      },
      x2: {
        // lowerRight
        x: (width - a) / 2 + a,
        y: (height - a) / 2 // borderPadding
      },
      x3: {
        // lowerLeft
        x: (width - a) / 2,
        y: (height - a) / 2 // borderPadding
      },
      x4: {
        // upperRight
        x: (width - a) / 2 + a,
        y: (height - a) / 2 + a // height - borderPadding
      }
    };

    return cross;
  }
};
AcroFormAppearance.internal.getWidth = function(formObject) {
  var result = 0;
  if (typeof formObject === "object") {
    result = scale(formObject.Rect[2]);
  }
  return result;
};
AcroFormAppearance.internal.getHeight = function(formObject) {
  var result = 0;
  if (typeof formObject === "object") {
    result = scale(formObject.Rect[3]);
  }
  return result;
};

// Public:

/**
 * Add an AcroForm-Field to the jsPDF-instance
 *
 * @name addField
 * @function
 * @instance
 * @param {Object} fieldObject
 * @returns {jsPDF}
 */
var addField = (jsPDFAPI.addField = function(fieldObject) {
  initializeAcroForm(this, fieldObject);

  if (fieldObject instanceof AcroFormField) {
    putForm(fieldObject);
  } else {
    throw new Error("Invalid argument passed to jsPDF.addField.");
  }
  fieldObject.page = fieldObject.scope.internal.getCurrentPageInfo().pageNumber;
  return this;
});

jsPDFAPI.AcroFormChoiceField = AcroFormChoiceField;
jsPDFAPI.AcroFormListBox = AcroFormListBox;
jsPDFAPI.AcroFormComboBox = AcroFormComboBox;
jsPDFAPI.AcroFormEditBox = AcroFormEditBox;
jsPDFAPI.AcroFormButton = AcroFormButton;
jsPDFAPI.AcroFormPushButton = AcroFormPushButton;
jsPDFAPI.AcroFormRadioButton = AcroFormRadioButton;
jsPDFAPI.AcroFormCheckBox = AcroFormCheckBox;
jsPDFAPI.AcroFormTextField = AcroFormTextField;
jsPDFAPI.AcroFormPasswordField = AcroFormPasswordField;
jsPDFAPI.AcroFormAppearance = AcroFormAppearance;

jsPDFAPI.AcroForm = {
  ChoiceField: AcroFormChoiceField,
  ListBox: AcroFormListBox,
  ComboBox: AcroFormComboBox,
  EditBox: AcroFormEditBox,
  Button: AcroFormButton,
  PushButton: AcroFormPushButton,
  RadioButton: AcroFormRadioButton,
  CheckBox: AcroFormCheckBox,
  TextField: AcroFormTextField,
  PasswordField: AcroFormPasswordField,
  Appearance: AcroFormAppearance
};

jsPDF.AcroForm = {
  ChoiceField: AcroFormChoiceField,
  ListBox: AcroFormListBox,
  ComboBox: AcroFormComboBox,
  EditBox: AcroFormEditBox,
  Button: AcroFormButton,
  PushButton: AcroFormPushButton,
  RadioButton: AcroFormRadioButton,
  CheckBox: AcroFormCheckBox,
  TextField: AcroFormTextField,
  PasswordField: AcroFormPasswordField,
  Appearance: AcroFormAppearance
};

/** @license
 * jsPDF addImage plugin
 * Copyright (c) 2012 Jason Siefken, https://github.com/siefkenj/
 *               2013 Chris Dowling, https://github.com/gingerchris
 *               2013 Trinh Ho, https://github.com/ineedfat
 *               2013 Edwin Alejandro Perez, https://github.com/eaparango
 *               2013 Norah Smith, https://github.com/burnburnrocket
 *               2014 Diego Casorran, https://github.com/diegocr
 *               2014 James Robb, https://github.com/jamesbrobb
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

(function(jsPDFAPI) {

  var namespace = "addImage_";
  jsPDFAPI.__addimage__ = {};

  var UNKNOWN = "UNKNOWN";

  // Heuristic selection of a good batch for large array .apply. Not limiting make the call overflow.
  // With too small batch iteration will be slow as more calls are made,
  // higher values cause larger and slower garbage collection.
  var ARRAY_APPLY_BATCH = 8192;

  var imageFileTypeHeaders = {
    PNG: [[0x89, 0x50, 0x4e, 0x47]],
    TIFF: [
      [0x4d, 0x4d, 0x00, 0x2a], //Motorola
      [0x49, 0x49, 0x2a, 0x00] //Intel
    ],
    JPEG: [
      [
        0xff,
        0xd8,
        0xff,
        0xe0,
        undefined,
        undefined,
        0x4a,
        0x46,
        0x49,
        0x46,
        0x00
      ], //JFIF
      [
        0xff,
        0xd8,
        0xff,
        0xe1,
        undefined,
        undefined,
        0x45,
        0x78,
        0x69,
        0x66,
        0x00,
        0x00
      ], //Exif
      [0xff, 0xd8, 0xff, 0xdb], //JPEG RAW
      [0xff, 0xd8, 0xff, 0xee] //EXIF RAW
    ],
    JPEG2000: [[0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20]],
    GIF87a: [[0x47, 0x49, 0x46, 0x38, 0x37, 0x61]],
    GIF89a: [[0x47, 0x49, 0x46, 0x38, 0x39, 0x61]],
    WEBP: [
      [
        0x52,
        0x49,
        0x46,
        0x46,
        undefined,
        undefined,
        undefined,
        undefined,
        0x57,
        0x45,
        0x42,
        0x50
      ]
    ],
    BMP: [
      [0x42, 0x4d], //BM - Windows 3.1x, 95, NT, ... etc.
      [0x42, 0x41], //BA - OS/2 struct bitmap array
      [0x43, 0x49], //CI - OS/2 struct color icon
      [0x43, 0x50], //CP - OS/2 const color pointer
      [0x49, 0x43], //IC - OS/2 struct icon
      [0x50, 0x54] //PT - OS/2 pointer
    ]
  };

  /**
   * Recognize filetype of Image by magic-bytes
   *
   * https://en.wikipedia.org/wiki/List_of_file_signatures
   *
   * @name getImageFileTypeByImageData
   * @public
   * @function
   * @param {string|arraybuffer} imageData imageData as binary String or arraybuffer
   * @param {string} format format of file if filetype-recognition fails, e.g. 'JPEG'
   *
   * @returns {string} filetype of Image
   */
  var getImageFileTypeByImageData = (jsPDFAPI.__addimage__.getImageFileTypeByImageData = function(
    imageData,
    fallbackFormat
  ) {
    fallbackFormat = fallbackFormat || UNKNOWN;
    var i;
    var j;
    var result = UNKNOWN;
    var headerSchemata;
    var compareResult;
    var fileType;

    if (
      fallbackFormat === "RGBA" ||
      (imageData.data !== undefined &&
        imageData.data instanceof Uint8ClampedArray &&
        "height" in imageData &&
        "width" in imageData)
    ) {
      return "RGBA";
    }

    if (isArrayBufferView(imageData)) {
      for (fileType in imageFileTypeHeaders) {
        headerSchemata = imageFileTypeHeaders[fileType];
        for (i = 0; i < headerSchemata.length; i += 1) {
          compareResult = true;
          for (j = 0; j < headerSchemata[i].length; j += 1) {
            if (headerSchemata[i][j] === undefined) {
              continue;
            }
            if (headerSchemata[i][j] !== imageData[j]) {
              compareResult = false;
              break;
            }
          }
          if (compareResult === true) {
            result = fileType;
            break;
          }
        }
      }
    } else {
      for (fileType in imageFileTypeHeaders) {
        headerSchemata = imageFileTypeHeaders[fileType];
        for (i = 0; i < headerSchemata.length; i += 1) {
          compareResult = true;
          for (j = 0; j < headerSchemata[i].length; j += 1) {
            if (headerSchemata[i][j] === undefined) {
              continue;
            }
            if (headerSchemata[i][j] !== imageData.charCodeAt(j)) {
              compareResult = false;
              break;
            }
          }
          if (compareResult === true) {
            result = fileType;
            break;
          }
        }
      }
    }

    if (result === UNKNOWN && fallbackFormat !== UNKNOWN) {
      result = fallbackFormat;
    }
    return result;
  });

  // Image functionality ported from pdf.js
  var putImage = function(image) {
    var out = this.internal.write;
    var putStream = this.internal.putStream;
    var getFilters = this.internal.getFilters;

    var filter = getFilters();
    while (filter.indexOf("FlateEncode") !== -1) {
      filter.splice(filter.indexOf("FlateEncode"), 1);
    }

    image.objectId = this.internal.newObject();

    var additionalKeyValues = [];
    additionalKeyValues.push({ key: "Type", value: "/XObject" });
    additionalKeyValues.push({ key: "Subtype", value: "/Image" });
    additionalKeyValues.push({ key: "Width", value: image.width });
    additionalKeyValues.push({ key: "Height", value: image.height });

    if (image.colorSpace === color_spaces.INDEXED) {
      additionalKeyValues.push({
        key: "ColorSpace",
        value:
          "[/Indexed /DeviceRGB " +
          // if an indexed png defines more than one colour with transparency, we've created a sMask
          (image.palette.length / 3 - 1) +
          " " +
          ("sMask" in image && typeof image.sMask !== "undefined"
            ? image.objectId + 2
            : image.objectId + 1) +
          " 0 R]"
      });
    } else {
      additionalKeyValues.push({
        key: "ColorSpace",
        value: "/" + image.colorSpace
      });
      if (image.colorSpace === color_spaces.DEVICE_CMYK) {
        additionalKeyValues.push({ key: "Decode", value: "[1 0 1 0 1 0 1 0]" });
      }
    }
    additionalKeyValues.push({
      key: "BitsPerComponent",
      value: image.bitsPerComponent
    });
    if (
      "decodeParameters" in image &&
      typeof image.decodeParameters !== "undefined"
    ) {
      additionalKeyValues.push({
        key: "DecodeParms",
        value: "<<" + image.decodeParameters + ">>"
      });
    }
    if (
      "transparency" in image &&
      Array.isArray(image.transparency) &&
      image.transparency.length > 0
    ) {
      var transparency = "",
        i = 0,
        len = image.transparency.length;
      for (; i < len; i++)
        transparency +=
          image.transparency[i] + " " + image.transparency[i] + " ";

      additionalKeyValues.push({
        key: "Mask",
        value: "[" + transparency + "]"
      });
    }
    if (typeof image.sMask !== "undefined") {
      additionalKeyValues.push({
        key: "SMask",
        value: image.objectId + 1 + " 0 R"
      });
    }

    var alreadyAppliedFilters =
      typeof image.filter !== "undefined" ? ["/" + image.filter] : undefined;

    putStream({
      data: image.data,
      additionalKeyValues: additionalKeyValues,
      alreadyAppliedFilters: alreadyAppliedFilters,
      objectId: image.objectId
    });

    out("endobj");

    // Soft mask
    if ("sMask" in image && typeof image.sMask !== "undefined") {
      const sMaskBitsPerComponent =
        image.sMaskBitsPerComponent ?? image.bitsPerComponent;
      const sMask = {
        width: image.width,
        height: image.height,
        colorSpace: "DeviceGray",
        bitsPerComponent: sMaskBitsPerComponent,
        data: image.sMask
      };
      if ("filter" in image) {
        sMask.decodeParameters = `/Predictor ${image.predictor} /Colors 1 /BitsPerComponent ${sMaskBitsPerComponent} /Columns ${image.width}`;
        sMask.filter = image.filter;
      }
      putImage.call(this, sMask);
    }

    //Palette
    if (image.colorSpace === color_spaces.INDEXED) {
      var objId = this.internal.newObject();
      //out('<< /Filter / ' + img['f'] +' /Length ' + img['pal'].length + '>>');
      //putStream(zlib.compress(img['pal']));
      putStream({
        data: arrayBufferToBinaryString(new Uint8Array(image.palette)),
        objectId: objId
      });
      out("endobj");
    }
  };
  var putResourcesCallback = function() {
    var images = this.internal.collections[namespace + "images"];
    for (var i in images) {
      putImage.call(this, images[i]);
    }
  };
  var putXObjectsDictCallback = function() {
    var images = this.internal.collections[namespace + "images"],
      out = this.internal.write,
      image;
    for (var i in images) {
      image = images[i];
      out("/I" + image.index, image.objectId, "0", "R");
    }
  };

  var checkCompressValue = function(value) {
    if (value && typeof value === "string") value = value.toUpperCase();
    return value in jsPDFAPI.image_compression ? value : image_compression.NONE;
  };

  var initialize = function() {
    if (!this.internal.collections[namespace + "images"]) {
      this.internal.collections[namespace + "images"] = {};
      this.internal.events.subscribe("putResources", putResourcesCallback);
      this.internal.events.subscribe("putXobjectDict", putXObjectsDictCallback);
    }
  };

  var getImages = function() {
    var images = this.internal.collections[namespace + "images"];
    initialize.call(this);
    return images;
  };
  var getImageIndex = function() {
    return Object.keys(this.internal.collections[namespace + "images"]).length;
  };
  var notDefined = function(value) {
    return typeof value === "undefined" || value === null || value.length === 0;
  };
  var generateAliasFromImageData = function(imageData) {
    if (typeof imageData === "string" || isArrayBufferView(imageData)) {
      return sHashCode(imageData);
    } else if (isArrayBufferView(imageData.data)) {
      return sHashCode(imageData.data);
    }

    return null;
  };

  var isImageTypeSupported = function(type) {
    return typeof jsPDFAPI["process" + type.toUpperCase()] === "function";
  };

  var isDOMElement = function(object) {
    return typeof object === "object" && object.nodeType === 1;
  };

  var getImageDataFromElement = function(element, format) {
    //if element is an image which uses data url definition, just return the dataurl
    if (element.nodeName === "IMG" && element.hasAttribute("src")) {
      var src = "" + element.getAttribute("src");

      //is base64 encoded dataUrl, directly process it
      if (src.indexOf("data:image/") === 0) {
        return atob$1(
          unescape(src)
            .split("base64,")
            .pop()
        );
      }

      //it is probably an url, try to load it
      var tmpImageData = jsPDFAPI.loadFile(src, true);
      if (tmpImageData !== undefined) {
        return tmpImageData;
      }
    }

    if (element.nodeName === "CANVAS") {
      if (element.width === 0 || element.height === 0) {
        throw new Error(
          "Given canvas must have data. Canvas width: " +
            element.width +
            ", height: " +
            element.height
        );
      }
      var mimeType;
      switch (format) {
        case "PNG":
          mimeType = "image/png";
          break;
        case "WEBP":
          mimeType = "image/webp";
          break;
        case "JPEG":
        case "JPG":
        default:
          mimeType = "image/jpeg";
          break;
      }
      return atob$1(
        element
          .toDataURL(mimeType, 1.0)
          .split("base64,")
          .pop()
      );
    }
  };

  var checkImagesForAlias = function(alias) {
    var images = this.internal.collections[namespace + "images"];
    if (images) {
      for (var e in images) {
        if (alias === images[e].alias) {
          return images[e];
        }
      }
    }
  };

  var determineWidthAndHeight = function(width, height, image) {
    if (!width && !height) {
      width = -96;
      height = -96;
    }
    if (width < 0) {
      width = (-1 * image.width * 72) / width / this.internal.scaleFactor;
    }
    if (height < 0) {
      height = (-1 * image.height * 72) / height / this.internal.scaleFactor;
    }
    if (width === 0) {
      width = (height * image.width) / image.height;
    }
    if (height === 0) {
      height = (width * image.height) / image.width;
    }

    return [width, height];
  };

  var writeImageToPDF = function(x, y, width, height, image, rotation) {
    var dims = determineWidthAndHeight.call(this, width, height, image),
      coord = this.internal.getCoordinateString,
      vcoord = this.internal.getVerticalCoordinateString;

    var images = getImages.call(this);

    width = dims[0];
    height = dims[1];
    images[image.index] = image;

    if (rotation) {
      rotation *= Math.PI / 180;
      var c = Math.cos(rotation);
      var s = Math.sin(rotation);
      //like in pdf Reference do it 4 digits instead of 2
      var f4 = function(number) {
        return number.toFixed(4);
      };
      var rotationTransformationMatrix = [
        f4(c),
        f4(s),
        f4(s * -1),
        f4(c),
        0,
        0,
        "cm"
      ];
    }
    this.internal.write("q"); //Save graphics state
    if (rotation) {
      this.internal.write(
        [1, "0", "0", 1, coord(x), vcoord(y + height), "cm"].join(" ")
      ); //Translate
      this.internal.write(rotationTransformationMatrix.join(" ")); //Rotate
      this.internal.write(
        [coord(width), "0", "0", coord(height), "0", "0", "cm"].join(" ")
      ); //Scale
    } else {
      this.internal.write(
        [
          coord(width),
          "0",
          "0",
          coord(height),
          coord(x),
          vcoord(y + height),
          "cm"
        ].join(" ")
      ); //Translate and Scale
    }

    if (this.isAdvancedAPI()) {
      // draw image bottom up when in "advanced" API mode
      this.internal.write([1, 0, 0, -1, 0, 0, "cm"].join(" "));
    }

    this.internal.write("/I" + image.index + " Do"); //Paint Image
    this.internal.write("Q"); //Restore graphics state
  };

  /**
   * COLOR SPACES
   */
  var color_spaces = (jsPDFAPI.color_spaces = {
    DEVICE_RGB: "DeviceRGB",
    DEVICE_GRAY: "DeviceGray",
    DEVICE_CMYK: "DeviceCMYK",
    CAL_GREY: "CalGray",
    CAL_RGB: "CalRGB",
    LAB: "Lab",
    ICC_BASED: "ICCBased",
    INDEXED: "Indexed",
    PATTERN: "Pattern",
    SEPARATION: "Separation",
    DEVICE_N: "DeviceN"
  });

  /**
   * DECODE METHODS
   */
  jsPDFAPI.decode = {
    DCT_DECODE: "DCTDecode",
    FLATE_DECODE: "FlateDecode",
    LZW_DECODE: "LZWDecode",
    JPX_DECODE: "JPXDecode",
    JBIG2_DECODE: "JBIG2Decode",
    ASCII85_DECODE: "ASCII85Decode",
    ASCII_HEX_DECODE: "ASCIIHexDecode",
    RUN_LENGTH_DECODE: "RunLengthDecode",
    CCITT_FAX_DECODE: "CCITTFaxDecode"
  };

  /**
   * IMAGE COMPRESSION TYPES
   */
  var image_compression = (jsPDFAPI.image_compression = {
    NONE: "NONE",
    FAST: "FAST",
    MEDIUM: "MEDIUM",
    SLOW: "SLOW"
  });

  /**
   * @name sHashCode
   * @function
   * @param {string} data
   * @returns {string}
   */
  var sHashCode = (jsPDFAPI.__addimage__.sHashCode = function(data) {
    var hash = 0,
      i,
      len;

    if (typeof data === "string") {
      len = data.length;
      for (i = 0; i < len; i++) {
        hash = (hash << 5) - hash + data.charCodeAt(i);
        hash |= 0; // Convert to 32bit integer
      }
    } else if (isArrayBufferView(data)) {
      len = data.byteLength / 2;
      for (i = 0; i < len; i++) {
        hash = (hash << 5) - hash + data[i];
        hash |= 0; // Convert to 32bit integer
      }
    }
    return hash;
  });

  /**
   * Validates if given String is a valid Base64-String
   *
   * @name validateStringAsBase64
   * @public
   * @function
   * @param {String} possible Base64-String
   *
   * @returns {boolean}
   */
  var validateStringAsBase64 = (jsPDFAPI.__addimage__.validateStringAsBase64 = function(
    possibleBase64String
  ) {
    possibleBase64String = possibleBase64String || "";
    possibleBase64String.toString().trim();

    var result = true;

    if (possibleBase64String.length === 0) {
      result = false;
    }

    if (possibleBase64String.length % 4 !== 0) {
      result = false;
    }

    if (
      /^[A-Za-z0-9+/]+$/.test(
        possibleBase64String.substr(0, possibleBase64String.length - 2)
      ) === false
    ) {
      result = false;
    }

    if (
      /^[A-Za-z0-9/][A-Za-z0-9+/]|[A-Za-z0-9+/]=|==$/.test(
        possibleBase64String.substr(-2)
      ) === false
    ) {
      result = false;
    }
    return result;
  });

  /**
   * Strips out and returns info from a valid base64 data URI
   *
   * @name extractImageFromDataUrl
   * @function
   * @param {string} dataUrl a valid data URI of format 'data:[<MIME-type>][;base64],<data>'
   * @returns {string} The raw Base64-encoded data.
   */
  var extractImageFromDataUrl = (jsPDFAPI.__addimage__.extractImageFromDataUrl = function(
    dataUrl
  ) {
    if (dataUrl == null) {
      return null;
    }

    // avoid using a regexp for parsing because it might be vulnerable against ReDoS attacks

    dataUrl = dataUrl.trim();

    if (!dataUrl.startsWith("data:")) {
      return null;
    }

    const commaIndex = dataUrl.indexOf(",");
    if (commaIndex < 0) {
      return null;
    }

    const dataScheme = dataUrl.substring(0, commaIndex).trim();
    if (!dataScheme.endsWith("base64")) {
      return null;
    }

    return dataUrl.substring(commaIndex + 1);
  });

  /**
   * Tests supplied object to determine if ArrayBuffer
   *
   * @name isArrayBuffer
   * @function
   * @param {Object} object an Object
   *
   * @returns {boolean}
   */
  jsPDFAPI.__addimage__.isArrayBuffer = function(object) {
    return object instanceof ArrayBuffer;
  };

  /**
   * Tests supplied object to determine if it implements the ArrayBufferView (TypedArray) interface
   *
   * @name isArrayBufferView
   * @function
   * @param {Object} object an Object
   * @returns {boolean}
   */
  var isArrayBufferView = (jsPDFAPI.__addimage__.isArrayBufferView = function(
    object
  ) {
    return (
      object instanceof Int8Array ||
      object instanceof Uint8Array ||
      object instanceof Uint8ClampedArray ||
      object instanceof Int16Array ||
      object instanceof Uint16Array ||
      object instanceof Int32Array ||
      object instanceof Uint32Array ||
      object instanceof Float32Array ||
      object instanceof Float64Array
    );
  });

  /**
   * Convert Binary String to ArrayBuffer
   *
   * @name binaryStringToUint8Array
   * @public
   * @function
   * @param {string} BinaryString with ImageData
   * @returns {Uint8Array}
   */
  var binaryStringToUint8Array = (jsPDFAPI.__addimage__.binaryStringToUint8Array = function(
    binary_string
  ) {
    var len = binary_string.length;
    var bytes = new Uint8Array(len);
    for (var i = 0; i < len; i++) {
      bytes[i] = binary_string.charCodeAt(i);
    }
    return bytes;
  });

  /**
   * Convert the Buffer to a Binary String
   *
   * @name arrayBufferToBinaryString
   * @public
   * @function
   * @param {ArrayBuffer|ArrayBufferView} ArrayBuffer buffer or bufferView with ImageData
   *
   * @returns {String}
   */
  var arrayBufferToBinaryString = (jsPDFAPI.__addimage__.arrayBufferToBinaryString = function(
    buffer
  ) {
    var out = "";
    // There are calls with both ArrayBuffer and already converted Uint8Array or other BufferView.
    // Do not copy the array if input is already an array.
    var buf = isArrayBufferView(buffer) ? buffer : new Uint8Array(buffer);
    for (var i = 0; i < buf.length; i += ARRAY_APPLY_BATCH) {
      // Limit the amount of characters being parsed to prevent overflow.
      // Note that while TextDecoder would be faster, it does not have the same
      // functionality as fromCharCode with any provided encodings as of 3/2021.
      out += String.fromCharCode.apply(
        null,
        buf.subarray(i, i + ARRAY_APPLY_BATCH)
      );
    }
    return out;
  });

  /**
   * Possible parameter for addImage, an RGBA buffer with size.
   *
   * @typedef {Object} RGBAData
   * @property {Uint8ClampedArray} data - Single dimensional array of RGBA values. For example from canvas getImageData.
   * @property {number} width - Image width as the data does not carry this information in itself.
   * @property {number} height - Image height as the data does not carry this information in itself.
   */

  /**
   * Adds an Image to the PDF.
   *
   * @name addImage
   * @public
   * @function
   * @param {string|HTMLImageElement|HTMLCanvasElement|Uint8Array|RGBAData} imageData imageData as base64 encoded DataUrl or Image-HTMLElement or Canvas-HTMLElement or object containing RGBA array (like output from canvas.getImageData).
   * @param {string} format format of file if filetype-recognition fails or in case of a Canvas-Element needs to be specified (default for Canvas is JPEG), e.g. 'JPEG', 'PNG', 'WEBP'
   * @param {number} x x Coordinate (in units declared at inception of PDF document) against left edge of the page
   * @param {number} y y Coordinate (in units declared at inception of PDF document) against upper edge of the page
   * @param {number} width width of the image (in units declared at inception of PDF document)
   * @param {number} height height of the Image (in units declared at inception of PDF document)
   * @param {string} alias alias of the image (if used multiple times)
   * @param {string} compression compression of the generated JPEG, can have the values 'NONE', 'FAST', 'MEDIUM' and 'SLOW'
   * @param {number} rotation rotation of the image in degrees (0-359)
   *
   * @returns jsPDF
   */
  jsPDFAPI.addImage = function() {
    var imageData, format, x, y, w, h, alias, compression, rotation;

    imageData = arguments[0];
    if (typeof arguments[1] === "number") {
      format = UNKNOWN;
      x = arguments[1];
      y = arguments[2];
      w = arguments[3];
      h = arguments[4];
      alias = arguments[5];
      compression = arguments[6];
      rotation = arguments[7];
    } else {
      format = arguments[1];
      x = arguments[2];
      y = arguments[3];
      w = arguments[4];
      h = arguments[5];
      alias = arguments[6];
      compression = arguments[7];
      rotation = arguments[8];
    }

    if (
      typeof imageData === "object" &&
      !isDOMElement(imageData) &&
      "imageData" in imageData
    ) {
      var options = imageData;

      imageData = options.imageData;
      format = options.format || format || UNKNOWN;
      x = options.x || x || 0;
      y = options.y || y || 0;
      w = options.w || options.width || w;
      h = options.h || options.height || h;
      alias = options.alias || alias;
      compression = options.compression || compression;
      rotation = options.rotation || options.angle || rotation;
    }

    //If compression is not explicitly set, determine if we should use compression
    var filter = this.internal.getFilters();
    if (compression === undefined && filter.indexOf("FlateEncode") !== -1) {
      compression = "SLOW";
    }

    if (isNaN(x) || isNaN(y)) {
      throw new Error("Invalid coordinates passed to jsPDF.addImage");
    }

    initialize.call(this);

    var image = processImageData.call(
      this,
      imageData,
      format,
      alias,
      compression
    );

    writeImageToPDF.call(this, x, y, w, h, image, rotation);

    return this;
  };

  var processImageData = function(imageData, format, alias, compression) {
    var result, dataAsBinaryString;

    if (
      typeof imageData === "string" &&
      getImageFileTypeByImageData(imageData) === UNKNOWN
    ) {
      imageData = unescape(imageData);
      var tmpImageData = convertBase64ToBinaryString(imageData, false);

      if (tmpImageData !== "") {
        imageData = tmpImageData;
      } else {
        tmpImageData = jsPDFAPI.loadFile(imageData, true);
        if (tmpImageData !== undefined) {
          imageData = tmpImageData;
        }
      }
    }

    if (isDOMElement(imageData)) {
      imageData = getImageDataFromElement(imageData, format);
    }

    format = getImageFileTypeByImageData(imageData, format);
    if (!isImageTypeSupported(format)) {
      throw new Error(
        "addImage does not support files of type '" +
          format +
          "', please ensure that a plugin for '" +
          format +
          "' support is added."
      );
    }

    // now do the heavy lifting

    if (notDefined(alias)) {
      alias = generateAliasFromImageData(imageData);
    }
    result = checkImagesForAlias.call(this, alias);

    if (!result) {
      // no need to convert if imageData is already uint8array
      if (!(imageData instanceof Uint8Array) && format !== "RGBA") {
        dataAsBinaryString = imageData;
        imageData = binaryStringToUint8Array(imageData);
      }

      result = this["process" + format.toUpperCase()](
        imageData,
        getImageIndex.call(this),
        alias,
        checkCompressValue(compression),
        dataAsBinaryString
      );
    }

    if (!result) {
      throw new Error("An unknown error occurred whilst processing the image.");
    }
    return result;
  };

  /**
   * @name convertBase64ToBinaryString
   * @function
   * @param {string} stringData
   * @returns {string} binary string
   */
  var convertBase64ToBinaryString = (jsPDFAPI.__addimage__.convertBase64ToBinaryString = function(
    stringData,
    throwError
  ) {
    throwError = typeof throwError === "boolean" ? throwError : true;
    var imageData = "";
    var rawData;

    if (typeof stringData === "string") {
      rawData = extractImageFromDataUrl(stringData) ?? stringData;

      try {
        imageData = atob$1(rawData);
      } catch (e) {
        if (throwError) {
          if (!validateStringAsBase64(rawData)) {
            throw new Error(
              "Supplied Data is not a valid base64-String jsPDF.convertBase64ToBinaryString "
            );
          } else {
            throw new Error(
              "atob-Error in jsPDF.convertBase64ToBinaryString " + e.message
            );
          }
        }
      }
    }
    return imageData;
  });

  /**
   * @name getImageProperties
   * @function
   * @param {Object} imageData
   * @returns {Object}
   */
  jsPDFAPI.getImageProperties = function(imageData) {
    var image;
    var tmpImageData = "";
    var format;

    if (isDOMElement(imageData)) {
      imageData = getImageDataFromElement(imageData);
    }

    if (
      typeof imageData === "string" &&
      getImageFileTypeByImageData(imageData) === UNKNOWN
    ) {
      tmpImageData = convertBase64ToBinaryString(imageData, false);

      if (tmpImageData === "") {
        tmpImageData = jsPDFAPI.loadFile(imageData) || "";
      }
      imageData = tmpImageData;
    }

    format = getImageFileTypeByImageData(imageData);
    if (!isImageTypeSupported(format)) {
      throw new Error(
        "addImage does not support files of type '" +
          format +
          "', please ensure that a plugin for '" +
          format +
          "' support is added."
      );
    }

    if (!(imageData instanceof Uint8Array)) {
      imageData = binaryStringToUint8Array(imageData);
    }

    image = this["process" + format.toUpperCase()](imageData);

    if (!image) {
      throw new Error("An unknown error occurred whilst processing the image");
    }

    image.fileType = format;

    return image;
  };
})(jsPDF.API);

/**
 * @license
 * Copyright (c) 2014 Steven Spungin (TwelveTone LLC)  steven@twelvetone.tv
 *
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 */

(function(jsPDFAPI) {

  var notEmpty = function(obj) {
    if (typeof obj != "undefined") {
      if (obj != "") {
        return true;
      }
    }
  };

  jsPDF.API.events.push([
    "addPage",
    function(addPageData) {
      var pageInfo = this.internal.getPageInfo(addPageData.pageNumber);
      pageInfo.pageContext.annotations = [];
    }
  ]);

  jsPDFAPI.events.push([
    "putPage",
    function(putPageData) {
      var getHorizontalCoordinateString = this.internal.getCoordinateString;
      var getVerticalCoordinateString = this.internal
        .getVerticalCoordinateString;
      var pageInfo = this.internal.getPageInfoByObjId(putPageData.objId);
      var pageAnnos = putPageData.pageContext.annotations;

      var anno, rect, line;
      var found = false;
      for (var a = 0; a < pageAnnos.length && !found; a++) {
        anno = pageAnnos[a];
        switch (anno.type) {
          case "link":
            if (
              notEmpty(anno.options.url) ||
              notEmpty(anno.options.pageNumber)
            ) {
              found = true;
            }
            break;
          case "reference":
          case "text":
          case "freetext":
            found = true;
            break;
        }
      }
      if (found == false) {
        return;
      }

      this.internal.write("/Annots [");
      for (var i = 0; i < pageAnnos.length; i++) {
        anno = pageAnnos[i];
        var escape = this.internal.pdfEscape;
        var encryptor = this.internal.getEncryptor(putPageData.objId);

        switch (anno.type) {
          case "reference":
            // References to Widget Annotations (for AcroForm Fields)
            this.internal.write(" " + anno.object.objId + " 0 R ");
            break;
          case "text":
            // Create a an object for both the text and the popup
            var objText = this.internal.newAdditionalObject();
            var objPopup = this.internal.newAdditionalObject();
            var encryptorText = this.internal.getEncryptor(objText.objId);

            var title = anno.title || "Note";
            rect =
              "/Rect [" +
              getHorizontalCoordinateString(anno.bounds.x) +
              " " +
              getVerticalCoordinateString(anno.bounds.y + anno.bounds.h) +
              " " +
              getHorizontalCoordinateString(anno.bounds.x + anno.bounds.w) +
              " " +
              getVerticalCoordinateString(anno.bounds.y) +
              "] ";

            line =
              "<</Type /Annot /Subtype /" +
              "Text" +
              " " +
              rect +
              "/Contents (" +
              escape(encryptorText(anno.contents)) +
              ")";
            line += " /Popup " + objPopup.objId + " 0 R";
            line += " /P " + pageInfo.objId + " 0 R";
            line += " /T (" + escape(encryptorText(title)) + ") >>";
            objText.content = line;

            var parent = objText.objId + " 0 R";
            var popoff = 30;
            rect =
              "/Rect [" +
              getHorizontalCoordinateString(anno.bounds.x + popoff) +
              " " +
              getVerticalCoordinateString(anno.bounds.y + anno.bounds.h) +
              " " +
              getHorizontalCoordinateString(
                anno.bounds.x + anno.bounds.w + popoff
              ) +
              " " +
              getVerticalCoordinateString(anno.bounds.y) +
              "] ";
            line =
              "<</Type /Annot /Subtype /" +
              "Popup" +
              " " +
              rect +
              " /Parent " +
              parent;
            if (anno.open) {
              line += " /Open true";
            }
            line += " >>";
            objPopup.content = line;

            this.internal.write(objText.objId, "0 R", objPopup.objId, "0 R");

            break;
          case "freetext":
            rect =
              "/Rect [" +
              getHorizontalCoordinateString(anno.bounds.x) +
              " " +
              getVerticalCoordinateString(anno.bounds.y) +
              " " +
              getHorizontalCoordinateString(anno.bounds.x + anno.bounds.w) +
              " " +
              getVerticalCoordinateString(anno.bounds.y + anno.bounds.h) +
              "] ";
            var color = anno.color || "#000000";
            line =
              "<</Type /Annot /Subtype /" +
              "FreeText" +
              " " +
              rect +
              "/Contents (" +
              escape(encryptor(anno.contents)) +
              ")";
            line +=
              " /DS(font: Helvetica,sans-serif 12.0pt; text-align:left; color:#" +
              color +
              ")";
            line += " /Border [0 0 0]";
            line += " >>";
            this.internal.write(line);
            break;
          case "link":
            if (anno.options.name) {
              var loc = this.annotations._nameMap[anno.options.name];
              anno.options.pageNumber = loc.page;
              anno.options.top = loc.y;
            } else {
              if (!anno.options.top) {
                anno.options.top = 0;
              }
            }

            rect =
              "/Rect [" +
              anno.finalBounds.x +
              " " +
              anno.finalBounds.y +
              " " +
              anno.finalBounds.w +
              " " +
              anno.finalBounds.h +
              "] ";

            line = "";
            if (anno.options.url) {
              line =
                "<</Type /Annot /Subtype /Link " +
                rect +
                "/Border [0 0 0] /A <</S /URI /URI (" +
                escape(encryptor(anno.options.url)) +
                ") >>";
            } else if (anno.options.pageNumber) {
              // first page is 0
              var info = this.internal.getPageInfo(anno.options.pageNumber);
              line =
                "<</Type /Annot /Subtype /Link " +
                rect +
                "/Border [0 0 0] /Dest [" +
                info.objId +
                " 0 R";
              anno.options.magFactor = anno.options.magFactor || "XYZ";
              switch (anno.options.magFactor) {
                case "Fit":
                  line += " /Fit]";
                  break;
                case "FitH":
                  line += " /FitH " + anno.options.top + "]";
                  break;
                case "FitV":
                  anno.options.left = anno.options.left || 0;
                  line += " /FitV " + anno.options.left + "]";
                  break;
                case "XYZ":
                default:
                  var top = getVerticalCoordinateString(anno.options.top);
                  anno.options.left = anno.options.left || 0;
                  // 0 or null zoom will not change zoom factor
                  if (typeof anno.options.zoom === "undefined") {
                    anno.options.zoom = 0;
                  }
                  line +=
                    " /XYZ " +
                    anno.options.left +
                    " " +
                    top +
                    " " +
                    anno.options.zoom +
                    "]";
                  break;
              }
            }

            if (line != "") {
              line += " >>";
              this.internal.write(line);
            }
            break;
        }
      }
      this.internal.write("]");
    }
  ]);

  /**
   * @name createAnnotation
   * @function
   * @param {Object} options
   */
  jsPDFAPI.createAnnotation = function(options) {
    var pageInfo = this.internal.getCurrentPageInfo();
    switch (options.type) {
      case "link":
        this.link(
          options.bounds.x,
          options.bounds.y,
          options.bounds.w,
          options.bounds.h,
          options
        );
        break;
      case "text":
      case "freetext":
        pageInfo.pageContext.annotations.push(options);
        break;
    }
  };

  /**
   * Create a link
   *
   * valid options
   * <li> pageNumber or url [required]
   * <p>If pageNumber is specified, top and zoom may also be specified</p>
   * @name link
   * @function
   * @param {number} x
   * @param {number} y
   * @param {number} w
   * @param {number} h
   * @param {Object} options
   */
  jsPDFAPI.link = function(x, y, w, h, options) {
    var pageInfo = this.internal.getCurrentPageInfo();
    var getHorizontalCoordinateString = this.internal.getCoordinateString;
    var getVerticalCoordinateString = this.internal.getVerticalCoordinateString;

    pageInfo.pageContext.annotations.push({
      finalBounds: {
        x: getHorizontalCoordinateString(x),
        y: getVerticalCoordinateString(y),
        w: getHorizontalCoordinateString(x + w),
        h: getVerticalCoordinateString(y + h)
      },
      options: options,
      type: "link"
    });
  };

  /**
   * Currently only supports single line text.
   * Returns the width of the text/link
   *
   * @name textWithLink
   * @function
   * @param {string} text
   * @param {number} x
   * @param {number} y
   * @param {Object} options
   * @returns {number} width the width of the text/link
   */
  jsPDFAPI.textWithLink = function(text, x, y, options) {
    var totalLineWidth = this.getTextWidth(text);
    var lineHeight = this.internal.getLineHeight() / this.internal.scaleFactor;
    var linkHeight, linkWidth;

    // Checking if maxWidth option is passed to determine lineWidth and number of lines for each line
    if (options.maxWidth !== undefined) {
      var { maxWidth } = options;
      linkWidth = maxWidth;
      var numOfLines = this.splitTextToSize(text, linkWidth).length;
      linkHeight = Math.ceil(lineHeight * numOfLines);
    } else {
      linkWidth = totalLineWidth;
      linkHeight = lineHeight;
    }

    this.text(text, x, y, options);

    //TODO We really need the text baseline height to do this correctly.
    // Or ability to draw text on top, bottom, center, or baseline.
    y += lineHeight * 0.2;
    //handle x position based on the align option
    if (options.align === "center") {
      x = x - totalLineWidth / 2; //since starting from center move the x position by half of text width
    }
    if (options.align === "right") {
      x = x - totalLineWidth;
    }
    this.link(x, y - lineHeight, linkWidth, linkHeight, options);
    return totalLineWidth;
  };

  //TODO move into external library
  /**
   * @name getTextWidth
   * @function
   * @param {string} text
   * @returns {number} txtWidth
   */
  jsPDFAPI.getTextWidth = function(text) {
    var fontSize = this.internal.getFontSize();
    var txtWidth =
      (this.getStringUnitWidth(text) * fontSize) / this.internal.scaleFactor;
    return txtWidth;
  };

  return this;
})(jsPDF.API);

/**
 * @license
 * Copyright (c) 2017 Aras Abbasi
 *
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 */

/**
 * jsPDF arabic parser PlugIn
 *
 * @name arabic
 * @module
 */
(function(jsPDFAPI) {

  /**
   * Arabic shape substitutions: char code => (isolated, final, initial, medial).
   * Arabic Substition A
   */
  var arabicSubstitionA = {
    0x0621: [0xfe80], // ARABIC LETTER HAMZA
    0x0622: [0xfe81, 0xfe82], // ARABIC LETTER ALEF WITH MADDA ABOVE
    0x0623: [0xfe83, 0xfe84], // ARABIC LETTER ALEF WITH HAMZA ABOVE
    0x0624: [0xfe85, 0xfe86], // ARABIC LETTER WAW WITH HAMZA ABOVE
    0x0625: [0xfe87, 0xfe88], // ARABIC LETTER ALEF WITH HAMZA BELOW
    0x0626: [0xfe89, 0xfe8a, 0xfe8b, 0xfe8c], // ARABIC LETTER YEH WITH HAMZA ABOVE
    0x0627: [0xfe8d, 0xfe8e], // ARABIC LETTER ALEF
    0x0628: [0xfe8f, 0xfe90, 0xfe91, 0xfe92], // ARABIC LETTER BEH
    0x0629: [0xfe93, 0xfe94], // ARABIC LETTER TEH MARBUTA
    0x062a: [0xfe95, 0xfe96, 0xfe97, 0xfe98], // ARABIC LETTER TEH
    0x062b: [0xfe99, 0xfe9a, 0xfe9b, 0xfe9c], // ARABIC LETTER THEH
    0x062c: [0xfe9d, 0xfe9e, 0xfe9f, 0xfea0], // ARABIC LETTER JEEM
    0x062d: [0xfea1, 0xfea2, 0xfea3, 0xfea4], // ARABIC LETTER HAH
    0x062e: [0xfea5, 0xfea6, 0xfea7, 0xfea8], // ARABIC LETTER KHAH
    0x062f: [0xfea9, 0xfeaa], // ARABIC LETTER DAL
    0x0630: [0xfeab, 0xfeac], // ARABIC LETTER THAL
    0x0631: [0xfead, 0xfeae], // ARABIC LETTER REH
    0x0632: [0xfeaf, 0xfeb0], // ARABIC LETTER ZAIN
    0x0633: [0xfeb1, 0xfeb2, 0xfeb3, 0xfeb4], // ARABIC LETTER SEEN
    0x0634: [0xfeb5, 0xfeb6, 0xfeb7, 0xfeb8], // ARABIC LETTER SHEEN
    0x0635: [0xfeb9, 0xfeba, 0xfebb, 0xfebc], // ARABIC LETTER SAD
    0x0636: [0xfebd, 0xfebe, 0xfebf, 0xfec0], // ARABIC LETTER DAD
    0x0637: [0xfec1, 0xfec2, 0xfec3, 0xfec4], // ARABIC LETTER TAH
    0x0638: [0xfec5, 0xfec6, 0xfec7, 0xfec8], // ARABIC LETTER ZAH
    0x0639: [0xfec9, 0xfeca, 0xfecb, 0xfecc], // ARABIC LETTER AIN
    0x063a: [0xfecd, 0xfece, 0xfecf, 0xfed0], // ARABIC LETTER GHAIN
    0x0641: [0xfed1, 0xfed2, 0xfed3, 0xfed4], // ARABIC LETTER FEH
    0x0642: [0xfed5, 0xfed6, 0xfed7, 0xfed8], // ARABIC LETTER QAF
    0x0643: [0xfed9, 0xfeda, 0xfedb, 0xfedc], // ARABIC LETTER KAF
    0x0644: [0xfedd, 0xfede, 0xfedf, 0xfee0], // ARABIC LETTER LAM
    0x0645: [0xfee1, 0xfee2, 0xfee3, 0xfee4], // ARABIC LETTER MEEM
    0x0646: [0xfee5, 0xfee6, 0xfee7, 0xfee8], // ARABIC LETTER NOON
    0x0647: [0xfee9, 0xfeea, 0xfeeb, 0xfeec], // ARABIC LETTER HEH
    0x0648: [0xfeed, 0xfeee], // ARABIC LETTER WAW
    0x0649: [0xfeef, 0xfef0, 64488, 64489], // ARABIC LETTER ALEF MAKSURA
    0x064a: [0xfef1, 0xfef2, 0xfef3, 0xfef4], // ARABIC LETTER YEH
    0x0671: [0xfb50, 0xfb51], // ARABIC LETTER ALEF WASLA
    0x0677: [0xfbdd], // ARABIC LETTER U WITH HAMZA ABOVE
    0x0679: [0xfb66, 0xfb67, 0xfb68, 0xfb69], // ARABIC LETTER TTEH
    0x067a: [0xfb5e, 0xfb5f, 0xfb60, 0xfb61], // ARABIC LETTER TTEHEH
    0x067b: [0xfb52, 0xfb53, 0xfb54, 0xfb55], // ARABIC LETTER BEEH
    0x067e: [0xfb56, 0xfb57, 0xfb58, 0xfb59], // ARABIC LETTER PEH
    0x067f: [0xfb62, 0xfb63, 0xfb64, 0xfb65], // ARABIC LETTER TEHEH
    0x0680: [0xfb5a, 0xfb5b, 0xfb5c, 0xfb5d], // ARABIC LETTER BEHEH
    0x0683: [0xfb76, 0xfb77, 0xfb78, 0xfb79], // ARABIC LETTER NYEH
    0x0684: [0xfb72, 0xfb73, 0xfb74, 0xfb75], // ARABIC LETTER DYEH
    0x0686: [0xfb7a, 0xfb7b, 0xfb7c, 0xfb7d], // ARABIC LETTER TCHEH
    0x0687: [0xfb7e, 0xfb7f, 0xfb80, 0xfb81], // ARABIC LETTER TCHEHEH
    0x0688: [0xfb88, 0xfb89], // ARABIC LETTER DDAL
    0x068c: [0xfb84, 0xfb85], // ARABIC LETTER DAHAL
    0x068d: [0xfb82, 0xfb83], // ARABIC LETTER DDAHAL
    0x068e: [0xfb86, 0xfb87], // ARABIC LETTER DUL
    0x0691: [0xfb8c, 0xfb8d], // ARABIC LETTER RREH
    0x0698: [0xfb8a, 0xfb8b], // ARABIC LETTER JEH
    0x06a4: [0xfb6a, 0xfb6b, 0xfb6c, 0xfb6d], // ARABIC LETTER VEH
    0x06a6: [0xfb6e, 0xfb6f, 0xfb70, 0xfb71], // ARABIC LETTER PEHEH
    0x06a9: [0xfb8e, 0xfb8f, 0xfb90, 0xfb91], // ARABIC LETTER KEHEH
    0x06ad: [0xfbd3, 0xfbd4, 0xfbd5, 0xfbd6], // ARABIC LETTER NG
    0x06af: [0xfb92, 0xfb93, 0xfb94, 0xfb95], // ARABIC LETTER GAF
    0x06b1: [0xfb9a, 0xfb9b, 0xfb9c, 0xfb9d], // ARABIC LETTER NGOEH
    0x06b3: [0xfb96, 0xfb97, 0xfb98, 0xfb99], // ARABIC LETTER GUEH
    0x06ba: [0xfb9e, 0xfb9f], // ARABIC LETTER NOON GHUNNA
    0x06bb: [0xfba0, 0xfba1, 0xfba2, 0xfba3], // ARABIC LETTER RNOON
    0x06be: [0xfbaa, 0xfbab, 0xfbac, 0xfbad], // ARABIC LETTER HEH DOACHASHMEE
    0x06c0: [0xfba4, 0xfba5], // ARABIC LETTER HEH WITH YEH ABOVE
    0x06c1: [0xfba6, 0xfba7, 0xfba8, 0xfba9], // ARABIC LETTER HEH GOAL
    0x06c5: [0xfbe0, 0xfbe1], // ARABIC LETTER KIRGHIZ OE
    0x06c6: [0xfbd9, 0xfbda], // ARABIC LETTER OE
    0x06c7: [0xfbd7, 0xfbd8], // ARABIC LETTER U
    0x06c8: [0xfbdb, 0xfbdc], // ARABIC LETTER YU
    0x06c9: [0xfbe2, 0xfbe3], // ARABIC LETTER KIRGHIZ YU
    0x06cb: [0xfbde, 0xfbdf], // ARABIC LETTER VE
    0x06cc: [0xfbfc, 0xfbfd, 0xfbfe, 0xfbff], // ARABIC LETTER FARSI YEH
    0x06d0: [0xfbe4, 0xfbe5, 0xfbe6, 0xfbe7], //ARABIC LETTER E
    0x06d2: [0xfbae, 0xfbaf], // ARABIC LETTER YEH BARREE
    0x06d3: [0xfbb0, 0xfbb1] // ARABIC LETTER YEH BARREE WITH HAMZA ABOVE
  };

  /*
    var ligaturesSubstitutionA = {
        0xFBEA: []// ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ALEF ISOLATED FORM
    };
    */

  var ligatures = {
    0xfedf: {
      0xfe82: 0xfef5, // ARABIC LIGATURE LAM WITH ALEF WITH MADDA ABOVE ISOLATED FORM
      0xfe84: 0xfef7, // ARABIC LIGATURE LAM WITH ALEF WITH HAMZA ABOVE ISOLATED FORM
      0xfe88: 0xfef9, // ARABIC LIGATURE LAM WITH ALEF WITH HAMZA BELOW ISOLATED FORM
      0xfe8e: 0xfefb // ARABIC LIGATURE LAM WITH ALEF ISOLATED FORM
    },
    0xfee0: {
      0xfe82: 0xfef6, // ARABIC LIGATURE LAM WITH ALEF WITH MADDA ABOVE FINAL FORM
      0xfe84: 0xfef8, // ARABIC LIGATURE LAM WITH ALEF WITH HAMZA ABOVE FINAL FORM
      0xfe88: 0xfefa, // ARABIC LIGATURE LAM WITH ALEF WITH HAMZA BELOW FINAL FORM
      0xfe8e: 0xfefc // ARABIC LIGATURE LAM WITH ALEF FINAL FORM
    },
    0xfe8d: { 0xfedf: { 0xfee0: { 0xfeea: 0xfdf2 } } }, // ALLAH
    0x0651: {
      0x064c: 0xfc5e, // Shadda + Dammatan
      0x064d: 0xfc5f, // Shadda + Kasratan
      0x064e: 0xfc60, // Shadda + Fatha
      0x064f: 0xfc61, // Shadda + Damma
      0x0650: 0xfc62 // Shadda + Kasra
    }
  };

  var arabic_diacritics = {
    1612: 64606, // Shadda + Dammatan
    1613: 64607, // Shadda + Kasratan
    1614: 64608, // Shadda + Fatha
    1615: 64609, // Shadda + Damma
    1616: 64610 // Shadda + Kasra
  };

  var alfletter = [1570, 1571, 1573, 1575];

  var noChangeInForm = -1;
  var isolatedForm = 0;
  var finalForm = 1;
  var initialForm = 2;
  var medialForm = 3;

  jsPDFAPI.__arabicParser__ = {};

  //private
  var isInArabicSubstitutionA = (jsPDFAPI.__arabicParser__.isInArabicSubstitutionA = function(
    letter
  ) {
    return typeof arabicSubstitionA[letter.charCodeAt(0)] !== "undefined";
  });

  var isArabicLetter = (jsPDFAPI.__arabicParser__.isArabicLetter = function(
    letter
  ) {
    return (
      typeof letter === "string" &&
      /^[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]+$/.test(
        letter
      )
    );
  });

  var isArabicEndLetter = (jsPDFAPI.__arabicParser__.isArabicEndLetter = function(
    letter
  ) {
    return (
      isArabicLetter(letter) &&
      isInArabicSubstitutionA(letter) &&
      arabicSubstitionA[letter.charCodeAt(0)].length <= 2
    );
  });

  var isArabicAlfLetter = (jsPDFAPI.__arabicParser__.isArabicAlfLetter = function(
    letter
  ) {
    return (
      isArabicLetter(letter) && alfletter.indexOf(letter.charCodeAt(0)) >= 0
    );
  });

  jsPDFAPI.__arabicParser__.arabicLetterHasIsolatedForm = function(letter) {
    return (
      isArabicLetter(letter) &&
      isInArabicSubstitutionA(letter) &&
      arabicSubstitionA[letter.charCodeAt(0)].length >= 1
    );
  };

  var arabicLetterHasFinalForm = (jsPDFAPI.__arabicParser__.arabicLetterHasFinalForm = function(
    letter
  ) {
    return (
      isArabicLetter(letter) &&
      isInArabicSubstitutionA(letter) &&
      arabicSubstitionA[letter.charCodeAt(0)].length >= 2
    );
  });

  jsPDFAPI.__arabicParser__.arabicLetterHasInitialForm = function(letter) {
    return (
      isArabicLetter(letter) &&
      isInArabicSubstitutionA(letter) &&
      arabicSubstitionA[letter.charCodeAt(0)].length >= 3
    );
  };

  var arabicLetterHasMedialForm = (jsPDFAPI.__arabicParser__.arabicLetterHasMedialForm = function(
    letter
  ) {
    return (
      isArabicLetter(letter) &&
      isInArabicSubstitutionA(letter) &&
      arabicSubstitionA[letter.charCodeAt(0)].length == 4
    );
  });

  var resolveLigatures = (jsPDFAPI.__arabicParser__.resolveLigatures = function(
    letters
  ) {
    var i = 0;
    var tmpLigatures = ligatures;
    var result = "";
    var effectedLetters = 0;

    for (i = 0; i < letters.length; i += 1) {
      if (typeof tmpLigatures[letters.charCodeAt(i)] !== "undefined") {
        effectedLetters++;
        tmpLigatures = tmpLigatures[letters.charCodeAt(i)];

        if (typeof tmpLigatures === "number") {
          result += String.fromCharCode(tmpLigatures);
          tmpLigatures = ligatures;
          effectedLetters = 0;
        }
        if (i === letters.length - 1) {
          tmpLigatures = ligatures;
          result += letters.charAt(i - (effectedLetters - 1));
          i = i - (effectedLetters - 1);
          effectedLetters = 0;
        }
      } else {
        tmpLigatures = ligatures;
        result += letters.charAt(i - effectedLetters);
        i = i - effectedLetters;
        effectedLetters = 0;
      }
    }

    return result;
  });

  jsPDFAPI.__arabicParser__.isArabicDiacritic = function(letter) {
    return (
      letter !== undefined &&
      arabic_diacritics[letter.charCodeAt(0)] !== undefined
    );
  };

  var getCorrectForm = (jsPDFAPI.__arabicParser__.getCorrectForm = function(
    currentChar,
    beforeChar,
    nextChar
  ) {
    if (!isArabicLetter(currentChar)) {
      return -1;
    }

    if (isInArabicSubstitutionA(currentChar) === false) {
      return noChangeInForm;
    }
    if (
      !arabicLetterHasFinalForm(currentChar) ||
      (!isArabicLetter(beforeChar) && !isArabicLetter(nextChar)) ||
      (!isArabicLetter(nextChar) && isArabicEndLetter(beforeChar)) ||
      (isArabicEndLetter(currentChar) && !isArabicLetter(beforeChar)) ||
      (isArabicEndLetter(currentChar) && isArabicAlfLetter(beforeChar)) ||
      (isArabicEndLetter(currentChar) && isArabicEndLetter(beforeChar))
    ) {
      return isolatedForm;
    }

    if (
      arabicLetterHasMedialForm(currentChar) &&
      isArabicLetter(beforeChar) &&
      !isArabicEndLetter(beforeChar) &&
      isArabicLetter(nextChar) &&
      arabicLetterHasFinalForm(nextChar)
    ) {
      return medialForm;
    }

    if (isArabicEndLetter(currentChar) || !isArabicLetter(nextChar)) {
      return finalForm;
    }
    return initialForm;
  });

  /**
   * @name processArabic
   * @function
   * @param {string} text
   * @returns {string}
   */
  var parseArabic = function(text) {
    text = text || "";

    var result = "";
    var i = 0;
    var j = 0;
    var position = 0;
    var currentLetter = "";
    var prevLetter = "";
    var nextLetter = "";

    var words = text.split("\\s+");
    var newWords = [];
    for (i = 0; i < words.length; i += 1) {
      newWords.push("");
      for (j = 0; j < words[i].length; j += 1) {
        currentLetter = words[i][j];
        prevLetter = words[i][j - 1];
        nextLetter = words[i][j + 1];
        if (isArabicLetter(currentLetter)) {
          position = getCorrectForm(currentLetter, prevLetter, nextLetter);
          if (position !== -1) {
            newWords[i] += String.fromCharCode(
              arabicSubstitionA[currentLetter.charCodeAt(0)][position]
            );
          } else {
            newWords[i] += currentLetter;
          }
        } else {
          newWords[i] += currentLetter;
        }
      }

      newWords[i] = resolveLigatures(newWords[i]);
    }
    result = newWords.join(" ");

    return result;
  };

  var processArabic = (jsPDFAPI.__arabicParser__.processArabic = jsPDFAPI.processArabic = function() {
    var text =
      typeof arguments[0] === "string" ? arguments[0] : arguments[0].text;
    var tmpText = [];
    var result;

    if (Array.isArray(text)) {
      var i = 0;
      tmpText = [];
      for (i = 0; i < text.length; i += 1) {
        if (Array.isArray(text[i])) {
          tmpText.push([parseArabic(text[i][0]), text[i][1], text[i][2]]);
        } else {
          tmpText.push([parseArabic(text[i])]);
        }
      }
      result = tmpText;
    } else {
      result = parseArabic(text);
    }
    if (typeof arguments[0] === "string") {
      return result;
    } else {
      arguments[0].text = result;
      return arguments[0];
    }
  });

  jsPDFAPI.events.push(["preProcessText", processArabic]);
})(jsPDF.API);

/** @license
 * jsPDF Autoprint Plugin
 *
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 */

/**
 * @name autoprint
 * @module
 */
(function(jsPDFAPI) {

  /**
   * Makes the PDF automatically open the print-Dialog when opened in a PDF-viewer.
   *
   * @name autoPrint
   * @function
   * @param {Object} options (optional) Set the attribute variant to 'non-conform' (default) or 'javascript' to activate different methods of automatic printing when opening in a PDF-viewer .
   * @returns {jsPDF}
   * @example
   * var doc = new jsPDF();
   * doc.text(10, 10, 'This is a test');
   * doc.autoPrint({variant: 'non-conform'});
   * doc.save('autoprint.pdf');
   */
  jsPDFAPI.autoPrint = function(options) {
    var refAutoPrintTag;
    options = options || {};
    options.variant = options.variant || "non-conform";

    switch (options.variant) {
      case "javascript":
        //https://github.com/Rob--W/pdf.js/commit/c676ecb5a0f54677b9f3340c3ef2cf42225453bb
        this.addJS("print({});");
        break;
      case "non-conform":
      default:
        this.internal.events.subscribe("postPutResources", function() {
          refAutoPrintTag = this.internal.newObject();
          this.internal.out("<<");
          this.internal.out("/S /Named");
          this.internal.out("/Type /Action");
          this.internal.out("/N /Print");
          this.internal.out(">>");
          this.internal.out("endobj");
        });

        this.internal.events.subscribe("putCatalog", function() {
          this.internal.out("/OpenAction " + refAutoPrintTag + " 0 R");
        });
        break;
    }
    return this;
  };
})(jsPDF.API);

/**
 * @license
 * Copyright (c) 2014 Steven Spungin (TwelveTone LLC)  steven@twelvetone.tv
 *
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 */

/**
 * jsPDF Canvas PlugIn
 * This plugin mimics the HTML5 Canvas
 *
 * The goal is to provide a way for current canvas users to print directly to a PDF.
 * @name canvas
 * @module
 */
(function(jsPDFAPI) {

  /**
   * @class Canvas
   * @classdesc A Canvas Wrapper for jsPDF
   */
  var Canvas = function() {
    var jsPdfInstance = undefined;
    Object.defineProperty(this, "pdf", {
      get: function() {
        return jsPdfInstance;
      },
      set: function(value) {
        jsPdfInstance = value;
      }
    });

    var _width = 150;
    /**
     * The height property is a positive integer reflecting the height HTML attribute of the <canvas> element interpreted in CSS pixels. When the attribute is not specified, or if it is set to an invalid value, like a negative, the default value of 150 is used.
     * This is one of the two properties, the other being width, that controls the size of the canvas.
     *
     * @name width
     */
    Object.defineProperty(this, "width", {
      get: function() {
        return _width;
      },
      set: function(value) {
        if (isNaN(value) || Number.isInteger(value) === false || value < 0) {
          _width = 150;
        } else {
          _width = value;
        }
        if (this.getContext("2d").pageWrapXEnabled) {
          this.getContext("2d").pageWrapX = _width + 1;
        }
      }
    });

    var _height = 300;
    /**
     * The width property is a positive integer reflecting the width HTML attribute of the <canvas> element interpreted in CSS pixels. When the attribute is not specified, or if it is set to an invalid value, like a negative, the default value of 300 is used.
     * This is one of the two properties, the other being height, that controls the size of the canvas.
     *
     * @name height
     */
    Object.defineProperty(this, "height", {
      get: function() {
        return _height;
      },
      set: function(value) {
        if (isNaN(value) || Number.isInteger(value) === false || value < 0) {
          _height = 300;
        } else {
          _height = value;
        }
        if (this.getContext("2d").pageWrapYEnabled) {
          this.getContext("2d").pageWrapY = _height + 1;
        }
      }
    });

    var _childNodes = [];
    Object.defineProperty(this, "childNodes", {
      get: function() {
        return _childNodes;
      },
      set: function(value) {
        _childNodes = value;
      }
    });

    var _style = {};
    Object.defineProperty(this, "style", {
      get: function() {
        return _style;
      },
      set: function(value) {
        _style = value;
      }
    });

    Object.defineProperty(this, "parentNode", {});
  };

  /**
   * The getContext() method returns a drawing context on the canvas, or null if the context identifier is not supported.
   *
   * @name getContext
   * @function
   * @param {string} contextType Is a String containing the context identifier defining the drawing context associated to the canvas. Possible value is "2d", leading to the creation of a Context2D object representing a two-dimensional rendering context.
   * @param {object} contextAttributes
   */
  Canvas.prototype.getContext = function(contextType, contextAttributes) {
    contextType = contextType || "2d";
    var key;

    if (contextType !== "2d") {
      return null;
    }
    for (key in contextAttributes) {
      if (this.pdf.context2d.hasOwnProperty(key)) {
        this.pdf.context2d[key] = contextAttributes[key];
      }
    }
    this.pdf.context2d._canvas = this;
    return this.pdf.context2d;
  };

  /**
   * The toDataURL() method is just a stub to throw an error if accidently called.
   *
   * @name toDataURL
   * @function
   */
  Canvas.prototype.toDataURL = function() {
    throw new Error("toDataURL is not implemented.");
  };

  jsPDFAPI.events.push([
    "initialized",
    function() {
      this.canvas = new Canvas();
      this.canvas.pdf = this;
    }
  ]);

  return this;
})(jsPDF.API);

/**
 * @license
 * ====================================================================
 * Copyright (c) 2013 Youssef Beddad, youssef.beddad@gmail.com
 *               2013 Eduardo Menezes de Morais, eduardo.morais@usp.br
 *               2013 Lee Driscoll, https://github.com/lsdriscoll
 *               2014 Juan Pablo Gaviria, https://github.com/juanpgaviria
 *               2014 James Hall, james@parall.ax
 *               2014 Diego Casorran, https://github.com/diegocr
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * ====================================================================
 */

/**
 * @name cell
 * @module
 */
(function(jsPDFAPI) {

  var NO_MARGINS = { left: 0, top: 0, bottom: 0, right: 0 };

  var px2pt = (0.264583 * 72) / 25.4;
  var printingHeaderRow = false;

  var _initialize = function() {
    if (typeof this.internal.__cell__ === "undefined") {
      this.internal.__cell__ = {};
      this.internal.__cell__.padding = 3;
      this.internal.__cell__.headerFunction = undefined;
      this.internal.__cell__.margins = Object.assign({}, NO_MARGINS);
      this.internal.__cell__.margins.width = this.getPageWidth();
      _reset.call(this);
    }
  };

  var _reset = function() {
    this.internal.__cell__.lastCell = new Cell();
    this.internal.__cell__.pages = 1;
  };

  var Cell = function() {
    var _x = arguments[0];
    Object.defineProperty(this, "x", {
      enumerable: true,
      get: function() {
        return _x;
      },
      set: function(value) {
        _x = value;
      }
    });
    var _y = arguments[1];
    Object.defineProperty(this, "y", {
      enumerable: true,
      get: function() {
        return _y;
      },
      set: function(value) {
        _y = value;
      }
    });
    var _width = arguments[2];
    Object.defineProperty(this, "width", {
      enumerable: true,
      get: function() {
        return _width;
      },
      set: function(value) {
        _width = value;
      }
    });
    var _height = arguments[3];
    Object.defineProperty(this, "height", {
      enumerable: true,
      get: function() {
        return _height;
      },
      set: function(value) {
        _height = value;
      }
    });
    var _text = arguments[4];
    Object.defineProperty(this, "text", {
      enumerable: true,
      get: function() {
        return _text;
      },
      set: function(value) {
        _text = value;
      }
    });
    var _lineNumber = arguments[5];
    Object.defineProperty(this, "lineNumber", {
      enumerable: true,
      get: function() {
        return _lineNumber;
      },
      set: function(value) {
        _lineNumber = value;
      }
    });
    var _align = arguments[6];
    Object.defineProperty(this, "align", {
      enumerable: true,
      get: function() {
        return _align;
      },
      set: function(value) {
        _align = value;
      }
    });

    return this;
  };

  Cell.prototype.clone = function() {
    return new Cell(
      this.x,
      this.y,
      this.width,
      this.height,
      this.text,
      this.lineNumber,
      this.align
    );
  };

  Cell.prototype.toArray = function() {
    return [
      this.x,
      this.y,
      this.width,
      this.height,
      this.text,
      this.lineNumber,
      this.align
    ];
  };

  /**
   * @name setHeaderFunction
   * @function
   * @param {function} func
   */
  jsPDFAPI.setHeaderFunction = function(func) {
    _initialize.call(this);
    this.internal.__cell__.headerFunction =
      typeof func === "function" ? func : undefined;
    return this;
  };

  /**
   * @name getTextDimensions
   * @function
   * @param {string} txt
   * @returns {Object} dimensions
   */
  jsPDFAPI.getTextDimensions = function(text, options) {
    _initialize.call(this);
    options = options || {};
    var fontSize = options.fontSize || this.getFontSize();
    var font = options.font || this.getFont();
    var scaleFactor = options.scaleFactor || this.internal.scaleFactor;
    var width = 0;
    var amountOfLines = 0;
    var height = 0;
    var tempWidth = 0;
    var scope = this;

    if (!Array.isArray(text) && typeof text !== "string") {
      if (typeof text === "number") {
        text = String(text);
      } else {
        throw new Error(
          "getTextDimensions expects text-parameter to be of type String or type Number or an Array of Strings."
        );
      }
    }

    const maxWidth = options.maxWidth;
    if (maxWidth > 0) {
      if (typeof text === "string") {
        text = this.splitTextToSize(text, maxWidth);
      } else if (Object.prototype.toString.call(text) === "[object Array]") {
        text = text.reduce(function(acc, textLine) {
          return acc.concat(scope.splitTextToSize(textLine, maxWidth));
        }, []);
      }
    } else {
      // Without the else clause, it will not work if you do not pass along maxWidth
      text = Array.isArray(text) ? text : [text];
    }

    for (var i = 0; i < text.length; i++) {
      tempWidth = this.getStringUnitWidth(text[i], { font: font }) * fontSize;
      if (width < tempWidth) {
        width = tempWidth;
      }
    }

    if (width !== 0) {
      amountOfLines = text.length;
    }

    width = width / scaleFactor;
    height = Math.max(
      (amountOfLines * fontSize * this.getLineHeightFactor() -
        fontSize * (this.getLineHeightFactor() - 1)) /
        scaleFactor,
      0
    );
    return { w: width, h: height };
  };

  /**
   * @name cellAddPage
   * @function
   */
  jsPDFAPI.cellAddPage = function() {
    _initialize.call(this);

    this.addPage();

    var margins = this.internal.__cell__.margins || NO_MARGINS;
    this.internal.__cell__.lastCell = new Cell(
      margins.left,
      margins.top,
      undefined,
      undefined
    );
    this.internal.__cell__.pages += 1;

    return this;
  };

  /**
   * @name cell
   * @function
   * @param {number} x
   * @param {number} y
   * @param {number} width
   * @param {number} height
   * @param {string} text
   * @param {number} lineNumber lineNumber
   * @param {string} align
   * @return {jsPDF} jsPDF-instance
   */
  var cell = (jsPDFAPI.cell = function() {
    var currentCell;

    if (arguments[0] instanceof Cell) {
      currentCell = arguments[0];
    } else {
      currentCell = new Cell(
        arguments[0],
        arguments[1],
        arguments[2],
        arguments[3],
        arguments[4],
        arguments[5]
      );
    }
    _initialize.call(this);
    var lastCell = this.internal.__cell__.lastCell;
    var padding = this.internal.__cell__.padding;
    var margins = this.internal.__cell__.margins || NO_MARGINS;
    var tableHeaderRow = this.internal.__cell__.tableHeaderRow;
    var printHeaders = this.internal.__cell__.printHeaders;
    // If this is not the first cell, we must change its position
    if (typeof lastCell.lineNumber !== "undefined") {
      if (lastCell.lineNumber === currentCell.lineNumber) {
        //Same line
        currentCell.x = (lastCell.x || 0) + (lastCell.width || 0);
        currentCell.y = lastCell.y || 0;
      } else {
        //New line
        if (
          lastCell.y + lastCell.height + currentCell.height + margins.bottom >
          this.getPageHeight()
        ) {
          this.cellAddPage();
          currentCell.y = margins.top;
          if (printHeaders && tableHeaderRow) {
            this.printHeaderRow(currentCell.lineNumber, true);
            currentCell.y += tableHeaderRow[0].height;
          }
        } else {
          currentCell.y = lastCell.y + lastCell.height || currentCell.y;
        }
      }
    }

    if (typeof currentCell.text[0] !== "undefined") {
      this.rect(
        currentCell.x,
        currentCell.y,
        currentCell.width,
        currentCell.height,
        printingHeaderRow === true ? "FD" : undefined
      );
      if (currentCell.align === "right") {
        this.text(
          currentCell.text,
          currentCell.x + currentCell.width - padding,
          currentCell.y + padding,
          { align: "right", baseline: "top" }
        );
      } else if (currentCell.align === "center") {
        this.text(
          currentCell.text,
          currentCell.x + currentCell.width / 2,
          currentCell.y + padding,
          {
            align: "center",
            baseline: "top",
            maxWidth: currentCell.width - padding - padding
          }
        );
      } else {
        this.text(
          currentCell.text,
          currentCell.x + padding,
          currentCell.y + padding,
          {
            align: "left",
            baseline: "top",
            maxWidth: currentCell.width - padding - padding
          }
        );
      }
    }
    this.internal.__cell__.lastCell = currentCell;
    return this;
  });

  /**
     * Create a table from a set of data.
     * @name table
     * @function
     * @param {Integer} [x] : left-position for top-left corner of table
     * @param {Integer} [y] top-position for top-left corner of table
     * @param {Object[]} [data] An array of objects containing key-value pairs corresponding to a row of data.
     * @param {String[]} [headers] Omit or null to auto-generate headers at a performance cost

     * @param {Object} [config.printHeaders] True to print column headers at the top of every page
     * @param {Object} [config.autoSize] True to dynamically set the column widths to match the widest cell value
     * @param {Object} [config.margins] margin values for left, top, bottom, and width
     * @param {Object} [config.fontSize] Integer fontSize to use (optional)
     * @param {Object} [config.padding] cell-padding in pt to use (optional)
     * @param {Object} [config.headerBackgroundColor] default is #c8c8c8 (optional)
     * @param {Object} [config.headerTextColor] default is #000 (optional)
     * @param {Object} [config.rowStart] callback to handle before print each row (optional)
     * @param {Object} [config.cellStart] callback to handle before print each cell (optional)
     * @returns {jsPDF} jsPDF-instance
     */

  jsPDFAPI.table = function(x, y, data, headers, config) {
    _initialize.call(this);
    if (!data) {
      throw new Error("No data for PDF table.");
    }

    config = config || {};

    var headerNames = [],
      headerLabels = [],
      headerAligns = [],
      i,
      columnMatrix = {},
      columnWidths = {},
      column,
      columnMinWidths = [],
      j,
      tableHeaderConfigs = [],
      //set up defaults. If a value is provided in config, defaults will be overwritten:
      autoSize = config.autoSize || false,
      printHeaders = config.printHeaders === false ? false : true,
      fontSize =
        config.css && typeof config.css["font-size"] !== "undefined"
          ? config.css["font-size"] * 16
          : config.fontSize || 12,
      margins =
        config.margins ||
        Object.assign({ width: this.getPageWidth() }, NO_MARGINS),
      padding = typeof config.padding === "number" ? config.padding : 3,
      headerBackgroundColor = config.headerBackgroundColor || "#c8c8c8",
      headerTextColor = config.headerTextColor || "#000";

    _reset.call(this);

    this.internal.__cell__.printHeaders = printHeaders;
    this.internal.__cell__.margins = margins;
    this.internal.__cell__.table_font_size = fontSize;
    this.internal.__cell__.padding = padding;
    this.internal.__cell__.headerBackgroundColor = headerBackgroundColor;
    this.internal.__cell__.headerTextColor = headerTextColor;
    this.setFontSize(fontSize);

    // Set header values
    if (headers === undefined || headers === null) {
      // No headers defined so we derive from data
      headerNames = Object.keys(data[0]);
      headerLabels = headerNames;
      headerAligns = headerNames.map(function() {
        return "left";
      });
    } else if (Array.isArray(headers) && typeof headers[0] === "object") {
      headerNames = headers.map(function(header) {
        return header.name;
      });
      headerLabels = headers.map(function(header) {
        return header.prompt || header.name || "";
      });
      headerAligns = headers.map(function(header) {
        return header.align || "left";
      });
      // Split header configs into names and prompts
      for (i = 0; i < headers.length; i += 1) {
        columnWidths[headers[i].name] = headers[i].width * px2pt;
      }
    } else if (Array.isArray(headers) && typeof headers[0] === "string") {
      headerNames = headers;
      headerLabels = headerNames;
      headerAligns = headerNames.map(function() {
        return "left";
      });
    }

    if (
      autoSize ||
      (Array.isArray(headers) && typeof headers[0] === "string")
    ) {
      var headerName;
      for (i = 0; i < headerNames.length; i += 1) {
        headerName = headerNames[i];

        // Create a matrix of columns e.g., {column_title: [row1_Record, row2_Record]}

        columnMatrix[headerName] = data.map(function(rec) {
          return rec[headerName];
        });

        // get header width
        this.setFont(undefined, "bold");
        columnMinWidths.push(
          this.getTextDimensions(headerLabels[i], {
            fontSize: this.internal.__cell__.table_font_size,
            scaleFactor: this.internal.scaleFactor
          }).w
        );
        column = columnMatrix[headerName];

        // get cell widths
        this.setFont(undefined, "normal");
        for (j = 0; j < column.length; j += 1) {
          columnMinWidths.push(
            this.getTextDimensions(column[j], {
              fontSize: this.internal.__cell__.table_font_size,
              scaleFactor: this.internal.scaleFactor
            }).w
          );
        }

        // get final column width
        columnWidths[headerName] =
          Math.max.apply(null, columnMinWidths) + padding + padding;

        //have to reset
        columnMinWidths = [];
      }
    }

    // -- Construct the table

    if (printHeaders) {
      var row = {};
      for (i = 0; i < headerNames.length; i += 1) {
        row[headerNames[i]] = {};
        row[headerNames[i]].text = headerLabels[i];
        row[headerNames[i]].align = headerAligns[i];
      }

      var rowHeight = calculateLineHeight.call(this, row, columnWidths);

      // Construct the header row
      tableHeaderConfigs = headerNames.map(function(value) {
        return new Cell(
          x,
          y,
          columnWidths[value],
          rowHeight,
          row[value].text,
          undefined,
          row[value].align
        );
      });

      // Store the table header config
      this.setTableHeaderRow(tableHeaderConfigs);

      // Print the header for the start of the table
      this.printHeaderRow(1, false);
    }

    // Construct the data rows

    var align = headers.reduce(function(pv, cv) {
      pv[cv.name] = cv.align;
      return pv;
    }, {});
    for (i = 0; i < data.length; i += 1) {
      if ("rowStart" in config && config.rowStart instanceof Function) {
        config.rowStart(
          {
            row: i,
            data: data[i]
          },
          this
        );
      }
      var lineHeight = calculateLineHeight.call(this, data[i], columnWidths);

      for (j = 0; j < headerNames.length; j += 1) {
        var cellData = data[i][headerNames[j]];
        if ("cellStart" in config && config.cellStart instanceof Function) {
          config.cellStart(
            {
              row: i,
              col: j,
              data: cellData
            },
            this
          );
        }
        cell.call(
          this,
          new Cell(
            x,
            y,
            columnWidths[headerNames[j]],
            lineHeight,
            cellData,
            i + 2,
            align[headerNames[j]]
          )
        );
      }
    }
    this.internal.__cell__.table_x = x;
    this.internal.__cell__.table_y = y;
    return this;
  };

  /**
   * Calculate the height for containing the highest column
   *
   * @name calculateLineHeight
   * @function
   * @param {Object[]} model is the line of data we want to calculate the height of
   * @param {Integer[]} columnWidths is size of each column
   * @returns {number} lineHeight
   * @private
   */
  var calculateLineHeight = function calculateLineHeight(model, columnWidths) {
    var padding = this.internal.__cell__.padding;
    var fontSize = this.internal.__cell__.table_font_size;
    var scaleFactor = this.internal.scaleFactor;

    return Object.keys(model)
      .map(function(key) {
        var value = model[key];
        return this.splitTextToSize(
          value.hasOwnProperty("text") ? value.text : value,
          columnWidths[key] - padding - padding
        );
      }, this)
      .map(function(value) {
        return (
          (this.getLineHeightFactor() * value.length * fontSize) / scaleFactor +
          padding +
          padding
        );
      }, this)
      .reduce(function(pv, cv) {
        return Math.max(pv, cv);
      }, 0);
  };

  /**
   * Store the config for outputting a table header
   *
   * @name setTableHeaderRow
   * @function
   * @param {Object[]} config
   * An array of cell configs that would define a header row: Each config matches the config used by jsPDFAPI.cell
   * except the lineNumber parameter is excluded
   */
  jsPDFAPI.setTableHeaderRow = function(config) {
    _initialize.call(this);
    this.internal.__cell__.tableHeaderRow = config;
  };

  /**
   * Output the store header row
   *
   * @name printHeaderRow
   * @function
   * @param {number} lineNumber The line number to output the header at
   * @param {boolean} new_page
   */
  jsPDFAPI.printHeaderRow = function(lineNumber, new_page) {
    _initialize.call(this);
    if (!this.internal.__cell__.tableHeaderRow) {
      throw new Error("Property tableHeaderRow does not exist.");
    }

    var tableHeaderCell;

    printingHeaderRow = true;
    if (typeof this.internal.__cell__.headerFunction === "function") {
      var position = this.internal.__cell__.headerFunction(
        this,
        this.internal.__cell__.pages
      );
      this.internal.__cell__.lastCell = new Cell(
        position[0],
        position[1],
        position[2],
        position[3],
        undefined,
        -1
      );
    }
    this.setFont(undefined, "bold");

    var tempHeaderConf = [];
    for (var i = 0; i < this.internal.__cell__.tableHeaderRow.length; i += 1) {
      tableHeaderCell = this.internal.__cell__.tableHeaderRow[i].clone();
      if (new_page) {
        tableHeaderCell.y = this.internal.__cell__.margins.top || 0;
        tempHeaderConf.push(tableHeaderCell);
      }
      tableHeaderCell.lineNumber = lineNumber;
      var currentTextColor = this.getTextColor();
      this.setTextColor(this.internal.__cell__.headerTextColor);
      this.setFillColor(this.internal.__cell__.headerBackgroundColor);
      cell.call(this, tableHeaderCell);
      this.setTextColor(currentTextColor);
    }
    if (tempHeaderConf.length > 0) {
      this.setTableHeaderRow(tempHeaderConf);
    }
    this.setFont(undefined, "normal");
    printingHeaderRow = false;
  };
})(jsPDF.API);

function toLookup(arr) {
  return arr.reduce(function(lookup, name, index) {
    lookup[name] = index;

    return lookup;
  }, {});
}

var fontStyleOrder = {
  italic: ["italic", "oblique", "normal"],
  oblique: ["oblique", "italic", "normal"],
  normal: ["normal", "oblique", "italic"]
};

var fontStretchOrder = [
  "ultra-condensed",
  "extra-condensed",
  "condensed",
  "semi-condensed",
  "normal",
  "semi-expanded",
  "expanded",
  "extra-expanded",
  "ultra-expanded"
];

// For a given font-stretch value, we need to know where to start our search
// from in the fontStretchOrder list.
var fontStretchLookup = toLookup(fontStretchOrder);

var fontWeights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
var fontWeightsLookup = toLookup(fontWeights);

function normalizeFontStretch(stretch) {
  stretch = stretch || "normal";

  return typeof fontStretchLookup[stretch] === "number" ? stretch : "normal";
}

function normalizeFontStyle(style) {
  style = style || "normal";

  return fontStyleOrder[style] ? style : "normal";
}

function normalizeFontWeight(weight) {
  if (!weight) {
    return 400;
  }

  if (typeof weight === "number") {
    // Ignore values which aren't valid font-weights.
    return weight >= 100 && weight <= 900 && weight % 100 === 0 ? weight : 400;
  }

  if (/^\d00$/.test(weight)) {
    return parseInt(weight);
  }

  switch (weight) {
    case "bold":
      return 700;

    case "normal":
    default:
      return 400;
  }
}

function normalizeFontFace(fontFace) {
  var family = fontFace.family.replace(/"|'/g, "").toLowerCase();

  var style = normalizeFontStyle(fontFace.style);
  var weight = normalizeFontWeight(fontFace.weight);
  var stretch = normalizeFontStretch(fontFace.stretch);

  return {
    family: family,
    style: style,
    weight: weight,
    stretch: stretch,
    src: fontFace.src || [],

    // The ref property maps this font-face to the font
    // added by the .addFont() method.
    ref: fontFace.ref || {
      name: family,
      style: [stretch, style, weight].join(" ")
    }
  };
}

/**
 * Turns a list of font-faces into a map, for easier lookup when resolving
 * fonts.
 * @private
 */
function buildFontFaceMap(fontFaces) {
  var map = {};

  for (var i = 0; i < fontFaces.length; ++i) {
    var normalized = normalizeFontFace(fontFaces[i]);

    var name = normalized.family;
    var stretch = normalized.stretch;
    var style = normalized.style;
    var weight = normalized.weight;

    map[name] = map[name] || {};

    map[name][stretch] = map[name][stretch] || {};
    map[name][stretch][style] = map[name][stretch][style] || {};
    map[name][stretch][style][weight] = normalized;
  }

  return map;
}

/**
 * Searches a map of stretches, weights, etc. in the given direction and
 * then, if no match has been found, in the opposite directions.
 *
 * @param {Object.<string, any>} matchingSet A map of the various font variations.
 * @param {any[]} order The order of the different variations
 * @param {number} pivot The starting point of the search in the order list.
 * @param {number} dir The initial direction of the search (desc = -1, asc = 1)
 * @private
 */

function searchFromPivot(matchingSet, order, pivot, dir) {
  var i;

  for (i = pivot; i >= 0 && i < order.length; i += dir) {
    if (matchingSet[order[i]]) {
      return matchingSet[order[i]];
    }
  }

  for (i = pivot; i >= 0 && i < order.length; i -= dir) {
    if (matchingSet[order[i]]) {
      return matchingSet[order[i]];
    }
  }
}

function resolveFontStretch(stretch, matchingSet) {
  if (matchingSet[stretch]) {
    return matchingSet[stretch];
  }

  var pivot = fontStretchLookup[stretch];

  // If the font-stretch value is normal or more condensed, we want to
  // start with a descending search, otherwise we should do ascending.
  var dir = pivot <= fontStretchLookup["normal"] ? -1 : 1;
  var match = searchFromPivot(matchingSet, fontStretchOrder, pivot, dir);

  if (!match) {
    // Since a font-family cannot exist without having at least one stretch value
    // we should never reach this point.
    throw new Error(
      "Could not find a matching font-stretch value for " + stretch
    );
  }

  return match;
}

function resolveFontStyle(fontStyle, matchingSet) {
  if (matchingSet[fontStyle]) {
    return matchingSet[fontStyle];
  }

  var ordering = fontStyleOrder[fontStyle];

  for (var i = 0; i < ordering.length; ++i) {
    if (matchingSet[ordering[i]]) {
      return matchingSet[ordering[i]];
    }
  }

  // Since a font-family cannot exist without having at least one style value
  // we should never reach this point.
  throw new Error("Could not find a matching font-style for " + fontStyle);
}

function resolveFontWeight(weight, matchingSet) {
  if (matchingSet[weight]) {
    return matchingSet[weight];
  }

  if (weight === 400 && matchingSet[500]) {
    return matchingSet[500];
  }

  if (weight === 500 && matchingSet[400]) {
    return matchingSet[400];
  }

  var pivot = fontWeightsLookup[weight];

  // If the font-stretch value is normal or more condensed, we want to
  // start with a descending search, otherwise we should do ascending.
  var dir = weight < 400 ? -1 : 1;
  var match = searchFromPivot(matchingSet, fontWeights, pivot, dir);

  if (!match) {
    // Since a font-family cannot exist without having at least one stretch value
    // we should never reach this point.
    throw new Error(
      "Could not find a matching font-weight for value " + weight
    );
  }

  return match;
}

var defaultGenericFontFamilies = {
  "sans-serif": "helvetica",
  fixed: "courier",
  monospace: "courier",
  terminal: "courier",
  cursive: "times",
  fantasy: "times",
  serif: "times"
};

var systemFonts = {
  caption: "times",
  icon: "times",
  menu: "times",
  "message-box": "times",
  "small-caption": "times",
  "status-bar": "times"
};

function ruleToString(rule) {
  return [rule.stretch, rule.style, rule.weight, rule.family].join(" ");
}

function resolveFontFace(fontFaceMap, rules, opts) {
  opts = opts || {};

  var defaultFontFamily = opts.defaultFontFamily || "times";
  var genericFontFamilies = Object.assign(
    {},
    defaultGenericFontFamilies,
    opts.genericFontFamilies || {}
  );

  var rule = null;
  var matches = null;

  for (var i = 0; i < rules.length; ++i) {
    rule = normalizeFontFace(rules[i]);

    if (genericFontFamilies[rule.family]) {
      rule.family = genericFontFamilies[rule.family];
    }

    if (fontFaceMap.hasOwnProperty(rule.family)) {
      matches = fontFaceMap[rule.family];

      break;
    }
  }

  // Always fallback to a known font family.
  matches = matches || fontFaceMap[defaultFontFamily];

  if (!matches) {
    // At this point we should definitiely have a font family, but if we
    // don't there is something wrong with our configuration
    throw new Error(
      "Could not find a font-family for the rule '" +
        ruleToString(rule) +
        "' and default family '" +
        defaultFontFamily +
        "'."
    );
  }

  matches = resolveFontStretch(rule.stretch, matches);
  matches = resolveFontStyle(rule.style, matches);
  matches = resolveFontWeight(rule.weight, matches);

  if (!matches) {
    // We should've fount
    throw new Error(
      "Failed to resolve a font for the rule '" + ruleToString(rule) + "'."
    );
  }

  return matches;
}

function eatWhiteSpace(input) {
  return input.trimLeft();
}

function parseQuotedFontFamily(input, quote) {
  var index = 0;

  while (index < input.length) {
    var current = input.charAt(index);

    if (current === quote) {
      return [input.substring(0, index), input.substring(index + 1)];
    }

    index += 1;
  }

  // Unexpected end of input
  return null;
}

function parseNonQuotedFontFamily(input) {
  // It implements part of the identifier parser here: https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
  //
  // NOTE: This parser pretty much ignores escaped identifiers and that there is a thing called unicode.
  //
  // Breakdown of regexp:
  // -[a-z_]     - when identifier starts with a hyphen, you're not allowed to have another hyphen or a digit
  // [a-z_]      - allow a-z and underscore at beginning of input
  // [a-z0-9_-]* - after that, anything goes
  var match = input.match(/^(-[a-z_]|[a-z_])[a-z0-9_-]*/i);

  // non quoted value contains illegal characters
  if (match === null) {
    return null;
  }

  return [match[0], input.substring(match[0].length)];
}

var defaultFont = ["times"];

function parseFontFamily(input) {
  var result = [];
  var ch, parsed;
  var remaining = input.trim();

  if (remaining === "") {
    return defaultFont;
  }

  if (remaining in systemFonts) {
    return [systemFonts[remaining]];
  }

  while (remaining !== "") {
    parsed = null;
    remaining = eatWhiteSpace(remaining);
    ch = remaining.charAt(0);

    switch (ch) {
      case '"':
      case "'":
        parsed = parseQuotedFontFamily(remaining.substring(1), ch);
        break;

      default:
        parsed = parseNonQuotedFontFamily(remaining);
        break;
    }

    if (parsed === null) {
      return defaultFont;
    }

    result.push(parsed[0]);

    remaining = eatWhiteSpace(parsed[1]);

    // We expect end of input or a comma separator here
    if (remaining !== "" && remaining.charAt(0) !== ",") {
      return defaultFont;
    }

    remaining = remaining.replace(/^,/, "");
  }

  return result;
}

/* eslint-disable no-fallthrough */

/**
 * This plugin mimics the HTML5 CanvasRenderingContext2D.
 *
 * The goal is to provide a way for current canvas implementations to print directly to a PDF.
 *
 * @name context2d
 * @module
 */
(function(jsPDFAPI) {
  var ContextLayer = function(ctx) {
    ctx = ctx || {};
    this.isStrokeTransparent = ctx.isStrokeTransparent || false;
    this.strokeOpacity = ctx.strokeOpacity || 1;
    this.strokeStyle = ctx.strokeStyle || "#000000";
    this.fillStyle = ctx.fillStyle || "#000000";
    this.isFillTransparent = ctx.isFillTransparent || false;
    this.fillOpacity = ctx.fillOpacity || 1;
    this.font = ctx.font || "10px sans-serif";
    this.textBaseline = ctx.textBaseline || "alphabetic";
    this.textAlign = ctx.textAlign || "left";
    this.lineWidth = ctx.lineWidth || 1;
    this.lineJoin = ctx.lineJoin || "miter";
    this.lineCap = ctx.lineCap || "butt";
    this.path = ctx.path || [];
    this.transform =
      typeof ctx.transform !== "undefined"
        ? ctx.transform.clone()
        : new Matrix();
    this.globalCompositeOperation = ctx.globalCompositeOperation || "normal";
    this.globalAlpha = ctx.globalAlpha || 1.0;
    this.clip_path = ctx.clip_path || [];
    this.currentPoint = ctx.currentPoint || new Point();
    this.miterLimit = ctx.miterLimit || 10.0;
    this.lastPoint = ctx.lastPoint || new Point();
    this.lineDashOffset = ctx.lineDashOffset || 0.0;
    this.lineDash = ctx.lineDash || [];
    this.margin = ctx.margin || [0, 0, 0, 0];
    this.prevPageLastElemOffset = ctx.prevPageLastElemOffset || 0;

    this.ignoreClearRect =
      typeof ctx.ignoreClearRect === "boolean" ? ctx.ignoreClearRect : true;
    return this;
  };

  //stub
  var f2,
    getHorizontalCoordinateString,
    getVerticalCoordinateString,
    getHorizontalCoordinate,
    getVerticalCoordinate,
    Point,
    Rectangle,
    Matrix,
    _ctx;
  jsPDFAPI.events.push([
    "initialized",
    function() {
      this.context2d = new Context2D(this);

      f2 = this.internal.f2;
      getHorizontalCoordinateString = this.internal.getCoordinateString;
      getVerticalCoordinateString = this.internal.getVerticalCoordinateString;
      getHorizontalCoordinate = this.internal.getHorizontalCoordinate;
      getVerticalCoordinate = this.internal.getVerticalCoordinate;
      Point = this.internal.Point;
      Rectangle = this.internal.Rectangle;
      Matrix = this.internal.Matrix;
      _ctx = new ContextLayer();
    }
  ]);

  var Context2D = function(pdf) {
    Object.defineProperty(this, "canvas", {
      get: function() {
        return { parentNode: false, style: false };
      }
    });

    var _pdf = pdf;
    Object.defineProperty(this, "pdf", {
      get: function() {
        return _pdf;
      }
    });

    var _pageWrapXEnabled = false;
    /**
     * @name pageWrapXEnabled
     * @type {boolean}
     * @default false
     */
    Object.defineProperty(this, "pageWrapXEnabled", {
      get: function() {
        return _pageWrapXEnabled;
      },
      set: function(value) {
        _pageWrapXEnabled = Boolean(value);
      }
    });

    var _pageWrapYEnabled = false;
    /**
     * @name pageWrapYEnabled
     * @type {boolean}
     * @default true
     */
    Object.defineProperty(this, "pageWrapYEnabled", {
      get: function() {
        return _pageWrapYEnabled;
      },
      set: function(value) {
        _pageWrapYEnabled = Boolean(value);
      }
    });

    var _posX = 0;
    /**
     * @name posX
     * @type {number}
     * @default 0
     */
    Object.defineProperty(this, "posX", {
      get: function() {
        return _posX;
      },
      set: function(value) {
        if (!isNaN(value)) {
          _posX = value;
        }
      }
    });

    var _posY = 0;
    /**
     * @name posY
     * @type {number}
     * @default 0
     */
    Object.defineProperty(this, "posY", {
      get: function() {
        return _posY;
      },
      set: function(value) {
        if (!isNaN(value)) {
          _posY = value;
        }
      }
    });

    /**
     * Gets or sets the page margin when using auto paging. Has no effect when {@link autoPaging} is off.
     * @name margin
     * @type {number|number[]}
     * @default [0, 0, 0, 0]
     */
    Object.defineProperty(this, "margin", {
      get: function() {
        return _ctx.margin;
      },
      set: function(value) {
        var margin;
        if (typeof value === "number") {
          margin = [value, value, value, value];
        } else {
          margin = new Array(4);
          margin[0] = value[0];
          margin[1] = value.length >= 2 ? value[1] : margin[0];
          margin[2] = value.length >= 3 ? value[2] : margin[0];
          margin[3] = value.length >= 4 ? value[3] : margin[1];
        }
        _ctx.margin = margin;
      }
    });

    var _autoPaging = false;
    /**
     * Gets or sets the auto paging mode. When auto paging is enabled, the context2d will automatically draw on the
     * next page if a shape or text chunk doesn't fit entirely on the current page. The context2d will create new
     * pages if required.
     *
     * Context2d supports different modes:
     * <ul>
     * <li>
     *   <code>false</code>: Auto paging is disabled.
     * </li>
     * <li>
     *   <code>true</code> or <code>'slice'</code>: Will cut shapes or text chunks across page breaks. Will possibly
     *   slice text in half, making it difficult to read.
     * </li>
     * <li>
     *   <code>'text'</code>: Trys not to cut text in half across page breaks. Works best for documents consisting
     *   mostly of a single column of text.
     * </li>
     * </ul>
     * @name Context2D#autoPaging
     * @type {boolean|"slice"|"text"}
     * @default false
     */
    Object.defineProperty(this, "autoPaging", {
      get: function() {
        return _autoPaging;
      },
      set: function(value) {
        _autoPaging = value;
      }
    });

    var lastBreak = 0;
    /**
     * @name lastBreak
     * @type {number}
     * @default 0
     */
    Object.defineProperty(this, "lastBreak", {
      get: function() {
        return lastBreak;
      },
      set: function(value) {
        lastBreak = value;
      }
    });

    var pageBreaks = [];
    /**
     * Y Position of page breaks.
     * @name pageBreaks
     * @type {number}
     * @default 0
     */
    Object.defineProperty(this, "pageBreaks", {
      get: function() {
        return pageBreaks;
      },
      set: function(value) {
        pageBreaks = value;
      }
    });

    /**
     * @name ctx
     * @type {object}
     * @default {}
     */
    Object.defineProperty(this, "ctx", {
      get: function() {
        return _ctx;
      },
      set: function(value) {
        if (value instanceof ContextLayer) {
          _ctx = value;
        }
      }
    });

    /**
     * @name path
     * @type {array}
     * @default []
     */
    Object.defineProperty(this, "path", {
      get: function() {
        return _ctx.path;
      },
      set: function(value) {
        _ctx.path = value;
      }
    });

    /**
     * @name ctxStack
     * @type {array}
     * @default []
     */
    var _ctxStack = [];
    Object.defineProperty(this, "ctxStack", {
      get: function() {
        return _ctxStack;
      },
      set: function(value) {
        _ctxStack = value;
      }
    });

    /**
     * Sets or returns the color, gradient, or pattern used to fill the drawing
     *
     * @name fillStyle
     * @default #000000
     * @property {(color|gradient|pattern)} value The color of the drawing. Default value is #000000<br />
     * A gradient object (linear or radial) used to fill the drawing (not supported by context2d)<br />
     * A pattern object to use to fill the drawing (not supported by context2d)
     */
    Object.defineProperty(this, "fillStyle", {
      get: function() {
        return this.ctx.fillStyle;
      },
      set: function(value) {
        var rgba;
        rgba = getRGBA(value);

        this.ctx.fillStyle = rgba.style;
        this.ctx.isFillTransparent = rgba.a === 0;
        this.ctx.fillOpacity = rgba.a;

        this.pdf.setFillColor(rgba.r, rgba.g, rgba.b, { a: rgba.a });
        this.pdf.setTextColor(rgba.r, rgba.g, rgba.b, { a: rgba.a });
      }
    });

    /**
     * Sets or returns the color, gradient, or pattern used for strokes
     *
     * @name strokeStyle
     * @default #000000
     * @property {color} color A CSS color value that indicates the stroke color of the drawing. Default value is #000000 (not supported by context2d)
     * @property {gradient} gradient A gradient object (linear or radial) used to create a gradient stroke (not supported by context2d)
     * @property {pattern} pattern A pattern object used to create a pattern stroke (not supported by context2d)
     */
    Object.defineProperty(this, "strokeStyle", {
      get: function() {
        return this.ctx.strokeStyle;
      },
      set: function(value) {
        var rgba = getRGBA(value);

        this.ctx.strokeStyle = rgba.style;
        this.ctx.isStrokeTransparent = rgba.a === 0;
        this.ctx.strokeOpacity = rgba.a;

        if (rgba.a === 0) {
          this.pdf.setDrawColor(255, 255, 255);
        } else if (rgba.a === 1) {
          this.pdf.setDrawColor(rgba.r, rgba.g, rgba.b);
        } else {
          this.pdf.setDrawColor(rgba.r, rgba.g, rgba.b);
        }
      }
    });

    /**
     * Sets or returns the style of the end caps for a line
     *
     * @name lineCap
     * @default butt
     * @property {(butt|round|square)} lineCap butt A flat edge is added to each end of the line <br/>
     * round A rounded end cap is added to each end of the line<br/>
     * square A square end cap is added to each end of the line<br/>
     */
    Object.defineProperty(this, "lineCap", {
      get: function() {
        return this.ctx.lineCap;
      },
      set: function(value) {
        if (["butt", "round", "square"].indexOf(value) !== -1) {
          this.ctx.lineCap = value;
          this.pdf.setLineCap(value);
        }
      }
    });

    /**
     * Sets or returns the current line width
     *
     * @name lineWidth
     * @default 1
     * @property {number} lineWidth The current line width, in pixels
     */
    Object.defineProperty(this, "lineWidth", {
      get: function() {
        return this.ctx.lineWidth;
      },
      set: function(value) {
        if (!isNaN(value)) {
          this.ctx.lineWidth = value;
          this.pdf.setLineWidth(value);
        }
      }
    });

    /**
     * Sets or returns the type of corner created, when two lines meet
     */
    Object.defineProperty(this, "lineJoin", {
      get: function() {
        return this.ctx.lineJoin;
      },
      set: function(value) {
        if (["bevel", "round", "miter"].indexOf(value) !== -1) {
          this.ctx.lineJoin = value;
          this.pdf.setLineJoin(value);
        }
      }
    });

    /**
     * A number specifying the miter limit ratio in coordinate space units. Zero, negative, Infinity, and NaN values are ignored. The default value is 10.0.
     *
     * @name miterLimit
     * @default 10
     */
    Object.defineProperty(this, "miterLimit", {
      get: function() {
        return this.ctx.miterLimit;
      },
      set: function(value) {
        if (!isNaN(value)) {
          this.ctx.miterLimit = value;
          this.pdf.setMiterLimit(value);
        }
      }
    });

    Object.defineProperty(this, "textBaseline", {
      get: function() {
        return this.ctx.textBaseline;
      },
      set: function(value) {
        this.ctx.textBaseline = value;
      }
    });

    Object.defineProperty(this, "textAlign", {
      get: function() {
        return this.ctx.textAlign;
      },
      set: function(value) {
        if (["right", "end", "center", "left", "start"].indexOf(value) !== -1) {
          this.ctx.textAlign = value;
        }
      }
    });

    var _fontFaceMap = null;

    function getFontFaceMap(pdf, fontFaces) {
      if (_fontFaceMap === null) {
        var fontMap = pdf.getFontList();

        var convertedFontFaces = convertToFontFaces(fontMap);

        _fontFaceMap = buildFontFaceMap(convertedFontFaces.concat(fontFaces));
      }

      return _fontFaceMap;
    }

    function convertToFontFaces(fontMap) {
      var fontFaces = [];

      Object.keys(fontMap).forEach(function(family) {
        var styles = fontMap[family];

        styles.forEach(function(style) {
          var fontFace = null;

          switch (style) {
            case "bold":
              fontFace = {
                family: family,
                weight: "bold"
              };
              break;

            case "italic":
              fontFace = {
                family: family,
                style: "italic"
              };
              break;

            case "bolditalic":
              fontFace = {
                family: family,
                weight: "bold",
                style: "italic"
              };
              break;

            case "":
            case "normal":
              fontFace = {
                family: family
              };
              break;
          }

          // If font-face is still null here, it is a font with some styling we don't recognize and
          // cannot map or it is a font added via the fontFaces option of .html().
          if (fontFace !== null) {
            fontFace.ref = {
              name: family,
              style: style
            };

            fontFaces.push(fontFace);
          }
        });
      });

      return fontFaces;
    }

    var _fontFaces = null;
    /**
     * A map of available font-faces, as passed in the options of
     * .html(). If set a limited implementation of the font style matching
     * algorithm defined by https://www.w3.org/TR/css-fonts-3/#font-matching-algorithm
     * will be used. If not set it will fallback to previous behavior.
     */

    Object.defineProperty(this, "fontFaces", {
      get: function() {
        return _fontFaces;
      },
      set: function(value) {
        _fontFaceMap = null;
        _fontFaces = value;
      }
    });

    Object.defineProperty(this, "font", {
      get: function() {
        return this.ctx.font;
      },
      set: function(value) {
        this.ctx.font = value;
        var rx, matches;

        //source: https://stackoverflow.com/a/10136041
        // eslint-disable-next-line no-useless-escape
        rx = /^\s*(?=(?:(?:[-a-z]+\s*){0,2}(italic|oblique))?)(?=(?:(?:[-a-z]+\s*){0,2}(small-caps))?)(?=(?:(?:[-a-z]+\s*){0,2}(bold(?:er)?|lighter|[1-9]00))?)(?:(?:normal|\1|\2|\3)\s*){0,3}((?:xx?-)?(?:small|large)|medium|smaller|larger|[.\d]+(?:\%|in|[cem]m|ex|p[ctx]))(?:\s*\/\s*(normal|[.\d]+(?:\%|in|[cem]m|ex|p[ctx])))?\s*([-_,\"\'\sa-z]+?)\s*$/i;
        matches = rx.exec(value);
        if (matches !== null) {
          var fontStyle = matches[1];
          matches[2];
          var fontWeight = matches[3];
          var fontSize = matches[4];
          matches[5];
          var fontFamily = matches[6];
        } else {
          return;
        }
        var rxFontSize = /^([.\d]+)((?:%|in|[cem]m|ex|p[ctx]))$/i;
        var fontSizeUnit = rxFontSize.exec(fontSize)[2];

        if ("px" === fontSizeUnit) {
          fontSize = Math.floor(
            parseFloat(fontSize) * this.pdf.internal.scaleFactor
          );
        } else if ("em" === fontSizeUnit) {
          fontSize = Math.floor(parseFloat(fontSize) * this.pdf.getFontSize());
        } else {
          fontSize = Math.floor(
            parseFloat(fontSize) * this.pdf.internal.scaleFactor
          );
        }

        this.pdf.setFontSize(fontSize);
        var parts = parseFontFamily(fontFamily);

        if (this.fontFaces) {
          var fontFaceMap = getFontFaceMap(this.pdf, this.fontFaces);

          var rules = parts.map(function(ff) {
            return {
              family: ff,
              stretch: "normal", // TODO: Extract font-stretch from font rule (perhaps write proper parser for it?)
              weight: fontWeight,
              style: fontStyle
            };
          });

          var font = resolveFontFace(fontFaceMap, rules);
          this.pdf.setFont(font.ref.name, font.ref.style);
          return;
        }

        var style = "";
        if (
          fontWeight === "bold" ||
          parseInt(fontWeight, 10) >= 700 ||
          fontStyle === "bold"
        ) {
          style = "bold";
        }

        if (fontStyle === "italic") {
          style += "italic";
        }

        if (style.length === 0) {
          style = "normal";
        }
        var jsPdfFontName = "";

        var fallbackFonts = {
          arial: "Helvetica",
          Arial: "Helvetica",
          verdana: "Helvetica",
          Verdana: "Helvetica",
          helvetica: "Helvetica",
          Helvetica: "Helvetica",
          "sans-serif": "Helvetica",
          fixed: "Courier",
          monospace: "Courier",
          terminal: "Courier",
          cursive: "Times",
          fantasy: "Times",
          serif: "Times"
        };

        for (var i = 0; i < parts.length; i++) {
          if (
            this.pdf.internal.getFont(parts[i], style, {
              noFallback: true,
              disableWarning: true
            }) !== undefined
          ) {
            jsPdfFontName = parts[i];
            break;
          } else if (
            style === "bolditalic" &&
            this.pdf.internal.getFont(parts[i], "bold", {
              noFallback: true,
              disableWarning: true
            }) !== undefined
          ) {
            jsPdfFontName = parts[i];
            style = "bold";
          } else if (
            this.pdf.internal.getFont(parts[i], "normal", {
              noFallback: true,
              disableWarning: true
            }) !== undefined
          ) {
            jsPdfFontName = parts[i];
            style = "normal";
            break;
          }
        }
        if (jsPdfFontName === "") {
          for (var j = 0; j < parts.length; j++) {
            if (fallbackFonts[parts[j]]) {
              jsPdfFontName = fallbackFonts[parts[j]];
              break;
            }
          }
        }
        jsPdfFontName = jsPdfFontName === "" ? "Times" : jsPdfFontName;
        this.pdf.setFont(jsPdfFontName, style);
      }
    });

    Object.defineProperty(this, "globalCompositeOperation", {
      get: function() {
        return this.ctx.globalCompositeOperation;
      },
      set: function(value) {
        this.ctx.globalCompositeOperation = value;
      }
    });

    Object.defineProperty(this, "globalAlpha", {
      get: function() {
        return this.ctx.globalAlpha;
      },
      set: function(value) {
        this.ctx.globalAlpha = value;
      }
    });

    /**
     * A float specifying the amount of the line dash offset. The default value is 0.0.
     *
     * @name lineDashOffset
     * @default 0.0
     */
    Object.defineProperty(this, "lineDashOffset", {
      get: function() {
        return this.ctx.lineDashOffset;
      },
      set: function(value) {
        this.ctx.lineDashOffset = value;
        setLineDash.call(this);
      }
    });

    // Not HTML API
    Object.defineProperty(this, "lineDash", {
      get: function() {
        return this.ctx.lineDash;
      },
      set: function(value) {
        this.ctx.lineDash = value;
        setLineDash.call(this);
      }
    });

    // Not HTML API
    Object.defineProperty(this, "ignoreClearRect", {
      get: function() {
        return this.ctx.ignoreClearRect;
      },
      set: function(value) {
        this.ctx.ignoreClearRect = Boolean(value);
      }
    });
  };

  /**
   * Sets the line dash pattern used when stroking lines.
   * @name setLineDash
   * @function
   * @description It uses an array of values that specify alternating lengths of lines and gaps which describe the pattern.
   */
  Context2D.prototype.setLineDash = function(dashArray) {
    this.lineDash = dashArray;
  };

  /**
   * gets the current line dash pattern.
   * @name getLineDash
   * @function
   * @returns {Array} An Array of numbers that specify distances to alternately draw a line and a gap (in coordinate space units). If the number, when setting the elements, is odd, the elements of the array get copied and concatenated. For example, setting the line dash to [5, 15, 25] will result in getting back [5, 15, 25, 5, 15, 25].
   */
  Context2D.prototype.getLineDash = function() {
    if (this.lineDash.length % 2) {
      // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getLineDash#return_value
      return this.lineDash.concat(this.lineDash);
    } else {
      // The copied value is returned to prevent contamination from outside.
      return this.lineDash.slice();
    }
  };

  Context2D.prototype.fill = function() {
    pathPreProcess.call(this, "fill", false);
  };

  /**
   * Actually draws the path you have defined
   *
   * @name stroke
   * @function
   * @description The stroke() method actually draws the path you have defined with all those moveTo() and lineTo() methods. The default color is black.
   */
  Context2D.prototype.stroke = function() {
    pathPreProcess.call(this, "stroke", false);
  };

  /**
   * Begins a path, or resets the current
   *
   * @name beginPath
   * @function
   * @description The beginPath() method begins a path, or resets the current path.
   */
  Context2D.prototype.beginPath = function() {
    this.path = [
      {
        type: "begin"
      }
    ];
  };

  /**
   * Moves the path to the specified point in the canvas, without creating a line
   *
   * @name moveTo
   * @function
   * @param x {Number} The x-coordinate of where to move the path to
   * @param y {Number} The y-coordinate of where to move the path to
   */
  Context2D.prototype.moveTo = function(x, y) {
    if (isNaN(x) || isNaN(y)) {
      console$1.error("jsPDF.context2d.moveTo: Invalid arguments", arguments);
      throw new Error("Invalid arguments passed to jsPDF.context2d.moveTo");
    }

    var pt = this.ctx.transform.applyToPoint(new Point(x, y));

    this.path.push({
      type: "mt",
      x: pt.x,
      y: pt.y
    });
    this.ctx.lastPoint = new Point(x, y);
  };

  /**
   * Creates a path from the current point back to the starting point
   *
   * @name closePath
   * @function
   * @description The closePath() method creates a path from the current point back to the starting point.
   */
  Context2D.prototype.closePath = function() {
    var pathBegin = new Point(0, 0);
    var i = 0;
    for (i = this.path.length - 1; i !== -1; i--) {
      if (this.path[i].type === "begin") {
        if (
          typeof this.path[i + 1] === "object" &&
          typeof this.path[i + 1].x === "number"
        ) {
          pathBegin = new Point(this.path[i + 1].x, this.path[i + 1].y);
          break;
        }
      }
    }
    this.path.push({
      type: "close"
    });
    this.ctx.lastPoint = new Point(pathBegin.x, pathBegin.y);
  };

  /**
   * Adds a new point and creates a line to that point from the last specified point in the canvas
   *
   * @name lineTo
   * @function
   * @param x The x-coordinate of where to create the line to
   * @param y The y-coordinate of where to create the line to
   * @description The lineTo() method adds a new point and creates a line TO that point FROM the last specified point in the canvas (this method does not draw the line).
   */
  Context2D.prototype.lineTo = function(x, y) {
    if (isNaN(x) || isNaN(y)) {
      console$1.error("jsPDF.context2d.lineTo: Invalid arguments", arguments);
      throw new Error("Invalid arguments passed to jsPDF.context2d.lineTo");
    }

    var pt = this.ctx.transform.applyToPoint(new Point(x, y));

    this.path.push({
      type: "lt",
      x: pt.x,
      y: pt.y
    });
    this.ctx.lastPoint = new Point(pt.x, pt.y);
  };

  /**
   * Clips a region of any shape and size from the original canvas
   *
   * @name clip
   * @function
   * @description The clip() method clips a region of any shape and size from the original canvas.
   */
  Context2D.prototype.clip = function() {
    this.ctx.clip_path = JSON.parse(JSON.stringify(this.path));
    pathPreProcess.call(this, null, true);
  };

  /**
   * Creates a cubic Bézier curve
   *
   * @name quadraticCurveTo
   * @function
   * @param cpx {Number} The x-coordinate of the Bézier control point
   * @param cpy {Number} The y-coordinate of the Bézier control point
   * @param x {Number} The x-coordinate of the ending point
   * @param y {Number} The y-coordinate of the ending point
   * @description The quadraticCurveTo() method adds a point to the current path by using the specified control points that represent a quadratic Bézier curve.<br /><br /> A quadratic Bézier curve requires two points. The first point is a control point that is used in the quadratic Bézier calculation and the second point is the ending point for the curve. The starting point for the curve is the last point in the current path. If a path does not exist, use the beginPath() and moveTo() methods to define a starting point.
   */
  Context2D.prototype.quadraticCurveTo = function(cpx, cpy, x, y) {
    if (isNaN(x) || isNaN(y) || isNaN(cpx) || isNaN(cpy)) {
      console$1.error(
        "jsPDF.context2d.quadraticCurveTo: Invalid arguments",
        arguments
      );
      throw new Error(
        "Invalid arguments passed to jsPDF.context2d.quadraticCurveTo"
      );
    }

    var pt0 = this.ctx.transform.applyToPoint(new Point(x, y));
    var pt1 = this.ctx.transform.applyToPoint(new Point(cpx, cpy));

    this.path.push({
      type: "qct",
      x1: pt1.x,
      y1: pt1.y,
      x: pt0.x,
      y: pt0.y
    });
    this.ctx.lastPoint = new Point(pt0.x, pt0.y);
  };

  /**
   * Creates a cubic Bézier curve
   *
   * @name bezierCurveTo
   * @function
   * @param cp1x {Number} The x-coordinate of the first Bézier control point
   * @param cp1y {Number} The y-coordinate of the first Bézier control point
   * @param cp2x {Number} The x-coordinate of the second Bézier control point
   * @param cp2y {Number} The y-coordinate of the second Bézier control point
   * @param x {Number} The x-coordinate of the ending point
   * @param y {Number} The y-coordinate of the ending point
   * @description The bezierCurveTo() method adds a point to the current path by using the specified control points that represent a cubic Bézier curve. <br /><br />A cubic bezier curve requires three points. The first two points are control points that are used in the cubic Bézier calculation and the last point is the ending point for the curve.  The starting point for the curve is the last point in the current path. If a path does not exist, use the beginPath() and moveTo() methods to define a starting point.
   */
  Context2D.prototype.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
    if (
      isNaN(x) ||
      isNaN(y) ||
      isNaN(cp1x) ||
      isNaN(cp1y) ||
      isNaN(cp2x) ||
      isNaN(cp2y)
    ) {
      console$1.error(
        "jsPDF.context2d.bezierCurveTo: Invalid arguments",
        arguments
      );
      throw new Error(
        "Invalid arguments passed to jsPDF.context2d.bezierCurveTo"
      );
    }
    var pt0 = this.ctx.transform.applyToPoint(new Point(x, y));
    var pt1 = this.ctx.transform.applyToPoint(new Point(cp1x, cp1y));
    var pt2 = this.ctx.transform.applyToPoint(new Point(cp2x, cp2y));

    this.path.push({
      type: "bct",
      x1: pt1.x,
      y1: pt1.y,
      x2: pt2.x,
      y2: pt2.y,
      x: pt0.x,
      y: pt0.y
    });
    this.ctx.lastPoint = new Point(pt0.x, pt0.y);
  };

  /**
   * Creates an arc/curve (used to create circles, or parts of circles)
   *
   * @name arc
   * @function
   * @param x {Number} The x-coordinate of the center of the circle
   * @param y {Number} The y-coordinate of the center of the circle
   * @param radius {Number} The radius of the circle
   * @param startAngle {Number} The starting angle, in radians (0 is at the 3 o'clock position of the arc's circle)
   * @param endAngle {Number} The ending angle, in radians
   * @param counterclockwise {Boolean} Optional. Specifies whether the drawing should be counterclockwise or clockwise. False is default, and indicates clockwise, while true indicates counter-clockwise.
   * @description The arc() method creates an arc/curve (used to create circles, or parts of circles).
   */
  Context2D.prototype.arc = function(
    x,
    y,
    radius,
    startAngle,
    endAngle,
    counterclockwise
  ) {
    if (
      isNaN(x) ||
      isNaN(y) ||
      isNaN(radius) ||
      isNaN(startAngle) ||
      isNaN(endAngle)
    ) {
      console$1.error("jsPDF.context2d.arc: Invalid arguments", arguments);
      throw new Error("Invalid arguments passed to jsPDF.context2d.arc");
    }
    counterclockwise = Boolean(counterclockwise);

    if (!this.ctx.transform.isIdentity) {
      var xpt = this.ctx.transform.applyToPoint(new Point(x, y));
      x = xpt.x;
      y = xpt.y;

      var x_radPt = this.ctx.transform.applyToPoint(new Point(0, radius));
      var x_radPt0 = this.ctx.transform.applyToPoint(new Point(0, 0));
      radius = Math.sqrt(
        Math.pow(x_radPt.x - x_radPt0.x, 2) +
          Math.pow(x_radPt.y - x_radPt0.y, 2)
      );
    }
    if (Math.abs(endAngle - startAngle) >= 2 * Math.PI) {
      startAngle = 0;
      endAngle = 2 * Math.PI;
    }

    this.path.push({
      type: "arc",
      x: x,
      y: y,
      radius: radius,
      startAngle: startAngle,
      endAngle: endAngle,
      counterclockwise: counterclockwise
    });
    // this.ctx.lastPoint(new Point(pt.x,pt.y));
  };

  /**
   * Creates an arc/curve between two tangents
   *
   * @name arcTo
   * @function
   * @param x1 {Number} The x-coordinate of the first tangent
   * @param y1 {Number} The y-coordinate of the first tangent
   * @param x2 {Number} The x-coordinate of the second tangent
   * @param y2 {Number} The y-coordinate of the second tangent
   * @param radius The radius of the arc
   * @description The arcTo() method creates an arc/curve between two tangents on the canvas.
   */
  // eslint-disable-next-line no-unused-vars
  Context2D.prototype.arcTo = function(x1, y1, x2, y2, radius) {
    throw new Error("arcTo not implemented.");
  };

  /**
   * Creates a rectangle
   *
   * @name rect
   * @function
   * @param x {Number} The x-coordinate of the upper-left corner of the rectangle
   * @param y {Number} The y-coordinate of the upper-left corner of the rectangle
   * @param w {Number} The width of the rectangle, in pixels
   * @param h {Number} The height of the rectangle, in pixels
   * @description The rect() method creates a rectangle.
   */
  Context2D.prototype.rect = function(x, y, w, h) {
    if (isNaN(x) || isNaN(y) || isNaN(w) || isNaN(h)) {
      console$1.error("jsPDF.context2d.rect: Invalid arguments", arguments);
      throw new Error("Invalid arguments passed to jsPDF.context2d.rect");
    }
    this.moveTo(x, y);
    this.lineTo(x + w, y);
    this.lineTo(x + w, y + h);
    this.lineTo(x, y + h);
    this.lineTo(x, y);
    this.lineTo(x + w, y);
    this.lineTo(x, y);
  };

  /**
   * Draws a "filled" rectangle
   *
   * @name fillRect
   * @function
   * @param x {Number} The x-coordinate of the upper-left corner of the rectangle
   * @param y {Number} The y-coordinate of the upper-left corner of the rectangle
   * @param w {Number} The width of the rectangle, in pixels
   * @param h {Number} The height of the rectangle, in pixels
   * @description The fillRect() method draws a "filled" rectangle. The default color of the fill is black.
   */
  Context2D.prototype.fillRect = function(x, y, w, h) {
    if (isNaN(x) || isNaN(y) || isNaN(w) || isNaN(h)) {
      console$1.error("jsPDF.context2d.fillRect: Invalid arguments", arguments);
      throw new Error("Invalid arguments passed to jsPDF.context2d.fillRect");
    }
    if (isFillTransparent.call(this)) {
      return;
    }
    var tmp = {};
    if (this.lineCap !== "butt") {
      tmp.lineCap = this.lineCap;
      this.lineCap = "butt";
    }
    if (this.lineJoin !== "miter") {
      tmp.lineJoin = this.lineJoin;
      this.lineJoin = "miter";
    }

    this.beginPath();
    this.rect(x, y, w, h);
    this.fill();

    if (tmp.hasOwnProperty("lineCap")) {
      this.lineCap = tmp.lineCap;
    }
    if (tmp.hasOwnProperty("lineJoin")) {
      this.lineJoin = tmp.lineJoin;
    }
  };

  /**
   *     Draws a rectangle (no fill)
   *
   * @name strokeRect
   * @function
   * @param x {Number} The x-coordinate of the upper-left corner of the rectangle
   * @param y {Number} The y-coordinate of the upper-left corner of the rectangle
   * @param w {Number} The width of the rectangle, in pixels
   * @param h {Number} The height of the rectangle, in pixels
   * @description The strokeRect() method draws a rectangle (no fill). The default color of the stroke is black.
   */
  Context2D.prototype.strokeRect = function strokeRect(x, y, w, h) {
    if (isNaN(x) || isNaN(y) || isNaN(w) || isNaN(h)) {
      console$1.error("jsPDF.context2d.strokeRect: Invalid arguments", arguments);
      throw new Error("Invalid arguments passed to jsPDF.context2d.strokeRect");
    }
    if (isStrokeTransparent.call(this)) {
      return;
    }
    this.beginPath();
    this.rect(x, y, w, h);
    this.stroke();
  };

  /**
   * Clears the specified pixels within a given rectangle
   *
   * @name clearRect
   * @function
   * @param x {Number} The x-coordinate of the upper-left corner of the rectangle
   * @param y {Number} The y-coordinate of the upper-left corner of the rectangle
   * @param w {Number} The width of the rectangle to clear, in pixels
   * @param h {Number} The height of the rectangle to clear, in pixels
   * @description We cannot clear PDF commands that were already written to PDF, so we use white instead. <br />
   * As a special case, read a special flag (ignoreClearRect) and do nothing if it is set.
   * This results in all calls to clearRect() to do nothing, and keep the canvas transparent.
   * This flag is stored in the save/restore context and is managed the same way as other drawing states.
   *
   */
  Context2D.prototype.clearRect = function(x, y, w, h) {
    if (isNaN(x) || isNaN(y) || isNaN(w) || isNaN(h)) {
      console$1.error("jsPDF.context2d.clearRect: Invalid arguments", arguments);
      throw new Error("Invalid arguments passed to jsPDF.context2d.clearRect");
    }
    if (this.ignoreClearRect) {
      return;
    }

    this.fillStyle = "#ffffff";
    this.fillRect(x, y, w, h);
  };

  /**
   * Saves the state of the current context
   *
   * @name save
   * @function
   */
  Context2D.prototype.save = function(doStackPush) {
    doStackPush = typeof doStackPush === "boolean" ? doStackPush : true;
    var tmpPageNumber = this.pdf.internal.getCurrentPageInfo().pageNumber;
    for (var i = 0; i < this.pdf.internal.getNumberOfPages(); i++) {
      this.pdf.setPage(i + 1);
      this.pdf.internal.out("q");
    }
    this.pdf.setPage(tmpPageNumber);

    if (doStackPush) {
      this.ctx.fontSize = this.pdf.internal.getFontSize();
      var ctx = new ContextLayer(this.ctx);
      this.ctxStack.push(this.ctx);
      this.ctx = ctx;
    }
  };

  /**
   * Returns previously saved path state and attributes
   *
   * @name restore
   * @function
   */
  Context2D.prototype.restore = function(doStackPop) {
    doStackPop = typeof doStackPop === "boolean" ? doStackPop : true;
    var tmpPageNumber = this.pdf.internal.getCurrentPageInfo().pageNumber;
    for (var i = 0; i < this.pdf.internal.getNumberOfPages(); i++) {
      this.pdf.setPage(i + 1);
      this.pdf.internal.out("Q");
    }
    this.pdf.setPage(tmpPageNumber);

    if (doStackPop && this.ctxStack.length !== 0) {
      this.ctx = this.ctxStack.pop();
      this.fillStyle = this.ctx.fillStyle;
      this.strokeStyle = this.ctx.strokeStyle;
      this.font = this.ctx.font;
      this.lineCap = this.ctx.lineCap;
      this.lineWidth = this.ctx.lineWidth;
      this.lineJoin = this.ctx.lineJoin;
      this.lineDash = this.ctx.lineDash;
      this.lineDashOffset = this.ctx.lineDashOffset;
    }
  };

  /**
   * @name toDataURL
   * @function
   */
  Context2D.prototype.toDataURL = function() {
    throw new Error("toDataUrl not implemented.");
  };

  //helper functions

  /**
   * Get the decimal values of r, g, b and a
   *
   * @name getRGBA
   * @function
   * @private
   * @ignore
   */
  var getRGBA = function(style) {
    var rxRgb = /rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/;
    var rxRgba = /rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/;
    var rxTransparent = /transparent|rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*0+\s*\)/;

    var r, g, b, a;

    if (style.isCanvasGradient === true) {
      style = style.getColor();
    }

    if (!style) {
      return { r: 0, g: 0, b: 0, a: 0, style: style };
    }

    if (rxTransparent.test(style)) {
      r = 0;
      g = 0;
      b = 0;
      a = 0;
    } else {
      var matches = rxRgb.exec(style);
      if (matches !== null) {
        r = parseInt(matches[1]);
        g = parseInt(matches[2]);
        b = parseInt(matches[3]);
        a = 1;
      } else {
        matches = rxRgba.exec(style);
        if (matches !== null) {
          r = parseInt(matches[1]);
          g = parseInt(matches[2]);
          b = parseInt(matches[3]);
          a = parseFloat(matches[4]);
        } else {
          a = 1;

          if (typeof style === "string" && style.charAt(0) !== "#") {
            var rgbColor = new RGBColor$1(style);
            if (rgbColor.ok) {
              style = rgbColor.toHex();
            } else {
              style = "#000000";
            }
          }

          if (style.length === 4) {
            r = style.substring(1, 2);
            r += r;
            g = style.substring(2, 3);
            g += g;
            b = style.substring(3, 4);
            b += b;
          } else {
            r = style.substring(1, 3);
            g = style.substring(3, 5);
            b = style.substring(5, 7);
          }
          r = parseInt(r, 16);
          g = parseInt(g, 16);
          b = parseInt(b, 16);
        }
      }
    }
    return { r: r, g: g, b: b, a: a, style: style };
  };

  /**
   * @name isFillTransparent
   * @function
   * @private
   * @ignore
   * @returns {Boolean}
   */
  var isFillTransparent = function() {
    return this.ctx.isFillTransparent || this.globalAlpha == 0;
  };

  /**
   * @name isStrokeTransparent
   * @function
   * @private
   * @ignore
   * @returns {Boolean}
   */
  var isStrokeTransparent = function() {
    return Boolean(this.ctx.isStrokeTransparent || this.globalAlpha == 0);
  };

  /**
   * Draws "filled" text on the canvas
   *
   * @name fillText
   * @function
   * @param text {String} Specifies the text that will be written on the canvas
   * @param x {Number} The x coordinate where to start painting the text (relative to the canvas)
   * @param y {Number} The y coordinate where to start painting the text (relative to the canvas)
   * @param maxWidth {Number} Optional. The maximum allowed width of the text, in pixels
   * @description The fillText() method draws filled text on the canvas. The default color of the text is black.
   */
  Context2D.prototype.fillText = function(text, x, y, maxWidth) {
    if (isNaN(x) || isNaN(y) || typeof text !== "string") {
      console$1.error("jsPDF.context2d.fillText: Invalid arguments", arguments);
      throw new Error("Invalid arguments passed to jsPDF.context2d.fillText");
    }
    maxWidth = isNaN(maxWidth) ? undefined : maxWidth;
    if (isFillTransparent.call(this)) {
      return;
    }

    var degs = rad2deg(this.ctx.transform.rotation);

    // We only use X axis as scale hint
    var scale = this.ctx.transform.scaleX;

    putText.call(this, {
      text: text,
      x: x,
      y: y,
      scale: scale,
      angle: degs,
      align: this.textAlign,
      maxWidth: maxWidth
    });
  };

  /**
   * Draws text on the canvas (no fill)
   *
   * @name strokeText
   * @function
   * @param text {String} Specifies the text that will be written on the canvas
   * @param x {Number} The x coordinate where to start painting the text (relative to the canvas)
   * @param y {Number} The y coordinate where to start painting the text (relative to the canvas)
   * @param maxWidth {Number} Optional. The maximum allowed width of the text, in pixels
   * @description The strokeText() method draws text (with no fill) on the canvas. The default color of the text is black.
   */
  Context2D.prototype.strokeText = function(text, x, y, maxWidth) {
    if (isNaN(x) || isNaN(y) || typeof text !== "string") {
      console$1.error("jsPDF.context2d.strokeText: Invalid arguments", arguments);
      throw new Error("Invalid arguments passed to jsPDF.context2d.strokeText");
    }
    if (isStrokeTransparent.call(this)) {
      return;
    }

    maxWidth = isNaN(maxWidth) ? undefined : maxWidth;

    var degs = rad2deg(this.ctx.transform.rotation);
    var scale = this.ctx.transform.scaleX;

    putText.call(this, {
      text: text,
      x: x,
      y: y,
      scale: scale,
      renderingMode: "stroke",
      angle: degs,
      align: this.textAlign,
      maxWidth: maxWidth
    });
  };

  /**
   * Returns an object that contains the width of the specified text
   *
   * @name measureText
   * @function
   * @param text {String} The text to be measured
   * @description The measureText() method returns an object that contains the width of the specified text, in pixels.
   * @returns {Number}
   */
  Context2D.prototype.measureText = function(text) {
    if (typeof text !== "string") {
      console$1.error(
        "jsPDF.context2d.measureText: Invalid arguments",
        arguments
      );
      throw new Error(
        "Invalid arguments passed to jsPDF.context2d.measureText"
      );
    }
    var pdf = this.pdf;
    var k = this.pdf.internal.scaleFactor;

    var fontSize = pdf.internal.getFontSize();
    var txtWidth =
      (pdf.getStringUnitWidth(text) * fontSize) / pdf.internal.scaleFactor;
    txtWidth *= Math.round(((k * 96) / 72) * 10000) / 10000;

    var TextMetrics = function(options) {
      options = options || {};
      var _width = options.width || 0;
      Object.defineProperty(this, "width", {
        get: function() {
          return _width;
        }
      });
      return this;
    };
    return new TextMetrics({ width: txtWidth });
  };

  //Transformations

  /**
   * Scales the current drawing bigger or smaller
   *
   * @name scale
   * @function
   * @param scalewidth {Number} Scales the width of the current drawing (1=100%, 0.5=50%, 2=200%, etc.)
   * @param scaleheight {Number} Scales the height of the current drawing (1=100%, 0.5=50%, 2=200%, etc.)
   * @description The scale() method scales the current drawing, bigger or smaller.
   */
  Context2D.prototype.scale = function(scalewidth, scaleheight) {
    if (isNaN(scalewidth) || isNaN(scaleheight)) {
      console$1.error("jsPDF.context2d.scale: Invalid arguments", arguments);
      throw new Error("Invalid arguments passed to jsPDF.context2d.scale");
    }
    var matrix = new Matrix(scalewidth, 0.0, 0.0, scaleheight, 0.0, 0.0);
    this.ctx.transform = this.ctx.transform.multiply(matrix);
  };

  /**
   * Rotates the current drawing
   *
   * @name rotate
   * @function
   * @param angle {Number} The rotation angle, in radians.
   * @description To calculate from degrees to radians: degrees*Math.PI/180. <br />
   * Example: to rotate 5 degrees, specify the following: 5*Math.PI/180
   */
  Context2D.prototype.rotate = function(angle) {
    if (isNaN(angle)) {
      console$1.error("jsPDF.context2d.rotate: Invalid arguments", arguments);
      throw new Error("Invalid arguments passed to jsPDF.context2d.rotate");
    }
    var matrix = new Matrix(
      Math.cos(angle),
      Math.sin(angle),
      -Math.sin(angle),
      Math.cos(angle),
      0.0,
      0.0
    );
    this.ctx.transform = this.ctx.transform.multiply(matrix);
  };

  /**
   * Remaps the (0,0) position on the canvas
   *
   * @name translate
   * @function
   * @param x {Number} The value to add to horizontal (x) coordinates
   * @param y {Number} The value to add to vertical (y) coordinates
   * @description The translate() method remaps the (0,0) position on the canvas.
   */
  Context2D.prototype.translate = function(x, y) {
    if (isNaN(x) || isNaN(y)) {
      console$1.error("jsPDF.context2d.translate: Invalid arguments", arguments);
      throw new Error("Invalid arguments passed to jsPDF.context2d.translate");
    }
    var matrix = new Matrix(1.0, 0.0, 0.0, 1.0, x, y);
    this.ctx.transform = this.ctx.transform.multiply(matrix);
  };

  /**
   * Replaces the current transformation matrix for the drawing
   *
   * @name transform
   * @function
   * @param a {Number} Horizontal scaling
   * @param b {Number} Horizontal skewing
   * @param c {Number} Vertical skewing
   * @param d {Number} Vertical scaling
   * @param e {Number} Horizontal moving
   * @param f {Number} Vertical moving
   * @description Each object on the canvas has a current transformation matrix.<br /><br />The transform() method replaces the current transformation matrix. It multiplies the current transformation matrix with the matrix described by:<br /><br /><br /><br />a    c    e<br /><br />b    d    f<br /><br />0    0    1<br /><br />In other words, the transform() method lets you scale, rotate, move, and skew the current context.
   */
  Context2D.prototype.transform = function(a, b, c, d, e, f) {
    if (isNaN(a) || isNaN(b) || isNaN(c) || isNaN(d) || isNaN(e) || isNaN(f)) {
      console$1.error("jsPDF.context2d.transform: Invalid arguments", arguments);
      throw new Error("Invalid arguments passed to jsPDF.context2d.transform");
    }
    var matrix = new Matrix(a, b, c, d, e, f);
    this.ctx.transform = this.ctx.transform.multiply(matrix);
  };

  /**
   * Resets the current transform to the identity matrix. Then runs transform()
   *
   * @name setTransform
   * @function
   * @param a {Number} Horizontal scaling
   * @param b {Number} Horizontal skewing
   * @param c {Number} Vertical skewing
   * @param d {Number} Vertical scaling
   * @param e {Number} Horizontal moving
   * @param f {Number} Vertical moving
   * @description Each object on the canvas has a current transformation matrix. <br /><br />The setTransform() method resets the current transform to the identity matrix, and then runs transform() with the same arguments.<br /><br />In other words, the setTransform() method lets you scale, rotate, move, and skew the current context.
   */
  Context2D.prototype.setTransform = function(a, b, c, d, e, f) {
    a = isNaN(a) ? 1 : a;
    b = isNaN(b) ? 0 : b;
    c = isNaN(c) ? 0 : c;
    d = isNaN(d) ? 1 : d;
    e = isNaN(e) ? 0 : e;
    f = isNaN(f) ? 0 : f;
    this.ctx.transform = new Matrix(a, b, c, d, e, f);
  };

  var hasMargins = function() {
    return (
      this.margin[0] > 0 ||
      this.margin[1] > 0 ||
      this.margin[2] > 0 ||
      this.margin[3] > 0
    );
  };

  /**
   * Draws an image, canvas, or video onto the canvas
   *
   * @function
   * @param img {} Specifies the image, canvas, or video element to use
   * @param sx {Number} Optional. The x coordinate where to start clipping
   * @param sy {Number} Optional. The y coordinate where to start clipping
   * @param swidth {Number} Optional. The width of the clipped image
   * @param sheight {Number} Optional. The height of the clipped image
   * @param x {Number} The x coordinate where to place the image on the canvas
   * @param y {Number} The y coordinate where to place the image on the canvas
   * @param width {Number} Optional. The width of the image to use (stretch or reduce the image)
   * @param height {Number} Optional. The height of the image to use (stretch or reduce the image)
   */
  Context2D.prototype.drawImage = function(
    img,
    sx,
    sy,
    swidth,
    sheight,
    x,
    y,
    width,
    height
  ) {
    var imageProperties = this.pdf.getImageProperties(img);
    var factorX = 1;
    var factorY = 1;

    var clipFactorX = 1;
    var clipFactorY = 1;

    if (typeof swidth !== "undefined" && typeof width !== "undefined") {
      clipFactorX = width / swidth;
      clipFactorY = height / sheight;
      factorX = ((imageProperties.width / swidth) * width) / swidth;
      factorY = ((imageProperties.height / sheight) * height) / sheight;
    }

    //is sx and sy are set and x and y not, set x and y with values of sx and sy
    if (typeof x === "undefined") {
      x = sx;
      y = sy;
      sx = 0;
      sy = 0;
    }

    if (typeof swidth !== "undefined" && typeof width === "undefined") {
      width = swidth;
      height = sheight;
    }
    if (typeof swidth === "undefined" && typeof width === "undefined") {
      width = imageProperties.width;
      height = imageProperties.height;
    }

    var decomposedTransformationMatrix = this.ctx.transform.decompose();
    var angle = rad2deg(decomposedTransformationMatrix.rotate.shx);
    var matrix = new Matrix();
    matrix = matrix.multiply(decomposedTransformationMatrix.translate);
    matrix = matrix.multiply(decomposedTransformationMatrix.skew);
    matrix = matrix.multiply(decomposedTransformationMatrix.scale);
    var xRect = matrix.applyToRectangle(
      new Rectangle(
        x - sx * clipFactorX,
        y - sy * clipFactorY,
        swidth * factorX,
        sheight * factorY
      )
    );
    var pageArray = getPagesByPath.call(this, xRect);
    var pages = [];
    for (var ii = 0; ii < pageArray.length; ii += 1) {
      if (pages.indexOf(pageArray[ii]) === -1) {
        pages.push(pageArray[ii]);
      }
    }

    sortPages(pages);

    var clipPath;
    if (this.autoPaging) {
      var min = pages[0];
      var max = pages[pages.length - 1];
      for (var i = min; i < max + 1; i++) {
        this.pdf.setPage(i);

        var pageWidthMinusMargins =
          this.pdf.internal.pageSize.width - this.margin[3] - this.margin[1];
        var topMargin = i === 1 ? this.posY + this.margin[0] : this.margin[0];
        var firstPageHeight =
          this.pdf.internal.pageSize.height -
          this.posY -
          this.margin[0] -
          this.margin[2];
        var pageHeightMinusMargins =
          this.pdf.internal.pageSize.height - this.margin[0] - this.margin[2];
        var previousPageHeightSum =
          i === 1 ? 0 : firstPageHeight + (i - 2) * pageHeightMinusMargins;

        if (this.ctx.clip_path.length !== 0) {
          var tmpPaths = this.path;
          clipPath = JSON.parse(JSON.stringify(this.ctx.clip_path));
          this.path = pathPositionRedo(
            clipPath,
            this.posX + this.margin[3],
            -previousPageHeightSum + topMargin + this.ctx.prevPageLastElemOffset
          );
          drawPaths.call(this, "fill", true);
          this.path = tmpPaths;
        }
        var tmpRect = JSON.parse(JSON.stringify(xRect));
        tmpRect = pathPositionRedo(
          [tmpRect],
          this.posX + this.margin[3],
          -previousPageHeightSum + topMargin + this.ctx.prevPageLastElemOffset
        )[0];

        const needsClipping = (i > min || i < max) && hasMargins.call(this);

        if (needsClipping) {
          this.pdf.saveGraphicsState();
          this.pdf
            .rect(
              this.margin[3],
              this.margin[0],
              pageWidthMinusMargins,
              pageHeightMinusMargins,
              null
            )
            .clip()
            .discardPath();
        }
        this.pdf.addImage(
          img,
          "JPEG",
          tmpRect.x,
          tmpRect.y,
          tmpRect.w,
          tmpRect.h,
          null,
          null,
          angle
        );
        if (needsClipping) {
          this.pdf.restoreGraphicsState();
        }
      }
    } else {
      this.pdf.addImage(
        img,
        "JPEG",
        xRect.x,
        xRect.y,
        xRect.w,
        xRect.h,
        null,
        null,
        angle
      );
    }
  };

  var getPagesByPath = function(path, pageWrapX, pageWrapY) {
    var result = [];
    pageWrapX = pageWrapX || this.pdf.internal.pageSize.width;
    pageWrapY =
      pageWrapY ||
      this.pdf.internal.pageSize.height - this.margin[0] - this.margin[2];
    var yOffset = this.posY + this.ctx.prevPageLastElemOffset;

    switch (path.type) {
      default:
      case "mt":
      case "lt":
        result.push(Math.floor((path.y + yOffset) / pageWrapY) + 1);
        break;
      case "arc":
        result.push(
          Math.floor((path.y + yOffset - path.radius) / pageWrapY) + 1
        );
        result.push(
          Math.floor((path.y + yOffset + path.radius) / pageWrapY) + 1
        );
        break;
      case "qct":
        var rectOfQuadraticCurve = getQuadraticCurveBoundary(
          this.ctx.lastPoint.x,
          this.ctx.lastPoint.y,
          path.x1,
          path.y1,
          path.x,
          path.y
        );
        result.push(
          Math.floor((rectOfQuadraticCurve.y + yOffset) / pageWrapY) + 1
        );
        result.push(
          Math.floor(
            (rectOfQuadraticCurve.y + rectOfQuadraticCurve.h + yOffset) /
              pageWrapY
          ) + 1
        );
        break;
      case "bct":
        var rectOfBezierCurve = getBezierCurveBoundary(
          this.ctx.lastPoint.x,
          this.ctx.lastPoint.y,
          path.x1,
          path.y1,
          path.x2,
          path.y2,
          path.x,
          path.y
        );
        result.push(
          Math.floor((rectOfBezierCurve.y + yOffset) / pageWrapY) + 1
        );
        result.push(
          Math.floor(
            (rectOfBezierCurve.y + rectOfBezierCurve.h + yOffset) / pageWrapY
          ) + 1
        );
        break;
      case "rect":
        result.push(Math.floor((path.y + yOffset) / pageWrapY) + 1);
        result.push(Math.floor((path.y + path.h + yOffset) / pageWrapY) + 1);
    }

    for (var i = 0; i < result.length; i += 1) {
      while (this.pdf.internal.getNumberOfPages() < result[i]) {
        addPage.call(this);
      }
    }
    return result;
  };

  var addPage = function() {
    var fillStyle = this.fillStyle;
    var strokeStyle = this.strokeStyle;
    var font = this.font;
    var lineCap = this.lineCap;
    var lineWidth = this.lineWidth;
    var lineJoin = this.lineJoin;
    this.pdf.addPage();
    this.fillStyle = fillStyle;
    this.strokeStyle = strokeStyle;
    this.font = font;
    this.lineCap = lineCap;
    this.lineWidth = lineWidth;
    this.lineJoin = lineJoin;
  };

  var pathPositionRedo = function(paths, x, y) {
    for (var i = 0; i < paths.length; i++) {
      switch (paths[i].type) {
        case "bct":
          paths[i].x2 += x;
          paths[i].y2 += y;
        case "qct":
          paths[i].x1 += x;
          paths[i].y1 += y;
        case "mt":
        case "lt":
        case "arc":
        default:
          paths[i].x += x;
          paths[i].y += y;
      }
    }
    return paths;
  };

  var sortPages = function(pages) {
    return pages.sort(function(a, b) {
      return a - b;
    });
  };

  var pathPreProcess = function(rule, isClip) {
    var fillStyle = this.fillStyle;
    var strokeStyle = this.strokeStyle;
    var lineCap = this.lineCap;
    var oldLineWidth = this.lineWidth;
    var lineWidth = Math.abs(oldLineWidth * this.ctx.transform.scaleX);
    var lineJoin = this.lineJoin;

    var origPath = JSON.parse(JSON.stringify(this.path));
    var xPath = JSON.parse(JSON.stringify(this.path));
    var clipPath;
    var tmpPath;
    var pages = [];

    for (var i = 0; i < xPath.length; i++) {
      if (typeof xPath[i].x !== "undefined") {
        var page = getPagesByPath.call(this, xPath[i]);

        for (var ii = 0; ii < page.length; ii += 1) {
          if (pages.indexOf(page[ii]) === -1) {
            pages.push(page[ii]);
          }
        }
      }
    }

    for (var j = 0; j < pages.length; j++) {
      while (this.pdf.internal.getNumberOfPages() < pages[j]) {
        addPage.call(this);
      }
    }
    sortPages(pages);

    if (this.autoPaging) {
      var min = pages[0];
      var max = pages[pages.length - 1];
      for (var k = min; k < max + 1; k++) {
        this.pdf.setPage(k);

        this.fillStyle = fillStyle;
        this.strokeStyle = strokeStyle;
        this.lineCap = lineCap;
        this.lineWidth = lineWidth;
        this.lineJoin = lineJoin;

        var pageWidthMinusMargins =
          this.pdf.internal.pageSize.width - this.margin[3] - this.margin[1];
        var topMargin = k === 1 ? this.posY + this.margin[0] : this.margin[0];
        var firstPageHeight =
          this.pdf.internal.pageSize.height -
          this.posY -
          this.margin[0] -
          this.margin[2];
        var pageHeightMinusMargins =
          this.pdf.internal.pageSize.height - this.margin[0] - this.margin[2];
        var previousPageHeightSum =
          k === 1 ? 0 : firstPageHeight + (k - 2) * pageHeightMinusMargins;

        if (this.ctx.clip_path.length !== 0) {
          var tmpPaths = this.path;
          clipPath = JSON.parse(JSON.stringify(this.ctx.clip_path));
          this.path = pathPositionRedo(
            clipPath,
            this.posX + this.margin[3],
            -previousPageHeightSum + topMargin + this.ctx.prevPageLastElemOffset
          );
          drawPaths.call(this, rule, true);
          this.path = tmpPaths;
        }
        tmpPath = JSON.parse(JSON.stringify(origPath));
        this.path = pathPositionRedo(
          tmpPath,
          this.posX + this.margin[3],
          -previousPageHeightSum + topMargin + this.ctx.prevPageLastElemOffset
        );
        if (isClip === false || k === 0) {
          const needsClipping = (k > min || k < max) && hasMargins.call(this);
          if (needsClipping) {
            this.pdf.saveGraphicsState();
            this.pdf
              .rect(
                this.margin[3],
                this.margin[0],
                pageWidthMinusMargins,
                pageHeightMinusMargins,
                null
              )
              .clip()
              .discardPath();
          }
          drawPaths.call(this, rule, isClip);
          if (needsClipping) {
            this.pdf.restoreGraphicsState();
          }
        }
        this.lineWidth = oldLineWidth;
      }
    } else {
      this.lineWidth = lineWidth;
      drawPaths.call(this, rule, isClip);
      this.lineWidth = oldLineWidth;
    }
    this.path = origPath;
  };

  /**
   * Processes the paths
   *
   * @function
   * @param rule {String}
   * @param isClip {Boolean}
   * @private
   * @ignore
   */
  var drawPaths = function(rule, isClip) {
    if (rule === "stroke" && !isClip && isStrokeTransparent.call(this)) {
      return;
    }

    if (rule !== "stroke" && !isClip && isFillTransparent.call(this)) {
      return;
    }

    var moves = [];

    //var alpha = (this.ctx.fillOpacity < 1) ? this.ctx.fillOpacity : this.ctx.globalAlpha;
    var delta;
    var xPath = this.path;
    for (var i = 0; i < xPath.length; i++) {
      var pt = xPath[i];

      switch (pt.type) {
        case "begin":
          moves.push({
            begin: true
          });
          break;

        case "close":
          moves.push({
            close: true
          });
          break;

        case "mt":
          moves.push({
            start: pt,
            deltas: [],
            abs: []
          });
          break;

        case "lt":
          var iii = moves.length;
          if (xPath[i - 1] && !isNaN(xPath[i - 1].x)) {
            delta = [pt.x - xPath[i - 1].x, pt.y - xPath[i - 1].y];
            if (iii > 0) {
              for (iii; iii >= 0; iii--) {
                if (
                  moves[iii - 1].close !== true &&
                  moves[iii - 1].begin !== true
                ) {
                  moves[iii - 1].deltas.push(delta);
                  moves[iii - 1].abs.push(pt);
                  break;
                }
              }
            }
          }
          break;

        case "bct":
          delta = [
            pt.x1 - xPath[i - 1].x,
            pt.y1 - xPath[i - 1].y,
            pt.x2 - xPath[i - 1].x,
            pt.y2 - xPath[i - 1].y,
            pt.x - xPath[i - 1].x,
            pt.y - xPath[i - 1].y
          ];
          moves[moves.length - 1].deltas.push(delta);
          break;

        case "qct":
          var x1 = xPath[i - 1].x + (2.0 / 3.0) * (pt.x1 - xPath[i - 1].x);
          var y1 = xPath[i - 1].y + (2.0 / 3.0) * (pt.y1 - xPath[i - 1].y);
          var x2 = pt.x + (2.0 / 3.0) * (pt.x1 - pt.x);
          var y2 = pt.y + (2.0 / 3.0) * (pt.y1 - pt.y);
          var x3 = pt.x;
          var y3 = pt.y;
          delta = [
            x1 - xPath[i - 1].x,
            y1 - xPath[i - 1].y,
            x2 - xPath[i - 1].x,
            y2 - xPath[i - 1].y,
            x3 - xPath[i - 1].x,
            y3 - xPath[i - 1].y
          ];
          moves[moves.length - 1].deltas.push(delta);
          break;

        case "arc":
          moves.push({
            deltas: [],
            abs: [],
            arc: true
          });

          if (Array.isArray(moves[moves.length - 1].abs)) {
            moves[moves.length - 1].abs.push(pt);
          }
          break;
      }
    }
    var style;
    if (!isClip) {
      if (rule === "stroke") {
        style = "stroke";
      } else {
        style = "fill";
      }
    } else {
      style = null;
    }

    var began = false;
    for (var k = 0; k < moves.length; k++) {
      if (moves[k].arc) {
        var arcs = moves[k].abs;

        for (var ii = 0; ii < arcs.length; ii++) {
          var arc = arcs[ii];

          if (arc.type === "arc") {
            drawArc.call(
              this,
              arc.x,
              arc.y,
              arc.radius,
              arc.startAngle,
              arc.endAngle,
              arc.counterclockwise,
              undefined,
              isClip,
              !began
            );
          } else {
            drawLine.call(this, arc.x, arc.y);
          }
          began = true;
        }
      } else if (moves[k].close === true) {
        this.pdf.internal.out("h");
        began = false;
      } else if (moves[k].begin !== true) {
        var x = moves[k].start.x;
        var y = moves[k].start.y;
        drawLines.call(this, moves[k].deltas, x, y);
        began = true;
      }
    }

    if (style) {
      putStyle.call(this, style);
    }
    if (isClip) {
      doClip.call(this);
    }
  };

  var getBaseline = function(y) {
    var height =
      this.pdf.internal.getFontSize() / this.pdf.internal.scaleFactor;
    var descent = height * (this.pdf.internal.getLineHeightFactor() - 1);
    switch (this.ctx.textBaseline) {
      case "bottom":
        return y - descent;
      case "top":
        return y + height - descent;
      case "hanging":
        return y + height - 2 * descent;
      case "middle":
        return y + height / 2 - descent;
      case "ideographic":
        // TODO not implemented
        return y;
      case "alphabetic":
      default:
        return y;
    }
  };

  var getTextBottom = function(yBaseLine) {
    var height =
      this.pdf.internal.getFontSize() / this.pdf.internal.scaleFactor;
    var descent = height * (this.pdf.internal.getLineHeightFactor() - 1);
    return yBaseLine + descent;
  };

  Context2D.prototype.createLinearGradient = function createLinearGradient() {
    var canvasGradient = function canvasGradient() {};

    canvasGradient.colorStops = [];
    canvasGradient.addColorStop = function(offset, color) {
      this.colorStops.push([offset, color]);
    };

    canvasGradient.getColor = function() {
      if (this.colorStops.length === 0) {
        return "#000000";
      }

      return this.colorStops[0][1];
    };

    canvasGradient.isCanvasGradient = true;
    return canvasGradient;
  };
  Context2D.prototype.createPattern = function createPattern() {
    return this.createLinearGradient();
  };
  Context2D.prototype.createRadialGradient = function createRadialGradient() {
    return this.createLinearGradient();
  };

  /**
   *
   * @param x Edge point X
   * @param y Edge point Y
   * @param r Radius
   * @param a1 start angle
   * @param a2 end angle
   * @param counterclockwise
   * @param style
   * @param isClip
   */
  var drawArc = function(
    x,
    y,
    r,
    a1,
    a2,
    counterclockwise,
    style,
    isClip,
    includeMove
  ) {
    // http://hansmuller-flex.blogspot.com/2011/10/more-about-approximating-circular-arcs.html
    var curves = createArc.call(this, r, a1, a2, counterclockwise);

    for (var i = 0; i < curves.length; i++) {
      var curve = curves[i];
      if (i === 0) {
        if (includeMove) {
          doMove.call(this, curve.x1 + x, curve.y1 + y);
        } else {
          drawLine.call(this, curve.x1 + x, curve.y1 + y);
        }
      }
      drawCurve.call(
        this,
        x,
        y,
        curve.x2,
        curve.y2,
        curve.x3,
        curve.y3,
        curve.x4,
        curve.y4
      );
    }

    if (!isClip) {
      putStyle.call(this, style);
    } else {
      doClip.call(this);
    }
  };

  var putStyle = function(style) {
    switch (style) {
      case "stroke":
        this.pdf.internal.out("S");
        break;
      case "fill":
        this.pdf.internal.out("f");
        break;
    }
  };

  var doClip = function() {
    this.pdf.clip();
    this.pdf.discardPath();
  };

  var doMove = function(x, y) {
    this.pdf.internal.out(
      getHorizontalCoordinateString(x) +
        " " +
        getVerticalCoordinateString(y) +
        " m"
    );
  };

  var putText = function(options) {
    var textAlign;
    switch (options.align) {
      case "right":
      case "end":
        textAlign = "right";
        break;
      case "center":
        textAlign = "center";
        break;
      case "left":
      case "start":
      default:
        textAlign = "left";
        break;
    }

    var textDimensions = this.pdf.getTextDimensions(options.text);
    var yBaseLine = getBaseline.call(this, options.y);
    var yBottom = getTextBottom.call(this, yBaseLine);
    var yTop = yBottom - textDimensions.h;

    var pt = this.ctx.transform.applyToPoint(new Point(options.x, yBaseLine));
    var decomposedTransformationMatrix = this.ctx.transform.decompose();
    var matrix = new Matrix();
    matrix = matrix.multiply(decomposedTransformationMatrix.translate);
    matrix = matrix.multiply(decomposedTransformationMatrix.skew);
    matrix = matrix.multiply(decomposedTransformationMatrix.scale);

    var baselineRect = this.ctx.transform.applyToRectangle(
      new Rectangle(options.x, yBaseLine, textDimensions.w, textDimensions.h)
    );
    var textBounds = matrix.applyToRectangle(
      new Rectangle(options.x, yTop, textDimensions.w, textDimensions.h)
    );
    var pageArray = getPagesByPath.call(this, textBounds);
    var pages = [];
    for (var ii = 0; ii < pageArray.length; ii += 1) {
      if (pages.indexOf(pageArray[ii]) === -1) {
        pages.push(pageArray[ii]);
      }
    }

    sortPages(pages);

    var clipPath, oldSize, oldLineWidth;
    if (this.autoPaging) {
      var min = pages[0];
      var max = pages[pages.length - 1];
      for (var i = min; i < max + 1; i++) {
        this.pdf.setPage(i);

        var topMargin = i === 1 ? this.posY + this.margin[0] : this.margin[0];
        var firstPageHeight =
          this.pdf.internal.pageSize.height -
          this.posY -
          this.margin[0] -
          this.margin[2];
        var pageHeightMinusBottomMargin =
          this.pdf.internal.pageSize.height - this.margin[2];
        var pageHeightMinusMargins =
          pageHeightMinusBottomMargin - this.margin[0];
        var pageWidthMinusRightMargin =
          this.pdf.internal.pageSize.width - this.margin[1];
        var pageWidthMinusMargins = pageWidthMinusRightMargin - this.margin[3];
        var previousPageHeightSum =
          i === 1 ? 0 : firstPageHeight + (i - 2) * pageHeightMinusMargins;

        if (this.ctx.clip_path.length !== 0) {
          var tmpPaths = this.path;
          clipPath = JSON.parse(JSON.stringify(this.ctx.clip_path));
          this.path = pathPositionRedo(
            clipPath,
            this.posX + this.margin[3],
            -1 * previousPageHeightSum + topMargin
          );
          drawPaths.call(this, "fill", true);
          this.path = tmpPaths;
        }
        var textBoundsOnPage = pathPositionRedo(
          [JSON.parse(JSON.stringify(textBounds))],
          this.posX + this.margin[3],
          -previousPageHeightSum + topMargin + this.ctx.prevPageLastElemOffset
        )[0];

        if (options.scale >= 0.01) {
          oldSize = this.pdf.internal.getFontSize();
          this.pdf.setFontSize(oldSize * options.scale);
          oldLineWidth = this.lineWidth;
          this.lineWidth = oldLineWidth * options.scale;
        }

        var doSlice = this.autoPaging !== "text";

        if (
          doSlice ||
          textBoundsOnPage.y + textBoundsOnPage.h <= pageHeightMinusBottomMargin
        ) {
          if (
            doSlice ||
            (textBoundsOnPage.y >= topMargin &&
              textBoundsOnPage.x <= pageWidthMinusRightMargin)
          ) {
            var croppedText = doSlice
              ? options.text
              : this.pdf.splitTextToSize(
                  options.text,
                  options.maxWidth ||
                    pageWidthMinusRightMargin - textBoundsOnPage.x
                )[0];
            var baseLineRectOnPage = pathPositionRedo(
              [JSON.parse(JSON.stringify(baselineRect))],
              this.posX + this.margin[3],
              -previousPageHeightSum +
                topMargin +
                this.ctx.prevPageLastElemOffset
            )[0];

            const needsClipping =
              doSlice && (i > min || i < max) && hasMargins.call(this);

            if (needsClipping) {
              this.pdf.saveGraphicsState();
              this.pdf
                .rect(
                  this.margin[3],
                  this.margin[0],
                  pageWidthMinusMargins,
                  pageHeightMinusMargins,
                  null
                )
                .clip()
                .discardPath();
            }

            this.pdf.text(
              croppedText,
              baseLineRectOnPage.x,
              baseLineRectOnPage.y,
              {
                angle: options.angle,
                align: textAlign,
                renderingMode: options.renderingMode
              }
            );

            if (needsClipping) {
              this.pdf.restoreGraphicsState();
            }
          }
        } else {
          // This text is the last element of the page, but it got cut off due to the margin
          // so we render it in the next page

          if (textBoundsOnPage.y < pageHeightMinusBottomMargin) {
            // As a result, all other elements have their y offset increased
            this.ctx.prevPageLastElemOffset +=
              pageHeightMinusBottomMargin - textBoundsOnPage.y;
          }
        }

        if (options.scale >= 0.01) {
          this.pdf.setFontSize(oldSize);
          this.lineWidth = oldLineWidth;
        }
      }
    } else {
      if (options.scale >= 0.01) {
        oldSize = this.pdf.internal.getFontSize();
        this.pdf.setFontSize(oldSize * options.scale);
        oldLineWidth = this.lineWidth;
        this.lineWidth = oldLineWidth * options.scale;
      }
      this.pdf.text(options.text, pt.x + this.posX, pt.y + this.posY, {
        angle: options.angle,
        align: textAlign,
        renderingMode: options.renderingMode,
        maxWidth: options.maxWidth
      });

      if (options.scale >= 0.01) {
        this.pdf.setFontSize(oldSize);
        this.lineWidth = oldLineWidth;
      }
    }
  };

  var drawLine = function(x, y, prevX, prevY) {
    prevX = prevX || 0;
    prevY = prevY || 0;

    this.pdf.internal.out(
      getHorizontalCoordinateString(x + prevX) +
        " " +
        getVerticalCoordinateString(y + prevY) +
        " l"
    );
  };

  var drawLines = function(lines, x, y) {
    return this.pdf.lines(lines, x, y, null, null);
  };

  var drawCurve = function(x, y, x1, y1, x2, y2, x3, y3) {
    this.pdf.internal.out(
      [
        f2(getHorizontalCoordinate(x1 + x)),
        f2(getVerticalCoordinate(y1 + y)),
        f2(getHorizontalCoordinate(x2 + x)),
        f2(getVerticalCoordinate(y2 + y)),
        f2(getHorizontalCoordinate(x3 + x)),
        f2(getVerticalCoordinate(y3 + y)),
        "c"
      ].join(" ")
    );
  };

  /**
   * Return a array of objects that represent bezier curves which approximate the circular arc centered at the origin, from startAngle to endAngle (radians) with the specified radius.
   *
   * Each bezier curve is an object with four points, where x1,y1 and x4,y4 are the arc's end points and x2,y2 and x3,y3 are the cubic bezier's control points.
   * @function createArc
   */
  var createArc = function(radius, startAngle, endAngle, anticlockwise) {
    var EPSILON = 0.00001; // Roughly 1/1000th of a degree, see below
    var twoPi = Math.PI * 2;
    var halfPi = Math.PI / 2.0;

    while (startAngle > endAngle) {
      startAngle = startAngle - twoPi;
    }
    var totalAngle = Math.abs(endAngle - startAngle);
    if (totalAngle < twoPi) {
      if (anticlockwise) {
        totalAngle = twoPi - totalAngle;
      }
    }

    // Compute the sequence of arc curves, up to PI/2 at a time.
    var curves = [];

    // clockwise or counterclockwise
    var sgn = anticlockwise ? -1 : 1;

    var a1 = startAngle;
    for (; totalAngle > EPSILON; ) {
      var remain = sgn * Math.min(totalAngle, halfPi);
      var a2 = a1 + remain;
      curves.push(createSmallArc.call(this, radius, a1, a2));
      totalAngle -= Math.abs(a2 - a1);
      a1 = a2;
    }

    return curves;
  };

  /**
   * Cubic bezier approximation of a circular arc centered at the origin, from (radians) a1 to a2, where a2-a1 < pi/2. The arc's radius is r.
   *
   * Returns an object with four points, where x1,y1 and x4,y4 are the arc's end points and x2,y2 and x3,y3 are the cubic bezier's control points.
   *
   * This algorithm is based on the approach described in: A. Riškus, "Approximation of a Cubic Bezier Curve by Circular Arcs and Vice Versa," Information Technology and Control, 35(4), 2006 pp. 371-378.
   */
  var createSmallArc = function(r, a1, a2) {
    var a = (a2 - a1) / 2.0;

    var x4 = r * Math.cos(a);
    var y4 = r * Math.sin(a);
    var x1 = x4;
    var y1 = -y4;

    var q1 = x1 * x1 + y1 * y1;
    var q2 = q1 + x1 * x4 + y1 * y4;
    var k2 = ((4 / 3) * (Math.sqrt(2 * q1 * q2) - q2)) / (x1 * y4 - y1 * x4);

    var x2 = x1 - k2 * y1;
    var y2 = y1 + k2 * x1;
    var x3 = x2;
    var y3 = -y2;

    var ar = a + a1;
    var cos_ar = Math.cos(ar);
    var sin_ar = Math.sin(ar);

    return {
      x1: r * Math.cos(a1),
      y1: r * Math.sin(a1),
      x2: x2 * cos_ar - y2 * sin_ar,
      y2: x2 * sin_ar + y2 * cos_ar,
      x3: x3 * cos_ar - y3 * sin_ar,
      y3: x3 * sin_ar + y3 * cos_ar,
      x4: r * Math.cos(a2),
      y4: r * Math.sin(a2)
    };
  };

  var rad2deg = function(value) {
    return (value * 180) / Math.PI;
  };

  var getQuadraticCurveBoundary = function(sx, sy, cpx, cpy, ex, ey) {
    var midX1 = sx + (cpx - sx) * 0.5;
    var midY1 = sy + (cpy - sy) * 0.5;
    var midX2 = ex + (cpx - ex) * 0.5;
    var midY2 = ey + (cpy - ey) * 0.5;
    var resultX1 = Math.min(sx, ex, midX1, midX2);
    var resultX2 = Math.max(sx, ex, midX1, midX2);
    var resultY1 = Math.min(sy, ey, midY1, midY2);
    var resultY2 = Math.max(sy, ey, midY1, midY2);
    return new Rectangle(
      resultX1,
      resultY1,
      resultX2 - resultX1,
      resultY2 - resultY1
    );
  };

  //De Casteljau algorithm
  var getBezierCurveBoundary = function(ax, ay, bx, by, cx, cy, dx, dy) {
    var tobx = bx - ax;
    var toby = by - ay;
    var tocx = cx - bx;
    var tocy = cy - by;
    var todx = dx - cx;
    var tody = dy - cy;
    var precision = 40;
    var d,
      i,
      px,
      py,
      qx,
      qy,
      rx,
      ry,
      tx,
      ty,
      sx,
      sy,
      x,
      y,
      minx,
      miny,
      maxx,
      maxy,
      toqx,
      toqy,
      torx,
      tory,
      totx,
      toty;
    for (i = 0; i < precision + 1; i++) {
      d = i / precision;
      px = ax + d * tobx;
      py = ay + d * toby;
      qx = bx + d * tocx;
      qy = by + d * tocy;
      rx = cx + d * todx;
      ry = cy + d * tody;
      toqx = qx - px;
      toqy = qy - py;
      torx = rx - qx;
      tory = ry - qy;

      sx = px + d * toqx;
      sy = py + d * toqy;
      tx = qx + d * torx;
      ty = qy + d * tory;
      totx = tx - sx;
      toty = ty - sy;

      x = sx + d * totx;
      y = sy + d * toty;
      if (i == 0) {
        minx = x;
        miny = y;
        maxx = x;
        maxy = y;
      } else {
        minx = Math.min(minx, x);
        miny = Math.min(miny, y);
        maxx = Math.max(maxx, x);
        maxy = Math.max(maxy, y);
      }
    }
    return new Rectangle(
      Math.round(minx),
      Math.round(miny),
      Math.round(maxx - minx),
      Math.round(maxy - miny)
    );
  };

  var getPrevLineDashValue = function(lineDash, lineDashOffset) {
    return JSON.stringify({
      lineDash: lineDash,
      lineDashOffset: lineDashOffset
    });
  };

  var setLineDash = function() {
    // Avoid unnecessary line dash declarations.
    if (
      !this.prevLineDash &&
      !this.ctx.lineDash.length &&
      !this.ctx.lineDashOffset
    ) {
      return;
    }

    // Avoid unnecessary line dash declarations.
    const nextLineDash = getPrevLineDashValue(
      this.ctx.lineDash,
      this.ctx.lineDashOffset
    );
    if (this.prevLineDash !== nextLineDash) {
      this.pdf.setLineDash(this.ctx.lineDash, this.ctx.lineDashOffset);
      this.prevLineDash = nextLineDash;
    }
  };
})(jsPDF.API);

// DEFLATE is a complex format; to read this code, you should probably check the RFC first:

// aliases for shorter compressed code (most minifers don't do this)
var u8 = Uint8Array, u16 = Uint16Array, i32 = Int32Array;
// fixed length extra bits
var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, /* unused */ 0, 0, /* impossible */ 0]);
// fixed distance extra bits
var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, /* unused */ 0, 0]);
// code length index map
var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
// get base, reverse index map from extra bits
var freb = function (eb, start) {
    var b = new u16(31);
    for (var i = 0; i < 31; ++i) {
        b[i] = start += 1 << eb[i - 1];
    }
    // numbers here are at max 18 bits
    var r = new i32(b[30]);
    for (var i = 1; i < 30; ++i) {
        for (var j = b[i]; j < b[i + 1]; ++j) {
            r[j] = ((j - b[i]) << 5) | i;
        }
    }
    return { b: b, r: r };
};
var _a = freb(fleb, 2), fl = _a.b, revfl = _a.r;
// we can ignore the fact that the other numbers are wrong; they never happen anyway
fl[28] = 258, revfl[258] = 28;
var _b = freb(fdeb, 0), revfd = _b.r;
// map of value to reverse (assuming 16 bits)
var rev = new u16(32768);
for (var i = 0; i < 32768; ++i) {
    // reverse table algorithm from SO
    var x = ((i & 0xAAAA) >> 1) | ((i & 0x5555) << 1);
    x = ((x & 0xCCCC) >> 2) | ((x & 0x3333) << 2);
    x = ((x & 0xF0F0) >> 4) | ((x & 0x0F0F) << 4);
    rev[i] = (((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8)) >> 1;
}
// create huffman tree from u8 "map": index -> code length for code index
// mb (max bits) must be at most 15
// TODO: optimize/split up?
var hMap = (function (cd, mb, r) {
    var s = cd.length;
    // index
    var i = 0;
    // u16 "map": index -> # of codes with bit length = index
    var l = new u16(mb);
    // length of cd must be 288 (total # of codes)
    for (; i < s; ++i) {
        if (cd[i])
            ++l[cd[i] - 1];
    }
    // u16 "map": index -> minimum code for bit length = index
    var le = new u16(mb);
    for (i = 1; i < mb; ++i) {
        le[i] = (le[i - 1] + l[i - 1]) << 1;
    }
    var co;
    {
        co = new u16(s);
        for (i = 0; i < s; ++i) {
            if (cd[i]) {
                co[i] = rev[le[cd[i] - 1]++] >> (15 - cd[i]);
            }
        }
    }
    return co;
});
// fixed length tree
var flt = new u8(288);
for (var i = 0; i < 144; ++i)
    flt[i] = 8;
for (var i = 144; i < 256; ++i)
    flt[i] = 9;
for (var i = 256; i < 280; ++i)
    flt[i] = 7;
for (var i = 280; i < 288; ++i)
    flt[i] = 8;
// fixed distance tree
var fdt = new u8(32);
for (var i = 0; i < 32; ++i)
    fdt[i] = 5;
// fixed length map
var flm = /*#__PURE__*/ hMap(flt, 9);
// fixed distance map
var fdm = /*#__PURE__*/ hMap(fdt, 5);
// get end of byte
var shft = function (p) { return ((p + 7) / 8) | 0; };
// typed array slice - allows garbage collector to free original reference,
// while being more compatible than .slice
var slc = function (v, s, e) {
    if (e == null || e > v.length)
        e = v.length;
    // can't use .constructor in case user-supplied
    return new u8(v.subarray(s, e));
};
// starting at p, write the minimum number of bits that can hold v to d
var wbits = function (d, p, v) {
    v <<= p & 7;
    var o = (p / 8) | 0;
    d[o] |= v;
    d[o + 1] |= v >> 8;
};
// starting at p, write the minimum number of bits (>8) that can hold v to d
var wbits16 = function (d, p, v) {
    v <<= p & 7;
    var o = (p / 8) | 0;
    d[o] |= v;
    d[o + 1] |= v >> 8;
    d[o + 2] |= v >> 16;
};
// creates code lengths from a frequency table
var hTree = function (d, mb) {
    // Need extra info to make a tree
    var t = [];
    for (var i = 0; i < d.length; ++i) {
        if (d[i])
            t.push({ s: i, f: d[i] });
    }
    var s = t.length;
    var t2 = t.slice();
    if (!s)
        return { t: et, l: 0 };
    if (s == 1) {
        var v = new u8(t[0].s + 1);
        v[t[0].s] = 1;
        return { t: v, l: 1 };
    }
    t.sort(function (a, b) { return a.f - b.f; });
    // after i2 reaches last ind, will be stopped
    // freq must be greater than largest possible number of symbols
    t.push({ s: -1, f: 25001 });
    var l = t[0], r = t[1], i0 = 0, i1 = 1, i2 = 2;
    t[0] = { s: -1, f: l.f + r.f, l: l, r: r };
    // efficient algorithm from UZIP.js
    // i0 is lookbehind, i2 is lookahead - after processing two low-freq
    // symbols that combined have high freq, will start processing i2 (high-freq,
    // non-composite) symbols instead
    // see https://reddit.com/r/photopea/comments/ikekht/uzipjs_questions/
    while (i1 != s - 1) {
        l = t[t[i0].f < t[i2].f ? i0++ : i2++];
        r = t[i0 != i1 && t[i0].f < t[i2].f ? i0++ : i2++];
        t[i1++] = { s: -1, f: l.f + r.f, l: l, r: r };
    }
    var maxSym = t2[0].s;
    for (var i = 1; i < s; ++i) {
        if (t2[i].s > maxSym)
            maxSym = t2[i].s;
    }
    // code lengths
    var tr = new u16(maxSym + 1);
    // max bits in tree
    var mbt = ln(t[i1 - 1], tr, 0);
    if (mbt > mb) {
        // more algorithms from UZIP.js
        // TODO: find out how this code works (debt)
        //  ind    debt
        var i = 0, dt = 0;
        //    left            cost
        var lft = mbt - mb, cst = 1 << lft;
        t2.sort(function (a, b) { return tr[b.s] - tr[a.s] || a.f - b.f; });
        for (; i < s; ++i) {
            var i2_1 = t2[i].s;
            if (tr[i2_1] > mb) {
                dt += cst - (1 << (mbt - tr[i2_1]));
                tr[i2_1] = mb;
            }
            else
                break;
        }
        dt >>= lft;
        while (dt > 0) {
            var i2_2 = t2[i].s;
            if (tr[i2_2] < mb)
                dt -= 1 << (mb - tr[i2_2]++ - 1);
            else
                ++i;
        }
        for (; i >= 0 && dt; --i) {
            var i2_3 = t2[i].s;
            if (tr[i2_3] == mb) {
                --tr[i2_3];
                ++dt;
            }
        }
        mbt = mb;
    }
    return { t: new u8(tr), l: mbt };
};
// get the max length and assign length codes
var ln = function (n, l, d) {
    return n.s == -1
        ? Math.max(ln(n.l, l, d + 1), ln(n.r, l, d + 1))
        : (l[n.s] = d);
};
// length codes generation
var lc = function (c) {
    var s = c.length;
    // Note that the semicolon was intentional
    while (s && !c[--s])
        ;
    var cl = new u16(++s);
    //  ind      num         streak
    var cli = 0, cln = c[0], cls = 1;
    var w = function (v) { cl[cli++] = v; };
    for (var i = 1; i <= s; ++i) {
        if (c[i] == cln && i != s)
            ++cls;
        else {
            if (!cln && cls > 2) {
                for (; cls > 138; cls -= 138)
                    w(32754);
                if (cls > 2) {
                    w(cls > 10 ? ((cls - 11) << 5) | 28690 : ((cls - 3) << 5) | 12305);
                    cls = 0;
                }
            }
            else if (cls > 3) {
                w(cln), --cls;
                for (; cls > 6; cls -= 6)
                    w(8304);
                if (cls > 2)
                    w(((cls - 3) << 5) | 8208), cls = 0;
            }
            while (cls--)
                w(cln);
            cls = 1;
            cln = c[i];
        }
    }
    return { c: cl.subarray(0, cli), n: s };
};
// calculate the length of output from tree, code lengths
var clen = function (cf, cl) {
    var l = 0;
    for (var i = 0; i < cl.length; ++i)
        l += cf[i] * cl[i];
    return l;
};
// writes a fixed block
// returns the new bit pos
var wfblk = function (out, pos, dat) {
    // no need to write 00 as type: TypedArray defaults to 0
    var s = dat.length;
    var o = shft(pos + 2);
    out[o] = s & 255;
    out[o + 1] = s >> 8;
    out[o + 2] = out[o] ^ 255;
    out[o + 3] = out[o + 1] ^ 255;
    for (var i = 0; i < s; ++i)
        out[o + i + 4] = dat[i];
    return (o + 4 + s) * 8;
};
// writes a block
var wblk = function (dat, out, final, syms, lf, df, eb, li, bs, bl, p) {
    wbits(out, p++, final);
    ++lf[256];
    var _a = hTree(lf, 15), dlt = _a.t, mlb = _a.l;
    var _b = hTree(df, 15), ddt = _b.t, mdb = _b.l;
    var _c = lc(dlt), lclt = _c.c, nlc = _c.n;
    var _d = lc(ddt), lcdt = _d.c, ndc = _d.n;
    var lcfreq = new u16(19);
    for (var i = 0; i < lclt.length; ++i)
        ++lcfreq[lclt[i] & 31];
    for (var i = 0; i < lcdt.length; ++i)
        ++lcfreq[lcdt[i] & 31];
    var _e = hTree(lcfreq, 7), lct = _e.t, mlcb = _e.l;
    var nlcc = 19;
    for (; nlcc > 4 && !lct[clim[nlcc - 1]]; --nlcc)
        ;
    var flen = (bl + 5) << 3;
    var ftlen = clen(lf, flt) + clen(df, fdt) + eb;
    var dtlen = clen(lf, dlt) + clen(df, ddt) + eb + 14 + 3 * nlcc + clen(lcfreq, lct) + 2 * lcfreq[16] + 3 * lcfreq[17] + 7 * lcfreq[18];
    if (bs >= 0 && flen <= ftlen && flen <= dtlen)
        return wfblk(out, p, dat.subarray(bs, bs + bl));
    var lm, ll, dm, dl;
    wbits(out, p, 1 + (dtlen < ftlen)), p += 2;
    if (dtlen < ftlen) {
        lm = hMap(dlt, mlb), ll = dlt, dm = hMap(ddt, mdb), dl = ddt;
        var llm = hMap(lct, mlcb);
        wbits(out, p, nlc - 257);
        wbits(out, p + 5, ndc - 1);
        wbits(out, p + 10, nlcc - 4);
        p += 14;
        for (var i = 0; i < nlcc; ++i)
            wbits(out, p + 3 * i, lct[clim[i]]);
        p += 3 * nlcc;
        var lcts = [lclt, lcdt];
        for (var it = 0; it < 2; ++it) {
            var clct = lcts[it];
            for (var i = 0; i < clct.length; ++i) {
                var len = clct[i] & 31;
                wbits(out, p, llm[len]), p += lct[len];
                if (len > 15)
                    wbits(out, p, (clct[i] >> 5) & 127), p += clct[i] >> 12;
            }
        }
    }
    else {
        lm = flm, ll = flt, dm = fdm, dl = fdt;
    }
    for (var i = 0; i < li; ++i) {
        var sym = syms[i];
        if (sym > 255) {
            var len = (sym >> 18) & 31;
            wbits16(out, p, lm[len + 257]), p += ll[len + 257];
            if (len > 7)
                wbits(out, p, (sym >> 23) & 31), p += fleb[len];
            var dst = sym & 31;
            wbits16(out, p, dm[dst]), p += dl[dst];
            if (dst > 3)
                wbits16(out, p, (sym >> 5) & 8191), p += fdeb[dst];
        }
        else {
            wbits16(out, p, lm[sym]), p += ll[sym];
        }
    }
    wbits16(out, p, lm[256]);
    return p + ll[256];
};
// deflate options (nice << 13) | chain
var deo = /*#__PURE__*/ new i32([65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632]);
// empty
var et = /*#__PURE__*/ new u8(0);
// compresses data into a raw DEFLATE buffer
var dflt = function (dat, lvl, plvl, pre, post, st) {
    var s = st.z || dat.length;
    var o = new u8(pre + s + 5 * (1 + Math.ceil(s / 7000)) + post);
    // writing to this writes to the output buffer
    var w = o.subarray(pre, o.length - post);
    var lst = st.l;
    var pos = (st.r || 0) & 7;
    if (lvl) {
        if (pos)
            w[0] = st.r >> 3;
        var opt = deo[lvl - 1];
        var n = opt >> 13, c = opt & 8191;
        var msk_1 = (1 << plvl) - 1;
        //    prev 2-byte val map    curr 2-byte val map
        var prev = st.p || new u16(32768), head = st.h || new u16(msk_1 + 1);
        var bs1_1 = Math.ceil(plvl / 3), bs2_1 = 2 * bs1_1;
        var hsh = function (i) { return (dat[i] ^ (dat[i + 1] << bs1_1) ^ (dat[i + 2] << bs2_1)) & msk_1; };
        // 24576 is an arbitrary number of maximum symbols per block
        // 424 buffer for last block
        var syms = new i32(25000);
        // length/literal freq   distance freq
        var lf = new u16(288), df = new u16(32);
        //  l/lcnt  exbits  index          l/lind  waitdx          blkpos
        var lc_1 = 0, eb = 0, i = st.i || 0, li = 0, wi = st.w || 0, bs = 0;
        for (; i + 2 < s; ++i) {
            // hash value
            var hv = hsh(i);
            // index mod 32768    previous index mod
            var imod = i & 32767, pimod = head[hv];
            prev[imod] = pimod;
            head[hv] = imod;
            // We always should modify head and prev, but only add symbols if
            // this data is not yet processed ("wait" for wait index)
            if (wi <= i) {
                // bytes remaining
                var rem = s - i;
                if ((lc_1 > 7000 || li > 24576) && (rem > 423 || !lst)) {
                    pos = wblk(dat, w, 0, syms, lf, df, eb, li, bs, i - bs, pos);
                    li = lc_1 = eb = 0, bs = i;
                    for (var j = 0; j < 286; ++j)
                        lf[j] = 0;
                    for (var j = 0; j < 30; ++j)
                        df[j] = 0;
                }
                //  len    dist   chain
                var l = 2, d = 0, ch_1 = c, dif = imod - pimod & 32767;
                if (rem > 2 && hv == hsh(i - dif)) {
                    var maxn = Math.min(n, rem) - 1;
                    var maxd = Math.min(32767, i);
                    // max possible length
                    // not capped at dif because decompressors implement "rolling" index population
                    var ml = Math.min(258, rem);
                    while (dif <= maxd && --ch_1 && imod != pimod) {
                        if (dat[i + l] == dat[i + l - dif]) {
                            var nl = 0;
                            for (; nl < ml && dat[i + nl] == dat[i + nl - dif]; ++nl)
                                ;
                            if (nl > l) {
                                l = nl, d = dif;
                                // break out early when we reach "nice" (we are satisfied enough)
                                if (nl > maxn)
                                    break;
                                // now, find the rarest 2-byte sequence within this
                                // length of literals and search for that instead.
                                // Much faster than just using the start
                                var mmd = Math.min(dif, nl - 2);
                                var md = 0;
                                for (var j = 0; j < mmd; ++j) {
                                    var ti = i - dif + j & 32767;
                                    var pti = prev[ti];
                                    var cd = ti - pti & 32767;
                                    if (cd > md)
                                        md = cd, pimod = ti;
                                }
                            }
                        }
                        // check the previous match
                        imod = pimod, pimod = prev[imod];
                        dif += imod - pimod & 32767;
                    }
                }
                // d will be nonzero only when a match was found
                if (d) {
                    // store both dist and len data in one int32
                    // Make sure this is recognized as a len/dist with 28th bit (2^28)
                    syms[li++] = 268435456 | (revfl[l] << 18) | revfd[d];
                    var lin = revfl[l] & 31, din = revfd[d] & 31;
                    eb += fleb[lin] + fdeb[din];
                    ++lf[257 + lin];
                    ++df[din];
                    wi = i + l;
                    ++lc_1;
                }
                else {
                    syms[li++] = dat[i];
                    ++lf[dat[i]];
                }
            }
        }
        for (i = Math.max(i, wi); i < s; ++i) {
            syms[li++] = dat[i];
            ++lf[dat[i]];
        }
        pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i - bs, pos);
        if (!lst) {
            st.r = (pos & 7) | w[(pos / 8) | 0] << 3;
            // shft(pos) now 1 less if pos & 7 != 0
            pos -= 7;
            st.h = head, st.p = prev, st.i = i, st.w = wi;
        }
    }
    else {
        for (var i = st.w || 0; i < s + lst; i += 65535) {
            // end
            var e = i + 65535;
            if (e >= s) {
                // write final block
                w[(pos / 8) | 0] = lst;
                e = s;
            }
            pos = wfblk(w, pos + 1, dat.subarray(i, e));
        }
        st.i = s;
    }
    return slc(o, 0, pre + shft(pos) + post);
};
// Adler32
var adler = function () {
    var a = 1, b = 0;
    return {
        p: function (d) {
            // closures have awful performance
            var n = a, m = b;
            var l = d.length | 0;
            for (var i = 0; i != l;) {
                var e = Math.min(i + 2655, l);
                for (; i < e; ++i)
                    m += n += d[i];
                n = (n & 65535) + 15 * (n >> 16), m = (m & 65535) + 15 * (m >> 16);
            }
            a = n, b = m;
        },
        d: function () {
            a %= 65521, b %= 65521;
            return (a & 255) << 24 | (a & 0xFF00) << 8 | (b & 255) << 8 | (b >> 8);
        }
    };
};
// deflate with opts
var dopt = function (dat, opt, pre, post, st) {
    if (!st) {
        st = { l: 1 };
        if (opt.dictionary) {
            var dict = opt.dictionary.subarray(-32768);
            var newDat = new u8(dict.length + dat.length);
            newDat.set(dict);
            newDat.set(dat, dict.length);
            dat = newDat;
            st.w = dict.length;
        }
    }
    return dflt(dat, opt.level == null ? 6 : opt.level, opt.mem == null ? Math.ceil(Math.max(8, Math.min(13, Math.log(dat.length))) * 1.5) : (12 + opt.mem), pre, post, st);
};
// write bytes
var wbytes = function (d, b, v) {
    for (; v; ++b)
        d[b] = v, v >>>= 8;
};
// zlib header
var zlh = function (c, o) {
    var lv = o.level, fl = lv == 0 ? 0 : lv < 6 ? 1 : lv == 9 ? 3 : 2;
    c[0] = 120, c[1] = (fl << 6) | (o.dictionary && 32);
    c[1] |= 31 - ((c[0] << 8) | c[1]) % 31;
    if (o.dictionary) {
        var h = adler();
        h.p(o.dictionary);
        wbytes(c, 2, h.d());
    }
};
/**
 * Compress data with Zlib
 * @param data The data to compress
 * @param opts The compression options
 * @returns The zlib-compressed version of the data
 */
function zlibSync(data, opts) {
    if (!opts)
        opts = {};
    var a = adler();
    a.p(data);
    var d = dopt(data, opts, opts.dictionary ? 6 : 2, 4);
    return zlh(d, opts), wbytes(d, d.length - 4, a.d()), d;
}
// text decoder
var td = typeof TextDecoder != 'undefined' && /*#__PURE__*/ new TextDecoder();
// text decoder stream
var tds = 0;
try {
    td.decode(et, { stream: true });
    tds = 1;
}
catch (e) { }

/**
 * @license
 * jsPDF filters PlugIn
 * Copyright (c) 2014 Aras Abbasi
 *
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 */

(function(jsPDFAPI) {

  var ASCII85Encode = function(a) {
    var b, c, d, e, f, g, h, i, j, k;
    // eslint-disable-next-line no-control-regex
    for (
      b = "\x00\x00\x00\x00".slice(a.length % 4 || 4),
        a += b,
        c = [],
        d = 0,
        e = a.length;
      e > d;
      d += 4
    )
      (f =
        (a.charCodeAt(d) << 24) +
        (a.charCodeAt(d + 1) << 16) +
        (a.charCodeAt(d + 2) << 8) +
        a.charCodeAt(d + 3)),
        0 !== f
          ? ((k = f % 85),
            (f = (f - k) / 85),
            (j = f % 85),
            (f = (f - j) / 85),
            (i = f % 85),
            (f = (f - i) / 85),
            (h = f % 85),
            (f = (f - h) / 85),
            (g = f % 85),
            c.push(g + 33, h + 33, i + 33, j + 33, k + 33))
          : c.push(122);
    return (
      (function(a, b) {
        for (var c = b; c > 0; c--) a.pop();
      })(c, b.length),
      String.fromCharCode.apply(String, c) + "~>"
    );
  };

  var ASCII85Decode = function(a) {
    var c,
      d,
      e,
      f,
      g,
      h = String,
      l = "length",
      w = 255,
      x = "charCodeAt",
      y = "slice",
      z = "replace";
    for (
      "~>" === a[y](-2),
        a = a[y](0, -2)
          [z](/\s/g, "")
          [z]("z", "!!!!!"),
        c = "uuuuu"[y](a[l] % 5 || 5),
        a += c,
        e = [],
        f = 0,
        g = a[l];
      g > f;
      f += 5
    )
      (d =
        52200625 * (a[x](f) - 33) +
        614125 * (a[x](f + 1) - 33) +
        7225 * (a[x](f + 2) - 33) +
        85 * (a[x](f + 3) - 33) +
        (a[x](f + 4) - 33)),
        e.push(w & (d >> 24), w & (d >> 16), w & (d >> 8), w & d);
    return (
      (function(a, b) {
        for (var c = b; c > 0; c--) a.pop();
      })(e, c[l]),
      h.fromCharCode.apply(h, e)
    );
  };

  var ASCIIHexEncode = function(value) {
    return (
      value
        .split("")
        .map(function(value) {
          return ("0" + value.charCodeAt().toString(16)).slice(-2);
        })
        .join("") + ">"
    );
  };

  var ASCIIHexDecode = function(value) {
    var regexCheckIfHex = new RegExp(/^([0-9A-Fa-f]{2})+$/);
    value = value.replace(/\s/g, "");
    if (value.indexOf(">") !== -1) {
      value = value.substr(0, value.indexOf(">"));
    }
    if (value.length % 2) {
      value += "0";
    }
    if (regexCheckIfHex.test(value) === false) {
      return "";
    }
    var result = "";
    for (var i = 0; i < value.length; i += 2) {
      result += String.fromCharCode("0x" + (value[i] + value[i + 1]));
    }
    return result;
  };
  /*
  var FlatePredictors = {
      None: 1,
      TIFF: 2,
      PNG_None: 10,
      PNG_Sub: 11,
      PNG_Up: 12,
      PNG_Average: 13,
      PNG_Paeth: 14,
      PNG_Optimum: 15
  };
  */

  var FlateEncode = function(data) {
    var arr = new Uint8Array(data.length);
    var i = data.length;
    while (i--) {
      arr[i] = data.charCodeAt(i);
    }
    arr = zlibSync(arr);
    data = arr.reduce(function(data, byte) {
      return data + String.fromCharCode(byte);
    }, "");
    return data;
  };

  jsPDFAPI.processDataByFilters = function(origData, filterChain) {
    var i = 0;
    var data = origData || "";
    var reverseChain = [];
    filterChain = filterChain || [];

    if (typeof filterChain === "string") {
      filterChain = [filterChain];
    }

    for (i = 0; i < filterChain.length; i += 1) {
      switch (filterChain[i]) {
        case "ASCII85Decode":
        case "/ASCII85Decode":
          data = ASCII85Decode(data);
          reverseChain.push("/ASCII85Encode");
          break;
        case "ASCII85Encode":
        case "/ASCII85Encode":
          data = ASCII85Encode(data);
          reverseChain.push("/ASCII85Decode");
          break;
        case "ASCIIHexDecode":
        case "/ASCIIHexDecode":
          data = ASCIIHexDecode(data);
          reverseChain.push("/ASCIIHexEncode");
          break;
        case "ASCIIHexEncode":
        case "/ASCIIHexEncode":
          data = ASCIIHexEncode(data);
          reverseChain.push("/ASCIIHexDecode");
          break;
        case "FlateEncode":
        case "/FlateEncode":
          data = FlateEncode(data);
          reverseChain.push("/FlateDecode");
          break;
        default:
          throw new Error(
            'The filter: "' + filterChain[i] + '" is not implemented'
          );
      }
    }

    return { data: data, reverseChain: reverseChain.reverse().join(" ") };
  };
})(jsPDF.API);

/**
 * @license
 * ====================================================================
 * Copyright (c) 2013 Youssef Beddad, youssef.beddad@gmail.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * ====================================================================
 */

/**
 * jsPDF JavaScript plugin
 *
 * @name javascript
 * @module
 */
(function(jsPDFAPI) {
  var jsNamesObj, jsJsObj, text;
  /**
   * @name addJS
   * @function
   * @param {string} javascript The javascript to be embedded into the PDF-file.
   * @returns {jsPDF}
   */
  jsPDFAPI.addJS = function(javascript) {
    text = javascript;
    this.internal.events.subscribe("postPutResources", function() {
      jsNamesObj = this.internal.newObject();
      this.internal.out("<<");
      this.internal.out("/Names [(EmbeddedJS) " + (jsNamesObj + 1) + " 0 R]");
      this.internal.out(">>");
      this.internal.out("endobj");

      jsJsObj = this.internal.newObject();
      this.internal.out("<<");
      this.internal.out("/S /JavaScript");
      this.internal.out("/JS (" + text + ")");
      this.internal.out(">>");
      this.internal.out("endobj");
    });
    this.internal.events.subscribe("putCatalog", function() {
      if (jsNamesObj !== undefined && jsJsObj !== undefined) {
        this.internal.out("/Names <</JavaScript " + jsNamesObj + " 0 R>>");
      }
    });
    return this;
  };
})(jsPDF.API);

/**
 * @license
 * Copyright (c) 2014 Steven Spungin (TwelveTone LLC)  steven@twelvetone.tv
 *
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 */

/**
 * jsPDF Outline PlugIn
 *
 * Generates a PDF Outline
 * @name outline
 * @module
 */
(function(jsPDFAPI) {

  var namesOid;
  //var destsGoto = [];

  jsPDFAPI.events.push([
    "postPutResources",
    function() {
      var pdf = this;
      var rx = /^(\d+) 0 obj$/;

      // Write action goto objects for each page
      // this.outline.destsGoto = [];
      // for (var i = 0; i < totalPages; i++) {
      // var id = pdf.internal.newObject();
      // this.outline.destsGoto.push(id);
      // pdf.internal.write("<</D[" + (i * 2 + 3) + " 0 R /XYZ null
      // null null]/S/GoTo>> endobj");
      // }
      //
      // for (var i = 0; i < dests.length; i++) {
      // pdf.internal.write("(page_" + (i + 1) + ")" + dests[i] + " 0
      // R");
      // }
      //
      if (this.outline.root.children.length > 0) {
        var lines = pdf.outline.render().split(/\r\n/);
        for (var i = 0; i < lines.length; i++) {
          var line = lines[i];
          var m = rx.exec(line);
          if (m != null) {
            var oid = m[1];
            pdf.internal.newObjectDeferredBegin(oid, false);
          }
          pdf.internal.write(line);
        }
      }

      // This code will write named destination for each page reference
      // (page_1, etc)
      if (this.outline.createNamedDestinations) {
        var totalPages = this.internal.pages.length;
        // WARNING: this assumes jsPDF starts on page 3 and pageIDs
        // follow 5, 7, 9, etc
        // Write destination objects for each page
        var dests = [];
        for (var i = 0; i < totalPages; i++) {
          var id = pdf.internal.newObject();
          dests.push(id);
          var info = pdf.internal.getPageInfo(i + 1);
          pdf.internal.write(
            "<< /D[" + info.objId + " 0 R /XYZ null null null]>> endobj"
          );
        }

        // assign a name for each destination
        var names2Oid = pdf.internal.newObject();
        pdf.internal.write("<< /Names [ ");
        for (var i = 0; i < dests.length; i++) {
          pdf.internal.write("(page_" + (i + 1) + ")" + dests[i] + " 0 R");
        }
        pdf.internal.write(" ] >>", "endobj");

        // var kids = pdf.internal.newObject();
        // pdf.internal.write('<< /Kids [ ' + names2Oid + ' 0 R');
        // pdf.internal.write(' ] >>', 'endobj');

        namesOid = pdf.internal.newObject();
        pdf.internal.write("<< /Dests " + names2Oid + " 0 R");
        pdf.internal.write(">>", "endobj");
      }
    }
  ]);

  jsPDFAPI.events.push([
    "putCatalog",
    function() {
      var pdf = this;
      if (pdf.outline.root.children.length > 0) {
        pdf.internal.write(
          "/Outlines",
          this.outline.makeRef(this.outline.root)
        );
        if (this.outline.createNamedDestinations) {
          pdf.internal.write("/Names " + namesOid + " 0 R");
        }
        // Open with Bookmarks showing
        // pdf.internal.write("/PageMode /UseOutlines");
      }
    }
  ]);

  jsPDFAPI.events.push([
    "initialized",
    function() {
      var pdf = this;

      pdf.outline = {
        createNamedDestinations: false,
        root: {
          children: []
        }
      };

      /**
       * Options: pageNumber
       */
      pdf.outline.add = function(parent, title, options) {
        var item = {
          title: title,
          options: options,
          children: []
        };
        if (parent == null) {
          parent = this.root;
        }
        parent.children.push(item);
        return item;
      };

      pdf.outline.render = function() {
        this.ctx = {};
        this.ctx.val = "";
        this.ctx.pdf = pdf;

        this.genIds_r(this.root);
        this.renderRoot(this.root);
        this.renderItems(this.root);

        return this.ctx.val;
      };

      pdf.outline.genIds_r = function(node) {
        node.id = pdf.internal.newObjectDeferred();
        for (var i = 0; i < node.children.length; i++) {
          this.genIds_r(node.children[i]);
        }
      };

      pdf.outline.renderRoot = function(node) {
        this.objStart(node);
        this.line("/Type /Outlines");
        if (node.children.length > 0) {
          this.line("/First " + this.makeRef(node.children[0]));
          this.line(
            "/Last " + this.makeRef(node.children[node.children.length - 1])
          );
        }
        this.line(
          "/Count " +
            this.count_r(
              {
                count: 0
              },
              node
            )
        );
        this.objEnd();
      };

      pdf.outline.renderItems = function(node) {
        var getVerticalCoordinateString = this.ctx.pdf.internal
          .getVerticalCoordinateString;
        for (var i = 0; i < node.children.length; i++) {
          var item = node.children[i];
          this.objStart(item);

          this.line("/Title " + this.makeString(item.title));

          this.line("/Parent " + this.makeRef(node));
          if (i > 0) {
            this.line("/Prev " + this.makeRef(node.children[i - 1]));
          }
          if (i < node.children.length - 1) {
            this.line("/Next " + this.makeRef(node.children[i + 1]));
          }
          if (item.children.length > 0) {
            this.line("/First " + this.makeRef(item.children[0]));
            this.line(
              "/Last " + this.makeRef(item.children[item.children.length - 1])
            );
          }

          var count = (this.count = this.count_r(
            {
              count: 0
            },
            item
          ));
          if (count > 0) {
            this.line("/Count " + count);
          }

          if (item.options) {
            if (item.options.pageNumber) {
              // Explicit Destination
              //WARNING this assumes page ids are 3,5,7, etc.
              var info = pdf.internal.getPageInfo(item.options.pageNumber);
              this.line(
                "/Dest " +
                  "[" +
                  info.objId +
                  " 0 R /XYZ 0 " +
                  getVerticalCoordinateString(0) +
                  " 0]"
              );
              // this line does not work on all clients (pageNumber instead of page ref)
              //this.line('/Dest ' + '[' + (item.options.pageNumber - 1) + ' /XYZ 0 ' + this.ctx.pdf.internal.pageSize.getHeight() + ' 0]');

              // Named Destination
              // this.line('/Dest (page_' + (item.options.pageNumber) + ')');

              // Action Destination
              // var id = pdf.internal.newObject();
              // pdf.internal.write('<</D[' + (item.options.pageNumber - 1) + ' /XYZ null null null]/S/GoTo>> endobj');
              // this.line('/A ' + id + ' 0 R' );
            }
          }
          this.objEnd();
        }
        for (var z = 0; z < node.children.length; z++) {
          this.renderItems(node.children[z]);
        }
      };

      pdf.outline.line = function(text) {
        this.ctx.val += text + "\r\n";
      };

      pdf.outline.makeRef = function(node) {
        return node.id + " 0 R";
      };

      pdf.outline.makeString = function(val) {
        return "(" + pdf.internal.pdfEscape(val) + ")";
      };

      pdf.outline.objStart = function(node) {
        this.ctx.val += "\r\n" + node.id + " 0 obj" + "\r\n<<\r\n";
      };

      pdf.outline.objEnd = function() {
        this.ctx.val += ">> \r\n" + "endobj" + "\r\n";
      };

      pdf.outline.count_r = function(ctx, node) {
        for (var i = 0; i < node.children.length; i++) {
          ctx.count++;
          this.count_r(ctx, node.children[i]);
        }
        return ctx.count;
      };
    }
  ]);

  return this;
})(jsPDF.API);

function decode(bytes, encoding = 'utf8') {
    const decoder = new TextDecoder(encoding);
    return decoder.decode(bytes);
}
const encoder = new TextEncoder();
function encode(str) {
    return encoder.encode(str);
}

const defaultByteLength = 1024 * 8;
const hostBigEndian = (() => {
    const array = new Uint8Array(4);
    const view = new Uint32Array(array.buffer);
    return !((view[0] = 1) & array[0]);
})();
const typedArrays = {
    int8: globalThis.Int8Array,
    uint8: globalThis.Uint8Array,
    int16: globalThis.Int16Array,
    uint16: globalThis.Uint16Array,
    int32: globalThis.Int32Array,
    uint32: globalThis.Uint32Array,
    uint64: globalThis.BigUint64Array,
    int64: globalThis.BigInt64Array,
    float32: globalThis.Float32Array,
    float64: globalThis.Float64Array,
};
class IOBuffer {
    /**
     * Reference to the internal ArrayBuffer object.
     */
    buffer;
    /**
     * Byte length of the internal ArrayBuffer.
     */
    byteLength;
    /**
     * Byte offset of the internal ArrayBuffer.
     */
    byteOffset;
    /**
     * Byte length of the internal ArrayBuffer.
     */
    length;
    /**
     * The current offset of the buffer's pointer.
     */
    offset;
    lastWrittenByte;
    littleEndian;
    _data;
    _mark;
    _marks;
    /**
     * Create a new IOBuffer.
     * @param data - The data to construct the IOBuffer with.
     * If data is a number, it will be the new buffer's length<br>
     * If data is `undefined`, the buffer will be initialized with a default length of 8Kb<br>
     * If data is an ArrayBuffer, SharedArrayBuffer, an ArrayBufferView (Typed Array), an IOBuffer instance,
     * or a Node.js Buffer, a view will be created over the underlying ArrayBuffer.
     * @param options - An object for the options.
     * @returns A new IOBuffer instance.
     */
    constructor(data = defaultByteLength, options = {}) {
        let dataIsGiven = false;
        if (typeof data === 'number') {
            data = new ArrayBuffer(data);
        }
        else {
            dataIsGiven = true;
            this.lastWrittenByte = data.byteLength;
        }
        const offset = options.offset ? options.offset >>> 0 : 0;
        const byteLength = data.byteLength - offset;
        let dvOffset = offset;
        if (ArrayBuffer.isView(data) || data instanceof IOBuffer) {
            if (data.byteLength !== data.buffer.byteLength) {
                dvOffset = data.byteOffset + offset;
            }
            data = data.buffer;
        }
        if (dataIsGiven) {
            this.lastWrittenByte = byteLength;
        }
        else {
            this.lastWrittenByte = 0;
        }
        this.buffer = data;
        this.length = byteLength;
        this.byteLength = byteLength;
        this.byteOffset = dvOffset;
        this.offset = 0;
        this.littleEndian = true;
        this._data = new DataView(this.buffer, dvOffset, byteLength);
        this._mark = 0;
        this._marks = [];
    }
    /**
     * Checks if the memory allocated to the buffer is sufficient to store more
     * bytes after the offset.
     * @param byteLength - The needed memory in bytes.
     * @returns `true` if there is sufficient space and `false` otherwise.
     */
    available(byteLength = 1) {
        return this.offset + byteLength <= this.length;
    }
    /**
     * Check if little-endian mode is used for reading and writing multi-byte
     * values.
     * @returns `true` if little-endian mode is used, `false` otherwise.
     */
    isLittleEndian() {
        return this.littleEndian;
    }
    /**
     * Set little-endian mode for reading and writing multi-byte values.
     * @returns This.
     */
    setLittleEndian() {
        this.littleEndian = true;
        return this;
    }
    /**
     * Check if big-endian mode is used for reading and writing multi-byte values.
     * @returns `true` if big-endian mode is used, `false` otherwise.
     */
    isBigEndian() {
        return !this.littleEndian;
    }
    /**
     * Switches to big-endian mode for reading and writing multi-byte values.
     * @returns This.
     */
    setBigEndian() {
        this.littleEndian = false;
        return this;
    }
    /**
     * Move the pointer n bytes forward.
     * @param n - Number of bytes to skip.
     * @returns This.
     */
    skip(n = 1) {
        this.offset += n;
        return this;
    }
    /**
     * Move the pointer n bytes backward.
     * @param n - Number of bytes to move back.
     * @returns This.
     */
    back(n = 1) {
        this.offset -= n;
        return this;
    }
    /**
     * Move the pointer to the given offset.
     * @param offset - The offset to move to.
     * @returns This.
     */
    seek(offset) {
        this.offset = offset;
        return this;
    }
    /**
     * Store the current pointer offset.
     * @see {@link IOBuffer#reset}
     * @returns This.
     */
    mark() {
        this._mark = this.offset;
        return this;
    }
    /**
     * Move the pointer back to the last pointer offset set by mark.
     * @see {@link IOBuffer#mark}
     * @returns This.
     */
    reset() {
        this.offset = this._mark;
        return this;
    }
    /**
     * Push the current pointer offset to the mark stack.
     * @see {@link IOBuffer#popMark}
     * @returns This.
     */
    pushMark() {
        this._marks.push(this.offset);
        return this;
    }
    /**
     * Pop the last pointer offset from the mark stack, and set the current
     * pointer offset to the popped value.
     * @see {@link IOBuffer#pushMark}
     * @returns This.
     */
    popMark() {
        const offset = this._marks.pop();
        if (offset === undefined) {
            throw new Error('Mark stack empty');
        }
        this.seek(offset);
        return this;
    }
    /**
     * Move the pointer offset back to 0.
     * @returns This.
     */
    rewind() {
        this.offset = 0;
        return this;
    }
    /**
     * Make sure the buffer has sufficient memory to write a given byteLength at
     * the current pointer offset.
     * If the buffer's memory is insufficient, this method will create a new
     * buffer (a copy) with a length that is twice (byteLength + current offset).
     * @param byteLength - The needed memory in bytes.
     * @returns This.
     */
    ensureAvailable(byteLength = 1) {
        if (!this.available(byteLength)) {
            const lengthNeeded = this.offset + byteLength;
            const newLength = lengthNeeded * 2;
            const newArray = new Uint8Array(newLength);
            newArray.set(new Uint8Array(this.buffer));
            this.buffer = newArray.buffer;
            this.length = newLength;
            this.byteLength = newLength;
            this._data = new DataView(this.buffer);
        }
        return this;
    }
    /**
     * Read a byte and return false if the byte's value is 0, or true otherwise.
     * Moves pointer forward by one byte.
     * @returns The read boolean.
     */
    readBoolean() {
        return this.readUint8() !== 0;
    }
    /**
     * Read a signed 8-bit integer and move pointer forward by 1 byte.
     * @returns The read byte.
     */
    readInt8() {
        return this._data.getInt8(this.offset++);
    }
    /**
     * Read an unsigned 8-bit integer and move pointer forward by 1 byte.
     * @returns The read byte.
     */
    readUint8() {
        return this._data.getUint8(this.offset++);
    }
    /**
     * Alias for {@link IOBuffer#readUint8}.
     * @returns The read byte.
     */
    readByte() {
        return this.readUint8();
    }
    /**
     * Read `n` bytes and move pointer forward by `n` bytes.
     * @param n - Number of bytes to read.
     * @returns The read bytes.
     */
    readBytes(n = 1) {
        return this.readArray(n, 'uint8');
    }
    /**
     * Creates an array of corresponding to the type `type` and size `size`.
     * For example type `uint8` will create a `Uint8Array`.
     * @param size - size of the resulting array
     * @param type - number type of elements to read
     * @returns The read array.
     */
    readArray(size, type) {
        const bytes = typedArrays[type].BYTES_PER_ELEMENT * size;
        const offset = this.byteOffset + this.offset;
        const slice = this.buffer.slice(offset, offset + bytes);
        if (this.littleEndian === hostBigEndian &&
            type !== 'uint8' &&
            type !== 'int8') {
            const slice = new Uint8Array(this.buffer.slice(offset, offset + bytes));
            slice.reverse();
            const returnArray = new typedArrays[type](slice.buffer);
            this.offset += bytes;
            returnArray.reverse();
            return returnArray;
        }
        const returnArray = new typedArrays[type](slice);
        this.offset += bytes;
        return returnArray;
    }
    /**
     * Read a 16-bit signed integer and move pointer forward by 2 bytes.
     * @returns The read value.
     */
    readInt16() {
        const value = this._data.getInt16(this.offset, this.littleEndian);
        this.offset += 2;
        return value;
    }
    /**
     * Read a 16-bit unsigned integer and move pointer forward by 2 bytes.
     * @returns The read value.
     */
    readUint16() {
        const value = this._data.getUint16(this.offset, this.littleEndian);
        this.offset += 2;
        return value;
    }
    /**
     * Read a 32-bit signed integer and move pointer forward by 4 bytes.
     * @returns The read value.
     */
    readInt32() {
        const value = this._data.getInt32(this.offset, this.littleEndian);
        this.offset += 4;
        return value;
    }
    /**
     * Read a 32-bit unsigned integer and move pointer forward by 4 bytes.
     * @returns The read value.
     */
    readUint32() {
        const value = this._data.getUint32(this.offset, this.littleEndian);
        this.offset += 4;
        return value;
    }
    /**
     * Read a 32-bit floating number and move pointer forward by 4 bytes.
     * @returns The read value.
     */
    readFloat32() {
        const value = this._data.getFloat32(this.offset, this.littleEndian);
        this.offset += 4;
        return value;
    }
    /**
     * Read a 64-bit floating number and move pointer forward by 8 bytes.
     * @returns The read value.
     */
    readFloat64() {
        const value = this._data.getFloat64(this.offset, this.littleEndian);
        this.offset += 8;
        return value;
    }
    /**
     * Read a 64-bit signed integer number and move pointer forward by 8 bytes.
     * @returns The read value.
     */
    readBigInt64() {
        const value = this._data.getBigInt64(this.offset, this.littleEndian);
        this.offset += 8;
        return value;
    }
    /**
     * Read a 64-bit unsigned integer number and move pointer forward by 8 bytes.
     * @returns The read value.
     */
    readBigUint64() {
        const value = this._data.getBigUint64(this.offset, this.littleEndian);
        this.offset += 8;
        return value;
    }
    /**
     * Read a 1-byte ASCII character and move pointer forward by 1 byte.
     * @returns The read character.
     */
    readChar() {
        // eslint-disable-next-line unicorn/prefer-code-point
        return String.fromCharCode(this.readInt8());
    }
    /**
     * Read `n` 1-byte ASCII characters and move pointer forward by `n` bytes.
     * @param n - Number of characters to read.
     * @returns The read characters.
     */
    readChars(n = 1) {
        let result = '';
        for (let i = 0; i < n; i++) {
            result += this.readChar();
        }
        return result;
    }
    /**
     * Read the next `n` bytes, return a UTF-8 decoded string and move pointer
     * forward by `n` bytes.
     * @param n - Number of bytes to read.
     * @returns The decoded string.
     */
    readUtf8(n = 1) {
        return decode(this.readBytes(n));
    }
    /**
     * Read the next `n` bytes, return a string decoded with `encoding` and move pointer
     * forward by `n` bytes.
     * If no encoding is passed, the function is equivalent to @see {@link IOBuffer#readUtf8}
     * @param n - Number of bytes to read.
     * @param encoding - The encoding to use. Default is 'utf8'.
     * @returns The decoded string.
     */
    decodeText(n = 1, encoding = 'utf8') {
        return decode(this.readBytes(n), encoding);
    }
    /**
     * Write 0xff if the passed value is truthy, 0x00 otherwise and move pointer
     * forward by 1 byte.
     * @param value - The value to write.
     * @returns This.
     */
    writeBoolean(value) {
        this.writeUint8(value ? 0xff : 0x00);
        return this;
    }
    /**
     * Write `value` as an 8-bit signed integer and move pointer forward by 1 byte.
     * @param value - The value to write.
     * @returns This.
     */
    writeInt8(value) {
        this.ensureAvailable(1);
        this._data.setInt8(this.offset++, value);
        this._updateLastWrittenByte();
        return this;
    }
    /**
     * Write `value` as an 8-bit unsigned integer and move pointer forward by 1
     * byte.
     * @param value - The value to write.
     * @returns This.
     */
    writeUint8(value) {
        this.ensureAvailable(1);
        this._data.setUint8(this.offset++, value);
        this._updateLastWrittenByte();
        return this;
    }
    /**
     * An alias for {@link IOBuffer#writeUint8}.
     * @param value - The value to write.
     * @returns This.
     */
    writeByte(value) {
        return this.writeUint8(value);
    }
    /**
     * Write all elements of `bytes` as uint8 values and move pointer forward by
     * `bytes.length` bytes.
     * @param bytes - The array of bytes to write.
     * @returns This.
     */
    writeBytes(bytes) {
        this.ensureAvailable(bytes.length);
        // eslint-disable-next-line @typescript-eslint/prefer-for-of
        for (let i = 0; i < bytes.length; i++) {
            this._data.setUint8(this.offset++, bytes[i]);
        }
        this._updateLastWrittenByte();
        return this;
    }
    /**
     * Write `value` as a 16-bit signed integer and move pointer forward by 2
     * bytes.
     * @param value - The value to write.
     * @returns This.
     */
    writeInt16(value) {
        this.ensureAvailable(2);
        this._data.setInt16(this.offset, value, this.littleEndian);
        this.offset += 2;
        this._updateLastWrittenByte();
        return this;
    }
    /**
     * Write `value` as a 16-bit unsigned integer and move pointer forward by 2
     * bytes.
     * @param value - The value to write.
     * @returns This.
     */
    writeUint16(value) {
        this.ensureAvailable(2);
        this._data.setUint16(this.offset, value, this.littleEndian);
        this.offset += 2;
        this._updateLastWrittenByte();
        return this;
    }
    /**
     * Write `value` as a 32-bit signed integer and move pointer forward by 4
     * bytes.
     * @param value - The value to write.
     * @returns This.
     */
    writeInt32(value) {
        this.ensureAvailable(4);
        this._data.setInt32(this.offset, value, this.littleEndian);
        this.offset += 4;
        this._updateLastWrittenByte();
        return this;
    }
    /**
     * Write `value` as a 32-bit unsigned integer and move pointer forward by 4
     * bytes.
     * @param value - The value to write.
     * @returns This.
     */
    writeUint32(value) {
        this.ensureAvailable(4);
        this._data.setUint32(this.offset, value, this.littleEndian);
        this.offset += 4;
        this._updateLastWrittenByte();
        return this;
    }
    /**
     * Write `value` as a 32-bit floating number and move pointer forward by 4
     * bytes.
     * @param value - The value to write.
     * @returns This.
     */
    writeFloat32(value) {
        this.ensureAvailable(4);
        this._data.setFloat32(this.offset, value, this.littleEndian);
        this.offset += 4;
        this._updateLastWrittenByte();
        return this;
    }
    /**
     * Write `value` as a 64-bit floating number and move pointer forward by 8
     * bytes.
     * @param value - The value to write.
     * @returns This.
     */
    writeFloat64(value) {
        this.ensureAvailable(8);
        this._data.setFloat64(this.offset, value, this.littleEndian);
        this.offset += 8;
        this._updateLastWrittenByte();
        return this;
    }
    /**
     * Write `value` as a 64-bit signed bigint and move pointer forward by 8
     * bytes.
     * @param value - The value to write.
     * @returns This.
     */
    writeBigInt64(value) {
        this.ensureAvailable(8);
        this._data.setBigInt64(this.offset, value, this.littleEndian);
        this.offset += 8;
        this._updateLastWrittenByte();
        return this;
    }
    /**
     * Write `value` as a 64-bit unsigned bigint and move pointer forward by 8
     * bytes.
     * @param value - The value to write.
     * @returns This.
     */
    writeBigUint64(value) {
        this.ensureAvailable(8);
        this._data.setBigUint64(this.offset, value, this.littleEndian);
        this.offset += 8;
        this._updateLastWrittenByte();
        return this;
    }
    /**
     * Write the charCode of `str`'s first character as an 8-bit unsigned integer
     * and move pointer forward by 1 byte.
     * @param str - The character to write.
     * @returns This.
     */
    writeChar(str) {
        // eslint-disable-next-line unicorn/prefer-code-point
        return this.writeUint8(str.charCodeAt(0));
    }
    /**
     * Write the charCodes of all `str`'s characters as 8-bit unsigned integers
     * and move pointer forward by `str.length` bytes.
     * @param str - The characters to write.
     * @returns This.
     */
    writeChars(str) {
        for (let i = 0; i < str.length; i++) {
            // eslint-disable-next-line unicorn/prefer-code-point
            this.writeUint8(str.charCodeAt(i));
        }
        return this;
    }
    /**
     * UTF-8 encode and write `str` to the current pointer offset and move pointer
     * forward according to the encoded length.
     * @param str - The string to write.
     * @returns This.
     */
    writeUtf8(str) {
        return this.writeBytes(encode(str));
    }
    /**
     * Export a Uint8Array view of the internal buffer.
     * The view starts at the byte offset and its length
     * is calculated to stop at the last written byte or the original length.
     * @returns A new Uint8Array view.
     */
    toArray() {
        return new Uint8Array(this.buffer, this.byteOffset, this.lastWrittenByte);
    }
    /**
     *  Get the total number of bytes written so far, regardless of the current offset.
     * @returns - Total number of bytes.
     */
    getWrittenByteLength() {
        return this.lastWrittenByte - this.byteOffset;
    }
    /**
     * Update the last written byte offset
     * @private
     */
    _updateLastWrittenByte() {
        if (this.offset > this.lastWrittenByte) {
            this.lastWrittenByte = this.offset;
        }
    }
}

/*============================================================================*/


function zero$1(buf) { let len = buf.length; while (--len >= 0) { buf[len] = 0; } }
/* The three kinds of block type */

const MIN_MATCH$1    = 3;
const MAX_MATCH$1    = 258;
/* The minimum and maximum match lengths */

// From deflate.h
/* ===========================================================================
 * Internal compression state.
 */

const LENGTH_CODES$1  = 29;
/* number of length codes, not counting the special END_BLOCK code */

const LITERALS$1      = 256;
/* number of literal bytes 0..255 */

const L_CODES$1       = LITERALS$1 + 1 + LENGTH_CODES$1;
/* number of Literal or Length codes, including the END_BLOCK code */

const D_CODES$1       = 30;
/* eslint-enable comma-spacing,array-bracket-spacing */

/* The lengths of the bit length codes are sent in order of decreasing
 * probability, to avoid transmitting the lengths for unused bit length codes.
 */

/* ===========================================================================
 * Local data. These are initialized only once.
 */

// We pre-fill arrays with 0 to avoid uninitialized gaps

const DIST_CODE_LEN = 512; /* see definition of array dist_code below */

// !!!! Use flat array instead of structure, Freq = i*2, Len = i*2+1
const static_ltree  = new Array((L_CODES$1 + 2) * 2);
zero$1(static_ltree);
/* The static literal tree. Since the bit lengths are imposed, there is no
 * need for the L_CODES extra codes used during heap construction. However
 * The codes 286 and 287 are needed to build a canonical tree (see _tr_init
 * below).
 */

const static_dtree  = new Array(D_CODES$1 * 2);
zero$1(static_dtree);
/* The static distance tree. (Actually a trivial tree since all codes use
 * 5 bits.)
 */

const _dist_code    = new Array(DIST_CODE_LEN);
zero$1(_dist_code);
/* Distance codes. The first 256 values correspond to the distances
 * 3 .. 258, the last 256 values correspond to the top 8 bits of
 * the 15 bit distances.
 */

const _length_code  = new Array(MAX_MATCH$1 - MIN_MATCH$1 + 1);
zero$1(_length_code);
/* length code for each normalized match length (0 == MIN_MATCH) */

const base_length   = new Array(LENGTH_CODES$1);
zero$1(base_length);
/* First normalized length for each code (0 = MIN_MATCH) */

const base_dist     = new Array(D_CODES$1);
zero$1(base_dist);

// Note: adler32 takes 12% for level 0 and 2% for level 6.
// It isn't worth it to make additional optimizations as in original.
// Small size is preferable.

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

const adler32 = (adler, buf, len, pos) => {
  let s1 = (adler & 0xffff) |0,
      s2 = ((adler >>> 16) & 0xffff) |0,
      n = 0;

  while (len !== 0) {
    // Set limit ~ twice less than 5552, to keep
    // s2 in 31-bits, because we force signed ints.
    // in other case %= will fail.
    n = len > 2000 ? 2000 : len;
    len -= n;

    do {
      s1 = (s1 + buf[pos++]) |0;
      s2 = (s2 + s1) |0;
    } while (--n);

    s1 %= 65521;
    s2 %= 65521;
  }

  return (s1 | (s2 << 16)) |0;
};


var adler32_1 = adler32;

// Note: we can't get significant speed boost here.
// So write code to minimize size - no pregenerated tables
// and array tools dependencies.

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

// Use ordinary array, since untyped makes no boost here
const makeTable = () => {
  let c, table = [];

  for (var n = 0; n < 256; n++) {
    c = n;
    for (var k = 0; k < 8; k++) {
      c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
    }
    table[n] = c;
  }

  return table;
};

// Create table on load. Just 255 signed longs. Not a problem.
const crcTable$1 = new Uint32Array(makeTable());


const crc32 = (crc, buf, len, pos) => {
  const t = crcTable$1;
  const end = pos + len;

  crc ^= -1;

  for (let i = pos; i < end; i++) {
    crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF];
  }

  return (crc ^ (-1)); // >>> 0;
};


var crc32_1 = crc32;

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

var messages = {
  2:      'need dictionary',     /* Z_NEED_DICT       2  */
  1:      'stream end',          /* Z_STREAM_END      1  */
  0:      '',                    /* Z_OK              0  */
  '-1':   'file error',          /* Z_ERRNO         (-1) */
  '-2':   'stream error',        /* Z_STREAM_ERROR  (-2) */
  '-3':   'data error',          /* Z_DATA_ERROR    (-3) */
  '-4':   'insufficient memory', /* Z_MEM_ERROR     (-4) */
  '-5':   'buffer error',        /* Z_BUF_ERROR     (-5) */
  '-6':   'incompatible version' /* Z_VERSION_ERROR (-6) */
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

var constants$2 = {

  /* Allowed flush values; see deflate() and inflate() below for details */
  Z_NO_FLUSH:         0,
  Z_FINISH:           4,
  Z_BLOCK:            5,
  Z_TREES:            6,

  /* Return codes for the compression/decompression functions. Negative values
  * are errors, positive values are used for special but normal events.
  */
  Z_OK:               0,
  Z_STREAM_END:       1,
  Z_NEED_DICT:        2,
  Z_STREAM_ERROR:    -2,
  Z_DATA_ERROR:      -3,
  Z_MEM_ERROR:       -4,
  Z_BUF_ERROR:       -5,
  /* The deflate compression method */
  Z_DEFLATED:               8
  //Z_NULL:                 null // Use -1 or null inline, depending on var type
};

const _has = (obj, key) => {
  return Object.prototype.hasOwnProperty.call(obj, key);
};

var assign = function (obj /*from1, from2, from3, ...*/) {
  const sources = Array.prototype.slice.call(arguments, 1);
  while (sources.length) {
    const source = sources.shift();
    if (!source) { continue; }

    if (typeof source !== 'object') {
      throw new TypeError(source + 'must be non-object');
    }

    for (const p in source) {
      if (_has(source, p)) {
        obj[p] = source[p];
      }
    }
  }

  return obj;
};


// Join array of chunks to single array.
var flattenChunks = (chunks) => {
  // calculate data length
  let len = 0;

  for (let i = 0, l = chunks.length; i < l; i++) {
    len += chunks[i].length;
  }

  // join chunks
  const result = new Uint8Array(len);

  for (let i = 0, pos = 0, l = chunks.length; i < l; i++) {
    let chunk = chunks[i];
    result.set(chunk, pos);
    pos += chunk.length;
  }

  return result;
};

var common = {
	assign: assign,
	flattenChunks: flattenChunks
};

// String encode/decode helpers


// Quick check if we can use fast array to bin string conversion
//
// - apply(Array) can fail on Android 2.2
// - apply(Uint8Array) can fail on iOS 5.1 Safari
//
let STR_APPLY_UIA_OK = true;

try { String.fromCharCode.apply(null, new Uint8Array(1)); } catch (__) { STR_APPLY_UIA_OK = false; }


// Table with utf8 lengths (calculated by first byte of sequence)
// Note, that 5 & 6-byte values and some 4-byte values can not be represented in JS,
// because max possible codepoint is 0x10ffff
const _utf8len = new Uint8Array(256);
for (let q = 0; q < 256; q++) {
  _utf8len[q] = (q >= 252 ? 6 : q >= 248 ? 5 : q >= 240 ? 4 : q >= 224 ? 3 : q >= 192 ? 2 : 1);
}
_utf8len[254] = _utf8len[254] = 1; // Invalid sequence start


// convert string to array (typed, when possible)
var string2buf = (str) => {
  if (typeof TextEncoder === 'function' && TextEncoder.prototype.encode) {
    return new TextEncoder().encode(str);
  }

  let buf, c, c2, m_pos, i, str_len = str.length, buf_len = 0;

  // count binary size
  for (m_pos = 0; m_pos < str_len; m_pos++) {
    c = str.charCodeAt(m_pos);
    if ((c & 0xfc00) === 0xd800 && (m_pos + 1 < str_len)) {
      c2 = str.charCodeAt(m_pos + 1);
      if ((c2 & 0xfc00) === 0xdc00) {
        c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
        m_pos++;
      }
    }
    buf_len += c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : 4;
  }

  // allocate buffer
  buf = new Uint8Array(buf_len);

  // convert
  for (i = 0, m_pos = 0; i < buf_len; m_pos++) {
    c = str.charCodeAt(m_pos);
    if ((c & 0xfc00) === 0xd800 && (m_pos + 1 < str_len)) {
      c2 = str.charCodeAt(m_pos + 1);
      if ((c2 & 0xfc00) === 0xdc00) {
        c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
        m_pos++;
      }
    }
    if (c < 0x80) {
      /* one byte */
      buf[i++] = c;
    } else if (c < 0x800) {
      /* two bytes */
      buf[i++] = 0xC0 | (c >>> 6);
      buf[i++] = 0x80 | (c & 0x3f);
    } else if (c < 0x10000) {
      /* three bytes */
      buf[i++] = 0xE0 | (c >>> 12);
      buf[i++] = 0x80 | (c >>> 6 & 0x3f);
      buf[i++] = 0x80 | (c & 0x3f);
    } else {
      /* four bytes */
      buf[i++] = 0xf0 | (c >>> 18);
      buf[i++] = 0x80 | (c >>> 12 & 0x3f);
      buf[i++] = 0x80 | (c >>> 6 & 0x3f);
      buf[i++] = 0x80 | (c & 0x3f);
    }
  }

  return buf;
};

// Helper
const buf2binstring = (buf, len) => {
  // On Chrome, the arguments in a function call that are allowed is `65534`.
  // If the length of the buffer is smaller than that, we can use this optimization,
  // otherwise we will take a slower path.
  if (len < 65534) {
    if (buf.subarray && STR_APPLY_UIA_OK) {
      return String.fromCharCode.apply(null, buf.length === len ? buf : buf.subarray(0, len));
    }
  }

  let result = '';
  for (let i = 0; i < len; i++) {
    result += String.fromCharCode(buf[i]);
  }
  return result;
};


// convert array to string
var buf2string = (buf, max) => {
  const len = max || buf.length;

  if (typeof TextDecoder === 'function' && TextDecoder.prototype.decode) {
    return new TextDecoder().decode(buf.subarray(0, max));
  }

  let i, out;

  // Reserve max possible length (2 words per char)
  // NB: by unknown reasons, Array is significantly faster for
  //     String.fromCharCode.apply than Uint16Array.
  const utf16buf = new Array(len * 2);

  for (out = 0, i = 0; i < len;) {
    let c = buf[i++];
    // quick process ascii
    if (c < 0x80) { utf16buf[out++] = c; continue; }

    let c_len = _utf8len[c];
    // skip 5 & 6 byte codes
    if (c_len > 4) { utf16buf[out++] = 0xfffd; i += c_len - 1; continue; }

    // apply mask on first byte
    c &= c_len === 2 ? 0x1f : c_len === 3 ? 0x0f : 0x07;
    // join the rest
    while (c_len > 1 && i < len) {
      c = (c << 6) | (buf[i++] & 0x3f);
      c_len--;
    }

    // terminated by end of string?
    if (c_len > 1) { utf16buf[out++] = 0xfffd; continue; }

    if (c < 0x10000) {
      utf16buf[out++] = c;
    } else {
      c -= 0x10000;
      utf16buf[out++] = 0xd800 | ((c >> 10) & 0x3ff);
      utf16buf[out++] = 0xdc00 | (c & 0x3ff);
    }
  }

  return buf2binstring(utf16buf, out);
};


// Calculate max possible position in utf8 buffer,
// that will not break sequence. If that's not possible
// - (very small limits) return max size as is.
//
// buf[] - utf8 bytes array
// max   - length limit (mandatory);
var utf8border = (buf, max) => {

  max = max || buf.length;
  if (max > buf.length) { max = buf.length; }

  // go back from last position, until start of sequence found
  let pos = max - 1;
  while (pos >= 0 && (buf[pos] & 0xC0) === 0x80) { pos--; }

  // Very small and broken sequence,
  // return max, because we should return something anyway.
  if (pos < 0) { return max; }

  // If we came to start of buffer - that means buffer is too small,
  // return max too.
  if (pos === 0) { return max; }

  return (pos + _utf8len[buf[pos]] > max) ? pos : max;
};

var strings = {
	string2buf: string2buf,
	buf2string: buf2string,
	utf8border: utf8border
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

function ZStream() {
  /* next input byte */
  this.input = null; // JS specific, because we have no pointers
  this.next_in = 0;
  /* number of bytes available at input */
  this.avail_in = 0;
  /* total number of input bytes read so far */
  this.total_in = 0;
  /* next output byte should be put there */
  this.output = null; // JS specific, because we have no pointers
  this.next_out = 0;
  /* remaining free space at output */
  this.avail_out = 0;
  /* total number of bytes output so far */
  this.total_out = 0;
  /* last error message, NULL if no error */
  this.msg = ''/*Z_NULL*/;
  /* not visible by applications */
  this.state = null;
  /* best guess about the data type: binary or text */
  this.data_type = 2/*Z_UNKNOWN*/;
  /* adler32 value of the uncompressed data */
  this.adler = 0;
}

var zstream = ZStream;

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

// See state defs from inflate.js
const BAD$1 = 16209;       /* got a data error -- remain here until reset */
const TYPE$1 = 16191;      /* i: waiting for type bits, including last-flag bit */

/*
   Decode literal, length, and distance codes and write out the resulting
   literal and match bytes until either not enough input or output is
   available, an end-of-block is encountered, or a data error is encountered.
   When large enough input and output buffers are supplied to inflate(), for
   example, a 16K input buffer and a 64K output buffer, more than 95% of the
   inflate execution time is spent in this routine.

   Entry assumptions:

        state.mode === LEN
        strm.avail_in >= 6
        strm.avail_out >= 258
        start >= strm.avail_out
        state.bits < 8

   On return, state.mode is one of:

        LEN -- ran out of enough output space or enough available input
        TYPE -- reached end of block code, inflate() to interpret next block
        BAD -- error in block data

   Notes:

    - The maximum input bits used by a length/distance pair is 15 bits for the
      length code, 5 bits for the length extra, 15 bits for the distance code,
      and 13 bits for the distance extra.  This totals 48 bits, or six bytes.
      Therefore if strm.avail_in >= 6, then there is enough input to avoid
      checking for available input while decoding.

    - The maximum bytes that a single length/distance pair can output is 258
      bytes, which is the maximum length that can be coded.  inflate_fast()
      requires strm.avail_out >= 258 for each loop to avoid checking for
      output space.
 */
var inffast = function inflate_fast(strm, start) {
  let _in;                    /* local strm.input */
  let last;                   /* have enough input while in < last */
  let _out;                   /* local strm.output */
  let beg;                    /* inflate()'s initial strm.output */
  let end;                    /* while out < end, enough space available */
//#ifdef INFLATE_STRICT
  let dmax;                   /* maximum distance from zlib header */
//#endif
  let wsize;                  /* window size or zero if not using window */
  let whave;                  /* valid bytes in the window */
  let wnext;                  /* window write index */
  // Use `s_window` instead `window`, avoid conflict with instrumentation tools
  let s_window;               /* allocated sliding window, if wsize != 0 */
  let hold;                   /* local strm.hold */
  let bits;                   /* local strm.bits */
  let lcode;                  /* local strm.lencode */
  let dcode;                  /* local strm.distcode */
  let lmask;                  /* mask for first level of length codes */
  let dmask;                  /* mask for first level of distance codes */
  let here;                   /* retrieved table entry */
  let op;                     /* code bits, operation, extra bits, or */
                              /*  window position, window bytes to copy */
  let len;                    /* match length, unused bytes */
  let dist;                   /* match distance */
  let from;                   /* where to copy match from */
  let from_source;


  let input, output; // JS specific, because we have no pointers

  /* copy state to local variables */
  const state = strm.state;
  //here = state.here;
  _in = strm.next_in;
  input = strm.input;
  last = _in + (strm.avail_in - 5);
  _out = strm.next_out;
  output = strm.output;
  beg = _out - (start - strm.avail_out);
  end = _out + (strm.avail_out - 257);
//#ifdef INFLATE_STRICT
  dmax = state.dmax;
//#endif
  wsize = state.wsize;
  whave = state.whave;
  wnext = state.wnext;
  s_window = state.window;
  hold = state.hold;
  bits = state.bits;
  lcode = state.lencode;
  dcode = state.distcode;
  lmask = (1 << state.lenbits) - 1;
  dmask = (1 << state.distbits) - 1;


  /* decode literals and length/distances until end-of-block or not enough
     input data or output space */

  top:
  do {
    if (bits < 15) {
      hold += input[_in++] << bits;
      bits += 8;
      hold += input[_in++] << bits;
      bits += 8;
    }

    here = lcode[hold & lmask];

    dolen:
    for (;;) { // Goto emulation
      op = here >>> 24/*here.bits*/;
      hold >>>= op;
      bits -= op;
      op = (here >>> 16) & 0xff/*here.op*/;
      if (op === 0) {                          /* literal */
        //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
        //        "inflate:         literal '%c'\n" :
        //        "inflate:         literal 0x%02x\n", here.val));
        output[_out++] = here & 0xffff/*here.val*/;
      }
      else if (op & 16) {                     /* length base */
        len = here & 0xffff/*here.val*/;
        op &= 15;                           /* number of extra bits */
        if (op) {
          if (bits < op) {
            hold += input[_in++] << bits;
            bits += 8;
          }
          len += hold & ((1 << op) - 1);
          hold >>>= op;
          bits -= op;
        }
        //Tracevv((stderr, "inflate:         length %u\n", len));
        if (bits < 15) {
          hold += input[_in++] << bits;
          bits += 8;
          hold += input[_in++] << bits;
          bits += 8;
        }
        here = dcode[hold & dmask];

        dodist:
        for (;;) { // goto emulation
          op = here >>> 24/*here.bits*/;
          hold >>>= op;
          bits -= op;
          op = (here >>> 16) & 0xff/*here.op*/;

          if (op & 16) {                      /* distance base */
            dist = here & 0xffff/*here.val*/;
            op &= 15;                       /* number of extra bits */
            if (bits < op) {
              hold += input[_in++] << bits;
              bits += 8;
              if (bits < op) {
                hold += input[_in++] << bits;
                bits += 8;
              }
            }
            dist += hold & ((1 << op) - 1);
//#ifdef INFLATE_STRICT
            if (dist > dmax) {
              strm.msg = 'invalid distance too far back';
              state.mode = BAD$1;
              break top;
            }
//#endif
            hold >>>= op;
            bits -= op;
            //Tracevv((stderr, "inflate:         distance %u\n", dist));
            op = _out - beg;                /* max distance in output */
            if (dist > op) {                /* see if copy from window */
              op = dist - op;               /* distance back in window */
              if (op > whave) {
                if (state.sane) {
                  strm.msg = 'invalid distance too far back';
                  state.mode = BAD$1;
                  break top;
                }

// (!) This block is disabled in zlib defaults,
// don't enable it for binary compatibility
//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
//                if (len <= op - whave) {
//                  do {
//                    output[_out++] = 0;
//                  } while (--len);
//                  continue top;
//                }
//                len -= op - whave;
//                do {
//                  output[_out++] = 0;
//                } while (--op > whave);
//                if (op === 0) {
//                  from = _out - dist;
//                  do {
//                    output[_out++] = output[from++];
//                  } while (--len);
//                  continue top;
//                }
//#endif
              }
              from = 0; // window index
              from_source = s_window;
              if (wnext === 0) {           /* very common case */
                from += wsize - op;
                if (op < len) {         /* some from window */
                  len -= op;
                  do {
                    output[_out++] = s_window[from++];
                  } while (--op);
                  from = _out - dist;  /* rest from output */
                  from_source = output;
                }
              }
              else if (wnext < op) {      /* wrap around window */
                from += wsize + wnext - op;
                op -= wnext;
                if (op < len) {         /* some from end of window */
                  len -= op;
                  do {
                    output[_out++] = s_window[from++];
                  } while (--op);
                  from = 0;
                  if (wnext < len) {  /* some from start of window */
                    op = wnext;
                    len -= op;
                    do {
                      output[_out++] = s_window[from++];
                    } while (--op);
                    from = _out - dist;      /* rest from output */
                    from_source = output;
                  }
                }
              }
              else {                      /* contiguous in window */
                from += wnext - op;
                if (op < len) {         /* some from window */
                  len -= op;
                  do {
                    output[_out++] = s_window[from++];
                  } while (--op);
                  from = _out - dist;  /* rest from output */
                  from_source = output;
                }
              }
              while (len > 2) {
                output[_out++] = from_source[from++];
                output[_out++] = from_source[from++];
                output[_out++] = from_source[from++];
                len -= 3;
              }
              if (len) {
                output[_out++] = from_source[from++];
                if (len > 1) {
                  output[_out++] = from_source[from++];
                }
              }
            }
            else {
              from = _out - dist;          /* copy direct from output */
              do {                        /* minimum length is three */
                output[_out++] = output[from++];
                output[_out++] = output[from++];
                output[_out++] = output[from++];
                len -= 3;
              } while (len > 2);
              if (len) {
                output[_out++] = output[from++];
                if (len > 1) {
                  output[_out++] = output[from++];
                }
              }
            }
          }
          else if ((op & 64) === 0) {          /* 2nd level distance code */
            here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
            continue dodist;
          }
          else {
            strm.msg = 'invalid distance code';
            state.mode = BAD$1;
            break top;
          }

          break; // need to emulate goto via "continue"
        }
      }
      else if ((op & 64) === 0) {              /* 2nd level length code */
        here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
        continue dolen;
      }
      else if (op & 32) {                     /* end-of-block */
        //Tracevv((stderr, "inflate:         end of block\n"));
        state.mode = TYPE$1;
        break top;
      }
      else {
        strm.msg = 'invalid literal/length code';
        state.mode = BAD$1;
        break top;
      }

      break; // need to emulate goto via "continue"
    }
  } while (_in < last && _out < end);

  /* return unused bytes (on entry, bits < 8, so in won't go too far back) */
  len = bits >> 3;
  _in -= len;
  bits -= len << 3;
  hold &= (1 << bits) - 1;

  /* update state and return */
  strm.next_in = _in;
  strm.next_out = _out;
  strm.avail_in = (_in < last ? 5 + (last - _in) : 5 - (_in - last));
  strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end));
  state.hold = hold;
  state.bits = bits;
  return;
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

const MAXBITS = 15;
const ENOUGH_LENS$1 = 852;
const ENOUGH_DISTS$1 = 592;
//const ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS);

const CODES$1 = 0;
const LENS$1 = 1;
const DISTS$1 = 2;

const lbase = new Uint16Array([ /* Length codes 257..285 base */
  3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
  35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0
]);

const lext = new Uint8Array([ /* Length codes 257..285 extra */
  16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18,
  19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 72, 78
]);

const dbase = new Uint16Array([ /* Distance codes 0..29 base */
  1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
  257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
  8193, 12289, 16385, 24577, 0, 0
]);

const dext = new Uint8Array([ /* Distance codes 0..29 extra */
  16, 16, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22,
  23, 23, 24, 24, 25, 25, 26, 26, 27, 27,
  28, 28, 29, 29, 64, 64
]);

const inflate_table = (type, lens, lens_index, codes, table, table_index, work, opts) =>
{
  const bits = opts.bits;
      //here = opts.here; /* table entry for duplication */

  let len = 0;               /* a code's length in bits */
  let sym = 0;               /* index of code symbols */
  let min = 0, max = 0;          /* minimum and maximum code lengths */
  let root = 0;              /* number of index bits for root table */
  let curr = 0;              /* number of index bits for current table */
  let drop = 0;              /* code bits to drop for sub-table */
  let left = 0;                   /* number of prefix codes available */
  let used = 0;              /* code entries in table used */
  let huff = 0;              /* Huffman code */
  let incr;              /* for incrementing code, index */
  let fill;              /* index for replicating entries */
  let low;               /* low bits for current root entry */
  let mask;              /* mask for low root bits */
  let next;             /* next available space in table */
  let base = null;     /* base value table to use */
//  let shoextra;    /* extra bits table to use */
  let match;                  /* use base and extra for symbol >= match */
  const count = new Uint16Array(MAXBITS + 1); //[MAXBITS+1];    /* number of codes of each length */
  const offs = new Uint16Array(MAXBITS + 1); //[MAXBITS+1];     /* offsets in table for each length */
  let extra = null;

  let here_bits, here_op, here_val;

  /*
   Process a set of code lengths to create a canonical Huffman code.  The
   code lengths are lens[0..codes-1].  Each length corresponds to the
   symbols 0..codes-1.  The Huffman code is generated by first sorting the
   symbols by length from short to long, and retaining the symbol order
   for codes with equal lengths.  Then the code starts with all zero bits
   for the first code of the shortest length, and the codes are integer
   increments for the same length, and zeros are appended as the length
   increases.  For the deflate format, these bits are stored backwards
   from their more natural integer increment ordering, and so when the
   decoding tables are built in the large loop below, the integer codes
   are incremented backwards.

   This routine assumes, but does not check, that all of the entries in
   lens[] are in the range 0..MAXBITS.  The caller must assure this.
   1..MAXBITS is interpreted as that code length.  zero means that that
   symbol does not occur in this code.

   The codes are sorted by computing a count of codes for each length,
   creating from that a table of starting indices for each length in the
   sorted table, and then entering the symbols in order in the sorted
   table.  The sorted table is work[], with that space being provided by
   the caller.

   The length counts are used for other purposes as well, i.e. finding
   the minimum and maximum length codes, determining if there are any
   codes at all, checking for a valid set of lengths, and looking ahead
   at length counts to determine sub-table sizes when building the
   decoding tables.
   */

  /* accumulate lengths for codes (assumes lens[] all in 0..MAXBITS) */
  for (len = 0; len <= MAXBITS; len++) {
    count[len] = 0;
  }
  for (sym = 0; sym < codes; sym++) {
    count[lens[lens_index + sym]]++;
  }

  /* bound code lengths, force root to be within code lengths */
  root = bits;
  for (max = MAXBITS; max >= 1; max--) {
    if (count[max] !== 0) { break; }
  }
  if (root > max) {
    root = max;
  }
  if (max === 0) {                     /* no symbols to code at all */
    //table.op[opts.table_index] = 64;  //here.op = (var char)64;    /* invalid code marker */
    //table.bits[opts.table_index] = 1;   //here.bits = (var char)1;
    //table.val[opts.table_index++] = 0;   //here.val = (var short)0;
    table[table_index++] = (1 << 24) | (64 << 16) | 0;


    //table.op[opts.table_index] = 64;
    //table.bits[opts.table_index] = 1;
    //table.val[opts.table_index++] = 0;
    table[table_index++] = (1 << 24) | (64 << 16) | 0;

    opts.bits = 1;
    return 0;     /* no symbols, but wait for decoding to report error */
  }
  for (min = 1; min < max; min++) {
    if (count[min] !== 0) { break; }
  }
  if (root < min) {
    root = min;
  }

  /* check for an over-subscribed or incomplete set of lengths */
  left = 1;
  for (len = 1; len <= MAXBITS; len++) {
    left <<= 1;
    left -= count[len];
    if (left < 0) {
      return -1;
    }        /* over-subscribed */
  }
  if (left > 0 && (type === CODES$1 || max !== 1)) {
    return -1;                      /* incomplete set */
  }

  /* generate offsets into symbol table for each length for sorting */
  offs[1] = 0;
  for (len = 1; len < MAXBITS; len++) {
    offs[len + 1] = offs[len] + count[len];
  }

  /* sort symbols by length, by symbol order within each length */
  for (sym = 0; sym < codes; sym++) {
    if (lens[lens_index + sym] !== 0) {
      work[offs[lens[lens_index + sym]]++] = sym;
    }
  }

  /*
   Create and fill in decoding tables.  In this loop, the table being
   filled is at next and has curr index bits.  The code being used is huff
   with length len.  That code is converted to an index by dropping drop
   bits off of the bottom.  For codes where len is less than drop + curr,
   those top drop + curr - len bits are incremented through all values to
   fill the table with replicated entries.

   root is the number of index bits for the root table.  When len exceeds
   root, sub-tables are created pointed to by the root entry with an index
   of the low root bits of huff.  This is saved in low to check for when a
   new sub-table should be started.  drop is zero when the root table is
   being filled, and drop is root when sub-tables are being filled.

   When a new sub-table is needed, it is necessary to look ahead in the
   code lengths to determine what size sub-table is needed.  The length
   counts are used for this, and so count[] is decremented as codes are
   entered in the tables.

   used keeps track of how many table entries have been allocated from the
   provided *table space.  It is checked for LENS and DIST tables against
   the constants ENOUGH_LENS and ENOUGH_DISTS to guard against changes in
   the initial root table size constants.  See the comments in inftrees.h
   for more information.

   sym increments through all symbols, and the loop terminates when
   all codes of length max, i.e. all codes, have been processed.  This
   routine permits incomplete codes, so another loop after this one fills
   in the rest of the decoding tables with invalid code markers.
   */

  /* set up for code type */
  // poor man optimization - use if-else instead of switch,
  // to avoid deopts in old v8
  if (type === CODES$1) {
    base = extra = work;    /* dummy value--not used */
    match = 20;

  } else if (type === LENS$1) {
    base = lbase;
    extra = lext;
    match = 257;

  } else {                    /* DISTS */
    base = dbase;
    extra = dext;
    match = 0;
  }

  /* initialize opts for loop */
  huff = 0;                   /* starting code */
  sym = 0;                    /* starting code symbol */
  len = min;                  /* starting code length */
  next = table_index;              /* current table to fill in */
  curr = root;                /* current table index bits */
  drop = 0;                   /* current bits to drop from code for index */
  low = -1;                   /* trigger new sub-table when len > root */
  used = 1 << root;          /* use root table entries */
  mask = used - 1;            /* mask for comparing low */

  /* check available table space */
  if ((type === LENS$1 && used > ENOUGH_LENS$1) ||
    (type === DISTS$1 && used > ENOUGH_DISTS$1)) {
    return 1;
  }

  /* process all codes and make table entries */
  for (;;) {
    /* create table entry */
    here_bits = len - drop;
    if (work[sym] + 1 < match) {
      here_op = 0;
      here_val = work[sym];
    }
    else if (work[sym] >= match) {
      here_op = extra[work[sym] - match];
      here_val = base[work[sym] - match];
    }
    else {
      here_op = 32 + 64;         /* end of block */
      here_val = 0;
    }

    /* replicate for those indices with low len bits equal to huff */
    incr = 1 << (len - drop);
    fill = 1 << curr;
    min = fill;                 /* save offset to next table */
    do {
      fill -= incr;
      table[next + (huff >> drop) + fill] = (here_bits << 24) | (here_op << 16) | here_val |0;
    } while (fill !== 0);

    /* backwards increment the len-bit code huff */
    incr = 1 << (len - 1);
    while (huff & incr) {
      incr >>= 1;
    }
    if (incr !== 0) {
      huff &= incr - 1;
      huff += incr;
    } else {
      huff = 0;
    }

    /* go to next symbol, update count, len */
    sym++;
    if (--count[len] === 0) {
      if (len === max) { break; }
      len = lens[lens_index + work[sym]];
    }

    /* create new sub-table if needed */
    if (len > root && (huff & mask) !== low) {
      /* if first time, transition to sub-tables */
      if (drop === 0) {
        drop = root;
      }

      /* increment past last table */
      next += min;            /* here min is 1 << curr */

      /* determine length of next table */
      curr = len - drop;
      left = 1 << curr;
      while (curr + drop < max) {
        left -= count[curr + drop];
        if (left <= 0) { break; }
        curr++;
        left <<= 1;
      }

      /* check for enough space */
      used += 1 << curr;
      if ((type === LENS$1 && used > ENOUGH_LENS$1) ||
        (type === DISTS$1 && used > ENOUGH_DISTS$1)) {
        return 1;
      }

      /* point entry in root table to sub-table */
      low = huff & mask;
      /*table.op[low] = curr;
      table.bits[low] = root;
      table.val[low] = next - opts.table_index;*/
      table[low] = (root << 24) | (curr << 16) | (next - table_index) |0;
    }
  }

  /* fill in remaining table entry if code is incomplete (guaranteed to have
   at most one remaining entry, since if the code is incomplete, the
   maximum code length that was allowed to get this far is one bit) */
  if (huff !== 0) {
    //table.op[next + huff] = 64;            /* invalid code marker */
    //table.bits[next + huff] = len - drop;
    //table.val[next + huff] = 0;
    table[next + huff] = ((len - drop) << 24) | (64 << 16) |0;
  }

  /* set return parameters */
  //opts.table_index += used;
  opts.bits = root;
  return 0;
};


var inftrees = inflate_table;

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.






const CODES = 0;
const LENS = 1;
const DISTS = 2;

/* Public constants ==========================================================*/
/* ===========================================================================*/

const {
  Z_FINISH: Z_FINISH$1, Z_BLOCK, Z_TREES,
  Z_OK: Z_OK$1, Z_STREAM_END: Z_STREAM_END$1, Z_NEED_DICT: Z_NEED_DICT$1, Z_STREAM_ERROR: Z_STREAM_ERROR$1, Z_DATA_ERROR: Z_DATA_ERROR$1, Z_MEM_ERROR: Z_MEM_ERROR$1, Z_BUF_ERROR,
  Z_DEFLATED
} = constants$2;


/* STATES ====================================================================*/
/* ===========================================================================*/


const    HEAD = 16180;       /* i: waiting for magic header */
const    FLAGS = 16181;      /* i: waiting for method and flags (gzip) */
const    TIME = 16182;       /* i: waiting for modification time (gzip) */
const    OS = 16183;         /* i: waiting for extra flags and operating system (gzip) */
const    EXLEN = 16184;      /* i: waiting for extra length (gzip) */
const    EXTRA = 16185;      /* i: waiting for extra bytes (gzip) */
const    NAME = 16186;       /* i: waiting for end of file name (gzip) */
const    COMMENT = 16187;    /* i: waiting for end of comment (gzip) */
const    HCRC = 16188;       /* i: waiting for header crc (gzip) */
const    DICTID = 16189;    /* i: waiting for dictionary check value */
const    DICT = 16190;      /* waiting for inflateSetDictionary() call */
const        TYPE = 16191;      /* i: waiting for type bits, including last-flag bit */
const        TYPEDO = 16192;    /* i: same, but skip check to exit inflate on new block */
const        STORED = 16193;    /* i: waiting for stored size (length and complement) */
const        COPY_ = 16194;     /* i/o: same as COPY below, but only first time in */
const        COPY = 16195;      /* i/o: waiting for input or output to copy stored block */
const        TABLE = 16196;     /* i: waiting for dynamic block table lengths */
const        LENLENS = 16197;   /* i: waiting for code length code lengths */
const        CODELENS = 16198;  /* i: waiting for length/lit and distance code lengths */
const            LEN_ = 16199;      /* i: same as LEN below, but only first time in */
const            LEN = 16200;       /* i: waiting for length/lit/eob code */
const            LENEXT = 16201;    /* i: waiting for length extra bits */
const            DIST = 16202;      /* i: waiting for distance code */
const            DISTEXT = 16203;   /* i: waiting for distance extra bits */
const            MATCH = 16204;     /* o: waiting for output space to copy string */
const            LIT = 16205;       /* o: waiting for output space to write literal */
const    CHECK = 16206;     /* i: waiting for 32-bit check value */
const    LENGTH = 16207;    /* i: waiting for 32-bit length (gzip) */
const    DONE = 16208;      /* finished check, done -- remain here until reset */
const    BAD = 16209;       /* got a data error -- remain here until reset */
const    MEM = 16210;       /* got an inflate() memory error -- remain here until reset */
const    SYNC = 16211;      /* looking for synchronization bytes to restart inflate() */

/* ===========================================================================*/



const ENOUGH_LENS = 852;
const ENOUGH_DISTS = 592;
//const ENOUGH =  (ENOUGH_LENS+ENOUGH_DISTS);

const MAX_WBITS = 15;
/* 32K LZ77 window */
const DEF_WBITS = MAX_WBITS;


const zswap32 = (q) => {

  return  (((q >>> 24) & 0xff) +
          ((q >>> 8) & 0xff00) +
          ((q & 0xff00) << 8) +
          ((q & 0xff) << 24));
};


function InflateState() {
  this.strm = null;           /* pointer back to this zlib stream */
  this.mode = 0;              /* current inflate mode */
  this.last = false;          /* true if processing last block */
  this.wrap = 0;              /* bit 0 true for zlib, bit 1 true for gzip,
                                 bit 2 true to validate check value */
  this.havedict = false;      /* true if dictionary provided */
  this.flags = 0;             /* gzip header method and flags (0 if zlib), or
                                 -1 if raw or no header yet */
  this.dmax = 0;              /* zlib header max distance (INFLATE_STRICT) */
  this.check = 0;             /* protected copy of check value */
  this.total = 0;             /* protected copy of output count */
  // TODO: may be {}
  this.head = null;           /* where to save gzip header information */

  /* sliding window */
  this.wbits = 0;             /* log base 2 of requested window size */
  this.wsize = 0;             /* window size or zero if not using window */
  this.whave = 0;             /* valid bytes in the window */
  this.wnext = 0;             /* window write index */
  this.window = null;         /* allocated sliding window, if needed */

  /* bit accumulator */
  this.hold = 0;              /* input bit accumulator */
  this.bits = 0;              /* number of bits in "in" */

  /* for string and stored block copying */
  this.length = 0;            /* literal or length of data to copy */
  this.offset = 0;            /* distance back to copy string from */

  /* for table and code decoding */
  this.extra = 0;             /* extra bits needed */

  /* fixed and dynamic code tables */
  this.lencode = null;          /* starting table for length/literal codes */
  this.distcode = null;         /* starting table for distance codes */
  this.lenbits = 0;           /* index bits for lencode */
  this.distbits = 0;          /* index bits for distcode */

  /* dynamic table building */
  this.ncode = 0;             /* number of code length code lengths */
  this.nlen = 0;              /* number of length code lengths */
  this.ndist = 0;             /* number of distance code lengths */
  this.have = 0;              /* number of code lengths in lens[] */
  this.next = null;              /* next available space in codes[] */

  this.lens = new Uint16Array(320); /* temporary storage for code lengths */
  this.work = new Uint16Array(288); /* work area for code table building */

  /*
   because we don't have pointers in js, we use lencode and distcode directly
   as buffers so we don't need codes
  */
  //this.codes = new Int32Array(ENOUGH);       /* space for code tables */
  this.lendyn = null;              /* dynamic table for length/literal codes (JS specific) */
  this.distdyn = null;             /* dynamic table for distance codes (JS specific) */
  this.sane = 0;                   /* if false, allow invalid distance too far */
  this.back = 0;                   /* bits back of last unprocessed length/lit */
  this.was = 0;                    /* initial length of match */
}


const inflateStateCheck = (strm) => {

  if (!strm) {
    return 1;
  }
  const state = strm.state;
  if (!state || state.strm !== strm ||
    state.mode < HEAD || state.mode > SYNC) {
    return 1;
  }
  return 0;
};


const inflateResetKeep = (strm) => {

  if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
  const state = strm.state;
  strm.total_in = strm.total_out = state.total = 0;
  strm.msg = ''; /*Z_NULL*/
  if (state.wrap) {       /* to support ill-conceived Java test suite */
    strm.adler = state.wrap & 1;
  }
  state.mode = HEAD;
  state.last = 0;
  state.havedict = 0;
  state.flags = -1;
  state.dmax = 32768;
  state.head = null/*Z_NULL*/;
  state.hold = 0;
  state.bits = 0;
  //state.lencode = state.distcode = state.next = state.codes;
  state.lencode = state.lendyn = new Int32Array(ENOUGH_LENS);
  state.distcode = state.distdyn = new Int32Array(ENOUGH_DISTS);

  state.sane = 1;
  state.back = -1;
  //Tracev((stderr, "inflate: reset\n"));
  return Z_OK$1;
};


const inflateReset = (strm) => {

  if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
  const state = strm.state;
  state.wsize = 0;
  state.whave = 0;
  state.wnext = 0;
  return inflateResetKeep(strm);

};


const inflateReset2 = (strm, windowBits) => {
  let wrap;

  /* get the state */
  if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
  const state = strm.state;

  /* extract wrap request from windowBits parameter */
  if (windowBits < 0) {
    wrap = 0;
    windowBits = -windowBits;
  }
  else {
    wrap = (windowBits >> 4) + 5;
    if (windowBits < 48) {
      windowBits &= 15;
    }
  }

  /* set number of window bits, free window if different */
  if (windowBits && (windowBits < 8 || windowBits > 15)) {
    return Z_STREAM_ERROR$1;
  }
  if (state.window !== null && state.wbits !== windowBits) {
    state.window = null;
  }

  /* update state and reset the rest of it */
  state.wrap = wrap;
  state.wbits = windowBits;
  return inflateReset(strm);
};


const inflateInit2 = (strm, windowBits) => {

  if (!strm) { return Z_STREAM_ERROR$1; }
  //strm.msg = Z_NULL;                 /* in case we return an error */

  const state = new InflateState();

  //if (state === Z_NULL) return Z_MEM_ERROR;
  //Tracev((stderr, "inflate: allocated\n"));
  strm.state = state;
  state.strm = strm;
  state.window = null/*Z_NULL*/;
  state.mode = HEAD;     /* to pass state test in inflateReset2() */
  const ret = inflateReset2(strm, windowBits);
  if (ret !== Z_OK$1) {
    strm.state = null/*Z_NULL*/;
  }
  return ret;
};


const inflateInit = (strm) => {

  return inflateInit2(strm, DEF_WBITS);
};


/*
 Return state with length and distance decoding tables and index sizes set to
 fixed code decoding.  Normally this returns fixed tables from inffixed.h.
 If BUILDFIXED is defined, then instead this routine builds the tables the
 first time it's called, and returns those tables the first time and
 thereafter.  This reduces the size of the code by about 2K bytes, in
 exchange for a little execution time.  However, BUILDFIXED should not be
 used for threaded applications, since the rewriting of the tables and virgin
 may not be thread-safe.
 */
let virgin = true;

let lenfix, distfix; // We have no pointers in JS, so keep tables separate


const fixedtables = (state) => {

  /* build fixed huffman tables if first call (may not be thread safe) */
  if (virgin) {
    lenfix = new Int32Array(512);
    distfix = new Int32Array(32);

    /* literal/length table */
    let sym = 0;
    while (sym < 144) { state.lens[sym++] = 8; }
    while (sym < 256) { state.lens[sym++] = 9; }
    while (sym < 280) { state.lens[sym++] = 7; }
    while (sym < 288) { state.lens[sym++] = 8; }

    inftrees(LENS,  state.lens, 0, 288, lenfix,   0, state.work, { bits: 9 });

    /* distance table */
    sym = 0;
    while (sym < 32) { state.lens[sym++] = 5; }

    inftrees(DISTS, state.lens, 0, 32,   distfix, 0, state.work, { bits: 5 });

    /* do this just once */
    virgin = false;
  }

  state.lencode = lenfix;
  state.lenbits = 9;
  state.distcode = distfix;
  state.distbits = 5;
};


/*
 Update the window with the last wsize (normally 32K) bytes written before
 returning.  If window does not exist yet, create it.  This is only called
 when a window is already in use, or when output has been written during this
 inflate call, but the end of the deflate stream has not been reached yet.
 It is also called to create a window for dictionary data when a dictionary
 is loaded.

 Providing output buffers larger than 32K to inflate() should provide a speed
 advantage, since only the last 32K of output is copied to the sliding window
 upon return from inflate(), and since all distances after the first 32K of
 output will fall in the output data, making match copies simpler and faster.
 The advantage may be dependent on the size of the processor's data caches.
 */
const updatewindow = (strm, src, end, copy) => {

  let dist;
  const state = strm.state;

  /* if it hasn't been done already, allocate space for the window */
  if (state.window === null) {
    state.wsize = 1 << state.wbits;
    state.wnext = 0;
    state.whave = 0;

    state.window = new Uint8Array(state.wsize);
  }

  /* copy state->wsize or less output bytes into the circular window */
  if (copy >= state.wsize) {
    state.window.set(src.subarray(end - state.wsize, end), 0);
    state.wnext = 0;
    state.whave = state.wsize;
  }
  else {
    dist = state.wsize - state.wnext;
    if (dist > copy) {
      dist = copy;
    }
    //zmemcpy(state->window + state->wnext, end - copy, dist);
    state.window.set(src.subarray(end - copy, end - copy + dist), state.wnext);
    copy -= dist;
    if (copy) {
      //zmemcpy(state->window, end - copy, copy);
      state.window.set(src.subarray(end - copy, end), 0);
      state.wnext = copy;
      state.whave = state.wsize;
    }
    else {
      state.wnext += dist;
      if (state.wnext === state.wsize) { state.wnext = 0; }
      if (state.whave < state.wsize) { state.whave += dist; }
    }
  }
  return 0;
};


const inflate$2 = (strm, flush) => {

  let state;
  let input, output;          // input/output buffers
  let next;                   /* next input INDEX */
  let put;                    /* next output INDEX */
  let have, left;             /* available input and output */
  let hold;                   /* bit buffer */
  let bits;                   /* bits in bit buffer */
  let _in, _out;              /* save starting available input and output */
  let copy;                   /* number of stored or match bytes to copy */
  let from;                   /* where to copy match bytes from */
  let from_source;
  let here = 0;               /* current decoding table entry */
  let here_bits, here_op, here_val; // paked "here" denormalized (JS specific)
  //let last;                   /* parent table entry */
  let last_bits, last_op, last_val; // paked "last" denormalized (JS specific)
  let len;                    /* length to copy for repeats, bits to drop */
  let ret;                    /* return code */
  const hbuf = new Uint8Array(4);    /* buffer for gzip header crc calculation */
  let opts;

  let n; // temporary variable for NEED_BITS

  const order = /* permutation of code lengths */
    new Uint8Array([ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ]);


  if (inflateStateCheck(strm) || !strm.output ||
      (!strm.input && strm.avail_in !== 0)) {
    return Z_STREAM_ERROR$1;
  }

  state = strm.state;
  if (state.mode === TYPE) { state.mode = TYPEDO; }    /* skip check */


  //--- LOAD() ---
  put = strm.next_out;
  output = strm.output;
  left = strm.avail_out;
  next = strm.next_in;
  input = strm.input;
  have = strm.avail_in;
  hold = state.hold;
  bits = state.bits;
  //---

  _in = have;
  _out = left;
  ret = Z_OK$1;

  inf_leave: // goto emulation
  for (;;) {
    switch (state.mode) {
      case HEAD:
        if (state.wrap === 0) {
          state.mode = TYPEDO;
          break;
        }
        //=== NEEDBITS(16);
        while (bits < 16) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        if ((state.wrap & 2) && hold === 0x8b1f) {  /* gzip header */
          if (state.wbits === 0) {
            state.wbits = 15;
          }
          state.check = 0/*crc32(0L, Z_NULL, 0)*/;
          //=== CRC2(state.check, hold);
          hbuf[0] = hold & 0xff;
          hbuf[1] = (hold >>> 8) & 0xff;
          state.check = crc32_1(state.check, hbuf, 2, 0);
          //===//

          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          state.mode = FLAGS;
          break;
        }
        if (state.head) {
          state.head.done = false;
        }
        if (!(state.wrap & 1) ||   /* check if zlib header allowed */
          (((hold & 0xff)/*BITS(8)*/ << 8) + (hold >> 8)) % 31) {
          strm.msg = 'incorrect header check';
          state.mode = BAD;
          break;
        }
        if ((hold & 0x0f)/*BITS(4)*/ !== Z_DEFLATED) {
          strm.msg = 'unknown compression method';
          state.mode = BAD;
          break;
        }
        //--- DROPBITS(4) ---//
        hold >>>= 4;
        bits -= 4;
        //---//
        len = (hold & 0x0f)/*BITS(4)*/ + 8;
        if (state.wbits === 0) {
          state.wbits = len;
        }
        if (len > 15 || len > state.wbits) {
          strm.msg = 'invalid window size';
          state.mode = BAD;
          break;
        }

        // !!! pako patch. Force use `options.windowBits` if passed.
        // Required to always use max window size by default.
        state.dmax = 1 << state.wbits;
        //state.dmax = 1 << len;

        state.flags = 0;               /* indicate zlib header */
        //Tracev((stderr, "inflate:   zlib header ok\n"));
        strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;
        state.mode = hold & 0x200 ? DICTID : TYPE;
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        break;
      case FLAGS:
        //=== NEEDBITS(16); */
        while (bits < 16) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        state.flags = hold;
        if ((state.flags & 0xff) !== Z_DEFLATED) {
          strm.msg = 'unknown compression method';
          state.mode = BAD;
          break;
        }
        if (state.flags & 0xe000) {
          strm.msg = 'unknown header flags set';
          state.mode = BAD;
          break;
        }
        if (state.head) {
          state.head.text = ((hold >> 8) & 1);
        }
        if ((state.flags & 0x0200) && (state.wrap & 4)) {
          //=== CRC2(state.check, hold);
          hbuf[0] = hold & 0xff;
          hbuf[1] = (hold >>> 8) & 0xff;
          state.check = crc32_1(state.check, hbuf, 2, 0);
          //===//
        }
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = TIME;
        /* falls through */
      case TIME:
        //=== NEEDBITS(32); */
        while (bits < 32) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        if (state.head) {
          state.head.time = hold;
        }
        if ((state.flags & 0x0200) && (state.wrap & 4)) {
          //=== CRC4(state.check, hold)
          hbuf[0] = hold & 0xff;
          hbuf[1] = (hold >>> 8) & 0xff;
          hbuf[2] = (hold >>> 16) & 0xff;
          hbuf[3] = (hold >>> 24) & 0xff;
          state.check = crc32_1(state.check, hbuf, 4, 0);
          //===
        }
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = OS;
        /* falls through */
      case OS:
        //=== NEEDBITS(16); */
        while (bits < 16) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        if (state.head) {
          state.head.xflags = (hold & 0xff);
          state.head.os = (hold >> 8);
        }
        if ((state.flags & 0x0200) && (state.wrap & 4)) {
          //=== CRC2(state.check, hold);
          hbuf[0] = hold & 0xff;
          hbuf[1] = (hold >>> 8) & 0xff;
          state.check = crc32_1(state.check, hbuf, 2, 0);
          //===//
        }
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = EXLEN;
        /* falls through */
      case EXLEN:
        if (state.flags & 0x0400) {
          //=== NEEDBITS(16); */
          while (bits < 16) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.length = hold;
          if (state.head) {
            state.head.extra_len = hold;
          }
          if ((state.flags & 0x0200) && (state.wrap & 4)) {
            //=== CRC2(state.check, hold);
            hbuf[0] = hold & 0xff;
            hbuf[1] = (hold >>> 8) & 0xff;
            state.check = crc32_1(state.check, hbuf, 2, 0);
            //===//
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
        }
        else if (state.head) {
          state.head.extra = null/*Z_NULL*/;
        }
        state.mode = EXTRA;
        /* falls through */
      case EXTRA:
        if (state.flags & 0x0400) {
          copy = state.length;
          if (copy > have) { copy = have; }
          if (copy) {
            if (state.head) {
              len = state.head.extra_len - state.length;
              if (!state.head.extra) {
                // Use untyped array for more convenient processing later
                state.head.extra = new Uint8Array(state.head.extra_len);
              }
              state.head.extra.set(
                input.subarray(
                  next,
                  // extra field is limited to 65536 bytes
                  // - no need for additional size check
                  next + copy
                ),
                /*len + copy > state.head.extra_max - len ? state.head.extra_max : copy,*/
                len
              );
              //zmemcpy(state.head.extra + len, next,
              //        len + copy > state.head.extra_max ?
              //        state.head.extra_max - len : copy);
            }
            if ((state.flags & 0x0200) && (state.wrap & 4)) {
              state.check = crc32_1(state.check, input, copy, next);
            }
            have -= copy;
            next += copy;
            state.length -= copy;
          }
          if (state.length) { break inf_leave; }
        }
        state.length = 0;
        state.mode = NAME;
        /* falls through */
      case NAME:
        if (state.flags & 0x0800) {
          if (have === 0) { break inf_leave; }
          copy = 0;
          do {
            // TODO: 2 or 1 bytes?
            len = input[next + copy++];
            /* use constant limit because in js we should not preallocate memory */
            if (state.head && len &&
                (state.length < 65536 /*state.head.name_max*/)) {
              state.head.name += String.fromCharCode(len);
            }
          } while (len && copy < have);

          if ((state.flags & 0x0200) && (state.wrap & 4)) {
            state.check = crc32_1(state.check, input, copy, next);
          }
          have -= copy;
          next += copy;
          if (len) { break inf_leave; }
        }
        else if (state.head) {
          state.head.name = null;
        }
        state.length = 0;
        state.mode = COMMENT;
        /* falls through */
      case COMMENT:
        if (state.flags & 0x1000) {
          if (have === 0) { break inf_leave; }
          copy = 0;
          do {
            len = input[next + copy++];
            /* use constant limit because in js we should not preallocate memory */
            if (state.head && len &&
                (state.length < 65536 /*state.head.comm_max*/)) {
              state.head.comment += String.fromCharCode(len);
            }
          } while (len && copy < have);
          if ((state.flags & 0x0200) && (state.wrap & 4)) {
            state.check = crc32_1(state.check, input, copy, next);
          }
          have -= copy;
          next += copy;
          if (len) { break inf_leave; }
        }
        else if (state.head) {
          state.head.comment = null;
        }
        state.mode = HCRC;
        /* falls through */
      case HCRC:
        if (state.flags & 0x0200) {
          //=== NEEDBITS(16); */
          while (bits < 16) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          if ((state.wrap & 4) && hold !== (state.check & 0xffff)) {
            strm.msg = 'header crc mismatch';
            state.mode = BAD;
            break;
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
        }
        if (state.head) {
          state.head.hcrc = ((state.flags >> 9) & 1);
          state.head.done = true;
        }
        strm.adler = state.check = 0;
        state.mode = TYPE;
        break;
      case DICTID:
        //=== NEEDBITS(32); */
        while (bits < 32) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        strm.adler = state.check = zswap32(hold);
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = DICT;
        /* falls through */
      case DICT:
        if (state.havedict === 0) {
          //--- RESTORE() ---
          strm.next_out = put;
          strm.avail_out = left;
          strm.next_in = next;
          strm.avail_in = have;
          state.hold = hold;
          state.bits = bits;
          //---
          return Z_NEED_DICT$1;
        }
        strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;
        state.mode = TYPE;
        /* falls through */
      case TYPE:
        if (flush === Z_BLOCK || flush === Z_TREES) { break inf_leave; }
        /* falls through */
      case TYPEDO:
        if (state.last) {
          //--- BYTEBITS() ---//
          hold >>>= bits & 7;
          bits -= bits & 7;
          //---//
          state.mode = CHECK;
          break;
        }
        //=== NEEDBITS(3); */
        while (bits < 3) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        state.last = (hold & 0x01)/*BITS(1)*/;
        //--- DROPBITS(1) ---//
        hold >>>= 1;
        bits -= 1;
        //---//

        switch ((hold & 0x03)/*BITS(2)*/) {
          case 0:                             /* stored block */
            //Tracev((stderr, "inflate:     stored block%s\n",
            //        state.last ? " (last)" : ""));
            state.mode = STORED;
            break;
          case 1:                             /* fixed block */
            fixedtables(state);
            //Tracev((stderr, "inflate:     fixed codes block%s\n",
            //        state.last ? " (last)" : ""));
            state.mode = LEN_;             /* decode codes */
            if (flush === Z_TREES) {
              //--- DROPBITS(2) ---//
              hold >>>= 2;
              bits -= 2;
              //---//
              break inf_leave;
            }
            break;
          case 2:                             /* dynamic block */
            //Tracev((stderr, "inflate:     dynamic codes block%s\n",
            //        state.last ? " (last)" : ""));
            state.mode = TABLE;
            break;
          case 3:
            strm.msg = 'invalid block type';
            state.mode = BAD;
        }
        //--- DROPBITS(2) ---//
        hold >>>= 2;
        bits -= 2;
        //---//
        break;
      case STORED:
        //--- BYTEBITS() ---// /* go to byte boundary */
        hold >>>= bits & 7;
        bits -= bits & 7;
        //---//
        //=== NEEDBITS(32); */
        while (bits < 32) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        if ((hold & 0xffff) !== ((hold >>> 16) ^ 0xffff)) {
          strm.msg = 'invalid stored block lengths';
          state.mode = BAD;
          break;
        }
        state.length = hold & 0xffff;
        //Tracev((stderr, "inflate:       stored length %u\n",
        //        state.length));
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = COPY_;
        if (flush === Z_TREES) { break inf_leave; }
        /* falls through */
      case COPY_:
        state.mode = COPY;
        /* falls through */
      case COPY:
        copy = state.length;
        if (copy) {
          if (copy > have) { copy = have; }
          if (copy > left) { copy = left; }
          if (copy === 0) { break inf_leave; }
          //--- zmemcpy(put, next, copy); ---
          output.set(input.subarray(next, next + copy), put);
          //---//
          have -= copy;
          next += copy;
          left -= copy;
          put += copy;
          state.length -= copy;
          break;
        }
        //Tracev((stderr, "inflate:       stored end\n"));
        state.mode = TYPE;
        break;
      case TABLE:
        //=== NEEDBITS(14); */
        while (bits < 14) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        state.nlen = (hold & 0x1f)/*BITS(5)*/ + 257;
        //--- DROPBITS(5) ---//
        hold >>>= 5;
        bits -= 5;
        //---//
        state.ndist = (hold & 0x1f)/*BITS(5)*/ + 1;
        //--- DROPBITS(5) ---//
        hold >>>= 5;
        bits -= 5;
        //---//
        state.ncode = (hold & 0x0f)/*BITS(4)*/ + 4;
        //--- DROPBITS(4) ---//
        hold >>>= 4;
        bits -= 4;
        //---//
//#ifndef PKZIP_BUG_WORKAROUND
        if (state.nlen > 286 || state.ndist > 30) {
          strm.msg = 'too many length or distance symbols';
          state.mode = BAD;
          break;
        }
//#endif
        //Tracev((stderr, "inflate:       table sizes ok\n"));
        state.have = 0;
        state.mode = LENLENS;
        /* falls through */
      case LENLENS:
        while (state.have < state.ncode) {
          //=== NEEDBITS(3);
          while (bits < 3) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.lens[order[state.have++]] = (hold & 0x07);//BITS(3);
          //--- DROPBITS(3) ---//
          hold >>>= 3;
          bits -= 3;
          //---//
        }
        while (state.have < 19) {
          state.lens[order[state.have++]] = 0;
        }
        // We have separate tables & no pointers. 2 commented lines below not needed.
        //state.next = state.codes;
        //state.lencode = state.next;
        // Switch to use dynamic table
        state.lencode = state.lendyn;
        state.lenbits = 7;

        opts = { bits: state.lenbits };
        ret = inftrees(CODES, state.lens, 0, 19, state.lencode, 0, state.work, opts);
        state.lenbits = opts.bits;

        if (ret) {
          strm.msg = 'invalid code lengths set';
          state.mode = BAD;
          break;
        }
        //Tracev((stderr, "inflate:       code lengths ok\n"));
        state.have = 0;
        state.mode = CODELENS;
        /* falls through */
      case CODELENS:
        while (state.have < state.nlen + state.ndist) {
          for (;;) {
            here = state.lencode[hold & ((1 << state.lenbits) - 1)];/*BITS(state.lenbits)*/
            here_bits = here >>> 24;
            here_op = (here >>> 16) & 0xff;
            here_val = here & 0xffff;

            if ((here_bits) <= bits) { break; }
            //--- PULLBYTE() ---//
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
            //---//
          }
          if (here_val < 16) {
            //--- DROPBITS(here.bits) ---//
            hold >>>= here_bits;
            bits -= here_bits;
            //---//
            state.lens[state.have++] = here_val;
          }
          else {
            if (here_val === 16) {
              //=== NEEDBITS(here.bits + 2);
              n = here_bits + 2;
              while (bits < n) {
                if (have === 0) { break inf_leave; }
                have--;
                hold += input[next++] << bits;
                bits += 8;
              }
              //===//
              //--- DROPBITS(here.bits) ---//
              hold >>>= here_bits;
              bits -= here_bits;
              //---//
              if (state.have === 0) {
                strm.msg = 'invalid bit length repeat';
                state.mode = BAD;
                break;
              }
              len = state.lens[state.have - 1];
              copy = 3 + (hold & 0x03);//BITS(2);
              //--- DROPBITS(2) ---//
              hold >>>= 2;
              bits -= 2;
              //---//
            }
            else if (here_val === 17) {
              //=== NEEDBITS(here.bits + 3);
              n = here_bits + 3;
              while (bits < n) {
                if (have === 0) { break inf_leave; }
                have--;
                hold += input[next++] << bits;
                bits += 8;
              }
              //===//
              //--- DROPBITS(here.bits) ---//
              hold >>>= here_bits;
              bits -= here_bits;
              //---//
              len = 0;
              copy = 3 + (hold & 0x07);//BITS(3);
              //--- DROPBITS(3) ---//
              hold >>>= 3;
              bits -= 3;
              //---//
            }
            else {
              //=== NEEDBITS(here.bits + 7);
              n = here_bits + 7;
              while (bits < n) {
                if (have === 0) { break inf_leave; }
                have--;
                hold += input[next++] << bits;
                bits += 8;
              }
              //===//
              //--- DROPBITS(here.bits) ---//
              hold >>>= here_bits;
              bits -= here_bits;
              //---//
              len = 0;
              copy = 11 + (hold & 0x7f);//BITS(7);
              //--- DROPBITS(7) ---//
              hold >>>= 7;
              bits -= 7;
              //---//
            }
            if (state.have + copy > state.nlen + state.ndist) {
              strm.msg = 'invalid bit length repeat';
              state.mode = BAD;
              break;
            }
            while (copy--) {
              state.lens[state.have++] = len;
            }
          }
        }

        /* handle error breaks in while */
        if (state.mode === BAD) { break; }

        /* check for end-of-block code (better have one) */
        if (state.lens[256] === 0) {
          strm.msg = 'invalid code -- missing end-of-block';
          state.mode = BAD;
          break;
        }

        /* build code tables -- note: do not change the lenbits or distbits
           values here (9 and 6) without reading the comments in inftrees.h
           concerning the ENOUGH constants, which depend on those values */
        state.lenbits = 9;

        opts = { bits: state.lenbits };
        ret = inftrees(LENS, state.lens, 0, state.nlen, state.lencode, 0, state.work, opts);
        // We have separate tables & no pointers. 2 commented lines below not needed.
        // state.next_index = opts.table_index;
        state.lenbits = opts.bits;
        // state.lencode = state.next;

        if (ret) {
          strm.msg = 'invalid literal/lengths set';
          state.mode = BAD;
          break;
        }

        state.distbits = 6;
        //state.distcode.copy(state.codes);
        // Switch to use dynamic table
        state.distcode = state.distdyn;
        opts = { bits: state.distbits };
        ret = inftrees(DISTS, state.lens, state.nlen, state.ndist, state.distcode, 0, state.work, opts);
        // We have separate tables & no pointers. 2 commented lines below not needed.
        // state.next_index = opts.table_index;
        state.distbits = opts.bits;
        // state.distcode = state.next;

        if (ret) {
          strm.msg = 'invalid distances set';
          state.mode = BAD;
          break;
        }
        //Tracev((stderr, 'inflate:       codes ok\n'));
        state.mode = LEN_;
        if (flush === Z_TREES) { break inf_leave; }
        /* falls through */
      case LEN_:
        state.mode = LEN;
        /* falls through */
      case LEN:
        if (have >= 6 && left >= 258) {
          //--- RESTORE() ---
          strm.next_out = put;
          strm.avail_out = left;
          strm.next_in = next;
          strm.avail_in = have;
          state.hold = hold;
          state.bits = bits;
          //---
          inffast(strm, _out);
          //--- LOAD() ---
          put = strm.next_out;
          output = strm.output;
          left = strm.avail_out;
          next = strm.next_in;
          input = strm.input;
          have = strm.avail_in;
          hold = state.hold;
          bits = state.bits;
          //---

          if (state.mode === TYPE) {
            state.back = -1;
          }
          break;
        }
        state.back = 0;
        for (;;) {
          here = state.lencode[hold & ((1 << state.lenbits) - 1)];  /*BITS(state.lenbits)*/
          here_bits = here >>> 24;
          here_op = (here >>> 16) & 0xff;
          here_val = here & 0xffff;

          if (here_bits <= bits) { break; }
          //--- PULLBYTE() ---//
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
          //---//
        }
        if (here_op && (here_op & 0xf0) === 0) {
          last_bits = here_bits;
          last_op = here_op;
          last_val = here_val;
          for (;;) {
            here = state.lencode[last_val +
                    ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];
            here_bits = here >>> 24;
            here_op = (here >>> 16) & 0xff;
            here_val = here & 0xffff;

            if ((last_bits + here_bits) <= bits) { break; }
            //--- PULLBYTE() ---//
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
            //---//
          }
          //--- DROPBITS(last.bits) ---//
          hold >>>= last_bits;
          bits -= last_bits;
          //---//
          state.back += last_bits;
        }
        //--- DROPBITS(here.bits) ---//
        hold >>>= here_bits;
        bits -= here_bits;
        //---//
        state.back += here_bits;
        state.length = here_val;
        if (here_op === 0) {
          //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
          //        "inflate:         literal '%c'\n" :
          //        "inflate:         literal 0x%02x\n", here.val));
          state.mode = LIT;
          break;
        }
        if (here_op & 32) {
          //Tracevv((stderr, "inflate:         end of block\n"));
          state.back = -1;
          state.mode = TYPE;
          break;
        }
        if (here_op & 64) {
          strm.msg = 'invalid literal/length code';
          state.mode = BAD;
          break;
        }
        state.extra = here_op & 15;
        state.mode = LENEXT;
        /* falls through */
      case LENEXT:
        if (state.extra) {
          //=== NEEDBITS(state.extra);
          n = state.extra;
          while (bits < n) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.length += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;
          //--- DROPBITS(state.extra) ---//
          hold >>>= state.extra;
          bits -= state.extra;
          //---//
          state.back += state.extra;
        }
        //Tracevv((stderr, "inflate:         length %u\n", state.length));
        state.was = state.length;
        state.mode = DIST;
        /* falls through */
      case DIST:
        for (;;) {
          here = state.distcode[hold & ((1 << state.distbits) - 1)];/*BITS(state.distbits)*/
          here_bits = here >>> 24;
          here_op = (here >>> 16) & 0xff;
          here_val = here & 0xffff;

          if ((here_bits) <= bits) { break; }
          //--- PULLBYTE() ---//
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
          //---//
        }
        if ((here_op & 0xf0) === 0) {
          last_bits = here_bits;
          last_op = here_op;
          last_val = here_val;
          for (;;) {
            here = state.distcode[last_val +
                    ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];
            here_bits = here >>> 24;
            here_op = (here >>> 16) & 0xff;
            here_val = here & 0xffff;

            if ((last_bits + here_bits) <= bits) { break; }
            //--- PULLBYTE() ---//
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
            //---//
          }
          //--- DROPBITS(last.bits) ---//
          hold >>>= last_bits;
          bits -= last_bits;
          //---//
          state.back += last_bits;
        }
        //--- DROPBITS(here.bits) ---//
        hold >>>= here_bits;
        bits -= here_bits;
        //---//
        state.back += here_bits;
        if (here_op & 64) {
          strm.msg = 'invalid distance code';
          state.mode = BAD;
          break;
        }
        state.offset = here_val;
        state.extra = (here_op) & 15;
        state.mode = DISTEXT;
        /* falls through */
      case DISTEXT:
        if (state.extra) {
          //=== NEEDBITS(state.extra);
          n = state.extra;
          while (bits < n) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.offset += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;
          //--- DROPBITS(state.extra) ---//
          hold >>>= state.extra;
          bits -= state.extra;
          //---//
          state.back += state.extra;
        }
//#ifdef INFLATE_STRICT
        if (state.offset > state.dmax) {
          strm.msg = 'invalid distance too far back';
          state.mode = BAD;
          break;
        }
//#endif
        //Tracevv((stderr, "inflate:         distance %u\n", state.offset));
        state.mode = MATCH;
        /* falls through */
      case MATCH:
        if (left === 0) { break inf_leave; }
        copy = _out - left;
        if (state.offset > copy) {         /* copy from window */
          copy = state.offset - copy;
          if (copy > state.whave) {
            if (state.sane) {
              strm.msg = 'invalid distance too far back';
              state.mode = BAD;
              break;
            }
// (!) This block is disabled in zlib defaults,
// don't enable it for binary compatibility
//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
//          Trace((stderr, "inflate.c too far\n"));
//          copy -= state.whave;
//          if (copy > state.length) { copy = state.length; }
//          if (copy > left) { copy = left; }
//          left -= copy;
//          state.length -= copy;
//          do {
//            output[put++] = 0;
//          } while (--copy);
//          if (state.length === 0) { state.mode = LEN; }
//          break;
//#endif
          }
          if (copy > state.wnext) {
            copy -= state.wnext;
            from = state.wsize - copy;
          }
          else {
            from = state.wnext - copy;
          }
          if (copy > state.length) { copy = state.length; }
          from_source = state.window;
        }
        else {                              /* copy from output */
          from_source = output;
          from = put - state.offset;
          copy = state.length;
        }
        if (copy > left) { copy = left; }
        left -= copy;
        state.length -= copy;
        do {
          output[put++] = from_source[from++];
        } while (--copy);
        if (state.length === 0) { state.mode = LEN; }
        break;
      case LIT:
        if (left === 0) { break inf_leave; }
        output[put++] = state.length;
        left--;
        state.mode = LEN;
        break;
      case CHECK:
        if (state.wrap) {
          //=== NEEDBITS(32);
          while (bits < 32) {
            if (have === 0) { break inf_leave; }
            have--;
            // Use '|' instead of '+' to make sure that result is signed
            hold |= input[next++] << bits;
            bits += 8;
          }
          //===//
          _out -= left;
          strm.total_out += _out;
          state.total += _out;
          if ((state.wrap & 4) && _out) {
            strm.adler = state.check =
                /*UPDATE_CHECK(state.check, put - _out, _out);*/
                (state.flags ? crc32_1(state.check, output, _out, put - _out) : adler32_1(state.check, output, _out, put - _out));

          }
          _out = left;
          // NB: crc32 stored as signed 32-bit int, zswap32 returns signed too
          if ((state.wrap & 4) && (state.flags ? hold : zswap32(hold)) !== state.check) {
            strm.msg = 'incorrect data check';
            state.mode = BAD;
            break;
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          //Tracev((stderr, "inflate:   check matches trailer\n"));
        }
        state.mode = LENGTH;
        /* falls through */
      case LENGTH:
        if (state.wrap && state.flags) {
          //=== NEEDBITS(32);
          while (bits < 32) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          if ((state.wrap & 4) && hold !== (state.total & 0xffffffff)) {
            strm.msg = 'incorrect length check';
            state.mode = BAD;
            break;
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          //Tracev((stderr, "inflate:   length matches trailer\n"));
        }
        state.mode = DONE;
        /* falls through */
      case DONE:
        ret = Z_STREAM_END$1;
        break inf_leave;
      case BAD:
        ret = Z_DATA_ERROR$1;
        break inf_leave;
      case MEM:
        return Z_MEM_ERROR$1;
      case SYNC:
        /* falls through */
      default:
        return Z_STREAM_ERROR$1;
    }
  }

  // inf_leave <- here is real place for "goto inf_leave", emulated via "break inf_leave"

  /*
     Return from inflate(), updating the total counts and the check value.
     If there was no progress during the inflate() call, return a buffer
     error.  Call updatewindow() to create and/or update the window state.
     Note: a memory error from inflate() is non-recoverable.
   */

  //--- RESTORE() ---
  strm.next_out = put;
  strm.avail_out = left;
  strm.next_in = next;
  strm.avail_in = have;
  state.hold = hold;
  state.bits = bits;
  //---

  if (state.wsize || (_out !== strm.avail_out && state.mode < BAD &&
                      (state.mode < CHECK || flush !== Z_FINISH$1))) {
    if (updatewindow(strm, strm.output, strm.next_out, _out - strm.avail_out)) ;
  }
  _in -= strm.avail_in;
  _out -= strm.avail_out;
  strm.total_in += _in;
  strm.total_out += _out;
  state.total += _out;
  if ((state.wrap & 4) && _out) {
    strm.adler = state.check = /*UPDATE_CHECK(state.check, strm.next_out - _out, _out);*/
      (state.flags ? crc32_1(state.check, output, _out, strm.next_out - _out) : adler32_1(state.check, output, _out, strm.next_out - _out));
  }
  strm.data_type = state.bits + (state.last ? 64 : 0) +
                    (state.mode === TYPE ? 128 : 0) +
                    (state.mode === LEN_ || state.mode === COPY_ ? 256 : 0);
  if (((_in === 0 && _out === 0) || flush === Z_FINISH$1) && ret === Z_OK$1) {
    ret = Z_BUF_ERROR;
  }
  return ret;
};


const inflateEnd = (strm) => {

  if (inflateStateCheck(strm)) {
    return Z_STREAM_ERROR$1;
  }

  let state = strm.state;
  if (state.window) {
    state.window = null;
  }
  strm.state = null;
  return Z_OK$1;
};


const inflateGetHeader = (strm, head) => {

  /* check state */
  if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
  const state = strm.state;
  if ((state.wrap & 2) === 0) { return Z_STREAM_ERROR$1; }

  /* save header structure */
  state.head = head;
  head.done = false;
  return Z_OK$1;
};


const inflateSetDictionary = (strm, dictionary) => {
  const dictLength = dictionary.length;

  let state;
  let dictid;
  let ret;

  /* check state */
  if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
  state = strm.state;

  if (state.wrap !== 0 && state.mode !== DICT) {
    return Z_STREAM_ERROR$1;
  }

  /* check for correct dictionary identifier */
  if (state.mode === DICT) {
    dictid = 1; /* adler32(0, null, 0)*/
    /* dictid = adler32(dictid, dictionary, dictLength); */
    dictid = adler32_1(dictid, dictionary, dictLength, 0);
    if (dictid !== state.check) {
      return Z_DATA_ERROR$1;
    }
  }
  /* copy dictionary to window using updatewindow(), which will amend the
   existing dictionary if appropriate */
  ret = updatewindow(strm, dictionary, dictLength, dictLength);
  if (ret) {
    state.mode = MEM;
    return Z_MEM_ERROR$1;
  }
  state.havedict = 1;
  // Tracev((stderr, "inflate:   dictionary set\n"));
  return Z_OK$1;
};


var inflateReset_1 = inflateReset;
var inflateReset2_1 = inflateReset2;
var inflateResetKeep_1 = inflateResetKeep;
var inflateInit_1 = inflateInit;
var inflateInit2_1 = inflateInit2;
var inflate_2$1 = inflate$2;
var inflateEnd_1 = inflateEnd;
var inflateGetHeader_1 = inflateGetHeader;
var inflateSetDictionary_1 = inflateSetDictionary;
var inflateInfo = 'pako inflate (from Nodeca project)';

/* Not implemented
module.exports.inflateCodesUsed = inflateCodesUsed;
module.exports.inflateCopy = inflateCopy;
module.exports.inflateGetDictionary = inflateGetDictionary;
module.exports.inflateMark = inflateMark;
module.exports.inflatePrime = inflatePrime;
module.exports.inflateSync = inflateSync;
module.exports.inflateSyncPoint = inflateSyncPoint;
module.exports.inflateUndermine = inflateUndermine;
module.exports.inflateValidate = inflateValidate;
*/

var inflate_1$2 = {
	inflateReset: inflateReset_1,
	inflateReset2: inflateReset2_1,
	inflateResetKeep: inflateResetKeep_1,
	inflateInit: inflateInit_1,
	inflateInit2: inflateInit2_1,
	inflate: inflate_2$1,
	inflateEnd: inflateEnd_1,
	inflateGetHeader: inflateGetHeader_1,
	inflateSetDictionary: inflateSetDictionary_1,
	inflateInfo: inflateInfo
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

function GZheader() {
  /* true if compressed data believed to be text */
  this.text       = 0;
  /* modification time */
  this.time       = 0;
  /* extra flags (not used when writing a gzip file) */
  this.xflags     = 0;
  /* operating system */
  this.os         = 0;
  /* pointer to extra field or Z_NULL if none */
  this.extra      = null;
  /* extra field length (valid if extra != Z_NULL) */
  this.extra_len  = 0; // Actually, we don't need it in JS,
                       // but leave for few code modifications

  //
  // Setup limits is not necessary because in js we should not preallocate memory
  // for inflate use constant limit in 65536 bytes
  //

  /* space at extra (only when reading header) */
  // this.extra_max  = 0;
  /* pointer to zero-terminated file name or Z_NULL */
  this.name       = '';
  /* space at name (only when reading header) */
  // this.name_max   = 0;
  /* pointer to zero-terminated comment or Z_NULL */
  this.comment    = '';
  /* space at comment (only when reading header) */
  // this.comm_max   = 0;
  /* true if there was or will be a header crc */
  this.hcrc       = 0;
  /* true when done reading gzip header (not used when writing a gzip file) */
  this.done       = false;
}

var gzheader = GZheader;

const toString = Object.prototype.toString;

/* Public constants ==========================================================*/
/* ===========================================================================*/

const {
  Z_NO_FLUSH, Z_FINISH,
  Z_OK, Z_STREAM_END, Z_NEED_DICT, Z_STREAM_ERROR, Z_DATA_ERROR, Z_MEM_ERROR
} = constants$2;

/* ===========================================================================*/


/**
 * class Inflate
 *
 * Generic JS-style wrapper for zlib calls. If you don't need
 * streaming behaviour - use more simple functions: [[inflate]]
 * and [[inflateRaw]].
 **/

/* internal
 * inflate.chunks -> Array
 *
 * Chunks of output data, if [[Inflate#onData]] not overridden.
 **/

/**
 * Inflate.result -> Uint8Array|String
 *
 * Uncompressed result, generated by default [[Inflate#onData]]
 * and [[Inflate#onEnd]] handlers. Filled after you push last chunk
 * (call [[Inflate#push]] with `Z_FINISH` / `true` param).
 **/

/**
 * Inflate.err -> Number
 *
 * Error code after inflate finished. 0 (Z_OK) on success.
 * Should be checked if broken data possible.
 **/

/**
 * Inflate.msg -> String
 *
 * Error message, if [[Inflate.err]] != 0
 **/


/**
 * new Inflate(options)
 * - options (Object): zlib inflate options.
 *
 * Creates new inflator instance with specified params. Throws exception
 * on bad params. Supported options:
 *
 * - `windowBits`
 * - `dictionary`
 *
 * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
 * for more information on these.
 *
 * Additional options, for internal needs:
 *
 * - `chunkSize` - size of generated data chunks (16K by default)
 * - `raw` (Boolean) - do raw inflate
 * - `to` (String) - if equal to 'string', then result will be converted
 *   from utf8 to utf16 (javascript) string. When string output requested,
 *   chunk length can differ from `chunkSize`, depending on content.
 *
 * By default, when no options set, autodetect deflate/gzip data format via
 * wrapper header.
 *
 * ##### Example:
 *
 * ```javascript
 * const pako = require('pako')
 * const chunk1 = new Uint8Array([1,2,3,4,5,6,7,8,9])
 * const chunk2 = new Uint8Array([10,11,12,13,14,15,16,17,18,19]);
 *
 * const inflate = new pako.Inflate({ level: 3});
 *
 * inflate.push(chunk1, false);
 * inflate.push(chunk2, true);  // true -> last chunk
 *
 * if (inflate.err) { throw new Error(inflate.err); }
 *
 * console.log(inflate.result);
 * ```
 **/
function Inflate$1(options) {
  this.options = common.assign({
    chunkSize: 1024 * 64,
    windowBits: 15,
    to: ''
  }, options || {});

  const opt = this.options;

  // Force window size for `raw` data, if not set directly,
  // because we have no header for autodetect.
  if (opt.raw && (opt.windowBits >= 0) && (opt.windowBits < 16)) {
    opt.windowBits = -opt.windowBits;
    if (opt.windowBits === 0) { opt.windowBits = -15; }
  }

  // If `windowBits` not defined (and mode not raw) - set autodetect flag for gzip/deflate
  if ((opt.windowBits >= 0) && (opt.windowBits < 16) &&
      !(options && options.windowBits)) {
    opt.windowBits += 32;
  }

  // Gzip header has no info about windows size, we can do autodetect only
  // for deflate. So, if window size not set, force it to max when gzip possible
  if ((opt.windowBits > 15) && (opt.windowBits < 48)) {
    // bit 3 (16) -> gzipped data
    // bit 4 (32) -> autodetect gzip/deflate
    if ((opt.windowBits & 15) === 0) {
      opt.windowBits |= 15;
    }
  }

  this.err    = 0;      // error code, if happens (0 = Z_OK)
  this.msg    = '';     // error message
  this.ended  = false;  // used to avoid multiple onEnd() calls
  this.chunks = [];     // chunks of compressed data

  this.strm   = new zstream();
  this.strm.avail_out = 0;

  let status  = inflate_1$2.inflateInit2(
    this.strm,
    opt.windowBits
  );

  if (status !== Z_OK) {
    throw new Error(messages[status]);
  }

  this.header = new gzheader();

  inflate_1$2.inflateGetHeader(this.strm, this.header);

  // Setup dictionary
  if (opt.dictionary) {
    // Convert data if needed
    if (typeof opt.dictionary === 'string') {
      opt.dictionary = strings.string2buf(opt.dictionary);
    } else if (toString.call(opt.dictionary) === '[object ArrayBuffer]') {
      opt.dictionary = new Uint8Array(opt.dictionary);
    }
    if (opt.raw) { //In raw mode we need to set the dictionary early
      status = inflate_1$2.inflateSetDictionary(this.strm, opt.dictionary);
      if (status !== Z_OK) {
        throw new Error(messages[status]);
      }
    }
  }
}

/**
 * Inflate#push(data[, flush_mode]) -> Boolean
 * - data (Uint8Array|ArrayBuffer): input data
 * - flush_mode (Number|Boolean): 0..6 for corresponding Z_NO_FLUSH..Z_TREE
 *   flush modes. See constants. Skipped or `false` means Z_NO_FLUSH,
 *   `true` means Z_FINISH.
 *
 * Sends input data to inflate pipe, generating [[Inflate#onData]] calls with
 * new output chunks. Returns `true` on success. If end of stream detected,
 * [[Inflate#onEnd]] will be called.
 *
 * `flush_mode` is not needed for normal operation, because end of stream
 * detected automatically. You may try to use it for advanced things, but
 * this functionality was not tested.
 *
 * On fail call [[Inflate#onEnd]] with error code and return false.
 *
 * ##### Example
 *
 * ```javascript
 * push(chunk, false); // push one of data chunks
 * ...
 * push(chunk, true);  // push last chunk
 * ```
 **/
Inflate$1.prototype.push = function (data, flush_mode) {
  const strm = this.strm;
  const chunkSize = this.options.chunkSize;
  const dictionary = this.options.dictionary;
  let status, _flush_mode, last_avail_out;

  if (this.ended) return false;

  if (flush_mode === ~~flush_mode) _flush_mode = flush_mode;
  else _flush_mode = flush_mode === true ? Z_FINISH : Z_NO_FLUSH;

  // Convert data if needed
  if (toString.call(data) === '[object ArrayBuffer]') {
    strm.input = new Uint8Array(data);
  } else {
    strm.input = data;
  }

  strm.next_in = 0;
  strm.avail_in = strm.input.length;

  for (;;) {
    if (strm.avail_out === 0) {
      strm.output = new Uint8Array(chunkSize);
      strm.next_out = 0;
      strm.avail_out = chunkSize;
    }

    status = inflate_1$2.inflate(strm, _flush_mode);

    if (status === Z_NEED_DICT && dictionary) {
      status = inflate_1$2.inflateSetDictionary(strm, dictionary);

      if (status === Z_OK) {
        status = inflate_1$2.inflate(strm, _flush_mode);
      } else if (status === Z_DATA_ERROR) {
        // Replace code with more verbose
        status = Z_NEED_DICT;
      }
    }

    // Skip snyc markers if more data follows and not raw mode
    while (strm.avail_in > 0 &&
           status === Z_STREAM_END &&
           strm.state.wrap > 0 &&
           data[strm.next_in] !== 0)
    {
      inflate_1$2.inflateReset(strm);
      status = inflate_1$2.inflate(strm, _flush_mode);
    }

    switch (status) {
      case Z_STREAM_ERROR:
      case Z_DATA_ERROR:
      case Z_NEED_DICT:
      case Z_MEM_ERROR:
        this.onEnd(status);
        this.ended = true;
        return false;
    }

    // Remember real `avail_out` value, because we may patch out buffer content
    // to align utf8 strings boundaries.
    last_avail_out = strm.avail_out;

    if (strm.next_out) {
      if (strm.avail_out === 0 || status === Z_STREAM_END) {

        if (this.options.to === 'string') {

          let next_out_utf8 = strings.utf8border(strm.output, strm.next_out);

          let tail = strm.next_out - next_out_utf8;
          let utf8str = strings.buf2string(strm.output, next_out_utf8);

          // move tail & realign counters
          strm.next_out = tail;
          strm.avail_out = chunkSize - tail;
          if (tail) strm.output.set(strm.output.subarray(next_out_utf8, next_out_utf8 + tail), 0);

          this.onData(utf8str);

        } else {
          this.onData(strm.output.length === strm.next_out ? strm.output : strm.output.subarray(0, strm.next_out));
        }
      }
    }

    // Must repeat iteration if out buffer is full
    if (status === Z_OK && last_avail_out === 0) continue;

    // Finalize if end of stream reached.
    if (status === Z_STREAM_END) {
      status = inflate_1$2.inflateEnd(this.strm);
      this.onEnd(status);
      this.ended = true;
      return true;
    }

    if (strm.avail_in === 0) break;
  }

  return true;
};


/**
 * Inflate#onData(chunk) -> Void
 * - chunk (Uint8Array|String): output data. When string output requested,
 *   each chunk will be string.
 *
 * By default, stores data blocks in `chunks[]` property and glue
 * those in `onEnd`. Override this handler, if you need another behaviour.
 **/
Inflate$1.prototype.onData = function (chunk) {
  this.chunks.push(chunk);
};


/**
 * Inflate#onEnd(status) -> Void
 * - status (Number): inflate status. 0 (Z_OK) on success,
 *   other if not.
 *
 * Called either after you tell inflate that the input stream is
 * complete (Z_FINISH). By default - join collected chunks,
 * free memory and fill `results` / `err` properties.
 **/
Inflate$1.prototype.onEnd = function (status) {
  // On success - join
  if (status === Z_OK) {
    if (this.options.to === 'string') {
      this.result = this.chunks.join('');
    } else {
      this.result = common.flattenChunks(this.chunks);
    }
  }
  this.chunks = [];
  this.err = status;
  this.msg = this.strm.msg;
};


/**
 * inflate(data[, options]) -> Uint8Array|String
 * - data (Uint8Array|ArrayBuffer): input data to decompress.
 * - options (Object): zlib inflate options.
 *
 * Decompress `data` with inflate/ungzip and `options`. Autodetect
 * format via wrapper header by default. That's why we don't provide
 * separate `ungzip` method.
 *
 * Supported options are:
 *
 * - windowBits
 *
 * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
 * for more information.
 *
 * Sugar (options):
 *
 * - `raw` (Boolean) - say that we work with raw stream, if you don't wish to specify
 *   negative windowBits implicitly.
 * - `to` (String) - if equal to 'string', then result will be converted
 *   from utf8 to utf16 (javascript) string. When string output requested,
 *   chunk length can differ from `chunkSize`, depending on content.
 *
 *
 * ##### Example:
 *
 * ```javascript
 * const pako = require('pako');
 * const input = pako.deflate(new Uint8Array([1,2,3,4,5,6,7,8,9]));
 * let output;
 *
 * try {
 *   output = pako.inflate(input);
 * } catch (err) {
 *   console.log(err);
 * }
 * ```
 **/
function inflate$1(input, options) {
  const inflator = new Inflate$1(options);

  inflator.push(input);

  // That will never happens, if you don't cheat with options :)
  if (inflator.err) throw inflator.msg || messages[inflator.err];

  return inflator.result;
}


/**
 * ungzip(data[, options]) -> Uint8Array|String
 * - data (Uint8Array|ArrayBuffer): input data to decompress.
 * - options (Object): zlib inflate options.
 *
 * Just shortcut to [[inflate]], because it autodetects format
 * by header.content. Done for convenience.
 **/


var Inflate_1$1 = Inflate$1;
var inflate_2 = inflate$1;

var inflate_1$1 = {
	Inflate: Inflate_1$1,
	inflate: inflate_2};

const { Inflate, inflate} = inflate_1$1;
var Inflate_1 = Inflate;
var inflate_1 = inflate;

const crcTable = [];
for (let n = 0; n < 256; n++) {
    let c = n;
    for (let k = 0; k < 8; k++) {
        if (c & 1) {
            c = 0xedb88320 ^ (c >>> 1);
        }
        else {
            c = c >>> 1;
        }
    }
    crcTable[n] = c;
}
const initialCrc = 0xffffffff;
function updateCrc(currentCrc, data, length) {
    let c = currentCrc;
    for (let n = 0; n < length; n++) {
        c = crcTable[(c ^ data[n]) & 0xff] ^ (c >>> 8);
    }
    return c;
}
function crc(data, length) {
    return (updateCrc(initialCrc, data, length) ^ initialCrc) >>> 0;
}
function checkCrc(buffer, crcLength, chunkName) {
    const expectedCrc = buffer.readUint32();
    const actualCrc = crc(new Uint8Array(buffer.buffer, buffer.byteOffset + buffer.offset - crcLength - 4, crcLength), crcLength); // "- 4" because we already advanced by reading the CRC
    if (actualCrc !== expectedCrc) {
        throw new Error(`CRC mismatch for chunk ${chunkName}. Expected ${expectedCrc}, found ${actualCrc}`);
    }
}

function unfilterNone(currentLine, newLine, bytesPerLine) {
    for (let i = 0; i < bytesPerLine; i++) {
        newLine[i] = currentLine[i];
    }
}
function unfilterSub(currentLine, newLine, bytesPerLine, bytesPerPixel) {
    let i = 0;
    for (; i < bytesPerPixel; i++) {
        // just copy first bytes
        newLine[i] = currentLine[i];
    }
    for (; i < bytesPerLine; i++) {
        newLine[i] = (currentLine[i] + newLine[i - bytesPerPixel]) & 0xff;
    }
}
function unfilterUp(currentLine, newLine, prevLine, bytesPerLine) {
    let i = 0;
    if (prevLine.length === 0) {
        // just copy bytes for first line
        for (; i < bytesPerLine; i++) {
            newLine[i] = currentLine[i];
        }
    }
    else {
        for (; i < bytesPerLine; i++) {
            newLine[i] = (currentLine[i] + prevLine[i]) & 0xff;
        }
    }
}
function unfilterAverage(currentLine, newLine, prevLine, bytesPerLine, bytesPerPixel) {
    let i = 0;
    if (prevLine.length === 0) {
        for (; i < bytesPerPixel; i++) {
            newLine[i] = currentLine[i];
        }
        for (; i < bytesPerLine; i++) {
            newLine[i] = (currentLine[i] + (newLine[i - bytesPerPixel] >> 1)) & 0xff;
        }
    }
    else {
        for (; i < bytesPerPixel; i++) {
            newLine[i] = (currentLine[i] + (prevLine[i] >> 1)) & 0xff;
        }
        for (; i < bytesPerLine; i++) {
            newLine[i] =
                (currentLine[i] + ((newLine[i - bytesPerPixel] + prevLine[i]) >> 1)) &
                    0xff;
        }
    }
}
function unfilterPaeth(currentLine, newLine, prevLine, bytesPerLine, bytesPerPixel) {
    let i = 0;
    if (prevLine.length === 0) {
        for (; i < bytesPerPixel; i++) {
            newLine[i] = currentLine[i];
        }
        for (; i < bytesPerLine; i++) {
            newLine[i] = (currentLine[i] + newLine[i - bytesPerPixel]) & 0xff;
        }
    }
    else {
        for (; i < bytesPerPixel; i++) {
            newLine[i] = (currentLine[i] + prevLine[i]) & 0xff;
        }
        for (; i < bytesPerLine; i++) {
            newLine[i] =
                (currentLine[i] +
                    paethPredictor$1(newLine[i - bytesPerPixel], prevLine[i], prevLine[i - bytesPerPixel])) &
                    0xff;
        }
    }
}
function paethPredictor$1(a, b, c) {
    const p = a + b - c;
    const pa = Math.abs(p - a);
    const pb = Math.abs(p - b);
    const pc = Math.abs(p - c);
    if (pa <= pb && pa <= pc)
        return a;
    else if (pb <= pc)
        return b;
    else
        return c;
}

/**
 * Apllies filter on scanline based on the filter type.
 * @param filterType - The filter type to apply.
 * @param currentLine - The current line of pixel data.
 * @param newLine - The new line of pixel data.
 * @param prevLine - The previous line of pixel data.
 * @param passLineBytes - The number of bytes in the pass line.
 * @param bytesPerPixel - The number of bytes per pixel.
 */
function applyUnfilter(filterType, currentLine, newLine, prevLine, passLineBytes, bytesPerPixel) {
    switch (filterType) {
        case 0:
            unfilterNone(currentLine, newLine, passLineBytes);
            break;
        case 1:
            unfilterSub(currentLine, newLine, passLineBytes, bytesPerPixel);
            break;
        case 2:
            unfilterUp(currentLine, newLine, prevLine, passLineBytes);
            break;
        case 3:
            unfilterAverage(currentLine, newLine, prevLine, passLineBytes, bytesPerPixel);
            break;
        case 4:
            unfilterPaeth(currentLine, newLine, prevLine, passLineBytes, bytesPerPixel);
            break;
        default:
            throw new Error(`Unsupported filter: ${filterType}`);
    }
}

const uint16$1 = new Uint16Array([0x00ff]);
const uint8$1 = new Uint8Array(uint16$1.buffer);
const osIsLittleEndian$1 = uint8$1[0] === 0xff;
/**
 * Decodes the Adam7 interlaced PNG data.
 *
 * @param params - DecodeInterlaceNullParams
 * @returns - array of pixel data.
 */
function decodeInterlaceAdam7(params) {
    const { data, width, height, channels, depth } = params;
    // Adam7 interlacing pattern
    const passes = [
        { x: 0, y: 0, xStep: 8, yStep: 8 }, // Pass 1
        { x: 4, y: 0, xStep: 8, yStep: 8 }, // Pass 2
        { x: 0, y: 4, xStep: 4, yStep: 8 }, // Pass 3
        { x: 2, y: 0, xStep: 4, yStep: 4 }, // Pass 4
        { x: 0, y: 2, xStep: 2, yStep: 4 }, // Pass 5
        { x: 1, y: 0, xStep: 2, yStep: 2 }, // Pass 6
        { x: 0, y: 1, xStep: 1, yStep: 2 }, // Pass 7
    ];
    const bytesPerPixel = Math.ceil(depth / 8) * channels;
    const resultData = new Uint8Array(height * width * bytesPerPixel);
    let offset = 0;
    // Process each pass
    for (let passIndex = 0; passIndex < 7; passIndex++) {
        const pass = passes[passIndex];
        // Calculate pass dimensions
        const passWidth = Math.ceil((width - pass.x) / pass.xStep);
        const passHeight = Math.ceil((height - pass.y) / pass.yStep);
        if (passWidth <= 0 || passHeight <= 0)
            continue;
        const passLineBytes = passWidth * bytesPerPixel;
        const prevLine = new Uint8Array(passLineBytes);
        // Process each scanline in this pass
        for (let y = 0; y < passHeight; y++) {
            // First byte is the filter type
            const filterType = data[offset++];
            const currentLine = data.subarray(offset, offset + passLineBytes);
            offset += passLineBytes;
            // Create a new line for the unfiltered data
            const newLine = new Uint8Array(passLineBytes);
            // Apply the appropriate unfilter
            applyUnfilter(filterType, currentLine, newLine, prevLine, passLineBytes, bytesPerPixel);
            prevLine.set(newLine);
            for (let x = 0; x < passWidth; x++) {
                const outputX = pass.x + x * pass.xStep;
                const outputY = pass.y + y * pass.yStep;
                if (outputX >= width || outputY >= height)
                    continue;
                for (let i = 0; i < bytesPerPixel; i++) {
                    resultData[(outputY * width + outputX) * bytesPerPixel + i] =
                        newLine[x * bytesPerPixel + i];
                }
            }
        }
    }
    if (depth === 16) {
        const uint16Data = new Uint16Array(resultData.buffer);
        if (osIsLittleEndian$1) {
            for (let k = 0; k < uint16Data.length; k++) {
                // PNG is always big endian. Swap the bytes.
                uint16Data[k] = swap16$1(uint16Data[k]);
            }
        }
        return uint16Data;
    }
    else {
        return resultData;
    }
}
function swap16$1(val) {
    return ((val & 0xff) << 8) | ((val >> 8) & 0xff);
}

const uint16 = new Uint16Array([0x00ff]);
const uint8 = new Uint8Array(uint16.buffer);
const osIsLittleEndian = uint8[0] === 0xff;
const empty = new Uint8Array(0);
function decodeInterlaceNull(params) {
    const { data, width, height, channels, depth } = params;
    const bytesPerPixel = Math.ceil(depth / 8) * channels;
    const bytesPerLine = Math.ceil((depth / 8) * channels * width);
    const newData = new Uint8Array(height * bytesPerLine);
    let prevLine = empty;
    let offset = 0;
    let currentLine;
    let newLine;
    for (let i = 0; i < height; i++) {
        currentLine = data.subarray(offset + 1, offset + 1 + bytesPerLine);
        newLine = newData.subarray(i * bytesPerLine, (i + 1) * bytesPerLine);
        switch (data[offset]) {
            case 0:
                unfilterNone(currentLine, newLine, bytesPerLine);
                break;
            case 1:
                unfilterSub(currentLine, newLine, bytesPerLine, bytesPerPixel);
                break;
            case 2:
                unfilterUp(currentLine, newLine, prevLine, bytesPerLine);
                break;
            case 3:
                unfilterAverage(currentLine, newLine, prevLine, bytesPerLine, bytesPerPixel);
                break;
            case 4:
                unfilterPaeth(currentLine, newLine, prevLine, bytesPerLine, bytesPerPixel);
                break;
            default:
                throw new Error(`Unsupported filter: ${data[offset]}`);
        }
        prevLine = newLine;
        offset += bytesPerLine + 1;
    }
    if (depth === 16) {
        const uint16Data = new Uint16Array(newData.buffer);
        if (osIsLittleEndian) {
            for (let k = 0; k < uint16Data.length; k++) {
                // PNG is always big endian. Swap the bytes.
                uint16Data[k] = swap16(uint16Data[k]);
            }
        }
        return uint16Data;
    }
    else {
        return newData;
    }
}
function swap16(val) {
    return ((val & 0xff) << 8) | ((val >> 8) & 0xff);
}

// https://www.w3.org/TR/PNG/#5PNG-file-signature
const pngSignature = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
function checkSignature(buffer) {
    if (!hasPngSignature(buffer.readBytes(pngSignature.length))) {
        throw new Error('wrong PNG signature');
    }
}
function hasPngSignature(array) {
    if (array.length < pngSignature.length) {
        return false;
    }
    for (let i = 0; i < pngSignature.length; i++) {
        if (array[i] !== pngSignature[i]) {
            return false;
        }
    }
    return true;
}

// https://www.w3.org/TR/png/#11tEXt
const textChunkName = 'tEXt';
const NULL = 0;
const latin1Decoder = new TextDecoder('latin1');
function validateKeyword(keyword) {
    validateLatin1(keyword);
    if (keyword.length === 0 || keyword.length > 79) {
        throw new Error('keyword length must be between 1 and 79');
    }
}
// eslint-disable-next-line no-control-regex
const latin1Regex = /^[\u0000-\u00FF]*$/;
function validateLatin1(text) {
    if (!latin1Regex.test(text)) {
        throw new Error('invalid latin1 text');
    }
}
function decodetEXt(text, buffer, length) {
    const keyword = readKeyword(buffer);
    text[keyword] = readLatin1(buffer, length - keyword.length - 1);
}
// https://www.w3.org/TR/png/#11keywords
function readKeyword(buffer) {
    buffer.mark();
    while (buffer.readByte() !== NULL) {
        /* advance */
    }
    const end = buffer.offset;
    buffer.reset();
    const keyword = latin1Decoder.decode(buffer.readBytes(end - buffer.offset - 1));
    // NULL
    buffer.skip(1);
    validateKeyword(keyword);
    return keyword;
}
function readLatin1(buffer, length) {
    return latin1Decoder.decode(buffer.readBytes(length));
}

const ColorType = {
    UNKNOWN: -1,
    GREYSCALE: 0,
    TRUECOLOUR: 2,
    INDEXED_COLOUR: 3,
    GREYSCALE_ALPHA: 4,
    TRUECOLOUR_ALPHA: 6,
};
const CompressionMethod = {
    UNKNOWN: -1,
    DEFLATE: 0,
};
const FilterMethod = {
    UNKNOWN: -1,
    ADAPTIVE: 0,
};
const InterlaceMethod = {
    UNKNOWN: -1,
    NO_INTERLACE: 0,
    ADAM7: 1,
};
const DisposeOpType = {
    NONE: 0,
    BACKGROUND: 1,
    PREVIOUS: 2,
};
const BlendOpType = {
    SOURCE: 0,
    OVER: 1,
};

class PngDecoder extends IOBuffer {
    _checkCrc;
    _inflator;
    _png;
    _apng;
    _end;
    _hasPalette;
    _palette;
    _hasTransparency;
    _transparency;
    _compressionMethod;
    _filterMethod;
    _interlaceMethod;
    _colorType;
    _isAnimated;
    _numberOfFrames;
    _numberOfPlays;
    _frames;
    _writingDataChunks;
    constructor(data, options = {}) {
        super(data);
        const { checkCrc = false } = options;
        this._checkCrc = checkCrc;
        this._inflator = new Inflate_1();
        this._png = {
            width: -1,
            height: -1,
            channels: -1,
            data: new Uint8Array(0),
            depth: 1,
            text: {},
        };
        this._apng = {
            width: -1,
            height: -1,
            channels: -1,
            depth: 1,
            numberOfFrames: 1,
            numberOfPlays: 0,
            text: {},
            frames: [],
        };
        this._end = false;
        this._hasPalette = false;
        this._palette = [];
        this._hasTransparency = false;
        this._transparency = new Uint16Array(0);
        this._compressionMethod = CompressionMethod.UNKNOWN;
        this._filterMethod = FilterMethod.UNKNOWN;
        this._interlaceMethod = InterlaceMethod.UNKNOWN;
        this._colorType = ColorType.UNKNOWN;
        this._isAnimated = false;
        this._numberOfFrames = 1;
        this._numberOfPlays = 0;
        this._frames = [];
        this._writingDataChunks = false;
        // PNG is always big endian
        // https://www.w3.org/TR/PNG/#7Integers-and-byte-order
        this.setBigEndian();
    }
    decode() {
        checkSignature(this);
        while (!this._end) {
            const length = this.readUint32();
            const type = this.readChars(4);
            this.decodeChunk(length, type);
        }
        this.decodeImage();
        return this._png;
    }
    decodeApng() {
        checkSignature(this);
        while (!this._end) {
            const length = this.readUint32();
            const type = this.readChars(4);
            this.decodeApngChunk(length, type);
        }
        this.decodeApngImage();
        return this._apng;
    }
    // https://www.w3.org/TR/PNG/#5Chunk-layout
    decodeChunk(length, type) {
        const offset = this.offset;
        switch (type) {
            // 11.2 Critical chunks
            case 'IHDR': // 11.2.2 IHDR Image header
                this.decodeIHDR();
                break;
            case 'PLTE': // 11.2.3 PLTE Palette
                this.decodePLTE(length);
                break;
            case 'IDAT': // 11.2.4 IDAT Image data
                this.decodeIDAT(length);
                break;
            case 'IEND': // 11.2.5 IEND Image trailer
                this._end = true;
                break;
            // 11.3 Ancillary chunks
            case 'tRNS': // 11.3.2.1 tRNS Transparency
                this.decodetRNS(length);
                break;
            case 'iCCP': // 11.3.3.3 iCCP Embedded ICC profile
                this.decodeiCCP(length);
                break;
            case textChunkName: // 11.3.4.3 tEXt Textual data
                decodetEXt(this._png.text, this, length);
                break;
            case 'pHYs': // 11.3.5.3 pHYs Physical pixel dimensions
                this.decodepHYs();
                break;
            default:
                this.skip(length);
                break;
        }
        if (this.offset - offset !== length) {
            throw new Error(`Length mismatch while decoding chunk ${type}`);
        }
        if (this._checkCrc) {
            checkCrc(this, length + 4, type);
        }
        else {
            this.skip(4);
        }
    }
    decodeApngChunk(length, type) {
        const offset = this.offset;
        if (type !== 'fdAT' && type !== 'IDAT' && this._writingDataChunks) {
            this.pushDataToFrame();
        }
        switch (type) {
            case 'acTL':
                this.decodeACTL();
                break;
            case 'fcTL':
                this.decodeFCTL();
                break;
            case 'fdAT':
                this.decodeFDAT(length);
                break;
            default:
                this.decodeChunk(length, type);
                this.offset = offset + length;
                break;
        }
        if (this.offset - offset !== length) {
            throw new Error(`Length mismatch while decoding chunk ${type}`);
        }
        if (this._checkCrc) {
            checkCrc(this, length + 4, type);
        }
        else {
            this.skip(4);
        }
    }
    // https://www.w3.org/TR/PNG/#11IHDR
    decodeIHDR() {
        const image = this._png;
        image.width = this.readUint32();
        image.height = this.readUint32();
        image.depth = checkBitDepth(this.readUint8());
        const colorType = this.readUint8();
        this._colorType = colorType;
        let channels;
        switch (colorType) {
            case ColorType.GREYSCALE:
                channels = 1;
                break;
            case ColorType.TRUECOLOUR:
                channels = 3;
                break;
            case ColorType.INDEXED_COLOUR:
                channels = 1;
                break;
            case ColorType.GREYSCALE_ALPHA:
                channels = 2;
                break;
            case ColorType.TRUECOLOUR_ALPHA:
                channels = 4;
                break;
            // Kept for exhaustiveness.
            // eslint-disable-next-line unicorn/no-useless-switch-case
            case ColorType.UNKNOWN:
            default:
                throw new Error(`Unknown color type: ${colorType}`);
        }
        this._png.channels = channels;
        this._compressionMethod = this.readUint8();
        if (this._compressionMethod !== CompressionMethod.DEFLATE) {
            throw new Error(`Unsupported compression method: ${this._compressionMethod}`);
        }
        this._filterMethod = this.readUint8();
        this._interlaceMethod = this.readUint8();
    }
    decodeACTL() {
        this._numberOfFrames = this.readUint32();
        this._numberOfPlays = this.readUint32();
        this._isAnimated = true;
    }
    decodeFCTL() {
        const image = {
            sequenceNumber: this.readUint32(),
            width: this.readUint32(),
            height: this.readUint32(),
            xOffset: this.readUint32(),
            yOffset: this.readUint32(),
            delayNumber: this.readUint16(),
            delayDenominator: this.readUint16(),
            disposeOp: this.readUint8(),
            blendOp: this.readUint8(),
            data: new Uint8Array(0),
        };
        this._frames.push(image);
    }
    // https://www.w3.org/TR/PNG/#11PLTE
    decodePLTE(length) {
        if (length % 3 !== 0) {
            throw new RangeError(`PLTE field length must be a multiple of 3. Got ${length}`);
        }
        const l = length / 3;
        this._hasPalette = true;
        const palette = [];
        this._palette = palette;
        for (let i = 0; i < l; i++) {
            palette.push([this.readUint8(), this.readUint8(), this.readUint8()]);
        }
    }
    // https://www.w3.org/TR/PNG/#11IDAT
    decodeIDAT(length) {
        this._writingDataChunks = true;
        const dataLength = length;
        const dataOffset = this.offset + this.byteOffset;
        this._inflator.push(new Uint8Array(this.buffer, dataOffset, dataLength));
        if (this._inflator.err) {
            throw new Error(`Error while decompressing the data: ${this._inflator.err}`);
        }
        this.skip(length);
    }
    decodeFDAT(length) {
        this._writingDataChunks = true;
        let dataLength = length;
        let dataOffset = this.offset + this.byteOffset;
        dataOffset += 4;
        dataLength -= 4;
        this._inflator.push(new Uint8Array(this.buffer, dataOffset, dataLength));
        if (this._inflator.err) {
            throw new Error(`Error while decompressing the data: ${this._inflator.err}`);
        }
        this.skip(length);
    }
    // https://www.w3.org/TR/PNG/#11tRNS
    decodetRNS(length) {
        switch (this._colorType) {
            case ColorType.GREYSCALE:
            case ColorType.TRUECOLOUR: {
                if (length % 2 !== 0) {
                    throw new RangeError(`tRNS chunk length must be a multiple of 2. Got ${length}`);
                }
                if (length / 2 > this._png.width * this._png.height) {
                    throw new Error(`tRNS chunk contains more alpha values than there are pixels (${length / 2} vs ${this._png.width * this._png.height})`);
                }
                this._hasTransparency = true;
                this._transparency = new Uint16Array(length / 2);
                for (let i = 0; i < length / 2; i++) {
                    this._transparency[i] = this.readUint16();
                }
                break;
            }
            case ColorType.INDEXED_COLOUR: {
                if (length > this._palette.length) {
                    throw new Error(`tRNS chunk contains more alpha values than there are palette colors (${length} vs ${this._palette.length})`);
                }
                let i = 0;
                for (; i < length; i++) {
                    const alpha = this.readByte();
                    this._palette[i].push(alpha);
                }
                for (; i < this._palette.length; i++) {
                    this._palette[i].push(255);
                }
                break;
            }
            // Kept for exhaustiveness.
            /* eslint-disable unicorn/no-useless-switch-case */
            case ColorType.UNKNOWN:
            case ColorType.GREYSCALE_ALPHA:
            case ColorType.TRUECOLOUR_ALPHA:
            default: {
                throw new Error(`tRNS chunk is not supported for color type ${this._colorType}`);
            }
            /* eslint-enable unicorn/no-useless-switch-case */
        }
    }
    // https://www.w3.org/TR/PNG/#11iCCP
    decodeiCCP(length) {
        const name = readKeyword(this);
        const compressionMethod = this.readUint8();
        if (compressionMethod !== CompressionMethod.DEFLATE) {
            throw new Error(`Unsupported iCCP compression method: ${compressionMethod}`);
        }
        const compressedProfile = this.readBytes(length - name.length - 2);
        this._png.iccEmbeddedProfile = {
            name,
            profile: inflate_1(compressedProfile),
        };
    }
    // https://www.w3.org/TR/PNG/#11pHYs
    decodepHYs() {
        const ppuX = this.readUint32();
        const ppuY = this.readUint32();
        const unitSpecifier = this.readByte();
        this._png.resolution = { x: ppuX, y: ppuY, unit: unitSpecifier };
    }
    decodeApngImage() {
        this._apng.width = this._png.width;
        this._apng.height = this._png.height;
        this._apng.channels = this._png.channels;
        this._apng.depth = this._png.depth;
        this._apng.numberOfFrames = this._numberOfFrames;
        this._apng.numberOfPlays = this._numberOfPlays;
        this._apng.text = this._png.text;
        this._apng.resolution = this._png.resolution;
        for (let i = 0; i < this._numberOfFrames; i++) {
            const newFrame = {
                sequenceNumber: this._frames[i].sequenceNumber,
                delayNumber: this._frames[i].delayNumber,
                delayDenominator: this._frames[i].delayDenominator,
                data: this._apng.depth === 8
                    ? new Uint8Array(this._apng.width * this._apng.height * this._apng.channels)
                    : new Uint16Array(this._apng.width * this._apng.height * this._apng.channels),
            };
            const frame = this._frames.at(i);
            if (frame) {
                frame.data = decodeInterlaceNull({
                    data: frame.data,
                    width: frame.width,
                    height: frame.height,
                    channels: this._apng.channels,
                    depth: this._apng.depth,
                });
                if (this._hasPalette) {
                    this._apng.palette = this._palette;
                }
                if (this._hasTransparency) {
                    this._apng.transparency = this._transparency;
                }
                if (i === 0 ||
                    (frame.xOffset === 0 &&
                        frame.yOffset === 0 &&
                        frame.width === this._png.width &&
                        frame.height === this._png.height)) {
                    newFrame.data = frame.data;
                }
                else {
                    const prevFrame = this._apng.frames.at(i - 1);
                    this.disposeFrame(frame, prevFrame, newFrame);
                    this.addFrameDataToCanvas(newFrame, frame);
                }
                this._apng.frames.push(newFrame);
            }
        }
        return this._apng;
    }
    disposeFrame(frame, prevFrame, imageFrame) {
        switch (frame.disposeOp) {
            case DisposeOpType.NONE:
                break;
            case DisposeOpType.BACKGROUND:
                for (let row = 0; row < this._png.height; row++) {
                    for (let col = 0; col < this._png.width; col++) {
                        const index = (row * frame.width + col) * this._png.channels;
                        for (let channel = 0; channel < this._png.channels; channel++) {
                            imageFrame.data[index + channel] = 0;
                        }
                    }
                }
                break;
            case DisposeOpType.PREVIOUS:
                imageFrame.data.set(prevFrame.data);
                break;
            default:
                throw new Error('Unknown disposeOp');
        }
    }
    addFrameDataToCanvas(imageFrame, frame) {
        const maxValue = 1 << this._png.depth;
        const calculatePixelIndices = (row, col) => {
            const index = ((row + frame.yOffset) * this._png.width + frame.xOffset + col) *
                this._png.channels;
            const frameIndex = (row * frame.width + col) * this._png.channels;
            return { index, frameIndex };
        };
        switch (frame.blendOp) {
            case BlendOpType.SOURCE:
                for (let row = 0; row < frame.height; row++) {
                    for (let col = 0; col < frame.width; col++) {
                        const { index, frameIndex } = calculatePixelIndices(row, col);
                        for (let channel = 0; channel < this._png.channels; channel++) {
                            imageFrame.data[index + channel] =
                                frame.data[frameIndex + channel];
                        }
                    }
                }
                break;
            // https://www.w3.org/TR/png-3/#13Alpha-channel-processing
            case BlendOpType.OVER:
                for (let row = 0; row < frame.height; row++) {
                    for (let col = 0; col < frame.width; col++) {
                        const { index, frameIndex } = calculatePixelIndices(row, col);
                        for (let channel = 0; channel < this._png.channels; channel++) {
                            const sourceAlpha = frame.data[frameIndex + this._png.channels - 1] / maxValue;
                            const foregroundValue = channel % (this._png.channels - 1) === 0
                                ? 1
                                : frame.data[frameIndex + channel];
                            const value = Math.floor(sourceAlpha * foregroundValue +
                                (1 - sourceAlpha) * imageFrame.data[index + channel]);
                            imageFrame.data[index + channel] += value;
                        }
                    }
                }
                break;
            default:
                throw new Error('Unknown blendOp');
        }
    }
    decodeImage() {
        if (this._inflator.err) {
            throw new Error(`Error while decompressing the data: ${this._inflator.err}`);
        }
        const data = this._isAnimated
            ? (this._frames?.at(0)).data
            : this._inflator.result;
        if (this._filterMethod !== FilterMethod.ADAPTIVE) {
            throw new Error(`Filter method ${this._filterMethod} not supported`);
        }
        if (this._interlaceMethod === InterlaceMethod.NO_INTERLACE) {
            this._png.data = decodeInterlaceNull({
                data: data,
                width: this._png.width,
                height: this._png.height,
                channels: this._png.channels,
                depth: this._png.depth,
            });
        }
        else if (this._interlaceMethod === InterlaceMethod.ADAM7) {
            this._png.data = decodeInterlaceAdam7({
                data: data,
                width: this._png.width,
                height: this._png.height,
                channels: this._png.channels,
                depth: this._png.depth,
            });
        }
        else {
            throw new Error(`Interlace method ${this._interlaceMethod} not supported`);
        }
        if (this._hasPalette) {
            this._png.palette = this._palette;
        }
        if (this._hasTransparency) {
            this._png.transparency = this._transparency;
        }
    }
    pushDataToFrame() {
        const result = this._inflator.result;
        const lastFrame = this._frames.at(-1);
        if (lastFrame) {
            lastFrame.data = result;
        }
        else {
            this._frames.push({
                sequenceNumber: 0,
                width: this._png.width,
                height: this._png.height,
                xOffset: 0,
                yOffset: 0,
                delayNumber: 0,
                delayDenominator: 0,
                disposeOp: DisposeOpType.NONE,
                blendOp: BlendOpType.SOURCE,
                data: result,
            });
        }
        this._inflator = new Inflate_1();
        this._writingDataChunks = false;
    }
}
function checkBitDepth(value) {
    if (value !== 1 &&
        value !== 2 &&
        value !== 4 &&
        value !== 8 &&
        value !== 16) {
        throw new Error(`invalid bit depth: ${value}`);
    }
    return value;
}

var ResolutionUnitSpecifier;
(function (ResolutionUnitSpecifier) {
    /**
     * Unit is unknown
     */
    ResolutionUnitSpecifier[ResolutionUnitSpecifier["UNKNOWN"] = 0] = "UNKNOWN";
    /**
     * Unit is the metre
     */
    ResolutionUnitSpecifier[ResolutionUnitSpecifier["METRE"] = 1] = "METRE";
})(ResolutionUnitSpecifier || (ResolutionUnitSpecifier = {}));

function decodePng(data, options) {
    const decoder = new PngDecoder(data, options);
    return decoder.decode();
}

/**
 * @license
 *
 * Copyright (c) 2014 James Robb, https://github.com/jamesbrobb
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * ====================================================================
 */

/*
 * @see http://www.w3.org/TR/PNG-Chunks.html
 *
 Color    Allowed      Interpretation
 Type     Bit Depths

   0       1,2,4,8,16  Each pixel is a grayscale sample.

   2       8,16        Each pixel is an R,G,B triple.

   3       1,2,4,8     Each pixel is a palette index;
                       a PLTE chunk must appear.

   4       8,16        Each pixel is a grayscale sample,
                       followed by an alpha sample.

   6       8,16        Each pixel is an R,G,B triple,
                       followed by an alpha sample.
*/

/*
 * @name processPNG
 * Entry point: process a PNG and return image dict and metadata for jsPDF
 */
jsPDF.API.processPNG = function(imageData, index, alias, compression) {
  if (this.__addimage__.isArrayBuffer(imageData)) {
    imageData = new Uint8Array(imageData);
  }
  if (!this.__addimage__.isArrayBufferView(imageData)) {
    return;
  }

  const decodedPng = decodePng(imageData, { checkCrc: true });
  const {
    width,
    height,
    channels,
    palette: decodedPalette,
    depth: bitsPerComponent
  } = decodedPng;

  let result;
  if (decodedPalette && channels === 1) {
    result = processIndexedPNG(decodedPng);
  } else if (channels === 2 || channels === 4) {
    result = processAlphaPNG(decodedPng);
  } else {
    result = processOpaquePNG(decodedPng);
  }

  const {
    colorSpace,
    colorsPerPixel,
    sMaskBitsPerComponent,
    colorBytes,
    alphaBytes,
    needSMask,
    palette,
    mask
  } = result;

  let predictor = null;

  let filter, decodeParameters, sMask;
  if (canCompress(compression)) {
    predictor = getPredictorFromCompression(compression);
    filter = this.decode.FLATE_DECODE;
    decodeParameters = `/Predictor ${predictor} /Colors ${colorsPerPixel} /BitsPerComponent ${bitsPerComponent} /Columns ${width}`;

    const rowByteLength = Math.ceil(
      (width * colorsPerPixel * bitsPerComponent) / 8
    );

    imageData = compressBytes(
      colorBytes,
      rowByteLength,
      colorsPerPixel,
      bitsPerComponent,
      compression
    );
    if (needSMask) {
      const sMaskRowByteLength = Math.ceil((width * sMaskBitsPerComponent) / 8);
      sMask = compressBytes(
        alphaBytes,
        sMaskRowByteLength,
        1,
        sMaskBitsPerComponent,
        compression
      );
    }
  } else {
    filter = undefined;
    decodeParameters = undefined;
    imageData = colorBytes;
    if (needSMask) sMask = alphaBytes;
  }

  if (
    this.__addimage__.isArrayBuffer(imageData) ||
    this.__addimage__.isArrayBufferView(imageData)
  ) {
    imageData = this.__addimage__.arrayBufferToBinaryString(imageData);
  }

  if (
    (sMask && this.__addimage__.isArrayBuffer(sMask)) ||
    this.__addimage__.isArrayBufferView(sMask)
  ) {
    sMask = this.__addimage__.arrayBufferToBinaryString(sMask);
  }

  return {
    alias,
    data: imageData,
    index,
    filter,
    decodeParameters,
    transparency: mask,
    palette,
    sMask,
    predictor,
    width,
    height,
    bitsPerComponent,
    sMaskBitsPerComponent,
    colorSpace
  };
};

/*
   * PNG filter method types
   *
   * @see http://www.w3.org/TR/PNG-Filters.html
   * @see http://www.libpng.org/pub/png/book/chapter09.html
   *
   * This is what the value 'Predictor' in decode params relates to
   *
   * 15 is "optimal prediction", which means the prediction algorithm can change from line to line.
   * In that case, you actually have to read the first byte off each line for the prediction algorthim (which should be 0-4, corresponding to PDF 10-14) and select the appropriate unprediction algorithm based on that byte.
   *
     0       None
     1       Sub
     2       Up
     3       Average
     4       Paeth
   */

function canCompress(value) {
  return value !== jsPDF.API.image_compression.NONE && hasCompressionJS();
}

function hasCompressionJS() {
  return typeof zlibSync === "function";
}
function compressBytes(
  bytes,
  lineByteLength,
  channels,
  bitsPerComponent,
  compression
) {
  let level = 4;
  let filter_method = filterUp;

  switch (compression) {
    case jsPDF.API.image_compression.FAST:
      level = 1;
      filter_method = filterSub;
      break;

    case jsPDF.API.image_compression.MEDIUM:
      level = 6;
      filter_method = filterAverage;
      break;

    case jsPDF.API.image_compression.SLOW:
      level = 9;
      filter_method = filterPaeth;
      break;
  }

  const bytesPerPixel = Math.ceil((channels * bitsPerComponent) / 8);
  bytes = applyPngFilterMethod(
    bytes,
    lineByteLength,
    bytesPerPixel,
    filter_method
  );
  const dat = zlibSync(bytes, { level: level });
  return jsPDF.API.__addimage__.arrayBufferToBinaryString(dat);
}

function applyPngFilterMethod(
  bytes,
  lineByteLength,
  bytesPerPixel,
  filter_method
) {
  const lines = bytes.length / lineByteLength;
  const result = new Uint8Array(bytes.length + lines);
  const filter_methods = getFilterMethods();
  let prevLine;

  for (let i = 0; i < lines; i += 1) {
    const offset = i * lineByteLength;
    const line = bytes.subarray(offset, offset + lineByteLength);

    if (filter_method) {
      result.set(filter_method(line, bytesPerPixel, prevLine), offset + i);
    } else {
      const len = filter_methods.length;
      const results = [];

      for (let j = 0; j < len; j += 1) {
        results[j] = filter_methods[j](line, bytesPerPixel, prevLine);
      }

      const ind = getIndexOfSmallestSum(results.concat());

      result.set(results[ind], offset + i);
    }

    prevLine = line;
  }

  return result;
}

function filterNone(line) {
  /*const result = new Uint8Array(line.length + 1);
    result[0] = 0;
    result.set(line, 1);*/

  const result = Array.apply([], line);
  result.unshift(0);

  return result;
}

function filterSub(line, colorsPerPixel) {
  const len = line.length;
  const result = [];

  result[0] = 1;

  for (let i = 0; i < len; i += 1) {
    const left = line[i - colorsPerPixel] || 0;
    result[i + 1] = (line[i] - left + 0x0100) & 0xff;
  }

  return result;
}

function filterUp(line, colorsPerPixel, prevLine) {
  const len = line.length;
  const result = [];

  result[0] = 2;

  for (let i = 0; i < len; i += 1) {
    const up = (prevLine && prevLine[i]) || 0;
    result[i + 1] = (line[i] - up + 0x0100) & 0xff;
  }

  return result;
}

function filterAverage(line, colorsPerPixel, prevLine) {
  const len = line.length;
  const result = [];

  result[0] = 3;

  for (let i = 0; i < len; i += 1) {
    const left = line[i - colorsPerPixel] || 0;
    const up = (prevLine && prevLine[i]) || 0;
    result[i + 1] = (line[i] + 0x0100 - ((left + up) >>> 1)) & 0xff;
  }

  return result;
}

function filterPaeth(line, colorsPerPixel, prevLine) {
  const len = line.length;
  const result = [];

  result[0] = 4;

  for (let i = 0; i < len; i += 1) {
    const left = line[i - colorsPerPixel] || 0;
    const up = (prevLine && prevLine[i]) || 0;
    const upLeft = (prevLine && prevLine[i - colorsPerPixel]) || 0;
    const paeth = paethPredictor(left, up, upLeft);
    result[i + 1] = (line[i] - paeth + 0x0100) & 0xff;
  }

  return result;
}

function paethPredictor(left, up, upLeft) {
  if (left === up && up === upLeft) {
    return left;
  }
  const pLeft = Math.abs(up - upLeft),
    pUp = Math.abs(left - upLeft),
    pUpLeft = Math.abs(left + up - upLeft - upLeft);
  return pLeft <= pUp && pLeft <= pUpLeft ? left : pUp <= pUpLeft ? up : upLeft;
}

function getFilterMethods() {
  return [filterNone, filterSub, filterUp, filterAverage, filterPaeth];
}

function getIndexOfSmallestSum(arrays) {
  const sum = arrays.map(function(value) {
    return value.reduce(function(pv, cv) {
      return pv + Math.abs(cv);
    }, 0);
  });
  return sum.indexOf(Math.min.apply(null, sum));
}

function getPredictorFromCompression(compression) {
  let predictor;
  switch (compression) {
    case jsPDF.API.image_compression.FAST:
      predictor = 11;
      break;

    case jsPDF.API.image_compression.MEDIUM:
      predictor = 13;
      break;

    case jsPDF.API.image_compression.SLOW:
      predictor = 14;
      break;

    default:
      predictor = 12;
      break;
  }
  return predictor;
}

// Extracted helper for Indexed PNGs (palette-based)
function processIndexedPNG(decodedPng) {
  const { width, height, data, palette: decodedPalette, depth } = decodedPng;
  let needSMask = false;
  let palette = [];
  let mask = [];
  let alphaBytes = undefined;
  let hasSemiTransparency = false;

  const maxMaskLength = 1;
  let maskLength = 0;

  for (let i = 0; i < decodedPalette.length; i++) {
    const [r, g, b, a] = decodedPalette[i];
    palette.push(r, g, b);
    if (a != null) {
      if (a === 0) {
        maskLength++;
        if (mask.length < maxMaskLength) {
          mask.push(i);
        }
      } else if (a < 255) {
        hasSemiTransparency = true;
      }
    }
  }

  if (hasSemiTransparency || maskLength > maxMaskLength) {
    needSMask = true;
    mask = undefined;

    const totalPixels = width * height;
    // per PNG spec, palettes always use 8 bits per component
    alphaBytes = new Uint8Array(totalPixels);
    const dataView = new DataView(data.buffer);
    for (let p = 0; p < totalPixels; p++) {
      const paletteIndex = readSample(dataView, p, depth);
      const [, , , alpha] = decodedPalette[paletteIndex];
      alphaBytes[p] = alpha;
    }
  } else if (maskLength === 0) {
    mask = undefined;
  }

  return {
    colorSpace: "Indexed",
    colorsPerPixel: 1,
    sMaskBitsPerComponent: needSMask ? 8 : undefined,
    colorBytes: data,
    alphaBytes,
    needSMask,
    palette,
    mask
  };
}

/*
 * Splits color and alpha values into separate buffers
 */
function processAlphaPNG(decodedPng) {
  const { data, width, height, channels, depth } = decodedPng;

  const colorSpace = channels === 2 ? "DeviceGray" : "DeviceRGB";
  const colorsPerPixel = channels - 1;

  const totalPixels = width * height;
  const colorChannels = colorsPerPixel; // 1 for Gray, 3 for RGB
  const alphaChannels = 1;
  const totalColorSamples = totalPixels * colorChannels;
  const totalAlphaSamples = totalPixels * alphaChannels;

  const colorByteLen = Math.ceil((totalColorSamples * depth) / 8);
  const alphaByteLen = Math.ceil((totalAlphaSamples * depth) / 8);
  const colorBytes = new Uint8Array(colorByteLen);
  const alphaBytes = new Uint8Array(alphaByteLen);

  const dataView = new DataView(data.buffer);
  const colorView = new DataView(colorBytes.buffer);
  const alphaView = new DataView(alphaBytes.buffer);

  let needSMask = false;
  for (let p = 0; p < totalPixels; p++) {
    const pixelStartIndex = p * channels;
    for (let s = 0; s < colorChannels; s++) {
      const sampleIndex = pixelStartIndex + s;
      const colorValue = readSample(dataView, sampleIndex, depth);
      writeSample(colorView, colorValue, p * colorChannels + s, depth);
    }
    const sampleIndex = pixelStartIndex + colorChannels;
    const alphaValue = readSample(dataView, sampleIndex, depth);
    if (alphaValue < (1 << depth) - 1) {
      needSMask = true;
    }
    writeSample(alphaView, alphaValue, p * alphaChannels, depth);
  }

  return {
    colorSpace,
    colorsPerPixel,
    sMaskBitsPerComponent: needSMask ? depth : undefined,
    colorBytes,
    alphaBytes,
    needSMask
  };
}

function processOpaquePNG(decodedPng) {
  const { data, channels } = decodedPng;
  const colorSpace = channels === 1 ? "DeviceGray" : "DeviceRGB";
  const colorsPerPixel = colorSpace === "DeviceGray" ? 1 : 3;

  let colorBytes;
  if (data instanceof Uint16Array) {
    colorBytes = convertUint16ArrayToUint8Array(data);
  } else {
    colorBytes = data;
  }

  return { colorSpace, colorsPerPixel, colorBytes, needSMask: false };
}

function convertUint16ArrayToUint8Array(data) {
  // PNG/PDF expect MSB-first byte order. Since EcmaScript does not specify
  // the byte order of Uint16Array, we need to use a DataView to ensure the
  // correct byte order.
  const sampleCount = data.length;
  const out = new Uint8Array(sampleCount * 2);
  const outView = new DataView(out.buffer, out.byteOffset, out.byteLength);

  for (let i = 0; i < sampleCount; i++) {
    outView.setUint16(i * 2, data[i], false);
  }
  return out;
}

function readSample(view, sampleIndex, depth) {
  const bitIndex = sampleIndex * depth;
  const byteIndex = Math.floor(bitIndex / 8);
  const bitOffset = 16 - (bitIndex - byteIndex * 8 + depth);
  const bitMask = (1 << depth) - 1;
  const word = safeGetUint16(view, byteIndex);
  return (word >> bitOffset) & bitMask;
}

function writeSample(view, value, sampleIndex, depth) {
  const bitIndex = sampleIndex * depth;
  const byteIndex = Math.floor(bitIndex / 8);
  const bitOffset = 16 - (bitIndex - byteIndex * 8 + depth);
  const bitMask = (1 << depth) - 1;
  const writeValue = (value & bitMask) << bitOffset;
  const word =
    safeGetUint16(view, byteIndex) & ~(bitMask << bitOffset) & 0xffff;
  safeSetUint16(view, byteIndex, word | writeValue);
}

function safeGetUint16(view, byteIndex) {
  if (byteIndex + 1 < view.byteLength) {
    return view.getUint16(byteIndex, false);
  }
  const b0 = view.getUint8(byteIndex);
  return b0 << 8;
}

function safeSetUint16(view, byteIndex, value) {
  if (byteIndex + 1 < view.byteLength) {
    view.setUint16(byteIndex, value, false);
    return;
  }
  const byteToWrite = (value >> 8) & 0xff;
  view.setUint8(byteIndex, byteToWrite);
}

/**
 * @license
 *
 * Copyright (c) 2021 Antti Palola, https://github.com/Pantura
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * ====================================================================
 */

/**
 * jsPDF RGBA array PlugIn
 * @name rgba_support
 * @module
 */
(function(jsPDFAPI) {

  /**
   * @name processRGBA
   * @function
   *
   * Process RGBA Array. This is a one-dimension array with pixel data [red, green, blue, alpha, red, green, ...].
   * RGBA array data can be obtained from DOM canvas getImageData.
   * @ignore
   */
  jsPDFAPI.processRGBA = function(imageData, index, alias) {

    var imagePixels = imageData.data;
    var length = imagePixels.length;
    // jsPDF takes alpha data separately so extract that.
    var rgbOut = new Uint8Array((length / 4) * 3);
    var alphaOut = new Uint8Array(length / 4);
    var outIndex = 0;
    var alphaIndex = 0;

    for (var i = 0; i < length; i += 4) {
      var r = imagePixels[i];
      var g = imagePixels[i + 1];
      var b = imagePixels[i + 2];
      var alpha = imagePixels[i + 3];
      rgbOut[outIndex++] = r;
      rgbOut[outIndex++] = g;
      rgbOut[outIndex++] = b;
      alphaOut[alphaIndex++] = alpha;
    }

    var rgbData = this.__addimage__.arrayBufferToBinaryString(rgbOut);
    var alphaData = this.__addimage__.arrayBufferToBinaryString(alphaOut);

    return {
      alpha: alphaData,
      data: rgbData,
      index: index,
      alias: alias,
      colorSpace: "DeviceRGB",
      bitsPerComponent: 8,
      width: imageData.width,
      height: imageData.height
    };
  };
})(jsPDF.API);

/**
 * @license
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 */

/**
 * jsPDF setLanguage Plugin
 *
 * @name setLanguage
 * @module
 */
(function(jsPDFAPI) {

  /**
   * Add Language Tag to the generated PDF
   *
   * @name setLanguage
   * @function
   * @param {string} langCode The Language code as ISO-639-1 (e.g. 'en') or as country language code (e.g. 'en-GB').
   * @returns {jsPDF}
   * @example
   * var doc = new jsPDF()
   * doc.text(10, 10, 'This is a test')
   * doc.setLanguage("en-US")
   * doc.save('english.pdf')
   */
  jsPDFAPI.setLanguage = function(langCode) {

    var langCodes = {
      af: "Afrikaans",
      sq: "Albanian",
      ar: "Arabic (Standard)",
      "ar-DZ": "Arabic (Algeria)",
      "ar-BH": "Arabic (Bahrain)",
      "ar-EG": "Arabic (Egypt)",
      "ar-IQ": "Arabic (Iraq)",
      "ar-JO": "Arabic (Jordan)",
      "ar-KW": "Arabic (Kuwait)",
      "ar-LB": "Arabic (Lebanon)",
      "ar-LY": "Arabic (Libya)",
      "ar-MA": "Arabic (Morocco)",
      "ar-OM": "Arabic (Oman)",
      "ar-QA": "Arabic (Qatar)",
      "ar-SA": "Arabic (Saudi Arabia)",
      "ar-SY": "Arabic (Syria)",
      "ar-TN": "Arabic (Tunisia)",
      "ar-AE": "Arabic (U.A.E.)",
      "ar-YE": "Arabic (Yemen)",
      an: "Aragonese",
      hy: "Armenian",
      as: "Assamese",
      ast: "Asturian",
      az: "Azerbaijani",
      eu: "Basque",
      be: "Belarusian",
      bn: "Bengali",
      bs: "Bosnian",
      br: "Breton",
      bg: "Bulgarian",
      my: "Burmese",
      ca: "Catalan",
      ch: "Chamorro",
      ce: "Chechen",
      zh: "Chinese",
      "zh-HK": "Chinese (Hong Kong)",
      "zh-CN": "Chinese (PRC)",
      "zh-SG": "Chinese (Singapore)",
      "zh-TW": "Chinese (Taiwan)",
      cv: "Chuvash",
      co: "Corsican",
      cr: "Cree",
      hr: "Croatian",
      cs: "Czech",
      da: "Danish",
      nl: "Dutch (Standard)",
      "nl-BE": "Dutch (Belgian)",
      en: "English",
      "en-AU": "English (Australia)",
      "en-BZ": "English (Belize)",
      "en-CA": "English (Canada)",
      "en-IE": "English (Ireland)",
      "en-JM": "English (Jamaica)",
      "en-NZ": "English (New Zealand)",
      "en-PH": "English (Philippines)",
      "en-ZA": "English (South Africa)",
      "en-TT": "English (Trinidad & Tobago)",
      "en-GB": "English (United Kingdom)",
      "en-US": "English (United States)",
      "en-ZW": "English (Zimbabwe)",
      eo: "Esperanto",
      et: "Estonian",
      fo: "Faeroese",
      fj: "Fijian",
      fi: "Finnish",
      fr: "French (Standard)",
      "fr-BE": "French (Belgium)",
      "fr-CA": "French (Canada)",
      "fr-FR": "French (France)",
      "fr-LU": "French (Luxembourg)",
      "fr-MC": "French (Monaco)",
      "fr-CH": "French (Switzerland)",
      fy: "Frisian",
      fur: "Friulian",
      gd: "Gaelic (Scots)",
      "gd-IE": "Gaelic (Irish)",
      gl: "Galacian",
      ka: "Georgian",
      de: "German (Standard)",
      "de-AT": "German (Austria)",
      "de-DE": "German (Germany)",
      "de-LI": "German (Liechtenstein)",
      "de-LU": "German (Luxembourg)",
      "de-CH": "German (Switzerland)",
      el: "Greek",
      gu: "Gujurati",
      ht: "Haitian",
      he: "Hebrew",
      hi: "Hindi",
      hu: "Hungarian",
      is: "Icelandic",
      id: "Indonesian",
      iu: "Inuktitut",
      ga: "Irish",
      it: "Italian (Standard)",
      "it-CH": "Italian (Switzerland)",
      ja: "Japanese",
      kn: "Kannada",
      ks: "Kashmiri",
      kk: "Kazakh",
      km: "Khmer",
      ky: "Kirghiz",
      tlh: "Klingon",
      ko: "Korean",
      "ko-KP": "Korean (North Korea)",
      "ko-KR": "Korean (South Korea)",
      la: "Latin",
      lv: "Latvian",
      lt: "Lithuanian",
      lb: "Luxembourgish",
      mk: "North Macedonia",
      ms: "Malay",
      ml: "Malayalam",
      mt: "Maltese",
      mi: "Maori",
      mr: "Marathi",
      mo: "Moldavian",
      nv: "Navajo",
      ng: "Ndonga",
      ne: "Nepali",
      no: "Norwegian",
      nb: "Norwegian (Bokmal)",
      nn: "Norwegian (Nynorsk)",
      oc: "Occitan",
      or: "Oriya",
      om: "Oromo",
      fa: "Persian",
      "fa-IR": "Persian/Iran",
      pl: "Polish",
      pt: "Portuguese",
      "pt-BR": "Portuguese (Brazil)",
      pa: "Punjabi",
      "pa-IN": "Punjabi (India)",
      "pa-PK": "Punjabi (Pakistan)",
      qu: "Quechua",
      rm: "Rhaeto-Romanic",
      ro: "Romanian",
      "ro-MO": "Romanian (Moldavia)",
      ru: "Russian",
      "ru-MO": "Russian (Moldavia)",
      sz: "Sami (Lappish)",
      sg: "Sango",
      sa: "Sanskrit",
      sc: "Sardinian",
      sd: "Sindhi",
      si: "Singhalese",
      sr: "Serbian",
      sk: "Slovak",
      sl: "Slovenian",
      so: "Somani",
      sb: "Sorbian",
      es: "Spanish",
      "es-AR": "Spanish (Argentina)",
      "es-BO": "Spanish (Bolivia)",
      "es-CL": "Spanish (Chile)",
      "es-CO": "Spanish (Colombia)",
      "es-CR": "Spanish (Costa Rica)",
      "es-DO": "Spanish (Dominican Republic)",
      "es-EC": "Spanish (Ecuador)",
      "es-SV": "Spanish (El Salvador)",
      "es-GT": "Spanish (Guatemala)",
      "es-HN": "Spanish (Honduras)",
      "es-MX": "Spanish (Mexico)",
      "es-NI": "Spanish (Nicaragua)",
      "es-PA": "Spanish (Panama)",
      "es-PY": "Spanish (Paraguay)",
      "es-PE": "Spanish (Peru)",
      "es-PR": "Spanish (Puerto Rico)",
      "es-ES": "Spanish (Spain)",
      "es-UY": "Spanish (Uruguay)",
      "es-VE": "Spanish (Venezuela)",
      sx: "Sutu",
      sw: "Swahili",
      sv: "Swedish",
      "sv-FI": "Swedish (Finland)",
      "sv-SV": "Swedish (Sweden)",
      ta: "Tamil",
      tt: "Tatar",
      te: "Teluga",
      th: "Thai",
      tig: "Tigre",
      ts: "Tsonga",
      tn: "Tswana",
      tr: "Turkish",
      tk: "Turkmen",
      uk: "Ukrainian",
      hsb: "Upper Sorbian",
      ur: "Urdu",
      ve: "Venda",
      vi: "Vietnamese",
      vo: "Volapuk",
      wa: "Walloon",
      cy: "Welsh",
      xh: "Xhosa",
      ji: "Yiddish",
      zu: "Zulu"
    };

    if (this.internal.languageSettings === undefined) {
      this.internal.languageSettings = {};
      this.internal.languageSettings.isSubscribed = false;
    }

    if (langCodes[langCode] !== undefined) {
      this.internal.languageSettings.languageCode = langCode;
      if (this.internal.languageSettings.isSubscribed === false) {
        this.internal.events.subscribe("putCatalog", function() {
          this.internal.write(
            "/Lang (" + this.internal.languageSettings.languageCode + ")"
          );
        });
        this.internal.languageSettings.isSubscribed = true;
      }
    }
    return this;
  };
})(jsPDF.API);

/** @license
 * MIT license.
 * Copyright (c) 2012 Willow Systems Corporation, https://github.com/willowsystems
 *               2014 Diego Casorran, https://github.com/diegocr
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * ====================================================================
 */

/**
 * jsPDF split_text_to_size plugin
 *
 * @name split_text_to_size
 * @module
 */
(function(API) {
  /**
   * Returns an array of length matching length of the 'word' string, with each
   * cell occupied by the width of the char in that position.
   *
   * @name getCharWidthsArray
   * @function
   * @param {string} text
   * @param {Object} options
   * @returns {Array}
   */
  var getCharWidthsArray = (API.getCharWidthsArray = function(text, options) {
    options = options || {};

    var activeFont = options.font || this.internal.getFont();
    var fontSize = options.fontSize || this.internal.getFontSize();
    var charSpace = options.charSpace || this.internal.getCharSpace();

    var widths = options.widths
      ? options.widths
      : activeFont.metadata.Unicode.widths;
    var widthsFractionOf = widths.fof ? widths.fof : 1;
    var kerning = options.kerning
      ? options.kerning
      : activeFont.metadata.Unicode.kerning;
    var kerningFractionOf = kerning.fof ? kerning.fof : 1;
    var doKerning = options.doKerning === false ? false : true;
    var kerningValue = 0;

    var i;
    var length = text.length;
    var char_code;
    var prior_char_code = 0; //for kerning
    var default_char_width = widths[0] || widthsFractionOf;
    var output = [];

    for (i = 0; i < length; i++) {
      char_code = text.charCodeAt(i);

      if (typeof activeFont.metadata.widthOfString === "function") {
        output.push(
          (activeFont.metadata.widthOfGlyph(
            activeFont.metadata.characterToGlyph(char_code)
          ) +
            charSpace * (1000 / fontSize) || 0) / 1000
        );
      } else {
        if (
          doKerning &&
          typeof kerning[char_code] === "object" &&
          !isNaN(parseInt(kerning[char_code][prior_char_code], 10))
        ) {
          kerningValue =
            kerning[char_code][prior_char_code] / kerningFractionOf;
        } else {
          kerningValue = 0;
        }
        output.push(
          (widths[char_code] || default_char_width) / widthsFractionOf +
            kerningValue
        );
      }
      prior_char_code = char_code;
    }

    return output;
  });

  /**
   * Returns a widths of string in a given font, if the font size is set as 1 point.
   *
   * In other words, this is "proportional" value. For 1 unit of font size, the length
   * of the string will be that much.
   *
   * Multiply by font size to get actual width in *points*
   * Then divide by 72 to get inches or divide by (72/25.4) to get 'mm' etc.
   *
   * @name getStringUnitWidth
   * @public
   * @function
   * @param {string} text
   * @param {string} options
   * @returns {number} result
   */
  var getStringUnitWidth = (API.getStringUnitWidth = function(text, options) {
    options = options || {};

    var fontSize = options.fontSize || this.internal.getFontSize();
    var font = options.font || this.internal.getFont();
    var charSpace = options.charSpace || this.internal.getCharSpace();
    var result = 0;

    if (API.processArabic) {
      text = API.processArabic(text);
    }

    if (typeof font.metadata.widthOfString === "function") {
      result =
        font.metadata.widthOfString(text, fontSize, charSpace) / fontSize;
    } else {
      result = getCharWidthsArray
        .apply(this, arguments)
        .reduce(function(pv, cv) {
          return pv + cv;
        }, 0);
    }
    return result;
  });

  /**
  returns array of lines
  */
  var splitLongWord = function(word, widths_array, firstLineMaxLen, maxLen) {
    var answer = [];

    // 1st, chop off the piece that can fit on the hanging line.
    var i = 0,
      l = word.length,
      workingLen = 0;
    while (i !== l && workingLen + widths_array[i] < firstLineMaxLen) {
      workingLen += widths_array[i];
      i++;
    }
    // this is first line.
    answer.push(word.slice(0, i));

    // 2nd. Split the rest into maxLen pieces.
    var startOfLine = i;
    workingLen = 0;
    while (i !== l) {
      if (workingLen + widths_array[i] > maxLen) {
        answer.push(word.slice(startOfLine, i));
        workingLen = 0;
        startOfLine = i;
      }
      workingLen += widths_array[i];
      i++;
    }
    if (startOfLine !== i) {
      answer.push(word.slice(startOfLine, i));
    }

    return answer;
  };

  // Note, all sizing inputs for this function must be in "font measurement units"
  // By default, for PDF, it's "point".
  var splitParagraphIntoLines = function(text, maxlen, options) {
    // at this time works only on Western scripts, ones with space char
    // separating the words. Feel free to expand.

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

    var line = [],
      lines = [line],
      line_length = options.textIndent || 0,
      separator_length = 0,
      current_word_length = 0,
      word,
      widths_array,
      words = text.split(" "),
      spaceCharWidth = getCharWidthsArray.apply(this, [" ", options])[0],
      i,
      l,
      tmp,
      lineIndent;

    if (options.lineIndent === -1) {
      lineIndent = words[0].length + 2;
    } else {
      lineIndent = options.lineIndent || 0;
    }
    if (lineIndent) {
      var pad = Array(lineIndent).join(" "),
        wrds = [];
      words.map(function(wrd) {
        wrd = wrd.split(/\s*\n/);
        if (wrd.length > 1) {
          wrds = wrds.concat(
            wrd.map(function(wrd, idx) {
              return (idx && wrd.length ? "\n" : "") + wrd;
            })
          );
        } else {
          wrds.push(wrd[0]);
        }
      });
      words = wrds;
      lineIndent = getStringUnitWidth.apply(this, [pad, options]);
    }

    for (i = 0, l = words.length; i < l; i++) {
      var force = 0;

      word = words[i];
      if (lineIndent && word[0] == "\n") {
        word = word.substr(1);
        force = 1;
      }
      widths_array = getCharWidthsArray.apply(this, [word, options]);
      current_word_length = widths_array.reduce(function(pv, cv) {
        return pv + cv;
      }, 0);

      if (
        line_length + separator_length + current_word_length > maxlen ||
        force
      ) {
        if (current_word_length > maxlen) {
          // this happens when you have space-less long URLs for example.
          // we just chop these to size. We do NOT insert hiphens
          tmp = splitLongWord.apply(this, [
            word,
            widths_array,
            maxlen - (line_length + separator_length),
            maxlen
          ]);
          // first line we add to existing line object
          line.push(tmp.shift()); // it's ok to have extra space indicator there
          // last line we make into new line object
          line = [tmp.pop()];
          // lines in the middle we apped to lines object as whole lines
          while (tmp.length) {
            lines.push([tmp.shift()]); // single fragment occupies whole line
          }
          current_word_length = widths_array
            .slice(word.length - (line[0] ? line[0].length : 0))
            .reduce(function(pv, cv) {
              return pv + cv;
            }, 0);
        } else {
          // just put it on a new line
          line = [word];
        }

        // now we attach new line to lines
        lines.push(line);
        line_length = current_word_length + lineIndent;
        separator_length = spaceCharWidth;
      } else {
        line.push(word);

        line_length += separator_length + current_word_length;
        separator_length = spaceCharWidth;
      }
    }

    var postProcess;
    if (lineIndent) {
      postProcess = function(ln, idx) {
        return (idx ? pad : "") + ln.join(" ");
      };
    } else {
      postProcess = function(ln) {
        return ln.join(" ");
      };
    }

    return lines.map(postProcess);
  };

  /**
   * Splits a given string into an array of strings. Uses 'size' value
   * (in measurement units declared as default for the jsPDF instance)
   * and the font's "widths" and "Kerning" tables, where available, to
   * determine display length of a given string for a given font.
   *
   * We use character's 100% of unit size (height) as width when Width
   * table or other default width is not available.
   *
   * @name splitTextToSize
   * @public
   * @function
   * @param {string} text Unencoded, regular JavaScript (Unicode, UTF-16 / UCS-2) string.
   * @param {number} size Nominal number, measured in units default to this instance of jsPDF.
   * @param {Object} options Optional flags needed for chopper to do the right thing.
   * @returns {Array} array Array with strings chopped to size.
   */
  API.splitTextToSize = function(text, maxlen, options) {

    options = options || {};

    var fsize = options.fontSize || this.internal.getFontSize(),
      newOptions = function(options) {
        var widths = {
            0: 1
          },
          kerning = {};

        if (!options.widths || !options.kerning) {
          var f = this.internal.getFont(options.fontName, options.fontStyle),
            encoding = "Unicode";
          // NOT UTF8, NOT UTF16BE/LE, NOT UCS2BE/LE
          // Actual JavaScript-native String's 16bit char codes used.
          // no multi-byte logic here

          if (f.metadata[encoding]) {
            return {
              widths: f.metadata[encoding].widths || widths,
              kerning: f.metadata[encoding].kerning || kerning
            };
          } else {
            return {
              font: f.metadata,
              fontSize: this.internal.getFontSize(),
              charSpace: this.internal.getCharSpace()
            };
          }
        } else {
          return {
            widths: options.widths,
            kerning: options.kerning
          };
        }
      }.call(this, options);

    // first we split on end-of-line chars
    var paragraphs;
    if (Array.isArray(text)) {
      paragraphs = text;
    } else {
      paragraphs = String(text).split(/\r?\n/);
    }

    // now we convert size (max length of line) into "font size units"
    // at present time, the "font size unit" is always 'point'
    // 'proportional' means, "in proportion to font size"
    var fontUnit_maxLen = (1.0 * this.internal.scaleFactor * maxlen) / fsize;
    // at this time, fsize is always in "points" regardless of the default measurement unit of the doc.
    // this may change in the future?
    // until then, proportional_maxlen is likely to be in 'points'

    // If first line is to be indented (shorter or longer) than maxLen
    // we indicate that by using CSS-style "text-indent" option.
    // here it's in font units too (which is likely 'points')
    // it can be negative (which makes the first line longer than maxLen)
    newOptions.textIndent = options.textIndent
      ? (options.textIndent * 1.0 * this.internal.scaleFactor) / fsize
      : 0;
    newOptions.lineIndent = options.lineIndent;

    var i,
      l,
      output = [];
    for (i = 0, l = paragraphs.length; i < l; i++) {
      output = output.concat(
        splitParagraphIntoLines.apply(this, [
          paragraphs[i],
          fontUnit_maxLen,
          newOptions
        ])
      );
    }

    return output;
  };
})(jsPDF.API);

/** @license
 jsPDF standard_fonts_metrics plugin
 * Copyright (c) 2012 Willow Systems Corporation, https://github.com/willowsystems
 * MIT license.
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * ====================================================================
 */

/**
 * This file adds the standard font metrics to jsPDF.
 *
 * Font metrics data is reprocessed derivative of contents of
 * "Font Metrics for PDF Core 14 Fonts" package, which exhibits the following copyright and license:
 *
 * Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
 *
 * This file and the 14 PostScript(R) AFM files it accompanies may be used,
 * copied, and distributed for any purpose and without charge, with or without
 * modification, provided that all copyright notices are retained; that the AFM
 * files are not distributed without this file; that all modifications to this
 * file or any of the AFM files are prominently noted in the modified file(s);
 * and that this paragraph is not modified. Adobe Systems has no responsibility
 * or obligation to support the use of the AFM files.
 *
 * @name standard_fonts_metrics
 * @module
 */

(function(API) {
  API.__fontmetrics__ = API.__fontmetrics__ || {};

  var decoded = "0123456789abcdef",
    encoded = "klmnopqrstuvwxyz",
    mappingUncompress = {},
    mappingCompress = {};

  for (var i = 0; i < encoded.length; i++) {
    mappingUncompress[encoded[i]] = decoded[i];
    mappingCompress[decoded[i]] = encoded[i];
  }

  var hex = function(value) {
    return "0x" + parseInt(value, 10).toString(16);
  };

  var compress = (API.__fontmetrics__.compress = function(data) {
    var vals = ["{"];
    var value, keystring, valuestring, numberprefix;

    for (var key in data) {
      value = data[key];

      if (!isNaN(parseInt(key, 10))) {
        key = parseInt(key, 10);
        keystring = hex(key).slice(2);
        keystring =
          keystring.slice(0, -1) + mappingCompress[keystring.slice(-1)];
      } else {
        keystring = "'" + key + "'";
      }

      if (typeof value == "number") {
        if (value < 0) {
          valuestring = hex(value).slice(3);
          numberprefix = "-";
        } else {
          valuestring = hex(value).slice(2);
          numberprefix = "";
        }
        valuestring =
          numberprefix +
          valuestring.slice(0, -1) +
          mappingCompress[valuestring.slice(-1)];
      } else {
        if (typeof value === "object") {
          valuestring = compress(value);
        } else {
          throw new Error(
            "Don't know what to do with value type " + typeof value + "."
          );
        }
      }
      vals.push(keystring + valuestring);
    }
    vals.push("}");
    return vals.join("");
  });

  /**
   * Uncompresses data compressed into custom, base16-like format.
   *
   * @public
   * @function
   * @param
   * @returns {Type}
   */
  var uncompress = (API.__fontmetrics__.uncompress = function(data) {
    if (typeof data !== "string") {
      throw new Error("Invalid argument passed to uncompress.");
    }

    var output = {},
      sign = 1,
      stringparts, // undef. will be [] in string mode
      activeobject = output,
      parentchain = [],
      parent_key_pair,
      keyparts = "",
      valueparts = "",
      key, // undef. will be Truthy when Key is resolved.
      datalen = data.length - 1, // stripping ending }
      ch;

    for (var i = 1; i < datalen; i += 1) {
      // - { } ' are special.

      ch = data[i];

      if (ch == "'") {
        if (stringparts) {
          // end of string mode
          key = stringparts.join("");
          stringparts = undefined;
        } else {
          // start of string mode
          stringparts = [];
        }
      } else if (stringparts) {
        stringparts.push(ch);
      } else if (ch == "{") {
        // start of object
        parentchain.push([activeobject, key]);
        activeobject = {};
        key = undefined;
      } else if (ch == "}") {
        // end of object
        parent_key_pair = parentchain.pop();
        parent_key_pair[0][parent_key_pair[1]] = activeobject;
        key = undefined;
        activeobject = parent_key_pair[0];
      } else if (ch == "-") {
        sign = -1;
      } else {
        // must be number
        if (key === undefined) {
          if (mappingUncompress.hasOwnProperty(ch)) {
            keyparts += mappingUncompress[ch];
            key = parseInt(keyparts, 16) * sign;
            sign = 1;
            keyparts = "";
          } else {
            keyparts += ch;
          }
        } else {
          if (mappingUncompress.hasOwnProperty(ch)) {
            valueparts += mappingUncompress[ch];
            activeobject[key] = parseInt(valueparts, 16) * sign;
            sign = 1;
            key = undefined;
            valueparts = "";
          } else {
            valueparts += ch;
          }
        }
      }
    }
    return output;
  });

  // encoding = 'Unicode'
  // NOT UTF8, NOT UTF16BE/LE, NOT UCS2BE/LE. NO clever BOM behavior
  // Actual 16bit char codes used.
  // no multi-byte logic here

  // Unicode characters to WinAnsiEncoding:
  // {402: 131, 8211: 150, 8212: 151, 8216: 145, 8217: 146, 8218: 130, 8220: 147, 8221: 148, 8222: 132, 8224: 134, 8225: 135, 8226: 149, 8230: 133, 8364: 128, 8240:137, 8249: 139, 8250: 155, 710: 136, 8482: 153, 338: 140, 339: 156, 732: 152, 352: 138, 353: 154, 376: 159, 381: 142, 382: 158}
  // as you can see, all Unicode chars are outside of 0-255 range. No char code conflicts.
  // this means that you can give Win cp1252 encoded strings to jsPDF for rendering directly
  // as well as give strings with some (supported by these fonts) Unicode characters and
  // these will be mapped to win cp1252
  // for example, you can send char code (cp1252) 0x80 or (unicode) 0x20AC, getting "Euro" glyph displayed in both cases.

  var encodingBlock = {
    codePages: ["WinAnsiEncoding"],
    WinAnsiEncoding: uncompress(
      "{19m8n201n9q201o9r201s9l201t9m201u8m201w9n201x9o201y8o202k8q202l8r202m9p202q8p20aw8k203k8t203t8v203u9v2cq8s212m9t15m8w15n9w2dw9s16k8u16l9u17s9z17x8y17y9y}"
    )
  };
  var encodings = {
    Unicode: {
      Courier: encodingBlock,
      "Courier-Bold": encodingBlock,
      "Courier-BoldOblique": encodingBlock,
      "Courier-Oblique": encodingBlock,
      Helvetica: encodingBlock,
      "Helvetica-Bold": encodingBlock,
      "Helvetica-BoldOblique": encodingBlock,
      "Helvetica-Oblique": encodingBlock,
      "Times-Roman": encodingBlock,
      "Times-Bold": encodingBlock,
      "Times-BoldItalic": encodingBlock,
      "Times-Italic": encodingBlock
      //	, 'Symbol'
      //	, 'ZapfDingbats'
    }
  };

  var fontMetrics = {
    Unicode: {
      // all sizing numbers are n/fontMetricsFractionOf = one font size unit
      // this means that if fontMetricsFractionOf = 1000, and letter A's width is 476, it's
      // width is 476/1000 or 47.6% of its height (regardless of font size)
      // At this time this value applies to "widths" and "kerning" numbers.

      // char code 0 represents "default" (average) width - use it for chars missing in this table.
      // key 'fof' represents the "fontMetricsFractionOf" value

      "Courier-Oblique": uncompress(
        "{'widths'{k3w'fof'6o}'kerning'{'fof'-6o}}"
      ),
      "Times-BoldItalic": uncompress(
        "{'widths'{k3o2q4ycx2r201n3m201o6o201s2l201t2l201u2l201w3m201x3m201y3m2k1t2l2r202m2n2n3m2o3m2p5n202q6o2r1w2s2l2t2l2u3m2v3t2w1t2x2l2y1t2z1w3k3m3l3m3m3m3n3m3o3m3p3m3q3m3r3m3s3m203t2l203u2l3v2l3w3t3x3t3y3t3z3m4k5n4l4m4m4m4n4m4o4s4p4m4q4m4r4s4s4y4t2r4u3m4v4m4w3x4x5t4y4s4z4s5k3x5l4s5m4m5n3r5o3x5p4s5q4m5r5t5s4m5t3x5u3x5v2l5w1w5x2l5y3t5z3m6k2l6l3m6m3m6n2w6o3m6p2w6q2l6r3m6s3r6t1w6u1w6v3m6w1w6x4y6y3r6z3m7k3m7l3m7m2r7n2r7o1w7p3r7q2w7r4m7s3m7t2w7u2r7v2n7w1q7x2n7y3t202l3mcl4mal2ram3man3mao3map3mar3mas2lat4uau1uav3maw3way4uaz2lbk2sbl3t'fof'6obo2lbp3tbq3mbr1tbs2lbu1ybv3mbz3mck4m202k3mcm4mcn4mco4mcp4mcq5ycr4mcs4mct4mcu4mcv4mcw2r2m3rcy2rcz2rdl4sdm4sdn4sdo4sdp4sdq4sds4sdt4sdu4sdv4sdw4sdz3mek3mel3mem3men3meo3mep3meq4ser2wes2wet2weu2wev2wew1wex1wey1wez1wfl3rfm3mfn3mfo3mfp3mfq3mfr3tfs3mft3rfu3rfv3rfw3rfz2w203k6o212m6o2dw2l2cq2l3t3m3u2l17s3x19m3m}'kerning'{cl{4qu5kt5qt5rs17ss5ts}201s{201ss}201t{cks4lscmscnscoscpscls2wu2yu201ts}201x{2wu2yu}2k{201ts}2w{4qx5kx5ou5qx5rs17su5tu}2x{17su5tu5ou}2y{4qx5kx5ou5qx5rs17ss5ts}'fof'-6ofn{17sw5tw5ou5qw5rs}7t{cksclscmscnscoscps4ls}3u{17su5tu5os5qs}3v{17su5tu5os5qs}7p{17su5tu}ck{4qu5kt5qt5rs17ss5ts}4l{4qu5kt5qt5rs17ss5ts}cm{4qu5kt5qt5rs17ss5ts}cn{4qu5kt5qt5rs17ss5ts}co{4qu5kt5qt5rs17ss5ts}cp{4qu5kt5qt5rs17ss5ts}6l{4qu5ou5qw5rt17su5tu}5q{ckuclucmucnucoucpu4lu}5r{ckuclucmucnucoucpu4lu}7q{cksclscmscnscoscps4ls}6p{4qu5ou5qw5rt17sw5tw}ek{4qu5ou5qw5rt17su5tu}el{4qu5ou5qw5rt17su5tu}em{4qu5ou5qw5rt17su5tu}en{4qu5ou5qw5rt17su5tu}eo{4qu5ou5qw5rt17su5tu}ep{4qu5ou5qw5rt17su5tu}es{17ss5ts5qs4qu}et{4qu5ou5qw5rt17sw5tw}eu{4qu5ou5qw5rt17ss5ts}ev{17ss5ts5qs4qu}6z{17sw5tw5ou5qw5rs}fm{17sw5tw5ou5qw5rs}7n{201ts}fo{17sw5tw5ou5qw5rs}fp{17sw5tw5ou5qw5rs}fq{17sw5tw5ou5qw5rs}7r{cksclscmscnscoscps4ls}fs{17sw5tw5ou5qw5rs}ft{17su5tu}fu{17su5tu}fv{17su5tu}fw{17su5tu}fz{cksclscmscnscoscps4ls}}}"
      ),
      "Helvetica-Bold": uncompress(
        "{'widths'{k3s2q4scx1w201n3r201o6o201s1w201t1w201u1w201w3m201x3m201y3m2k1w2l2l202m2n2n3r2o3r2p5t202q6o2r1s2s2l2t2l2u2r2v3u2w1w2x2l2y1w2z1w3k3r3l3r3m3r3n3r3o3r3p3r3q3r3r3r3s3r203t2l203u2l3v2l3w3u3x3u3y3u3z3x4k6l4l4s4m4s4n4s4o4s4p4m4q3x4r4y4s4s4t1w4u3r4v4s4w3x4x5n4y4s4z4y5k4m5l4y5m4s5n4m5o3x5p4s5q4m5r5y5s4m5t4m5u3x5v2l5w1w5x2l5y3u5z3r6k2l6l3r6m3x6n3r6o3x6p3r6q2l6r3x6s3x6t1w6u1w6v3r6w1w6x5t6y3x6z3x7k3x7l3x7m2r7n3r7o2l7p3x7q3r7r4y7s3r7t3r7u3m7v2r7w1w7x2r7y3u202l3rcl4sal2lam3ran3rao3rap3rar3ras2lat4tau2pav3raw3uay4taz2lbk2sbl3u'fof'6obo2lbp3xbq3rbr1wbs2lbu2obv3rbz3xck4s202k3rcm4scn4sco4scp4scq6ocr4scs4mct4mcu4mcv4mcw1w2m2zcy1wcz1wdl4sdm4ydn4ydo4ydp4ydq4yds4ydt4sdu4sdv4sdw4sdz3xek3rel3rem3ren3reo3rep3req5ter3res3ret3reu3rev3rew1wex1wey1wez1wfl3xfm3xfn3xfo3xfp3xfq3xfr3ufs3xft3xfu3xfv3xfw3xfz3r203k6o212m6o2dw2l2cq2l3t3r3u2l17s4m19m3r}'kerning'{cl{4qs5ku5ot5qs17sv5tv}201t{2ww4wy2yw}201w{2ks}201x{2ww4wy2yw}2k{201ts201xs}2w{7qs4qu5kw5os5qw5rs17su5tu7tsfzs}2x{5ow5qs}2y{7qs4qu5kw5os5qw5rs17su5tu7tsfzs}'fof'-6o7p{17su5tu5ot}ck{4qs5ku5ot5qs17sv5tv}4l{4qs5ku5ot5qs17sv5tv}cm{4qs5ku5ot5qs17sv5tv}cn{4qs5ku5ot5qs17sv5tv}co{4qs5ku5ot5qs17sv5tv}cp{4qs5ku5ot5qs17sv5tv}6l{17st5tt5os}17s{2kwclvcmvcnvcovcpv4lv4wwckv}5o{2kucltcmtcntcotcpt4lt4wtckt}5q{2ksclscmscnscoscps4ls4wvcks}5r{2ks4ws}5t{2kwclvcmvcnvcovcpv4lv4wwckv}eo{17st5tt5os}fu{17su5tu5ot}6p{17ss5ts}ek{17st5tt5os}el{17st5tt5os}em{17st5tt5os}en{17st5tt5os}6o{201ts}ep{17st5tt5os}es{17ss5ts}et{17ss5ts}eu{17ss5ts}ev{17ss5ts}6z{17su5tu5os5qt}fm{17su5tu5os5qt}fn{17su5tu5os5qt}fo{17su5tu5os5qt}fp{17su5tu5os5qt}fq{17su5tu5os5qt}fs{17su5tu5os5qt}ft{17su5tu5ot}7m{5os}fv{17su5tu5ot}fw{17su5tu5ot}}}"
      ),
      Courier: uncompress("{'widths'{k3w'fof'6o}'kerning'{'fof'-6o}}"),
      "Courier-BoldOblique": uncompress(
        "{'widths'{k3w'fof'6o}'kerning'{'fof'-6o}}"
      ),
      "Times-Bold": uncompress(
        "{'widths'{k3q2q5ncx2r201n3m201o6o201s2l201t2l201u2l201w3m201x3m201y3m2k1t2l2l202m2n2n3m2o3m2p6o202q6o2r1w2s2l2t2l2u3m2v3t2w1t2x2l2y1t2z1w3k3m3l3m3m3m3n3m3o3m3p3m3q3m3r3m3s3m203t2l203u2l3v2l3w3t3x3t3y3t3z3m4k5x4l4s4m4m4n4s4o4s4p4m4q3x4r4y4s4y4t2r4u3m4v4y4w4m4x5y4y4s4z4y5k3x5l4y5m4s5n3r5o4m5p4s5q4s5r6o5s4s5t4s5u4m5v2l5w1w5x2l5y3u5z3m6k2l6l3m6m3r6n2w6o3r6p2w6q2l6r3m6s3r6t1w6u2l6v3r6w1w6x5n6y3r6z3m7k3r7l3r7m2w7n2r7o2l7p3r7q3m7r4s7s3m7t3m7u2w7v2r7w1q7x2r7y3o202l3mcl4sal2lam3man3mao3map3mar3mas2lat4uau1yav3maw3tay4uaz2lbk2sbl3t'fof'6obo2lbp3rbr1tbs2lbu2lbv3mbz3mck4s202k3mcm4scn4sco4scp4scq6ocr4scs4mct4mcu4mcv4mcw2r2m3rcy2rcz2rdl4sdm4ydn4ydo4ydp4ydq4yds4ydt4sdu4sdv4sdw4sdz3rek3mel3mem3men3meo3mep3meq4ser2wes2wet2weu2wev2wew1wex1wey1wez1wfl3rfm3mfn3mfo3mfp3mfq3mfr3tfs3mft3rfu3rfv3rfw3rfz3m203k6o212m6o2dw2l2cq2l3t3m3u2l17s4s19m3m}'kerning'{cl{4qt5ks5ot5qy5rw17sv5tv}201t{cks4lscmscnscoscpscls4wv}2k{201ts}2w{4qu5ku7mu5os5qx5ru17su5tu}2x{17su5tu5ou5qs}2y{4qv5kv7mu5ot5qz5ru17su5tu}'fof'-6o7t{cksclscmscnscoscps4ls}3u{17su5tu5os5qu}3v{17su5tu5os5qu}fu{17su5tu5ou5qu}7p{17su5tu5ou5qu}ck{4qt5ks5ot5qy5rw17sv5tv}4l{4qt5ks5ot5qy5rw17sv5tv}cm{4qt5ks5ot5qy5rw17sv5tv}cn{4qt5ks5ot5qy5rw17sv5tv}co{4qt5ks5ot5qy5rw17sv5tv}cp{4qt5ks5ot5qy5rw17sv5tv}6l{17st5tt5ou5qu}17s{ckuclucmucnucoucpu4lu4wu}5o{ckuclucmucnucoucpu4lu4wu}5q{ckzclzcmzcnzcozcpz4lz4wu}5r{ckxclxcmxcnxcoxcpx4lx4wu}5t{ckuclucmucnucoucpu4lu4wu}7q{ckuclucmucnucoucpu4lu}6p{17sw5tw5ou5qu}ek{17st5tt5qu}el{17st5tt5ou5qu}em{17st5tt5qu}en{17st5tt5qu}eo{17st5tt5qu}ep{17st5tt5ou5qu}es{17ss5ts5qu}et{17sw5tw5ou5qu}eu{17sw5tw5ou5qu}ev{17ss5ts5qu}6z{17sw5tw5ou5qu5rs}fm{17sw5tw5ou5qu5rs}fn{17sw5tw5ou5qu5rs}fo{17sw5tw5ou5qu5rs}fp{17sw5tw5ou5qu5rs}fq{17sw5tw5ou5qu5rs}7r{cktcltcmtcntcotcpt4lt5os}fs{17sw5tw5ou5qu5rs}ft{17su5tu5ou5qu}7m{5os}fv{17su5tu5ou5qu}fw{17su5tu5ou5qu}fz{cksclscmscnscoscps4ls}}}"
      ),
      Symbol: uncompress(
        "{'widths'{k3uaw4r19m3m2k1t2l2l202m2y2n3m2p5n202q6o3k3m2s2l2t2l2v3r2w1t3m3m2y1t2z1wbk2sbl3r'fof'6o3n3m3o3m3p3m3q3m3r3m3s3m3t3m3u1w3v1w3w3r3x3r3y3r3z2wbp3t3l3m5v2l5x2l5z3m2q4yfr3r7v3k7w1o7x3k}'kerning'{'fof'-6o}}"
      ),
      Helvetica: uncompress(
        "{'widths'{k3p2q4mcx1w201n3r201o6o201s1q201t1q201u1q201w2l201x2l201y2l2k1w2l1w202m2n2n3r2o3r2p5t202q6o2r1n2s2l2t2l2u2r2v3u2w1w2x2l2y1w2z1w3k3r3l3r3m3r3n3r3o3r3p3r3q3r3r3r3s3r203t2l203u2l3v1w3w3u3x3u3y3u3z3r4k6p4l4m4m4m4n4s4o4s4p4m4q3x4r4y4s4s4t1w4u3m4v4m4w3r4x5n4y4s4z4y5k4m5l4y5m4s5n4m5o3x5p4s5q4m5r5y5s4m5t4m5u3x5v1w5w1w5x1w5y2z5z3r6k2l6l3r6m3r6n3m6o3r6p3r6q1w6r3r6s3r6t1q6u1q6v3m6w1q6x5n6y3r6z3r7k3r7l3r7m2l7n3m7o1w7p3r7q3m7r4s7s3m7t3m7u3m7v2l7w1u7x2l7y3u202l3rcl4mal2lam3ran3rao3rap3rar3ras2lat4tau2pav3raw3uay4taz2lbk2sbl3u'fof'6obo2lbp3rbr1wbs2lbu2obv3rbz3xck4m202k3rcm4mcn4mco4mcp4mcq6ocr4scs4mct4mcu4mcv4mcw1w2m2ncy1wcz1wdl4sdm4ydn4ydo4ydp4ydq4yds4ydt4sdu4sdv4sdw4sdz3xek3rel3rem3ren3reo3rep3req5ter3mes3ret3reu3rev3rew1wex1wey1wez1wfl3rfm3rfn3rfo3rfp3rfq3rfr3ufs3xft3rfu3rfv3rfw3rfz3m203k6o212m6o2dw2l2cq2l3t3r3u1w17s4m19m3r}'kerning'{5q{4wv}cl{4qs5kw5ow5qs17sv5tv}201t{2wu4w1k2yu}201x{2wu4wy2yu}17s{2ktclucmucnu4otcpu4lu4wycoucku}2w{7qs4qz5k1m17sy5ow5qx5rsfsu5ty7tufzu}2x{17sy5ty5oy5qs}2y{7qs4qz5k1m17sy5ow5qx5rsfsu5ty7tufzu}'fof'-6o7p{17sv5tv5ow}ck{4qs5kw5ow5qs17sv5tv}4l{4qs5kw5ow5qs17sv5tv}cm{4qs5kw5ow5qs17sv5tv}cn{4qs5kw5ow5qs17sv5tv}co{4qs5kw5ow5qs17sv5tv}cp{4qs5kw5ow5qs17sv5tv}6l{17sy5ty5ow}do{17st5tt}4z{17st5tt}7s{fst}dm{17st5tt}dn{17st5tt}5o{ckwclwcmwcnwcowcpw4lw4wv}dp{17st5tt}dq{17st5tt}7t{5ow}ds{17st5tt}5t{2ktclucmucnu4otcpu4lu4wycoucku}fu{17sv5tv5ow}6p{17sy5ty5ow5qs}ek{17sy5ty5ow}el{17sy5ty5ow}em{17sy5ty5ow}en{5ty}eo{17sy5ty5ow}ep{17sy5ty5ow}es{17sy5ty5qs}et{17sy5ty5ow5qs}eu{17sy5ty5ow5qs}ev{17sy5ty5ow5qs}6z{17sy5ty5ow5qs}fm{17sy5ty5ow5qs}fn{17sy5ty5ow5qs}fo{17sy5ty5ow5qs}fp{17sy5ty5qs}fq{17sy5ty5ow5qs}7r{5ow}fs{17sy5ty5ow5qs}ft{17sv5tv5ow}7m{5ow}fv{17sv5tv5ow}fw{17sv5tv5ow}}}"
      ),
      "Helvetica-BoldOblique": uncompress(
        "{'widths'{k3s2q4scx1w201n3r201o6o201s1w201t1w201u1w201w3m201x3m201y3m2k1w2l2l202m2n2n3r2o3r2p5t202q6o2r1s2s2l2t2l2u2r2v3u2w1w2x2l2y1w2z1w3k3r3l3r3m3r3n3r3o3r3p3r3q3r3r3r3s3r203t2l203u2l3v2l3w3u3x3u3y3u3z3x4k6l4l4s4m4s4n4s4o4s4p4m4q3x4r4y4s4s4t1w4u3r4v4s4w3x4x5n4y4s4z4y5k4m5l4y5m4s5n4m5o3x5p4s5q4m5r5y5s4m5t4m5u3x5v2l5w1w5x2l5y3u5z3r6k2l6l3r6m3x6n3r6o3x6p3r6q2l6r3x6s3x6t1w6u1w6v3r6w1w6x5t6y3x6z3x7k3x7l3x7m2r7n3r7o2l7p3x7q3r7r4y7s3r7t3r7u3m7v2r7w1w7x2r7y3u202l3rcl4sal2lam3ran3rao3rap3rar3ras2lat4tau2pav3raw3uay4taz2lbk2sbl3u'fof'6obo2lbp3xbq3rbr1wbs2lbu2obv3rbz3xck4s202k3rcm4scn4sco4scp4scq6ocr4scs4mct4mcu4mcv4mcw1w2m2zcy1wcz1wdl4sdm4ydn4ydo4ydp4ydq4yds4ydt4sdu4sdv4sdw4sdz3xek3rel3rem3ren3reo3rep3req5ter3res3ret3reu3rev3rew1wex1wey1wez1wfl3xfm3xfn3xfo3xfp3xfq3xfr3ufs3xft3xfu3xfv3xfw3xfz3r203k6o212m6o2dw2l2cq2l3t3r3u2l17s4m19m3r}'kerning'{cl{4qs5ku5ot5qs17sv5tv}201t{2ww4wy2yw}201w{2ks}201x{2ww4wy2yw}2k{201ts201xs}2w{7qs4qu5kw5os5qw5rs17su5tu7tsfzs}2x{5ow5qs}2y{7qs4qu5kw5os5qw5rs17su5tu7tsfzs}'fof'-6o7p{17su5tu5ot}ck{4qs5ku5ot5qs17sv5tv}4l{4qs5ku5ot5qs17sv5tv}cm{4qs5ku5ot5qs17sv5tv}cn{4qs5ku5ot5qs17sv5tv}co{4qs5ku5ot5qs17sv5tv}cp{4qs5ku5ot5qs17sv5tv}6l{17st5tt5os}17s{2kwclvcmvcnvcovcpv4lv4wwckv}5o{2kucltcmtcntcotcpt4lt4wtckt}5q{2ksclscmscnscoscps4ls4wvcks}5r{2ks4ws}5t{2kwclvcmvcnvcovcpv4lv4wwckv}eo{17st5tt5os}fu{17su5tu5ot}6p{17ss5ts}ek{17st5tt5os}el{17st5tt5os}em{17st5tt5os}en{17st5tt5os}6o{201ts}ep{17st5tt5os}es{17ss5ts}et{17ss5ts}eu{17ss5ts}ev{17ss5ts}6z{17su5tu5os5qt}fm{17su5tu5os5qt}fn{17su5tu5os5qt}fo{17su5tu5os5qt}fp{17su5tu5os5qt}fq{17su5tu5os5qt}fs{17su5tu5os5qt}ft{17su5tu5ot}7m{5os}fv{17su5tu5ot}fw{17su5tu5ot}}}"
      ),
      ZapfDingbats: uncompress("{'widths'{k4u2k1w'fof'6o}'kerning'{'fof'-6o}}"),
      "Courier-Bold": uncompress("{'widths'{k3w'fof'6o}'kerning'{'fof'-6o}}"),
      "Times-Italic": uncompress(
        "{'widths'{k3n2q4ycx2l201n3m201o5t201s2l201t2l201u2l201w3r201x3r201y3r2k1t2l2l202m2n2n3m2o3m2p5n202q5t2r1p2s2l2t2l2u3m2v4n2w1t2x2l2y1t2z1w3k3m3l3m3m3m3n3m3o3m3p3m3q3m3r3m3s3m203t2l203u2l3v2l3w4n3x4n3y4n3z3m4k5w4l3x4m3x4n4m4o4s4p3x4q3x4r4s4s4s4t2l4u2w4v4m4w3r4x5n4y4m4z4s5k3x5l4s5m3x5n3m5o3r5p4s5q3x5r5n5s3x5t3r5u3r5v2r5w1w5x2r5y2u5z3m6k2l6l3m6m3m6n2w6o3m6p2w6q1w6r3m6s3m6t1w6u1w6v2w6w1w6x4s6y3m6z3m7k3m7l3m7m2r7n2r7o1w7p3m7q2w7r4m7s2w7t2w7u2r7v2s7w1v7x2s7y3q202l3mcl3xal2ram3man3mao3map3mar3mas2lat4wau1vav3maw4nay4waz2lbk2sbl4n'fof'6obo2lbp3mbq3obr1tbs2lbu1zbv3mbz3mck3x202k3mcm3xcn3xco3xcp3xcq5tcr4mcs3xct3xcu3xcv3xcw2l2m2ucy2lcz2ldl4mdm4sdn4sdo4sdp4sdq4sds4sdt4sdu4sdv4sdw4sdz3mek3mel3mem3men3meo3mep3meq4mer2wes2wet2weu2wev2wew1wex1wey1wez1wfl3mfm3mfn3mfo3mfp3mfq3mfr4nfs3mft3mfu3mfv3mfw3mfz2w203k6o212m6m2dw2l2cq2l3t3m3u2l17s3r19m3m}'kerning'{cl{5kt4qw}201s{201sw}201t{201tw2wy2yy6q-t}201x{2wy2yy}2k{201tw}2w{7qs4qy7rs5ky7mw5os5qx5ru17su5tu}2x{17ss5ts5os}2y{7qs4qy7rs5ky7mw5os5qx5ru17su5tu}'fof'-6o6t{17ss5ts5qs}7t{5os}3v{5qs}7p{17su5tu5qs}ck{5kt4qw}4l{5kt4qw}cm{5kt4qw}cn{5kt4qw}co{5kt4qw}cp{5kt4qw}6l{4qs5ks5ou5qw5ru17su5tu}17s{2ks}5q{ckvclvcmvcnvcovcpv4lv}5r{ckuclucmucnucoucpu4lu}5t{2ks}6p{4qs5ks5ou5qw5ru17su5tu}ek{4qs5ks5ou5qw5ru17su5tu}el{4qs5ks5ou5qw5ru17su5tu}em{4qs5ks5ou5qw5ru17su5tu}en{4qs5ks5ou5qw5ru17su5tu}eo{4qs5ks5ou5qw5ru17su5tu}ep{4qs5ks5ou5qw5ru17su5tu}es{5ks5qs4qs}et{4qs5ks5ou5qw5ru17su5tu}eu{4qs5ks5qw5ru17su5tu}ev{5ks5qs4qs}ex{17ss5ts5qs}6z{4qv5ks5ou5qw5ru17su5tu}fm{4qv5ks5ou5qw5ru17su5tu}fn{4qv5ks5ou5qw5ru17su5tu}fo{4qv5ks5ou5qw5ru17su5tu}fp{4qv5ks5ou5qw5ru17su5tu}fq{4qv5ks5ou5qw5ru17su5tu}7r{5os}fs{4qv5ks5ou5qw5ru17su5tu}ft{17su5tu5qs}fu{17su5tu5qs}fv{17su5tu5qs}fw{17su5tu5qs}}}"
      ),
      "Times-Roman": uncompress(
        "{'widths'{k3n2q4ycx2l201n3m201o6o201s2l201t2l201u2l201w2w201x2w201y2w2k1t2l2l202m2n2n3m2o3m2p5n202q6o2r1m2s2l2t2l2u3m2v3s2w1t2x2l2y1t2z1w3k3m3l3m3m3m3n3m3o3m3p3m3q3m3r3m3s3m203t2l203u2l3v1w3w3s3x3s3y3s3z2w4k5w4l4s4m4m4n4m4o4s4p3x4q3r4r4s4s4s4t2l4u2r4v4s4w3x4x5t4y4s4z4s5k3r5l4s5m4m5n3r5o3x5p4s5q4s5r5y5s4s5t4s5u3x5v2l5w1w5x2l5y2z5z3m6k2l6l2w6m3m6n2w6o3m6p2w6q2l6r3m6s3m6t1w6u1w6v3m6w1w6x4y6y3m6z3m7k3m7l3m7m2l7n2r7o1w7p3m7q3m7r4s7s3m7t3m7u2w7v3k7w1o7x3k7y3q202l3mcl4sal2lam3man3mao3map3mar3mas2lat4wau1vav3maw3say4waz2lbk2sbl3s'fof'6obo2lbp3mbq2xbr1tbs2lbu1zbv3mbz2wck4s202k3mcm4scn4sco4scp4scq5tcr4mcs3xct3xcu3xcv3xcw2l2m2tcy2lcz2ldl4sdm4sdn4sdo4sdp4sdq4sds4sdt4sdu4sdv4sdw4sdz3mek2wel2wem2wen2weo2wep2weq4mer2wes2wet2weu2wev2wew1wex1wey1wez1wfl3mfm3mfn3mfo3mfp3mfq3mfr3sfs3mft3mfu3mfv3mfw3mfz3m203k6o212m6m2dw2l2cq2l3t3m3u1w17s4s19m3m}'kerning'{cl{4qs5ku17sw5ou5qy5rw201ss5tw201ws}201s{201ss}201t{ckw4lwcmwcnwcowcpwclw4wu201ts}2k{201ts}2w{4qs5kw5os5qx5ru17sx5tx}2x{17sw5tw5ou5qu}2y{4qs5kw5os5qx5ru17sx5tx}'fof'-6o7t{ckuclucmucnucoucpu4lu5os5rs}3u{17su5tu5qs}3v{17su5tu5qs}7p{17sw5tw5qs}ck{4qs5ku17sw5ou5qy5rw201ss5tw201ws}4l{4qs5ku17sw5ou5qy5rw201ss5tw201ws}cm{4qs5ku17sw5ou5qy5rw201ss5tw201ws}cn{4qs5ku17sw5ou5qy5rw201ss5tw201ws}co{4qs5ku17sw5ou5qy5rw201ss5tw201ws}cp{4qs5ku17sw5ou5qy5rw201ss5tw201ws}6l{17su5tu5os5qw5rs}17s{2ktclvcmvcnvcovcpv4lv4wuckv}5o{ckwclwcmwcnwcowcpw4lw4wu}5q{ckyclycmycnycoycpy4ly4wu5ms}5r{cktcltcmtcntcotcpt4lt4ws}5t{2ktclvcmvcnvcovcpv4lv4wuckv}7q{cksclscmscnscoscps4ls}6p{17su5tu5qw5rs}ek{5qs5rs}el{17su5tu5os5qw5rs}em{17su5tu5os5qs5rs}en{17su5qs5rs}eo{5qs5rs}ep{17su5tu5os5qw5rs}es{5qs}et{17su5tu5qw5rs}eu{17su5tu5qs5rs}ev{5qs}6z{17sv5tv5os5qx5rs}fm{5os5qt5rs}fn{17sv5tv5os5qx5rs}fo{17sv5tv5os5qx5rs}fp{5os5qt5rs}fq{5os5qt5rs}7r{ckuclucmucnucoucpu4lu5os}fs{17sv5tv5os5qx5rs}ft{17ss5ts5qs}fu{17sw5tw5qs}fv{17sw5tw5qs}fw{17ss5ts5qs}fz{ckuclucmucnucoucpu4lu5os5rs}}}"
      ),
      "Helvetica-Oblique": uncompress(
        "{'widths'{k3p2q4mcx1w201n3r201o6o201s1q201t1q201u1q201w2l201x2l201y2l2k1w2l1w202m2n2n3r2o3r2p5t202q6o2r1n2s2l2t2l2u2r2v3u2w1w2x2l2y1w2z1w3k3r3l3r3m3r3n3r3o3r3p3r3q3r3r3r3s3r203t2l203u2l3v1w3w3u3x3u3y3u3z3r4k6p4l4m4m4m4n4s4o4s4p4m4q3x4r4y4s4s4t1w4u3m4v4m4w3r4x5n4y4s4z4y5k4m5l4y5m4s5n4m5o3x5p4s5q4m5r5y5s4m5t4m5u3x5v1w5w1w5x1w5y2z5z3r6k2l6l3r6m3r6n3m6o3r6p3r6q1w6r3r6s3r6t1q6u1q6v3m6w1q6x5n6y3r6z3r7k3r7l3r7m2l7n3m7o1w7p3r7q3m7r4s7s3m7t3m7u3m7v2l7w1u7x2l7y3u202l3rcl4mal2lam3ran3rao3rap3rar3ras2lat4tau2pav3raw3uay4taz2lbk2sbl3u'fof'6obo2lbp3rbr1wbs2lbu2obv3rbz3xck4m202k3rcm4mcn4mco4mcp4mcq6ocr4scs4mct4mcu4mcv4mcw1w2m2ncy1wcz1wdl4sdm4ydn4ydo4ydp4ydq4yds4ydt4sdu4sdv4sdw4sdz3xek3rel3rem3ren3reo3rep3req5ter3mes3ret3reu3rev3rew1wex1wey1wez1wfl3rfm3rfn3rfo3rfp3rfq3rfr3ufs3xft3rfu3rfv3rfw3rfz3m203k6o212m6o2dw2l2cq2l3t3r3u1w17s4m19m3r}'kerning'{5q{4wv}cl{4qs5kw5ow5qs17sv5tv}201t{2wu4w1k2yu}201x{2wu4wy2yu}17s{2ktclucmucnu4otcpu4lu4wycoucku}2w{7qs4qz5k1m17sy5ow5qx5rsfsu5ty7tufzu}2x{17sy5ty5oy5qs}2y{7qs4qz5k1m17sy5ow5qx5rsfsu5ty7tufzu}'fof'-6o7p{17sv5tv5ow}ck{4qs5kw5ow5qs17sv5tv}4l{4qs5kw5ow5qs17sv5tv}cm{4qs5kw5ow5qs17sv5tv}cn{4qs5kw5ow5qs17sv5tv}co{4qs5kw5ow5qs17sv5tv}cp{4qs5kw5ow5qs17sv5tv}6l{17sy5ty5ow}do{17st5tt}4z{17st5tt}7s{fst}dm{17st5tt}dn{17st5tt}5o{ckwclwcmwcnwcowcpw4lw4wv}dp{17st5tt}dq{17st5tt}7t{5ow}ds{17st5tt}5t{2ktclucmucnu4otcpu4lu4wycoucku}fu{17sv5tv5ow}6p{17sy5ty5ow5qs}ek{17sy5ty5ow}el{17sy5ty5ow}em{17sy5ty5ow}en{5ty}eo{17sy5ty5ow}ep{17sy5ty5ow}es{17sy5ty5qs}et{17sy5ty5ow5qs}eu{17sy5ty5ow5qs}ev{17sy5ty5ow5qs}6z{17sy5ty5ow5qs}fm{17sy5ty5ow5qs}fn{17sy5ty5ow5qs}fo{17sy5ty5ow5qs}fp{17sy5ty5qs}fq{17sy5ty5ow5qs}7r{5ow}fs{17sy5ty5ow5qs}ft{17sv5tv5ow}7m{5ow}fv{17sv5tv5ow}fw{17sv5tv5ow}}}"
      )
    }
  };

  /*
	This event handler is fired when a new jsPDF object is initialized
	This event handler appends metrics data to standard fonts within
	that jsPDF instance. The metrics are mapped over Unicode character
	codes, NOT CIDs or other codes matching the StandardEncoding table of the
	standard PDF fonts.
	Future:
	Also included is the encoding maping table, converting Unicode (UCS-2, UTF-16)
	char codes to StandardEncoding character codes. The encoding table is to be used
	somewhere around "pdfEscape" call.
	*/
  API.events.push([
    "addFont",
    function(data) {
      var font = data.font;

      var metrics = fontMetrics["Unicode"][font.postScriptName];
      if (metrics) {
        font.metadata["Unicode"] = {};
        font.metadata["Unicode"].widths = metrics.widths;
        font.metadata["Unicode"].kerning = metrics.kerning;
      }

      var encodingBlock = encodings["Unicode"][font.postScriptName];
      if (encodingBlock) {
        font.metadata["Unicode"].encoding = encodingBlock;
        font.encoding = encodingBlock.codePages[0];
      }
    }
  ]); // end of adding event handler
})(jsPDF.API);

/**
 * @license
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 */

/**
 * @name ttfsupport
 * @module
 */
(function(jsPDF) {

  var binaryStringToUint8Array = function(binary_string) {
    var len = binary_string.length;
    var bytes = new Uint8Array(len);
    for (var i = 0; i < len; i++) {
      bytes[i] = binary_string.charCodeAt(i);
    }
    return bytes;
  };

  var addFont = function(font, file) {
    // eslint-disable-next-line no-control-regex
    if (/^\x00\x01\x00\x00/.test(file)) {
      file = binaryStringToUint8Array(file);
    } else {
      file = binaryStringToUint8Array(atob$1(file));
    }
    font.metadata = jsPDF.API.TTFFont.open(file);
    font.metadata.Unicode = font.metadata.Unicode || {
      encoding: {},
      kerning: {},
      widths: []
    };
    font.metadata.glyIdsUsed = [0];
  };

  jsPDF.API.events.push([
    "addFont",
    function(data) {
      var file = undefined;
      var font = data.font;
      var instance = data.instance;
      if (font.isStandardFont) {
        return;
      }
      if (typeof instance !== "undefined") {
        if (instance.existsFileInVFS(font.postScriptName) === false) {
          file = instance.loadFile(font.postScriptName);
        } else {
          file = instance.getFileFromVFS(font.postScriptName);
        }
        if (typeof file !== "string") {
          throw new Error(
            "Font is not stored as string-data in vFS, import fonts or remove declaration doc.addFont('" +
              font.postScriptName +
              "')."
          );
        }
        addFont(font, file);
      } else {
        throw new Error(
          "Font does not exist in vFS, import fonts or remove declaration doc.addFont('" +
            font.postScriptName +
            "')."
        );
      }
    }
  ]); // end of adding event handler
})(jsPDF);

/**
 * @license
 * ====================================================================
 * Copyright (c) 2013 Eduardo Menezes de Morais, eduardo.morais@usp.br
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * ====================================================================
 */

/**
 * jsPDF total_pages plugin
 * @name total_pages
 * @module
 */
(function(jsPDFAPI) {
  /**
   * @name putTotalPages
   * @function
   * @param {string} pageExpression Regular Expression
   * @returns {jsPDF} jsPDF-instance
   */

  jsPDFAPI.putTotalPages = function(pageExpression) {

    var replaceExpression;
    var totalNumberOfPages = 0;
    if (parseInt(this.internal.getFont().id.substr(1), 10) < 15) {
      replaceExpression = new RegExp(pageExpression, "g");
      totalNumberOfPages = this.internal.getNumberOfPages();
    } else {
      replaceExpression = new RegExp(
        this.pdfEscape16(pageExpression, this.internal.getFont()),
        "g"
      );
      totalNumberOfPages = this.pdfEscape16(
        this.internal.getNumberOfPages() + "",
        this.internal.getFont()
      );
    }

    for (var n = 1; n <= this.internal.getNumberOfPages(); n++) {
      for (var i = 0; i < this.internal.pages[n].length; i++) {
        this.internal.pages[n][i] = this.internal.pages[n][i].replace(
          replaceExpression,
          totalNumberOfPages
        );
      }
    }

    return this;
  };
})(jsPDF.API);

/**
 * @license
 * jsPDF viewerPreferences Plugin
 * @author Aras Abbasi (github.com/arasabbasi)
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 */

/**
 * Adds the ability to set ViewerPreferences and by thus
 * controlling the way the document is to be presented on the
 * screen or in print.
 * @name viewerpreferences
 * @module
 */
(function(jsPDFAPI) {
  /**
   * Set the ViewerPreferences of the generated PDF
   *
   * @name viewerPreferences
   * @function
   * @public
   * @param {Object} options Array with the ViewerPreferences<br />
   * Example: doc.viewerPreferences({"FitWindow":true});<br />
   * <br />
   * You can set following preferences:<br />
   * <br/>
   * <b>HideToolbar</b> <i>(boolean)</i><br />
   * Default value: false<br />
   * <br />
   * <b>HideMenubar</b> <i>(boolean)</i><br />
   * Default value: false.<br />
   * <br />
   * <b>HideWindowUI</b> <i>(boolean)</i><br />
   * Default value: false.<br />
   * <br />
   * <b>FitWindow</b> <i>(boolean)</i><br />
   * Default value: false.<br />
   * <br />
   * <b>CenterWindow</b> <i>(boolean)</i><br />
   * Default value: false<br />
   * <br />
   * <b>DisplayDocTitle</b> <i>(boolean)</i><br />
   * Default value: false.<br />
   * <br />
   * <b>NonFullScreenPageMode</b> <i>(string)</i><br />
   * Possible values: UseNone, UseOutlines, UseThumbs, UseOC<br />
   * Default value: UseNone<br/>
   * <br />
   * <b>Direction</b> <i>(string)</i><br />
   * Possible values: L2R, R2L<br />
   * Default value: L2R.<br />
   * <br />
   * <b>ViewArea</b> <i>(string)</i><br />
   * Possible values: MediaBox, CropBox, TrimBox, BleedBox, ArtBox<br />
   * Default value: CropBox.<br />
   * <br />
   * <b>ViewClip</b> <i>(string)</i><br />
   * Possible values: MediaBox, CropBox, TrimBox, BleedBox, ArtBox<br />
   * Default value: CropBox<br />
   * <br />
   * <b>PrintArea</b> <i>(string)</i><br />
   * Possible values: MediaBox, CropBox, TrimBox, BleedBox, ArtBox<br />
   * Default value: CropBox<br />
   * <br />
   * <b>PrintClip</b> <i>(string)</i><br />
   * Possible values: MediaBox, CropBox, TrimBox, BleedBox, ArtBox<br />
   * Default value: CropBox.<br />
   * <br />
   * <b>PrintScaling</b> <i>(string)</i><br />
   * Possible values: AppDefault, None<br />
   * Default value: AppDefault.<br />
   * <br />
   * <b>Duplex</b> <i>(string)</i><br />
   * Possible values: Simplex, DuplexFlipLongEdge, DuplexFlipShortEdge
   * Default value: none<br />
   * <br />
   * <b>PickTrayByPDFSize</b> <i>(boolean)</i><br />
   * Default value: false<br />
   * <br />
   * <b>PrintPageRange</b> <i>(Array)</i><br />
   * Example: [[1,5], [7,9]]<br />
   * Default value: as defined by PDF viewer application<br />
   * <br />
   * <b>NumCopies</b> <i>(Number)</i><br />
   * Possible values: 1, 2, 3, 4, 5<br />
   * Default value: 1<br />
   * <br />
   * For more information see the PDF Reference, sixth edition on Page 577
   * @param {boolean} doReset True to reset the settings
   * @function
   * @returns jsPDF jsPDF-instance
   * @example
   * var doc = new jsPDF()
   * doc.text('This is a test', 10, 10)
   * doc.viewerPreferences({'FitWindow': true}, true)
   * doc.save("viewerPreferences.pdf")
   *
   * // Example printing 10 copies, using cropbox, and hiding UI.
   * doc.viewerPreferences({
   *   'HideWindowUI': true,
   *   'PrintArea': 'CropBox',
   *   'NumCopies': 10
   * })
   */
  jsPDFAPI.viewerPreferences = function(options, doReset) {
    options = options || {};
    doReset = doReset || false;

    var configuration;
    var configurationTemplate = {
      HideToolbar: {
        defaultValue: false,
        value: false,
        type: "boolean",
        explicitSet: false,
        valueSet: [true, false],
        pdfVersion: 1.3
      },
      HideMenubar: {
        defaultValue: false,
        value: false,
        type: "boolean",
        explicitSet: false,
        valueSet: [true, false],
        pdfVersion: 1.3
      },
      HideWindowUI: {
        defaultValue: false,
        value: false,
        type: "boolean",
        explicitSet: false,
        valueSet: [true, false],
        pdfVersion: 1.3
      },
      FitWindow: {
        defaultValue: false,
        value: false,
        type: "boolean",
        explicitSet: false,
        valueSet: [true, false],
        pdfVersion: 1.3
      },
      CenterWindow: {
        defaultValue: false,
        value: false,
        type: "boolean",
        explicitSet: false,
        valueSet: [true, false],
        pdfVersion: 1.3
      },
      DisplayDocTitle: {
        defaultValue: false,
        value: false,
        type: "boolean",
        explicitSet: false,
        valueSet: [true, false],
        pdfVersion: 1.4
      },
      NonFullScreenPageMode: {
        defaultValue: "UseNone",
        value: "UseNone",
        type: "name",
        explicitSet: false,
        valueSet: ["UseNone", "UseOutlines", "UseThumbs", "UseOC"],
        pdfVersion: 1.3
      },
      Direction: {
        defaultValue: "L2R",
        value: "L2R",
        type: "name",
        explicitSet: false,
        valueSet: ["L2R", "R2L"],
        pdfVersion: 1.3
      },
      ViewArea: {
        defaultValue: "CropBox",
        value: "CropBox",
        type: "name",
        explicitSet: false,
        valueSet: ["MediaBox", "CropBox", "TrimBox", "BleedBox", "ArtBox"],
        pdfVersion: 1.4
      },
      ViewClip: {
        defaultValue: "CropBox",
        value: "CropBox",
        type: "name",
        explicitSet: false,
        valueSet: ["MediaBox", "CropBox", "TrimBox", "BleedBox", "ArtBox"],
        pdfVersion: 1.4
      },
      PrintArea: {
        defaultValue: "CropBox",
        value: "CropBox",
        type: "name",
        explicitSet: false,
        valueSet: ["MediaBox", "CropBox", "TrimBox", "BleedBox", "ArtBox"],
        pdfVersion: 1.4
      },
      PrintClip: {
        defaultValue: "CropBox",
        value: "CropBox",
        type: "name",
        explicitSet: false,
        valueSet: ["MediaBox", "CropBox", "TrimBox", "BleedBox", "ArtBox"],
        pdfVersion: 1.4
      },
      PrintScaling: {
        defaultValue: "AppDefault",
        value: "AppDefault",
        type: "name",
        explicitSet: false,
        valueSet: ["AppDefault", "None"],
        pdfVersion: 1.6
      },
      Duplex: {
        defaultValue: "",
        value: "none",
        type: "name",
        explicitSet: false,
        valueSet: [
          "Simplex",
          "DuplexFlipShortEdge",
          "DuplexFlipLongEdge",
          "none"
        ],
        pdfVersion: 1.7
      },
      PickTrayByPDFSize: {
        defaultValue: false,
        value: false,
        type: "boolean",
        explicitSet: false,
        valueSet: [true, false],
        pdfVersion: 1.7
      },
      PrintPageRange: {
        defaultValue: "",
        value: "",
        type: "array",
        explicitSet: false,
        valueSet: null,
        pdfVersion: 1.7
      },
      NumCopies: {
        defaultValue: 1,
        value: 1,
        type: "integer",
        explicitSet: false,
        valueSet: null,
        pdfVersion: 1.7
      }
    };

    var configurationKeys = Object.keys(configurationTemplate);

    var rangeArray = [];
    var i = 0;
    var j = 0;
    var k = 0;
    var isValid;

    var method;
    var value;

    function arrayContainsElement(array, element) {
      var iterator;
      var result = false;

      for (iterator = 0; iterator < array.length; iterator += 1) {
        if (array[iterator] === element) {
          result = true;
        }
      }
      return result;
    }

    if (this.internal.viewerpreferences === undefined) {
      this.internal.viewerpreferences = {};
      this.internal.viewerpreferences.configuration = JSON.parse(
        JSON.stringify(configurationTemplate)
      );
      this.internal.viewerpreferences.isSubscribed = false;
    }
    configuration = this.internal.viewerpreferences.configuration;

    if (options === "reset" || doReset === true) {
      var len = configurationKeys.length;

      for (k = 0; k < len; k += 1) {
        configuration[configurationKeys[k]].value =
          configuration[configurationKeys[k]].defaultValue;
        configuration[configurationKeys[k]].explicitSet = false;
      }
    }

    if (typeof options === "object") {
      for (method in options) {
        value = options[method];
        if (
          arrayContainsElement(configurationKeys, method) &&
          value !== undefined
        ) {
          if (
            configuration[method].type === "boolean" &&
            typeof value === "boolean"
          ) {
            configuration[method].value = value;
          } else if (
            configuration[method].type === "name" &&
            arrayContainsElement(configuration[method].valueSet, value)
          ) {
            configuration[method].value = value;
          } else if (
            configuration[method].type === "integer" &&
            Number.isInteger(value)
          ) {
            configuration[method].value = value;
          } else if (configuration[method].type === "array") {
            for (i = 0; i < value.length; i += 1) {
              isValid = true;
              if (value[i].length === 1 && typeof value[i][0] === "number") {
                rangeArray.push(String(value[i] - 1));
              } else if (value[i].length > 1) {
                for (j = 0; j < value[i].length; j += 1) {
                  if (typeof value[i][j] !== "number") {
                    isValid = false;
                  }
                }
                if (isValid === true) {
                  rangeArray.push([value[i][0] - 1, value[i][1] - 1].join(" "));
                }
              }
            }
            configuration[method].value = "[" + rangeArray.join(" ") + "]";
          } else {
            configuration[method].value = configuration[method].defaultValue;
          }

          configuration[method].explicitSet = true;
        }
      }
    }

    if (this.internal.viewerpreferences.isSubscribed === false) {
      this.internal.events.subscribe("putCatalog", function() {
        var pdfDict = [];
        var vPref;
        for (vPref in configuration) {
          if (configuration[vPref].explicitSet === true) {
            if (configuration[vPref].type === "name") {
              pdfDict.push("/" + vPref + " /" + configuration[vPref].value);
            } else {
              pdfDict.push("/" + vPref + " " + configuration[vPref].value);
            }
          }
        }
        if (pdfDict.length !== 0) {
          this.internal.write(
            "/ViewerPreferences\n<<\n" + pdfDict.join("\n") + "\n>>"
          );
        }
      });
      this.internal.viewerpreferences.isSubscribed = true;
    }

    this.internal.viewerpreferences.configuration = configuration;
    return this;
  };
})(jsPDF.API);

/** ====================================================================
 * @license
 * jsPDF XMP metadata plugin
 * Copyright (c) 2016 Jussi Utunen, u-jussi@suomi24.fi
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * ====================================================================
 */

/**
 * @name xmp_metadata
 * @module
 */
(function(jsPDFAPI) {

  var postPutResources = function() {
    var xmpmeta_beginning = '<x:xmpmeta xmlns:x="adobe:ns:meta/">';
    var rdf_beginning =
      '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:jspdf="' +
      this.internal.__metadata__.namespaceuri +
      '"><jspdf:metadata>';
    var rdf_ending = "</jspdf:metadata></rdf:Description></rdf:RDF>";
    var xmpmeta_ending = "</x:xmpmeta>";
    var utf8_xmpmeta_beginning = unescape(
      encodeURIComponent(xmpmeta_beginning)
    );
    var utf8_rdf_beginning = unescape(encodeURIComponent(rdf_beginning));
    var utf8_metadata = unescape(
      encodeURIComponent(this.internal.__metadata__.metadata)
    );
    var utf8_rdf_ending = unescape(encodeURIComponent(rdf_ending));
    var utf8_xmpmeta_ending = unescape(encodeURIComponent(xmpmeta_ending));

    var total_len =
      utf8_rdf_beginning.length +
      utf8_metadata.length +
      utf8_rdf_ending.length +
      utf8_xmpmeta_beginning.length +
      utf8_xmpmeta_ending.length;

    this.internal.__metadata__.metadata_object_number = this.internal.newObject();
    this.internal.write(
      "<< /Type /Metadata /Subtype /XML /Length " + total_len + " >>"
    );
    this.internal.write("stream");
    this.internal.write(
      utf8_xmpmeta_beginning +
        utf8_rdf_beginning +
        utf8_metadata +
        utf8_rdf_ending +
        utf8_xmpmeta_ending
    );
    this.internal.write("endstream");
    this.internal.write("endobj");
  };

  var putCatalog = function() {
    if (this.internal.__metadata__.metadata_object_number) {
      this.internal.write(
        "/Metadata " +
          this.internal.__metadata__.metadata_object_number +
          " 0 R"
      );
    }
  };

  /**
   * Adds XMP formatted metadata to PDF
   *
   * @name addMetadata
   * @function
   * @param {String} metadata The actual metadata to be added. The metadata shall be stored as XMP simple value. Note that if the metadata string contains XML markup characters "<", ">" or "&", those characters should be written using XML entities.
   * @param {String} namespaceuri Sets the namespace URI for the metadata. Last character should be slash or hash.
   * @returns {jsPDF} jsPDF-instance
   */
  jsPDFAPI.addMetadata = function(metadata, namespaceuri) {
    if (typeof this.internal.__metadata__ === "undefined") {
      this.internal.__metadata__ = {
        metadata: metadata,
        namespaceuri: namespaceuri || "http://jspdf.default.namespaceuri/"
      };
      this.internal.events.subscribe("putCatalog", putCatalog);

      this.internal.events.subscribe("postPutResources", postPutResources);
    }
    return this;
  };
})(jsPDF.API);

/**
 * @name utf8
 * @module
 */
(function(jsPDF) {
  var jsPDFAPI = jsPDF.API;

  /***************************************************************************************************/
  /* function : pdfEscape16                                                                          */
  /* comment : The character id of a 2-byte string is converted to a hexadecimal number by obtaining */
  /*   the corresponding glyph id and width, and then adding padding to the string.                  */
  /***************************************************************************************************/
  var pdfEscape16 = (jsPDFAPI.pdfEscape16 = function(text, font) {
    var widths = font.metadata.Unicode.widths;
    var padz = ["", "0", "00", "000", "0000"];
    var ar = [""];
    for (var i = 0, l = text.length, t; i < l; ++i) {
      t = font.metadata.characterToGlyph(text.charCodeAt(i));
      font.metadata.glyIdsUsed.push(t);
      font.metadata.toUnicode[t] = text.charCodeAt(i);
      if (widths.indexOf(t) == -1) {
        widths.push(t);
        widths.push([parseInt(font.metadata.widthOfGlyph(t), 10)]);
      }
      if (t == "0") {
        //Spaces are not allowed in cmap.
        return ar.join("");
      } else {
        t = t.toString(16);
        ar.push(padz[4 - t.length], t);
      }
    }
    return ar.join("");
  });

  var toUnicodeCmap = function(map) {
    var code, codes, range, unicode, unicodeMap, _i, _len;
    unicodeMap =
      "/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CIDSystemInfo <<\n  /Registry (Adobe)\n  /Ordering (UCS)\n  /Supplement 0\n>> def\n/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n1 begincodespacerange\n<0000><ffff>\nendcodespacerange";
    codes = Object.keys(map).sort(function(a, b) {
      return a - b;
    });

    range = [];
    for (_i = 0, _len = codes.length; _i < _len; _i++) {
      code = codes[_i];
      if (range.length >= 100) {
        unicodeMap +=
          "\n" +
          range.length +
          " beginbfchar\n" +
          range.join("\n") +
          "\nendbfchar";
        range = [];
      }

      if (
        map[code] !== undefined &&
        map[code] !== null &&
        typeof map[code].toString === "function"
      ) {
        unicode = ("0000" + map[code].toString(16)).slice(-4);
        code = ("0000" + (+code).toString(16)).slice(-4);
        range.push("<" + code + "><" + unicode + ">");
      }
    }

    if (range.length) {
      unicodeMap +=
        "\n" +
        range.length +
        " beginbfchar\n" +
        range.join("\n") +
        "\nendbfchar\n";
    }
    unicodeMap +=
      "endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend";
    return unicodeMap;
  };

  var identityHFunction = function(options) {
    var font = options.font;
    var out = options.out;
    var newObject = options.newObject;
    var putStream = options.putStream;

    if (
      font.metadata instanceof jsPDF.API.TTFFont &&
      font.encoding === "Identity-H"
    ) {
      //Tag with Identity-H
      var widths = font.metadata.Unicode.widths;
      var data = font.metadata.subset.encode(font.metadata.glyIdsUsed, 1);
      var pdfOutput = data;
      var pdfOutput2 = "";
      for (var i = 0; i < pdfOutput.length; i++) {
        pdfOutput2 += String.fromCharCode(pdfOutput[i]);
      }
      var fontTable = newObject();
      putStream({ data: pdfOutput2, addLength1: true, objectId: fontTable });
      out("endobj");

      var cmap = newObject();
      var cmapData = toUnicodeCmap(font.metadata.toUnicode);
      putStream({ data: cmapData, addLength1: true, objectId: cmap });
      out("endobj");

      var fontDescriptor = newObject();
      out("<<");
      out("/Type /FontDescriptor");
      out("/FontName /" + toPDFName(font.fontName));
      out("/FontFile2 " + fontTable + " 0 R");
      out("/FontBBox " + jsPDF.API.PDFObject.convert(font.metadata.bbox));
      out("/Flags " + font.metadata.flags);
      out("/StemV " + font.metadata.stemV);
      out("/ItalicAngle " + font.metadata.italicAngle);
      out("/Ascent " + font.metadata.ascender);
      out("/Descent " + font.metadata.decender);
      out("/CapHeight " + font.metadata.capHeight);
      out(">>");
      out("endobj");

      var DescendantFont = newObject();
      out("<<");
      out("/Type /Font");
      out("/BaseFont /" + toPDFName(font.fontName));
      out("/FontDescriptor " + fontDescriptor + " 0 R");
      out("/W " + jsPDF.API.PDFObject.convert(widths));
      out("/CIDToGIDMap /Identity");
      out("/DW 1000");
      out("/Subtype /CIDFontType2");
      out("/CIDSystemInfo");
      out("<<");
      out("/Supplement 0");
      out("/Registry (Adobe)");
      out("/Ordering (" + font.encoding + ")");
      out(">>");
      out(">>");
      out("endobj");

      font.objectNumber = newObject();
      out("<<");
      out("/Type /Font");
      out("/Subtype /Type0");
      out("/ToUnicode " + cmap + " 0 R");
      out("/BaseFont /" + toPDFName(font.fontName));
      out("/Encoding /" + font.encoding);
      out("/DescendantFonts [" + DescendantFont + " 0 R]");
      out(">>");
      out("endobj");

      font.isAlreadyPutted = true;
    }
  };

  jsPDFAPI.events.push([
    "putFont",
    function(args) {
      identityHFunction(args);
    }
  ]);

  var winAnsiEncodingFunction = function(options) {
    var font = options.font;
    var out = options.out;
    var newObject = options.newObject;
    var putStream = options.putStream;

    if (
      font.metadata instanceof jsPDF.API.TTFFont &&
      font.encoding === "WinAnsiEncoding"
    ) {
      //Tag with WinAnsi encoding
      var data = font.metadata.rawData;
      var pdfOutput = data;
      var pdfOutput2 = "";
      for (var i = 0; i < pdfOutput.length; i++) {
        pdfOutput2 += String.fromCharCode(pdfOutput[i]);
      }
      var fontTable = newObject();
      putStream({ data: pdfOutput2, addLength1: true, objectId: fontTable });
      out("endobj");

      var cmap = newObject();
      var cmapData = toUnicodeCmap(font.metadata.toUnicode);
      putStream({ data: cmapData, addLength1: true, objectId: cmap });
      out("endobj");

      var fontDescriptor = newObject();
      out("<<");
      out("/Descent " + font.metadata.decender);
      out("/CapHeight " + font.metadata.capHeight);
      out("/StemV " + font.metadata.stemV);
      out("/Type /FontDescriptor");
      out("/FontFile2 " + fontTable + " 0 R");
      out("/Flags 96");
      out("/FontBBox " + jsPDF.API.PDFObject.convert(font.metadata.bbox));
      out("/FontName /" + toPDFName(font.fontName));
      out("/ItalicAngle " + font.metadata.italicAngle);
      out("/Ascent " + font.metadata.ascender);
      out(">>");
      out("endobj");
      font.objectNumber = newObject();
      for (var j = 0; j < font.metadata.hmtx.widths.length; j++) {
        font.metadata.hmtx.widths[j] = parseInt(
          font.metadata.hmtx.widths[j] * (1000 / font.metadata.head.unitsPerEm)
        ); //Change the width of Em units to Point units.
      }
      out(
        "<</Subtype/TrueType/Type/Font/ToUnicode " +
          cmap +
          " 0 R/BaseFont/" +
          toPDFName(font.fontName) +
          "/FontDescriptor " +
          fontDescriptor +
          " 0 R" +
          "/Encoding/" +
          font.encoding +
          " /FirstChar 29 /LastChar 255 /Widths " +
          jsPDF.API.PDFObject.convert(font.metadata.hmtx.widths) +
          ">>"
      );
      out("endobj");
      font.isAlreadyPutted = true;
    }
  };

  jsPDFAPI.events.push([
    "putFont",
    function(args) {
      winAnsiEncodingFunction(args);
    }
  ]);

  var utf8TextFunction = function(args) {
    var text = args.text || "";
    var x = args.x;
    var y = args.y;
    var options = args.options || {};
    var mutex = args.mutex || {};

    var pdfEscape = mutex.pdfEscape;
    var activeFontKey = mutex.activeFontKey;
    var fonts = mutex.fonts;
    var key = activeFontKey;

    var str = "",
      s = 0,
      cmapConfirm;
    var strText = "";
    var encoding = fonts[key].encoding;

    if (fonts[key].encoding !== "Identity-H") {
      return {
        text: text,
        x: x,
        y: y,
        options: options,
        mutex: mutex
      };
    }
    strText = text;

    key = activeFontKey;
    if (Array.isArray(text)) {
      strText = text[0];
    }
    for (s = 0; s < strText.length; s += 1) {
      if (fonts[key].metadata.hasOwnProperty("cmap")) {
        cmapConfirm =
          fonts[key].metadata.cmap.unicode.codeMap[strText[s].charCodeAt(0)];
        /*
             if (Object.prototype.toString.call(text) === '[object Array]') {
                var i = 0;
               // for (i = 0; i < text.length; i += 1) {
                    if (Object.prototype.toString.call(text[s]) === '[object Array]') {
                        cmapConfirm = fonts[key].metadata.cmap.unicode.codeMap[strText[s][0].charCodeAt(0)]; //Make sure the cmap has the corresponding glyph id
                    } else {

                    }
                //}

            } else {
                cmapConfirm = fonts[key].metadata.cmap.unicode.codeMap[strText[s].charCodeAt(0)]; //Make sure the cmap has the corresponding glyph id
            }*/
      }
      if (!cmapConfirm) {
        if (
          strText[s].charCodeAt(0) < 256 &&
          fonts[key].metadata.hasOwnProperty("Unicode")
        ) {
          str += strText[s];
        } else {
          str += "";
        }
      } else {
        str += strText[s];
      }
    }
    var result = "";
    if (parseInt(key.slice(1)) < 14 || encoding === "WinAnsiEncoding") {
      //For the default 13 font
      result = pdfEscape(str, key)
        .split("")
        .map(function(cv) {
          return cv.charCodeAt(0).toString(16);
        })
        .join("");
    } else if (encoding === "Identity-H") {
      result = pdfEscape16(str, fonts[key]);
    }
    mutex.isHex = true;

    return {
      text: result,
      x: x,
      y: y,
      options: options,
      mutex: mutex
    };
  };

  var utf8EscapeFunction = function(parms) {
    var text = parms.text || "",
      x = parms.x,
      y = parms.y,
      options = parms.options,
      mutex = parms.mutex;
    var tmpText = [];
    var args = {
      text: text,
      x: x,
      y: y,
      options: options,
      mutex: mutex
    };

    if (Array.isArray(text)) {
      var i = 0;
      for (i = 0; i < text.length; i += 1) {
        if (Array.isArray(text[i])) {
          if (text[i].length === 3) {
            tmpText.push([
              utf8TextFunction(Object.assign({}, args, { text: text[i][0] }))
                .text,
              text[i][1],
              text[i][2]
            ]);
          } else {
            tmpText.push(
              utf8TextFunction(Object.assign({}, args, { text: text[i] })).text
            );
          }
        } else {
          tmpText.push(
            utf8TextFunction(Object.assign({}, args, { text: text[i] })).text
          );
        }
      }
      parms.text = tmpText;
    } else {
      parms.text = utf8TextFunction(
        Object.assign({}, args, { text: text })
      ).text;
    }
  };

  jsPDFAPI.events.push(["postProcessText", utf8EscapeFunction]);
})(jsPDF);

/**
 * @license
 * jsPDF virtual FileSystem functionality
 *
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 */

/**
 * Use the vFS to handle files
 *
 * @name vFS
 * @module
 */
(function(jsPDFAPI) {

  var _initializeVFS = function() {
    if (typeof this.internal.vFS === "undefined") {
      this.internal.vFS = {};
    }
    return true;
  };

  /**
   * Check if the file exists in the vFS
   *
   * @name existsFileInVFS
   * @function
   * @param {string} Possible filename in the vFS.
   * @returns {boolean}
   * @example
   * doc.existsFileInVFS("someFile.txt");
   */
  jsPDFAPI.existsFileInVFS = function(filename) {
    _initializeVFS.call(this);
    return typeof this.internal.vFS[filename] !== "undefined";
  };

  /**
   * Add a file to the vFS
   *
   * @name addFileToVFS
   * @function
   * @param {string} filename The name of the file which should be added.
   * @param {string} filecontent The content of the file.
   * @returns {jsPDF}
   * @example
   * doc.addFileToVFS("someFile.txt", "BADFACE1");
   */
  jsPDFAPI.addFileToVFS = function(filename, filecontent) {
    _initializeVFS.call(this);
    this.internal.vFS[filename] = filecontent;
    return this;
  };

  /**
   * Get the file from the vFS
   *
   * @name getFileFromVFS
   * @function
   * @param {string} The name of the file which gets requested.
   * @returns {string}
   * @example
   * doc.getFileFromVFS("someFile.txt");
   */
  jsPDFAPI.getFileFromVFS = function(filename) {
    _initializeVFS.call(this);

    if (typeof this.internal.vFS[filename] !== "undefined") {
      return this.internal.vFS[filename];
    }
    return null;
  };
})(jsPDF.API);

/**
 * @license
 * Unicode Bidi Engine based on the work of Alex Shensis (@asthensis)
 * MIT License
 */

(function(jsPDF) {
  /**
   * Table of Unicode types.
   *
   * Generated by:
   *
   * var bidi = require("./bidi/index");
   * var bidi_accumulate = bidi.slice(0, 256).concat(bidi.slice(0x0500, 0x0500 + 256 * 3)).
   * concat(bidi.slice(0x2000, 0x2000 + 256)).concat(bidi.slice(0xFB00, 0xFB00 + 256)).
   * concat(bidi.slice(0xFE00, 0xFE00 + 2 * 256));
   *
   * for( var i = 0; i < bidi_accumulate.length; i++) {
   * 	if(bidi_accumulate[i] === undefined || bidi_accumulate[i] === 'ON')
   * 		bidi_accumulate[i] = 'N'; //mark as neutral to conserve space and substitute undefined
   * }
   * var bidiAccumulateStr = 'return [ "' + bidi_accumulate.toString().replace(/,/g, '", "') + '" ];';
   * require("fs").writeFile('unicode-types.js', bidiAccumulateStr);
   *
   * Based on:
   * https://github.com/mathiasbynens/unicode-8.0.0
   */
  var bidiUnicodeTypes = [
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "S",
    "B",
    "S",
    "WS",
    "B",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "B",
    "B",
    "B",
    "S",
    "WS",
    "N",
    "N",
    "ET",
    "ET",
    "ET",
    "N",
    "N",
    "N",
    "N",
    "N",
    "ES",
    "CS",
    "ES",
    "CS",
    "CS",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "CS",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "N",
    "N",
    "N",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "B",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "CS",
    "N",
    "ET",
    "ET",
    "ET",
    "ET",
    "N",
    "N",
    "N",
    "N",
    "L",
    "N",
    "N",
    "BN",
    "N",
    "N",
    "ET",
    "ET",
    "EN",
    "EN",
    "N",
    "L",
    "N",
    "N",
    "N",
    "EN",
    "L",
    "N",
    "N",
    "N",
    "N",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "L",
    "N",
    "N",
    "N",
    "N",
    "N",
    "ET",
    "N",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "R",
    "NSM",
    "R",
    "NSM",
    "NSM",
    "R",
    "NSM",
    "NSM",
    "R",
    "NSM",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "N",
    "N",
    "N",
    "N",
    "N",
    "R",
    "R",
    "R",
    "R",
    "R",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "AN",
    "AN",
    "AN",
    "AN",
    "AN",
    "AN",
    "N",
    "N",
    "AL",
    "ET",
    "ET",
    "AL",
    "CS",
    "AL",
    "N",
    "N",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "AL",
    "AL",
    "N",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "AN",
    "AN",
    "AN",
    "AN",
    "AN",
    "AN",
    "AN",
    "AN",
    "AN",
    "AN",
    "ET",
    "AN",
    "AN",
    "AL",
    "AL",
    "AL",
    "NSM",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "AN",
    "N",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "AL",
    "AL",
    "NSM",
    "NSM",
    "N",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "AL",
    "AL",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "N",
    "AL",
    "AL",
    "NSM",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "N",
    "N",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "AL",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "R",
    "R",
    "N",
    "N",
    "N",
    "N",
    "R",
    "N",
    "N",
    "N",
    "N",
    "N",
    "WS",
    "WS",
    "WS",
    "WS",
    "WS",
    "WS",
    "WS",
    "WS",
    "WS",
    "WS",
    "WS",
    "BN",
    "BN",
    "BN",
    "L",
    "R",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "WS",
    "B",
    "LRE",
    "RLE",
    "PDF",
    "LRO",
    "RLO",
    "CS",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "CS",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "WS",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "N",
    "LRI",
    "RLI",
    "FSI",
    "PDI",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "BN",
    "EN",
    "L",
    "N",
    "N",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "ES",
    "ES",
    "N",
    "N",
    "N",
    "L",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "ES",
    "ES",
    "N",
    "N",
    "N",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "N",
    "N",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "ET",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "N",
    "N",
    "N",
    "N",
    "R",
    "NSM",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "ES",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "N",
    "R",
    "R",
    "R",
    "R",
    "R",
    "N",
    "R",
    "N",
    "R",
    "R",
    "N",
    "R",
    "R",
    "N",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "R",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "NSM",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "CS",
    "N",
    "CS",
    "N",
    "N",
    "CS",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "ET",
    "N",
    "N",
    "ES",
    "ES",
    "N",
    "N",
    "N",
    "N",
    "N",
    "ET",
    "ET",
    "N",
    "N",
    "N",
    "N",
    "N",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "N",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "AL",
    "N",
    "N",
    "BN",
    "N",
    "N",
    "N",
    "ET",
    "ET",
    "ET",
    "N",
    "N",
    "N",
    "N",
    "N",
    "ES",
    "CS",
    "ES",
    "CS",
    "CS",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "EN",
    "CS",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "N",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "N",
    "L",
    "L",
    "L",
    "L",
    "L",
    "L",
    "N",
    "N",
    "L",
    "L",
    "L",
    "N",
    "N",
    "N",
    "ET",
    "ET",
    "N",
    "N",
    "N",
    "ET",
    "ET",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N",
    "N"
  ];

  /**
   * Unicode Bidi algorithm compliant Bidi engine.
   * For reference see http://unicode.org/reports/tr9/
   */

  /**
   * constructor ( options )
   *
   * Initializes Bidi engine
   *
   * @param {Object} See 'setOptions' below for detailed description.
   * options are cashed between invocation of 'doBidiReorder' method
   *
   * sample usage pattern of BidiEngine:
   * var opt = {
   * 	isInputVisual: true,
   * 	isInputRtl: false,
   * 	isOutputVisual: false,
   * 	isOutputRtl: false,
   * 	isSymmetricSwapping: true
   * }
   * var sourceToTarget = [], levels = [];
   * var bidiEng = Globalize.bidiEngine(opt);
   * var src = "text string to be reordered";
   * var ret = bidiEng.doBidiReorder(src, sourceToTarget, levels);
   */

  jsPDF.__bidiEngine__ = jsPDF.prototype.__bidiEngine__ = function(options) {
    var _UNICODE_TYPES = _bidiUnicodeTypes;

    var _STATE_TABLE_LTR = [
      [0, 3, 0, 1, 0, 0, 0],
      [0, 3, 0, 1, 2, 2, 0],
      [0, 3, 0, 0x11, 2, 0, 1],
      [0, 3, 5, 5, 4, 1, 0],
      [0, 3, 0x15, 0x15, 4, 0, 1],
      [0, 3, 5, 5, 4, 2, 0]
    ];

    var _STATE_TABLE_RTL = [
      [2, 0, 1, 1, 0, 1, 0],
      [2, 0, 1, 1, 0, 2, 0],
      [2, 0, 2, 1, 3, 2, 0],
      [2, 0, 2, 0x21, 3, 1, 1]
    ];

    var _TYPE_NAMES_MAP = { L: 0, R: 1, EN: 2, AN: 3, N: 4, B: 5, S: 6 };

    var _UNICODE_RANGES_MAP = {
      0: 0,
      5: 1,
      6: 2,
      7: 3,
      0x20: 4,
      0xfb: 5,
      0xfe: 6,
      0xff: 7
    };

    var _SWAP_TABLE = [
      "\u0028",
      "\u0029",
      "\u0028",
      "\u003C",
      "\u003E",
      "\u003C",
      "\u005B",
      "\u005D",
      "\u005B",
      "\u007B",
      "\u007D",
      "\u007B",
      "\u00AB",
      "\u00BB",
      "\u00AB",
      "\u2039",
      "\u203A",
      "\u2039",
      "\u2045",
      "\u2046",
      "\u2045",
      "\u207D",
      "\u207E",
      "\u207D",
      "\u208D",
      "\u208E",
      "\u208D",
      "\u2264",
      "\u2265",
      "\u2264",
      "\u2329",
      "\u232A",
      "\u2329",
      "\uFE59",
      "\uFE5A",
      "\uFE59",
      "\uFE5B",
      "\uFE5C",
      "\uFE5B",
      "\uFE5D",
      "\uFE5E",
      "\uFE5D",
      "\uFE64",
      "\uFE65",
      "\uFE64"
    ];

    var _LTR_RANGES_REG_EXPR = new RegExp(
      /^([1-4|9]|1[0-9]|2[0-9]|3[0168]|4[04589]|5[012]|7[78]|159|16[0-9]|17[0-2]|21[569]|22[03489]|250)$/
    );

    var _lastArabic = false,
      _hasUbatB,
      _hasUbatS,
      DIR_LTR = 0,
      DIR_RTL = 1,
      _isInVisual,
      _isInRtl,
      _isOutVisual,
      _isOutRtl,
      _isSymmetricSwapping,
      _dir = DIR_LTR;

    this.__bidiEngine__ = {};

    var _init = function(text, sourceToTargetMap) {
      if (sourceToTargetMap) {
        for (var i = 0; i < text.length; i++) {
          sourceToTargetMap[i] = i;
        }
      }
      if (_isInRtl === undefined) {
        _isInRtl = _isContextualDirRtl(text);
      }
      if (_isOutRtl === undefined) {
        _isOutRtl = _isContextualDirRtl(text);
      }
    };

    // for reference see 3.2 in http://unicode.org/reports/tr9/
    //
    var _getCharType = function(ch) {
      var charCode = ch.charCodeAt(),
        range = charCode >> 8,
        rangeIdx = _UNICODE_RANGES_MAP[range];

      if (rangeIdx !== undefined) {
        return _UNICODE_TYPES[rangeIdx * 256 + (charCode & 0xff)];
      } else if (range === 0xfc || range === 0xfd) {
        return "AL";
      } else if (_LTR_RANGES_REG_EXPR.test(range)) {
        //unlikely case
        return "L";
      } else if (range === 8) {
        // even less likely
        return "R";
      }
      return "N"; //undefined type, mark as neutral
    };

    var _isContextualDirRtl = function(text) {
      for (var i = 0, charType; i < text.length; i++) {
        charType = _getCharType(text.charAt(i));
        if (charType === "L") {
          return false;
        } else if (charType === "R") {
          return true;
        }
      }
      return false;
    };

    // for reference see 3.3.4 & 3.3.5 in http://unicode.org/reports/tr9/
    //
    var _resolveCharType = function(chars, types, resolvedTypes, index) {
      var cType = types[index],
        wType,
        nType,
        i,
        len;
      switch (cType) {
        case "L":
        case "R":
          _lastArabic = false;
          break;
        case "N":
        case "AN":
          break;

        case "EN":
          if (_lastArabic) {
            cType = "AN";
          }
          break;

        case "AL":
          _lastArabic = true;
          cType = "R";
          break;

        case "WS":
          cType = "N";
          break;

        case "CS":
          if (
            index < 1 ||
            index + 1 >= types.length ||
            ((wType = resolvedTypes[index - 1]) !== "EN" && wType !== "AN") ||
            ((nType = types[index + 1]) !== "EN" && nType !== "AN")
          ) {
            cType = "N";
          } else if (_lastArabic) {
            nType = "AN";
          }
          cType = nType === wType ? nType : "N";
          break;

        case "ES":
          wType = index > 0 ? resolvedTypes[index - 1] : "B";
          cType =
            wType === "EN" &&
            index + 1 < types.length &&
            types[index + 1] === "EN"
              ? "EN"
              : "N";
          break;

        case "ET":
          if (index > 0 && resolvedTypes[index - 1] === "EN") {
            cType = "EN";
            break;
          } else if (_lastArabic) {
            cType = "N";
            break;
          }
          i = index + 1;
          len = types.length;
          while (i < len && types[i] === "ET") {
            i++;
          }
          if (i < len && types[i] === "EN") {
            cType = "EN";
          } else {
            cType = "N";
          }
          break;

        case "NSM":
          if (_isInVisual && !_isInRtl) {
            //V->L
            len = types.length;
            i = index + 1;
            while (i < len && types[i] === "NSM") {
              i++;
            }
            if (i < len) {
              var c = chars[index];
              var rtlCandidate = (c >= 0x0591 && c <= 0x08ff) || c === 0xfb1e;
              wType = types[i];
              if (rtlCandidate && (wType === "R" || wType === "AL")) {
                cType = "R";
                break;
              }
            }
          }
          if (index < 1 || (wType = types[index - 1]) === "B") {
            cType = "N";
          } else {
            cType = resolvedTypes[index - 1];
          }
          break;

        case "B":
          _lastArabic = false;
          _hasUbatB = true;
          cType = _dir;
          break;

        case "S":
          _hasUbatS = true;
          cType = "N";
          break;

        case "LRE":
        case "RLE":
        case "LRO":
        case "RLO":
        case "PDF":
          _lastArabic = false;
          break;
        case "BN":
          cType = "N";
          break;
      }
      return cType;
    };

    var _handleUbatS = function(types, levels, length) {
      for (var i = 0; i < length; i++) {
        if (types[i] === "S") {
          levels[i] = _dir;
          for (var j = i - 1; j >= 0; j--) {
            if (types[j] === "WS") {
              levels[j] = _dir;
            } else {
              break;
            }
          }
        }
      }
    };

    var _invertString = function(text, sourceToTargetMap, levels) {
      var charArray = text.split("");
      if (levels) {
        _computeLevels(charArray, levels, { hiLevel: _dir });
      }
      charArray.reverse();
      sourceToTargetMap && sourceToTargetMap.reverse();
      return charArray.join("");
    };

    // For reference see 3.3 in http://unicode.org/reports/tr9/
    //
    var _computeLevels = function(chars, levels, params) {
      var action,
        condition,
        i,
        index,
        newLevel,
        prevState,
        condPos = -1,
        len = chars.length,
        newState = 0,
        resolvedTypes = [],
        stateTable = _dir ? _STATE_TABLE_RTL : _STATE_TABLE_LTR,
        types = [];

      _lastArabic = false;
      _hasUbatB = false;
      _hasUbatS = false;
      for (i = 0; i < len; i++) {
        types[i] = _getCharType(chars[i]);
      }
      for (index = 0; index < len; index++) {
        prevState = newState;
        resolvedTypes[index] = _resolveCharType(
          chars,
          types,
          resolvedTypes,
          index
        );
        newState = stateTable[prevState][_TYPE_NAMES_MAP[resolvedTypes[index]]];
        action = newState & 0xf0;
        newState &= 0x0f;
        levels[index] = newLevel = stateTable[newState][5];
        if (action > 0) {
          if (action === 0x10) {
            for (i = condPos; i < index; i++) {
              levels[i] = 1;
            }
            condPos = -1;
          } else {
            condPos = -1;
          }
        }
        condition = stateTable[newState][6];
        if (condition) {
          if (condPos === -1) {
            condPos = index;
          }
        } else {
          if (condPos > -1) {
            for (i = condPos; i < index; i++) {
              levels[i] = newLevel;
            }
            condPos = -1;
          }
        }
        if (types[index] === "B") {
          levels[index] = 0;
        }
        params.hiLevel |= newLevel;
      }
      if (_hasUbatS) {
        _handleUbatS(types, levels, len);
      }
    };

    // for reference see 3.4 in http://unicode.org/reports/tr9/
    //
    var _invertByLevel = function(
      level,
      charArray,
      sourceToTargetMap,
      levels,
      params
    ) {
      if (params.hiLevel < level) {
        return;
      }
      if (level === 1 && _dir === DIR_RTL && !_hasUbatB) {
        charArray.reverse();
        sourceToTargetMap && sourceToTargetMap.reverse();
        return;
      }
      var ch,
        high,
        end,
        low,
        len = charArray.length,
        start = 0;

      while (start < len) {
        if (levels[start] >= level) {
          end = start + 1;
          while (end < len && levels[end] >= level) {
            end++;
          }
          for (low = start, high = end - 1; low < high; low++, high--) {
            ch = charArray[low];
            charArray[low] = charArray[high];
            charArray[high] = ch;
            if (sourceToTargetMap) {
              ch = sourceToTargetMap[low];
              sourceToTargetMap[low] = sourceToTargetMap[high];
              sourceToTargetMap[high] = ch;
            }
          }
          start = end;
        }
        start++;
      }
    };

    // for reference see 7 & BD16 in http://unicode.org/reports/tr9/
    //
    var _symmetricSwap = function(charArray, levels, params) {
      if (params.hiLevel !== 0 && _isSymmetricSwapping) {
        for (var i = 0, index; i < charArray.length; i++) {
          if (levels[i] === 1) {
            index = _SWAP_TABLE.indexOf(charArray[i]);
            if (index >= 0) {
              charArray[i] = _SWAP_TABLE[index + 1];
            }
          }
        }
      }
    };

    var _reorder = function(text, sourceToTargetMap, levels) {
      var charArray = text.split(""),
        params = { hiLevel: _dir };

      if (!levels) {
        levels = [];
      }
      _computeLevels(charArray, levels, params);
      _symmetricSwap(charArray, levels, params);
      _invertByLevel(DIR_RTL + 1, charArray, sourceToTargetMap, levels, params);
      _invertByLevel(DIR_RTL, charArray, sourceToTargetMap, levels, params);
      return charArray.join("");
    };

    // doBidiReorder( text, sourceToTargetMap, levels )
    // Performs Bidi reordering by implementing Unicode Bidi algorithm.
    // Returns reordered string
    // @text [String]:
    // - input string to be reordered, this is input parameter
    // $sourceToTargetMap [Array] (optional)
    // - resultant mapping between input and output strings, this is output parameter
    // $levels [Array] (optional)
    // - array of calculated Bidi levels, , this is output parameter
    this.__bidiEngine__.doBidiReorder = function(
      text,
      sourceToTargetMap,
      levels
    ) {
      _init(text, sourceToTargetMap);
      if (!_isInVisual && _isOutVisual && !_isOutRtl) {
        // LLTR->VLTR, LRTL->VLTR
        _dir = _isInRtl ? DIR_RTL : DIR_LTR;
        text = _reorder(text, sourceToTargetMap, levels);
      } else if (_isInVisual && _isOutVisual && _isInRtl ^ _isOutRtl) {
        // VRTL->VLTR, VLTR->VRTL
        _dir = _isInRtl ? DIR_RTL : DIR_LTR;
        text = _invertString(text, sourceToTargetMap, levels);
      } else if (!_isInVisual && _isOutVisual && _isOutRtl) {
        // LLTR->VRTL, LRTL->VRTL
        _dir = _isInRtl ? DIR_RTL : DIR_LTR;
        text = _reorder(text, sourceToTargetMap, levels);
        text = _invertString(text, sourceToTargetMap);
      } else if (_isInVisual && !_isInRtl && !_isOutVisual && !_isOutRtl) {
        // VLTR->LLTR
        _dir = DIR_LTR;
        text = _reorder(text, sourceToTargetMap, levels);
      } else if (_isInVisual && !_isOutVisual && _isInRtl ^ _isOutRtl) {
        // VLTR->LRTL, VRTL->LLTR
        text = _invertString(text, sourceToTargetMap);
        if (_isInRtl) {
          //LLTR -> VLTR
          _dir = DIR_LTR;
          text = _reorder(text, sourceToTargetMap, levels);
        } else {
          //LRTL -> VRTL
          _dir = DIR_RTL;
          text = _reorder(text, sourceToTargetMap, levels);
          text = _invertString(text, sourceToTargetMap);
        }
      } else if (_isInVisual && _isInRtl && !_isOutVisual && _isOutRtl) {
        //  VRTL->LRTL
        _dir = DIR_RTL;
        text = _reorder(text, sourceToTargetMap, levels);
        text = _invertString(text, sourceToTargetMap);
      } else if (!_isInVisual && !_isOutVisual && _isInRtl ^ _isOutRtl) {
        // LRTL->LLTR, LLTR->LRTL
        var isSymmetricSwappingOrig = _isSymmetricSwapping;
        if (_isInRtl) {
          //LRTL->LLTR
          _dir = DIR_RTL;
          text = _reorder(text, sourceToTargetMap, levels);
          _dir = DIR_LTR;
          _isSymmetricSwapping = false;
          text = _reorder(text, sourceToTargetMap, levels);
          _isSymmetricSwapping = isSymmetricSwappingOrig;
        } else {
          //LLTR->LRTL
          _dir = DIR_LTR;
          text = _reorder(text, sourceToTargetMap, levels);
          text = _invertString(text, sourceToTargetMap);
          _dir = DIR_RTL;
          _isSymmetricSwapping = false;
          text = _reorder(text, sourceToTargetMap, levels);
          _isSymmetricSwapping = isSymmetricSwappingOrig;
          text = _invertString(text, sourceToTargetMap);
        }
      }
      return text;
    };

    /**
     * @name setOptions( options )
     * @function
     * Sets options for Bidi conversion
     * @param {Object}:
     * - isInputVisual {boolean} (defaults to false): allowed values: true(Visual mode), false(Logical mode)
     * - isInputRtl {boolean}: allowed values true(Right-to-left direction), false (Left-to-right directiion), undefined(Contectual direction, i.e.direction defined by first strong character of input string)
     * - isOutputVisual {boolean} (defaults to false): allowed values: true(Visual mode), false(Logical mode)
     * - isOutputRtl {boolean}: allowed values true(Right-to-left direction), false (Left-to-right directiion), undefined(Contectual direction, i.e.direction defined by first strong characterof input string)
     * - isSymmetricSwapping {boolean} (defaults to false): allowed values true(needs symmetric swapping), false (no need in symmetric swapping),
     */
    this.__bidiEngine__.setOptions = function(options) {
      if (options) {
        _isInVisual = options.isInputVisual;
        _isOutVisual = options.isOutputVisual;
        _isInRtl = options.isInputRtl;
        _isOutRtl = options.isOutputRtl;
        _isSymmetricSwapping = options.isSymmetricSwapping;
      }
    };

    this.__bidiEngine__.setOptions(options);
    return this.__bidiEngine__;
  };

  var _bidiUnicodeTypes = bidiUnicodeTypes;

  var bidiEngine = new jsPDF.__bidiEngine__({ isInputVisual: true });

  var bidiEngineFunction = function(args) {
    var text = args.text;
    args.x;
    args.y;
    var options = args.options || {};
    args.mutex || {};
    options.lang;
    var tmpText = [];

    options.isInputVisual =
      typeof options.isInputVisual === "boolean" ? options.isInputVisual : true;
    bidiEngine.setOptions(options);

    if (Object.prototype.toString.call(text) === "[object Array]") {
      var i = 0;
      tmpText = [];
      for (i = 0; i < text.length; i += 1) {
        if (Object.prototype.toString.call(text[i]) === "[object Array]") {
          tmpText.push([
            bidiEngine.doBidiReorder(text[i][0]),
            text[i][1],
            text[i][2]
          ]);
        } else {
          tmpText.push([bidiEngine.doBidiReorder(text[i])]);
        }
      }
      args.text = tmpText;
    } else {
      args.text = bidiEngine.doBidiReorder(text);
    }
    bidiEngine.setOptions({ isInputVisual: true });
  };

  jsPDF.API.events.push(["postProcessText", bidiEngineFunction]);
})(jsPDF);

/* eslint-disable no-control-regex */

jsPDF.API.TTFFont = (function() {
  /************************************************************************/
  /* function : open                                                       */
  /* comment : Decode the encoded ttf content and create a TTFFont object. */
  /************************************************************************/
  TTFFont.open = function(file) {
    return new TTFFont(file);
  };
  /***************************************************************/
  /* function : TTFFont gernerator                               */
  /* comment : Decode TTF contents are parsed, Data,             */
  /* Subset object is created, and registerTTF function is called.*/
  /***************************************************************/
  function TTFFont(rawData) {
    var data;
    this.rawData = rawData;
    data = this.contents = new Data(rawData);
    this.contents.pos = 4;
    if (data.readString(4) === "ttcf") {
      throw new Error("TTCF not supported.");
    } else {
      data.pos = 0;
      this.parse();
      this.subset = new Subset(this);
      this.registerTTF();
    }
  }
  /********************************************************/
  /* function : parse                                     */
  /* comment : TTF Parses the file contents by each table.*/
  /********************************************************/
  TTFFont.prototype.parse = function() {
    this.directory = new Directory(this.contents);
    this.head = new HeadTable(this);
    this.name = new NameTable(this);
    this.cmap = new CmapTable(this);
    this.toUnicode = {};
    this.hhea = new HheaTable(this);
    this.maxp = new MaxpTable(this);
    this.hmtx = new HmtxTable(this);
    this.post = new PostTable(this);
    this.os2 = new OS2Table(this);
    this.loca = new LocaTable(this);
    this.glyf = new GlyfTable(this);
    this.ascender =
      (this.os2.exists && this.os2.ascender) || this.hhea.ascender;
    this.decender =
      (this.os2.exists && this.os2.decender) || this.hhea.decender;
    this.lineGap = (this.os2.exists && this.os2.lineGap) || this.hhea.lineGap;
    return (this.bbox = [
      this.head.xMin,
      this.head.yMin,
      this.head.xMax,
      this.head.yMax
    ]);
  };
  /***************************************************************/
  /* function : registerTTF                                      */
  /* comment : Get the value to assign pdf font descriptors.     */
  /***************************************************************/
  TTFFont.prototype.registerTTF = function() {
    var e, hi, low, raw, _ref;
    this.scaleFactor = 1000.0 / this.head.unitsPerEm;
    this.bbox = function() {
      var _i, _len, _ref, _results;
      _ref = this.bbox;
      _results = [];
      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
        e = _ref[_i];
        _results.push(Math.round(e * this.scaleFactor));
      }
      return _results;
    }.call(this);
    this.stemV = 0;
    if (this.post.exists) {
      raw = this.post.italic_angle;
      hi = raw >> 16;
      low = raw & 0xff;
      if ((hi & 0x8000) !== 0) {
        hi = -((hi ^ 0xffff) + 1);
      }
      this.italicAngle = +("" + hi + "." + low);
    } else {
      this.italicAngle = 0;
    }
    this.ascender = Math.round(this.ascender * this.scaleFactor);
    this.decender = Math.round(this.decender * this.scaleFactor);
    this.lineGap = Math.round(this.lineGap * this.scaleFactor);
    this.capHeight = (this.os2.exists && this.os2.capHeight) || this.ascender;
    this.xHeight = (this.os2.exists && this.os2.xHeight) || 0;
    this.familyClass = ((this.os2.exists && this.os2.familyClass) || 0) >> 8;
    this.isSerif =
      (_ref = this.familyClass) === 1 ||
      _ref === 2 ||
      _ref === 3 ||
      _ref === 4 ||
      _ref === 5 ||
      _ref === 7;
    this.isScript = this.familyClass === 10;
    this.flags = 0;
    if (this.post.isFixedPitch) {
      this.flags |= 1 << 0;
    }
    if (this.isSerif) {
      this.flags |= 1 << 1;
    }
    if (this.isScript) {
      this.flags |= 1 << 3;
    }
    if (this.italicAngle !== 0) {
      this.flags |= 1 << 6;
    }
    this.flags |= 1 << 5;
    if (!this.cmap.unicode) {
      throw new Error("No unicode cmap for font");
    }
  };
  TTFFont.prototype.characterToGlyph = function(character) {
    var _ref;
    return (
      ((_ref = this.cmap.unicode) != null ? _ref.codeMap[character] : void 0) ||
      0
    );
  };
  TTFFont.prototype.widthOfGlyph = function(glyph) {
    var scale;
    scale = 1000.0 / this.head.unitsPerEm;
    return this.hmtx.forGlyph(glyph).advance * scale;
  };
  TTFFont.prototype.widthOfString = function(string, size, charSpace) {
    var charCode, i, scale, width, _ref;
    string = "" + string;
    width = 0;
    for (
      i = 0, _ref = string.length;
      0 <= _ref ? i < _ref : i > _ref;
      i = 0 <= _ref ? ++i : --i
    ) {
      charCode = string.charCodeAt(i);
      width +=
        this.widthOfGlyph(this.characterToGlyph(charCode)) +
          charSpace * (1000 / size) || 0;
    }
    scale = size / 1000;
    return width * scale;
  };
  TTFFont.prototype.lineHeight = function(size, includeGap) {
    var gap;
    if (includeGap == null) {
      includeGap = false;
    }
    gap = includeGap ? this.lineGap : 0;
    return ((this.ascender + gap - this.decender) / 1000) * size;
  };
  return TTFFont;
})();

/************************************************************************************************/
/* function : Data                                                                              */
/* comment : The ttf data decoded and stored in an array is read and written to the Data object.*/
/************************************************************************************************/
var Data = (function() {
  function Data(data) {
    this.data = data != null ? data : [];
    this.pos = 0;
    this.length = this.data.length;
  }
  Data.prototype.readByte = function() {
    return this.data[this.pos++];
  };
  Data.prototype.writeByte = function(byte) {
    return (this.data[this.pos++] = byte);
  };
  Data.prototype.readUInt32 = function() {
    var b1, b2, b3, b4;
    b1 = this.readByte() * 0x1000000;
    b2 = this.readByte() << 16;
    b3 = this.readByte() << 8;
    b4 = this.readByte();
    return b1 + b2 + b3 + b4;
  };
  Data.prototype.writeUInt32 = function(val) {
    this.writeByte((val >>> 24) & 0xff);
    this.writeByte((val >> 16) & 0xff);
    this.writeByte((val >> 8) & 0xff);
    return this.writeByte(val & 0xff);
  };
  Data.prototype.readInt32 = function() {
    var int;
    int = this.readUInt32();
    if (int >= 0x80000000) {
      return int - 0x100000000;
    } else {
      return int;
    }
  };
  Data.prototype.writeInt32 = function(val) {
    if (val < 0) {
      val += 0x100000000;
    }
    return this.writeUInt32(val);
  };
  Data.prototype.readUInt16 = function() {
    var b1, b2;
    b1 = this.readByte() << 8;
    b2 = this.readByte();
    return b1 | b2;
  };
  Data.prototype.writeUInt16 = function(val) {
    this.writeByte((val >> 8) & 0xff);
    return this.writeByte(val & 0xff);
  };
  Data.prototype.readInt16 = function() {
    var int;
    int = this.readUInt16();
    if (int >= 0x8000) {
      return int - 0x10000;
    } else {
      return int;
    }
  };
  Data.prototype.writeInt16 = function(val) {
    if (val < 0) {
      val += 0x10000;
    }
    return this.writeUInt16(val);
  };
  Data.prototype.readString = function(length) {
    var i, ret;
    ret = [];
    for (
      i = 0;
      0 <= length ? i < length : i > length;
      i = 0 <= length ? ++i : --i
    ) {
      ret[i] = String.fromCharCode(this.readByte());
    }
    return ret.join("");
  };
  Data.prototype.writeString = function(val) {
    var i, _ref, _results;
    _results = [];
    for (
      i = 0, _ref = val.length;
      0 <= _ref ? i < _ref : i > _ref;
      i = 0 <= _ref ? ++i : --i
    ) {
      _results.push(this.writeByte(val.charCodeAt(i)));
    }
    return _results;
  };
  /*Data.prototype.stringAt = function (pos, length) {
            this.pos = pos;
            return this.readString(length);
        };*/
  Data.prototype.readShort = function() {
    return this.readInt16();
  };
  Data.prototype.writeShort = function(val) {
    return this.writeInt16(val);
  };
  Data.prototype.readLongLong = function() {
    var b1, b2, b3, b4, b5, b6, b7, b8;
    b1 = this.readByte();
    b2 = this.readByte();
    b3 = this.readByte();
    b4 = this.readByte();
    b5 = this.readByte();
    b6 = this.readByte();
    b7 = this.readByte();
    b8 = this.readByte();
    if (b1 & 0x80) {
      return (
        ((b1 ^ 0xff) * 0x100000000000000 +
          (b2 ^ 0xff) * 0x1000000000000 +
          (b3 ^ 0xff) * 0x10000000000 +
          (b4 ^ 0xff) * 0x100000000 +
          (b5 ^ 0xff) * 0x1000000 +
          (b6 ^ 0xff) * 0x10000 +
          (b7 ^ 0xff) * 0x100 +
          (b8 ^ 0xff) +
          1) *
        -1
      );
    }
    return (
      b1 * 0x100000000000000 +
      b2 * 0x1000000000000 +
      b3 * 0x10000000000 +
      b4 * 0x100000000 +
      b5 * 0x1000000 +
      b6 * 0x10000 +
      b7 * 0x100 +
      b8
    );
  };
  Data.prototype.writeLongLong = function(val) {
    var high, low;
    high = Math.floor(val / 0x100000000);
    low = val & 0xffffffff;
    this.writeByte((high >> 24) & 0xff);
    this.writeByte((high >> 16) & 0xff);
    this.writeByte((high >> 8) & 0xff);
    this.writeByte(high & 0xff);
    this.writeByte((low >> 24) & 0xff);
    this.writeByte((low >> 16) & 0xff);
    this.writeByte((low >> 8) & 0xff);
    return this.writeByte(low & 0xff);
  };
  Data.prototype.readInt = function() {
    return this.readInt32();
  };
  Data.prototype.writeInt = function(val) {
    return this.writeInt32(val);
  };
  /*Data.prototype.slice = function (start, end) {
            return this.data.slice(start, end);
        };*/
  Data.prototype.read = function(bytes) {
    var buf, i;
    buf = [];
    for (
      i = 0;
      0 <= bytes ? i < bytes : i > bytes;
      i = 0 <= bytes ? ++i : --i
    ) {
      buf.push(this.readByte());
    }
    return buf;
  };
  Data.prototype.write = function(bytes) {
    var byte, i, _len, _results;
    _results = [];
    for (i = 0, _len = bytes.length; i < _len; i++) {
      byte = bytes[i];
      _results.push(this.writeByte(byte));
    }
    return _results;
  };
  return Data;
})();

var Directory = (function() {
  var checksum;

  /*****************************************************************************************************/
  /* function : Directory generator                                                                    */
  /* comment : Initialize the offset, tag, length, and checksum for each table for the font to be used.*/
  /*****************************************************************************************************/
  function Directory(data) {
    var entry, i, _ref;
    this.scalarType = data.readInt();
    this.tableCount = data.readShort();
    this.searchRange = data.readShort();
    this.entrySelector = data.readShort();
    this.rangeShift = data.readShort();
    this.tables = {};
    for (
      i = 0, _ref = this.tableCount;
      0 <= _ref ? i < _ref : i > _ref;
      i = 0 <= _ref ? ++i : --i
    ) {
      entry = {
        tag: data.readString(4),
        checksum: data.readInt(),
        offset: data.readInt(),
        length: data.readInt()
      };
      this.tables[entry.tag] = entry;
    }
  }
  /********************************************************************************************************/
  /* function : encode                                                                                    */
  /* comment : It encodes and stores the font table object and information used for the directory object. */
  /********************************************************************************************************/
  Directory.prototype.encode = function(tables) {
    var adjustment,
      directory,
      directoryLength,
      entrySelector,
      headOffset,
      log2,
      offset,
      rangeShift,
      searchRange,
      sum,
      table,
      tableCount,
      tableData,
      tag;
    tableCount = Object.keys(tables).length;
    log2 = Math.log(2);
    searchRange = Math.floor(Math.log(tableCount) / log2) * 16;
    entrySelector = Math.floor(searchRange / log2);
    rangeShift = tableCount * 16 - searchRange;
    directory = new Data();
    directory.writeInt(this.scalarType);
    directory.writeShort(tableCount);
    directory.writeShort(searchRange);
    directory.writeShort(entrySelector);
    directory.writeShort(rangeShift);
    directoryLength = tableCount * 16;
    offset = directory.pos + directoryLength;
    headOffset = null;
    tableData = [];
    for (tag in tables) {
      table = tables[tag];
      directory.writeString(tag);
      directory.writeInt(checksum(table));
      directory.writeInt(offset);
      directory.writeInt(table.length);
      tableData = tableData.concat(table);
      if (tag === "head") {
        headOffset = offset;
      }
      offset += table.length;
      while (offset % 4) {
        tableData.push(0);
        offset++;
      }
    }
    directory.write(tableData);
    sum = checksum(directory.data);
    adjustment = 0xb1b0afba - sum;
    directory.pos = headOffset + 8;
    directory.writeUInt32(adjustment);
    return directory.data;
  };
  /***************************************************************/
  /* function : checksum                                         */
  /* comment : Duplicate the table for the tag.                  */
  /***************************************************************/
  checksum = function(data) {
    var i, sum, tmp, _ref;
    data = __slice.call(data);
    while (data.length % 4) {
      data.push(0);
    }
    tmp = new Data(data);
    sum = 0;
    for (i = 0, _ref = data.length; i < _ref; i = i += 4) {
      sum += tmp.readUInt32();
    }
    return sum & 0xffffffff;
  };
  return Directory;
})();

var Table,
  __hasProp = {}.hasOwnProperty,
  __extends$1 = function(child, parent) {
    for (var key in parent) {
      if (__hasProp.call(parent, key)) child[key] = parent[key];
    }

    function ctor() {
      this.constructor = child;
    }
    ctor.prototype = parent.prototype;
    child.prototype = new ctor();
    child.__super__ = parent.prototype;
    return child;
  };

/***************************************************************/
/* function : Table                                            */
/* comment : Save info for each table, and parse the table.    */
/***************************************************************/
Table = (function() {
  function Table(file) {
    var info;
    this.file = file;
    info = this.file.directory.tables[this.tag];
    this.exists = !!info;
    if (info) {
      (this.offset = info.offset), (this.length = info.length);
      this.parse(this.file.contents);
    }
  }
  Table.prototype.parse = function() {};
  Table.prototype.encode = function() {};
  Table.prototype.raw = function() {
    if (!this.exists) {
      return null;
    }
    this.file.contents.pos = this.offset;
    return this.file.contents.read(this.length);
  };
  return Table;
})();

var HeadTable = (function(_super) {
  __extends$1(HeadTable, _super);

  function HeadTable() {
    return HeadTable.__super__.constructor.apply(this, arguments);
  }
  HeadTable.prototype.tag = "head";
  HeadTable.prototype.parse = function(data) {
    data.pos = this.offset;
    this.version = data.readInt();
    this.revision = data.readInt();
    this.checkSumAdjustment = data.readInt();
    this.magicNumber = data.readInt();
    this.flags = data.readShort();
    this.unitsPerEm = data.readShort();
    this.created = data.readLongLong();
    this.modified = data.readLongLong();
    this.xMin = data.readShort();
    this.yMin = data.readShort();
    this.xMax = data.readShort();
    this.yMax = data.readShort();
    this.macStyle = data.readShort();
    this.lowestRecPPEM = data.readShort();
    this.fontDirectionHint = data.readShort();
    this.indexToLocFormat = data.readShort();
    return (this.glyphDataFormat = data.readShort());
  };
  HeadTable.prototype.encode = function(indexToLocFormat) {
    var table;
    table = new Data();
    table.writeInt(this.version);
    table.writeInt(this.revision);
    table.writeInt(this.checkSumAdjustment);
    table.writeInt(this.magicNumber);
    table.writeShort(this.flags);
    table.writeShort(this.unitsPerEm);
    table.writeLongLong(this.created);
    table.writeLongLong(this.modified);
    table.writeShort(this.xMin);
    table.writeShort(this.yMin);
    table.writeShort(this.xMax);
    table.writeShort(this.yMax);
    table.writeShort(this.macStyle);
    table.writeShort(this.lowestRecPPEM);
    table.writeShort(this.fontDirectionHint);
    table.writeShort(indexToLocFormat);
    table.writeShort(this.glyphDataFormat);
    return table.data;
  };
  return HeadTable;
})(Table);

/************************************************************************************/
/* function : CmapEntry                                                             */
/* comment : Cmap Initializes and encodes object information (required by pdf spec).*/
/************************************************************************************/
var CmapEntry = (function() {
  function CmapEntry(data, offset) {
    var code,
      count,
      endCode,
      glyphId,
      glyphIds,
      i,
      idDelta,
      idRangeOffset,
      index,
      saveOffset,
      segCount,
      segCountX2,
      start,
      startCode,
      tail,
      _j,
      _k,
      _len;
    this.platformID = data.readUInt16();
    this.encodingID = data.readShort();
    this.offset = offset + data.readInt();
    saveOffset = data.pos;
    data.pos = this.offset;
    this.format = data.readUInt16();
    this.length = data.readUInt16();
    this.language = data.readUInt16();
    this.isUnicode =
      (this.platformID === 3 && this.encodingID === 1 && this.format === 4) ||
      (this.platformID === 0 && this.format === 4) ||
      (this.platformID === 1 && this.encodingID === 0 &&  this.format === 0);
    this.codeMap = {};
    switch (this.format) {
      case 0:
        for (i = 0; i < 256; ++i) {
          this.codeMap[i] = data.readByte();
        }
        break;
      case 4:
        segCountX2 = data.readUInt16();
        segCount = segCountX2 / 2;
        data.pos += 6;
        endCode = (function() {
          var _j, _results;
          _results = [];
          for (
            i = _j = 0;
            0 <= segCount ? _j < segCount : _j > segCount;
            i = 0 <= segCount ? ++_j : --_j
          ) {
            _results.push(data.readUInt16());
          }
          return _results;
        })();
        data.pos += 2;
        startCode = (function() {
          var _j, _results;
          _results = [];
          for (
            i = _j = 0;
            0 <= segCount ? _j < segCount : _j > segCount;
            i = 0 <= segCount ? ++_j : --_j
          ) {
            _results.push(data.readUInt16());
          }
          return _results;
        })();
        idDelta = (function() {
          var _j, _results;
          _results = [];
          for (
            i = _j = 0;
            0 <= segCount ? _j < segCount : _j > segCount;
            i = 0 <= segCount ? ++_j : --_j
          ) {
            _results.push(data.readUInt16());
          }
          return _results;
        })();
        idRangeOffset = (function() {
          var _j, _results;
          _results = [];
          for (
            i = _j = 0;
            0 <= segCount ? _j < segCount : _j > segCount;
            i = 0 <= segCount ? ++_j : --_j
          ) {
            _results.push(data.readUInt16());
          }
          return _results;
        })();
        count = (this.length - data.pos + this.offset) / 2;
        glyphIds = (function() {
          var _j, _results;
          _results = [];
          for (
            i = _j = 0;
            0 <= count ? _j < count : _j > count;
            i = 0 <= count ? ++_j : --_j
          ) {
            _results.push(data.readUInt16());
          }
          return _results;
        })();
        for (i = _j = 0, _len = endCode.length; _j < _len; i = ++_j) {
          tail = endCode[i];
          start = startCode[i];
          for (
            code = _k = start;
            start <= tail ? _k <= tail : _k >= tail;
            code = start <= tail ? ++_k : --_k
          ) {
            if (idRangeOffset[i] === 0) {
              glyphId = code + idDelta[i];
            } else {
              index = idRangeOffset[i] / 2 + (code - start) - (segCount - i);
              glyphId = glyphIds[index] || 0;
              if (glyphId !== 0) {
                glyphId += idDelta[i];
              }
            }
            this.codeMap[code] = glyphId & 0xffff;
          }
        }
    }
    data.pos = saveOffset;
  }
  CmapEntry.encode = function(charmap, encoding) {
    var charMap,
      code,
      codeMap,
      codes,
      delta,
      deltas,
      diff,
      endCode,
      endCodes,
      entrySelector,
      glyphIDs,
      i,
      id,
      indexes,
      last,
      map,
      nextID,
      offset,
      old,
      rangeOffsets,
      rangeShift,
      searchRange,
      segCount,
      segCountX2,
      startCode,
      startCodes,
      startGlyph,
      subtable,
      _i,
      _j,
      _k,
      _l,
      _len,
      _len1,
      _len2,
      _len3,
      _len4,
      _len5,
      _len6,
      _len7,
      _m,
      _n,
      _name,
      _o,
      _p,
      _q;
    subtable = new Data();
    codes = Object.keys(charmap).sort(function(a, b) {
      return a - b;
    });
    switch (encoding) {
      case "macroman":
        id = 0;
        indexes = (function() {
          var _results = [];
          for (i = 0; i < 256; ++i) {
            _results.push(0);
          }
          return _results;
        })();
        map = {
          0: 0
        };
        codeMap = {};
        for (_i = 0, _len = codes.length; _i < _len; _i++) {
          code = codes[_i];
          if (map[(_name = charmap[code])] == null) {
            map[_name] = ++id;
          }
          codeMap[code] = {
            old: charmap[code],
            new: map[charmap[code]]
          };
          indexes[code] = map[charmap[code]];
        }
        subtable.writeUInt16(1);
        subtable.writeUInt16(0);
        subtable.writeUInt32(12);
        subtable.writeUInt16(0);
        subtable.writeUInt16(262);
        subtable.writeUInt16(0);
        subtable.write(indexes);
        return {
          charMap: codeMap,
          subtable: subtable.data,
          maxGlyphID: id + 1
        };
      case "unicode":
        startCodes = [];
        endCodes = [];
        nextID = 0;
        map = {};
        charMap = {};
        last = diff = null;
        for (_j = 0, _len1 = codes.length; _j < _len1; _j++) {
          code = codes[_j];
          old = charmap[code];
          if (map[old] == null) {
            map[old] = ++nextID;
          }
          charMap[code] = {
            old: old,
            new: map[old]
          };
          delta = map[old] - code;
          if (last == null || delta !== diff) {
            if (last) {
              endCodes.push(last);
            }
            startCodes.push(code);
            diff = delta;
          }
          last = code;
        }
        if (last) {
          endCodes.push(last);
        }
        endCodes.push(0xffff);
        startCodes.push(0xffff);
        segCount = startCodes.length;
        segCountX2 = segCount * 2;
        searchRange = 2 * Math.pow(Math.log(segCount) / Math.LN2, 2);
        entrySelector = Math.log(searchRange / 2) / Math.LN2;
        rangeShift = 2 * segCount - searchRange;
        deltas = [];
        rangeOffsets = [];
        glyphIDs = [];
        for (i = _k = 0, _len2 = startCodes.length; _k < _len2; i = ++_k) {
          startCode = startCodes[i];
          endCode = endCodes[i];
          if (startCode === 0xffff) {
            deltas.push(0);
            rangeOffsets.push(0);
            break;
          }
          startGlyph = charMap[startCode]["new"];
          if (startCode - startGlyph >= 0x8000) {
            deltas.push(0);
            rangeOffsets.push(2 * (glyphIDs.length + segCount - i));
            for (
              code = _l = startCode;
              startCode <= endCode ? _l <= endCode : _l >= endCode;
              code = startCode <= endCode ? ++_l : --_l
            ) {
              glyphIDs.push(charMap[code]["new"]);
            }
          } else {
            deltas.push(startGlyph - startCode);
            rangeOffsets.push(0);
          }
        }
        subtable.writeUInt16(3);
        subtable.writeUInt16(1);
        subtable.writeUInt32(12);
        subtable.writeUInt16(4);
        subtable.writeUInt16(16 + segCount * 8 + glyphIDs.length * 2);
        subtable.writeUInt16(0);
        subtable.writeUInt16(segCountX2);
        subtable.writeUInt16(searchRange);
        subtable.writeUInt16(entrySelector);
        subtable.writeUInt16(rangeShift);
        for (_m = 0, _len3 = endCodes.length; _m < _len3; _m++) {
          code = endCodes[_m];
          subtable.writeUInt16(code);
        }
        subtable.writeUInt16(0);
        for (_n = 0, _len4 = startCodes.length; _n < _len4; _n++) {
          code = startCodes[_n];
          subtable.writeUInt16(code);
        }
        for (_o = 0, _len5 = deltas.length; _o < _len5; _o++) {
          delta = deltas[_o];
          subtable.writeUInt16(delta);
        }
        for (_p = 0, _len6 = rangeOffsets.length; _p < _len6; _p++) {
          offset = rangeOffsets[_p];
          subtable.writeUInt16(offset);
        }
        for (_q = 0, _len7 = glyphIDs.length; _q < _len7; _q++) {
          id = glyphIDs[_q];
          subtable.writeUInt16(id);
        }
        return {
          charMap: charMap,
          subtable: subtable.data,
          maxGlyphID: nextID + 1
        };
    }
  };
  return CmapEntry;
})();

var CmapTable = (function(_super) {
  __extends$1(CmapTable, _super);

  function CmapTable() {
    return CmapTable.__super__.constructor.apply(this, arguments);
  }
  CmapTable.prototype.tag = "cmap";
  CmapTable.prototype.parse = function(data) {
    var entry, i, tableCount;
    data.pos = this.offset;
    this.version = data.readUInt16();
    tableCount = data.readUInt16();
    this.tables = [];
    this.unicode = null;
    for (
      i = 0;
      0 <= tableCount ? i < tableCount : i > tableCount;
      i = 0 <= tableCount ? ++i : --i
    ) {
      entry = new CmapEntry(data, this.offset);
      this.tables.push(entry);
      if (entry.isUnicode) {
        if (this.unicode == null) {
          this.unicode = entry;
        }
      }
    }
    return true;
  };
  /*************************************************************************/
  /* function : encode                                                     */
  /* comment : Encode the cmap table corresponding to the input character. */
  /*************************************************************************/
  CmapTable.encode = function(charmap, encoding) {
    var result, table;
    if (encoding == null) {
      encoding = "macroman";
    }
    result = CmapEntry.encode(charmap, encoding);
    table = new Data();
    table.writeUInt16(0);
    table.writeUInt16(1);
    result.table = table.data.concat(result.subtable);
    return result;
  };
  return CmapTable;
})(Table);

var HheaTable = (function(_super) {
  __extends$1(HheaTable, _super);

  function HheaTable() {
    return HheaTable.__super__.constructor.apply(this, arguments);
  }
  HheaTable.prototype.tag = "hhea";
  HheaTable.prototype.parse = function(data) {
    data.pos = this.offset;
    this.version = data.readInt();
    this.ascender = data.readShort();
    this.decender = data.readShort();
    this.lineGap = data.readShort();
    this.advanceWidthMax = data.readShort();
    this.minLeftSideBearing = data.readShort();
    this.minRightSideBearing = data.readShort();
    this.xMaxExtent = data.readShort();
    this.caretSlopeRise = data.readShort();
    this.caretSlopeRun = data.readShort();
    this.caretOffset = data.readShort();
    data.pos += 4 * 2;
    this.metricDataFormat = data.readShort();
    return (this.numberOfMetrics = data.readUInt16());
  };
  /*HheaTable.prototype.encode = function (ids) {
            var i, table, _i, _ref;
            table = new Data;
            table.writeInt(this.version);
            table.writeShort(this.ascender);
            table.writeShort(this.decender);
            table.writeShort(this.lineGap);
            table.writeShort(this.advanceWidthMax);
            table.writeShort(this.minLeftSideBearing);
            table.writeShort(this.minRightSideBearing);
            table.writeShort(this.xMaxExtent);
            table.writeShort(this.caretSlopeRise);
            table.writeShort(this.caretSlopeRun);
            table.writeShort(this.caretOffset);
            for (i = _i = 0, _ref = 4 * 2; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
                table.writeByte(0);
            }
            table.writeShort(this.metricDataFormat);
            table.writeUInt16(ids.length);
            return table.data;
        };*/
  return HheaTable;
})(Table);

var OS2Table = (function(_super) {
  __extends$1(OS2Table, _super);

  function OS2Table() {
    return OS2Table.__super__.constructor.apply(this, arguments);
  }
  OS2Table.prototype.tag = "OS/2";
  OS2Table.prototype.parse = function(data) {
    data.pos = this.offset;
    this.version = data.readUInt16();
    this.averageCharWidth = data.readShort();
    this.weightClass = data.readUInt16();
    this.widthClass = data.readUInt16();
    this.type = data.readShort();
    this.ySubscriptXSize = data.readShort();
    this.ySubscriptYSize = data.readShort();
    this.ySubscriptXOffset = data.readShort();
    this.ySubscriptYOffset = data.readShort();
    this.ySuperscriptXSize = data.readShort();
    this.ySuperscriptYSize = data.readShort();
    this.ySuperscriptXOffset = data.readShort();
    this.ySuperscriptYOffset = data.readShort();
    this.yStrikeoutSize = data.readShort();
    this.yStrikeoutPosition = data.readShort();
    this.familyClass = data.readShort();
    this.panose = (function() {
      var i, _results;
      _results = [];
      for (i = 0; i < 10; ++i) {
        _results.push(data.readByte());
      }
      return _results;
    })();
    this.charRange = (function() {
      var i, _results;
      _results = [];
      for (i = 0; i < 4; ++i) {
        _results.push(data.readInt());
      }
      return _results;
    })();
    this.vendorID = data.readString(4);
    this.selection = data.readShort();
    this.firstCharIndex = data.readShort();
    this.lastCharIndex = data.readShort();
    if (this.version > 0) {
      this.ascent = data.readShort();
      this.descent = data.readShort();
      this.lineGap = data.readShort();
      this.winAscent = data.readShort();
      this.winDescent = data.readShort();
      this.codePageRange = (function() {
        var i, _results;
        _results = [];
        for (i = 0; i < 2; i = ++i) {
          _results.push(data.readInt());
        }
        return _results;
      })();
      if (this.version > 1) {
        this.xHeight = data.readShort();
        this.capHeight = data.readShort();
        this.defaultChar = data.readShort();
        this.breakChar = data.readShort();
        return (this.maxContext = data.readShort());
      }
    }
  };
  /*OS2Table.prototype.encode = function () {
            return this.raw();
        };*/
  return OS2Table;
})(Table);

var PostTable = (function(_super) {
  __extends$1(PostTable, _super);

  function PostTable() {
    return PostTable.__super__.constructor.apply(this, arguments);
  }
  PostTable.prototype.tag = "post";
  PostTable.prototype.parse = function(data) {
    var length, numberOfGlyphs, _results;
    data.pos = this.offset;
    this.format = data.readInt();
    this.italicAngle = data.readInt();
    this.underlinePosition = data.readShort();
    this.underlineThickness = data.readShort();
    this.isFixedPitch = data.readInt();
    this.minMemType42 = data.readInt();
    this.maxMemType42 = data.readInt();
    this.minMemType1 = data.readInt();
    this.maxMemType1 = data.readInt();
    switch (this.format) {
      case 0x00010000:
        break;
      case 0x00020000:
        numberOfGlyphs = data.readUInt16();
        this.glyphNameIndex = [];
        var i;
        for (
          i = 0;
          0 <= numberOfGlyphs ? i < numberOfGlyphs : i > numberOfGlyphs;
          i = 0 <= numberOfGlyphs ? ++i : --i
        ) {
          this.glyphNameIndex.push(data.readUInt16());
        }
        this.names = [];
        _results = [];
        while (data.pos < this.offset + this.length) {
          length = data.readByte();
          _results.push(this.names.push(data.readString(length)));
        }
        return _results;
      case 0x00025000:
        numberOfGlyphs = data.readUInt16();
        return (this.offsets = data.read(numberOfGlyphs));
      case 0x00030000:
        break;
      case 0x00040000:
        return (this.map = function() {
          var _j, _ref, _results1;
          _results1 = [];
          for (
            i = _j = 0, _ref = this.file.maxp.numGlyphs;
            0 <= _ref ? _j < _ref : _j > _ref;
            i = 0 <= _ref ? ++_j : --_j
          ) {
            _results1.push(data.readUInt32());
          }
          return _results1;
        }.call(this));
    }
  };
  return PostTable;
})(Table);

/*********************************************************************************************************/
/* function : NameEntry                                                                                  */
/* comment : Store copyright information, platformID, encodingID, and languageID in the NameEntry object.*/
/*********************************************************************************************************/
var NameEntry = (function() {
  function NameEntry(raw, entry) {
    this.raw = raw;
    this.length = raw.length;
    this.platformID = entry.platformID;
    this.encodingID = entry.encodingID;
    this.languageID = entry.languageID;
  }
  return NameEntry;
})();

var NameTable = (function(_super) {
  __extends$1(NameTable, _super);

  function NameTable() {
    return NameTable.__super__.constructor.apply(this, arguments);
  }
  NameTable.prototype.tag = "name";
  NameTable.prototype.parse = function(data) {
    var count,
      entries,
      entry,
      i,
      name,
      stringOffset,
      strings,
      text,
      _j,
      _len,
      _name;
    data.pos = this.offset;
    data.readShort(); //format
    count = data.readShort();
    stringOffset = data.readShort();
    entries = [];
    for (
      i = 0;
      0 <= count ? i < count : i > count;
      i = 0 <= count ? ++i : --i
    ) {
      entries.push({
        platformID: data.readShort(),
        encodingID: data.readShort(),
        languageID: data.readShort(),
        nameID: data.readShort(),
        length: data.readShort(),
        offset: this.offset + stringOffset + data.readShort()
      });
    }
    strings = {};
    for (i = _j = 0, _len = entries.length; _j < _len; i = ++_j) {
      entry = entries[i];
      data.pos = entry.offset;
      text = data.readString(entry.length);
      name = new NameEntry(text, entry);
      if (strings[(_name = entry.nameID)] == null) {
        strings[_name] = [];
      }
      strings[entry.nameID].push(name);
    }
    this.strings = strings;
    this.copyright = strings[0];
    this.fontFamily = strings[1];
    this.fontSubfamily = strings[2];
    this.uniqueSubfamily = strings[3];
    this.fontName = strings[4];
    this.version = strings[5];
    try {
      this.postscriptName = strings[6][0].raw.replace(
        /[\x00-\x19\x80-\xff]/g,
        ""
      );
    } catch (e) {
      this.postscriptName = strings[4][0].raw.replace(
        /[\x00-\x19\x80-\xff]/g,
        ""
      );
    }
    this.trademark = strings[7];
    this.manufacturer = strings[8];
    this.designer = strings[9];
    this.description = strings[10];
    this.vendorUrl = strings[11];
    this.designerUrl = strings[12];
    this.license = strings[13];
    this.licenseUrl = strings[14];
    this.preferredFamily = strings[15];
    this.preferredSubfamily = strings[17];
    this.compatibleFull = strings[18];
    return (this.sampleText = strings[19]);
  };
  /*NameTable.prototype.encode = function () {
            var id, list, nameID, nameTable, postscriptName, strCount, strTable, string, strings, table, val, _i, _len, _ref;
            strings = {};
            _ref = this.strings;
            for (id in _ref) {
                val = _ref[id];
                strings[id] = val;
            }
            postscriptName = new NameEntry("" + subsetTag + "+" + this.postscriptName, {
                platformID: 1
                , encodingID: 0
                , languageID: 0
            });
            strings[6] = [postscriptName];
            subsetTag = successorOf(subsetTag);
            strCount = 0;
            for (id in strings) {
                list = strings[id];
                if (list != null) {
                    strCount += list.length;
                }
            }
            table = new Data;
            strTable = new Data;
            table.writeShort(0);
            table.writeShort(strCount);
            table.writeShort(6 + 12 * strCount);
            for (nameID in strings) {
                list = strings[nameID];
                if (list != null) {
                    for (_i = 0, _len = list.length; _i < _len; _i++) {
                        string = list[_i];
                        table.writeShort(string.platformID);
                        table.writeShort(string.encodingID);
                        table.writeShort(string.languageID);
                        table.writeShort(nameID);
                        table.writeShort(string.length);
                        table.writeShort(strTable.pos);
                        strTable.writeString(string.raw);
                    }
                }
            }
            return nameTable = {
                postscriptName: postscriptName.raw
                , table: table.data.concat(strTable.data)
            };
        };*/
  return NameTable;
})(Table);

var MaxpTable = (function(_super) {
  __extends$1(MaxpTable, _super);

  function MaxpTable() {
    return MaxpTable.__super__.constructor.apply(this, arguments);
  }
  MaxpTable.prototype.tag = "maxp";
  MaxpTable.prototype.parse = function(data) {
    data.pos = this.offset;
    this.version = data.readInt();
    this.numGlyphs = data.readUInt16();
    this.maxPoints = data.readUInt16();
    this.maxContours = data.readUInt16();
    this.maxCompositePoints = data.readUInt16();
    this.maxComponentContours = data.readUInt16();
    this.maxZones = data.readUInt16();
    this.maxTwilightPoints = data.readUInt16();
    this.maxStorage = data.readUInt16();
    this.maxFunctionDefs = data.readUInt16();
    this.maxInstructionDefs = data.readUInt16();
    this.maxStackElements = data.readUInt16();
    this.maxSizeOfInstructions = data.readUInt16();
    this.maxComponentElements = data.readUInt16();
    return (this.maxComponentDepth = data.readUInt16());
  };
  /*MaxpTable.prototype.encode = function (ids) {
            var table;
            table = new Data;
            table.writeInt(this.version);
            table.writeUInt16(ids.length);
            table.writeUInt16(this.maxPoints);
            table.writeUInt16(this.maxContours);
            table.writeUInt16(this.maxCompositePoints);
            table.writeUInt16(this.maxComponentContours);
            table.writeUInt16(this.maxZones);
            table.writeUInt16(this.maxTwilightPoints);
            table.writeUInt16(this.maxStorage);
            table.writeUInt16(this.maxFunctionDefs);
            table.writeUInt16(this.maxInstructionDefs);
            table.writeUInt16(this.maxStackElements);
            table.writeUInt16(this.maxSizeOfInstructions);
            table.writeUInt16(this.maxComponentElements);
            table.writeUInt16(this.maxComponentDepth);
            return table.data;
        };*/
  return MaxpTable;
})(Table);

var HmtxTable = (function(_super) {
  __extends$1(HmtxTable, _super);

  function HmtxTable() {
    return HmtxTable.__super__.constructor.apply(this, arguments);
  }
  HmtxTable.prototype.tag = "hmtx";
  HmtxTable.prototype.parse = function(data) {
    var i, last, lsbCount, m, _j, _ref, _results;
    data.pos = this.offset;
    this.metrics = [];
    for (
      i = 0, _ref = this.file.hhea.numberOfMetrics;
      0 <= _ref ? i < _ref : i > _ref;
      i = 0 <= _ref ? ++i : --i
    ) {
      this.metrics.push({
        advance: data.readUInt16(),
        lsb: data.readInt16()
      });
    }
    lsbCount = this.file.maxp.numGlyphs - this.file.hhea.numberOfMetrics;
    this.leftSideBearings = (function() {
      var _j, _results;
      _results = [];
      for (
        i = _j = 0;
        0 <= lsbCount ? _j < lsbCount : _j > lsbCount;
        i = 0 <= lsbCount ? ++_j : --_j
      ) {
        _results.push(data.readInt16());
      }
      return _results;
    })();
    this.widths = function() {
      var _j, _len, _ref1, _results;
      _ref1 = this.metrics;
      _results = [];
      for (_j = 0, _len = _ref1.length; _j < _len; _j++) {
        m = _ref1[_j];
        _results.push(m.advance);
      }
      return _results;
    }.call(this);
    last = this.widths[this.widths.length - 1];
    _results = [];
    for (
      i = _j = 0;
      0 <= lsbCount ? _j < lsbCount : _j > lsbCount;
      i = 0 <= lsbCount ? ++_j : --_j
    ) {
      _results.push(this.widths.push(last));
    }
    return _results;
  };
  /***************************************************************/
  /* function : forGlyph                                         */
  /* comment : Returns the advance width and lsb for this glyph. */
  /***************************************************************/
  HmtxTable.prototype.forGlyph = function(id) {
    if (id in this.metrics) {
      return this.metrics[id];
    }
    return {
      advance: this.metrics[this.metrics.length - 1].advance,
      lsb: this.leftSideBearings[id - this.metrics.length]
    };
  };
  /*HmtxTable.prototype.encode = function (mapping) {
            var id, metric, table, _i, _len;
            table = new Data;
            for (_i = 0, _len = mapping.length; _i < _len; _i++) {
                id = mapping[_i];
                metric = this.forGlyph(id);
                table.writeUInt16(metric.advance);
                table.writeUInt16(metric.lsb);
            }
            return table.data;
        };*/
  return HmtxTable;
})(Table);

var __slice = [].slice;

var GlyfTable = (function(_super) {
  __extends$1(GlyfTable, _super);

  function GlyfTable() {
    return GlyfTable.__super__.constructor.apply(this, arguments);
  }
  GlyfTable.prototype.tag = "glyf";
  GlyfTable.prototype.parse = function() {
    return (this.cache = {});
  };
  GlyfTable.prototype.glyphFor = function(id) {
    var data,
      index,
      length,
      loca,
      numberOfContours,
      raw,
      xMax,
      xMin,
      yMax,
      yMin;
    if (id in this.cache) {
      return this.cache[id];
    }
    loca = this.file.loca;
    data = this.file.contents;
    index = loca.indexOf(id);
    length = loca.lengthOf(id);
    if (length === 0) {
      return (this.cache[id] = null);
    }
    data.pos = this.offset + index;
    raw = new Data(data.read(length));
    numberOfContours = raw.readShort();
    xMin = raw.readShort();
    yMin = raw.readShort();
    xMax = raw.readShort();
    yMax = raw.readShort();
    if (numberOfContours === -1) {
      this.cache[id] = new CompoundGlyph(raw, xMin, yMin, xMax, yMax);
    } else {
      this.cache[id] = new SimpleGlyph(
        raw,
        numberOfContours,
        xMin,
        yMin,
        xMax,
        yMax
      );
    }
    return this.cache[id];
  };
  GlyfTable.prototype.encode = function(glyphs, mapping, old2new) {
    var glyph, id, offsets, table, _i, _len;
    table = [];
    offsets = [];
    for (_i = 0, _len = mapping.length; _i < _len; _i++) {
      id = mapping[_i];
      glyph = glyphs[id];
      offsets.push(table.length);
      if (glyph) {
        table = table.concat(glyph.encode(old2new));
      }
    }
    offsets.push(table.length);
    return {
      table: table,
      offsets: offsets
    };
  };
  return GlyfTable;
})(Table);

var SimpleGlyph = (function() {
  /**************************************************************************/
  /* function : SimpleGlyph                                                 */
  /* comment : Stores raw, xMin, yMin, xMax, and yMax values for this glyph.*/
  /**************************************************************************/
  function SimpleGlyph(raw, numberOfContours, xMin, yMin, xMax, yMax) {
    this.raw = raw;
    this.numberOfContours = numberOfContours;
    this.xMin = xMin;
    this.yMin = yMin;
    this.xMax = xMax;
    this.yMax = yMax;
    this.compound = false;
  }
  SimpleGlyph.prototype.encode = function() {
    return this.raw.data;
  };
  return SimpleGlyph;
})();

var CompoundGlyph = (function() {
  var ARG_1_AND_2_ARE_WORDS,
    MORE_COMPONENTS,
    WE_HAVE_AN_X_AND_Y_SCALE,
    WE_HAVE_A_SCALE,
    WE_HAVE_A_TWO_BY_TWO;
  ARG_1_AND_2_ARE_WORDS = 0x0001;
  WE_HAVE_A_SCALE = 0x0008;
  MORE_COMPONENTS = 0x0020;
  WE_HAVE_AN_X_AND_Y_SCALE = 0x0040;
  WE_HAVE_A_TWO_BY_TWO = 0x0080;

  /********************************************************************************************************************/
  /* function : CompoundGlypg generator                                                                               */
  /* comment : It stores raw, xMin, yMin, xMax, yMax, glyph id, and glyph offset for the corresponding compound glyph.*/
  /********************************************************************************************************************/
  function CompoundGlyph(raw, xMin, yMin, xMax, yMax) {
    var data, flags;
    this.raw = raw;
    this.xMin = xMin;
    this.yMin = yMin;
    this.xMax = xMax;
    this.yMax = yMax;
    this.compound = true;
    this.glyphIDs = [];
    this.glyphOffsets = [];
    data = this.raw;
    while (true) {
      flags = data.readShort();
      this.glyphOffsets.push(data.pos);
      this.glyphIDs.push(data.readUInt16());
      if (!(flags & MORE_COMPONENTS)) {
        break;
      }
      if (flags & ARG_1_AND_2_ARE_WORDS) {
        data.pos += 4;
      } else {
        data.pos += 2;
      }
      if (flags & WE_HAVE_A_TWO_BY_TWO) {
        data.pos += 8;
      } else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) {
        data.pos += 4;
      } else if (flags & WE_HAVE_A_SCALE) {
        data.pos += 2;
      }
    }
  }
  /****************************************************************************************************************/
  /* function : CompoundGlypg encode                                                                              */
  /* comment : After creating a table for the characters you typed, you call directory.encode to encode the table.*/
  /****************************************************************************************************************/
  CompoundGlyph.prototype.encode = function() {
    var i, result, _len, _ref;
    result = new Data(__slice.call(this.raw.data));
    _ref = this.glyphIDs;
    for (i = 0, _len = _ref.length; i < _len; ++i) {
      result.pos = this.glyphOffsets[i];
    }
    return result.data;
  };
  return CompoundGlyph;
})();

var LocaTable = (function(_super) {
  __extends$1(LocaTable, _super);

  function LocaTable() {
    return LocaTable.__super__.constructor.apply(this, arguments);
  }
  LocaTable.prototype.tag = "loca";
  LocaTable.prototype.parse = function(data) {
    var format, i;
    data.pos = this.offset;
    format = this.file.head.indexToLocFormat;
    if (format === 0) {
      return (this.offsets = function() {
        var _ref, _results;
        _results = [];
        for (i = 0, _ref = this.length; i < _ref; i += 2) {
          _results.push(data.readUInt16() * 2);
        }
        return _results;
      }.call(this));
    } else {
      return (this.offsets = function() {
        var _ref, _results;
        _results = [];
        for (i = 0, _ref = this.length; i < _ref; i += 4) {
          _results.push(data.readUInt32());
        }
        return _results;
      }.call(this));
    }
  };
  LocaTable.prototype.indexOf = function(id) {
    return this.offsets[id];
  };
  LocaTable.prototype.lengthOf = function(id) {
    return this.offsets[id + 1] - this.offsets[id];
  };
  LocaTable.prototype.encode = function(offsets, activeGlyphs) {
    var LocaTable = new Uint32Array(this.offsets.length);
    var glyfPtr = 0;
    var listGlyf = 0;
    for (var k = 0; k < LocaTable.length; ++k) {
      LocaTable[k] = glyfPtr;
      if (listGlyf < activeGlyphs.length && activeGlyphs[listGlyf] == k) {
        ++listGlyf;
        LocaTable[k] = glyfPtr;
        var start = this.offsets[k];
        var len = this.offsets[k + 1] - start;
        if (len > 0) {
          glyfPtr += len;
        }
      }
    }
    var newLocaTable = new Array(LocaTable.length * 4);
    for (var j = 0; j < LocaTable.length; ++j) {
      newLocaTable[4 * j + 3] = LocaTable[j] & 0x000000ff;
      newLocaTable[4 * j + 2] = (LocaTable[j] & 0x0000ff00) >> 8;
      newLocaTable[4 * j + 1] = (LocaTable[j] & 0x00ff0000) >> 16;
      newLocaTable[4 * j] = (LocaTable[j] & 0xff000000) >> 24;
    }
    return newLocaTable;
  };
  return LocaTable;
})(Table);

/************************************************************************************/
/* function : invert                                                                */
/* comment : Change the object's (key: value) to create an object with (value: key).*/
/************************************************************************************/
var invert = function(object) {
  var key, ret, val;
  ret = {};
  for (key in object) {
    val = object[key];
    ret[val] = key;
  }
  return ret;
};

/*var successorOf = function (input) {
        var added, alphabet, carry, i, index, isUpperCase, last, length, next, result;
        alphabet = 'abcdefghijklmnopqrstuvwxyz';
        length = alphabet.length;
        result = input;
        i = input.length;
        while (i >= 0) {
            last = input.charAt(--i);
            if (isNaN(last)) {
                index = alphabet.indexOf(last.toLowerCase());
                if (index === -1) {
                    next = last;
                    carry = true;
                }
                else {
                    next = alphabet.charAt((index + 1) % length);
                    isUpperCase = last === last.toUpperCase();
                    if (isUpperCase) {
                        next = next.toUpperCase();
                    }
                    carry = index + 1 >= length;
                    if (carry && i === 0) {
                        added = isUpperCase ? 'A' : 'a';
                        result = added + next + result.slice(1);
                        break;
                    }
                }
            }
            else {
                next = +last + 1;
                carry = next > 9;
                if (carry) {
                    next = 0;
                }
                if (carry && i === 0) {
                    result = '1' + next + result.slice(1);
                    break;
                }
            }
            result = result.slice(0, i) + next + result.slice(i + 1);
            if (!carry) {
                break;
            }
        }
        return result;
    };*/

var Subset = (function() {
  function Subset(font) {
    this.font = font;
    this.subset = {};
    this.unicodes = {};
    this.next = 33;
  }
  /*Subset.prototype.use = function (character) {
            var i, _i, _ref;
            if (typeof character === 'string') {
                for (i = _i = 0, _ref = character.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
                    this.use(character.charCodeAt(i));
                }
                return;
            }
            if (!this.unicodes[character]) {
                this.subset[this.next] = character;
                return this.unicodes[character] = this.next++;
            }
        };*/
  /*Subset.prototype.encodeText = function (text) {
            var char, i, string, _i, _ref;
            string = '';
            for (i = _i = 0, _ref = text.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
                char = this.unicodes[text.charCodeAt(i)];
                string += String.fromCharCode(char);
            }
            return string;
        };*/
  /***************************************************************/
  /* function : generateCmap                                     */
  /* comment : Returns the unicode cmap for this font.         */
  /***************************************************************/
  Subset.prototype.generateCmap = function() {
    var mapping, roman, unicode, unicodeCmap, _ref;
    unicodeCmap = this.font.cmap.tables[0].codeMap;
    mapping = {};
    _ref = this.subset;
    for (roman in _ref) {
      unicode = _ref[roman];
      mapping[roman] = unicodeCmap[unicode];
    }
    return mapping;
  };
  /*Subset.prototype.glyphIDs = function () {
            var ret, roman, unicode, unicodeCmap, val, _ref;
            unicodeCmap = this.font.cmap.tables[0].codeMap;
            ret = [0];
            _ref = this.subset;
            for (roman in _ref) {
                unicode = _ref[roman];
                val = unicodeCmap[unicode];
                if ((val != null) && __indexOf.call(ret, val) < 0) {
                    ret.push(val);
                }
            }
            return ret.sort();
        };*/
  /******************************************************************/
  /* function : glyphsFor                                           */
  /* comment : Returns simple glyph objects for the input character.*/
  /******************************************************************/
  Subset.prototype.glyphsFor = function(glyphIDs) {
    var additionalIDs, glyph, glyphs, id, _i, _len, _ref;
    glyphs = {};
    for (_i = 0, _len = glyphIDs.length; _i < _len; _i++) {
      id = glyphIDs[_i];
      glyphs[id] = this.font.glyf.glyphFor(id);
    }
    additionalIDs = [];
    for (id in glyphs) {
      glyph = glyphs[id];
      if (glyph != null ? glyph.compound : void 0) {
        additionalIDs.push.apply(additionalIDs, glyph.glyphIDs);
      }
    }
    if (additionalIDs.length > 0) {
      _ref = this.glyphsFor(additionalIDs);
      for (id in _ref) {
        glyph = _ref[id];
        glyphs[id] = glyph;
      }
    }
    return glyphs;
  };
  /***************************************************************/
  /* function : encode                                           */
  /* comment : Encode various tables for the characters you use. */
  /***************************************************************/
  Subset.prototype.encode = function(glyID, indexToLocFormat) {
    var cmap,
      code,
      glyf,
      glyphs,
      id,
      ids,
      loca,
      new2old,
      newIDs,
      nextGlyphID,
      old2new,
      oldID,
      oldIDs,
      tables,
      _ref;
    cmap = CmapTable.encode(this.generateCmap(), "unicode");
    glyphs = this.glyphsFor(glyID);
    old2new = {
      0: 0
    };
    _ref = cmap.charMap;
    for (code in _ref) {
      ids = _ref[code];
      old2new[ids.old] = ids["new"];
    }
    nextGlyphID = cmap.maxGlyphID;
    for (oldID in glyphs) {
      if (!(oldID in old2new)) {
        old2new[oldID] = nextGlyphID++;
      }
    }
    new2old = invert(old2new);
    newIDs = Object.keys(new2old).sort(function(a, b) {
      return a - b;
    });
    oldIDs = (function() {
      var _i, _len, _results;
      _results = [];
      for (_i = 0, _len = newIDs.length; _i < _len; _i++) {
        id = newIDs[_i];
        _results.push(new2old[id]);
      }
      return _results;
    })();
    glyf = this.font.glyf.encode(glyphs, oldIDs, old2new);
    loca = this.font.loca.encode(glyf.offsets, oldIDs);
    tables = {
      cmap: this.font.cmap.raw(),
      glyf: glyf.table,
      loca: loca,
      hmtx: this.font.hmtx.raw(),
      hhea: this.font.hhea.raw(),
      maxp: this.font.maxp.raw(),
      post: this.font.post.raw(),
      name: this.font.name.raw(),
      head: this.font.head.encode(indexToLocFormat)
    };
    if (this.font.os2.exists) {
      tables["OS/2"] = this.font.os2.raw();
    }
    return this.font.directory.encode(tables);
  };
  return Subset;
})();

jsPDF.API.PDFObject = (function() {
  var pad;

  function PDFObject() {}
  pad = function(str, length) {
    return (Array(length + 1).join("0") + str).slice(-length);
  };
  /*****************************************************************************/
  /* function : convert                                                        */
  /* comment :Converts pdf tag's / FontBBox and array values in / W to strings */
  /*****************************************************************************/
  PDFObject.convert = function(object) {
    var e, items, key, out, val;
    if (Array.isArray(object)) {
      items = (function() {
        var _i, _len, _results;
        _results = [];
        for (_i = 0, _len = object.length; _i < _len; _i++) {
          e = object[_i];
          _results.push(PDFObject.convert(e));
        }
        return _results;
      })().join(" ");
      return "[" + items + "]";
    } else if (typeof object === "string") {
      return "/" + object;
    } else if (object != null ? object.isString : void 0) {
      return "(" + object + ")";
    } else if (object instanceof Date) {
      return (
        "(D:" +
        pad(object.getUTCFullYear(), 4) +
        pad(object.getUTCMonth(), 2) +
        pad(object.getUTCDate(), 2) +
        pad(object.getUTCHours(), 2) +
        pad(object.getUTCMinutes(), 2) +
        pad(object.getUTCSeconds(), 2) +
        "Z)"
      );
    } else if ({}.toString.call(object) === "[object Object]") {
      out = ["<<"];
      for (key in object) {
        val = object[key];
        out.push("/" + key + " " + PDFObject.convert(val));
      }
      out.push(">>");
      return out.join("\n");
    } else {
      return "" + object;
    }
  };
  return PDFObject;
})();

/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2015-2023 yWorks GmbH
 * Copyright (c) 2013-2015 by Vitaly Puzrin
 *
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */


/******************************************************************************
Copyright (c) Microsoft Corporation.

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise */

var extendStatics = function(d, b) {
    extendStatics = Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
        function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
    return extendStatics(d, b);
};

function __extends(d, b) {
    if (typeof b !== "function" && b !== null)
        throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
    extendStatics(d, b);
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}

var __assign = function() {
    __assign = Object.assign || function __assign(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};

function __awaiter(thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
}

function __generator(thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
}

/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types */
var RGBColor = /** @class */ (function () {
    function RGBColor(colorString) {
        this.a = undefined;
        this.r = 0;
        this.g = 0;
        this.b = 0;
        this.simpleColors = {};
        // eslint-disable-next-line @typescript-eslint/ban-types
        this.colorDefs = [];
        this.ok = false;
        if (!colorString) {
            return;
        }
        // strip any leading #
        if (colorString.charAt(0) == '#') {
            // remove # if any
            colorString = colorString.substr(1, 6);
        }
        colorString = colorString.replace(/ /g, '');
        colorString = colorString.toLowerCase();
        // before getting into regexps, try simple matches
        // and overwrite the input
        this.simpleColors = {
            aliceblue: 'f0f8ff',
            antiquewhite: 'faebd7',
            aqua: '00ffff',
            aquamarine: '7fffd4',
            azure: 'f0ffff',
            beige: 'f5f5dc',
            bisque: 'ffe4c4',
            black: '000000',
            blanchedalmond: 'ffebcd',
            blue: '0000ff',
            blueviolet: '8a2be2',
            brown: 'a52a2a',
            burlywood: 'deb887',
            cadetblue: '5f9ea0',
            chartreuse: '7fff00',
            chocolate: 'd2691e',
            coral: 'ff7f50',
            cornflowerblue: '6495ed',
            cornsilk: 'fff8dc',
            crimson: 'dc143c',
            cyan: '00ffff',
            darkblue: '00008b',
            darkcyan: '008b8b',
            darkgoldenrod: 'b8860b',
            darkgray: 'a9a9a9',
            darkgrey: 'a9a9a9',
            darkgreen: '006400',
            darkkhaki: 'bdb76b',
            darkmagenta: '8b008b',
            darkolivegreen: '556b2f',
            darkorange: 'ff8c00',
            darkorchid: '9932cc',
            darkred: '8b0000',
            darksalmon: 'e9967a',
            darkseagreen: '8fbc8f',
            darkslateblue: '483d8b',
            darkslategray: '2f4f4f',
            darkslategrey: '2f4f4f',
            darkturquoise: '00ced1',
            darkviolet: '9400d3',
            deeppink: 'ff1493',
            deepskyblue: '00bfff',
            dimgray: '696969',
            dimgrey: '696969',
            dodgerblue: '1e90ff',
            feldspar: 'd19275',
            firebrick: 'b22222',
            floralwhite: 'fffaf0',
            forestgreen: '228b22',
            fuchsia: 'ff00ff',
            gainsboro: 'dcdcdc',
            ghostwhite: 'f8f8ff',
            gold: 'ffd700',
            goldenrod: 'daa520',
            gray: '808080',
            grey: '808080',
            green: '008000',
            greenyellow: 'adff2f',
            honeydew: 'f0fff0',
            hotpink: 'ff69b4',
            indianred: 'cd5c5c',
            indigo: '4b0082',
            ivory: 'fffff0',
            khaki: 'f0e68c',
            lavender: 'e6e6fa',
            lavenderblush: 'fff0f5',
            lawngreen: '7cfc00',
            lemonchiffon: 'fffacd',
            lightblue: 'add8e6',
            lightcoral: 'f08080',
            lightcyan: 'e0ffff',
            lightgoldenrodyellow: 'fafad2',
            lightgray: 'd3d3d3',
            lightgrey: 'd3d3d3',
            lightgreen: '90ee90',
            lightpink: 'ffb6c1',
            lightsalmon: 'ffa07a',
            lightseagreen: '20b2aa',
            lightskyblue: '87cefa',
            lightslateblue: '8470ff',
            lightslategray: '778899',
            lightslategrey: '778899',
            lightsteelblue: 'b0c4de',
            lightyellow: 'ffffe0',
            lime: '00ff00',
            limegreen: '32cd32',
            linen: 'faf0e6',
            magenta: 'ff00ff',
            maroon: '800000',
            mediumaquamarine: '66cdaa',
            mediumblue: '0000cd',
            mediumorchid: 'ba55d3',
            mediumpurple: '9370d8',
            mediumseagreen: '3cb371',
            mediumslateblue: '7b68ee',
            mediumspringgreen: '00fa9a',
            mediumturquoise: '48d1cc',
            mediumvioletred: 'c71585',
            midnightblue: '191970',
            mintcream: 'f5fffa',
            mistyrose: 'ffe4e1',
            moccasin: 'ffe4b5',
            navajowhite: 'ffdead',
            navy: '000080',
            oldlace: 'fdf5e6',
            olive: '808000',
            olivedrab: '6b8e23',
            orange: 'ffa500',
            orangered: 'ff4500',
            orchid: 'da70d6',
            palegoldenrod: 'eee8aa',
            palegreen: '98fb98',
            paleturquoise: 'afeeee',
            palevioletred: 'd87093',
            papayawhip: 'ffefd5',
            peachpuff: 'ffdab9',
            peru: 'cd853f',
            pink: 'ffc0cb',
            plum: 'dda0dd',
            powderblue: 'b0e0e6',
            purple: '800080',
            red: 'ff0000',
            rosybrown: 'bc8f8f',
            royalblue: '4169e1',
            saddlebrown: '8b4513',
            salmon: 'fa8072',
            sandybrown: 'f4a460',
            seagreen: '2e8b57',
            seashell: 'fff5ee',
            sienna: 'a0522d',
            silver: 'c0c0c0',
            skyblue: '87ceeb',
            slateblue: '6a5acd',
            slategray: '708090',
            slategrey: '708090',
            snow: 'fffafa',
            springgreen: '00ff7f',
            steelblue: '4682b4',
            tan: 'd2b48c',
            teal: '008080',
            thistle: 'd8bfd8',
            tomato: 'ff6347',
            turquoise: '40e0d0',
            violet: 'ee82ee',
            violetred: 'd02090',
            wheat: 'f5deb3',
            white: 'ffffff',
            whitesmoke: 'f5f5f5',
            yellow: 'ffff00',
            yellowgreen: '9acd32'
        };
        for (var key in this.simpleColors) {
            if (colorString == key) {
                colorString = this.simpleColors[key];
            }
        }
        // emd of simple type-in colors
        // array of color definition objects
        this.colorDefs = [
            {
                re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
                example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'],
                process: function (bits) {
                    return [parseInt(bits[1]), parseInt(bits[2]), parseInt(bits[3])];
                }
            },
            {
                re: /^rgb\(([0-9.]+)%,\s*([0-9.]+)%,\s*([0-9.]+)%\)$/,
                example: ['rgb(50.5%, 25.75%, 75.5%)', 'rgb(100%,0%,0%)'],
                process: function (bits) {
                    return [
                        Math.round(parseFloat(bits[1]) * 2.55),
                        Math.round(parseFloat(bits[2]) * 2.55),
                        Math.round(parseFloat(bits[3]) * 2.55)
                    ];
                }
            },
            {
                re: /^(\w{2})(\w{2})(\w{2})$/,
                example: ['#00ff00', '336699'],
                process: function (bits) {
                    return [parseInt(bits[1], 16), parseInt(bits[2], 16), parseInt(bits[3], 16)];
                }
            },
            {
                re: /^(\w{1})(\w{1})(\w{1})$/,
                example: ['#fb0', 'f0f'],
                process: function (bits) {
                    return [
                        parseInt(bits[1] + bits[1], 16),
                        parseInt(bits[2] + bits[2], 16),
                        parseInt(bits[3] + bits[3], 16)
                    ];
                }
            }
        ];
        // search through the definitions to find a match
        for (var i = 0; i < this.colorDefs.length; i++) {
            var re = this.colorDefs[i].re;
            var processor = this.colorDefs[i].process;
            var bits = re.exec(colorString);
            if (bits) {
                var channels = processor(bits);
                this.r = channels[0];
                this.g = channels[1];
                this.b = channels[2];
                this.ok = true;
            }
        }
        // validate/cleanup values
        this.r = this.r < 0 || isNaN(this.r) ? 0 : this.r > 255 ? 255 : this.r;
        this.g = this.g < 0 || isNaN(this.g) ? 0 : this.g > 255 ? 255 : this.g;
        this.b = this.b < 0 || isNaN(this.b) ? 0 : this.b > 255 ? 255 : this.b;
    }
    RGBColor.prototype.toRGB = function () {
        return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
    };
    RGBColor.prototype.toRGBA = function () {
        return 'rgba(' + this.r + ', ' + this.g + ', ' + this.b + ', ' + (this.a || '1') + ')';
    };
    RGBColor.prototype.toHex = function () {
        var r = this.r.toString(16);
        var g = this.g.toString(16);
        var b = this.b.toString(16);
        if (r.length == 1)
            r = '0' + r;
        if (g.length == 1)
            g = '0' + g;
        if (b.length == 1)
            b = '0' + b;
        return '#' + r + g + b;
    };
    // help
    RGBColor.prototype.getHelpXML = function () {
        var examples = [];
        // add regexps
        for (var i = 0; i < this.colorDefs.length; i++) {
            var example = this.colorDefs[i].example;
            for (var j = 0; j < example.length; j++) {
                examples[examples.length] = example[j];
            }
        }
        // add type-in colors
        for (var sc in this.simpleColors) {
            examples[examples.length] = sc;
        }
        var xml = document.createElement('ul');
        xml.setAttribute('id', 'rgbcolor-examples');
        for (var i = 0; i < examples.length; i++) {
            try {
                var listItem = document.createElement('li');
                var listColor = new RGBColor(examples[i]);
                var exampleDiv = document.createElement('div');
                exampleDiv.style.cssText =
                    'margin: 3px; ' +
                        'border: 1px solid black; ' +
                        'background:' +
                        listColor.toHex() +
                        '; ' +
                        'color:' +
                        listColor.toHex();
                exampleDiv.appendChild(document.createTextNode('test'));
                var listItemValue = document.createTextNode(' ' + examples[i] + ' -> ' + listColor.toRGB() + ' -> ' + listColor.toHex());
                listItem.appendChild(exampleDiv);
                listItem.appendChild(listItemValue);
                xml.appendChild(listItem);
            }
            catch (e) { }
        }
        return xml;
    };
    return RGBColor;
}());

var ColorFill = /** @class */ (function () {
    function ColorFill(color) {
        this.color = color;
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    ColorFill.prototype.getFillData = function (forNode, context) {
        return __awaiter(this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                return [2 /*return*/, undefined];
            });
        });
    };
    return ColorFill;
}());

var AttributeState = /** @class */ (function () {
    function AttributeState() {
        this.xmlSpace = '';
        this.whiteSpace = '';
        this.fill = null;
        this.fillOpacity = 1.0;
        // public fillRule: string = null
        this.fontFamily = '';
        this.fontSize = 16;
        this.fontStyle = '';
        // public fontVariant: string
        this.fontWeight = '';
        this.opacity = 1.0;
        this.stroke = null;
        this.strokeDasharray = null;
        this.strokeDashoffset = 0;
        this.strokeLinecap = '';
        this.strokeLinejoin = '';
        this.strokeMiterlimit = 4.0;
        this.strokeOpacity = 1.0;
        this.strokeWidth = 1.0;
        // public textAlign: string
        this.alignmentBaseline = '';
        this.textAnchor = '';
        this.visibility = '';
        this.color = null;
        this.contextFill = null;
        this.contextStroke = null;
        this.fillRule = null;
    }
    AttributeState.prototype.clone = function () {
        var clone = new AttributeState();
        clone.xmlSpace = this.xmlSpace;
        clone.whiteSpace = this.whiteSpace;
        clone.fill = this.fill;
        clone.fillOpacity = this.fillOpacity;
        // clone.fillRule = this.fillRule;
        clone.fontFamily = this.fontFamily;
        clone.fontSize = this.fontSize;
        clone.fontStyle = this.fontStyle;
        // clone.fontVariant = this.fontVariant;
        clone.fontWeight = this.fontWeight;
        clone.opacity = this.opacity;
        clone.stroke = this.stroke;
        clone.strokeDasharray = this.strokeDasharray;
        clone.strokeDashoffset = this.strokeDashoffset;
        clone.strokeLinecap = this.strokeLinecap;
        clone.strokeLinejoin = this.strokeLinejoin;
        clone.strokeMiterlimit = this.strokeMiterlimit;
        clone.strokeOpacity = this.strokeOpacity;
        clone.strokeWidth = this.strokeWidth;
        // clone.textAlign = this.textAlign;
        clone.textAnchor = this.textAnchor;
        clone.alignmentBaseline = this.alignmentBaseline;
        clone.visibility = this.visibility;
        clone.color = this.color;
        clone.fillRule = this.fillRule;
        clone.contextFill = this.contextFill;
        clone.contextStroke = this.contextStroke;
        return clone;
    };
    AttributeState.default = function () {
        var attributeState = new AttributeState();
        attributeState.xmlSpace = 'default';
        attributeState.whiteSpace = 'normal';
        attributeState.fill = new ColorFill(new RGBColor('rgb(0, 0, 0)'));
        attributeState.fillOpacity = 1.0;
        // attributeState.fillRule = "nonzero";
        attributeState.fontFamily = 'times';
        attributeState.fontSize = 16;
        attributeState.fontStyle = 'normal';
        // attributeState.fontVariant = "normal";
        attributeState.fontWeight = 'normal';
        attributeState.opacity = 1.0;
        attributeState.stroke = null;
        attributeState.strokeDasharray = null;
        attributeState.strokeDashoffset = 0;
        attributeState.strokeLinecap = 'butt';
        attributeState.strokeLinejoin = 'miter';
        attributeState.strokeMiterlimit = 4.0;
        attributeState.strokeOpacity = 1.0;
        attributeState.strokeWidth = 1.0;
        // attributeState.textAlign = "start";
        attributeState.alignmentBaseline = 'baseline';
        attributeState.textAnchor = 'start';
        attributeState.visibility = 'visible';
        attributeState.color = new RGBColor('rgb(0, 0, 0)');
        attributeState.fillRule = 'nonzero';
        attributeState.contextFill = null;
        attributeState.contextStroke = null;
        return attributeState;
    };
    AttributeState.getContextColors = function (context, includeCurrentColor) {
        if (includeCurrentColor === void 0) { includeCurrentColor = false; }
        var colors = {};
        if (context.attributeState.contextFill) {
            colors['contextFill'] = context.attributeState.contextFill;
        }
        if (context.attributeState.contextStroke) {
            colors['contextStroke'] = context.attributeState.contextStroke;
        }
        if (includeCurrentColor && context.attributeState.color) {
            colors['color'] = context.attributeState.color;
        }
        return colors;
    };
    return AttributeState;
}());

/**
 *
 * @package
 * @param values
 * @constructor
 * @property pdf
 * @property attributeState  Keeps track of parent attributes that are inherited automatically
 * @property refsHandler  The handler that will render references on demand
 * @property styleSheets
 * @property textMeasure
 * @property transform The current transformation matrix
 * @property withinClipPath
 */
var Context = /** @class */ (function () {
    function Context(pdf, values) {
        var _a, _b, _c;
        this.pdf = pdf;
        this.svg2pdfParameters = values.svg2pdfParameters;
        this.attributeState = values.attributeState
            ? values.attributeState.clone()
            : AttributeState.default();
        this.viewport = values.viewport;
        this.refsHandler = values.refsHandler;
        this.styleSheets = values.styleSheets;
        this.textMeasure = values.textMeasure;
        this.transform = (_a = values.transform) !== null && _a !== void 0 ? _a : this.pdf.unitMatrix;
        this.withinClipPath = (_b = values.withinClipPath) !== null && _b !== void 0 ? _b : false;
        this.withinUse = (_c = values.withinUse) !== null && _c !== void 0 ? _c : false;
    }
    Context.prototype.clone = function (values) {
        var _a, _b, _c, _d;
        if (values === void 0) { values = {}; }
        return new Context(this.pdf, {
            svg2pdfParameters: this.svg2pdfParameters,
            attributeState: values.attributeState
                ? values.attributeState.clone()
                : this.attributeState.clone(),
            viewport: (_a = values.viewport) !== null && _a !== void 0 ? _a : this.viewport,
            refsHandler: this.refsHandler,
            styleSheets: this.styleSheets,
            textMeasure: this.textMeasure,
            transform: (_b = values.transform) !== null && _b !== void 0 ? _b : this.transform,
            withinClipPath: (_c = values.withinClipPath) !== null && _c !== void 0 ? _c : this.withinClipPath,
            withinUse: (_d = values.withinUse) !== null && _d !== void 0 ? _d : this.withinUse
        });
    };
    return Context;
}());

var ReferencesHandler = /** @class */ (function () {
    function ReferencesHandler(idMap) {
        this.renderedElements = {};
        this.idMap = idMap;
        this.idPrefix = String(ReferencesHandler.instanceCounter++);
    }
    ReferencesHandler.prototype.getRendered = function (id, contextColors, renderCallback) {
        return __awaiter(this, void 0, void 0, function () {
            var key, svgNode;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        key = this.generateKey(id, contextColors);
                        if (this.renderedElements.hasOwnProperty(key)) {
                            return [2 /*return*/, this.renderedElements[id]];
                        }
                        svgNode = this.get(id);
                        this.renderedElements[key] = svgNode;
                        return [4 /*yield*/, renderCallback(svgNode)];
                    case 1:
                        _a.sent();
                        return [2 /*return*/, svgNode];
                }
            });
        });
    };
    ReferencesHandler.prototype.get = function (id) {
        // return this.idMap[cssEsc(id, { isIdentifier: true })]
        return this.idMap[id]; // jsroot uses plain ids
    };
    ReferencesHandler.prototype.generateKey = function (id, contextColors) {
        var colorHash = '';
        var keys = ['color', 'contextFill', 'contextStroke'];
        if (contextColors) {
            colorHash = keys.map(function (key) { var _a, _b; return (_b = (_a = contextColors[key]) === null || _a === void 0 ? void 0 : _a.toRGBA()) !== null && _b !== void 0 ? _b : ''; }).join('|');
        }
        return this.idPrefix + '|' + id + '|' + colorHash;
    };
    ReferencesHandler.instanceCounter = 0;
    return ReferencesHandler;
}());

function getAngle(from, to) {
    return Math.atan2(to[1] - from[1], to[0] - from[0]);
}
var cToQ = 2 / 3; // ratio to convert quadratic bezier curves to cubic ones
// transforms a cubic bezier control point to a quadratic one: returns from + (2/3) * (to - from)
function toCubic(from, to) {
    return [cToQ * (to[0] - from[0]) + from[0], cToQ * (to[1] - from[1]) + from[1]];
}
function normalize(v) {
    var length = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
    return [v[0] / length, v[1] / length];
}
function getDirectionVector(from, to) {
    var v = [to[0] - from[0], to[1] - from[1]];
    return normalize(v);
}
function addVectors(v1, v2) {
    return [v1[0] + v2[0], v1[1] + v2[1]];
}
// multiplies a vector with a matrix: vec' = vec * matrix
function multVecMatrix(vec, matrix) {
    var x = vec[0];
    var y = vec[1];
    return [matrix.a * x + matrix.c * y + matrix.e, matrix.b * x + matrix.d * y + matrix.f];
}

var Path = /** @class */ (function () {
    function Path() {
        this.segments = [];
    }
    Path.prototype.moveTo = function (x, y) {
        this.segments.push(new MoveTo(x, y));
        return this;
    };
    Path.prototype.lineTo = function (x, y) {
        this.segments.push(new LineTo(x, y));
        return this;
    };
    Path.prototype.curveTo = function (x1, y1, x2, y2, x, y) {
        this.segments.push(new CurveTo(x1, y1, x2, y2, x, y));
        return this;
    };
    Path.prototype.close = function () {
        this.segments.push(new Close());
        return this;
    };
    /**
     * Transforms the path in place
     */
    Path.prototype.transform = function (matrix) {
        this.segments.forEach(function (seg) {
            if (seg instanceof MoveTo || seg instanceof LineTo || seg instanceof CurveTo) {
                var p = multVecMatrix([seg.x, seg.y], matrix);
                seg.x = p[0];
                seg.y = p[1];
            }
            if (seg instanceof CurveTo) {
                var p1 = multVecMatrix([seg.x1, seg.y1], matrix);
                var p2 = multVecMatrix([seg.x2, seg.y2], matrix);
                seg.x1 = p1[0];
                seg.y1 = p1[1];
                seg.x2 = p2[0];
                seg.y2 = p2[1];
            }
        });
    };
    Path.prototype.draw = function (context) {
        var p = context.pdf;
        this.segments.forEach(function (s) {
            if (s instanceof MoveTo) {
                p.moveTo(s.x, s.y);
            }
            else if (s instanceof LineTo) {
                p.lineTo(s.x, s.y);
            }
            else if (s instanceof CurveTo) {
                p.curveTo(s.x1, s.y1, s.x2, s.y2, s.x, s.y);
            }
            else {
                p.close();
            }
        });
    };
    return Path;
}());
var MoveTo = /** @class */ (function () {
    function MoveTo(x, y) {
        this.x = x;
        this.y = y;
    }
    return MoveTo;
}());
var LineTo = /** @class */ (function () {
    function LineTo(x, y) {
        this.x = x;
        this.y = y;
    }
    return LineTo;
}());
var CurveTo = /** @class */ (function () {
    function CurveTo(x1, y1, x2, y2, x, y) {
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
        this.x = x;
        this.y = y;
    }
    return CurveTo;
}());
var Close = /** @class */ (function () {
    function Close() {
    }
    return Close;
}());

function nodeIs(node, tagsString) {
    return tagsString.split(',').indexOf((node.nodeName || node.tagName).toLowerCase()) >= 0;
}
function forEachChild(node, fn) {
    // copy list of children, as the original might be modified
    var children = [];
    for (var i = 0; i < node.childNodes.length; i++) {
        var childNode = node.childNodes[i];
        if (childNode.nodeName.charAt(0) !== '#')
            children.push(childNode);
    }
    for (var i = 0; i < children.length; i++) {
        fn(i, children[i]);
    }
}
// returns an attribute of a node, either from the node directly or from css
function getAttribute(node, styleSheets, propertyNode, propertyCss) {
    var _a;
    if (propertyCss === void 0) { propertyCss = propertyNode; }
    var attribute = (_a = node.style) === null || _a === void 0 ? void 0 : _a.getPropertyValue(propertyCss);
    if (attribute) {
        return attribute;
    }
    else {
        var propertyValue = styleSheets.getPropertyValue(node, propertyCss);
        if (propertyValue) {
            return propertyValue;
        }
        else if (node.hasAttribute(propertyNode)) {
            return node.getAttribute(propertyNode) || undefined;
        }
        else {
            return undefined;
        }
    }
}
function svgNodeIsVisible(svgNode, parentVisible, context) {
    if (getAttribute(svgNode.element, context.styleSheets, 'display') === 'none') {
        return false;
    }
    var visible = parentVisible;
    var visibility = getAttribute(svgNode.element, context.styleSheets, 'visibility');
    if (visibility) {
        visible = visibility !== 'hidden';
    }
    return visible;
}
function svgNodeAndChildrenVisible(svgNode, parentVisible, context) {
    var visible = svgNodeIsVisible(svgNode, parentVisible, context);
    if (svgNode.element.childNodes.length === 0) {
        return false;
    }
    svgNode.children.forEach(function (child) {
        if (child.isVisible(visible, context)) {
            visible = true;
        }
    });
    return visible;
}

/**
 * @constructor
 * @property {Marker[]} markers
 */
var MarkerList = /** @class */ (function () {
    function MarkerList() {
        this.markers = [];
    }
    MarkerList.prototype.addMarker = function (markers) {
        this.markers.push(markers);
    };
    MarkerList.prototype.draw = function (context) {
        return __awaiter(this, void 0, void 0, function () {
            var i, marker, tf, angle, anchor, cos, sin, contextColors;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        i = 0;
                        _a.label = 1;
                    case 1:
                        if (!(i < this.markers.length)) return [3 /*break*/, 4];
                        marker = this.markers[i];
                        tf = void 0;
                        angle = marker.angle, anchor = marker.anchor;
                        cos = Math.cos(angle);
                        sin = Math.sin(angle);
                        // position at and rotate around anchor
                        tf = context.pdf.Matrix(cos, sin, -sin, cos, anchor[0], anchor[1]);
                        // scale with stroke-width
                        tf = context.pdf.matrixMult(context.pdf.Matrix(context.attributeState.strokeWidth, 0, 0, context.attributeState.strokeWidth, 0, 0), tf);
                        tf = context.pdf.matrixMult(tf, context.transform);
                        // as the marker is already scaled by the current line width we must not apply the line width twice!
                        context.pdf.saveGraphicsState();
                        contextColors = AttributeState.getContextColors(context);
                        return [4 /*yield*/, context.refsHandler.getRendered(marker.id, contextColors, function (node) {
                                return node.apply(context);
                            })];
                    case 2:
                        _a.sent();
                        context.pdf.doFormObject(context.refsHandler.generateKey(marker.id, contextColors), tf);
                        context.pdf.restoreGraphicsState();
                        _a.label = 3;
                    case 3:
                        i++;
                        return [3 /*break*/, 1];
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    return MarkerList;
}());
/**
 * @param {string} id
 * @param {[number,number]} anchor
 * @param {number} angle
 */
var Marker = /** @class */ (function () {
    function Marker(id, anchor, angle, isStartMarker) {
        if (isStartMarker === void 0) { isStartMarker = false; }
        this.id = id;
        this.anchor = anchor;
        this.angle = angle;
        this.isStartMarker = isStartMarker;
    }
    return Marker;
}());

var iriReference = /url\(["']?#([^"']+)["']?\)/;
var alignmentBaselineMap = {
    bottom: 'bottom',
    'text-bottom': 'bottom',
    top: 'top',
    'text-top': 'top',
    hanging: 'hanging',
    middle: 'middle',
    central: 'middle',
    center: 'middle',
    mathematical: 'middle',
    ideographic: 'ideographic',
    alphabetic: 'alphabetic',
    baseline: 'alphabetic'
};
var svgNamespaceURI = 'http://www.w3.org/2000/svg';

/**
 * Convert em, px and bare number attributes to pixel values
 * @param {string} value
 * @param {number} pdfFontSize
 */
function toPixels(value, pdfFontSize) {
    var match;
    // em
    match = value && value.toString().match(/^([\-0-9.]+)em$/);
    if (match) {
        return parseFloat(match[1]) * pdfFontSize;
    }
    // pixels
    match = value && value.toString().match(/^([\-0-9.]+)(px|)$/);
    if (match) {
        return parseFloat(match[1]);
    }
    return 0;
}
function mapAlignmentBaseline(value) {
    return alignmentBaselineMap[value] || 'alphabetic';
}

function parseFloats(str) {
    var floats = [];
    var regex = /[+-]?(?:(?:\d+\.?\d*)|(?:\d*\.?\d+))(?:[eE][+-]?\d+)?/g;
    var match;
    while ((match = regex.exec(str))) {
        floats.push(parseFloat(match[0]));
    }
    return floats;
}
/**
 * extends RGBColor by rgba colors as RGBColor is not capable of it
 * currentcolor: the color to return if colorString === 'currentcolor'
 */
function parseColor(colorString, contextColors) {
    if (colorString === 'transparent') {
        var transparent = new RGBColor('rgb(0,0,0)');
        transparent.a = 0;
        return transparent;
    }
    if (contextColors && colorString.toLowerCase() === 'currentcolor') {
        return contextColors.color || new RGBColor('rgb(0,0,0)');
    }
    if (contextColors && colorString.toLowerCase() === 'context-stroke') {
        return contextColors.contextStroke || new RGBColor('rgb(0,0,0)');
    }
    if (contextColors && colorString.toLowerCase() === 'context-fill') {
        return contextColors.contextFill || new RGBColor('rgb(0,0,0)');
    }
    var match = /\s*rgba\(((?:[^,\)]*,){3}[^,\)]*)\)\s*/.exec(colorString);
    if (match) {
        var floats = parseFloats(match[1]);
        var color = new RGBColor('rgb(' + floats.slice(0, 3).join(',') + ')');
        color.a = floats[3];
        return color;
    }
    else {
        return new RGBColor(colorString);
    }
}

function getDefaultExportFromCjs (x) {
	return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}

var fontFamilyPapandreou;
var hasRequiredFontFamilyPapandreou;

function requireFontFamilyPapandreou () {
	if (hasRequiredFontFamilyPapandreou) return fontFamilyPapandreou;
	hasRequiredFontFamilyPapandreou = 1;
	// parse
	// =====

	// states
	// ------

	var PLAIN                = 0;
	var STRINGS              = 1;
	var ESCAPING             = 2;
	var IDENTIFIER           = 3;
	var SEPARATING           = 4;
	var SPACEAFTERIDENTIFIER = 5;

	// patterns
	// --------

	var identifierPattern = /[a-z0-9_-]/i;
	var spacePattern      = /[\s\t]/;

	// ---

	var parse = function(str) {

	  // vars
	  // ----

	  var starting = true;
	  var state    = PLAIN;
	  var buffer   = '';
	  var i        = 0;
	  var quote;
	  var c;

	  // result
	  // ------

	  var names  = [];

	  // parse
	  // -----

	  while (true) {

	    c = str[i];

	    if (state === PLAIN) {

	      if (!c && starting) {

	        break;

	      } else if (!c && !starting) {

	        throw new Error('Parse error');

	      } else if (c === '"' || c === "'") {

	        quote = c;
	        state = STRINGS;
	        starting = false;

	      } else if (spacePattern.test(c)) ; else if (identifierPattern.test(c)) {

	        state = IDENTIFIER;
	        starting = false;
	        i--;

	      } else {

	        throw new Error('Parse error');

	      }

	    } else if (state === STRINGS) {

	      if (!c) {

	        throw new Error('Parse Error');

	      } else if (c === "\\") {

	        state = ESCAPING;

	      } else if (c === quote) {

	        names.push(buffer);
	        buffer = '';
	        state = SEPARATING;

	      } else {

	        buffer += c;

	      }

	    } else if (state === ESCAPING) {

	      if (c === quote || c === "\\") {

	        buffer += c;
	        state = STRINGS;

	      } else {

	        throw new Error('Parse error');

	      }

	    } else if (state === IDENTIFIER) {

	      if (!c) {

	        names.push(buffer);
	        break;

	      } else if (identifierPattern.test(c)) {

	        buffer += c;

	      } else if (c === ',') {

	        names.push(buffer);
	        buffer = '';
	        state = PLAIN;

	      } else if (spacePattern.test(c)) {

	        state = SPACEAFTERIDENTIFIER;
	      } else {

	        throw new Error('Parse error');

	      }
	    } else if (state === SPACEAFTERIDENTIFIER) {

	      if (!c) {

	        names.push(buffer);
	        break;

	      } else if (identifierPattern.test(c)) {

	        buffer += ' ' + c;
	        state = IDENTIFIER;

	      } else if (c === ',') {

	        names.push(buffer);
	        buffer = '';
	        state = PLAIN;

	      } else if (spacePattern.test(c)) ; else {

	        throw new Error('Parse error');

	      }

	    } else if (state === SEPARATING) {

	      if (!c) {

	        break;

	      } else if (c === ',') {

	        state = PLAIN;

	      } else if (spacePattern.test(c)) ; else {

	        throw new Error('Parse error');

	      }

	    }

	    i++;

	  }

	  // result
	  // ------

	  return names;

	};

	// stringify
	// =========

	// pattern
	// -------

	var stringsPattern = /[^a-z0-9_-]/i;

	// ---

	var stringify = function(names, options) {

	  // quote
	  // -----

	  var quote = options && options.quote || '"';
	  if (quote !== '"' && quote !== "'") {
	    throw new Error('Quote must be `\'` or `"`');
	  }
	  var quotePattern = new RegExp(quote, 'g');

	  // stringify
	  // ---------

	  var safeNames = [];

	  for (var i = 0; i < names.length; ++i) {
	    var name = names[i];

	    if (stringsPattern.test(name)) {
	      name = name
	        .replace(/\\/g, "\\\\")
	        .replace(quotePattern, "\\" + quote);
	      name = quote + name + quote;
	    }
	    safeNames.push(name);
	  }

	  // result
	  // ------

	  return safeNames.join(', ');
	};

	// export
	// ======

	fontFamilyPapandreou = {
	  parse:     parse,
	  stringify: stringify,
	};
	return fontFamilyPapandreou;
}

var fontFamilyPapandreouExports = requireFontFamilyPapandreou();
var FontFamily = /*@__PURE__*/getDefaultExportFromCjs(fontFamilyPapandreouExports);

var fontAliases = {
    'sans-serif': 'helvetica',
    verdana: 'helvetica',
    arial: 'helvetica',
    fixed: 'courier',
    monospace: 'courier',
    terminal: 'courier',
    serif: 'times',
    cursive: 'times',
    fantasy: 'times'
};
function findFirstAvailableFontFamily(attributeState, fontFamilies, context) {
    var fontType = combineFontStyleAndFontWeight(attributeState.fontStyle, attributeState.fontWeight);
    var availableFonts = context.pdf.getFontList();
    var firstAvailable = '';
    var fontIsAvailable = fontFamilies.some(function (font) {
        var availableStyles = availableFonts[font];
        if (availableStyles && availableStyles.indexOf(fontType) >= 0) {
            firstAvailable = font;
            return true;
        }
        font = font.toLowerCase();
        if (fontAliases.hasOwnProperty(font)) {
            firstAvailable = font;
            return true;
        }
        return false;
    });
    if (!fontIsAvailable) {
        firstAvailable = 'times';
    }
    return firstAvailable;
}
var isJsPDF23 = (function () {
    var parts = jsPDF.version.split('.');
    return parseFloat(parts[0]) === 2 && parseFloat(parts[1]) === 3;
})();
function combineFontStyleAndFontWeight(fontStyle, fontWeight) {
    if (isJsPDF23) {
        return fontWeight == 400
            ? fontStyle == 'italic'
                ? 'italic'
                : 'normal'
            : fontWeight == 700 && fontStyle !== 'italic'
                ? 'bold'
                : fontStyle + '' + fontWeight;
    }
    else {
        return fontWeight == 400 || fontWeight === 'normal'
            ? fontStyle === 'italic'
                ? 'italic'
                : 'normal'
            : (fontWeight == 700 || fontWeight === 'bold') && fontStyle === 'normal'
                ? 'bold'
                : (fontWeight == 700 ? 'bold' : fontWeight) + '' + fontStyle;
    }
}

function getBoundingBoxByChildren(context, svgnode) {
    if (getAttribute(svgnode.element, context.styleSheets, 'display') === 'none') {
        return [0, 0, 0, 0];
    }
    var boundingBox = [];
    svgnode.children.forEach(function (child) {
        var nodeBox = child.getBoundingBox(context);
        if (nodeBox[0] === 0 && nodeBox[1] === 0 && nodeBox[2] === 0 && nodeBox[3] === 0)
            return;
        var transform = child.computeNodeTransform(context);
        // TODO: take into account rotation matrix
        nodeBox[0] = nodeBox[0] * transform.sx + transform.tx;
        nodeBox[1] = nodeBox[1] * transform.sy + transform.ty;
        nodeBox[2] = nodeBox[2] * transform.sx;
        nodeBox[3] = nodeBox[3] * transform.sy;
        if (boundingBox.length === 0)
            boundingBox = nodeBox;
        else
            boundingBox = [
                Math.min(boundingBox[0], nodeBox[0]),
                Math.min(boundingBox[1], nodeBox[1]),
                Math.max(boundingBox[0] + boundingBox[2], nodeBox[0] + nodeBox[2]) -
                    Math.min(boundingBox[0], nodeBox[0]),
                Math.max(boundingBox[1] + boundingBox[3], nodeBox[1] + nodeBox[3]) -
                    Math.min(boundingBox[1], nodeBox[1])
            ];
    });
    return boundingBox.length === 0 ? [0, 0, 0, 0] : boundingBox;
}
function defaultBoundingBox(element, context) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    var pf = parseFloat;
    // TODO: check if there are other possible coordinate attributes
    var x1 = pf(element.getAttribute('x1')) ||
        pf(getAttribute(element, context.styleSheets, 'x')) ||
        pf(getAttribute(element, context.styleSheets, 'cx')) -
            pf(getAttribute(element, context.styleSheets, 'r')) ||
        0;
    var x2 = pf(element.getAttribute('x2')) ||
        x1 + pf(getAttribute(element, context.styleSheets, 'width')) ||
        pf(getAttribute(element, context.styleSheets, 'cx')) +
            pf(getAttribute(element, context.styleSheets, 'r')) ||
        0;
    var y1 = pf(element.getAttribute('y1')) ||
        pf(getAttribute(element, context.styleSheets, 'y')) ||
        pf(getAttribute(element, context.styleSheets, 'cy')) -
            pf(getAttribute(element, context.styleSheets, 'r')) ||
        0;
    var y2 = pf(element.getAttribute('y2')) ||
        y1 + pf(getAttribute(element, context.styleSheets, 'height')) ||
        pf(getAttribute(element, context.styleSheets, 'cy')) +
            pf(getAttribute(element, context.styleSheets, 'r')) ||
        0;
    return [
        Math.min(x1, x2),
        Math.min(y1, y2),
        Math.max(x1, x2) - Math.min(x1, x2),
        Math.max(y1, y2) - Math.min(y1, y2)
    ];
}

function computeViewBoxTransform(node, viewBox, eX, eY, eWidth, eHeight, context, noTranslate) {
    if (noTranslate === void 0) { noTranslate = false; }
    var vbX = viewBox[0];
    var vbY = viewBox[1];
    var vbWidth = viewBox[2];
    var vbHeight = viewBox[3];
    var scaleX = eWidth / vbWidth;
    var scaleY = eHeight / vbHeight;
    var align, meetOrSlice;
    var preserveAspectRatio = node.getAttribute('preserveAspectRatio');
    if (preserveAspectRatio) {
        var alignAndMeetOrSlice = preserveAspectRatio.split(' ');
        if (alignAndMeetOrSlice[0] === 'defer') {
            alignAndMeetOrSlice = alignAndMeetOrSlice.slice(1);
        }
        align = alignAndMeetOrSlice[0];
        meetOrSlice = alignAndMeetOrSlice[1] || 'meet';
    }
    else {
        align = 'xMidYMid';
        meetOrSlice = 'meet';
    }
    if (align !== 'none') {
        if (meetOrSlice === 'meet') {
            // uniform scaling with min scale
            scaleX = scaleY = Math.min(scaleX, scaleY);
        }
        else if (meetOrSlice === 'slice') {
            // uniform scaling with max scale
            scaleX = scaleY = Math.max(scaleX, scaleY);
        }
    }
    if (noTranslate) {
        return context.pdf.Matrix(scaleX, 0, 0, scaleY, 0, 0);
    }
    var translateX = eX - vbX * scaleX;
    var translateY = eY - vbY * scaleY;
    if (align.indexOf('xMid') >= 0) {
        translateX += (eWidth - vbWidth * scaleX) / 2;
    }
    else if (align.indexOf('xMax') >= 0) {
        translateX += eWidth - vbWidth * scaleX;
    }
    if (align.indexOf('YMid') >= 0) {
        translateY += (eHeight - vbHeight * scaleY) / 2;
    }
    else if (align.indexOf('YMax') >= 0) {
        translateY += eHeight - vbHeight * scaleY;
    }
    var translate = context.pdf.Matrix(1, 0, 0, 1, translateX, translateY);
    var scale = context.pdf.Matrix(scaleX, 0, 0, scaleY, 0, 0);
    return context.pdf.matrixMult(scale, translate);
}
// parses the "transform" string
function parseTransform(transformString, context) {
    if (!transformString || transformString === 'none')
        return context.pdf.unitMatrix;
    var mRegex = /^[\s,]*matrix\(([^)]+)\)\s*/, tRegex = /^[\s,]*translate\(([^)]+)\)\s*/, rRegex = /^[\s,]*rotate\(([^)]+)\)\s*/, sRegex = /^[\s,]*scale\(([^)]+)\)\s*/, sXRegex = /^[\s,]*skewX\(([^)]+)\)\s*/, sYRegex = /^[\s,]*skewY\(([^)]+)\)\s*/;
    var resultMatrix = context.pdf.unitMatrix;
    var m;
    var tSLength;
    while (transformString.length > 0 && transformString.length !== tSLength) {
        tSLength = transformString.length;
        var match = mRegex.exec(transformString);
        if (match) {
            m = parseFloats(match[1]);
            resultMatrix = context.pdf.matrixMult(context.pdf.Matrix(m[0], m[1], m[2], m[3], m[4], m[5]), resultMatrix);
            transformString = transformString.substr(match[0].length);
        }
        match = rRegex.exec(transformString);
        if (match) {
            m = parseFloats(match[1]);
            var a = (Math.PI * m[0]) / 180;
            resultMatrix = context.pdf.matrixMult(context.pdf.Matrix(Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0), resultMatrix);
            if (m[1] || m[2]) {
                var t1 = context.pdf.Matrix(1, 0, 0, 1, m[1], m[2]);
                var t2 = context.pdf.Matrix(1, 0, 0, 1, -m[1], -m[2]);
                resultMatrix = context.pdf.matrixMult(t2, context.pdf.matrixMult(resultMatrix, t1));
            }
            transformString = transformString.substr(match[0].length);
        }
        match = tRegex.exec(transformString);
        if (match) {
            m = parseFloats(match[1]);
            resultMatrix = context.pdf.matrixMult(context.pdf.Matrix(1, 0, 0, 1, m[0], m[1] || 0), resultMatrix);
            transformString = transformString.substr(match[0].length);
        }
        match = sRegex.exec(transformString);
        if (match) {
            m = parseFloats(match[1]);
            if (!m[1])
                m[1] = m[0];
            resultMatrix = context.pdf.matrixMult(context.pdf.Matrix(m[0], 0, 0, m[1], 0, 0), resultMatrix);
            transformString = transformString.substr(match[0].length);
        }
        match = sXRegex.exec(transformString);
        if (match) {
            m = parseFloat(match[1]);
            m *= Math.PI / 180;
            resultMatrix = context.pdf.matrixMult(context.pdf.Matrix(1, 0, Math.tan(m), 1, 0, 0), resultMatrix);
            transformString = transformString.substr(match[0].length);
        }
        match = sYRegex.exec(transformString);
        if (match) {
            m = parseFloat(match[1]);
            m *= Math.PI / 180;
            resultMatrix = context.pdf.matrixMult(context.pdf.Matrix(1, Math.tan(m), 0, 1, 0, 0), resultMatrix);
            transformString = transformString.substr(match[0].length);
        }
    }
    return resultMatrix;
}

var SvgNode = /** @class */ (function () {
    function SvgNode(element, children) {
        this.element = element;
        this.children = children;
        this.parent = null;
    }
    SvgNode.prototype.setParent = function (parent) {
        this.parent = parent;
    };
    SvgNode.prototype.getParent = function () {
        return this.parent;
    };
    SvgNode.prototype.getBoundingBox = function (context) {
        if (getAttribute(this.element, context.styleSheets, 'display') === 'none') {
            return [0, 0, 0, 0];
        }
        return this.getBoundingBoxCore(context);
    };
    SvgNode.prototype.computeNodeTransform = function (context) {
        var nodeTransform = this.computeNodeTransformCore(context);
        var transformString = getAttribute(this.element, context.styleSheets, 'transform');
        if (!transformString)
            return nodeTransform;
        else
            return context.pdf.matrixMult(nodeTransform, parseTransform(transformString, context));
    };
    return SvgNode;
}());

var NonRenderedNode = /** @class */ (function (_super) {
    __extends(NonRenderedNode, _super);
    function NonRenderedNode() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    NonRenderedNode.prototype.render = function (parentContext) {
        return Promise.resolve();
    };
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    NonRenderedNode.prototype.getBoundingBoxCore = function (context) {
        return [];
    };
    NonRenderedNode.prototype.computeNodeTransformCore = function (context) {
        return context.pdf.unitMatrix;
    };
    return NonRenderedNode;
}(SvgNode));

var Gradient = /** @class */ (function (_super) {
    __extends(Gradient, _super);
    function Gradient(pdfGradientType, element, children) {
        var _this = _super.call(this, element, children) || this;
        _this.pdfGradientType = pdfGradientType;
        _this.contextColor = undefined;
        return _this;
    }
    Gradient.prototype.apply = function (context) {
        return __awaiter(this, void 0, void 0, function () {
            var id, colors, opacitySum, hasOpacity, gState, pattern;
            return __generator(this, function (_a) {
                id = this.element.getAttribute('id');
                if (!id) {
                    return [2 /*return*/];
                }
                colors = this.getStops(context.styleSheets);
                opacitySum = 0;
                hasOpacity = false;
                colors.forEach(function (_a) {
                    var opacity = _a.opacity;
                    if (opacity && opacity !== 1) {
                        opacitySum += opacity;
                        hasOpacity = true;
                    }
                });
                if (hasOpacity) {
                    gState = new GState({ opacity: opacitySum / colors.length });
                }
                pattern = new ShadingPattern(this.pdfGradientType, this.getCoordinates(), colors, gState);
                context.pdf.addShadingPattern(id, pattern);
                return [2 /*return*/];
            });
        });
    };
    Gradient.prototype.getStops = function (styleSheets) {
        var _this = this;
        if (this.stops) {
            return this.stops;
        }
        // Only need to calculate contextColor once
        if (this.contextColor === undefined) {
            this.contextColor = null;
            var ancestor = this;
            while (ancestor) {
                var colorAttr = getAttribute(ancestor.element, styleSheets, 'color');
                if (colorAttr) {
                    this.contextColor = parseColor(colorAttr, null);
                    break;
                }
                ancestor = ancestor.getParent();
            }
        }
        var stops = [];
        this.children.forEach(function (stop) {
            if (stop.element.tagName.toLowerCase() === 'stop') {
                var colorAttr = getAttribute(stop.element, styleSheets, 'color');
                var color = parseColor(getAttribute(stop.element, styleSheets, 'stop-color') || '', colorAttr
                    ? { color: parseColor(colorAttr, null) }
                    : { color: _this.contextColor });
                var opacity = parseFloat(getAttribute(stop.element, styleSheets, 'stop-opacity') || '1');
                stops.push({
                    offset: Gradient.parseGradientOffset(stop.element.getAttribute('offset') || '0'),
                    color: [color.r, color.g, color.b],
                    opacity: opacity
                });
            }
        });
        return (this.stops = stops);
    };
    Gradient.prototype.getBoundingBoxCore = function (context) {
        return defaultBoundingBox(this.element, context);
    };
    Gradient.prototype.computeNodeTransformCore = function (context) {
        return context.pdf.unitMatrix;
    };
    Gradient.prototype.isVisible = function (parentVisible, context) {
        return svgNodeAndChildrenVisible(this, parentVisible, context);
    };
    /**
     * Convert percentage to decimal
     */
    Gradient.parseGradientOffset = function (value) {
        var parsedValue = parseFloat(value);
        if (!isNaN(parsedValue) && value.indexOf('%') >= 0) {
            return parsedValue / 100;
        }
        return parsedValue;
    };
    return Gradient;
}(NonRenderedNode));

var LinearGradient = /** @class */ (function (_super) {
    __extends(LinearGradient, _super);
    function LinearGradient(element, children) {
        return _super.call(this, 'axial', element, children) || this;
    }
    LinearGradient.prototype.getCoordinates = function () {
        return [
            parseFloat(this.element.getAttribute('x1') || '0'),
            parseFloat(this.element.getAttribute('y1') || '0'),
            parseFloat(this.element.getAttribute('x2') || '1'),
            parseFloat(this.element.getAttribute('y2') || '0')
        ];
    };
    return LinearGradient;
}(Gradient));

var RadialGradient = /** @class */ (function (_super) {
    __extends(RadialGradient, _super);
    function RadialGradient(element, children) {
        return _super.call(this, 'radial', element, children) || this;
    }
    RadialGradient.prototype.getCoordinates = function () {
        var cx = this.element.getAttribute('cx');
        var cy = this.element.getAttribute('cy');
        var fx = this.element.getAttribute('fx');
        var fy = this.element.getAttribute('fy');
        return [
            parseFloat(fx || cx || '0.5'),
            parseFloat(fy || cy || '0.5'),
            0,
            parseFloat(cx || '0.5'),
            parseFloat(cy || '0.5'),
            parseFloat(this.element.getAttribute('r') || '0.5')
        ];
    };
    return RadialGradient;
}(Gradient));

var GradientFill = /** @class */ (function () {
    function GradientFill(key, gradient) {
        this.key = key;
        this.gradient = gradient;
    }
    GradientFill.prototype.getFillData = function (forNode, context) {
        return __awaiter(this, void 0, void 0, function () {
            var gradientUnitsMatrix, bBox, gradientTransform;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, context.refsHandler.getRendered(this.key, null, function (node) {
                            return node.apply(new Context(context.pdf, {
                                refsHandler: context.refsHandler,
                                textMeasure: context.textMeasure,
                                styleSheets: context.styleSheets,
                                viewport: context.viewport,
                                svg2pdfParameters: context.svg2pdfParameters
                            }));
                        })
                        // matrix to convert between gradient space and user space
                        // for "userSpaceOnUse" this is the current transformation: tfMatrix
                        // for "objectBoundingBox" or default, the gradient gets scaled and transformed to the bounding box
                    ];
                    case 1:
                        _a.sent();
                        if (!this.gradient.element.hasAttribute('gradientUnits') ||
                            this.gradient.element.getAttribute('gradientUnits').toLowerCase() === 'objectboundingbox') {
                            bBox = forNode.getBoundingBox(context);
                            gradientUnitsMatrix = context.pdf.Matrix(bBox[2], 0, 0, bBox[3], bBox[0], bBox[1]);
                        }
                        else {
                            gradientUnitsMatrix = context.pdf.unitMatrix;
                        }
                        gradientTransform = parseTransform(getAttribute(this.gradient.element, context.styleSheets, 'gradientTransform', 'transform'), context);
                        return [2 /*return*/, {
                                key: this.key,
                                matrix: context.pdf.matrixMult(gradientTransform, gradientUnitsMatrix)
                            }];
                }
            });
        });
    };
    return GradientFill;
}());

var Pattern = /** @class */ (function (_super) {
    __extends(Pattern, _super);
    function Pattern() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    Pattern.prototype.apply = function (context) {
        return __awaiter(this, void 0, void 0, function () {
            var id, bBox, pattern, _i, _a, child;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        id = this.element.getAttribute('id');
                        if (!id) {
                            return [2 /*return*/];
                        }
                        bBox = this.getBoundingBox(context);
                        pattern = new TilingPattern([bBox[0], bBox[1], bBox[0] + bBox[2], bBox[1] + bBox[3]], bBox[2], bBox[3]);
                        context.pdf.beginTilingPattern(pattern);
                        _i = 0, _a = this.children;
                        _b.label = 1;
                    case 1:
                        if (!(_i < _a.length)) return [3 /*break*/, 4];
                        child = _a[_i];
                        return [4 /*yield*/, child.render(new Context(context.pdf, {
                                attributeState: context.attributeState,
                                refsHandler: context.refsHandler,
                                styleSheets: context.styleSheets,
                                viewport: context.viewport,
                                svg2pdfParameters: context.svg2pdfParameters,
                                textMeasure: context.textMeasure
                            }))];
                    case 2:
                        _b.sent();
                        _b.label = 3;
                    case 3:
                        _i++;
                        return [3 /*break*/, 1];
                    case 4:
                        context.pdf.endTilingPattern(id, pattern);
                        return [2 /*return*/];
                }
            });
        });
    };
    Pattern.prototype.getBoundingBoxCore = function (context) {
        return defaultBoundingBox(this.element, context);
    };
    Pattern.prototype.computeNodeTransformCore = function (context) {
        return context.pdf.unitMatrix;
    };
    Pattern.prototype.isVisible = function (parentVisible, context) {
        return svgNodeAndChildrenVisible(this, parentVisible, context);
    };
    return Pattern;
}(NonRenderedNode));

var PatternFill = /** @class */ (function () {
    function PatternFill(key, pattern) {
        this.key = key;
        this.pattern = pattern;
    }
    PatternFill.prototype.getFillData = function (forNode, context) {
        return __awaiter(this, void 0, void 0, function () {
            var patternData, bBox, patternUnitsMatrix, fillBBox, x, y, width, height, patternContentUnitsMatrix, fillBBox, x, y, width, height, patternTransformMatrix, patternTransform, matrix;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, context.refsHandler.getRendered(this.key, null, function (node) {
                            return node.apply(new Context(context.pdf, {
                                refsHandler: context.refsHandler,
                                textMeasure: context.textMeasure,
                                styleSheets: context.styleSheets,
                                viewport: context.viewport,
                                svg2pdfParameters: context.svg2pdfParameters
                            }));
                        })];
                    case 1:
                        _a.sent();
                        patternData = {
                            key: this.key,
                            boundingBox: undefined,
                            xStep: 0,
                            yStep: 0,
                            matrix: undefined
                        };
                        patternUnitsMatrix = context.pdf.unitMatrix;
                        if (!this.pattern.element.hasAttribute('patternUnits') ||
                            this.pattern.element.getAttribute('patternUnits').toLowerCase() === 'objectboundingbox') {
                            bBox = forNode.getBoundingBox(context);
                            patternUnitsMatrix = context.pdf.Matrix(1, 0, 0, 1, bBox[0], bBox[1]);
                            fillBBox = this.pattern.getBoundingBox(context);
                            x = fillBBox[0] * bBox[0] || 0;
                            y = fillBBox[1] * bBox[1] || 0;
                            width = fillBBox[2] * bBox[2] || 0;
                            height = fillBBox[3] * bBox[3] || 0;
                            patternData.boundingBox = [x, y, x + width, y + height];
                            patternData.xStep = width;
                            patternData.yStep = height;
                        }
                        patternContentUnitsMatrix = context.pdf.unitMatrix;
                        if (this.pattern.element.hasAttribute('patternContentUnits') &&
                            this.pattern.element.getAttribute('patternContentUnits').toLowerCase() ===
                                'objectboundingbox') {
                            bBox || (bBox = forNode.getBoundingBox(context));
                            patternContentUnitsMatrix = context.pdf.Matrix(bBox[2], 0, 0, bBox[3], 0, 0);
                            fillBBox = patternData.boundingBox || this.pattern.getBoundingBox(context);
                            x = fillBBox[0] / bBox[0] || 0;
                            y = fillBBox[1] / bBox[1] || 0;
                            width = fillBBox[2] / bBox[2] || 0;
                            height = fillBBox[3] / bBox[3] || 0;
                            patternData.boundingBox = [x, y, x + width, y + height];
                            patternData.xStep = width;
                            patternData.yStep = height;
                        }
                        patternTransformMatrix = context.pdf.unitMatrix;
                        patternTransform = getAttribute(this.pattern.element, context.styleSheets, 'patternTransform', 'transform');
                        if (patternTransform) {
                            patternTransformMatrix = parseTransform(patternTransform, context);
                        }
                        matrix = patternContentUnitsMatrix;
                        matrix = context.pdf.matrixMult(matrix, patternUnitsMatrix); // translate by
                        matrix = context.pdf.matrixMult(matrix, patternTransformMatrix);
                        matrix = context.pdf.matrixMult(matrix, context.transform);
                        patternData.matrix = matrix;
                        return [2 /*return*/, patternData];
                }
            });
        });
    };
    return PatternFill;
}());

function parseFill(fill, context) {
    var url = iriReference.exec(fill);
    if (url) {
        var fillUrl = url[1];
        var fillNode = context.refsHandler.get(fillUrl);
        if (fillNode && (fillNode instanceof LinearGradient || fillNode instanceof RadialGradient)) {
            return getGradientFill(fillUrl, fillNode, context);
        }
        else if (fillNode && fillNode instanceof Pattern) {
            return new PatternFill(fillUrl, fillNode);
        }
        else {
            // unsupported fill argument -> fill black
            return new ColorFill(new RGBColor('rgb(0, 0, 0)'));
        }
    }
    else {
        // plain color
        var fillColor = parseColor(fill, context.attributeState);
        if (fillColor.ok) {
            return new ColorFill(fillColor);
        }
        else if (fill === 'none') {
            return null;
        }
        else {
            return null;
        }
    }
}
function getGradientFill(fillUrl, gradient, context) {
    // "It is necessary that at least two stops are defined to have a gradient effect. If no stops are
    // defined, then painting shall occur as if 'none' were specified as the paint style. If one stop
    // is defined, then paint with the solid color fill using the color defined for that gradient
    // stop."
    var stops = gradient.getStops(context.styleSheets);
    if (stops.length === 0) {
        return null;
    }
    if (stops.length === 1) {
        var stopColor = stops[0].color;
        var rgbColor = new RGBColor();
        rgbColor.ok = true;
        rgbColor.r = stopColor[0];
        rgbColor.g = stopColor[1];
        rgbColor.b = stopColor[2];
        rgbColor.a = stops[0].opacity;
        return new ColorFill(rgbColor);
    }
    return new GradientFill(fillUrl, gradient);
}

function parseAttributes(context, svgNode, node) {
    var domNode = node || svgNode.element;
    // update color first so currentColor becomes available for this node
    var color = getAttribute(domNode, context.styleSheets, 'color');
    if (color) {
        var fillColor = parseColor(color, context.attributeState);
        if (fillColor.ok) {
            context.attributeState.color = fillColor;
        }
        else {
            // invalid color passed, reset to black
            context.attributeState.color = new RGBColor('rgb(0,0,0)');
        }
    }
    var visibility = getAttribute(domNode, context.styleSheets, 'visibility');
    if (visibility) {
        context.attributeState.visibility = visibility;
    }
    // fill mode
    var fill = getAttribute(domNode, context.styleSheets, 'fill');
    if (fill) {
        context.attributeState.fill = parseFill(fill, context);
    }
    // opacity is realized via a pdf graphics state
    var fillOpacity = getAttribute(domNode, context.styleSheets, 'fill-opacity');
    if (fillOpacity) {
        context.attributeState.fillOpacity = parseFloat(fillOpacity);
    }
    var strokeOpacity = getAttribute(domNode, context.styleSheets, 'stroke-opacity');
    if (strokeOpacity) {
        context.attributeState.strokeOpacity = parseFloat(strokeOpacity);
    }
    var opacity = getAttribute(domNode, context.styleSheets, 'opacity');
    if (opacity) {
        context.attributeState.opacity = parseFloat(opacity);
    }
    // stroke mode
    var strokeWidth = getAttribute(domNode, context.styleSheets, 'stroke-width');
    if (strokeWidth !== void 0 && strokeWidth !== '') {
        context.attributeState.strokeWidth = Math.abs(parseFloat(strokeWidth));
    }
    var stroke = getAttribute(domNode, context.styleSheets, 'stroke');
    if (stroke) {
        if (stroke === 'none') {
            context.attributeState.stroke = null;
        }
        else {
            // gradients, patterns not supported for strokes ...
            var strokeRGB = parseColor(stroke, context.attributeState);
            if (strokeRGB.ok) {
                context.attributeState.stroke = new ColorFill(strokeRGB);
            }
        }
    }
    if (stroke && context.attributeState.stroke instanceof ColorFill) {
        context.attributeState.contextStroke = context.attributeState.stroke.color;
    }
    if (fill && context.attributeState.fill instanceof ColorFill) {
        context.attributeState.contextFill = context.attributeState.fill.color;
    }
    var lineCap = getAttribute(domNode, context.styleSheets, 'stroke-linecap');
    if (lineCap) {
        context.attributeState.strokeLinecap = lineCap;
    }
    var lineJoin = getAttribute(domNode, context.styleSheets, 'stroke-linejoin');
    if (lineJoin) {
        context.attributeState.strokeLinejoin = lineJoin;
    }
    var dashArray = getAttribute(domNode, context.styleSheets, 'stroke-dasharray');
    if (dashArray) {
        var dashOffset = parseInt(getAttribute(domNode, context.styleSheets, 'stroke-dashoffset') || '0');
        context.attributeState.strokeDasharray = parseFloats(dashArray);
        context.attributeState.strokeDashoffset = dashOffset;
    }
    var miterLimit = getAttribute(domNode, context.styleSheets, 'stroke-miterlimit');
    if (miterLimit !== void 0 && miterLimit !== '') {
        context.attributeState.strokeMiterlimit = parseFloat(miterLimit);
    }
    var xmlSpace = domNode.getAttribute('xml:space');
    if (xmlSpace) {
        context.attributeState.xmlSpace = xmlSpace;
    }
    var whiteSpace = getAttribute(domNode, context.styleSheets, 'white-space');
    if (whiteSpace) {
        context.attributeState.whiteSpace = whiteSpace;
    }
    var fontWeight = getAttribute(domNode, context.styleSheets, 'font-weight');
    if (fontWeight) {
        context.attributeState.fontWeight = fontWeight;
    }
    var fontStyle = getAttribute(domNode, context.styleSheets, 'font-style');
    if (fontStyle) {
        context.attributeState.fontStyle = fontStyle;
    }
    var fontFamily = getAttribute(domNode, context.styleSheets, 'font-family');
    if (fontFamily) {
        var fontFamilies = FontFamily.parse(fontFamily);
        context.attributeState.fontFamily = findFirstAvailableFontFamily(context.attributeState, fontFamilies, context);
    }
    var fontSize = getAttribute(domNode, context.styleSheets, 'font-size');
    if (fontSize) {
        var pdfFontSize = context.pdf.getFontSize();
        context.attributeState.fontSize = toPixels(fontSize, pdfFontSize);
    }
    var alignmentBaseline = getAttribute(domNode, context.styleSheets, 'vertical-align') ||
        getAttribute(domNode, context.styleSheets, 'alignment-baseline');
    if (alignmentBaseline) {
        var matchArr = alignmentBaseline.match(/(baseline|text-bottom|alphabetic|ideographic|middle|central|mathematical|text-top|bottom|center|top|hanging)/);
        if (matchArr) {
            context.attributeState.alignmentBaseline = matchArr[0];
        }
    }
    var textAnchor = getAttribute(domNode, context.styleSheets, 'text-anchor');
    if (textAnchor) {
        context.attributeState.textAnchor = textAnchor;
    }
    var fillRule = getAttribute(domNode, context.styleSheets, 'fill-rule');
    if (fillRule) {
        context.attributeState.fillRule = fillRule;
    }
}
function applyAttributes(childContext, parentContext, node) {
    var fillOpacity = 1.0, strokeOpacity = 1.0;
    fillOpacity *= childContext.attributeState.fillOpacity;
    fillOpacity *= childContext.attributeState.opacity;
    if (childContext.attributeState.fill instanceof ColorFill &&
        typeof childContext.attributeState.fill.color.a !== 'undefined') {
        fillOpacity *= childContext.attributeState.fill.color.a;
    }
    strokeOpacity *= childContext.attributeState.strokeOpacity;
    strokeOpacity *= childContext.attributeState.opacity;
    if (childContext.attributeState.stroke instanceof ColorFill &&
        typeof childContext.attributeState.stroke.color.a !== 'undefined') {
        strokeOpacity *= childContext.attributeState.stroke.color.a;
    }
    var hasFillOpacity = fillOpacity < 1.0;
    var hasStrokeOpacity = strokeOpacity < 1.0;
    // This is a workaround for symbols that are used multiple times with different
    // fill/stroke attributes. All paths within symbols are both filled and stroked
    // and we set the fill/stroke to transparent if the use element has
    // fill/stroke="none".
    if (nodeIs(node, 'use')) {
        hasFillOpacity = true;
        hasStrokeOpacity = true;
        fillOpacity *= childContext.attributeState.fill ? 1 : 0;
        strokeOpacity *= childContext.attributeState.stroke ? 1 : 0;
    }
    else if (childContext.withinUse) {
        if (childContext.attributeState.fill !== parentContext.attributeState.fill) {
            hasFillOpacity = true;
            fillOpacity *= childContext.attributeState.fill ? 1 : 0;
        }
        else if (hasFillOpacity && !childContext.attributeState.fill) {
            fillOpacity = 0;
        }
        if (childContext.attributeState.stroke !== parentContext.attributeState.stroke) {
            hasStrokeOpacity = true;
            strokeOpacity *= childContext.attributeState.stroke ? 1 : 0;
        }
        else if (hasStrokeOpacity && !childContext.attributeState.stroke) {
            strokeOpacity = 0;
        }
    }
    if (hasFillOpacity || hasStrokeOpacity) {
        var gState = {};
        hasFillOpacity && (gState['opacity'] = fillOpacity);
        hasStrokeOpacity && (gState['stroke-opacity'] = strokeOpacity);
        childContext.pdf.setGState(new GState(gState));
    }
    if (childContext.attributeState.fill &&
        childContext.attributeState.fill !== parentContext.attributeState.fill &&
        childContext.attributeState.fill instanceof ColorFill &&
        childContext.attributeState.fill.color.ok &&
        !nodeIs(node, 'text')) {
        // text fill color will be applied through setTextColor()
        childContext.pdf.setFillColor(childContext.attributeState.fill.color.r, childContext.attributeState.fill.color.g, childContext.attributeState.fill.color.b);
    }
    if (childContext.attributeState.strokeWidth !== parentContext.attributeState.strokeWidth) {
        childContext.pdf.setLineWidth(childContext.attributeState.strokeWidth);
    }
    if (childContext.attributeState.stroke !== parentContext.attributeState.stroke &&
        childContext.attributeState.stroke instanceof ColorFill) {
        childContext.pdf.setDrawColor(childContext.attributeState.stroke.color.r, childContext.attributeState.stroke.color.g, childContext.attributeState.stroke.color.b);
    }
    if (childContext.attributeState.strokeLinecap !== parentContext.attributeState.strokeLinecap) {
        childContext.pdf.setLineCap(childContext.attributeState.strokeLinecap);
    }
    if (childContext.attributeState.strokeLinejoin !== parentContext.attributeState.strokeLinejoin) {
        childContext.pdf.setLineJoin(childContext.attributeState.strokeLinejoin);
    }
    if ((childContext.attributeState.strokeDasharray !== parentContext.attributeState.strokeDasharray ||
        childContext.attributeState.strokeDashoffset !==
            parentContext.attributeState.strokeDashoffset) &&
        childContext.attributeState.strokeDasharray) {
        childContext.pdf.setLineDashPattern(childContext.attributeState.strokeDasharray, childContext.attributeState.strokeDashoffset);
    }
    if (childContext.attributeState.strokeMiterlimit !== parentContext.attributeState.strokeMiterlimit) {
        childContext.pdf.setLineMiterLimit(childContext.attributeState.strokeMiterlimit);
    }
    var font;
    if (childContext.attributeState.fontFamily !== parentContext.attributeState.fontFamily) {
        if (fontAliases.hasOwnProperty(childContext.attributeState.fontFamily)) {
            font = fontAliases[childContext.attributeState.fontFamily];
        }
        else {
            font = childContext.attributeState.fontFamily;
        }
    }
    if (childContext.attributeState.fill &&
        childContext.attributeState.fill !== parentContext.attributeState.fill &&
        childContext.attributeState.fill instanceof ColorFill &&
        childContext.attributeState.fill.color.ok) {
        var fillColor = childContext.attributeState.fill.color;
        childContext.pdf.setTextColor(fillColor.r, fillColor.g, fillColor.b);
    }
    var fontStyle;
    if (childContext.attributeState.fontWeight !== parentContext.attributeState.fontWeight ||
        childContext.attributeState.fontStyle !== parentContext.attributeState.fontStyle) {
        fontStyle = combineFontStyleAndFontWeight(childContext.attributeState.fontStyle, childContext.attributeState.fontWeight);
    }
    if (font !== undefined || fontStyle !== undefined) {
        if (font === undefined) {
            if (fontAliases.hasOwnProperty(childContext.attributeState.fontFamily)) {
                font = fontAliases[childContext.attributeState.fontFamily];
            }
            else {
                font = childContext.attributeState.fontFamily;
            }
        }
        childContext.pdf.setFont(font, fontStyle);
    }
    if (childContext.attributeState.fontSize !== parentContext.attributeState.fontSize) {
        // correct for a jsPDF-instance measurement unit that differs from `pt`
        childContext.pdf.setFontSize(childContext.attributeState.fontSize * childContext.pdf.internal.scaleFactor);
    }
}
function applyContext(context) {
    var attributeState = context.attributeState, pdf = context.pdf;
    var fillOpacity = 1.0, strokeOpacity = 1.0;
    fillOpacity *= attributeState.fillOpacity;
    fillOpacity *= attributeState.opacity;
    if (attributeState.fill instanceof ColorFill &&
        typeof attributeState.fill.color.a !== 'undefined') {
        fillOpacity *= attributeState.fill.color.a;
    }
    strokeOpacity *= attributeState.strokeOpacity;
    strokeOpacity *= attributeState.opacity;
    if (attributeState.stroke instanceof ColorFill &&
        typeof attributeState.stroke.color.a !== 'undefined') {
        strokeOpacity *= attributeState.stroke.color.a;
    }
    var gState = {};
    gState['opacity'] = fillOpacity;
    gState['stroke-opacity'] = strokeOpacity;
    pdf.setGState(new GState(gState));
    if (attributeState.fill &&
        attributeState.fill instanceof ColorFill &&
        attributeState.fill.color.ok) {
        // text fill color will be applied through setTextColor()
        pdf.setFillColor(attributeState.fill.color.r, attributeState.fill.color.g, attributeState.fill.color.b);
    }
    else {
        pdf.setFillColor(0, 0, 0);
    }
    pdf.setLineWidth(attributeState.strokeWidth);
    if (attributeState.stroke instanceof ColorFill) {
        pdf.setDrawColor(attributeState.stroke.color.r, attributeState.stroke.color.g, attributeState.stroke.color.b);
    }
    else {
        pdf.setDrawColor(0, 0, 0);
    }
    pdf.setLineCap(attributeState.strokeLinecap);
    pdf.setLineJoin(attributeState.strokeLinejoin);
    if (attributeState.strokeDasharray) {
        pdf.setLineDashPattern(attributeState.strokeDasharray, attributeState.strokeDashoffset);
    }
    else {
        pdf.setLineDashPattern([], 0);
    }
    pdf.setLineMiterLimit(attributeState.strokeMiterlimit);
    var font;
    if (fontAliases.hasOwnProperty(attributeState.fontFamily)) {
        font = fontAliases[attributeState.fontFamily];
    }
    else {
        font = attributeState.fontFamily;
    }
    if (attributeState.fill &&
        attributeState.fill instanceof ColorFill &&
        attributeState.fill.color.ok) {
        var fillColor = attributeState.fill.color;
        pdf.setTextColor(fillColor.r, fillColor.g, fillColor.b);
    }
    else {
        pdf.setTextColor(0, 0, 0);
    }
    var fontStyle = '';
    if (attributeState.fontWeight === 'bold') {
        fontStyle = 'bold';
    }
    if (attributeState.fontStyle === 'italic') {
        fontStyle += 'italic';
    }
    if (fontStyle === '') {
        fontStyle = 'normal';
    }
    if (font !== undefined || fontStyle !== undefined) {
        if (font === undefined) {
            if (fontAliases.hasOwnProperty(attributeState.fontFamily)) {
                font = fontAliases[attributeState.fontFamily];
            }
            else {
                font = attributeState.fontFamily;
            }
        }
        pdf.setFont(font, fontStyle);
    }
    else {
        pdf.setFont('helvetica', fontStyle);
    }
    // correct for a jsPDF-instance measurement unit that differs from `pt`
    pdf.setFontSize(attributeState.fontSize * pdf.internal.scaleFactor);
}

function getClipPathNode(clipPathAttr, targetNode, context) {
    var match = iriReference.exec(clipPathAttr);
    if (!match) {
        return undefined;
    }
    var clipPathId = match[1];
    var clipNode = context.refsHandler.get(clipPathId);
    return clipNode || undefined;
}
function applyClipPath(targetNode, clipPathNode, context) {
    return __awaiter(this, void 0, void 0, function () {
        var clipContext, bBox;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    clipContext = context.clone();
                    if (clipPathNode.element.hasAttribute('clipPathUnits') &&
                        clipPathNode.element.getAttribute('clipPathUnits').toLowerCase() === 'objectboundingbox') {
                        bBox = targetNode.getBoundingBox(context);
                        clipContext.transform = context.pdf.matrixMult(context.pdf.Matrix(bBox[2], 0, 0, bBox[3], bBox[0], bBox[1]), context.transform);
                    }
                    return [4 /*yield*/, clipPathNode.apply(clipContext)];
                case 1:
                    _a.sent();
                    return [2 /*return*/];
            }
        });
    });
}

var RenderedNode = /** @class */ (function (_super) {
    __extends(RenderedNode, _super);
    function RenderedNode() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    RenderedNode.prototype.render = function (parentContext) {
        return __awaiter(this, void 0, void 0, function () {
            var context, clipPathAttribute, hasClipPath, clipNode;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (!this.isVisible(parentContext.attributeState.visibility !== 'hidden', parentContext)) {
                            return [2 /*return*/];
                        }
                        context = parentContext.clone();
                        context.transform = context.pdf.matrixMult(this.computeNodeTransform(context), parentContext.transform);
                        parseAttributes(context, this);
                        clipPathAttribute = getAttribute(this.element, context.styleSheets, 'clip-path');
                        hasClipPath = clipPathAttribute && clipPathAttribute !== 'none';
                        if (!hasClipPath) return [3 /*break*/, 5];
                        clipNode = getClipPathNode(clipPathAttribute, this, context);
                        if (!clipNode) return [3 /*break*/, 4];
                        if (!clipNode.isVisible(true, context)) return [3 /*break*/, 2];
                        context.pdf.saveGraphicsState();
                        return [4 /*yield*/, applyClipPath(this, clipNode, context)];
                    case 1:
                        _a.sent();
                        return [3 /*break*/, 3];
                    case 2: return [2 /*return*/];
                    case 3: return [3 /*break*/, 5];
                    case 4:
                        hasClipPath = false;
                        _a.label = 5;
                    case 5:
                        if (!context.withinClipPath) {
                            context.pdf.saveGraphicsState();
                        }
                        applyAttributes(context, parentContext, this.element);
                        return [4 /*yield*/, this.renderCore(context)];
                    case 6:
                        _a.sent();
                        if (!context.withinClipPath) {
                            context.pdf.restoreGraphicsState();
                        }
                        if (hasClipPath) {
                            context.pdf.restoreGraphicsState();
                        }
                        return [2 /*return*/];
                }
            });
        });
    };
    return RenderedNode;
}(SvgNode));

var GraphicsNode = /** @class */ (function (_super) {
    __extends(GraphicsNode, _super);
    function GraphicsNode() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    return GraphicsNode;
}(RenderedNode));

var GeometryNode = /** @class */ (function (_super) {
    __extends(GeometryNode, _super);
    function GeometryNode(hasMarkers, element, children) {
        var _this = _super.call(this, element, children) || this;
        _this.cachedPath = null;
        _this.hasMarkers = hasMarkers;
        return _this;
    }
    GeometryNode.prototype.renderCore = function (context) {
        return __awaiter(this, void 0, void 0, function () {
            var path;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        path = this.getCachedPath(context);
                        if (path === null || path.segments.length === 0) {
                            return [2 /*return*/];
                        }
                        if (context.withinClipPath) {
                            path.transform(context.transform);
                        }
                        else {
                            context.pdf.setCurrentTransformationMatrix(context.transform);
                        }
                        path.draw(context);
                        return [4 /*yield*/, this.fillOrStroke(context)];
                    case 1:
                        _a.sent();
                        if (!this.hasMarkers) return [3 /*break*/, 3];
                        return [4 /*yield*/, this.drawMarkers(context, path)];
                    case 2:
                        _a.sent();
                        _a.label = 3;
                    case 3: return [2 /*return*/];
                }
            });
        });
    };
    GeometryNode.prototype.getCachedPath = function (context) {
        return this.cachedPath || (this.cachedPath = this.getPath(context));
    };
    GeometryNode.prototype.drawMarkers = function (context, path) {
        return __awaiter(this, void 0, void 0, function () {
            var markers;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        markers = this.getMarkers(path, context);
                        return [4 /*yield*/, markers.draw(context.clone({ transform: context.pdf.unitMatrix }))];
                    case 1:
                        _a.sent();
                        return [2 /*return*/];
                }
            });
        });
    };
    GeometryNode.prototype.fillOrStroke = function (context) {
        return __awaiter(this, void 0, void 0, function () {
            var fill, stroke, fillData, _a, isNodeFillRuleEvenOdd;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        if (context.withinClipPath) {
                            return [2 /*return*/];
                        }
                        fill = context.attributeState.fill;
                        stroke = context.attributeState.stroke && context.attributeState.strokeWidth !== 0;
                        if (!fill) return [3 /*break*/, 2];
                        return [4 /*yield*/, fill.getFillData(this, context)];
                    case 1:
                        _a = _b.sent();
                        return [3 /*break*/, 3];
                    case 2:
                        _a = undefined;
                        _b.label = 3;
                    case 3:
                        fillData = _a;
                        isNodeFillRuleEvenOdd = context.attributeState.fillRule === 'evenodd';
                        // This is a workaround for symbols that are used multiple times with different
                        // fill/stroke attributes. All paths within symbols are both filled and stroked
                        // and we set the fill/stroke to transparent if the use element has
                        // fill/stroke="none".
                        if ((fill && stroke) || context.withinUse) {
                            if (isNodeFillRuleEvenOdd) {
                                context.pdf.fillStrokeEvenOdd(fillData);
                            }
                            else {
                                context.pdf.fillStroke(fillData);
                            }
                        }
                        else if (fill) {
                            if (isNodeFillRuleEvenOdd) {
                                context.pdf.fillEvenOdd(fillData);
                            }
                            else {
                                context.pdf.fill(fillData);
                            }
                        }
                        else if (stroke) {
                            context.pdf.stroke();
                        }
                        else {
                            context.pdf.discardPath();
                        }
                        return [2 /*return*/];
                }
            });
        });
    };
    GeometryNode.prototype.getBoundingBoxCore = function (context) {
        var path = this.getCachedPath(context);
        if (!path || !path.segments.length) {
            return [0, 0, 0, 0];
        }
        var minX = Number.POSITIVE_INFINITY;
        var minY = Number.POSITIVE_INFINITY;
        var maxX = Number.NEGATIVE_INFINITY;
        var maxY = Number.NEGATIVE_INFINITY;
        var x = 0, y = 0;
        for (var i = 0; i < path.segments.length; i++) {
            var seg = path.segments[i];
            if (seg instanceof MoveTo || seg instanceof LineTo || seg instanceof CurveTo) {
                x = seg.x;
                y = seg.y;
            }
            if (seg instanceof CurveTo) {
                minX = Math.min(minX, x, seg.x1, seg.x2, seg.x);
                maxX = Math.max(maxX, x, seg.x1, seg.x2, seg.x);
                minY = Math.min(minY, y, seg.y1, seg.y2, seg.y);
                maxY = Math.max(maxY, y, seg.y1, seg.y2, seg.y);
            }
            else {
                minX = Math.min(minX, x);
                maxX = Math.max(maxX, x);
                minY = Math.min(minY, y);
                maxY = Math.max(maxY, y);
            }
        }
        return [minX, minY, maxX - minX, maxY - minY];
    };
    GeometryNode.prototype.getMarkers = function (path, context) {
        var markerStart = getAttribute(this.element, context.styleSheets, 'marker-start');
        var markerMid = getAttribute(this.element, context.styleSheets, 'marker-mid');
        var markerEnd = getAttribute(this.element, context.styleSheets, 'marker-end');
        var markers = new MarkerList();
        if (markerStart || markerMid || markerEnd) {
            markerEnd && (markerEnd = iri(markerEnd));
            markerStart && (markerStart = iri(markerStart));
            markerMid && (markerMid = iri(markerMid));
            var list_1 = path.segments;
            var prevAngle = [1, 0], curAngle = void 0, first = false, firstAngle = [1, 0], last_1 = false;
            var _loop_1 = function (i) {
                var curr = list_1[i];
                var hasStartMarker = markerStart &&
                    (i === 1 || (!(list_1[i] instanceof MoveTo) && list_1[i - 1] instanceof MoveTo));
                if (hasStartMarker) {
                    list_1.forEach(function (value, index) {
                        if (!last_1 && value instanceof Close && index > i) {
                            var tmp = list_1[index - 1];
                            last_1 =
                                (tmp instanceof MoveTo || tmp instanceof LineTo || tmp instanceof CurveTo) && tmp;
                        }
                    });
                }
                var hasEndMarker = markerEnd &&
                    (i === list_1.length - 1 || (!(list_1[i] instanceof MoveTo) && list_1[i + 1] instanceof MoveTo));
                var hasMidMarker = markerMid && i > 0 && !(i === 1 && list_1[i - 1] instanceof MoveTo);
                var prev = list_1[i - 1] || null;
                if (prev instanceof MoveTo || prev instanceof LineTo || prev instanceof CurveTo) {
                    if (curr instanceof CurveTo) {
                        hasStartMarker &&
                            markers.addMarker(new Marker(markerStart, [prev.x, prev.y], 
                            // @ts-ignore
                            getAngle(last_1 ? [last_1.x, last_1.y] : [prev.x, prev.y], [curr.x1, curr.y1]), true));
                        hasEndMarker &&
                            markers.addMarker(new Marker(markerEnd, [curr.x, curr.y], getAngle([curr.x2, curr.y2], [curr.x, curr.y])));
                        if (hasMidMarker) {
                            curAngle = getDirectionVector([prev.x, prev.y], [curr.x1, curr.y1]);
                            curAngle =
                                prev instanceof MoveTo ? curAngle : normalize(addVectors(prevAngle, curAngle));
                            markers.addMarker(new Marker(markerMid, [prev.x, prev.y], Math.atan2(curAngle[1], curAngle[0])));
                        }
                        prevAngle = getDirectionVector([curr.x2, curr.y2], [curr.x, curr.y]);
                    }
                    else if (curr instanceof MoveTo || curr instanceof LineTo) {
                        curAngle = getDirectionVector([prev.x, prev.y], [curr.x, curr.y]);
                        if (hasStartMarker) {
                            // @ts-ignore
                            var angle = last_1 ? getDirectionVector([last_1.x, last_1.y], [curr.x, curr.y]) : curAngle;
                            markers.addMarker(new Marker(markerStart, [prev.x, prev.y], Math.atan2(angle[1], angle[0]), true));
                        }
                        hasEndMarker &&
                            markers.addMarker(new Marker(markerEnd, [curr.x, curr.y], Math.atan2(curAngle[1], curAngle[0])));
                        if (hasMidMarker) {
                            var angle = curr instanceof MoveTo
                                ? prevAngle
                                : prev instanceof MoveTo
                                    ? curAngle
                                    : normalize(addVectors(prevAngle, curAngle));
                            markers.addMarker(new Marker(markerMid, [prev.x, prev.y], Math.atan2(angle[1], angle[0])));
                        }
                        prevAngle = curAngle;
                    }
                    else if (curr instanceof Close) {
                        // @ts-ignore
                        curAngle = getDirectionVector([prev.x, prev.y], [first.x, first.y]);
                        if (hasMidMarker) {
                            var angle = prev instanceof MoveTo ? curAngle : normalize(addVectors(prevAngle, curAngle));
                            markers.addMarker(new Marker(markerMid, [prev.x, prev.y], Math.atan2(angle[1], angle[0])));
                        }
                        if (hasEndMarker) {
                            var angle = normalize(addVectors(curAngle, firstAngle));
                            markers.addMarker(
                            // @ts-ignore
                            new Marker(markerEnd, [first.x, first.y], Math.atan2(angle[1], angle[0])));
                        }
                        prevAngle = curAngle;
                    }
                }
                else {
                    first = curr instanceof MoveTo && curr;
                    var next = list_1[i + 1];
                    if (next instanceof MoveTo || next instanceof LineTo || next instanceof CurveTo) {
                        // @ts-ignore
                        firstAngle = getDirectionVector([first.x, first.y], [next.x, next.y]);
                    }
                }
            };
            for (var i = 0; i < list_1.length; i++) {
                _loop_1(i);
            }
        }
        markers.markers.forEach(function (marker) {
            var markerNode = context.refsHandler.get(marker.id);
            if (!markerNode)
                return;
            var orient = getAttribute(markerNode.element, context.styleSheets, 'orient');
            if (orient == null)
                return;
            if (marker.isStartMarker && orient === 'auto-start-reverse') {
                marker.angle += Math.PI;
            }
            if (!isNaN(Number(orient))) {
                marker.angle = (parseFloat(orient) / 180) * Math.PI;
            }
        });
        return markers;
    };
    return GeometryNode;
}(GraphicsNode));
function iri(attribute) {
    var match = iriReference.exec(attribute);
    return (match && match[1]) || undefined;
}

var Line = /** @class */ (function (_super) {
    __extends(Line, _super);
    function Line(node, children) {
        return _super.call(this, true, node, children) || this;
    }
    Line.prototype.getPath = function (context) {
        if (context.withinClipPath || context.attributeState.stroke === null) {
            return null;
        }
        var x1 = parseFloat(this.element.getAttribute('x1') || '0'), y1 = parseFloat(this.element.getAttribute('y1') || '0');
        var x2 = parseFloat(this.element.getAttribute('x2') || '0'), y2 = parseFloat(this.element.getAttribute('y2') || '0');
        if (!(x1 || x2 || y1 || y2)) {
            return null;
        }
        return new Path().moveTo(x1, y1).lineTo(x2, y2);
    };
    Line.prototype.computeNodeTransformCore = function (context) {
        return context.pdf.unitMatrix;
    };
    Line.prototype.isVisible = function (parentVisible, context) {
        return svgNodeIsVisible(this, parentVisible, context);
    };
    Line.prototype.fillOrStroke = function (context) {
        return __awaiter(this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        context.attributeState.fill = null;
                        return [4 /*yield*/, _super.prototype.fillOrStroke.call(this, context)];
                    case 1:
                        _a.sent();
                        return [2 /*return*/];
                }
            });
        });
    };
    return Line;
}(GeometryNode));

var Symbol$1 = /** @class */ (function (_super) {
    __extends(Symbol, _super);
    function Symbol() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    Symbol.prototype.apply = function (parentContext) {
        return __awaiter(this, void 0, void 0, function () {
            var context, clipPathAttribute, hasClipPath, clipNode, _i, _a, child;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        if (!this.isVisible(parentContext.attributeState.visibility !== 'hidden', parentContext)) {
                            return [2 /*return*/];
                        }
                        context = parentContext.clone();
                        context.transform = context.pdf.unitMatrix;
                        parseAttributes(context, this);
                        clipPathAttribute = getAttribute(this.element, context.styleSheets, 'clip-path');
                        hasClipPath = clipPathAttribute && clipPathAttribute !== 'none';
                        if (!hasClipPath) return [3 /*break*/, 3];
                        clipNode = getClipPathNode(clipPathAttribute, this, context);
                        if (!clipNode) return [3 /*break*/, 3];
                        if (!clipNode.isVisible(true, context)) return [3 /*break*/, 2];
                        return [4 /*yield*/, applyClipPath(this, clipNode, context)];
                    case 1:
                        _b.sent();
                        return [3 /*break*/, 3];
                    case 2: return [2 /*return*/];
                    case 3:
                        applyAttributes(context, parentContext, this.element);
                        _i = 0, _a = this.children;
                        _b.label = 4;
                    case 4:
                        if (!(_i < _a.length)) return [3 /*break*/, 7];
                        child = _a[_i];
                        return [4 /*yield*/, child.render(context)];
                    case 5:
                        _b.sent();
                        _b.label = 6;
                    case 6:
                        _i++;
                        return [3 /*break*/, 4];
                    case 7: return [2 /*return*/];
                }
            });
        });
    };
    Symbol.prototype.getBoundingBoxCore = function (context) {
        return getBoundingBoxByChildren(context, this);
    };
    Symbol.prototype.isVisible = function (parentVisible, context) {
        return svgNodeAndChildrenVisible(this, parentVisible, context);
    };
    Symbol.prototype.computeNodeTransformCore = function (context) {
        var x = parseFloat(getAttribute(this.element, context.styleSheets, 'x') || '0');
        var y = parseFloat(getAttribute(this.element, context.styleSheets, 'y') || '0');
        // TODO: implement refX/refY - this is still to do because common browsers don't seem to support the feature yet
        // x += parseFloat(this.element.getAttribute("refX")) || 0; ???
        // y += parseFloat(this.element.getAttribute("refY")) || 0; ???
        var viewBox = this.element.getAttribute('viewBox');
        if (viewBox) {
            var box = parseFloats(viewBox);
            var width = parseFloat(getAttribute(this.element, context.styleSheets, 'width') ||
                getAttribute(this.element.ownerSVGElement, context.styleSheets, 'width') ||
                viewBox[2]);
            var height = parseFloat(getAttribute(this.element, context.styleSheets, 'height') ||
                getAttribute(this.element.ownerSVGElement, context.styleSheets, 'height') ||
                viewBox[3]);
            return computeViewBoxTransform(this.element, box, x, y, width, height, context);
        }
        else {
            return context.pdf.Matrix(1, 0, 0, 1, x, y);
        }
    };
    return Symbol;
}(NonRenderedNode));

var Viewport = /** @class */ (function () {
    function Viewport(width, height) {
        this.width = width;
        this.height = height;
    }
    return Viewport;
}());

/**
 * Draws the element referenced by a use node, makes use of pdf's XObjects/FormObjects so nodes are only written once
 * to the pdf document. This highly reduces the file size and computation time.
 */
var Use = /** @class */ (function (_super) {
    __extends(Use, _super);
    function Use() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    Use.prototype.renderCore = function (context) {
        return __awaiter(this, void 0, void 0, function () {
            var pf, url, id, refNode, refNodeOpensViewport, x, y, width, height, t, viewBox, contextColors, refContext;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        pf = parseFloat;
                        url = this.element.getAttribute('href') || this.element.getAttribute('xlink:href');
                        // just in case someone has the idea to use empty use-tags, wtf???
                        if (!url)
                            return [2 /*return*/];
                        id = url.substring(1);
                        refNode = context.refsHandler.get(id);
                        refNodeOpensViewport = nodeIs(refNode.element, 'symbol,svg') && refNode.element.hasAttribute('viewBox');
                        x = pf(getAttribute(this.element, context.styleSheets, 'x') || '0');
                        y = pf(getAttribute(this.element, context.styleSheets, 'y') || '0');
                        width = undefined;
                        height = undefined;
                        if (refNodeOpensViewport) {
                            //  <use> inherits width/height only to svg/symbol
                            // if there is no viewBox attribute, width/height don't have an effect
                            // in theory, the default value for width/height is 100%, but we currently don't support this
                            width = pf(getAttribute(this.element, context.styleSheets, 'width') ||
                                getAttribute(refNode.element, context.styleSheets, 'width') ||
                                '0');
                            height = pf(getAttribute(this.element, context.styleSheets, 'height') ||
                                getAttribute(refNode.element, context.styleSheets, 'height') ||
                                '0');
                            //  accumulate x/y to calculate the viewBox transform
                            x += pf(getAttribute(refNode.element, context.styleSheets, 'x') || '0');
                            y += pf(getAttribute(refNode.element, context.styleSheets, 'y') || '0');
                            viewBox = parseFloats(refNode.element.getAttribute('viewBox'));
                            t = computeViewBoxTransform(refNode.element, viewBox, x, y, width, height, context);
                        }
                        else {
                            t = context.pdf.Matrix(1, 0, 0, 1, x, y);
                        }
                        contextColors = AttributeState.getContextColors(context, true);
                        refContext = new Context(context.pdf, {
                            refsHandler: context.refsHandler,
                            styleSheets: context.styleSheets,
                            withinUse: true,
                            viewport: refNodeOpensViewport ? new Viewport(width, height) : context.viewport,
                            svg2pdfParameters: context.svg2pdfParameters,
                            textMeasure: context.textMeasure,
                            attributeState: Object.assign(AttributeState.default(), contextColors)
                        });
                        return [4 /*yield*/, context.refsHandler.getRendered(id, contextColors, function (node) {
                                return Use.renderReferencedNode(node, id, refContext);
                            })];
                    case 1:
                        _a.sent();
                        context.pdf.saveGraphicsState();
                        context.pdf.setCurrentTransformationMatrix(context.transform);
                        //  apply the bbox (i.e. clip) if needed
                        if (refNodeOpensViewport &&
                            getAttribute(refNode.element, context.styleSheets, 'overflow') !== 'visible') {
                            context.pdf.rect(x, y, width, height);
                            context.pdf.clip().discardPath();
                        }
                        context.pdf.doFormObject(context.refsHandler.generateKey(id, contextColors), t);
                        context.pdf.restoreGraphicsState();
                        return [2 /*return*/];
                }
            });
        });
    };
    Use.renderReferencedNode = function (node, id, refContext) {
        return __awaiter(this, void 0, void 0, function () {
            var bBox;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        bBox = node.getBoundingBox(refContext);
                        // The content of a PDF form object is implicitly clipped at its /BBox property.
                        // SVG, however, applies its clip rect at the <use> attribute, which may modify it.
                        // So, make the bBox a lot larger than it needs to be and hope any thick strokes are
                        // still within.
                        bBox = [bBox[0] - 0.5 * bBox[2], bBox[1] - 0.5 * bBox[3], bBox[2] * 2, bBox[3] * 2];
                        refContext.pdf.beginFormObject(bBox[0], bBox[1], bBox[2], bBox[3], refContext.pdf.unitMatrix);
                        if (!(node instanceof Symbol$1)) return [3 /*break*/, 2];
                        return [4 /*yield*/, node.apply(refContext)];
                    case 1:
                        _a.sent();
                        return [3 /*break*/, 4];
                    case 2: return [4 /*yield*/, node.render(refContext)];
                    case 3:
                        _a.sent();
                        _a.label = 4;
                    case 4:
                        refContext.pdf.endFormObject(refContext.refsHandler.generateKey(id, refContext.attributeState));
                        return [2 /*return*/];
                }
            });
        });
    };
    Use.prototype.getBoundingBoxCore = function (context) {
        return defaultBoundingBox(this.element, context);
    };
    Use.prototype.isVisible = function (parentVisible, context) {
        return svgNodeIsVisible(this, parentVisible, context);
    };
    Use.prototype.computeNodeTransformCore = function (context) {
        return context.pdf.unitMatrix;
    };
    return Use;
}(GraphicsNode));

var Rect = /** @class */ (function (_super) {
    __extends(Rect, _super);
    function Rect(element, children) {
        return _super.call(this, false, element, children) || this;
    }
    Rect.prototype.getPath = function (context) {
        var w = parseFloat(getAttribute(this.element, context.styleSheets, 'width') || '0');
        var h = parseFloat(getAttribute(this.element, context.styleSheets, 'height') || '0');
        if (!isFinite(w) || w <= 0 || !isFinite(h) || h <= 0) {
            return null;
        }
        var rxAttr = getAttribute(this.element, context.styleSheets, 'rx');
        var ryAttr = getAttribute(this.element, context.styleSheets, 'ry');
        var rx = Math.min(parseFloat(rxAttr || ryAttr || '0'), w * 0.5);
        var ry = Math.min(parseFloat(ryAttr || rxAttr || '0'), h * 0.5);
        var x = parseFloat(getAttribute(this.element, context.styleSheets, 'x') || '0');
        var y = parseFloat(getAttribute(this.element, context.styleSheets, 'y') || '0');
        var arc = (4 / 3) * (Math.SQRT2 - 1);
        if (rx === 0 && ry === 0) {
            return new Path()
                .moveTo(x, y)
                .lineTo(x + w, y)
                .lineTo(x + w, y + h)
                .lineTo(x, y + h)
                .close();
        }
        else {
            return new Path()
                .moveTo((x += rx), y)
                .lineTo((x += w - 2 * rx), y)
                .curveTo(x + rx * arc, y, x + rx, y + (ry - ry * arc), (x += rx), (y += ry))
                .lineTo(x, (y += h - 2 * ry))
                .curveTo(x, y + ry * arc, x - rx * arc, y + ry, (x -= rx), (y += ry))
                .lineTo((x += -w + 2 * rx), y)
                .curveTo(x - rx * arc, y, x - rx, y - ry * arc, (x -= rx), (y -= ry))
                .lineTo(x, (y += -h + 2 * ry))
                .curveTo(x, y - ry * arc, x + rx * arc, y - ry, (x += rx), (y -= ry))
                .close();
        }
    };
    Rect.prototype.computeNodeTransformCore = function (context) {
        return context.pdf.unitMatrix;
    };
    Rect.prototype.isVisible = function (parentVisible, context) {
        return svgNodeIsVisible(this, parentVisible, context);
    };
    return Rect;
}(GeometryNode));

var EllipseBase = /** @class */ (function (_super) {
    __extends(EllipseBase, _super);
    function EllipseBase(element, children) {
        return _super.call(this, false, element, children) || this;
    }
    EllipseBase.prototype.getPath = function (context) {
        var rx = this.getRx(context);
        var ry = this.getRy(context);
        if (!isFinite(rx) || ry <= 0 || !isFinite(ry) || ry <= 0) {
            return null;
        }
        var x = parseFloat(getAttribute(this.element, context.styleSheets, 'cx') || '0'), y = parseFloat(getAttribute(this.element, context.styleSheets, 'cy') || '0');
        var lx = (4 / 3) * (Math.SQRT2 - 1) * rx, ly = (4 / 3) * (Math.SQRT2 - 1) * ry;
        return new Path()
            .moveTo(x + rx, y)
            .curveTo(x + rx, y - ly, x + lx, y - ry, x, y - ry)
            .curveTo(x - lx, y - ry, x - rx, y - ly, x - rx, y)
            .curveTo(x - rx, y + ly, x - lx, y + ry, x, y + ry)
            .curveTo(x + lx, y + ry, x + rx, y + ly, x + rx, y);
    };
    EllipseBase.prototype.computeNodeTransformCore = function (context) {
        return context.pdf.unitMatrix;
    };
    EllipseBase.prototype.isVisible = function (parentVisible, context) {
        return svgNodeIsVisible(this, parentVisible, context);
    };
    return EllipseBase;
}(GeometryNode));

var Ellipse = /** @class */ (function (_super) {
    __extends(Ellipse, _super);
    function Ellipse(element, children) {
        return _super.call(this, element, children) || this;
    }
    Ellipse.prototype.getRx = function (context) {
        return parseFloat(getAttribute(this.element, context.styleSheets, 'rx') || '0');
    };
    Ellipse.prototype.getRy = function (context) {
        return parseFloat(getAttribute(this.element, context.styleSheets, 'ry') || '0');
    };
    return Ellipse;
}(EllipseBase));

function getTextRenderingMode(attributeState) {
    var renderingMode = 'invisible';
    var doStroke = attributeState.stroke && attributeState.strokeWidth !== 0;
    var doFill = attributeState.fill;
    if (doFill && doStroke) {
        renderingMode = 'fillThenStroke';
    }
    else if (doFill) {
        renderingMode = 'fill';
    }
    else if (doStroke) {
        renderingMode = 'stroke';
    }
    return renderingMode;
}
function transformXmlSpace(trimmedText, attributeState) {
    trimmedText = removeNewlines(trimmedText);
    trimmedText = replaceTabsBySpace(trimmedText);
    var shouldPreserve = attributeState.xmlSpace === 'preserve' || attributeState.whiteSpace === 'pre';
    if (!shouldPreserve) {
        trimmedText = trimmedText.trim();
        trimmedText = consolidateSpaces(trimmedText);
    }
    return trimmedText;
}
function removeNewlines(str) {
    return str.replace(/[\n\r]/g, '');
}
function replaceTabsBySpace(str) {
    return str.replace(/[\t]/g, ' ');
}
function consolidateSpaces(str) {
    return str.replace(/ +/g, ' ');
}
// applies text transformations to a text node
function transformText(node, text, context) {
    var textTransform = getAttribute(node, context.styleSheets, 'text-transform');
    switch (textTransform) {
        case 'uppercase':
            return text.toUpperCase();
        case 'lowercase':
            return text.toLowerCase();
        default:
            return text;
        // TODO: capitalize, full-width
    }
}
function trimLeft(str) {
    return str.replace(/^\s+/, '');
}
function trimRight(str) {
    return str.replace(/\s+$/, '');
}

/**
 * @param {string} textAnchor
 * @param {number} originX
 * @param {number} originY
 * @constructor
 */
var TextChunk = /** @class */ (function () {
    function TextChunk(parent, textAnchor, originX, originY) {
        this.textNode = parent;
        this.texts = [];
        this.textNodes = [];
        this.contexts = [];
        this.textAnchor = textAnchor;
        this.originX = originX;
        this.originY = originY;
        this.textMeasures = [];
    }
    TextChunk.prototype.setX = function (originX) {
        this.originX = originX;
    };
    TextChunk.prototype.setY = function (originY) {
        this.originY = originY;
    };
    TextChunk.prototype.add = function (tSpan, text, context) {
        this.texts.push(text);
        this.textNodes.push(tSpan);
        this.contexts.push(context);
    };
    TextChunk.prototype.rightTrimText = function () {
        for (var r = this.texts.length - 1; r >= 0; r--) {
            var shouldPreserve = this.contexts[r].attributeState.xmlSpace === 'preserve' ||
                this.contexts[r].attributeState.whiteSpace === 'pre';
            if (!shouldPreserve) {
                this.texts[r] = trimRight(this.texts[r]);
            }
            // If find a letter, stop right-trimming
            if (this.texts[r].match(/[^\s]/)) {
                return false;
            }
        }
        return true;
    };
    TextChunk.prototype.measureText = function (context) {
        for (var i = 0; i < this.texts.length; i++) {
            this.textMeasures.push({
                width: context.textMeasure.measureTextWidth(this.texts[i], this.contexts[i].attributeState),
                length: this.texts[i].length
            });
        }
    };
    TextChunk.prototype.put = function (context, charSpace) {
        var i, textNode, textNodeContext, textMeasure;
        var alreadySeen = [];
        var xs = [], ys = [];
        var currentTextX = this.originX, currentTextY = this.originY;
        var minX = currentTextX, maxX = currentTextX;
        for (i = 0; i < this.textNodes.length; i++) {
            textNode = this.textNodes[i];
            textNodeContext = this.contexts[i];
            textMeasure = this.textMeasures[i] || {
                width: context.textMeasure.measureTextWidth(this.texts[i], this.contexts[i].attributeState),
                length: this.texts[i].length
            };
            var x = currentTextX;
            var y = currentTextY;
            if (textNode.nodeName !== '#text') {
                if (!alreadySeen.includes(textNode)) {
                    alreadySeen.push(textNode);
                    var tSpanDx = TextChunk.resolveRelativePositionAttribute(textNode, 'dx');
                    if (tSpanDx !== null) {
                        x += toPixels(tSpanDx, textNodeContext.attributeState.fontSize);
                    }
                    var tSpanDy = TextChunk.resolveRelativePositionAttribute(textNode, 'dy');
                    if (tSpanDy !== null) {
                        y += toPixels(tSpanDy, textNodeContext.attributeState.fontSize);
                    }
                }
            }
            xs[i] = x;
            ys[i] = y;
            currentTextX = x + textMeasure.width + textMeasure.length * charSpace;
            currentTextY = y;
            minX = Math.min(minX, x);
            maxX = Math.max(maxX, currentTextX);
        }
        var textOffset = 0;
        switch (this.textAnchor) {
            case 'start':
                textOffset = 0;
                break;
            case 'middle':
                textOffset = (maxX - minX) / 2;
                break;
            case 'end':
                textOffset = maxX - minX;
                break;
        }
        for (i = 0; i < this.textNodes.length; i++) {
            textNode = this.textNodes[i];
            textNodeContext = this.contexts[i];
            if (textNode.nodeName !== '#text') {
                if (textNodeContext.attributeState.visibility === 'hidden') {
                    continue;
                }
            }
            context.pdf.saveGraphicsState();
            applyAttributes(textNodeContext, context, textNode);
            var alignmentBaseline = textNodeContext.attributeState.alignmentBaseline;
            var textRenderingMode = getTextRenderingMode(textNodeContext.attributeState);
            context.pdf.text(this.texts[i], xs[i] - textOffset, ys[i], {
                baseline: mapAlignmentBaseline(alignmentBaseline),
                angle: context.transform,
                renderingMode: textRenderingMode === 'fill' ? void 0 : textRenderingMode,
                charSpace: charSpace === 0 ? void 0 : charSpace
            });
            context.pdf.restoreGraphicsState();
        }
        return [currentTextX, currentTextY];
    };
    /**
     * Resolves a positional attribute (dx, dy) on a given tSpan, possibly
     * inheriting it from the nearest ancestor. Positional attributes
     * are only inherited from a parent to its first child.
     */
    TextChunk.resolveRelativePositionAttribute = function (element, attributeName) {
        var _a;
        var currentElement = element;
        while (currentElement && nodeIs(currentElement, 'tspan')) {
            if (currentElement.hasAttribute(attributeName)) {
                return currentElement.getAttribute(attributeName);
            }
            if (!(((_a = element.parentElement) === null || _a === void 0 ? void 0 : _a.firstChild) === element)) {
                // positional attributes are only inherited from a parent to its first child
                break;
            }
            currentElement = currentElement.parentElement;
        }
        return null;
    };
    return TextChunk;
}());

var TextNode = /** @class */ (function (_super) {
    __extends(TextNode, _super);
    function TextNode() {
        var _this = _super !== null && _super.apply(this, arguments) || this;
        _this.boundingBox = [];
        return _this;
    }
    TextNode.prototype.processTSpans = function (textNode, node, context, textChunks, currentTextSegment, trimInfo) {
        var pdfFontSize = context.pdf.getFontSize();
        var shouldPreserve = context.attributeState.xmlSpace === 'preserve' || context.attributeState.whiteSpace === 'pre';
        var firstText = true, initialSpace = false;
        for (var i = 0; i < node.childNodes.length; i++) {
            var childNode = node.childNodes[i];
            if (!childNode.textContent) {
                continue;
            }
            var textContent = childNode.textContent;
            if (childNode.nodeName === '#text') {
                var trimmedText = removeNewlines(textContent);
                trimmedText = replaceTabsBySpace(trimmedText);
                if (!shouldPreserve) {
                    trimmedText = consolidateSpaces(trimmedText);
                    // If first text in tspan and starts with a space
                    if (firstText && trimmedText.match(/^\s/)) {
                        initialSpace = true;
                    }
                    // No longer the first text if we've found a letter
                    if (trimmedText.match(/[^\s]/)) {
                        firstText = false;
                    }
                    // Consolidate spaces across different children
                    if (trimInfo.prevText.match(/\s$/)) {
                        trimmedText = trimLeft(trimmedText);
                    }
                }
                var transformedText = transformText(node, trimmedText, context);
                currentTextSegment.add(node, transformedText, context);
                trimInfo.prevText = textContent;
                trimInfo.prevContext = context;
            }
            else if (nodeIs(childNode, 'title')) ;
            else if (nodeIs(childNode, 'tspan')) {
                var tSpan = childNode;
                var tSpanAbsX = tSpan.getAttribute('x');
                if (tSpanAbsX !== null) {
                    var x = toPixels(tSpanAbsX, pdfFontSize);
                    currentTextSegment = new TextChunk(this, getAttribute(tSpan, context.styleSheets, 'text-anchor') ||
                        context.attributeState.textAnchor, x, 0);
                    textChunks.push({ type: 'y', chunk: currentTextSegment });
                }
                var tSpanAbsY = tSpan.getAttribute('y');
                if (tSpanAbsY !== null) {
                    var y = toPixels(tSpanAbsY, pdfFontSize);
                    currentTextSegment = new TextChunk(this, getAttribute(tSpan, context.styleSheets, 'text-anchor') ||
                        context.attributeState.textAnchor, 0, y);
                    textChunks.push({ type: 'x', chunk: currentTextSegment });
                }
                var childContext = context.clone();
                parseAttributes(childContext, textNode, tSpan);
                this.processTSpans(textNode, tSpan, childContext, textChunks, currentTextSegment, trimInfo);
            }
        }
        return initialSpace;
    };
    TextNode.prototype.renderCore = function (context) {
        return __awaiter(this, void 0, void 0, function () {
            var xOffset, charSpace, lengthAdjustment, pdfFontSize, textX, textY, dx, dy, textLength, visibility, tSpanCount, textContent, trimmedText, transformedText, defaultSize, shouldPreserve, alignmentBaseline, textRenderingMode, textChunks, currentTextSegment, initialSpace, trimRight, r, totalDefaultWidth_1, totalLength_1;
            return __generator(this, function (_a) {
                context.pdf.saveGraphicsState();
                xOffset = 0;
                charSpace = 0;
                lengthAdjustment = 1;
                pdfFontSize = context.pdf.getFontSize();
                textX = toPixels(this.element.getAttribute('x'), pdfFontSize);
                textY = toPixels(this.element.getAttribute('y'), pdfFontSize);
                dx = toPixels(this.element.getAttribute('dx'), pdfFontSize);
                dy = toPixels(this.element.getAttribute('dy'), pdfFontSize);
                textLength = parseFloat(this.element.getAttribute('textLength') || '0');
                visibility = context.attributeState.visibility;
                tSpanCount = this.element.childElementCount;
                if (tSpanCount === 0) {
                    textContent = this.element.textContent || '';
                    trimmedText = transformXmlSpace(textContent, context.attributeState);
                    transformedText = transformText(this.element, trimmedText, context);
                    xOffset = context.textMeasure.getTextOffset(transformedText, context.attributeState);
                    if (textLength > 0) {
                        defaultSize = context.textMeasure.measureTextWidth(transformedText, context.attributeState);
                        shouldPreserve = context.attributeState.xmlSpace === 'preserve' || context.attributeState.whiteSpace === 'pre';
                        if (!shouldPreserve && textContent.match(/^\s/)) {
                            lengthAdjustment = 0;
                        }
                        charSpace = (textLength - defaultSize) / (transformedText.length - lengthAdjustment) || 0;
                    }
                    if (visibility === 'visible') {
                        alignmentBaseline = context.attributeState.alignmentBaseline;
                        textRenderingMode = getTextRenderingMode(context.attributeState);
                        context.pdf.text(transformedText, textX + dx - xOffset, textY + dy, {
                            baseline: mapAlignmentBaseline(alignmentBaseline),
                            angle: context.transform,
                            renderingMode: textRenderingMode === 'fill' ? void 0 : textRenderingMode,
                            charSpace: charSpace === 0 ? void 0 : charSpace
                        });
                        this.boundingBox = [
                            textX + dx - xOffset,
                            textY + dy + 0.1 * pdfFontSize,
                            context.textMeasure.measureTextWidth(transformedText, context.attributeState),
                            pdfFontSize
                        ];
                    }
                }
                else {
                    textChunks = [];
                    currentTextSegment = new TextChunk(this, context.attributeState.textAnchor, textX + dx, textY + dy);
                    textChunks.push({ type: '', chunk: currentTextSegment });
                    initialSpace = this.processTSpans(this, this.element, context, textChunks, currentTextSegment, 
                    // Set prevText to ' ' so any spaces on left of <text> are trimmed
                    { prevText: ' ', prevContext: context });
                    lengthAdjustment = initialSpace ? 0 : 1;
                    trimRight = true;
                    for (r = textChunks.length - 1; r >= 0; r--) {
                        if (trimRight) {
                            trimRight = textChunks[r].chunk.rightTrimText();
                        }
                    }
                    if (textLength > 0) {
                        totalDefaultWidth_1 = 0;
                        totalLength_1 = 0;
                        textChunks.forEach(function (_a) {
                            var chunk = _a.chunk;
                            chunk.measureText(context);
                            chunk.textMeasures.forEach(function (_a) {
                                var width = _a.width, length = _a.length;
                                totalDefaultWidth_1 += width;
                                totalLength_1 += length;
                            });
                        });
                        charSpace = (textLength - totalDefaultWidth_1) / (totalLength_1 - lengthAdjustment);
                    }
                    // Put the textchunks
                    textChunks.reduce(function (lastPositions, _a) {
                        var type = _a.type, chunk = _a.chunk;
                        if (type === 'x') {
                            chunk.setX(lastPositions[0]);
                        }
                        else if (type === 'y') {
                            chunk.setY(lastPositions[1]);
                        }
                        return chunk.put(context, charSpace);
                    }, [0, 0]);
                }
                context.pdf.restoreGraphicsState();
                return [2 /*return*/];
            });
        });
    };
    TextNode.prototype.isVisible = function (parentVisible, context) {
        return svgNodeAndChildrenVisible(this, parentVisible, context);
    };
    TextNode.prototype.getBoundingBoxCore = function (context) {
        return this.boundingBox.length > 0
            ? this.boundingBox
            : defaultBoundingBox(this.element, context);
    };
    TextNode.prototype.computeNodeTransformCore = function (context) {
        return context.pdf.unitMatrix;
    };
    return TextNode;
}(GraphicsNode));

var path_parse;
var hasRequiredPath_parse;

function requirePath_parse () {
	if (hasRequiredPath_parse) return path_parse;
	hasRequiredPath_parse = 1;


	var paramCounts = { a: 7, c: 6, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, z: 0 };

	var SPECIAL_SPACES = [
	  0x1680, 0x180E, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006,
	  0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000, 0xFEFF
	];

	function isSpace(ch) {
	  return (ch === 0x0A) || (ch === 0x0D) || (ch === 0x2028) || (ch === 0x2029) || // Line terminators
	    // White spaces
	    (ch === 0x20) || (ch === 0x09) || (ch === 0x0B) || (ch === 0x0C) || (ch === 0xA0) ||
	    (ch >= 0x1680 && SPECIAL_SPACES.indexOf(ch) >= 0);
	}

	function isCommand(code) {
	  /*eslint-disable no-bitwise*/
	  switch (code | 0x20) {
	    case 0x6D/* m */:
	    case 0x7A/* z */:
	    case 0x6C/* l */:
	    case 0x68/* h */:
	    case 0x76/* v */:
	    case 0x63/* c */:
	    case 0x73/* s */:
	    case 0x71/* q */:
	    case 0x74/* t */:
	    case 0x61/* a */:
	    case 0x72/* r */:
	      return true;
	  }
	  return false;
	}

	function isArc(code) {
	  return (code | 0x20) === 0x61;
	}

	function isDigit(code) {
	  return (code >= 48 && code <= 57);   // 0..9
	}

	function isDigitStart(code) {
	  return (code >= 48 && code <= 57) || /* 0..9 */
	          code === 0x2B || /* + */
	          code === 0x2D || /* - */
	          code === 0x2E;   /* . */
	}


	function State(path) {
	  this.index  = 0;
	  this.path   = path;
	  this.max    = path.length;
	  this.result = [];
	  this.param  = 0.0;
	  this.err    = '';
	  this.segmentStart = 0;
	  this.data   = [];
	}

	function skipSpaces(state) {
	  while (state.index < state.max && isSpace(state.path.charCodeAt(state.index))) {
	    state.index++;
	  }
	}


	function scanFlag(state) {
	  var ch = state.path.charCodeAt(state.index);

	  if (ch === 0x30/* 0 */) {
	    state.param = 0;
	    state.index++;
	    return;
	  }

	  if (ch === 0x31/* 1 */) {
	    state.param = 1;
	    state.index++;
	    return;
	  }

	  state.err = 'SvgPath: arc flag can be 0 or 1 only (at pos ' + state.index + ')';
	}


	function scanParam(state) {
	  var start = state.index,
	      index = start,
	      max = state.max,
	      zeroFirst = false,
	      hasCeiling = false,
	      hasDecimal = false,
	      hasDot = false,
	      ch;

	  if (index >= max) {
	    state.err = 'SvgPath: missed param (at pos ' + index + ')';
	    return;
	  }
	  ch = state.path.charCodeAt(index);

	  if (ch === 0x2B/* + */ || ch === 0x2D/* - */) {
	    index++;
	    ch = (index < max) ? state.path.charCodeAt(index) : 0;
	  }

	  // This logic is shamelessly borrowed from Esprima
	  // https://github.com/ariya/esprimas
	  //
	  if (!isDigit(ch) && ch !== 0x2E/* . */) {
	    state.err = 'SvgPath: param should start with 0..9 or `.` (at pos ' + index + ')';
	    return;
	  }

	  if (ch !== 0x2E/* . */) {
	    zeroFirst = (ch === 0x30/* 0 */);
	    index++;

	    ch = (index < max) ? state.path.charCodeAt(index) : 0;

	    if (zeroFirst && index < max) {
	      // decimal number starts with '0' such as '09' is illegal.
	      if (ch && isDigit(ch)) {
	        state.err = 'SvgPath: numbers started with `0` such as `09` are illegal (at pos ' + start + ')';
	        return;
	      }
	    }

	    while (index < max && isDigit(state.path.charCodeAt(index))) {
	      index++;
	      hasCeiling = true;
	    }
	    ch = (index < max) ? state.path.charCodeAt(index) : 0;
	  }

	  if (ch === 0x2E/* . */) {
	    hasDot = true;
	    index++;
	    while (isDigit(state.path.charCodeAt(index))) {
	      index++;
	      hasDecimal = true;
	    }
	    ch = (index < max) ? state.path.charCodeAt(index) : 0;
	  }

	  if (ch === 0x65/* e */ || ch === 0x45/* E */) {
	    if (hasDot && !hasCeiling && !hasDecimal) {
	      state.err = 'SvgPath: invalid float exponent (at pos ' + index + ')';
	      return;
	    }

	    index++;

	    ch = (index < max) ? state.path.charCodeAt(index) : 0;
	    if (ch === 0x2B/* + */ || ch === 0x2D/* - */) {
	      index++;
	    }
	    if (index < max && isDigit(state.path.charCodeAt(index))) {
	      while (index < max && isDigit(state.path.charCodeAt(index))) {
	        index++;
	      }
	    } else {
	      state.err = 'SvgPath: invalid float exponent (at pos ' + index + ')';
	      return;
	    }
	  }

	  state.index = index;
	  state.param = parseFloat(state.path.slice(start, index)) + 0.0;
	}


	function finalizeSegment(state) {
	  var cmd, cmdLC;

	  // Process duplicated commands (without comand name)

	  // This logic is shamelessly borrowed from Raphael
	  // https://github.com/DmitryBaranovskiy/raphael/
	  //
	  cmd   = state.path[state.segmentStart];
	  cmdLC = cmd.toLowerCase();

	  var params = state.data;

	  if (cmdLC === 'm' && params.length > 2) {
	    state.result.push([ cmd, params[0], params[1] ]);
	    params = params.slice(2);
	    cmdLC = 'l';
	    cmd = (cmd === 'm') ? 'l' : 'L';
	  }

	  if (cmdLC === 'r') {
	    state.result.push([ cmd ].concat(params));
	  } else {

	    while (params.length >= paramCounts[cmdLC]) {
	      state.result.push([ cmd ].concat(params.splice(0, paramCounts[cmdLC])));
	      if (!paramCounts[cmdLC]) {
	        break;
	      }
	    }
	  }
	}


	function scanSegment(state) {
	  var max = state.max,
	      cmdCode, is_arc, comma_found, need_params, i;

	  state.segmentStart = state.index;
	  cmdCode = state.path.charCodeAt(state.index);
	  is_arc = isArc(cmdCode);

	  if (!isCommand(cmdCode)) {
	    state.err = 'SvgPath: bad command ' + state.path[state.index] + ' (at pos ' + state.index + ')';
	    return;
	  }

	  need_params = paramCounts[state.path[state.index].toLowerCase()];

	  state.index++;
	  skipSpaces(state);

	  state.data = [];

	  if (!need_params) {
	    // Z
	    finalizeSegment(state);
	    return;
	  }

	  comma_found = false;

	  for (;;) {
	    for (i = need_params; i > 0; i--) {
	      if (is_arc && (i === 3 || i === 4)) scanFlag(state);
	      else scanParam(state);

	      if (state.err.length) {
	        return;
	      }
	      state.data.push(state.param);

	      skipSpaces(state);
	      comma_found = false;

	      if (state.index < max && state.path.charCodeAt(state.index) === 0x2C/* , */) {
	        state.index++;
	        skipSpaces(state);
	        comma_found = true;
	      }
	    }

	    // after ',' param is mandatory
	    if (comma_found) {
	      continue;
	    }

	    if (state.index >= state.max) {
	      break;
	    }

	    // Stop on next segment
	    if (!isDigitStart(state.path.charCodeAt(state.index))) {
	      break;
	    }
	  }

	  finalizeSegment(state);
	}


	/* Returns array of segments:
	 *
	 * [
	 *   [ command, coord1, coord2, ... ]
	 * ]
	 */
	path_parse = function pathParse(svgPath) {
	  var state = new State(svgPath);
	  var max = state.max;

	  skipSpaces(state);

	  while (state.index < max && !state.err.length) {
	    scanSegment(state);
	  }

	  if (state.err.length) {
	    state.result = [];

	  } else if (state.result.length) {

	    if ('mM'.indexOf(state.result[0][0]) < 0) {
	      state.err = 'SvgPath: string should start with `M` or `m`';
	      state.result = [];
	    } else {
	      state.result[0][0] = 'M';
	    }
	  }

	  return {
	    err: state.err,
	    segments: state.result
	  };
	};
	return path_parse;
}

var matrix;
var hasRequiredMatrix;

function requireMatrix () {
	if (hasRequiredMatrix) return matrix;
	hasRequiredMatrix = 1;

	// combine 2 matrixes
	// m1, m2 - [a, b, c, d, e, g]
	//
	function combine(m1, m2) {
	  return [
	    m1[0] * m2[0] + m1[2] * m2[1],
	    m1[1] * m2[0] + m1[3] * m2[1],
	    m1[0] * m2[2] + m1[2] * m2[3],
	    m1[1] * m2[2] + m1[3] * m2[3],
	    m1[0] * m2[4] + m1[2] * m2[5] + m1[4],
	    m1[1] * m2[4] + m1[3] * m2[5] + m1[5]
	  ];
	}


	function Matrix() {
	  if (!(this instanceof Matrix)) { return new Matrix(); }
	  this.queue = [];   // list of matrixes to apply
	  this.cache = null; // combined matrix cache
	}


	Matrix.prototype.matrix = function (m) {
	  if (m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0) {
	    return this;
	  }
	  this.cache = null;
	  this.queue.push(m);
	  return this;
	};


	Matrix.prototype.translate = function (tx, ty) {
	  if (tx !== 0 || ty !== 0) {
	    this.cache = null;
	    this.queue.push([ 1, 0, 0, 1, tx, ty ]);
	  }
	  return this;
	};


	Matrix.prototype.scale = function (sx, sy) {
	  if (sx !== 1 || sy !== 1) {
	    this.cache = null;
	    this.queue.push([ sx, 0, 0, sy, 0, 0 ]);
	  }
	  return this;
	};


	Matrix.prototype.rotate = function (angle, rx, ry) {
	  var rad, cos, sin;

	  if (angle !== 0) {
	    this.translate(rx, ry);

	    rad = angle * Math.PI / 180;
	    cos = Math.cos(rad);
	    sin = Math.sin(rad);

	    this.queue.push([ cos, sin, -sin, cos, 0, 0 ]);
	    this.cache = null;

	    this.translate(-rx, -ry);
	  }
	  return this;
	};


	Matrix.prototype.skewX = function (angle) {
	  if (angle !== 0) {
	    this.cache = null;
	    this.queue.push([ 1, 0, Math.tan(angle * Math.PI / 180), 1, 0, 0 ]);
	  }
	  return this;
	};


	Matrix.prototype.skewY = function (angle) {
	  if (angle !== 0) {
	    this.cache = null;
	    this.queue.push([ 1, Math.tan(angle * Math.PI / 180), 0, 1, 0, 0 ]);
	  }
	  return this;
	};


	// Flatten queue
	//
	Matrix.prototype.toArray = function () {
	  if (this.cache) {
	    return this.cache;
	  }

	  if (!this.queue.length) {
	    this.cache = [ 1, 0, 0, 1, 0, 0 ];
	    return this.cache;
	  }

	  this.cache = this.queue[0];

	  if (this.queue.length === 1) {
	    return this.cache;
	  }

	  for (var i = 1; i < this.queue.length; i++) {
	    this.cache = combine(this.cache, this.queue[i]);
	  }

	  return this.cache;
	};


	// Apply list of matrixes to (x,y) point.
	// If `isRelative` set, `translate` component of matrix will be skipped
	//
	Matrix.prototype.calc = function (x, y, isRelative) {
	  var m;

	  // Don't change point on empty transforms queue
	  if (!this.queue.length) { return [ x, y ]; }

	  // Calculate final matrix, if not exists
	  //
	  // NB. if you deside to apply transforms to point one-by-one,
	  // they should be taken in reverse order

	  if (!this.cache) {
	    this.cache = this.toArray();
	  }

	  m = this.cache;

	  // Apply matrix to point
	  return [
	    x * m[0] + y * m[2] + (isRelative ? 0 : m[4]),
	    x * m[1] + y * m[3] + (isRelative ? 0 : m[5])
	  ];
	};


	matrix = Matrix;
	return matrix;
}

var transform_parse;
var hasRequiredTransform_parse;

function requireTransform_parse () {
	if (hasRequiredTransform_parse) return transform_parse;
	hasRequiredTransform_parse = 1;


	var Matrix = requireMatrix();

	var operations = {
	  matrix: true,
	  scale: true,
	  rotate: true,
	  translate: true,
	  skewX: true,
	  skewY: true
	};

	var CMD_SPLIT_RE    = /\s*(matrix|translate|scale|rotate|skewX|skewY)\s*\(\s*(.+?)\s*\)[\s,]*/;
	var PARAMS_SPLIT_RE = /[\s,]+/;


	transform_parse = function transformParse(transformString) {
	  var matrix = new Matrix();
	  var cmd, params;

	  // Split value into ['', 'translate', '10 50', '', 'scale', '2', '', 'rotate',  '-45', '']
	  transformString.split(CMD_SPLIT_RE).forEach(function (item) {

	    // Skip empty elements
	    if (!item.length) { return; }

	    // remember operation
	    if (typeof operations[item] !== 'undefined') {
	      cmd = item;
	      return;
	    }

	    // extract params & att operation to matrix
	    params = item.split(PARAMS_SPLIT_RE).map(function (i) {
	      return +i || 0;
	    });

	    // If params count is not correct - ignore command
	    switch (cmd) {
	      case 'matrix':
	        if (params.length === 6) {
	          matrix.matrix(params);
	        }
	        return;

	      case 'scale':
	        if (params.length === 1) {
	          matrix.scale(params[0], params[0]);
	        } else if (params.length === 2) {
	          matrix.scale(params[0], params[1]);
	        }
	        return;

	      case 'rotate':
	        if (params.length === 1) {
	          matrix.rotate(params[0], 0, 0);
	        } else if (params.length === 3) {
	          matrix.rotate(params[0], params[1], params[2]);
	        }
	        return;

	      case 'translate':
	        if (params.length === 1) {
	          matrix.translate(params[0], 0);
	        } else if (params.length === 2) {
	          matrix.translate(params[0], params[1]);
	        }
	        return;

	      case 'skewX':
	        if (params.length === 1) {
	          matrix.skewX(params[0]);
	        }
	        return;

	      case 'skewY':
	        if (params.length === 1) {
	          matrix.skewY(params[0]);
	        }
	        return;
	    }
	  });

	  return matrix;
	};
	return transform_parse;
}

var a2c;
var hasRequiredA2c;

function requireA2c () {
	if (hasRequiredA2c) return a2c;
	hasRequiredA2c = 1;


	var TAU = Math.PI * 2;


	/* eslint-disable space-infix-ops */

	// Calculate an angle between two unit vectors
	//
	// Since we measure angle between radii of circular arcs,
	// we can use simplified math (without length normalization)
	//
	function unit_vector_angle(ux, uy, vx, vy) {
	  var sign = (ux * vy - uy * vx < 0) ? -1 : 1;
	  var dot  = ux * vx + uy * vy;

	  // Add this to work with arbitrary vectors:
	  // dot /= Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);

	  // rounding errors, e.g. -1.0000000000000002 can screw up this
	  if (dot >  1.0) { dot =  1.0; }
	  if (dot < -1) { dot = -1; }

	  return sign * Math.acos(dot);
	}


	// Convert from endpoint to center parameterization,
	// see http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
	//
	// Return [cx, cy, theta1, delta_theta]
	//
	function get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi) {
	  // Step 1.
	  //
	  // Moving an ellipse so origin will be the middlepoint between our two
	  // points. After that, rotate it to line up ellipse axes with coordinate
	  // axes.
	  //
	  var x1p =  cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2;
	  var y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2;

	  var rx_sq  =  rx * rx;
	  var ry_sq  =  ry * ry;
	  var x1p_sq = x1p * x1p;
	  var y1p_sq = y1p * y1p;

	  // Step 2.
	  //
	  // Compute coordinates of the centre of this ellipse (cx', cy')
	  // in the new coordinate system.
	  //
	  var radicant = (rx_sq * ry_sq) - (rx_sq * y1p_sq) - (ry_sq * x1p_sq);

	  if (radicant < 0) {
	    // due to rounding errors it might be e.g. -1.3877787807814457e-17
	    radicant = 0;
	  }

	  radicant /=   (rx_sq * y1p_sq) + (ry_sq * x1p_sq);
	  radicant = Math.sqrt(radicant) * (fa === fs ? -1 : 1);

	  var cxp = radicant *  rx/ry * y1p;
	  var cyp = radicant * -ry/rx * x1p;

	  // Step 3.
	  //
	  // Transform back to get centre coordinates (cx, cy) in the original
	  // coordinate system.
	  //
	  var cx = cos_phi*cxp - sin_phi*cyp + (x1+x2)/2;
	  var cy = sin_phi*cxp + cos_phi*cyp + (y1+y2)/2;

	  // Step 4.
	  //
	  // Compute angles (theta1, delta_theta).
	  //
	  var v1x =  (x1p - cxp) / rx;
	  var v1y =  (y1p - cyp) / ry;
	  var v2x = (-x1p - cxp) / rx;
	  var v2y = (-y1p - cyp) / ry;

	  var theta1 = unit_vector_angle(1, 0, v1x, v1y);
	  var delta_theta = unit_vector_angle(v1x, v1y, v2x, v2y);

	  if (fs === 0 && delta_theta > 0) {
	    delta_theta -= TAU;
	  }
	  if (fs === 1 && delta_theta < 0) {
	    delta_theta += TAU;
	  }

	  return [ cx, cy, theta1, delta_theta ];
	}

	//
	// Approximate one unit arc segment with bézier curves,
	// see http://math.stackexchange.com/questions/873224
	//
	function approximate_unit_arc(theta1, delta_theta) {
	  var alpha = 4/3 * Math.tan(delta_theta/4);

	  var x1 = Math.cos(theta1);
	  var y1 = Math.sin(theta1);
	  var x2 = Math.cos(theta1 + delta_theta);
	  var y2 = Math.sin(theta1 + delta_theta);

	  return [ x1, y1, x1 - y1*alpha, y1 + x1*alpha, x2 + y2*alpha, y2 - x2*alpha, x2, y2 ];
	}

	a2c = function a2c(x1, y1, x2, y2, fa, fs, rx, ry, phi) {
	  var sin_phi = Math.sin(phi * TAU / 360);
	  var cos_phi = Math.cos(phi * TAU / 360);

	  // Make sure radii are valid
	  //
	  var x1p =  cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2;
	  var y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2;

	  if (x1p === 0 && y1p === 0) {
	    // we're asked to draw line to itself
	    return [];
	  }

	  if (rx === 0 || ry === 0) {
	    // one of the radii is zero
	    return [];
	  }


	  // Compensate out-of-range radii
	  //
	  rx = Math.abs(rx);
	  ry = Math.abs(ry);

	  var lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
	  if (lambda > 1) {
	    rx *= Math.sqrt(lambda);
	    ry *= Math.sqrt(lambda);
	  }


	  // Get center parameters (cx, cy, theta1, delta_theta)
	  //
	  var cc = get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi);

	  var result = [];
	  var theta1 = cc[2];
	  var delta_theta = cc[3];

	  // Split an arc to multiple segments, so each segment
	  // will be less than τ/4 (= 90°)
	  //
	  var segments = Math.max(Math.ceil(Math.abs(delta_theta) / (TAU / 4)), 1);
	  delta_theta /= segments;

	  for (var i = 0; i < segments; i++) {
	    result.push(approximate_unit_arc(theta1, delta_theta));
	    theta1 += delta_theta;
	  }

	  // We have a bezier approximation of a unit circle,
	  // now need to transform back to the original ellipse
	  //
	  return result.map(function (curve) {
	    for (var i = 0; i < curve.length; i += 2) {
	      var x = curve[i + 0];
	      var y = curve[i + 1];

	      // scale
	      x *= rx;
	      y *= ry;

	      // rotate
	      var xp = cos_phi*x - sin_phi*y;
	      var yp = sin_phi*x + cos_phi*y;

	      // translate
	      curve[i + 0] = xp + cc[0];
	      curve[i + 1] = yp + cc[1];
	    }

	    return curve;
	  });
	};
	return a2c;
}

var ellipse;
var hasRequiredEllipse;

function requireEllipse () {
	if (hasRequiredEllipse) return ellipse;
	hasRequiredEllipse = 1;

	/* eslint-disable space-infix-ops */

	// The precision used to consider an ellipse as a circle
	//
	var epsilon = 0.0000000001;

	// To convert degree in radians
	//
	var torad = Math.PI / 180;

	// Class constructor :
	//  an ellipse centred at 0 with radii rx,ry and x - axis - angle ax.
	//
	function Ellipse(rx, ry, ax) {
	  if (!(this instanceof Ellipse)) { return new Ellipse(rx, ry, ax); }
	  this.rx = rx;
	  this.ry = ry;
	  this.ax = ax;
	}

	// Apply a linear transform m to the ellipse
	// m is an array representing a matrix :
	//    -         -
	//   | m[0] m[2] |
	//   | m[1] m[3] |
	//    -         -
	//
	Ellipse.prototype.transform = function (m) {
	  // We consider the current ellipse as image of the unit circle
	  // by first scale(rx,ry) and then rotate(ax) ...
	  // So we apply ma =  m x rotate(ax) x scale(rx,ry) to the unit circle.
	  var c = Math.cos(this.ax * torad), s = Math.sin(this.ax * torad);
	  var ma = [
	    this.rx * (m[0]*c + m[2]*s),
	    this.rx * (m[1]*c + m[3]*s),
	    this.ry * (-m[0]*s + m[2]*c),
	    this.ry * (-m[1]*s + m[3]*c)
	  ];

	  // ma * transpose(ma) = [ J L ]
	  //                      [ L K ]
	  // L is calculated later (if the image is not a circle)
	  var J = ma[0]*ma[0] + ma[2]*ma[2],
	      K = ma[1]*ma[1] + ma[3]*ma[3];

	  // the discriminant of the characteristic polynomial of ma * transpose(ma)
	  var D = ((ma[0]-ma[3])*(ma[0]-ma[3]) + (ma[2]+ma[1])*(ma[2]+ma[1])) *
	          ((ma[0]+ma[3])*(ma[0]+ma[3]) + (ma[2]-ma[1])*(ma[2]-ma[1]));

	  // the "mean eigenvalue"
	  var JK = (J + K) / 2;

	  // check if the image is (almost) a circle
	  if (D < epsilon * JK) {
	    // if it is
	    this.rx = this.ry = Math.sqrt(JK);
	    this.ax = 0;
	    return this;
	  }

	  // if it is not a circle
	  var L = ma[0]*ma[1] + ma[2]*ma[3];

	  D = Math.sqrt(D);

	  // {l1,l2} = the two eigen values of ma * transpose(ma)
	  var l1 = JK + D/2,
	      l2 = JK - D/2;
	  // the x - axis - rotation angle is the argument of the l1 - eigenvector
	  /*eslint-disable indent*/
	  this.ax = (Math.abs(L) < epsilon && Math.abs(l1 - K) < epsilon) ?
	    90
	  :
	    Math.atan(Math.abs(L) > Math.abs(l1 - K) ?
	      (l1 - J) / L
	    :
	      L / (l1 - K)
	    ) * 180 / Math.PI;
	  /*eslint-enable indent*/

	  // if ax > 0 => rx = sqrt(l1), ry = sqrt(l2), else exchange axes and ax += 90
	  if (this.ax >= 0) {
	    // if ax in [0,90]
	    this.rx = Math.sqrt(l1);
	    this.ry = Math.sqrt(l2);
	  } else {
	    // if ax in ]-90,0[ => exchange axes
	    this.ax += 90;
	    this.rx = Math.sqrt(l2);
	    this.ry = Math.sqrt(l1);
	  }

	  return this;
	};

	// Check if the ellipse is (almost) degenerate, i.e. rx = 0 or ry = 0
	//
	Ellipse.prototype.isDegenerate = function () {
	  return (this.rx < epsilon * this.ry || this.ry < epsilon * this.rx);
	};

	ellipse = Ellipse;
	return ellipse;
}

var svgpath$1;
var hasRequiredSvgpath$1;

function requireSvgpath$1 () {
	if (hasRequiredSvgpath$1) return svgpath$1;
	hasRequiredSvgpath$1 = 1;


	var pathParse      = requirePath_parse();
	var transformParse = requireTransform_parse();
	var matrix         = requireMatrix();
	var a2c            = requireA2c();
	var ellipse        = requireEllipse();


	// Class constructor
	//
	function SvgPath(path) {
	  if (!(this instanceof SvgPath)) { return new SvgPath(path); }

	  var pstate = pathParse(path);

	  // Array of path segments.
	  // Each segment is array [command, param1, param2, ...]
	  this.segments = pstate.segments;

	  // Error message on parse error.
	  this.err      = pstate.err;

	  // Transforms stack for lazy evaluation
	  this.__stack    = [];
	}

	SvgPath.from = function (src) {
	  if (typeof src === 'string') return new SvgPath(src);

	  if (src instanceof SvgPath) {
	    // Create empty object
	    var s = new SvgPath('');

	    // Clone properies
	    s.err = src.err;
	    s.segments = src.segments.map(function (sgm) { return sgm.slice(); });
	    s.__stack = src.__stack.map(function (m) {
	      return matrix().matrix(m.toArray());
	    });

	    return s;
	  }

	  throw new Error('SvgPath.from: invalid param type ' + src);
	};


	SvgPath.prototype.__matrix = function (m) {
	  var self = this, i;

	  // Quick leave for empty matrix
	  if (!m.queue.length) { return; }

	  this.iterate(function (s, index, x, y) {
	    var p, result, name, isRelative;

	    switch (s[0]) {

	      // Process 'assymetric' commands separately
	      case 'v':
	        p      = m.calc(0, s[1], true);
	        result = (p[0] === 0) ? [ 'v', p[1] ] : [ 'l', p[0], p[1] ];
	        break;

	      case 'V':
	        p      = m.calc(x, s[1], false);
	        result = (p[0] === m.calc(x, y, false)[0]) ? [ 'V', p[1] ] : [ 'L', p[0], p[1] ];
	        break;

	      case 'h':
	        p      = m.calc(s[1], 0, true);
	        result = (p[1] === 0) ? [ 'h', p[0] ] : [ 'l', p[0], p[1] ];
	        break;

	      case 'H':
	        p      = m.calc(s[1], y, false);
	        result = (p[1] === m.calc(x, y, false)[1]) ? [ 'H', p[0] ] : [ 'L', p[0], p[1] ];
	        break;

	      case 'a':
	      case 'A':
	        // ARC is: ['A', rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y]

	        // Drop segment if arc is empty (end point === start point)
	        /*if ((s[0] === 'A' && s[6] === x && s[7] === y) ||
	            (s[0] === 'a' && s[6] === 0 && s[7] === 0)) {
	          return [];
	        }*/

	        // Transform rx, ry and the x-axis-rotation
	        var ma = m.toArray();
	        var e = ellipse(s[1], s[2], s[3]).transform(ma);

	        // flip sweep-flag if matrix is not orientation-preserving
	        if (ma[0] * ma[3] - ma[1] * ma[2] < 0) {
	          s[5] = s[5] ? '0' : '1';
	        }

	        // Transform end point as usual (without translation for relative notation)
	        p = m.calc(s[6], s[7], s[0] === 'a');

	        // Empty arcs can be ignored by renderer, but should not be dropped
	        // to avoid collisions with `S A S` and so on. Replace with empty line.
	        if ((s[0] === 'A' && s[6] === x && s[7] === y) ||
	            (s[0] === 'a' && s[6] === 0 && s[7] === 0)) {
	          result = [ s[0] === 'a' ? 'l' : 'L', p[0], p[1] ];
	          break;
	        }

	        // if the resulting ellipse is (almost) a segment ...
	        if (e.isDegenerate()) {
	          // replace the arc by a line
	          result = [ s[0] === 'a' ? 'l' : 'L', p[0], p[1] ];
	        } else {
	          // if it is a real ellipse
	          // s[0], s[4] and s[5] are not modified
	          result = [ s[0], e.rx, e.ry, e.ax, s[4], s[5], p[0], p[1] ];
	        }

	        break;

	      case 'm':
	        // Edge case. The very first `m` should be processed as absolute, if happens.
	        // Make sense for coord shift transforms.
	        isRelative = index > 0;

	        p = m.calc(s[1], s[2], isRelative);
	        result = [ 'm', p[0], p[1] ];
	        break;

	      default:
	        name       = s[0];
	        result     = [ name ];
	        isRelative = (name.toLowerCase() === name);

	        // Apply transformations to the segment
	        for (i = 1; i < s.length; i += 2) {
	          p = m.calc(s[i], s[i + 1], isRelative);
	          result.push(p[0], p[1]);
	        }
	    }

	    self.segments[index] = result;
	  }, true);
	};


	// Apply stacked commands
	//
	SvgPath.prototype.__evaluateStack = function () {
	  var m, i;

	  if (!this.__stack.length) { return; }

	  if (this.__stack.length === 1) {
	    this.__matrix(this.__stack[0]);
	    this.__stack = [];
	    return;
	  }

	  m = matrix();
	  i = this.__stack.length;

	  while (--i >= 0) {
	    m.matrix(this.__stack[i].toArray());
	  }

	  this.__matrix(m);
	  this.__stack = [];
	};


	// Convert processed SVG Path back to string
	//
	SvgPath.prototype.toString = function () {
	  var result = '', prevCmd = '', cmdSkipped = false;

	  this.__evaluateStack();

	  for (var i = 0, len = this.segments.length; i < len; i++) {
	    var segment = this.segments[i];
	    var cmd = segment[0];

	    // Command not repeating => store
	    if (cmd !== prevCmd || cmd === 'm' || cmd === 'M') {
	      // workaround for FontForge SVG importing bug, keep space between "z m".
	      if (cmd === 'm' && prevCmd === 'z') result += ' ';
	      result += cmd;

	      cmdSkipped = false;
	    } else {
	      cmdSkipped = true;
	    }

	    // Store segment params
	    for (var pos = 1; pos < segment.length; pos++) {
	      var val = segment[pos];
	      // Space can be skipped
	      // 1. After command (always)
	      // 2. For negative value (with '-' at start)
	      if (pos === 1) {
	        if (cmdSkipped && val >= 0) result += ' ';
	      } else if (val >= 0) result += ' ';

	      result += val;
	    }

	    prevCmd = cmd;
	  }

	  return result;
	};


	// Translate path to (x [, y])
	//
	SvgPath.prototype.translate = function (x, y) {
	  this.__stack.push(matrix().translate(x, y || 0));
	  return this;
	};


	// Scale path to (sx [, sy])
	// sy = sx if not defined
	//
	SvgPath.prototype.scale = function (sx, sy) {
	  this.__stack.push(matrix().scale(sx, (!sy && (sy !== 0)) ? sx : sy));
	  return this;
	};


	// Rotate path around point (sx [, sy])
	// sy = sx if not defined
	//
	SvgPath.prototype.rotate = function (angle, rx, ry) {
	  this.__stack.push(matrix().rotate(angle, rx || 0, ry || 0));
	  return this;
	};


	// Skew path along the X axis by `degrees` angle
	//
	SvgPath.prototype.skewX = function (degrees) {
	  this.__stack.push(matrix().skewX(degrees));
	  return this;
	};


	// Skew path along the Y axis by `degrees` angle
	//
	SvgPath.prototype.skewY = function (degrees) {
	  this.__stack.push(matrix().skewY(degrees));
	  return this;
	};


	// Apply matrix transform (array of 6 elements)
	//
	SvgPath.prototype.matrix = function (m) {
	  this.__stack.push(matrix().matrix(m));
	  return this;
	};


	// Transform path according to "transform" attr of SVG spec
	//
	SvgPath.prototype.transform = function (transformString) {
	  if (!transformString.trim()) {
	    return this;
	  }
	  this.__stack.push(transformParse(transformString));
	  return this;
	};


	// Round coords with given decimal precition.
	// 0 by default (to integers)
	//
	SvgPath.prototype.round = function (d) {
	  var contourStartDeltaX = 0, contourStartDeltaY = 0, deltaX = 0, deltaY = 0, l;

	  d = d || 0;

	  this.__evaluateStack();

	  this.segments.forEach(function (s) {
	    var isRelative = (s[0].toLowerCase() === s[0]);

	    switch (s[0]) {
	      case 'H':
	      case 'h':
	        if (isRelative) { s[1] += deltaX; }
	        deltaX = s[1] - s[1].toFixed(d);
	        s[1] = +s[1].toFixed(d);
	        return;

	      case 'V':
	      case 'v':
	        if (isRelative) { s[1] += deltaY; }
	        deltaY = s[1] - s[1].toFixed(d);
	        s[1] = +s[1].toFixed(d);
	        return;

	      case 'Z':
	      case 'z':
	        deltaX = contourStartDeltaX;
	        deltaY = contourStartDeltaY;
	        return;

	      case 'M':
	      case 'm':
	        if (isRelative) {
	          s[1] += deltaX;
	          s[2] += deltaY;
	        }

	        deltaX = s[1] - s[1].toFixed(d);
	        deltaY = s[2] - s[2].toFixed(d);

	        contourStartDeltaX = deltaX;
	        contourStartDeltaY = deltaY;

	        s[1] = +s[1].toFixed(d);
	        s[2] = +s[2].toFixed(d);
	        return;

	      case 'A':
	      case 'a':
	        // [cmd, rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y]
	        if (isRelative) {
	          s[6] += deltaX;
	          s[7] += deltaY;
	        }

	        deltaX = s[6] - s[6].toFixed(d);
	        deltaY = s[7] - s[7].toFixed(d);

	        s[1] = +s[1].toFixed(d);
	        s[2] = +s[2].toFixed(d);
	        s[3] = +s[3].toFixed(d + 2); // better precision for rotation
	        s[6] = +s[6].toFixed(d);
	        s[7] = +s[7].toFixed(d);
	        return;

	      default:
	        // a c l q s t
	        l = s.length;

	        if (isRelative) {
	          s[l - 2] += deltaX;
	          s[l - 1] += deltaY;
	        }

	        deltaX = s[l - 2] - s[l - 2].toFixed(d);
	        deltaY = s[l - 1] - s[l - 1].toFixed(d);

	        s.forEach(function (val, i) {
	          if (!i) { return; }
	          s[i] = +s[i].toFixed(d);
	        });
	        return;
	    }
	  });

	  return this;
	};


	// Apply iterator function to all segments. If function returns result,
	// current segment will be replaced to array of returned segments.
	// If empty array is returned, current regment will be deleted.
	//
	SvgPath.prototype.iterate = function (iterator, keepLazyStack) {
	  var segments = this.segments,
	      replacements = {},
	      needReplace = false,
	      lastX = 0,
	      lastY = 0,
	      countourStartX = 0,
	      countourStartY = 0;
	  var i, j, newSegments;

	  if (!keepLazyStack) {
	    this.__evaluateStack();
	  }

	  segments.forEach(function (s, index) {

	    var res = iterator(s, index, lastX, lastY);

	    if (Array.isArray(res)) {
	      replacements[index] = res;
	      needReplace = true;
	    }

	    var isRelative = (s[0] === s[0].toLowerCase());

	    // calculate absolute X and Y
	    switch (s[0]) {
	      case 'm':
	      case 'M':
	        lastX = s[1] + (isRelative ? lastX : 0);
	        lastY = s[2] + (isRelative ? lastY : 0);
	        countourStartX = lastX;
	        countourStartY = lastY;
	        return;

	      case 'h':
	      case 'H':
	        lastX = s[1] + (isRelative ? lastX : 0);
	        return;

	      case 'v':
	      case 'V':
	        lastY = s[1] + (isRelative ? lastY : 0);
	        return;

	      case 'z':
	      case 'Z':
	        // That make sence for multiple contours
	        lastX = countourStartX;
	        lastY = countourStartY;
	        return;

	      default:
	        lastX = s[s.length - 2] + (isRelative ? lastX : 0);
	        lastY = s[s.length - 1] + (isRelative ? lastY : 0);
	    }
	  });

	  // Replace segments if iterator return results

	  if (!needReplace) { return this; }

	  newSegments = [];

	  for (i = 0; i < segments.length; i++) {
	    if (typeof replacements[i] !== 'undefined') {
	      for (j = 0; j < replacements[i].length; j++) {
	        newSegments.push(replacements[i][j]);
	      }
	    } else {
	      newSegments.push(segments[i]);
	    }
	  }

	  this.segments = newSegments;

	  return this;
	};


	// Converts segments from relative to absolute
	//
	SvgPath.prototype.abs = function () {

	  this.iterate(function (s, index, x, y) {
	    var name = s[0],
	        nameUC = name.toUpperCase(),
	        i;

	    // Skip absolute commands
	    if (name === nameUC) { return; }

	    s[0] = nameUC;

	    switch (name) {
	      case 'v':
	        // v has shifted coords parity
	        s[1] += y;
	        return;

	      case 'a':
	        // ARC is: ['A', rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y]
	        // touch x, y only
	        s[6] += x;
	        s[7] += y;
	        return;

	      default:
	        for (i = 1; i < s.length; i++) {
	          s[i] += i % 2 ? x : y; // odd values are X, even - Y
	        }
	    }
	  }, true);

	  return this;
	};


	// Converts segments from absolute to relative
	//
	SvgPath.prototype.rel = function () {

	  this.iterate(function (s, index, x, y) {
	    var name = s[0],
	        nameLC = name.toLowerCase(),
	        i;

	    // Skip relative commands
	    if (name === nameLC) { return; }

	    // Don't touch the first M to avoid potential confusions.
	    if (index === 0 && name === 'M') { return; }

	    s[0] = nameLC;

	    switch (name) {
	      case 'V':
	        // V has shifted coords parity
	        s[1] -= y;
	        return;

	      case 'A':
	        // ARC is: ['A', rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y]
	        // touch x, y only
	        s[6] -= x;
	        s[7] -= y;
	        return;

	      default:
	        for (i = 1; i < s.length; i++) {
	          s[i] -= i % 2 ? x : y; // odd values are X, even - Y
	        }
	    }
	  }, true);

	  return this;
	};


	// Converts arcs to cubic bézier curves
	//
	SvgPath.prototype.unarc = function () {
	  this.iterate(function (s, index, x, y) {
	    var new_segments, nextX, nextY, result = [], name = s[0];

	    // Skip anything except arcs
	    if (name !== 'A' && name !== 'a') { return null; }

	    if (name === 'a') {
	      // convert relative arc coordinates to absolute
	      nextX = x + s[6];
	      nextY = y + s[7];
	    } else {
	      nextX = s[6];
	      nextY = s[7];
	    }

	    new_segments = a2c(x, y, nextX, nextY, s[4], s[5], s[1], s[2], s[3]);

	    // Degenerated arcs can be ignored by renderer, but should not be dropped
	    // to avoid collisions with `S A S` and so on. Replace with empty line.
	    if (new_segments.length === 0) {
	      return [ [ s[0] === 'a' ? 'l' : 'L', s[6], s[7] ] ];
	    }

	    new_segments.forEach(function (s) {
	      result.push([ 'C', s[2], s[3], s[4], s[5], s[6], s[7] ]);
	    });

	    return result;
	  });

	  return this;
	};


	// Converts smooth curves (with missed control point) to generic curves
	//
	SvgPath.prototype.unshort = function () {
	  var segments = this.segments;
	  var prevControlX, prevControlY, prevSegment;
	  var curControlX, curControlY;

	  // TODO: add lazy evaluation flag when relative commands supported

	  this.iterate(function (s, idx, x, y) {
	    var name = s[0], nameUC = name.toUpperCase(), isRelative;

	    // First command MUST be M|m, it's safe to skip.
	    // Protect from access to [-1] for sure.
	    if (!idx) { return; }

	    if (nameUC === 'T') { // quadratic curve
	      isRelative = (name === 't');

	      prevSegment = segments[idx - 1];

	      if (prevSegment[0] === 'Q') {
	        prevControlX = prevSegment[1] - x;
	        prevControlY = prevSegment[2] - y;
	      } else if (prevSegment[0] === 'q') {
	        prevControlX = prevSegment[1] - prevSegment[3];
	        prevControlY = prevSegment[2] - prevSegment[4];
	      } else {
	        prevControlX = 0;
	        prevControlY = 0;
	      }

	      curControlX = -prevControlX;
	      curControlY = -prevControlY;

	      if (!isRelative) {
	        curControlX += x;
	        curControlY += y;
	      }

	      segments[idx] = [
	        isRelative ? 'q' : 'Q',
	        curControlX, curControlY,
	        s[1], s[2]
	      ];

	    } else if (nameUC === 'S') { // cubic curve
	      isRelative = (name === 's');

	      prevSegment = segments[idx - 1];

	      if (prevSegment[0] === 'C') {
	        prevControlX = prevSegment[3] - x;
	        prevControlY = prevSegment[4] - y;
	      } else if (prevSegment[0] === 'c') {
	        prevControlX = prevSegment[3] - prevSegment[5];
	        prevControlY = prevSegment[4] - prevSegment[6];
	      } else {
	        prevControlX = 0;
	        prevControlY = 0;
	      }

	      curControlX = -prevControlX;
	      curControlY = -prevControlY;

	      if (!isRelative) {
	        curControlX += x;
	        curControlY += y;
	      }

	      segments[idx] = [
	        isRelative ? 'c' : 'C',
	        curControlX, curControlY,
	        s[1], s[2], s[3], s[4]
	      ];
	    }
	  });

	  return this;
	};


	svgpath$1 = SvgPath;
	return svgpath$1;
}

var svgpath;
var hasRequiredSvgpath;

function requireSvgpath () {
	if (hasRequiredSvgpath) return svgpath;
	hasRequiredSvgpath = 1;

	svgpath = requireSvgpath$1();
	return svgpath;
}

var svgpathExports = requireSvgpath();
var SvgPath = /*@__PURE__*/getDefaultExportFromCjs(svgpathExports);

var PathNode = /** @class */ (function (_super) {
    __extends(PathNode, _super);
    function PathNode(node, children) {
        return _super.call(this, true, node, children) || this;
    }
    PathNode.prototype.computeNodeTransformCore = function (context) {
        return context.pdf.unitMatrix;
    };
    PathNode.prototype.isVisible = function (parentVisible, context) {
        return svgNodeIsVisible(this, parentVisible, context);
    };
    PathNode.prototype.getPath = function (context) {
        var svgPath = new SvgPath(getAttribute(this.element, context.styleSheets, 'd') || '')
            .unshort()
            .unarc()
            .abs();
        var path = new Path();
        var prevX;
        var prevY;
        svgPath.iterate(function (seg) {
            switch (seg[0]) {
                case 'M':
                    path.moveTo(seg[1], seg[2]);
                    break;
                case 'L':
                    path.lineTo(seg[1], seg[2]);
                    break;
                case 'H':
                    path.lineTo(seg[1], prevY);
                    break;
                case 'V':
                    path.lineTo(prevX, seg[1]);
                    break;
                case 'C':
                    path.curveTo(seg[1], seg[2], seg[3], seg[4], seg[5], seg[6]);
                    break;
                case 'Q':
                    var p2 = toCubic([prevX, prevY], [seg[1], seg[2]]);
                    var p3 = toCubic([seg[3], seg[4]], [seg[1], seg[2]]);
                    path.curveTo(p2[0], p2[1], p3[0], p3[1], seg[3], seg[4]);
                    break;
                case 'Z':
                    path.close();
                    break;
            }
            switch (seg[0]) {
                case 'M':
                case 'L':
                    prevX = seg[1];
                    prevY = seg[2];
                    break;
                case 'H':
                    prevX = seg[1];
                    break;
                case 'V':
                    prevY = seg[1];
                    break;
                case 'C':
                    prevX = seg[5];
                    prevY = seg[6];
                    break;
                case 'Q':
                    prevX = seg[3];
                    prevY = seg[4];
                    break;
            }
        });
        return path;
    };
    return PathNode;
}(GeometryNode));

// groups: 1: mime-type (+ charset), 2: mime-type (w/o charset), 3: charset, 4: base64?, 5: body
var dataUriRegex = /^\s*data:(([^/,;]+\/[^/,;]+)(?:;([^,;=]+=[^,;=]+))?)?(?:;(base64))?,((?:.|\s)*)$/i;
var ImageNode = /** @class */ (function (_super) {
    __extends(ImageNode, _super);
    function ImageNode(element, children) {
        var _this = _super.call(this, element, children) || this;
        _this.imageLoadingPromise = null;
        _this.imageUrl = _this.element.getAttribute('xlink:href') || _this.element.getAttribute('href');
        if (_this.imageUrl) {
            // start loading the image as early as possible
            _this.imageLoadingPromise = ImageNode.fetchImageData(_this.imageUrl);
        }
        return _this;
    }
    ImageNode.prototype.renderCore = function (context) {
        return __awaiter(this, void 0, void 0, function () {
            var width, height, x, y, _a, data, format, parser, svgElement, preserveAspectRatio, idMap, svgnode, dataUri, _b, imgWidth, imgHeight, viewBox, transform, e_1;
            return __generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        if (!this.imageLoadingPromise) {
                            return [2 /*return*/];
                        }
                        context.pdf.setCurrentTransformationMatrix(context.transform);
                        width = parseFloat(getAttribute(this.element, context.styleSheets, 'width') || '0'), height = parseFloat(getAttribute(this.element, context.styleSheets, 'height') || '0'), x = parseFloat(getAttribute(this.element, context.styleSheets, 'x') || '0'), y = parseFloat(getAttribute(this.element, context.styleSheets, 'y') || '0');
                        if (!isFinite(width) || width <= 0 || !isFinite(height) || height <= 0) {
                            return [2 /*return*/];
                        }
                        return [4 /*yield*/, this.imageLoadingPromise];
                    case 1:
                        _a = _c.sent(), data = _a.data, format = _a.format;
                        if (!(format.indexOf('svg') === 0)) return [3 /*break*/, 3];
                        parser = new DOMParser();
                        svgElement = parser.parseFromString(data, 'image/svg+xml').firstElementChild;
                        preserveAspectRatio = this.element.getAttribute('preserveAspectRatio');
                        if (!preserveAspectRatio ||
                            preserveAspectRatio.indexOf('defer') < 0 ||
                            !svgElement.getAttribute('preserveAspectRatio')) {
                            svgElement.setAttribute('preserveAspectRatio', preserveAspectRatio || '');
                        }
                        svgElement.setAttribute('x', String(x));
                        svgElement.setAttribute('y', String(y));
                        svgElement.setAttribute('width', String(width));
                        svgElement.setAttribute('height', String(height));
                        idMap = {};
                        svgnode = parse(svgElement, idMap);
                        return [4 /*yield*/, svgnode.render(new Context(context.pdf, {
                                refsHandler: new ReferencesHandler(idMap),
                                styleSheets: context.styleSheets,
                                viewport: new Viewport(width, height),
                                svg2pdfParameters: context.svg2pdfParameters,
                                textMeasure: context.textMeasure
                            }))];
                    case 2:
                        _c.sent();
                        return [2 /*return*/];
                    case 3:
                        dataUri = "data:image/".concat(format, ";base64,").concat(btoa(data));
                        _c.label = 4;
                    case 4:
                        _c.trys.push([4, 6, , 7]);
                        return [4 /*yield*/, ImageNode.getImageDimensions(dataUri)];
                    case 5:
                        _b = _c.sent(), imgWidth = _b[0], imgHeight = _b[1];
                        viewBox = [0, 0, imgWidth, imgHeight];
                        transform = computeViewBoxTransform(this.element, viewBox, x, y, width, height, context);
                        context.pdf.setCurrentTransformationMatrix(transform);
                        context.pdf.addImage(dataUri, '', // will be ignored anyways if imageUrl is a data url
                        0, 0, imgWidth, imgHeight);
                        return [3 /*break*/, 7];
                    case 6:
                        e_1 = _c.sent();
                        typeof console === 'object' &&
                            console.warn &&
                            console.warn("Could not load image ".concat(this.imageUrl, ". \n").concat(e_1));
                        return [3 /*break*/, 7];
                    case 7: return [2 /*return*/];
                }
            });
        });
    };
    ImageNode.prototype.getBoundingBoxCore = function (context) {
        return defaultBoundingBox(this.element, context);
    };
    ImageNode.prototype.computeNodeTransformCore = function (context) {
        return context.pdf.unitMatrix;
    };
    ImageNode.prototype.isVisible = function (parentVisible, context) {
        return svgNodeIsVisible(this, parentVisible, context);
    };
    ImageNode.fetchImageData = function (imageUrl) {
        return __awaiter(this, void 0, void 0, function () {
            var data, format, match, mimeType, mimeTypeParts;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        match = imageUrl.match(dataUriRegex);
                        if (!match) return [3 /*break*/, 1];
                        mimeType = match[2];
                        mimeTypeParts = mimeType.split('/');
                        if (mimeTypeParts[0] !== 'image') {
                            throw new Error("Unsupported image URL: ".concat(imageUrl));
                        }
                        format = mimeTypeParts[1];
                        data = match[5];
                        if (match[4] === 'base64') {
                            data = data.replace(/\s/g, '');
                            data = atob(data);
                        }
                        else {
                            data = decodeURIComponent(data);
                        }
                        return [3 /*break*/, 3];
                    case 1: return [4 /*yield*/, ImageNode.fetchImage(imageUrl)];
                    case 2:
                        data = _a.sent();
                        format = imageUrl.substring(imageUrl.lastIndexOf('.') + 1);
                        _a.label = 3;
                    case 3: return [2 /*return*/, {
                            data: data,
                            format: format
                        }];
                }
            });
        });
    };
    ImageNode.fetchImage = function (imageUrl) {
        return new Promise(function (resolve, reject) {
            var xhr = new XMLHttpRequest();
            xhr.open('GET', imageUrl, true);
            xhr.responseType = 'arraybuffer';
            xhr.onload = function () {
                if (xhr.status !== 200) {
                    throw new Error("Error ".concat(xhr.status, ": Failed to load image '").concat(imageUrl, "'"));
                }
                var bytes = new Uint8Array(xhr.response);
                var data = '';
                for (var i = 0; i < bytes.length; i++) {
                    data += String.fromCharCode(bytes[i]);
                }
                resolve(data);
            };
            xhr.onerror = reject;
            xhr.onabort = reject;
            xhr.send(null);
        });
    };
    ImageNode.getMimeType = function (format) {
        format = format.toLowerCase();
        switch (format) {
            case 'jpg':
            case 'jpeg':
                return 'image/jpeg';
            default:
                return "image/".concat(format);
        }
    };
    ImageNode.getImageDimensions = function (src) {
        return new Promise(function (resolve, reject) {
            var img = new Image();
            img.onload = function () {
                resolve([img.width, img.height]);
            };
            img.onerror = reject;
            img.src = src;
        });
    };
    return ImageNode;
}(GraphicsNode));

var Traverse = /** @class */ (function (_super) {
    __extends(Traverse, _super);
    function Traverse(closed, node, children) {
        var _this = _super.call(this, true, node, children) || this;
        _this.closed = closed;
        return _this;
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    Traverse.prototype.getPath = function (context) {
        if (!this.element.hasAttribute('points') || this.element.getAttribute('points') === '') {
            return null;
        }
        // @ts-ignore
        var points = Traverse.parsePointsString(this.element.getAttribute('points'));
        var path = new Path();
        if (points.length < 1) {
            return path;
        }
        path.moveTo(points[0][0], points[0][1]);
        for (var i = 1; i < points.length; i++) {
            path.lineTo(points[i][0], points[i][1]);
        }
        if (this.closed) {
            path.close();
        }
        return path;
    };
    Traverse.prototype.isVisible = function (parentVisible, context) {
        return svgNodeIsVisible(this, parentVisible, context);
    };
    Traverse.prototype.computeNodeTransformCore = function (context) {
        return context.pdf.unitMatrix;
    };
    Traverse.parsePointsString = function (string) {
        var floats = parseFloats(string);
        var result = [];
        for (var i = 0; i < floats.length - 1; i += 2) {
            var x = floats[i];
            var y = floats[i + 1];
            result.push([x, y]);
        }
        return result;
    };
    return Traverse;
}(GeometryNode));

var Polygon = /** @class */ (function (_super) {
    __extends(Polygon, _super);
    function Polygon(node, children) {
        return _super.call(this, true, node, children) || this;
    }
    return Polygon;
}(Traverse));

var VoidNode = /** @class */ (function (_super) {
    __extends(VoidNode, _super);
    function VoidNode() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    VoidNode.prototype.render = function (parentContext) {
        return Promise.resolve();
    };
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    VoidNode.prototype.getBoundingBoxCore = function (context) {
        return [0, 0, 0, 0];
    };
    VoidNode.prototype.computeNodeTransformCore = function (context) {
        return context.pdf.unitMatrix;
    };
    VoidNode.prototype.isVisible = function (parentVisible, context) {
        return svgNodeIsVisible(this, parentVisible, context);
    };
    return VoidNode;
}(SvgNode));

var MarkerNode = /** @class */ (function (_super) {
    __extends(MarkerNode, _super);
    function MarkerNode() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    MarkerNode.prototype.apply = function (parentContext) {
        return __awaiter(this, void 0, void 0, function () {
            var tfMatrix, bBox, contextColors, childContext, _i, _a, child;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        tfMatrix = this.computeNodeTransform(parentContext);
                        bBox = this.getBoundingBox(parentContext);
                        parentContext.pdf.beginFormObject(bBox[0], bBox[1], bBox[2], bBox[3], tfMatrix);
                        contextColors = AttributeState.getContextColors(parentContext);
                        childContext = new Context(parentContext.pdf, {
                            refsHandler: parentContext.refsHandler,
                            styleSheets: parentContext.styleSheets,
                            viewport: parentContext.viewport,
                            svg2pdfParameters: parentContext.svg2pdfParameters,
                            textMeasure: parentContext.textMeasure,
                            attributeState: Object.assign(AttributeState.default(), contextColors)
                        });
                        // "Properties do not inherit from the element referencing the 'marker' into the contents of the
                        // marker. However, by using the context-stroke value for the fill or stroke on elements in its
                        // definition, a single marker can be designed to match the style of the element referencing the
                        // marker."
                        // -> we need to reset all attributes
                        applyContext(childContext);
                        _i = 0, _a = this.children;
                        _b.label = 1;
                    case 1:
                        if (!(_i < _a.length)) return [3 /*break*/, 4];
                        child = _a[_i];
                        return [4 /*yield*/, child.render(childContext)];
                    case 2:
                        _b.sent();
                        _b.label = 3;
                    case 3:
                        _i++;
                        return [3 /*break*/, 1];
                    case 4:
                        parentContext.pdf.endFormObject(childContext.refsHandler.generateKey(this.element.getAttribute('id'), contextColors));
                        return [2 /*return*/];
                }
            });
        });
    };
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    MarkerNode.prototype.getBoundingBoxCore = function (context) {
        var viewBox = this.element.getAttribute('viewBox');
        var vb;
        if (viewBox) {
            vb = parseFloats(viewBox);
        }
        return [
            (vb && vb[0]) || 0,
            (vb && vb[1]) || 0,
            (vb && vb[2]) || parseFloat(this.element.getAttribute('markerWidth') || '3'),
            (vb && vb[3]) || parseFloat(this.element.getAttribute('markerHeight') || '3')
        ];
    };
    MarkerNode.prototype.computeNodeTransformCore = function (context) {
        var refX = parseFloat(this.element.getAttribute('refX') || '0');
        var refY = parseFloat(this.element.getAttribute('refY') || '0');
        var viewBox = this.element.getAttribute('viewBox');
        var nodeTransform;
        if (viewBox) {
            var bounds = parseFloats(viewBox);
            // "Markers are drawn such that their reference point (i.e., attributes ‘refX’ and ‘refY’)
            // is positioned at the given vertex." - The "translate" part of the viewBox transform is
            // ignored.
            nodeTransform = computeViewBoxTransform(this.element, bounds, 0, 0, parseFloat(this.element.getAttribute('markerWidth') || '3'), parseFloat(this.element.getAttribute('markerHeight') || '3'), context, true);
            nodeTransform = context.pdf.matrixMult(context.pdf.Matrix(1, 0, 0, 1, -refX, -refY), nodeTransform);
        }
        else {
            nodeTransform = context.pdf.Matrix(1, 0, 0, 1, -refX, -refY);
        }
        return nodeTransform;
    };
    MarkerNode.prototype.isVisible = function (parentVisible, context) {
        return svgNodeAndChildrenVisible(this, parentVisible, context);
    };
    return MarkerNode;
}(NonRenderedNode));

var Circle = /** @class */ (function (_super) {
    __extends(Circle, _super);
    function Circle(node, children) {
        return _super.call(this, node, children) || this;
    }
    Circle.prototype.getR = function (context) {
        var _a;
        return ((_a = this.r) !== null && _a !== void 0 ? _a : (this.r = parseFloat(getAttribute(this.element, context.styleSheets, 'r') || '0')));
    };
    Circle.prototype.getRx = function (context) {
        return this.getR(context);
    };
    Circle.prototype.getRy = function (context) {
        return this.getR(context);
    };
    return Circle;
}(EllipseBase));

var Polyline = /** @class */ (function (_super) {
    __extends(Polyline, _super);
    function Polyline(node, children) {
        return _super.call(this, false, node, children) || this;
    }
    return Polyline;
}(Traverse));

var ContainerNode = /** @class */ (function (_super) {
    __extends(ContainerNode, _super);
    function ContainerNode() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    ContainerNode.prototype.renderCore = function (context) {
        return __awaiter(this, void 0, void 0, function () {
            var _i, _a, child;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        _i = 0, _a = this.children;
                        _b.label = 1;
                    case 1:
                        if (!(_i < _a.length)) return [3 /*break*/, 4];
                        child = _a[_i];
                        return [4 /*yield*/, child.render(context)];
                    case 2:
                        _b.sent();
                        _b.label = 3;
                    case 3:
                        _i++;
                        return [3 /*break*/, 1];
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    ContainerNode.prototype.getBoundingBoxCore = function (context) {
        return getBoundingBoxByChildren(context, this);
    };
    return ContainerNode;
}(RenderedNode));

var Svg = /** @class */ (function (_super) {
    __extends(Svg, _super);
    function Svg() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    Svg.prototype.isVisible = function (parentVisible, context) {
        return svgNodeAndChildrenVisible(this, parentVisible, context);
    };
    Svg.prototype.render = function (context) {
        return __awaiter(this, void 0, void 0, function () {
            var x, y, width, height, transform;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (!this.isVisible(context.attributeState.visibility !== 'hidden', context)) {
                            return [2 /*return*/];
                        }
                        x = this.getX(context);
                        y = this.getY(context);
                        width = this.getWidth(context);
                        height = this.getHeight(context);
                        context.pdf.saveGraphicsState();
                        transform = context.transform;
                        if (this.element.hasAttribute('transform')) {
                            // SVG 2 allows transforms on SVG elements
                            // "The transform should be applied as if the ‘svg’ had a parent element with that transform set."
                            transform = context.pdf.matrixMult(
                            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                            parseTransform(this.element.getAttribute('transform'), context), transform);
                        }
                        context.pdf.setCurrentTransformationMatrix(transform);
                        if (!context.withinUse &&
                            getAttribute(this.element, context.styleSheets, 'overflow') !== 'visible') {
                            // establish a new viewport
                            context.pdf
                                .rect(x, y, width, height)
                                .clip()
                                .discardPath();
                        }
                        return [4 /*yield*/, _super.prototype.render.call(this, context.clone({
                                transform: context.pdf.unitMatrix,
                                viewport: context.withinUse ? context.viewport : new Viewport(width, height)
                            }))];
                    case 1:
                        _a.sent();
                        context.pdf.restoreGraphicsState();
                        return [2 /*return*/];
                }
            });
        });
    };
    Svg.prototype.computeNodeTransform = function (context) {
        return this.computeNodeTransformCore(context);
    };
    Svg.prototype.computeNodeTransformCore = function (context) {
        if (context.withinUse) {
            return context.pdf.unitMatrix;
        }
        var x = this.getX(context);
        var y = this.getY(context);
        var viewBox = this.getViewBox();
        var nodeTransform;
        if (viewBox) {
            var width = this.getWidth(context);
            var height = this.getHeight(context);
            nodeTransform = computeViewBoxTransform(this.element, viewBox, x, y, width, height, context);
        }
        else {
            nodeTransform = context.pdf.Matrix(1, 0, 0, 1, x, y);
        }
        return nodeTransform;
    };
    Svg.prototype.getWidth = function (context) {
        if (this.width !== undefined) {
            return this.width;
        }
        var width;
        var parameters = context.svg2pdfParameters;
        if (this.isOutermostSvg(context)) {
            // special treatment for the outermost SVG element
            if (parameters.width != null) {
                // if there is a user defined width, use it
                width = parameters.width;
            }
            else {
                // otherwise check if the SVG element defines the width itself
                var widthAttr = getAttribute(this.element, context.styleSheets, 'width');
                if (widthAttr) {
                    width = parseFloat(widthAttr);
                }
                else {
                    // if not, check if we can figure out the aspect ratio from the viewBox attribute
                    var viewBox = this.getViewBox();
                    if (viewBox &&
                        (parameters.height != null || getAttribute(this.element, context.styleSheets, 'height'))) {
                        // if there is a viewBox and the height is defined, use the width that matches the height together with the aspect ratio
                        var aspectRatio = viewBox[2] / viewBox[3];
                        width = this.getHeight(context) * aspectRatio;
                    }
                    else {
                        // if there is no viewBox use a default of 300 or the largest size that fits into the outer viewport
                        // at an aspect ratio of 2:1
                        width = Math.min(300, context.viewport.width, context.viewport.height * 2);
                    }
                }
            }
        }
        else {
            var widthAttr = getAttribute(this.element, context.styleSheets, 'width');
            width = widthAttr ? parseFloat(widthAttr) : context.viewport.width;
        }
        return (this.width = width);
    };
    Svg.prototype.getHeight = function (context) {
        if (this.height !== undefined) {
            return this.height;
        }
        var height;
        var parameters = context.svg2pdfParameters;
        if (this.isOutermostSvg(context)) {
            // special treatment for the outermost SVG element
            if (parameters.height != null) {
                // if there is a user defined height, use it
                height = parameters.height;
            }
            else {
                // otherwise check if the SVG element defines the height itself
                var heightAttr = getAttribute(this.element, context.styleSheets, 'height');
                if (heightAttr) {
                    height = parseFloat(heightAttr);
                }
                else {
                    // if not, check if we can figure out the aspect ratio from the viewBox attribute
                    var viewBox = this.getViewBox();
                    if (viewBox) {
                        // if there is a viewBox, use the height that matches the width together with the aspect ratio
                        var aspectRatio = viewBox[2] / viewBox[3];
                        height = this.getWidth(context) / aspectRatio;
                    }
                    else {
                        // if there is no viewBox use a default of 150 or the largest size that fits into the outer viewport
                        // at an aspect ratio of 2:1
                        height = Math.min(150, context.viewport.width / 2, context.viewport.height);
                    }
                }
            }
        }
        else {
            var heightAttr = getAttribute(this.element, context.styleSheets, 'height');
            height = heightAttr ? parseFloat(heightAttr) : context.viewport.height;
        }
        return (this.height = height);
    };
    Svg.prototype.getX = function (context) {
        if (this.x !== undefined) {
            return this.x;
        }
        if (this.isOutermostSvg(context)) {
            return (this.x = 0);
        }
        var xAttr = getAttribute(this.element, context.styleSheets, 'x');
        return (this.x = xAttr ? parseFloat(xAttr) : 0);
    };
    Svg.prototype.getY = function (context) {
        if (this.y !== undefined) {
            return this.y;
        }
        if (this.isOutermostSvg(context)) {
            return (this.y = 0);
        }
        var yAttr = getAttribute(this.element, context.styleSheets, 'y');
        return (this.y = yAttr ? parseFloat(yAttr) : 0);
    };
    Svg.prototype.getViewBox = function () {
        if (this.viewBox !== undefined) {
            return this.viewBox;
        }
        var viewBox = this.element.getAttribute('viewBox');
        return (this.viewBox = viewBox ? parseFloats(viewBox) : undefined);
    };
    Svg.prototype.isOutermostSvg = function (context) {
        return context.svg2pdfParameters.element === this.element;
    };
    return Svg;
}(ContainerNode));

var Group = /** @class */ (function (_super) {
    __extends(Group, _super);
    function Group() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    Group.prototype.isVisible = function (parentVisible, context) {
        return svgNodeAndChildrenVisible(this, parentVisible, context);
    };
    Group.prototype.computeNodeTransformCore = function (context) {
        return context.pdf.unitMatrix;
    };
    return Group;
}(ContainerNode));

var Anchor = /** @class */ (function (_super) {
    __extends(Anchor, _super);
    function Anchor() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    Anchor.prototype.renderCore = function (context) {
        return __awaiter(this, void 0, void 0, function () {
            var href, box, scale, ph;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, _super.prototype.renderCore.call(this, context)];
                    case 1:
                        _a.sent();
                        href = getAttribute(this.element, context.styleSheets, 'href');
                        if (href) {
                            box = this.getBoundingBox(context);
                            scale = context.pdf.internal.scaleFactor;
                            ph = context.pdf.internal.pageSize.getHeight();
                            context.pdf.link(scale * (box[0] * context.transform.sx + context.transform.tx), scale * (ph - box[1] * context.transform.sy - context.transform.ty), scale * context.transform.sx * box[2], scale * context.transform.sy * box[3], { url: href });
                        }
                        return [2 /*return*/];
                }
            });
        });
    };
    return Anchor;
}(Group));

var ClipPath = /** @class */ (function (_super) {
    __extends(ClipPath, _super);
    function ClipPath() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    ClipPath.prototype.apply = function (context) {
        return __awaiter(this, void 0, void 0, function () {
            var clipPathMatrix, _i, _a, child, hasClipRuleFromFirstChild, clipRule;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        if (!this.isVisible(true, context)) {
                            return [2 /*return*/];
                        }
                        clipPathMatrix = context.pdf.matrixMult(this.computeNodeTransform(context), context.transform);
                        context.pdf.setCurrentTransformationMatrix(clipPathMatrix);
                        _i = 0, _a = this.children;
                        _b.label = 1;
                    case 1:
                        if (!(_i < _a.length)) return [3 /*break*/, 4];
                        child = _a[_i];
                        return [4 /*yield*/, child.render(new Context(context.pdf, {
                                refsHandler: context.refsHandler,
                                styleSheets: context.styleSheets,
                                viewport: context.viewport,
                                withinClipPath: true,
                                svg2pdfParameters: context.svg2pdfParameters,
                                textMeasure: context.textMeasure
                            }))];
                    case 2:
                        _b.sent();
                        _b.label = 3;
                    case 3:
                        _i++;
                        return [3 /*break*/, 1];
                    case 4:
                        hasClipRuleFromFirstChild = this.children.length > 0 && !!getAttribute(this.children[0].element, context.styleSheets, 'clip-rule');
                        clipRule = hasClipRuleFromFirstChild
                            ? this.getClipRuleAttr(this.children[0].element, context.styleSheets)
                            : this.getClipRuleAttr(this.element, context.styleSheets);
                        context.pdf.clip(clipRule).discardPath();
                        // as we cannot use restoreGraphicsState() to reset the transform (this would reset the clipping path, as well),
                        // we must append the inverse instead
                        context.pdf.setCurrentTransformationMatrix(clipPathMatrix.inversed());
                        return [2 /*return*/];
                }
            });
        });
    };
    ClipPath.prototype.getBoundingBoxCore = function (context) {
        return getBoundingBoxByChildren(context, this);
    };
    ClipPath.prototype.isVisible = function (parentVisible, context) {
        return svgNodeAndChildrenVisible(this, parentVisible, context);
    };
    ClipPath.prototype.getClipRuleAttr = function (element, styleSheets) {
        return getAttribute(element, styleSheets, 'clip-rule') === 'evenodd' ? 'evenodd' : undefined;
    };
    return ClipPath;
}(NonRenderedNode));

function parse(node, idMap) {
    var svgnode;
    var children = [];
    forEachChild(node, function (i, n) { return children.push(parse(n, idMap)); });
    switch (node.tagName.toLowerCase()) {
        case 'a':
            svgnode = new Anchor(node, children);
            break;
        case 'g':
            svgnode = new Group(node, children);
            break;
        case 'circle':
            svgnode = new Circle(node, children);
            break;
        case 'clippath':
            svgnode = new ClipPath(node, children);
            break;
        case 'ellipse':
            svgnode = new Ellipse(node, children);
            break;
        case 'lineargradient':
            svgnode = new LinearGradient(node, children);
            break;
        case 'image':
            svgnode = new ImageNode(node, children);
            break;
        case 'line':
            svgnode = new Line(node, children);
            break;
        case 'marker':
            svgnode = new MarkerNode(node, children);
            break;
        case 'path':
            svgnode = new PathNode(node, children);
            break;
        case 'pattern':
            svgnode = new Pattern(node, children);
            break;
        case 'polygon':
            svgnode = new Polygon(node, children);
            break;
        case 'polyline':
            svgnode = new Polyline(node, children);
            break;
        case 'radialgradient':
            svgnode = new RadialGradient(node, children);
            break;
        case 'rect':
            svgnode = new Rect(node, children);
            break;
        case 'svg':
            svgnode = new Svg(node, children);
            break;
        case 'symbol':
            svgnode = new Symbol$1(node, children);
            break;
        case 'text':
            svgnode = new TextNode(node, children);
            break;
        case 'use':
            svgnode = new Use(node, children);
            break;
        default:
            svgnode = new VoidNode(node, children);
            break;
    }
    if (idMap != undefined && svgnode.element.hasAttribute('id')) {
        // const id = cssesc(svgnode.element.id, { isIdentifier: true })
        var id = svgnode.element.id; // jsroot has plain ids
        idMap[id] = idMap[id] || svgnode;
    }
    svgnode.children.forEach(function (c) { return c.setParent(svgnode); });
    return svgnode;
}

var StyleSheets = /** @class */ (function () {
    function StyleSheets(rootSvg, loadExtSheets) {
        this.rootSvg = rootSvg;
        this.loadExternalSheets = loadExtSheets;
        this.styleSheets = [];
    }
    StyleSheets.prototype.load = function () {
        return __awaiter(this, void 0, void 0, function () {
            var sheetTexts;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, this.collectStyleSheetTexts()];
                    case 1:
                        sheetTexts = _a.sent();
                        this.parseCssSheets(sheetTexts);
                        return [2 /*return*/];
                }
            });
        });
    };
    StyleSheets.prototype.collectStyleSheetTexts = function () {
        return __awaiter(this, void 0, void 0, function () {
            var sheetTexts, i, node, styleElements, i, styleElement;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        sheetTexts = [];
                        if (this.loadExternalSheets && this.rootSvg.ownerDocument) {
                            for (i = 0; i < this.rootSvg.ownerDocument.childNodes.length; i++) {
                                node = this.rootSvg.ownerDocument.childNodes[i];
                                // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                                // @ts-ignore
                                if (node.nodeName === 'xml-stylesheet' && typeof node.data === 'string') {
                                    sheetTexts.push(StyleSheets.loadSheet(
                                    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                                    // @ts-ignore
                                    node.data
                                        .match(/href=["'].*?["']/)[0]
                                        .split('=')[1]
                                        .slice(1, -1)));
                                }
                            }
                        }
                        styleElements = this.rootSvg.querySelectorAll('style,link');
                        for (i = 0; i < styleElements.length; i++) {
                            styleElement = styleElements[i];
                            if (nodeIs(styleElement, 'style')) {
                                sheetTexts.push(styleElement.textContent);
                            }
                            else if (this.loadExternalSheets &&
                                nodeIs(styleElement, 'link') &&
                                styleElement.getAttribute('rel') === 'stylesheet' &&
                                styleElement.hasAttribute('href')) {
                                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                                sheetTexts.push(StyleSheets.loadSheet(styleElement.getAttribute('href')));
                            }
                        }
                        return [4 /*yield*/, Promise.all(sheetTexts)];
                    case 1: return [2 /*return*/, (_a.sent()).filter(function (sheet) { return sheet !== null; })];
                }
            });
        });
    };
    StyleSheets.prototype.parseCssSheets = function (sheetTexts) {
        var styleDoc = document.implementation.createHTMLDocument('');
        for (var _i = 0, sheetTexts_1 = sheetTexts; _i < sheetTexts_1.length; _i++) {
            var sheetText = sheetTexts_1[_i];
            var style = styleDoc.createElement('style');
            style.textContent = sheetText;
            styleDoc.body.appendChild(style);
            var sheet = style.sheet;
            if (sheet instanceof CSSStyleSheet) {
                for (var i = sheet.cssRules.length - 1; i >= 0; i--) {
                    var cssRule = sheet.cssRules[i];
                    if (!(cssRule instanceof CSSStyleRule)) {
                        sheet.deleteRule(i);
                        continue;
                    }
                    var cssStyleRule = cssRule;
                    if (cssStyleRule.selectorText.indexOf(',') >= 0) {
                        sheet.deleteRule(i);
                        var body = cssStyleRule.cssText.substring(cssStyleRule.selectorText.length);
                        var selectors = StyleSheets.splitSelectorAtCommas(cssStyleRule.selectorText);
                        for (var j = 0; j < selectors.length; j++) {
                            sheet.insertRule(selectors[j] + body, i + j);
                        }
                    }
                }
                this.styleSheets.push(sheet);
            }
        }
    };
    StyleSheets.splitSelectorAtCommas = function (selectorText) {
        var initialRegex = /,|["']/g;
        var closingDoubleQuotesRegex = /[^\\]["]/g;
        var closingSingleQuotesRegex = /[^\\][']/g;
        var parts = [];
        var state = 'initial';
        var match;
        var lastCommaIndex = -1;
        var closingQuotesRegex = closingDoubleQuotesRegex;
        for (var i = 0; i < selectorText.length;) {
            switch (state) {
                case 'initial':
                    initialRegex.lastIndex = i;
                    match = initialRegex.exec(selectorText);
                    if (match) {
                        if (match[0] === ',') {
                            parts.push(selectorText.substring(lastCommaIndex + 1, initialRegex.lastIndex - 1).trim());
                            lastCommaIndex = initialRegex.lastIndex - 1;
                        }
                        else {
                            state = 'withinQuotes';
                            closingQuotesRegex =
                                match[0] === '"' ? closingDoubleQuotesRegex : closingSingleQuotesRegex;
                        }
                        i = initialRegex.lastIndex;
                    }
                    else {
                        parts.push(selectorText.substring(lastCommaIndex + 1).trim());
                        i = selectorText.length;
                    }
                    break;
                case 'withinQuotes':
                    closingQuotesRegex.lastIndex = i;
                    match = closingQuotesRegex.exec(selectorText);
                    if (match) {
                        i = closingQuotesRegex.lastIndex;
                        state = 'initial';
                    }
                    // else this is a syntax error - omit the last part...
                    break;
            }
        }
        return parts;
    };
    StyleSheets.loadSheet = function (url) {
        return (new Promise(function (resolve, reject) {
            var xhr = new XMLHttpRequest();
            xhr.open('GET', url, true);
            xhr.responseType = 'text';
            xhr.onload = function () {
                if (xhr.status !== 200) {
                    reject(new Error("Error ".concat(xhr.status, ": Failed to load '").concat(url, "'")));
                }
                resolve(xhr.responseText);
            };
            xhr.onerror = reject;
            xhr.onabort = reject;
            xhr.send(null);
        })
            // ignore the error since some stylesheets may not be accessible
            // due to CORS policies
            .catch(function () { return null; }));
    };
    StyleSheets.prototype.getPropertyValue = function (node, propertyCss) {
        var matchingRules = [];
        for (var _i = 0, _a = this.styleSheets; _i < _a.length; _i++) {
            var sheet = _a[_i];
            for (var i = 0; i < sheet.cssRules.length; i++) {
                var rule = sheet.cssRules[i];
                if (rule.style.getPropertyValue(propertyCss) && node.matches(rule.selectorText)) {
                    matchingRules.push(rule);
                }
            }
        }
        if (matchingRules.length === 0) {
            return undefined;
        }
        var compare = function (a, b) {
            var priorityA = a.style.getPropertyPriority(propertyCss);
            var priorityB = b.style.getPropertyPriority(propertyCss);
            if (priorityA !== priorityB) {
                return priorityA === 'important' ? 1 : -1;
            }
            // console.log('removed specificity check ', a.selectorText, b.selectorText);
            return 0;
            // return compareSpecificity(a.selectorText, b.selectorText)
        };
        var mostSpecificRule = matchingRules.reduce(function (previousValue, currentValue) {
            return compare(previousValue, currentValue) === 1 ? previousValue : currentValue;
        });
        return mostSpecificRule.style.getPropertyValue(propertyCss) || undefined;
    };
    return StyleSheets;
}());

var TextMeasure = /** @class */ (function () {
    function TextMeasure() {
        this.measureMethods = {};
    }
    TextMeasure.prototype.getTextOffset = function (text, attributeState) {
        var textAnchor = attributeState.textAnchor;
        if (textAnchor === 'start') {
            return 0;
        }
        var width = this.measureTextWidth(text, attributeState);
        var xOffset = 0;
        switch (textAnchor) {
            case 'end':
                xOffset = width;
                break;
            case 'middle':
                xOffset = width / 2;
                break;
        }
        return xOffset;
    };
    TextMeasure.prototype.measureTextWidth = function (text, attributeState) {
        if (text.length === 0) {
            return 0;
        }
        var fontFamily = attributeState.fontFamily;
        var measure = this.getMeasureFunction(fontFamily);
        return measure.call(this, text, attributeState.fontFamily, attributeState.fontSize + 'px', attributeState.fontStyle, attributeState.fontWeight);
    };
    TextMeasure.prototype.getMeasurementTextNode = function () {
        if (!this.textMeasuringTextElement) {
            this.textMeasuringTextElement = document.createElementNS(svgNamespaceURI, 'text');
            var svg = document.createElementNS(svgNamespaceURI, 'svg');
            svg.appendChild(this.textMeasuringTextElement);
            svg.style.setProperty('position', 'absolute');
            svg.style.setProperty('visibility', 'hidden');
            document.body.appendChild(svg);
        }
        return this.textMeasuringTextElement;
    };
    TextMeasure.prototype.canvasTextMeasure = function (text, fontFamily, fontSize, fontStyle, fontWeight) {
        var canvas = document.createElement('canvas');
        var context = canvas.getContext('2d');
        if (context != null) {
            context.font = [fontStyle, fontWeight, fontSize, fontFamily].join(' ');
            return context.measureText(text).width;
        }
        return 0;
    };
    TextMeasure.prototype.svgTextMeasure = function (text, fontFamily, fontSize, fontStyle, fontWeight, measurementTextNode) {
        if (measurementTextNode === void 0) { measurementTextNode = this.getMeasurementTextNode(); }
        var textNode = measurementTextNode;
        textNode.setAttribute('font-family', fontFamily);
        textNode.setAttribute('font-size', fontSize);
        textNode.setAttribute('font-style', fontStyle);
        textNode.setAttribute('font-weight', fontWeight);
        textNode.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
        textNode.textContent = text;
        return textNode.getBBox().width;
    };
    /**
     * Canvas text measuring is a lot faster than svg measuring. However, it is inaccurate for some fonts. So test each
     * font once and decide if canvas is accurate enough.
     */
    TextMeasure.prototype.getMeasureFunction = function (fontFamily) {
        var method = this.measureMethods[fontFamily];
        if (!method) {
            var fontSize = '16px';
            var fontStyle = 'normal';
            var fontWeight = 'normal';
            var canvasWidth = this.canvasTextMeasure(TextMeasure.testString, fontFamily, fontSize, fontStyle, fontWeight);
            var svgWidth = this.svgTextMeasure(TextMeasure.testString, fontFamily, fontSize, fontStyle, fontWeight);
            method =
                Math.abs(canvasWidth - svgWidth) < TextMeasure.epsilon
                    ? this.canvasTextMeasure
                    : this.svgTextMeasure;
            this.measureMethods[fontFamily] = method;
        }
        return method;
    };
    TextMeasure.prototype.cleanupTextMeasuring = function () {
        if (this.textMeasuringTextElement) {
            var parentNode = this.textMeasuringTextElement.parentNode;
            if (parentNode) {
                document.body.removeChild(parentNode);
            }
            this.textMeasuringTextElement = undefined;
        }
    };
    TextMeasure.testString = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789!"$%&/()=?\'\\+*-_.:,;^}][{#~|<>';
    TextMeasure.epsilon = 0.1;
    return TextMeasure;
}());

function svg2pdf(element_1, pdf_1) {
    return __awaiter(this, arguments, void 0, function (element, pdf, options) {
        var x, y, extCss, idMap, refsHandler, styleSheets, viewport, svg2pdfParameters, textMeasure, context, fill, node;
        var _a, _b, _c;
        if (options === void 0) { options = {}; }
        return __generator(this, function (_d) {
            switch (_d.label) {
                case 0:
                    x = (_a = options.x) !== null && _a !== void 0 ? _a : 0.0;
                    y = (_b = options.y) !== null && _b !== void 0 ? _b : 0.0;
                    extCss = (_c = options.loadExternalStyleSheets) !== null && _c !== void 0 ? _c : false;
                    idMap = {};
                    refsHandler = new ReferencesHandler(idMap);
                    styleSheets = new StyleSheets(element, extCss);
                    return [4 /*yield*/, styleSheets.load()
                        // start with the entire page size as viewport
                    ];
                case 1:
                    _d.sent();
                    viewport = new Viewport(pdf.internal.pageSize.getWidth(), pdf.internal.pageSize.getHeight());
                    svg2pdfParameters = __assign(__assign({}, options), { element: element });
                    textMeasure = new TextMeasure();
                    context = new Context(pdf, {
                        refsHandler: refsHandler,
                        styleSheets: styleSheets,
                        viewport: viewport,
                        svg2pdfParameters: svg2pdfParameters,
                        textMeasure: textMeasure
                    });
                    pdf.advancedAPI();
                    pdf.saveGraphicsState();
                    // set offsets
                    pdf.setCurrentTransformationMatrix(pdf.Matrix(1, 0, 0, 1, x, y));
                    // set default values that differ from pdf defaults
                    pdf.setLineWidth(context.attributeState.strokeWidth);
                    fill = context.attributeState.fill.color;
                    pdf.setFillColor(fill.r, fill.g, fill.b);
                    pdf.setFont(context.attributeState.fontFamily);
                    // correct for a jsPDF-instance measurement unit that differs from `pt`
                    pdf.setFontSize(context.attributeState.fontSize * pdf.internal.scaleFactor);
                    node = parse(element, idMap);
                    return [4 /*yield*/, node.render(context)];
                case 2:
                    _d.sent();
                    pdf.restoreGraphicsState();
                    pdf.compatAPI();
                    context.textMeasure.cleanupTextMeasuring();
                    return [2 /*return*/, pdf];
            }
        });
    });
}
jsPDF.API.svg = function (element, options) {
    if (options === void 0) { options = {}; }
    return svg2pdf(element, this, options);
};

/** @summary Create pdf for existing SVG element
  * @return {Promise} with produced PDF file as url string
  * @private */
async function makePDF(svg, args) {
   const nodejs = isNodeJs();
   let need_symbols = false;

   const restore_fonts = [], restore_symb = [], restore_wing = [], restore_dominant = [], restore_oblique = [], restore_text = [],
         node_transform = svg.node.getAttribute('transform'), custom_fonts = {};

   if (svg.reset_tranform)
      svg.node.removeAttribute('transform');

   select(svg.node).selectAll('g').each(function() {
      if (this.hasAttribute('font-family')) {
         const name = this.getAttribute('font-family');
         if (name === kCourier) {
            this.setAttribute('font-family', 'courier');
            if (!svg.can_modify)
               restore_fonts.push(this); // keep to restore it
         }
         if (name === kSymbol) {
            this.setAttribute('font-family', 'symbol');
            if (!svg.can_modify)
               restore_symb.push(this); // keep to restore it
         }
         if (name === kWingdings) {
            this.setAttribute('font-family', 'zapfdingbats');
            if (!svg.can_modify)
               restore_wing.push(this); // keep to restore it
         }

         if (((name === kArial) || (name === kCourier)) && (this.getAttribute('font-weight') === 'bold') && (this.getAttribute('font-style') === 'oblique')) {
            this.setAttribute('font-style', 'italic');
            if (!svg.can_modify)
               restore_oblique.push(this); // keep to restore it
         } else if ((name === kCourier) && (this.getAttribute('font-style') === 'oblique')) {
            this.setAttribute('font-style', 'italic');
            if (!svg.can_modify)
               restore_oblique.push(this); // keep to restore it
         }
      }
   });

   select(svg.node).selectAll('text').each(function() {
      if (this.hasAttribute('dominant-baseline')) {
         this.setAttribute('dy', '.2em'); // slightly different as in plain text
         this.removeAttribute('dominant-baseline');
         if (!svg.can_modify)
            restore_dominant.push(this); // keep to restore it
      } else if (svg.can_modify && nodejs && this.getAttribute('dy') === '.4em')
         this.setAttribute('dy', '.2em'); // better alignment in PDF

      if (replaceSymbolsInTextNode(this)) {
         need_symbols = true;
         if (!svg.can_modify)
            restore_text.push(this); // keep to restore it
      }
   });

   let pr = Promise.resolve();

   if (nodejs) {
      const doc = internals.nodejs_document;
      doc.originalCreateElementNS = doc.createElementNS;
      globalThis.document = doc;
      globalThis.CSSStyleSheet = internals.nodejs_window.CSSStyleSheet;
      globalThis.CSSStyleRule = internals.nodejs_window.CSSStyleRule;
      doc.createElementNS = function(ns, kind) {
         const res = doc.originalCreateElementNS(ns, kind);
         res.getBBox = function() {
            let width = 50, height = 10;
            if (this.tagName === 'text') {
               // TODO: use jsDOC fonts for label width estimation
               const font = detectPdfFont(this);
               width = approximateLabelWidth(this.textContent, font);
               height = font.size * 1.2;
            }

            return { x: 0, y: 0, width, height };
         };
         return res;
      };

      pr = Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(handle => {
         globalThis.Image = handle.Image;
      });
   }

   const orientation = (svg.width < svg.height) ? 'portrait' : 'landscape';

   let doc = args?.as_doc ? args.doc : null;

   if (doc) {
      doc.addPage({
         orientation,
         unit: 'px',
         format: [svg.width + 10, svg.height + 10]
      });
   } else {
      doc = new jsPDF({
         orientation,
         unit: 'px',
         format: [svg.width + 10, svg.height + 10]
      });
      if (args?.as_doc)
         args.doc = doc;
   }

   // add custom fonts to PDF document, only TTF format supported
   select(svg.node).selectAll('style').each(function() {
      const fcfg = this.$fontcfg;
      if (!fcfg?.n || !fcfg?.base64)
         return;
      const name = fcfg.n;
      if ((name === kSymbol) || (name === kWingdings))
         return;
      if (custom_fonts[name])
         return;
      custom_fonts[name] = true;

      const filename = name.toLowerCase().replace(/\s/g, '') + '.ttf';
      doc.addFileToVFS(filename, fcfg.base64);
      doc.addFont(filename, fcfg.n, fcfg.s || 'normal');
   });

   if (need_symbols && !custom_fonts[kSymbol] && settings.LoadSymbolTtf) {
      const handler = new FontHandler(122, 10);
      pr = pr.then(() => handler.load()).then(() => {
         handler.addCustomFontToSvg(select(svg.node));
         doc.addFileToVFS(kSymbol + '.ttf', handler.base64);
         doc.addFont(kSymbol + '.ttf', kSymbol, 'normal');
      });
   }

   return pr.then(() => svg2pdf(svg.node, doc, { x: 5, y: 5, width: svg.width, height: svg.height })).then(() => {
      if (svg.reset_tranform && !svg.can_modify && node_transform)
         svg.node.setAttribute('transform', node_transform);

      restore_fonts.forEach(node => node.setAttribute('font-family', kCourier));
      restore_symb.forEach(node => node.setAttribute('font-family', kSymbol));
      restore_wing.forEach(node => node.setAttribute('font-family', kWingdings));
      restore_oblique.forEach(node => node.setAttribute('font-style', 'oblique'));
      restore_dominant.forEach(node => {
         node.setAttribute('dominant-baseline', 'middle');
         node.removeAttribute('dy');
      });

      restore_text.forEach(node => {
         node.innerHTML = node.$originalHTML;
         if (node.$originalFont)
            node.setAttribute('font-family', node.$originalFont);
         else
            node.removeAttribute('font-family');
      });

      const res = args?.as_buffer ? doc.output('arraybuffer') : doc.output('dataurlstring');
      if (nodejs) {
         globalThis.document = undefined;
         globalThis.CSSStyleSheet = undefined;
         globalThis.CSSStyleRule = undefined;
         globalThis.Image = undefined;
         internals.nodejs_document.createElementNS = internals.nodejs_document.originalCreateElementNS;
         if (args?.as_buffer)
            return Buffer.from(res);
      }

      return res;
   });
}

async function import_more() { return Promise.resolve().then(function () { return more; }); }

async function import_canvas() { return Promise.resolve().then(function () { return TCanvasPainter$1; }); }

async function import_tree() { return Promise.resolve().then(function () { return TTree; }); }

async function import_h() { return Promise.resolve().then(function () { return HierarchyPainter$1; }); }

let import_v7 = null, import_geo = null;

const clTGraph2D = 'TGraph2D', clTH2Poly = 'TH2Poly', clTEllipse = 'TEllipse',
      clTSpline3 = 'TSpline3', clTCanvasWebSnapshot = 'TCanvasWebSnapshot',
      fPrimitives = 'fPrimitives', fFunctions = 'fFunctions';

/** @summary list of registered draw functions
  * @private */
/* eslint-disable-next-line one-var */
const drawFuncs = { lst: [
   { name: clTCanvas, icon: 'img_canvas', class: () => import_canvas().then(h => h.TCanvasPainter), opt: ';grid;gridx;gridy;tick;tickx;ticky;log;logx;logy;logz', expand_item: fPrimitives, noappend: true },
   { name: clTPad, icon: 'img_canvas', func: TPadPainter.draw, opt: ';grid;gridx;gridy;tick;tickx;ticky;log;logx;logy;logz', expand_item: fPrimitives, noappend: true },
   { name: 'TSlider', icon: 'img_canvas', func: TPadPainter.draw },
   { name: clTButton, icon: 'img_canvas', func: TPadPainter.draw },
   { name: 'TInspectCanvas', icon: 'img_canvas', sameas: clTCanvas },
   { name: clTFrame, icon: 'img_frame', draw: () => import_canvas().then(h => h.drawTFrame) },
   { name: clTPave, icon: 'img_pavetext', class: () => Promise.resolve().then(function () { return TPavePainter$1; }).then(h => h.TPavePainter) },
   { name: clTPaveText, sameas: clTPave },
   { name: clTPavesText, sameas: clTPave },
   { name: clTPaveStats, sameas: clTPave },
   { name: clTPaveLabel, sameas: clTPave },
   { name: clTPaveClass, sameas: clTPave },
   { name: clTDiamond, sameas: clTPave },
   { name: clTLegend, icon: 'img_pavelabel', sameas: clTPave },
   { name: clTPaletteAxis, icon: 'img_colz', sameas: clTPave },
   { name: clTText, icon: 'img_text', class: () => Promise.resolve().then(function () { return TTextPainter$1; }).then(h => h.TTextPainter), build3d: () => Promise.resolve().then(function () { return latex3d; }).then(h => h.build3dlatex) },
   { name: clTMathText, sameas: clTText },
   { name: clTLatex, sameas: clTText },
   { name: clTLink, sameas: clTText },
   { name: clTAnnotation, icon: 'img_text', class: () => Promise.resolve().then(function () { return TAnnotation3DPainter$1; }).then(h => h.TAnnotation3DPainter) },
   { name: /^TH1/, icon: 'img_histo1d', class: () => Promise.resolve().then(function () { return TH1Painter$1; }).then(h => h.TH1Painter), opt: ';hist;P;P0;E;E1;E2;E3;E4;E1X0;L;LF2;C;B;B1;A;TEXT;LEGO;same', ctrl: 'l', expand_item: fFunctions, for_derived: true },
   { name: clTProfile, icon: 'img_profile', class: () => Promise.resolve().then(function () { return TH1Painter$1; }).then(h => h.TH1Painter), opt: ';E0;E1;E2;p;AH;hist;projx;projxb;projxc=e;projxw', expand_item: fFunctions },
   { name: clTH2Poly, icon: 'img_histo2d', class: () => Promise.resolve().then(function () { return TH2Painter$1; }).then(h => h.TH2Painter), opt: ';COL;COL0;COLZ;LCOL;LCOL0;LCOLZ;LEGO;TEXT;same', expand_item: 'fBins', theonly: true },
   { name: 'TProfile2Poly', sameas: clTH2Poly },
   { name: 'TH2PolyBin', icon: 'img_histo2d', draw_field: 'fPoly', draw_field_opt: 'L' },
   { name: /^TH2/, icon: 'img_histo2d', class: () => Promise.resolve().then(function () { return TH2Painter$1; }).then(h => h.TH2Painter), opt: ';COL;COLZ;COL0;COL1;COL0Z;COL1Z;COLA;COL_POL;COL_ARR;BOX;BOX1;PROJ;PROJX1;PROJX2;PROJX3;PROJY1;PROJY2;PROJY3;PROJXY1;PROJXY2;PROJXY3;SCAT;TEXT;TEXTE;TEXTE0;CANDLE;CANDLE1;CANDLE2;CANDLE3;CANDLE4;CANDLE5;CANDLE6;CANDLEY1;CANDLEY2;CANDLEY3;CANDLEY4;CANDLEY5;CANDLEY6;VIOLIN;VIOLIN1;VIOLIN2;VIOLINY1;VIOLINY2;CONT;CONT1;CONT2;CONT3;CONT4;ARR;CHORD;SURF;SURF1;SURF2;SURF4;SURF6;E;A;LEGO;LEGO0;LEGO1;LEGO2;LEGO3;LEGO4;same', ctrl: 'lego', expand_item: fFunctions, for_derived: true },
   { name: clTProfile2D, sameas: clTH2, opt2: ';projxyb;projxyc=e;projxyw' },
   { name: /^TH3/, icon: 'img_histo3d', class: () => Promise.resolve().then(function () { return TH3Painter$1; }).then(h => h.TH3Painter), opt: ';SCAT;BOX;BOX2;BOX3;GLBOX1;GLBOX2;GLCOL', expand_item: fFunctions, for_derived: true },
   { name: clTProfile3D, sameas: clTH3 },
   { name: clTHStack, icon: 'img_histo1d', class: () => Promise.resolve().then(function () { return THStackPainter$1; }).then(h => h.THStackPainter), expand_item: 'fHists', opt: 'NOSTACK;HIST;COL;LEGO;E;PFC;PLC;PADS' },
   { name: clTPolyMarker3D, icon: 'img_histo3d', draw: () => Promise.resolve().then(function () { return draw3d; }).then(h => h.drawPolyMarker3D), direct: true, frame: '3d' },
   { name: clTPolyLine3D, icon: 'img_graph', draw: () => Promise.resolve().then(function () { return draw3d; }).then(h => h.drawPolyLine3D), direct: true, frame: '3d' },
   { name: 'TGraphStruct' },
   { name: 'TGraphNode' },
   { name: 'TGraphEdge' },
   { name: clTGraphTime, icon: 'img_graph', class: () => Promise.resolve().then(function () { return TGraphTimePainter$1; }).then(h => h.TGraphTimePainter), opt: 'once;repeat;first', theonly: true },
   { name: clTGraph2D, icon: 'img_graph', class: () => Promise.resolve().then(function () { return TGraph2DPainter$1; }).then(h => h.TGraph2DPainter), opt: ';P;PCOL', theonly: true },
   { name: clTGraph2DErrors, sameas: clTGraph2D, opt: ';P;PCOL;ERR', theonly: true },
   { name: clTGraph2DAsymmErrors, sameas: clTGraph2D, opt: ';P;PCOL;ERR', theonly: true },
   { name: clTGraphPolargram, icon: 'img_graph', class: () => Promise.resolve().then(function () { return TGraphPolarPainter$1; }).then(h => h.TGraphPolargramPainter), theonly: true },
   { name: clTGraphPolar, icon: 'img_graph', class: () => Promise.resolve().then(function () { return TGraphPolarPainter$1; }).then(h => h.TGraphPolarPainter), opt: ';F;L;P;PE', theonly: true },
   { name: /^TGraph/, icon: 'img_graph', class: () => Promise.resolve().then(function () { return TGraphPainter$2; }).then(h => h.TGraphPainter), opt: ';L;P' },
   { name: 'TEfficiency', icon: 'img_graph', class: () => Promise.resolve().then(function () { return TEfficiencyPainter$1; }).then(h => h.TEfficiencyPainter), opt: ';AP;A4P;B' },
   { name: clTCutG, sameas: clTGraph },
   { name: /^RooHist/, sameas: clTGraph },
   { name: /^RooCurve/, sameas: clTGraph },
   { name: /^RooEllipse/, sameas: clTGraph },
   { name: 'TScatter', icon: 'img_graph', class: () => Promise.resolve().then(function () { return TScatterPainter$1; }).then(h => h.TScatterPainter), opt: ';A' },
   { name: 'RooPlot', icon: 'img_canvas', draw: () => Promise.resolve().then(function () { return TGraphTimePainter$1; }).then(h => h.drawRooPlot) },
   { name: 'TRatioPlot', icon: 'img_mgraph', class: () => Promise.resolve().then(function () { return TRatioPlotPainter$1; }).then(h => h.TRatioPlotPainter), opt: '' },
   { name: clTMultiGraph, icon: 'img_mgraph', class: () => Promise.resolve().then(function () { return TMultiGraphPainter$1; }).then(h => h.TMultiGraphPainter), opt: ';ac;l;p;3d;pads', expand_item: 'fGraphs' },
   { name: clTStreamerInfoList, icon: 'img_question', draw: () => import_h().then(h => h.drawStreamerInfo) },
   { name: 'TWebPainting', icon: 'img_graph', class: () => Promise.resolve().then(function () { return TWebPaintingPainter$1; }).then(h => h.TWebPaintingPainter) },
   { name: clTCanvasWebSnapshot, icon: 'img_canvas', draw: () => import_canvas().then(h => h.drawTPadSnapshot) },
   { name: 'TPadWebSnapshot', sameas: clTCanvasWebSnapshot },
   { name: 'kind:Text', icon: 'img_text', func: drawRawText },
   { name: clTObjString, icon: 'img_text', func: drawRawText },
   { name: clTF1, icon: 'img_tf1', class: () => Promise.resolve().then(function () { return TF1Painter$1; }).then(h => h.TF1Painter), opt: ';L;C;FC;FL' },
   { name: clTF12, sameas: clTF1 },
   { name: clTF2, icon: 'img_tf2', class: () => Promise.resolve().then(function () { return TF2Painter$1; }).then(h => h.TF2Painter), opt: ';BOX;ARR;SURF;SURF1;SURF2;SURF4;SURF6;LEGO;LEGO0;LEGO1;LEGO2;LEGO3;LEGO4;same' },
   { name: clTF3, icon: 'img_histo3d', class: () => Promise.resolve().then(function () { return TF3Painter$1; }).then(h => h.TF3Painter), opt: ';SURF' },
   { name: clTSpline3, icon: 'img_tf1', class: () => Promise.resolve().then(function () { return TSplinePainter$1; }).then(h => h.TSplinePainter) },
   { name: 'TSpline5', sameas: clTSpline3 },
   { name: clTEllipse, icon: 'img_graph', draw: () => import_more().then(h => h.drawEllipse), direct: true },
   { name: 'TArc', sameas: clTEllipse },
   { name: 'TCrown', sameas: clTEllipse },
   { name: 'TPie', icon: 'img_graph', draw: () => import_more().then(h => h.drawPie), direct: true },
   { name: 'TPieSlice', icon: 'img_graph', dummy: true },
   { name: 'TExec', icon: 'img_graph', dummy: true },
   { name: clTLine, icon: 'img_graph', class: () => Promise.resolve().then(function () { return TLinePainter$1; }).then(h => h.TLinePainter) },
   { name: 'TArrow', icon: 'img_graph', class: () => Promise.resolve().then(function () { return TArrowPainter$1; }).then(h => h.TArrowPainter) },
   { name: clTPolyLine, icon: 'img_graph', class: () => Promise.resolve().then(function () { return TPolyLinePainter$1; }).then(h => h.TPolyLinePainter), opt: ';F' },
   { name: 'TCurlyLine', sameas: clTPolyLine },
   { name: 'TCurlyArc', sameas: clTPolyLine },
   { name: 'TParallelCoord', icon: 'img_graph', dummy: true },
   { name: clTGaxis, icon: 'img_graph', class: () => Promise.resolve().then(function () { return TGaxisPainter$1; }).then(h => h.TGaxisPainter) },
   { name: clTBox, icon: 'img_graph', class: () => Promise.resolve().then(function () { return TBoxPainter$1; }).then(h => h.TBoxPainter), opt: ';L' },
   { name: 'TWbox', sameas: clTBox },
   { name: 'TSliderBox', sameas: clTBox },
   { name: clTMarker, icon: 'img_graph', draw: () => import_more().then(h => h.drawMarker), direct: true },
   { name: 'TPolyMarker', icon: 'img_graph', draw: () => import_more().then(h => h.drawPolyMarker), direct: true },
   { name: 'TASImage', icon: 'img_mgraph', class: () => Promise.resolve().then(function () { return TASImagePainter$1; }).then(h => h.TASImagePainter), opt: ';z' },
   { name: 'TJSImage', icon: 'img_mgraph', draw: () => import_more().then(h => h.drawJSImage), opt: ';scale;center' },
   { name: clTGeoVolume, icon: 'img_histo3d', class: () => import_geo().then(h => h.TGeoPainter), get_expand: () => import_geo().then(h => h.expandGeoObject), opt: ';more;all;count;projx;projz;wire;no_screen;dflt', ctrl: 'dflt' },
   { name: 'TEveGeoShapeExtract', sameas: clTGeoVolume, opt: ';more;all;count;projx;projz;wire;dflt' },
   { name: nsREX + 'REveGeoShapeExtract', sameas: clTGeoVolume, opt: ';more;all;count;projx;projz;wire;dflt' },
   { name: 'TGeoOverlap', sameas: clTGeoVolume, opt: ';more;all;count;projx;projz;wire;dflt', dflt: 'dflt', ctrl: 'expand' },
   { name: 'TGeoManager', sameas: clTGeoVolume, opt: ';more;all;count;projx;projz;wire;tracks;no_screen;dflt', dflt: 'expand', pm: true, ctrl: 'dflt', noappend: true, expand_after_draw: true },
   { name: 'TGeoVolumeAssembly', sameas: clTGeoVolume, /* icon: 'img_geoassembly', */ opt: ';more;all;count' },
   { name: /^TGeo/, class: () => import_geo().then(h => h.TGeoPainter), get_expand: () => import_geo().then(h => h.expandGeoObject), opt: ';more;all;axis;compa;count;projx;projz;wire;no_screen;dflt', dflt: 'dflt', ctrl: 'expand' },
   { name: 'TAxis3D', icon: 'img_graph', draw: () => import_geo().then(h => h.drawAxis3D), direct: true },
   // these are not draw functions, but provide extra info about correspondent classes
   { name: 'kind:Command', icon: 'img_execute', execute: true },
   { name: 'TFolder', icon: 'img_folder', icon2: 'img_folderopen', noinspect: true, get_expand: () => import_h().then(h => h.folderHierarchy) },
   { name: 'TTask', icon: 'img_task', get_expand: () => import_h().then(h => h.taskHierarchy), for_derived: true },
   { name: clTTree, icon: 'img_tree', get_expand: () => Promise.resolve().then(function () { return tree; }).then(h => h.treeHierarchy), draw: () => import_tree().then(h => h.drawTree), dflt: 'expand', opt: 'player;testio', shift: kInspect, pm: true },
   { name: 'TNtuple', sameas: clTTree },
   { name: 'TNtupleD', sameas: clTTree },
   { name: clTBranchFunc, icon: 'img_leaf_method', draw: () => import_tree().then(h => h.drawTree), opt: ';dump', noinspect: true },
   { name: /^TBranch/, icon: 'img_branch', draw: () => import_tree().then(h => h.drawTree), dflt: 'expand', opt: ';dump', ctrl: 'dump', shift: kInspect, ignore_online: true, always_draw: true },
   { name: /^TLeaf/, icon: 'img_leaf', noexpand: true, draw: () => import_tree().then(h => h.drawTree), opt: ';dump', ctrl: 'dump', ignore_online: true, always_draw: true },
   { name: 'ROOT::RNTuple', icon: 'img_tree', get_expand: () => Promise.resolve().then(function () { return rntuple; }).then(h => h.tupleHierarchy), draw: () => Promise.resolve().then(function () { return RNTuple; }).then(h => h.drawRNTuple), dflt: 'expand', pm: true },
   { name: 'ROOT::RNTupleField', icon: 'img_leaf', draw: () => Promise.resolve().then(function () { return RNTuple; }).then(h => h.drawRNTuple), opt: ';dump', ctrl: 'dump', shift: kInspect, ignore_online: true, always_draw: true },
   { name: clTList, icon: 'img_list', draw: () => import_h().then(h => h.drawList), get_expand: () => import_h().then(h => h.listHierarchy), dflt: 'expand' },
   { name: clTHashList, sameas: clTList },
   { name: clTObjArray, sameas: clTList },
   { name: clTClonesArray, sameas: clTList },
   { name: clTMap, sameas: clTList },
   { name: clTColor, icon: 'img_color' },
   { name: clTFile, icon: 'img_file', noinspect: true },
   { name: 'TMemFile', icon: 'img_file', noinspect: true },
   { name: clTStyle, icon: 'img_question', noexpand: true },
   { name: 'Session', icon: 'img_globe' },
   { name: 'kind:TopFolder', icon: 'img_base' },
   { name: 'kind:Folder', icon: 'img_folder', icon2: 'img_folderopen', noinspect: true },
   { name: nsREX + 'RCanvas', icon: 'img_canvas', class: () => import_v7().then(h => h.RCanvasPainter), opt: '', expand_item: fPrimitives },
   { name: nsREX + 'RCanvasDisplayItem', icon: 'img_canvas', draw: () => import_v7().then(h => h.drawRPadSnapshot), opt: '', expand_item: fPrimitives },
   { name: nsREX + 'RText', icon: 'img_text', draw: () => import_v7('more').then(h => h.drawText), opt: '', direct: 'v7', csstype: 'text' },
   { name: nsREX + 'RFrameTitle', icon: 'img_text', draw: () => import_v7().then(h => h.drawRFrameTitle), opt: '', direct: 'v7', csstype: 'title' },
   { name: nsREX + 'RPaletteDrawable', icon: 'img_text', class: () => import_v7('more').then(h => h.RPalettePainter), opt: '' },
   { name: nsREX + 'RLine', icon: 'img_graph', draw: () => import_v7('more').then(h => h.drawLine), opt: '', direct: 'v7', csstype: 'line' },
   { name: nsREX + 'RBox', icon: 'img_graph', draw: () => import_v7('more').then(h => h.drawBox), opt: '', direct: 'v7', csstype: 'box' },
   { name: nsREX + 'RMarker', icon: 'img_graph', draw: () => import_v7('more').then(h => h.drawMarker), opt: '', direct: 'v7', csstype: 'marker' },
   { name: nsREX + 'RPave', icon: 'img_pavetext', class: () => import_v7('pave').then(h => h.RPavePainter), opt: '' },
   { name: nsREX + 'RLegend', icon: 'img_graph', class: () => import_v7('pave').then(h => h.RLegendPainter), opt: '' },
   { name: nsREX + 'RPaveText', icon: 'img_pavetext', class: () => import_v7('pave').then(h => h.RPaveTextPainter), opt: '' },
   { name: nsREX + 'RFrame', icon: 'img_frame', draw: () => import_v7().then(h => h.drawRFrame), opt: '' },
   { name: nsREX + 'RFont', icon: 'img_text', draw: () => import_v7().then(h => h.drawRFont), opt: '', direct: 'v7', csstype: 'font' },
   { name: nsREX + 'RAxisDrawable', icon: 'img_frame', draw: () => import_v7().then(h => h.drawRAxis), opt: '' },
   { name: nsREX + 'RTreeMapPainter', class: () => Promise.resolve().then(function () { return RTreeMapPainter$1; }).then(h => h.RTreeMapPainter), opt: '' }
], cache: {} };


/** @summary Register draw function for the class
  * @desc List of supported draw options could be provided, separated  with ';'
  * @param {object} args - arguments
  * @param {string|regexp} args.name - class name or regexp pattern
  * @param {function} [args.func] - draw function
  * @param {function} [args.draw] - async function to load draw function
  * @param {function} [args.class] - async function to load painter class with static draw function
  * @param {boolean} [args.direct] - if true, function is just Redraw() method of ObjectPainter
  * @param {string} [args.opt] - list of supported draw options (separated with semicolon) like 'col;scat;'
  * @param {string} [args.icon] - icon name shown for the class in hierarchy browser
  * @param {string} [args.draw_field] - draw only data member from object, like fHistogram
  * @protected */
function addDrawFunc(args) {
   drawFuncs.lst.push(args);
   return args;
}

/** @summary return draw handle for specified item kind
  * @desc kind could be ROOT.TH1I for ROOT classes or just
  * kind string like 'Command' or 'Text'
  * selector can be used to search for draw handle with specified option (string)
  * or just sequence id
  * @private */
function getDrawHandle(kind, selector) {
   if (!isStr(kind))
      return null;
   if (selector === '')
      selector = null;

   let first = null;

   if ((selector === null) && (kind in drawFuncs.cache))
      return drawFuncs.cache[kind];

   const search = getTypeForKind(kind) || `kind:${kind}`;
   let counter = 0;
   for (let i = 0; i < drawFuncs.lst.length; ++i) {
      const h = drawFuncs.lst[i];
      if (isStr(h.name)) {
         if (h.name !== search)
            continue;
      } else if (!search.match(h.name))
         continue;

      if (h.sameas) {
         const hs = getDrawHandle(getKindForType(h.sameas), selector);
         if (hs) {
            for (const key in hs) {
               if (h[key] === undefined)
                  h[key] = hs[key];
            }
            delete h.sameas;
         }
         return h;
      }

      if ((selector === null) || (selector === undefined)) {
         // store found handle in cache, can reuse later
         if (!(kind in drawFuncs.cache))
            drawFuncs.cache[kind] = h;
         return h;
      } else if (isStr(selector)) {
         if (!first)
            first = h;
         // if draw option specified, check it present in the list

         if (selector === '::expand') {
            if (('expand' in h) || ('expand_item' in h))
               return h;
         } else if ('opt' in h) {
            const opts = h.opt.split(';');
            for (let j = 0; j < opts.length; ++j) {
               if (opts[j].toLowerCase() === selector.toLowerCase())
                  return h;
            }
         }
      } else if (selector === counter)
         return h;
      ++counter;
   }

   return first;
}

/** @summary Returns true if handle can be potentially drawn
  * @private */
function canDrawHandle(h) {
   if (isStr(h))
      h = getDrawHandle(h);
   if (!isObject(h))
      return false;
   return (h.func || h.class || h.draw || h.draw_field || h.opt === 'inspect');
}

/** @summary Provide draw settings for specified class or kind
  * @private */
function getDrawSettings(kind, selector) {
   const res = { opts: null, inspect: false, expand: false, draw: false, handle: null };
   if (!isStr(kind))
      return res;
   let isany = false, noinspect = false, canexpand = false;
   if (!isStr(selector))
      selector = '';

   for (let cnt = 0; cnt < 1000; ++cnt) {
      const h = getDrawHandle(kind, cnt);
      if (!h)
         break;
      if (!res.handle)
         res.handle = h;
      if (h.noinspect)
         noinspect = true;
      if (h.noappend)
         res.noappend = true;
      if (h.expand || h.get_expand || h.expand_item || h.can_expand)
         canexpand = true;
      if (!h.func && !h.class && !h.draw)
         break;
      isany = true;
      if (h.opt === undefined)
         continue;
      let opt = h.opt;
      if (isStr(h.opt2))
         opt += h.opt2;
      const opts = opt.split(';');
      for (let i = 0; i < opts.length; ++i) {
         opts[i] = opts[i].toLowerCase();
         if (opts[i].indexOf('same') === 0) {
            res.has_same = true;
            if (selector.indexOf('nosame') >= 0)
               continue;
         }

         if (res.opts === null)
            res.opts = [];
         if (res.opts.indexOf(opts[i]) < 0)
            res.opts.push(opts[i]);
      }
      if (h.theonly)
         break;
   }

   if (selector.indexOf('noinspect') >= 0)
      noinspect = true;

   if (isany && (res.opts === null))
      res.opts = [''];

   // if no any handle found, let inspect ROOT-based objects
   if (!isany && getTypeForKind(kind) && !noinspect)
      res.opts = [];

   if (!noinspect && res.opts)
      res.opts.push(kInspect);

   res.inspect = !noinspect;
   res.expand = canexpand;
   res.draw = Boolean(res.opts);

   return res;
}

/** @summary Set default draw option for provided class
  * @example
  import { setDefaultDrawOpt } from 'https://root.cern/js/latest/modules/draw.mjs';
  setDefaultDrawOpt('TH1', 'text');
  setDefaultDrawOpt('TH2', 'col');  */
function setDefaultDrawOpt(classname, opt) {
   if (!classname)
      return;
   if ((opt === undefined) && isStr(classname) && (classname.indexOf(':') > 0)) {
      // special usage to set list of options like TH2:lego2;TH3:glbox2
      opt.split(';').forEach(part => {
         const arr = part.split(':');
         if (arr.length >= 1)
            setDefaultDrawOpt(arr[0], arr[1] || '');
      });
   } else {
      const handle = getDrawHandle(getKindForType(classname), 0);
      if (handle)
         handle.dflt = opt;
   }
}

/** @summary Draw object in specified HTML element with given draw options.
  * @param {string|object} dom - id of div element to draw or directly DOMElement
  * @param {object} obj - object to draw, object type should be registered before with {@link addDrawFunc}
  * @param {string} opt - draw options separated by space, comma or semicolon
  * @return {Promise} with painter object
  * @public
  * @desc An extensive list of support draw options can be found on [examples page]{@link https://root.cern/js/latest/examples.htm}
  * @example
  * import { openFile } from 'https://root.cern/js/latest/modules/io.mjs';
  * import { draw } from 'https://root.cern/js/latest/modules/draw.mjs';
  * let file = await openFile('https://root.cern/js/files/hsimple.root');
  * let obj = await file.readObject('hpxpy;1');
  * await draw('drawing', obj, 'colz;logx;gridx;gridy'); */
async function draw(dom, obj, opt) {
   if (!isObject(obj))
      return Promise.reject(Error('not an object in draw call'));

   if (isStr(opt) && (opt.indexOf(kInspect) === 0))
      return import_h().then(h => h.drawInspector(dom, obj, opt));

   let handle, type_info;
   if ('_typename' in obj) {
      type_info = 'type ' + obj._typename;
      handle = getDrawHandle(getKindForType(obj._typename), opt);
   } else if ('_kind' in obj) {
      type_info = 'kind ' + obj._kind;
      handle = getDrawHandle(obj._kind, opt);
   } else
      return import_h().then(h => h.drawInspector(dom, obj, opt));

   // this is case of unsupported class, close it normally
   if (!handle)
      return Promise.reject(Error(`Object of ${type_info} cannot be shown with draw`));

   if (handle.dummy)
      return null;

   if (handle.draw_field && obj[handle.draw_field])
      return draw(dom, obj[handle.draw_field], opt || handle.draw_field_opt);

   if (!canDrawHandle(handle)) {
      if (opt && (opt.indexOf('same') >= 0)) {
         const main_painter = getElementMainPainter(dom);

         if (isFunc(main_painter?.performDrop))
            return main_painter.performDrop(obj, '', null, opt);
      }

      return Promise.reject(Error(`Function not specified to draw object ${type_info}`));
   }

   function performDraw() {
      let promise, painter;
      if (handle.direct === 'v7') {
         promise = Promise.resolve().then(function () { return RCanvasPainter$1; }).then(v7h => {
            painter = new v7h.RObjectPainter(dom, obj, opt, handle.csstype);
            painter.redraw = handle.func;
            return v7h.ensureRCanvas(painter, handle.frame || false);
         }).then(() => painter.redraw());
      } else if (handle.direct) {
         painter = new ObjectPainter(dom, obj, opt);
         painter.redraw = handle.func;
         promise = import_canvas().then(v6h => v6h.ensureTCanvas(painter, handle.frame || false))
                                  .then(() => painter.redraw());
      } else
         promise = getPromise(handle.func(dom, obj, opt));

      return promise.then(p => {
         if (!painter)
            painter = p;
         if (painter === false)
            return null;
         if (!painter)
            throw Error(`Fail to draw object ${type_info}`);
         if (isObject(painter) && !painter.options)
            painter.options = { original: opt || '' }; // keep original draw options
         return painter;
      });
   }

   if (isFunc(handle.func))
      return performDraw();

   let promise;

   if (isFunc(handle.class)) {
      // class coded as async function which returns class handle
      // simple extract class and access class.draw method
      promise = handle.class().then(cl => { handle.func = cl.draw; });
   } else if (isFunc(handle.draw)) {
      // draw function without special class
      promise = handle.draw().then(h => { handle.func = h; });
   } else if (!handle.func || !isStr(handle.func))
      return Promise.reject(Error(`Draw function or class not specified to draw ${type_info}`));
   else {
      let func = findFunction(handle.func);
      if (isFunc(func)) {
         handle.func = func;
         return performDraw();
      }
      let modules = null;
      if (isStr(handle.script)) {
         if (handle.script.indexOf('modules:') === 0)
            modules = handle.script.slice(8);
         else if (handle.script.indexOf('.mjs') > 0)
            modules = handle.script;
      }

      if (!modules && !handle.prereq && !handle.script)
         return Promise.reject(Error(`Prerequicities to load ${handle.func} are not specified`));

      let init_promise = Promise.resolve(true);
      if (modules)
         init_promise = loadModules(modules);
      else if (!internals.ignore_v6) {
         init_promise = exports._ensureJSROOT().then(v6 => {
            const pr = handle.prereq ? v6.require(handle.prereq) : Promise.resolve(true);
            return pr.then(() => {
               if (handle.script)
                  return loadScript(handle.script);
            }).then(() => v6._complete_loading());
         });
      }

      promise = init_promise.then(() => {
         func = findFunction(handle.func);
         if (!isFunc(func))
            return Promise.reject(Error(`Fail to find function ${handle.func} after loading ${handle.prereq || handle.script}`));

         handle.func = func;
      });
   }

   return promise.then(() => performDraw());
}

/** @summary Redraw object in specified HTML element with given draw options.
  * @param {string|object} dom - id of div element to draw or directly DOMElement
  * @param {object} obj - object to draw, object type should be registered before with {@link addDrawFunc}
  * @param {string} opt - draw options
  * @return {Promise} with painter object
  * @desc If drawing was not done before, it will be performed with {@link draw}.
  * Otherwise drawing content will be updated
  * @public
  * @example
  * import { openFile } from 'https://root.cern/js/latest/modules/io.mjs';
  * import { draw, redraw } from 'https://root.cern/js/latest/modules/draw.mjs';
  * let file = await openFile('https://root.cern/js/files/hsimple.root');
  * let obj = await file.readObject('hpxpy;1');
  * await draw('drawing', obj, 'colz');
  * let cnt = 0;
  * setInterval(() => {
  *    obj.fTitle = `Next iteration ${cnt++}`;
  *    redraw('drawing', obj, 'colz');
  * }, 1000); */
async function redraw(dom, obj, opt) {
   if (!isObject(obj))
      return Promise.reject(Error('not an object in redraw'));

   const can_painter = getElementCanvPainter(dom);
   let handle, res_painter = null, redraw_res;
   if (obj._typename)
      handle = getDrawHandle(getKindForType(obj._typename));
   if (handle?.draw_field && obj[handle.draw_field])
      obj = obj[handle.draw_field];

   if (can_painter) {
      if (can_painter.matchObjectType(obj._typename)) {
         redraw_res = can_painter.redrawObject(obj, opt);
         if (redraw_res)
            res_painter = can_painter;
      } else {
         can_painter.forEachPainterInPad(painter => {
            if (!res_painter && painter.matchObjectType(obj._typename)) {
               redraw_res = painter.redrawObject(obj, opt);
               if (redraw_res)
                  res_painter = painter;
            }
         }, 'objects');
      }
   } else {
      const top = new BasePainter(dom).getTopPainter();
      // base painter do not have this method, if it there use it
      // it can be object painter here or can be specially introduce method to handling redraw!
      if (isFunc(top?.redrawObject)) {
         redraw_res = top.redrawObject(obj, opt);
         if (redraw_res)
            res_painter = top;
      }
   }

   if (res_painter)
      return getPromise(redraw_res).then(() => res_painter);

   cleanup(dom);

   return draw(dom, obj, opt);
}

/** @summary Create three.js model for object
  * @param {object} obj - object
  * @param {string} opt - draw options
  * @return {Promise} with three.js model */

async function build3d(obj, opt) {
   if (!isObject(obj) || !obj?._typename)
      return Promise.reject(Error('not an object in build3d'));

   const handle = getDrawHandle(getKindForType(obj._typename));
   if (!handle?.class && !handle.build3d)
      return Promise.reject(Error(`not able to create three.js for ${obj._typename}`));

   if (handle.build3d)
      return handle.build3d().then(func => func(obj, opt));

   return handle.class().then(cl => {
      if (!isFunc(cl?.build3d))
         return Promise.reject(Error(`painter class for ${obj._typename} does not implement build3d method`));

      return cl.build3d(obj, opt);
   });
}

/** @summary Scan streamer infos for derived classes
  * @desc Assign draw functions for such derived classes
  * @private */
function addStreamerInfosForPainter(lst) {
   if (!lst)
      return;

   const basics = [clTObject, clTNamed, clTString, 'TCollection', clTAttLine, clTAttFill, clTAttMarker, clTAttText];

   function checkBaseClasses(si, lvl) {
      const element = si.fElements?.arr[0];
      if ((element?.fTypeName !== kBaseClass) || (lvl > 4))
         return null;
      // exclude very basic classes
      if (basics.indexOf(element.fName) >= 0)
         return null;

      let handle = getDrawHandle(getKindForType(element.fName));
      if (handle && !handle.for_derived)
         handle = null;

      // now try find that base class of base in the list
      if (handle === null) {
         for (let k = 0; k < lst.arr.length; ++k) {
            if (lst.arr[k].fName === element.fName) {
               handle = checkBaseClasses(lst.arr[k], lvl + 1);
               break;
            }
         }
      }

      return handle?.for_derived ? handle : null;
   }

   lst.arr.forEach(si => {
      if (getDrawHandle(getKindForType(si.fName)) !== null)
         return;

      const handle = checkBaseClasses(si, 0);
      if (handle) {
         const newhandle = Object.assign({}, handle);
         delete newhandle.for_derived; // should we disable?
         newhandle.name = si.fName;
         addDrawFunc(newhandle);
      }
   });
}

/** @summary Create SVG/PNG/JPEG image for provided object.
  * @desc Function especially useful in Node.js environment to generate images for
  * supported ROOT classes, but also can be used from web browser
  * @param {object} args - function settings
  * @param {object} args.object - object for the drawing
  * @param {string} [args.format = 'svg'] - image format like 'svg' (default), 'png' or 'jpeg'
  * @param {string} [args.option = ''] - draw options
  * @param {number} [args.width = 1200] - image width
  * @param {number} [args.height = 800] - image height
  * @param {boolean} [args.as_buffer = false] - returns image as Buffer instance, can store directly to file
  * @param {boolean} [args.use_canvas_size = false] - if configured used size stored in TCanvas object
  * @return {Promise} with image code - svg as is, png/jpeg as base64 string or buffer (if as_buffer) specified
  * @example
  * // how makeImage can be used in node.js
  * import { openFile, makeImage } from 'jsroot';
  * let file = await openFile('https://root.cern/js/files/hsimple.root');
  * let object = await file.readObject('hpxpy;1');
  * let png64 = await makeImage({ format: 'png', object, option: 'colz', width: 1200, height: 800 });
  * let pngbuf = await makeImage({ format: 'png', as_buffer: true, object, option: 'colz', width: 1200, height: 800 }); */
async function makeImage(args) {
   if (!args)
      args = {};

   if (!isObject(args.object))
      return Promise.reject(Error('No object specified to generate SVG'));
   if (!args.format)
      args.format = 'svg';
   if (!args.width)
      args.width = settings.CanvasWidth;
   if (!args.height)
      args.height = settings.CanvasHeight;

   async function build(main) {
      main.attr('width', args.width).attr('height', args.height)
          .style('width', args.width + 'px').style('height', args.height + 'px')
          .property('_batch_use_canvsize', args.use_canvas_size ?? false)
          .property('_batch_mode', true)
          .property('_batch_format', args.format !== 'svg' ? args.format : null);

      function complete(res) {
         cleanup(main.node());
         main.remove();
         return res;
      }

      return draw(main.node(), args.object, args.option || '').then(() => {
         if (args.format !== 'svg') {
            const only_img = main.select('svg').selectChild('image');
            if (!only_img.empty()) {
               const href = only_img.attr('href');

               if (args.as_buffer) {
                  const p = href.indexOf('base64,'),
                        str = atob_func(href.slice(p + 7)),
                        buf = new ArrayBuffer(str.length),
                        bufView = new Uint8Array(buf);
                  for (let i = 0; i < str.length; i++)
                     bufView[i] = str.charCodeAt(i);
                  return isNodeJs() ? Buffer.from(buf) : buf;
               }
               return href;
            }
         }

         const mainsvg = main.select('svg'),
               style_filter = mainsvg.style('filter');

         mainsvg.attr('xmlns', nsSVG)
                .attr('style', null).attr('class', null).attr('x', null).attr('y', null);

         if (!mainsvg.attr('width') && !mainsvg.attr('height'))
            mainsvg.attr('width', args.width).attr('height', args.height);

         if (style_filter)
            mainsvg.style('filter', style_filter);

         function clear_element() {
            const elem = select(this);
            if (elem.style('display') === 'none')
               elem.remove();
         }

         main.selectAll('g.root_frame').each(clear_element);
         main.selectAll('svg').each(clear_element);

         let svg;
         if (args.format === 'pdf')
            svg = { node: mainsvg.node(), width: args.width, height: args.height, can_modify: true };
         else {
            svg = compressSVG(main.html());
            if (args.format === 'svg')
               return complete(svg);
         }

         return svgToImage(svg, args.format, args).then(complete);
      });
   }

   return isNodeJs()
          ? _loadJSDOM().then(handle => build(handle.body.append('div')))
          : build(select('body').append('div').style('display', 'none'));
}


/** @summary Create SVG image for provided object.
  * @desc Function especially useful in Node.js environment to generate images for
  * supported ROOT classes
  * @param {object} args - function settings
  * @param {object} args.object - object for the drawing
  * @param {string} [args.option] - draw options
  * @param {number} [args.width = 1200] - image width
  * @param {number} [args.height = 800] - image height
  * @param {boolean} [args.use_canvas_size = false] - if configured used size stored in TCanvas object
  * @return {Promise} with svg code
  * @example
  * // how makeSVG can be used in node.js
  * import { openFile, makeSVG } from 'jsroot';
  * let file = await openFile('https://root.cern/js/files/hsimple.root');
  * let object = await file.readObject('hpxpy;1');
  * let svg = await makeSVG({ object, option: 'lego2,pal50', width: 1200, height: 800 }); */
async function makeSVG(args) {
   if (!args)
      args = {};
   args.format = 'svg';
   return makeImage(args);
}

function assignPadPainterDraw(PadPainterClass) {
   PadPainterClass.prototype.drawObject = draw;
   PadPainterClass.prototype.getObjectDrawSettings = getDrawSettings;
}

// only now one can draw primitives in the canvas
assignPadPainterDraw(TPadPainter);

import_geo = async function() {
   return Promise.resolve().then(function () { return TGeoPainter$1; }).then(geo => {
      const handle = getDrawHandle(getKindForType('TGeoVolumeAssembly'));
      if (handle)
         handle.icon = 'img_geoassembly';
      return geo;
   });
};

// load v7 only on demand
import_v7 = async function(arg) {
   return Promise.resolve().then(function () { return RCanvasPainter$1; }).then(h => {
      // only now one can draw primitives in the canvas
      assignPadPainterDraw(h.RPadPainter);
      switch (arg) {
         case 'more': return Promise.resolve().then(function () { return v7more; });
         case 'pave': return Promise.resolve().then(function () { return RPavePainter$1; });
      }
      return h;
   });
};

// to avoid cross-dependency between modules
Object.assign(internals, { addStreamerInfosForPainter, addDrawFunc, setDefaultDrawOpt, makePDF });

Object.assign(internals.jsroot, { draw, redraw, makeSVG, makeImage, addDrawFunc });

const kTopFolder = 'TopFolder', kExpand = 'expand', kPM = 'plusminus', kDfltDrawOpt = '__default_draw_option__',
      cssValueNum = 'h_value_num', cssButton = 'h_button', cssItem = 'h_item', cssTree = 'h_tree';

function injectHStyle(node) {
   function img(name, sz, fmt, code) {
      return `.jsroot .img_${name} { display: inline-block; height: ${sz}px; width: ${sz}px; background-image: url("data:image/${fmt};base64,${code}"); }`;
   }

   const bkgr_color = settings.DarkMode ? 'black' : '#E6E6FA',
         border_color = settings.DarkMode ? 'green' : 'black',
         shadow_color = settings.DarkMode ? '#555' : '#aaa';

   injectStyle(`
.jsroot .${cssTree} { display: block; white-space: nowrap; }
.jsroot .${cssTree} * { padding: 0; margin: 0; font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; box-sizing: content-box; line-height: 14px }
.jsroot .${cssTree} img { border: 0px; vertical-align: middle; }
.jsroot .${cssTree} a { text-decoration: none; vertical-align: top; white-space: nowrap; padding: 1px 2px 0px 2px; display: inline-block; margin: 0; }
.jsroot .${cssTree} p { font-weight: bold; white-space: nowrap; text-decoration: none; vertical-align: top; white-space: nowrap; padding: 1px 2px 0px 2px; display: inline-block; margin: 0; }
.jsroot .h_value_str { color: green; }
.jsroot .${cssValueNum} { color: blue; }
.jsroot .h_line { height: 18px; display: block; }
.jsroot .${cssButton} { cursor: pointer; color: blue; text-decoration: underline; }
.jsroot .${cssItem} { cursor: pointer; user-select: none; }
.jsroot .${cssItem}:hover { text-decoration: underline; }
.jsroot .h_childs { overflow: hidden; display: block; }
.jsroot_fastcmd_btn { height: 32px; width: 32px; display: inline-block; margin: 2px; padding: 2px; background-position: left 2px top 2px;
                      background-repeat: no-repeat; background-size: 24px 24px; border-color: inherit; }
.jsroot_inspector { border: 1px solid ${border_color}; box-shadow: 1px 1px 2px 2px ${shadow_color}; opacity: 0.95; background-color: ${bkgr_color}; }
.jsroot_drag_area { background-color: #007fff; }
${img('minus', 18, 'gif', 'R0lGODlhEgASAJEDAIKCgoCAgAAAAP///yH5BAEAAAMALAAAAAASABIAAAInnD+By+2rnpyhWvsizE0zf4CIIpRlgiqaiDosa7zZdU22A9y6u98FADs=')}
${img('minusbottom', 18, 'gif', 'R0lGODlhEgASAJECAICAgAAAAP///wAAACH5BAEAAAIALAAAAAASABIAAAImlC+Ay+2rnpygWvsizE0zf4CIEpRlgiqaiDosa7zZdU32jed6XgAAOw==')}
${img('plus', 18, 'gif', 'R0lGODlhEgASAJECAICAgAAAAP///wAAACH5BAEAAAIALAAAAAASABIAAAIqlC+Ay+2rnpygWvsizCcczWieAW7BeSaqookfZ4yqU5LZdU06vfe8rysAADs=')}
${img('plusbottom', 18, 'gif', 'R0lGODlhEgASAJECAICAgAAAAP///wAAACH5BAEAAAIALAAAAAASABIAAAIplC+Ay+2rnpygWvsizCcczWieAW7BeSaqookfZ4yqU5LZdU36zvd+XwAAOw==')}
${img('empty', 18, 'gif', 'R0lGODlhEgASAJEAAAAAAP///4CAgP///yH5BAEAAAMALAAAAAASABIAAAIPnI+py+0Po5y02ouz3pwXADs=')}
${img('line', 18, 'gif', 'R0lGODlhEgASAIABAICAgP///yH5BAEAAAEALAAAAAASABIAAAIZjB+Ay+2rnpwo0uss3kfz7X1XKE5k+ZxoAQA7')}
${img('join', 18, 'gif', 'R0lGODlhEgASAIABAICAgP///yH5BAEAAAEALAAAAAASABIAAAIcjB+Ay+2rnpwo0uss3kf5BGocNJZiSZ2opK5BAQA7')}
${img('joinbottom', 18, 'gif', 'R0lGODlhEgASAIABAICAgP///yH5BAEAAAEALAAAAAASABIAAAIZjB+Ay+2rnpwo0uss3kf5BGrcSJbmiaZGAQA7')}
${img('base', 18, 'gif', 'R0lGODlhEwASAPcAAPv6/Pn4+mnN/4zf/764x2vO//Dv84HZ/5jl/0ZGmfTz9vLy8lHB/+zr70u+/7S03IODtd7d6c/P0ndqiq/w/4Pb/5SKo/Py9fPy9tTU121kjd/f4MzM062tx5+zy5rO67GwxNDM14d8mJzn/7awwry713zX/9bW27u71lFRmW5uoZ+fxjOy/zm1/9HQ2o3g/2xfgZeMplav7sn9/6Cgv37X/6Dp/3jU/2uJ2M7J1JC63vn5+v38/d7e38PD0Z7o/9LR4LS01cPDzPb1+Nzb5IJ2lHCEv5bk/53C3MrJ3X56t+np6YF7o3JsndTU5Wtgh5GHoKaesuLi4mrO/19RdnnV/4WBqF5QdWPK/4+PvW5uu4+PuuHh4q7w/97e68C9z63w/9PT0+zs7FtbmWVXerS0yaqitpuSqWVlpcL6/8jD0H/C9mVajqWu3nFwpYqHtFfE/42DnaWl0bTz/5OPt+7u7tra5Y+Yz+Tk56fM6Gek5pG50LGpvOHh72LJ/9XU5lbD/6GnwHpujfDu8mxpntzb45qav7PH41+n6JeXyUZGopyYsWeGyDu2/6LQ44re/1yV41TD/8LC1zix/sS/zdTU4Y+gsd/c5L7z+a6uzE+3+XG89L6+087O1sTD3K2twoGBtWVbgomo4P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKMALAAAAAATABIAAAjtAEcJFLgDTyE7SVCsAAJgoMNRYTII8fEpkAckOpiEaPhwlARLexxhmpEGzJEmBAJ0HMXhw6MfXeZQsDHADZ8hK13kMTEAwQgEL2oYiaJgJZFDU24cqHCgSgFGFgysBJAJkB8BBQRggQNJxKCVo0rIcMAgEgMHmnBMaADWEyIWLRptEqWETRG2K//ombSmjRZFoaCo4djRyZ0HchIlSECIRNGVXur0WcAlCJoUoOhcAltpyQIxPSRtGQPhjRkMKyN0krLhBCcaKrJoOCO1I48vi0CU6WDIyhNBKcEGyBEDBpUrZOJQugC2ufPnDwMCADs=')}
${img('folder', 18, 'gif', 'R0lGODlhEgASANUAAPv7++/v79u3UsyZNOTk5MHBwaNxC8KPKre3t55sBrqHIpxqBMmWMb2KJbOBG5lnAdu3cbWCHaBuCMuYM///urB+GMWSLad1D8eUL6ampqVzDbeEH6t5E8iVMMCNKMbGxq58FppoAqh2EKx6FP/Ub//4k+vr6///nP/bdf/kf//viba2tv//////mQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAC4ALAAAAAASABIAAAaRQJdwSCwaj8ik0jUYTBidAEA5YFkplANhehxABGAwpKHYRByVwHBibbvbo8+Q0TrZ7/jWBTHEtP6AgX8GK0MWLSWJiostEoVCBy0qk5SVLQmPLh4tKZ2eny0LmQ0tKKanqC0hmQotJK+wsS0PfEIBZxUgHCIaBhIJCw8ZBUMABAUrycrLBQREAAEm0tPUUktKQQA7')}
${img('folderopen', 18, 'gif', 'R0lGODlhEgASANUAAO/v76VzDfv7+8yZNMHBweTk5JpoAqBuCMuYM8mWMZ5sBpxqBPr7/Le3t///pcaaGvDker2KJc+iJqd1D7B+GOKzQ8KPKqJwCrOBG7WCHbeEH9e4QNq/bP/rhJlnAffwiaampuLBUMmgIf3VcKRyDP/XhLqHIqNxC8iVMMbGxqx6FP/kf//bdf/vievr67a2tv/4k8aaGf//nP//mf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAADUALAAAAAASABIAAAaVwJpwSCwaj8ikUjgYIBIogEA5oFkZDEtheqzKvl9axKTJYCiAIYIGblutqtQwQYPZ73jZpCGM+f+AfiEdJy99M21tMxwxJQeGNTGIeHcyHzEjCpAAki2en54OIhULkAKSMiuqqysOGxIGkDWcMyy2t7YQDx58QqcBwMAkFwcKCwYgBEQFBC/Oz9AEBUUALtbX2FJLSUEAOw==')}
${img('page', 18, 'gif', 'R0lGODlhEgASAOYAAPv7++/v7/j7/+32/8HBweTk5P39/djr/8Df//7///P5/8Ph//T09fn5+YGVw2t0pc7n/15hkFWn7ZOq0nqDsMDA/9nh7YSbyoqo2eTx/5G46pK873N+sPX6//f395Cjy83m/7rd/9jl9m13qGVqmoeh0n+OvI+z5Yyu387T//b6/2dtnvz9/32JtpS/8sbGxv7+/tvn92lwom96rHJ8rnSAsoep3NHp/8nk/7e3t+vr67a2tun1/3V4o+Hw/9vt/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAEEALAAAAAASABIAAAejgEGCg4SFhoeILjaLjDY1AQCHG0AGAA0eDBY1E5CGGjBAoQkCMTUSHwGGJwaiAh0iNbEvhiihAgIDPDwpFRw5hhgsuLk8Pz8HNL+FJSoKuT4+xzczyoQXzjzQxjcgI9WDDrraPzc4OA/fgibZ0eTmCzLpQS0Z7TflCwgr8hT2EOYIQpCQ16OgwYMRCBgqQGCHw4cOCRQwBCCAjosYL3ZCxNFQIAA7')}
${img('question', 18, 'gif', 'R0lGODlhEgASAPelAOP0//7//9bs//n///j//9Ls/8Pn//r//6rB1t3f5crO2N7g5k1livT4+7PW9dXt/+v4/+Xl5LHW9Ov6/+j1/6CyxrfCz9rd5Nzj6un1/Z6ouwcvj8HBzO7+/+3//+Ln7BUuXNHv/6K4y+/9/wEBZvX08snn/19qhufs8fP7/87n/+/t7czr/5q1yk55q97v/3Cfztnu//z//+X6/ypIdMHY7rPc/7fX9cbl/9/h52WHr2yKrd/0/9fw/4KTs9rm75Svzb2+ya690pu92mWJrcT3//H//+Dv/Xym35S216Ouwsvt/3N/mMnZ5gEBcMnq/wEBXs/o/wEBetzw/zdYpTdZpsvP2ClGml2N3b3H0Nzu/2Z2lF1ricrl/93w/97h6JqluktojM/u/+/z9g8pVff4+ebu9q+1xa6/zzdFaIiXr5Wyz0xslrTK4uL//2uIp11rh8Xj/NXn+Oz2/9bf6bG2xAEBePP//1xwkK/K5Nbr/8fp/2OBtG53kai3ykVCYwEBde/6/7O4xabI+fD//+by/x8+jDhZpM/q/6jK58nO19ny/7jV7ZO42NHr/9H4/2ZwimSV6VBxwMDX7Nvf5hYwX5m20sfb6Ieqyk9Yjr/k/cPM2NDp/+/098Tl9yQ9jLfW+Mne8sjU30JklP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKUALAAAAAASABIAAAjxAEsJHEiwoMEyGMaQWthg0xeDAlGUWKjoz5mFAegY/LBiIalMUK54JCWEoJkIpA6kSDmoAykKgRaqGSiq04A5A5r4AKOEAAAtE2S0USAwSwYIhUb8METiUwAvemLMCMVEoIUjAF5MIYXAThUCDzgVWDQJjkA0cngIEHAHCCAqRqJ0QeQoDxeBFS71KKDCwxonhwiZwPEkzo4+AimJqBFCjBs+UjZ4WmLgxhAQVgb6acGIBShJkbAgMSAhCQ1IBTW8sZRI055HDhoRqXQCYo4tDMJgsqGDTJo6EAlyYFNkVJDgBgXBcJEAucEFeC44n04wIAA7')}
${img('histo1d', 16, 'gif', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAADAFBMVEW9vb2np6empqanpqenpqivr6//AAD3+fn09vb19vf3+Pv8+v//+//29/v3+fr19vbZ3Nza3d7X0+Lb3t7b3N3AwMP2+PimpqXe4+Th6uvQ0dTi6uzg5ebFx8nt6vb////r5/T2+fnl4e3a3uDN0NT7/P6lpqX3+vvn9vhcVVHu+//W1uH48//29P///f+mpqelpqb4/v/t/f9oY2H6///59v/x8fXw9fny9/78/v+lpqf7//9iXl12dHPW2t/R1tdtaGbT2dpoZmT6/v9ycnKCgoJpZGJ6dnT3///2///0//95entpa2t+gIKLjI55d3aDgYBvcXL1+/z9/v6lpaWGiIt7fH6Ji42SlJeEhIZubGyMjI17fYD+//+kpKSmpaaRk5WIioyRk5aYmp2OkJJ+f4KTlZilpKWcnqGVl5qcnqCfoaOYmp6PkZOdn6GsrrGoqq6qrK+rrbGpq66lp6uqrbCoqq20tLSsrKzc3NzMzMzPz88AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB6enrU4/9iYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmLU4/9KSkoAAAAAAAAAAAB6enrU4//m5uZiYmLm5uZiYmLm5uZiYmLm5uZiYmLm5ubU4/9KSkoAAAAAAAAAAAB6enrU4/9KSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkrU4/9KSkoAAAAAAAAAAABubm7U4//U4//U4//U4//U4//U4//U4//U4//U4//U4//U4/9KSkoAAAAAAAAAAABubm5KSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABt6dBwBYjWHVG2AAAAB3RSTlP///////8AGksDRgAAAAlwSFlzAAALEgAACxIB0t1+/AAAAOxJREFUeNpjYGBkggBmFmYmRlY2BkZ2DhDg5OLm4eblY2RjYOIXEBQSFhEFkgKCYkxsDOKcEpJS0jKycvJS8gpcIAFFJWUVGFIFCqipa8hrymtpy+sI6crr6bMxGBgayRvLm8iamkmZW1gCBayslWxs7ewd7OwdlZStrYC2ODm7uLrJu3t4usl7mRiwMeh7+/j6+VsHBMr7+wQFhwAFQsPCIyKjomOiIsOiYuPYGOITEpOSU1LTElNTElPlgQLpGZlZ2Tm5eZm5OZm5IAGm/ILCouKS0rKS4oISeaDDypniEICpgo2hsgoZVLMBAHIaNxuoIXy2AAAAAElFTkSuQmCC')}
${img('histo2d', 16, 'gif', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsSAAALEgHS3X78AAABUUlEQVR42o1R0U7CQBDkU/w/v8qk1/OhCS+88miMiQYMBMNRTqiisQiRhF6h13adsuVKDEYvm81kdmdv9q7V7XallP65I30UrpErLGW73SaiFtDF5dXWmNNITJrubJ4RWUI2qU33GTorAdSJMeMwhOxpEE20noRTYISaajBcMrsdOlkgME+/vILtPw6j+BPg5vZuFRuUgZGX71tc2AjALuYrpWcP/WE1+ADAADMAY/OyFghfpJnlSTCAvLb1YDbJmArC5izwQa0K4g5EdgSbTQKTX8keOC8bgXSWAEbqmbs5BmPF3iyR8I+vdNrhIj3ewzdnlaBeWroCDHBZxWtm9DzaEyU2L8pSCNEI+N76+fVs8rE8fbeRUiWR53kHgWgs6cXbD2OOIScQnji7g7OE2UVZNILflnbrulx/XKfTAfL+OugJgqAShGF4/7/T6/Ug+AYZrx7y3UV8agAAAABJRU5ErkJggg==')}
${img('histo3d', 16, 'gif', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAADAFBMVEX////48OjIgTrHfjjKgTr78+yixH9HiQBHiACiw37jvJXfpVP6wzT7zTn7yj3lp1qOhyJzvgCa3wCa3gB2ugBinQ6Pt2D4+vfOjEr96p3986z83mT99rD99a3WhEvC0kaU3gCV3ADG71zo/KORzw1gowBonS3Z5snHfTb6uyD6tzD+/Nb7z0/70D3KdTXI1l3h+qTi+KXD7luU3ACY3gCc4QCi3g1QjwXHfjr710T6xi/+9sn70UH73E/MdDqhvQCi1BKkug2XxACU1wCS2ADD51rr9aJXkw/MpYDgpkb71U7+9MP7007hnEO3niOj0hGq3SCZtQCbtQCjtwj//+7F4Vui0wBDhgDk5eTMxcGxfi3TfTq+fyPPz4ak3xux5TG87kmZuwCZvACWtgDf8a+c0gCy3yNLiwD7/Ps1iwCiyAPF3F7j7bG67EW77kmq5yWYzwCZwwCTugDc8KTE51ve9YZCigCgwgCVuQDa5p7U9YSq4yWT2gCV2wCT2wCp2h/y+9HC6lW87DlChQBGigCixgCYvgDK3nyXvgC72UjG7mSj3xXL7XDK7W7b9J+36TrG9lBDhQBHigClywCbxQDJ33SXvwCYvQCcwADq+8S77Ei460Hd+KDD9VHU/2VEhgBdlR1rowCXwwDK4W6bxgCaxQCVvQDp/L+/8k7F91fn/6zC9V18tiNbkx/U1dSyv6RglihnoQCYwwChyQDs/7/P/2fE92F5tCBdkib19vXW1taoupVLiwNooQCWwADo/7h5tSBFhgaouZXx8vHOz86ftYVJiQBNjQKetIXt7u3Nzs0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBfAAAAAAAAAA2tmA2tmAAACQAAAAAAAAAAAAAAAAAAAAAATgAABNBfMAAAAAAAAA2tpQ2tpQAACQAAAAAAAAAAAAAAAAAAAAAAdQAABNBfMAAAAAAAAA2tsg2tsgAACQAAAAAAAAAAAAAAAAAAAAAAggAABNBfMCaVmCSAAAAAXRSTlMAQObYZgAAAAlwSFlzAAALEgAACxIB0t1+/AAAAQVJREFUeNpjYGBkYmZhZWBj5+BkAAMubh5ePn4BQSFhEVExcaCAhKSUtIysnLyCopKyiqqaOoOGppa2jq6evoGhkbGJqZk5g4WllbWNrZ29g6OTs4urmzuDh6eXt4+vn39AYFBwSGhYOENEZFR0TGxcfEJiUnJKalo6A0NGZlZ2Tm5efkFhUXFJqTnQnrLyisqq6prauvqGxqZmoEBLa1t7R2dXd09vX/+EiUCBSZOnTJ02fcbMWbPnzJ03HyiwYOGixUuWLlu+YuWq1WvWAgXWrd+wcdPmTVu2btu+Y/06kHd27tq9Z+++/QcOHtq1E+JBhsNHjh47fuLIYQYEOHnq1EkwAwCuO1brXBOTOwAAAABJRU5ErkJggg==')}
${img('graph', 16, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4AEFCgohaz8VogAAAT9JREFUOMulkz1LQlEYx39XmrIhcLa5i4mD4JBQrtHieDbb+gx3dbl9hca7tLi4VOsRMkKQVO7LLAQNNdSQgyJPg903tDT8w4HzPDznd56Xc1BKCVsokzTGjhPBXDcQAAEZDgPZCHDQaESH5/PYXyqZxp8A349vGHkjOXo3uXtp035sy79KABi8DQCwshb7x3U6gIYU6KNej+1kEwUEjbQeWtIb9mTsOCIgN1eXgiYd96OdcKNBOoCuQc47pFgoGmHw7skZTK9X16CUku5zV9BIkhz4vgSuG/nsWzvKIhmXAah+VpfJsxnGZMKkUln05NwykqOORq6UWkn+TRokXFEG/Vx/45c3fbrnFKjpRVkZgHKxbAC8NptrAfm9PAD2l42VtdJjDDwv2CSLpSaGMgsFc91hpdRFKtNtf6OxLeAbVYSb7ipFh3AAAAAASUVORK5CYII=')}
${img('mgraph', 16, 'gif', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAADAFBMVEW9vb2np6empqanpqenpqivr68AAAD3+fn09vb19vf3+Pv8+v//+//29/v3+fr19vbZ3Nza3d6/wcLb3t7b3N3AwMPi4et2oz0yfwDh3+n2+PimpqXe4+Th6uvD0NHi6uzg5ebFx8nt6vY2ggDs/881gQDr5/T2+fnFz9DDZVrAIhDEZVvJ0tTN0NTX0+IvZAA4hAAuYgDT0N77/P6lpqX3+vvn9vi/JRL81cHBJhTu+//W1uEkXgD48//29P8fWwD//f+mpqelpqb4/v/t/f+yCwDBKBi3CgD6//8kYAD59v/x8fXQ0dTw9fny9/78/v+lpqf7//+wAADV5ezZ5e7g6PQjZQDf4+/W2t/R1tfT2drT3+OvAAD9///6/v/////k4vIiXwC1AAD3///2///X6Oz0//9+rUgzfwAwdADa6u6xCwDAJxb5///1+/z9/v6lpaUwfADo/8vl4e3a3uDb6eu+IxL808C+IhDZ5+nW2tr+//+kpKSmpaaArUgvewB1oj39/v/e5ebVd227HgvJa2H8///6/PylpKXn4+ze4eLg5+j9/v20tLSsrKzc3NzMzMzPz88AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAPAAAAAAEAAAEAAABzL1z/CSMAAAAAAAAAAAAAAAMAAAAmCTsAAAAAAAAAAAAAAAAAAAQAAQEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7op0gAAAAB3RSTlP///////8AGksDRgAAAAlwSFlzAAALEgAACxIB0t1+/AAAAOhJREFUeNpjYGBkggBmFmYmRlY2BkZ2DhDg5OLm4eblY2RjYOIXEBQSFhEVE5cQl5RiYmOQ5pSRlZNXUFRSVlFV4wIJqGtoamnr6OrpGxgaGQMFTEzNzC0sraxtbPXs7B0c2RicnF1c3dw9PL28fXz9/IECAYFBwSGhYeERkVHRMYEBQFti4+ITEuOTklNSg9I8nNgYHOPTMzLjA7Oyc7Jz8/ILQAKFRRnFJaVl5RWVVdU1bAy18XX1DfGNTc0trW3t8UCBjvj4+M746q74+O7qHpAAUzwyADqsl6kGAZj62Bj6JyCDiWwAyPNF46u5fYIAAAAASUVORK5CYII=')}
${img('tree', 16, 'gif', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAQAAAAEABcxq3DAAACjklEQVQ4y4WTy49LcRzFP+2tzmVUr9dIDJOWGGVBicgEyTQTCzIetUFssDKJSFhY2SARCYvBbGrj8QcIkYglk8xCmEQ9xqNexbQVY2Zub3un9/W7PwstHZH4Jie/7+Kc8/suzgnwr+kjBqSBbm2lkm6bHyH3XM9SZQ8Z8s3UQJPo0IJVof5EZ7v2faxMrKONlhmQWN5GSFEwLbhybjBPhDwVsmQ4AaA09Mou+k8d702EAzXiS6KEgzahoIthGOi6DtKlN71GS+/cEPs0WewaX2R9ZphssP776UhESY0WSpQNg7Jh4Anx+zgJVKpV3uZyvHjzir27NwGs/XVBH8c7N2nnjx7eSqlYxPM8JCCkxBU+rhA4dVhCYJgmyc4Ej96/7rLi8nNAPc/k2ZNp7cnTpziuiy8lvpSI+tvYhS/xpY8vJXMiEbZv3MzFq3cJqaqiPX72jnKt9kfQRPZ9f5qZ70sMawyAas1GseIy1rNtVXK8Mkm1VsP2PBzhYQuB5Qns+t6AJQSqqlIcrTAy+ONGENBWLF3MN71MxXGo1mE6DqbrYLou8z/a7L3uMKvgUnU8xk2T3u71ADGFDdgvCx/3TwkLEfKxhWDHbY+eYZ+Obz6tJcmRApRsuJ8Ex4Po7Jl8/TDBl7flm4Gm5F1vSZKaFQUh4cB9OLgaDB3UVrjwA+6tBnKAis4El8lwujmJSVQeoKAxFzqDcG0KWhZC6R30tUJRQD3Odxqy4G+DDFks4pisY5RLgRx5pZ5T4cKy95yhSrxZDBCaVqIMOpAd2EIeSEW7wLQh3Ar7RtCHbk0v0vQy1WdgCymgf147Sa0dhAOVMZgoALDu2BDZ/xloQAzQgIOhMCnPYQ+gHRvi4d/8n00kYDRVLifLAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDEwLTAyLTExVDE0OjUxOjE3LTA2OjAwHh/NoQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAwNC0wOS0yMFQxNzoxMDoyNi0wNTowMCcJijsAAAAASUVORK5CYII=')}
${img('branch', 16, 'gif', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAADAFBMVEX///99plFAfADL27hpmyfP8YxyoilSiRiv0XGGygK02VtRiBmVwjh8xQCcziFZkhLz9+9BfQB2rwaCyACRygFQigXw9Ox0mkpXkQCJzwBblgBmkzP8/fxEgQBCfwBEgQejwITe3t5hkC1CfgBfjynZ2tmSq3eArDu72oNvoDJajyTY2dhFgQDCzLqhvn9EgAazx55XkwCVzC2824GMs1J0oUTY48xajiK72YR9qj2Tq3dhkix+th99xAB3uADA3oQ+fABEgABIgwW82oOUyi5VkgCf0CaEygB+wwCbzjN1mkrA3YZ1tAB7wAB+uB1vl0JdmgCJwwCKzwBoqAB4nVBikiuayzZ8wQCFywCg0Sjd3t1lkjFBfABLgwhKgwlmpgCK0QCJxQBclwDMzMzPz89GggCDpFxDfgCIpmPl5eUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABhAABQEABuZQBjYQBvcgAIZABiYQBlZAAABQDU/wCx/wCO/wBr/wBI/wAl/wAA/wAA3AAAuQAAlgAAcwAAUADU/wCx/wCO/wBr/wBI/wAl/wAA/gAA3AAAuQAAlgAAcwAAUADj/wDH/wCr/wCP/wBz/wBX/wBV/wBJ3AA9uQAxlgAlcwAZUADw/wDi/wDU/wDG/wC4/wCq/wCq/wCS3AB6uQBilgBKcwAyUAD//wD//wD//wD//wD//wD//wD+/gDc3AC5uQCWlgBzcwBQUAD/8AD/4gD/1AD/xgD/uAD/qgD/qgDckgC5egCWYgBzSgBQMgD/4wD/xwD/qwD/jwD/cwD/VwD/VQDcSQC5PQCWMQBzJQBQGQD/1AD/sQD/jgD/awD/SAD/JQD+AADcAAC5AACWAABzAABQAAD/1AD/sQD/jgD/awD/SAD/JQD/AADcAACwULzWAAAAAXRSTlMAQObYZgAAAAlwSFlzAAALEgAACxIB0t1+/AAAALZJREFUeNpjYAADRiZGBmTAzMLKxowswM7BycWNLMDEw8vHL4AkICgkLCIqhiQgLiEpJS0D5cjKySsoKimrqMJk1dQ1NLW0dXQZ9PTlZEECBoZGxiamOmbmmhaWViABaxtbO3sHRycTZxdXA7ANbu4enkxeDt4+vn7WIAH/gMCg4JBQprDwiEhBkEBUtGBMrI5OXHxCYpI/2BrV5OSU5NS09BjB6CiE01JTM5KTVZHcmpycCWEAANfrHJleKislAAAAAElFTkSuQmCC')}
${img('leaf', 16, 'gif', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAADAFBMVEX////M27mQs2tilDA9eQA7egBbkhVTjAxJgwWBqVdGgQBrnySdxViu0WrE4oaYv2PC35NtoCqxvaSevX5FgAB7qje73nDK6neu109vpyVupCGo2kJ9xwBQhBtilC9pnx7G63PM6olgnAB/vQBDigCVv0yb1CaDzAB8uBJwmkNnnBnB52ui2Ca94WZopAE/hgCtz2ue2CmDywCByACKujtdjyqdvHpdlhLV9YdkowCFxwCw1lFXmAJvpC5jng1coABlpwBprAB8sitAfABDfgKx31Gr3TuCsi5sqABtqgBUkxTV85zL7I213mef0j+OxyKk00k/ewCp3TCSyhCw0mRRjQC23HmU0h55wQB5vQB4uQB1tgCIwBeJxgCBvQDC3ndCjACYx1204Fx6wwB7vQB1tABzsQBBfQBpkzdtpQB9tQA/iQCMu1SMukNUlQBYmQBsqAd4rh11rwZyrQBvqgBDfwCqvZVWkQBUnACp0Hq/43K733C+4X+w12eZyT2IvSN5sgpZkwBxmUSDqFlbnACJzQy742p/wwB2ugBysgBwrwBvqwBwqQBhmgBCfwDV2NN8pk1foACO1QBZmABRkABpqwB3uQB0sgB0rgBnogBUjgC7w7NymkFdnQBUhxmis41okjdCfgBGgQWHpWPMzMzb3NtumD5NhQzT09Pv8O/a2trOz87l5eXc3NzPz88AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtHAA4HXQAAEgAAB9CTigAAABCfCQ4HTxy6Kw4HXRy+8xy+8wAAMwAAAAAAAAAAAAAAAAAAAAAAAgAAAgAABgYAAG7AAAAACgAAAgAAAgYAAEAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4Hnw4HnwAAFRpRiYmO2V0aWRtSSY7ZWdsZVNpdGNBO251amRGO3R0bCYmO3J3ZWlvVCY7c2xuaVc7d28ABCwBG8q3AAAAAXRSTlMAQObYZgAAAAlwSFlzAAALEgAACxIB0t1+/AAAAOtJREFUeNpjYIACRiZmFlY2dg4ol5OLm4eXj19AUAjMFRYRFROXkJSSlpEF8+XkFRSVlFVU1dQ1NMF8LW0dXT19A0MjYxNTIN/M3MLSytrG1s7ewdHJGSjg4urm7uHp5e3j6+cfABIIDAoOCVUJC4+IjIqOAQk4x8bFJyQmJadEpaalpQMFMjKzsnNy8/ILCouKS0qBAmXlFZVV1TW1dfUNJY1NQIHmlta29o7ozq7unt6+fgaGCRMnTZ4ydVrU9BkzZ5XOBiqYM3HuvPkL0tPTFy5avATkzqXLlq9YuWoJEKxeA/Ho2nUMyAAA9OtDOfv2TiUAAAAASUVORK5CYII=')}
${img('leaf_method', 16, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAKlBMVEUAAAAAgADzExMAgIAAAADAwMCAgADxGRnuFxLnHhHuIyPKJQ/rLi7////aW8ZOAAAAAXRSTlMAQObYZgAAAAFiS0dEDfa0YfUAAAAHdElNRQfgCxIPFR/msbP7AAAAaUlEQVQI12NggANBBiYFMMNQxAjCYA4UUoZIBRpBGMyiQorGIIaxWRCEwSYo3igiCNJlaLkwGSwkJn1QGMhgNDQ0TDU2dACqERYTDksGG5SkmGoApBnFhBRTBUAiaYJpDIJgs10cGBgdACxbDamu76Z5AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE3LTAxLTE3VDA5OjMwOjM1KzAxOjAwyGHxKQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNi0xMS0xOFQxNToyMTozMSswMTowMJgvuUkAAAAASUVORK5CYII=')}
${img('globe', 18, 'gif', 'R0lGODlhEwASAPcAAPr7/AFyAQFCpwGD6jy3/wE9on7N0AE+pAFjyMLI0AE2mwF94wGP9QFpzgU3nISSopWgrmJsfTNLfgFHqAFuBilNiTp4sLnGzwWb/0xYb/P09mRygGl0hRlnMgR12V2Pr6e4xF9peS2Cyh5FpBdSfgF84YmisdPa30hjvw+foQFYvlWj4HWIlkWb5gk5n/b4+gw+kgFMscXb6ylmieDj5ju2pylTsniElgqd/u/x8wGW/O7v8SVMsUq+JSSJXQFiwfv+/AFqvB9ntobZeKbc/9vt+B+YmW2rvKruzQGPkm3PPrjmxQFIklrFLVbD4QGMYaXkoIPD13LC+nGw5AGFQHG66gF2eBaJxket9sLf84HI+wF7axBdbg2c0CR+1QFsEIfJ7yqoUIbH41tldgF+KzVTjn3QfitZgTJZkaDR8gKDsXeWrE+zogE3nCeKzQFtJ0tknjdnbQGB6EJgxQFqAcLJ0WC//yKm/wE+o7vI0ARozEOz/4/g/4KToyaX4/D09pCpuNHV24HA6gw7oAF/AXWKnEVSb5TI6VzDTrPprxBQts7e6FNdcBA9oySd9RRjPAhnD2NvgIydrF+6wdLo9v7//2K+twKSdDmKyeD56wGCyHq12VnF+ZXXsARdTjZWthShoo7gtilDlAFw1RCXvF+z6p/R8kqZzAF0Oj5jjFuJqgFoAkRgxtzr9YmcrJKsugFlylfBgxJGhjJIeFnFuhmi/+bo65ipt8Hn+UhVco7B5SZowAGBKoaZqAGGAVHBUwF8Qq7Y819qe4DEoVyYwrnb8QGN9GCy6QFTuHB9jgGY/gFRtuTu9ZOhr150iwFbwTFiwFus4h9mYt/y+kWZ35vM7hGfccz43Xy/6m3BuS1GiYveqDRfwnbUV4rdu////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAN8ALAAAAAATABIAAAj/AL8JHEiwVTVspar8ITiwiJhswyaBibJJUq9Trxh+S2OAVihvSzqRcoTpmy5ADIPFqrHtGpBETbrIuXJEBgiGbHoogTItExJOoAbw8rHmAkFTC8KYwTWkGx8COp4AozAjD8Epo4wQQfTLCQEcxqigoiONBUFqerRYspYCgzIGmgi98cRlA8EVLaR4UJPk0oASVgKs6kAiBMFDdrzAarDFF5kgCJA9ilNBGMFjWAQse/YjwBcVMfCcgTMr2UBKe0QIaHNgAiQmBRS4+CSKEYSBWe44E6JoEAxZDhrxmDPCEAcaA4vVinTCwi5uKFhBs6EtQ4QEOQYy8+NGUDRiqdCUJJGQa8yNQDsADHyxSNUHE4Vc3erzoFkdWxoAVNLIv7///98EBAA7')}
${img('canvas', 16, 'gif', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAADAFBMVEX/AAC1t7etsLCsrq6rrq6rrq2tr6+0tratsK/////p6enIysrl5OTn5uXo5+ajpaXo5+dhhKdliKlmialgg6elp6f6+/vIycnr7Ozw7u7x7u7x7u3t6+vLzMvp7vbs7/bz8PD17+3z7u2rrq/6xS76xy13zv9+z/+EwLF4zP/38/NfgqWAoL36uCj6vCmR2f+TxamSrBmNvoj++fz8+Pf69/WZ3f+g4P+n4/+Cnw2Dox16nQ3//f9hg6eBob6x5/+46f+77P+p2NKSZhOi1s////7//fusrq98sB6CsyWDtSmFuC9+dBl/tilfgqasr6+sr7DbAADcAABcgqWAoLyusLC4urqssLCssLGrsLCrr7Ctr67c3NzMzMwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABoAAAKAgJldmV0dU8GB3JvTnZDBWVyb2xsYwdjYWxhUBB0bmVrY2F1b3IICGRPYmFyZWQAAAXj1P/Hsf+rjv+Pa/9zSP9XJf9VAP9JANw9ALkxAJYlAHMZAFDU1P+xsf+Ojv9ra/9ISP8lJf8AAP4AANwAALkAAJYAAHMAAFDU4/+xx/+Oq/9rj/9Ic/8lV/8AVf8ASdwAPbkAMZYAJXMAGVDU8P+x4v+O1P9rxv9IuP8lqv8Aqv8AktwAerkAYpYASnMAMlDU//+x//+O//9r//9I//8l//8A/v4A3NwAubkAlpYAc3MAUFDU//Cx/+KO/9Rr/8ZI/7gl/6oA/6oA3JIAuXoAlmIAc0oAUDLU/+Ox/8eO/6tr/49I/3Ml/1cA/1UA3EkAuT0AljEAcyUAUBnU/9Sx/7GO/45r/2tI/0gl/yUA/gAA3AAAuQAAlgAAcwAAUADj/9TH/7Gr/46P/2tz/0hX/yVV/wBJ3AAQ+AFLAAAAAXRSTlMAQObYZgAAAAlwSFlzAAALEgAACxIB0t1+/AAAALpJREFUeNpjYGBkYmZhYWFlYWNngAAOTijg4oYIMHPy8PLx8nDycwpwQwUEhYSFRDhFxTi5xCECEpJS0jKcsqL8nGwgARZOOXkFRSWwMcwgAWVOFVU1dQ1NLW0dmICunr6BoZGxiSlEgJnTzNzC0sraxtYOJmDv4Ojk7MLp6gYRcOf08PTy9vHl9IOa4c+JAGCBAM7AoEDOwEDO4BCIABOSilCQQBhTeERkVGS4f3R0aBhIICYWAWIYGAClIBsa7hXG7gAAAABJRU5ErkJggg==')}
${img('profile', 16, 'gif', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsSAAALEgHS3X78AAABZElEQVR42o1R22rCQBD1U/p//apCNtsHwRdfBaFIKbRoUVKMMTWBWIxVCq2b+07POrn4UKjDMpw9O2fm7G5vNBpJKe2/Qto4uEc2WMrBYEBEPaAky36UulwnlSRpUeZEBSGrpEiyHJVGAPVJqZvbO3ftv83Dle+vvPV4/LD0PGYAcKrSFJUsEOgHKoj3s9dFGH9uou3k8ekQKxyDQcYpBnYC7Hm9zBZmlL8BiIJDC0AWpa4FwhZJXoDCBgYAjgU5ToBt+k1tL14ssFNNvIEBAFwVljJlSDBfpwyg1ISnYoEsiHju5XLcd+T50q0tEQm7eaWKKNfUWgKApUsbPFY0lzY6DraEZm585Do/CLMzqLQWQnSC9k34lVa7PTsBs/zYOa4LB5ZlnQXCbif40Ra50jUwE6JtCcMlUiMQlugEQYisG8CWtGlRdQL+jmui/rjhcAhk/Reo6ff7RuB53vN1MZ1OIfgFQC1cuR3Y6lIAAAAASUVORK5CYII=')}
${img('execute', 16, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEXAwMAAxwCvbOAvAAAAAXRSTlMAQObYZgAAAAlwSFlzAAALEgAACxIB0t1+/AAAACBJREFUCFtjYIABHgYGfiA6wMD/gYH/B5g8ABLhYUAGAHniBNrUPuoHAAAAAElFTkSuQmCC')}
${img('file', 16, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABmJLR0T///////8JWPfcAAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAQAAAAEABcxq3DAAAA2klEQVRIx61VURbDIAgTX+9ljg4n2z5sNouj1ml+LE9rQkSU5PA6kTZBToTznj5aqKqq+py4lFJKScnMzCwlAAB6IbnNuyXycd1g3oHrf32CmR9mZqpVOdDHs2DmI+c+AiJixu1RAN9xFUcdWCjVIr8xCX8Jubc8Ao9CJF8nRFgNJBxZSCEkjmrIxxSS0yIAoBU4OkpfU8sCPEbEvqaOXcR31zWORbYJ8EI8rsK+DWm7gMVb8F/GK7eg6818jNjJZjMn0agY7x6oxqL5sWbIbhLHoQN78PQ5F3kDgX8u9tphBfoAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMDItMDZUMTA6Mjc6MzErMDE6MDChLu/mAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjIwKzAxOjAwIGvf8wAAAABJRU5ErkJggg==')}
${img('text', 16, 'gif', 'R0lGODlhEgASALMAAP/////MzP+Zmf9mZv8zM/8AAMzM/8zMzJmZ/5mZmWZm/2ZmZjMz/zMzMwAA/////yH5BAUUAA8ALAAAAAASABIAAARo8MlJq73SKGSwdSDjUQoIjhNYOujDnGAnFXRBZKoBIpMw1ICHaaigBAq/AUK1CVEIhcfPNFlRbAEBEvWr0VDYQLYgkCQWh8XiAfgRymPyoTFRa2uPO009maP8ZmsjAHxnBygLDQ1zihEAOw==')}
${img('task', 18, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAAAAACMfPpKAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAABIAAAASABGyWs+AAAATklEQVQY05XQUQoAIAgD0N3JY3fIChWttKR9xYvBCj0J0FsI3VVKQflwV22J0oyo3LOCc6pHW4dqi56v2CebbpMLtcmr+uTizz6UYpBnADSS8gvhaL5WAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE2LTA0LTA3VDA5OjQyOjQ4KzAyOjAwMgzRmQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOCswMTowMJ0LlncAAAAASUVORK5CYII=')}
${img('pavetext', 18, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAAsSURBVBjTY2CgCuBAAt1gASS5KKgARBpJACSEooIsARRbkABYoDsKCRDhEQBA2Am/6OrPewAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNi0wMS0wNFQxMDoxODoyNyswMTowMHsz6UQAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MjArMDE6MDAga9/zAAAAAElFTkSuQmCC')}
${img('pavelabel', 18, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAApSURBVBjTY2CgCuBAAt1gASS5KJgABzUEgABFANUWJAAWYIhCAkR4BAAHoAkEyi2U3wAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNi0wMS0wNFQxMDoxODoyNyswMTowMHsz6UQAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MjArMDE6MDAga9/zAAAAAElFTkSuQmCC')}
${img('list', 16, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4AEECTc01vBgywAAAE9JREFUOMu1k8ERwDAMwqRc9l/Z/eeRpKZlABkOLFD0JQGgAAah5kp8Y30F2HEwDhGTCG6tX5yqtAV/acEdwHQHl0Y8RbA7pLIxRPziGyM9xLEOKSpp/5AAAAAASUVORK5CYII=')}
${img('color', 16, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAM1BMVEUAAAAA4xcavGZGS1xZT79lW+9wdvFz/3N6fo3RISTZwXbyniXz80v/AAD/zAD/66v//6vGWiYeAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAADswAAA7MAbGhBn4AAAAHdElNRQfgAQQLLBhOmhPcAAAAIklEQVQY02NgRgEMDAzMnLzcfDwC7IxMbKwsQ10A3XMEAQA3JQVNowlkTAAAAABJRU5ErkJggg==')}
${img('colz', 16, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEV6fo0A4xcavGZGS1xZT79lW+9wdvFz/3PRISTZwXbyniXz80v/AAD/zAD/66v//6t1AkcGAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAADswAAA7MAbGhBn4AAAAHdElNRQfgAQQLNwdqpNWzAAAAT0lEQVQI12NgYGAwNjZmAAOLjmY0hs2ZwxCG1arFEIbt3csQhvXuzRCG/f/PEIZ5eTGEYSgoDGEYKSlDGGZpyRCGaWgwhGHi4gxhwG0HAwCr3BFWzqCkcAAAAABJRU5ErkJggg==')}
${img('frame', 16, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfgAQQLOwq4oOYCAAAAcUlEQVQoz7WQMQqAMAxFX0Uk4OLgIbp4oZ7BA/cOXR0KDnGpRbGayT+EQF74nw+GHIBo+5hdWdqAaFDoLIsegCSeWE0VcMxXYM6xvmiZSYDTooSR4WlxzzBZwGYBuwWs4mWUpVHJe1H9F1J7yC4ov+kAkTYXFCNzDrEAAAAASUVORK5CYII=')}
${img('class', 16, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAvQC9AL1pQtWoAAAAjUlEQVR42p2T2wnAIAxFM0g/O6jDdBBHcAyHKKQYjfiI0UY4P8I9BG4CID8smB4+8SUsohpO3CFzKqmBFrhCO4kqQnCR6MJF4BEJTVQFhBAmASNIZkH6a0OMc8oUDAu8z7RhTTBVyIIEhxeCdYWjQApvK2TBrgGpwpP1livsBXC0ROMO/LqDKjKEzaf8AZWbJP6pTT9BAAAATHpUWHRTb2Z0d2FyZQAAeNpz0FDW9MxNTE/1TUzPTM5WMNEz0jNQsLTUNzDWNzBUSC7KLC6pdMitLC7JTNZLLdZLKS3IzyvRS87PBQDzvxJ8u4pLSgAAADN6VFh0U2lnbmF0dXJlAAB42ktKs0hLMkk2MzJKNEuzMLKwtEizSElMMbNITUw0NUtNAQCc7Qma0Goe1QAAAABJRU5ErkJggg==')}
${img('member', 16, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAvQC9AL1pQtWoAAAAX0lEQVR42mNgAAIVBob/+DADPgBS8GCPBV6M1xCKDcDnBRcoZhgW4D8DBV75v2bLATAmxyC4ZmRMrCFYNfeU9BBvwJwpS8AYWTNZBoAwTDPFBpAciDCDyNFMtXSAFwAAUyq0GRPbbz4AAABMelRYdFNvZnR3YXJlAAB42nPQUNb0zE1MT/VNTM9MzlYw0TPSM1CwtNQ3MNY3MFRILsosLql0yK0sLslM1kst1kspLcjPK9FLzs8FAPO/Eny7iktKAAAAM3pUWHRTaWduYXR1cmUAAHjaS01JNrE0S00zSbU0NEsxMbMwM0xOSjYwNzY3NLRIMjUCAJcdCJ2BHe6SAAAAAElFTkSuQmCC')}
${img('tf1', 16, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAADFBMVEX/////AP8/SMz///+Cf5VqAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAHdElNRQfgCw4QHgSCla+2AAAAL0lEQVQI12MQYAACrAQXiFBoABINCgwMQgwcDAwSDEwMDKmhodMYJjAwaKDrAAEAoRAEjHDJ/uQAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTYtMTEtMTRUMTc6Mjk6MjErMDE6MDDxcSccAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE2LTExLTE0VDE3OjI5OjA1KzAxOjAwNka8zgAAAABJRU5ErkJggg==')}
${img('tf2', 16, 'png', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAADFBMVEX/////AP8A/wD////pL6WoAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAHdElNRQfgCw4PNgzGaW1jAAAARUlEQVQI12NgEGDQZAASKkBigQKQ6GhgYBDiYgASIiAigIGBS8iBgUFhEpCnoAEkUkNDQxkagUIMrUDMMAVETAARQI0MAD5GCJ7tAr1aAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE2LTExLTE0VDE2OjUxOjUzKzAxOjAwi1Gz3gAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNi0xMS0xNFQxNjo1MTozNiswMTowMG5bLUIAAAAASUVORK5CYII=')}
`, node, 'jsroot_hstyle');
}

/** @summary Return size as string with suffix like MB or KB
  * @private */
function getSizeStr(sz) {
   if (sz < 10000)
      return sz.toFixed(0) + 'B';
   if (sz < 1e6)
      return (sz / 1e3).toFixed(2) + 'KiB';
   if (sz < 1e9)
      return (sz / 1e6).toFixed(2) + 'MiB';
   return (sz / 1e9).toFixed(2) + 'GiB';
}

/** @summary Return ROOT version as string
  * @private */
function getVersionStr(v) {
   const major = Math.floor(v / 10000);
   let minor = Math.floor((v - major * 10000) / 100).toString(),
       patch = (v % 100).toString();
   if (minor.length < 2)
      minor = '0' + minor;
   if (patch.length < 2)
      patch = '0' + patch;
   return `${major}.${minor}.${patch}`;
}

/** @summary draw list content
  * @desc used to draw all items from TList or TObjArray inserted into the TCanvas list of primitives
  * @private */
async function drawList(dom, lst, opt) {
   if (!lst?.arr)
      return null;

   const handle = {
      dom, lst, opt,
      indx: -1, painter: null,
      draw_next() {
         while (++this.indx < this.lst.arr.length) {
            const item = this.lst.arr[this.indx],
                  opt2 = (this.lst.opt && this.lst.opt[this.indx]) ? this.lst.opt[this.indx] : this.opt;
            if (!item)
               continue;
            return draw(this.dom, item, opt2).then(p => {
               if (p && !this.painter)
                  this.painter = p;
               return this.draw_next(); // reenter loop
            });
         }
         return this.painter;
      }
   };

   return handle.draw_next();
}

// ===================== hierarchy scanning functions ==================================

/** @summary Create hierarchy elements for TFolder object
  * @private */
function folderHierarchy(item, obj) {
   if (!obj?.fFolders)
      return false;

   if (!obj.fFolders.arr.length) {
      item._more = false;
      return true;
   }

   item._childs = [];

   for (let i = 0; i < obj.fFolders.arr.length; ++i) {
      const chld = obj.fFolders.arr[i];
      item._childs.push({
         _name: chld.fName,
         _kind: getKindForType(chld._typename),
         _obj: chld
      });
   }
   return true;
}

/** @summary Create hierarchy elements for TList object
  * @private */
function listHierarchy(folder, lst) {
   if (!isRootCollection(lst))
      return false;

   if (!lst.arr?.length) {
      folder._more = false;
      return true;
   }

   let do_context = false, prnt = folder;
   while (prnt) {
      if (prnt._do_context)
         do_context = true;
      prnt = prnt._parent;
   }

   // if list has objects with similar names, create cycle number for them
   const ismap = (lst._typename === clTMap), names = [], cnt = [], cycle = [];

   for (let i = 0; i < lst.arr.length; ++i) {
      const obj = ismap ? lst.arr[i].first : lst.arr[i];
      if (!obj)
         continue; // for such objects index will be used as name
      const objname = obj.fName || obj.name;
      if (!objname)
         continue;
      const indx = names.indexOf(objname);
      if (indx >= 0)
         cnt[indx]++;
      else {
         cnt[names.length] = cycle[names.length] = 1;
         names.push(objname);
      }
   }

   folder._childs = [];
   for (let i = 0; i < lst.arr.length; ++i) {
      const obj = ismap ? lst.arr[i].first : lst.arr[i];
      let item;
      if (!obj?._typename) {
         item = {
            _name: i.toString(),
            _kind: getKindForType('NULL'),
            _title: 'NULL',
            _value: 'null',
            _obj: null
         };
      } else {
         item = {
            _name: obj.fName || obj.name,
            _kind: getKindForType(obj._typename),
            _title: `${obj.fTitle || ''} type:${obj._typename}`,
            _obj: obj
         };

         switch (obj._typename) {
            case clTColor:
               item._value = getRGBfromTColor(obj);
               break;
            case clTText:
            case clTLatex:
               item._value = obj.fTitle;
               break;
            case clTObjString:
               item._value = obj.fString;
               break;
            default:
               if (lst.opt && lst.opt[i] && lst.opt[i].length)
                  item._value = lst.opt[i];
         }

         if (do_context && canDrawHandle(obj._typename))
            item._direct_context = true;

         // if name is integer value, it should match array index
         if (!item._name || (Number.isInteger(parseInt(item._name)) && (parseInt(item._name) !== i)) || (lst.arr.indexOf(obj) < i))
            item._name = i.toString();
         else {
            // if there are several such names, add cycle number to the item name
            const indx = names.indexOf(obj.fName);
            if ((indx >= 0) && (cnt[indx] > 1)) {
               item._cycle = cycle[indx]++;
               item._keyname = item._name;
               item._name = item._keyname + ';' + item._cycle;
            }
         }
      }

      folder._childs.push(item);
   }
   return true;
}

/** @summary Create hierarchy of TKey lists in file or sub-directory
  * @private */
function keysHierarchy(folder, keys, file, dirname) {
   if (keys === undefined)
      return false;

   folder._childs = [];

   for (let i = 0; i < keys.length; ++i) {
      const key = keys[i];

      if (settings.OnlyLastCycle && (i > 0) && (key.fName === keys[i - 1].fName) && (key.fCycle < keys[i - 1].fCycle))
         continue;

      const item = {
         _name: key.fName + ';' + key.fCycle,
         _cycle: key.fCycle,
         _kind: getKindForType(key.fClassName),
         _title: key.fTitle + ` (size: ${getSizeStr(key.fObjlen)})`,
         _keyname: key.fName,
         _readobj: null,
         _parent: folder
      };

      if (key.fRealName)
         item._realname = key.fRealName + ';' + key.fCycle;

      if (key.fClassName === clTDirectory || key.fClassName === clTDirectoryFile) {
         const dir = (dirname && file) ? file.getDir(dirname + key.fName) : null;
         if (dir) {
            // remove cycle number - we have already directory
            item._name = key.fName;
            keysHierarchy(item, dir.fKeys, file, dirname + key.fName + '/');
         } else {
            item._more = true;
            item._expand = function(node, obj) {
               // one can get expand call from child objects - ignore them
               return keysHierarchy(node, obj.fKeys);
            };
         }
      } else if ((key.fClassName === clTList) && (key.fName === nameStreamerInfo)) {
         if (settings.SkipStreamerInfos)
            continue;
         item._name = nameStreamerInfo;
         item._kind = getKindForType(clTStreamerInfoList);
         item._title = 'List of streamer infos for binary I/O';
         item._readobj = file.fStreamerInfos;
      }

      folder._childs.push(item);
   }

   return true;
}

/** @summary Create hierarchy for arbitrary object
  * @private */
function objectHierarchy(top, obj, args = undefined) {
   if (!top || (obj === null))
      return false;

   top._childs = [];

   let proto = Object.prototype.toString.apply(obj);

   if (proto === '[object DataView]') {
      let item = {
         _parent: top,
         _name: 'size',
         _value: obj.byteLength.toString(),
         _vclass: cssValueNum
      };

      top._childs.push(item);
      const namelen = (obj.byteLength < 10) ? 1 : Math.log10(obj.byteLength);

      for (let k = 0; k < obj.byteLength; ++k) {
         if (k % 16 === 0) {
            item = {
               _parent: top,
               _name: k.toString(),
               _value: '',
               _vclass: cssValueNum
            };
            while (item._name.length < namelen)
               item._name = '0' + item._name;
            top._childs.push(item);
         }

         let val = obj.getUint8(k).toString(16);
         while (val.length < 2)
            val = '0' + val;
         if (item._value)
            item._value += (k % 4 === 0) ? ' | ' : ' ';

         item._value += val;
      }
      return true;
   }

   // check _nosimple property in all parents
   let nosimple = true, do_context = false, prnt = top;
   while (prnt) {
      if (prnt._do_context)
         do_context = true;
      if ('_nosimple' in prnt) {
         nosimple = prnt._nosimple;
         break;
      }
      prnt = prnt._parent;
   }

   const isarray = (isArrayProto(proto) > 0) && obj.length,
         compress = isarray && (obj.length > settings.HierarchyLimit);
   let arrcompress = false;

   if (isarray && (top._name === 'Object') && !top._parent)
      top._name = 'Array';

   if (compress) {
      arrcompress = true;
      for (let k = 0; k < obj.length; ++k) {
         const typ = typeof obj[k];
         if ((typ === 'number') || (typ === 'boolean') || ((typ === 'string') && (obj[k].length < 16)))
            continue;
         arrcompress = false; break;
      }
   }

   if (!('_obj' in top))
      top._obj = obj;
   else if (top._obj !== obj)
      alert('object missmatch');

   if (!top._title) {
      if (obj._typename)
         top._title = getKindForType(obj._typename);
      else if (isarray)
         top._title = 'Array len: ' + obj.length;
   }

   if (arrcompress) {
      for (let k = 0; k < obj.length;) {
         let nextk = Math.min(k + 10, obj.length), allsame = true, prevk = k;

         while (allsame) {
            allsame = true;
            for (let d = prevk; d < nextk; ++d) {
               if (obj[k] !== obj[d])
                  allsame = false;
            }

            if (allsame) {
               if (nextk === obj.length)
                  break;
               prevk = nextk;
               nextk = Math.min(nextk + 10, obj.length);
            } else if (prevk !== k) {
               // last block with similar
               nextk = prevk;
               allsame = true;
               break;
            }
         }

         const item = { _parent: top, _name: k + '..' + (nextk - 1), _vclass: cssValueNum };

         if (allsame)
            item._value = obj[k].toString();
         else {
            item._value = '';
            for (let d = k; d < nextk; ++d)
               item._value += ((d === k) ? '[ ' : ', ') + obj[d].toString();
            item._value += ' ]';
         }

         top._childs.push(item);

         k = nextk;
      }
      return true;
   }

   let lastitem, lastkey, lastfield, cnt;

   for (const key in obj) {
      if ((key === '_typename') || (key[0] === '$'))
         continue;
      const fld = obj[key];
      if (isFunc(fld))
         continue;
      if (args?.exclude && (args.exclude.indexOf(key) >= 0))
         continue;

      if (compress && lastitem) {
         if (lastfield === fld) {
            ++cnt;
            lastkey = key;
            continue;
         }
         if (cnt > 0)
            lastitem._name += '..' + lastkey;
      }

      const item = { _parent: top, _name: key };

      if (compress) {
         lastitem = item;
         lastkey = key;
         lastfield = fld;
         cnt = 0;
      }

      if (fld === null) {
         item._value = item._title = 'null';
         if (!nosimple)
            top._childs.push(item);
         continue;
      }

      let simple = false;

      if (isObject(fld)) {
         proto = Object.prototype.toString.apply(fld);

         if (isArrayProto(proto) > 0) {
            item._title = 'array len=' + fld.length;
            simple = (proto !== '[object Array]');
            if (!fld.length) {
               item._value = '[ ]';
               item._more = false; // hpainter will not try to expand again
            } else {
               item._value = '[...]';
               item._more = true;
               item._expand = objectHierarchy;
               item._obj = fld;
            }
         } else if (proto === '[object DataView]') {
            item._title = 'DataView len=' + fld.byteLength;
            item._value = '[...]';
            item._more = true;
            item._expand = objectHierarchy;
            item._obj = fld;
         } else if (proto === '[object Date]') {
            item._more = false;
            item._title = 'Date';
            item._value = fld.toString();
            item._vclass = cssValueNum;
         } else {
            if (fld.$kind || fld._typename)
               item._kind = item._title = getKindForType(fld.$kind || fld._typename);

            if (fld._typename) {
               item._title = fld._typename;
               if (do_context && canDrawHandle(fld._typename))
                  item._direct_context = true;
            }

            // check if object already shown in hierarchy (circular dependency)
            let curr = top, inparent = false;
            while (curr && !inparent) {
               inparent = (curr._obj === fld);
               curr = curr._parent;
            }

            if (inparent) {
               item._value = '{ prnt }';
               item._vclass = cssValueNum;
               item._more = false;
               simple = true;
            } else {
               item._obj = fld;
               item._more = false;

               switch (fld._typename) {
                  case clTColor:
                     item._value = getRGBfromTColor(fld);
                     break;
                  case clTText:
                  case clTLatex:
                     item._value = fld.fTitle;
                     break;
                  case clTObjString:
                     item._value = fld.fString;
                     break;
                  default:
                     if (isRootCollection(fld) && isObject(fld.arr)) {
                        item._value = fld.arr.length ? '[...]' : '[]';
                        item._title += ', size:' + fld.arr.length;
                        if (fld.arr.length)
                           item._more = true;
                     } else {
                        item._more = true;
                        item._value = '{ }';
                     }
               }
            }
         }
      } else if ((typeof fld === 'number') || (typeof fld === 'boolean') || (typeof fld === 'bigint')) {
         simple = true;
         if (key === 'fBits')
            item._value = '0x' + fld.toString(16);
         else
            item._value = fld.toString();
         item._vclass = cssValueNum;
      } else if (isStr(fld)) {
         simple = true;
         item._value = '"' + fld + '"';
         item._vclass = 'h_value_str';
      } else if (typeof fld === 'undefined') {
         simple = true;
         item._value = 'undefined';
         item._vclass = cssValueNum;
      } else {
         simple = true;
         alert(`miss ${key} type ${typeof fld}`);
      }

      if (!simple || !nosimple)
         top._childs.push(item);
   }

   if (compress && lastitem && (cnt > 0))
      lastitem._name += '..' + lastkey;

   return true;
}

/** @summary Create hierarchy elements for TTask object
  * @desc function can be used for different derived classes
  * we show not only child tasks, but all complex data members
  * @private */
function taskHierarchy(item, obj) {
   if (!obj?.fTasks)
      return false;

   objectHierarchy(item, obj, { exclude: ['fTasks', 'fName'] });

   if (!obj.fTasks.arr.length && !item._childs.length) {
      item._more = false;
      return true;
   }

   for (let i = 0; i < obj.fTasks.arr.length; ++i) {
      const chld = obj.fTasks.arr[i];
      item._childs.push({
         _name: chld.fName,
         _kind: getKindForType(chld._typename),
         _obj: chld
      });
   }

   return true;
}

/** @summary Create hierarchy for streamer info object
  * @private */
function createStreamerInfoContent(lst) {
   const h = { _name: nameStreamerInfo, _childs: [] };

   for (let i = 0; i < lst.arr.length; ++i) {
      const entry = lst.arr[i];

      if (entry._typename === clTList)
         continue;

      if (typeof entry.fName === 'undefined') {
         console.warn(`strange element in StreamerInfo with type ${entry._typename}`);
         continue;
      }

      const item = {
         _name: `${entry.fName};${entry.fClassVersion}`,
         _kind: `class ${entry.fName}`,
         _title: `class:${entry.fName} version:${entry.fClassVersion} checksum:${entry.fCheckSum}`,
         _icon: 'img_class',
         _childs: []
      };

      if (entry.fTitle)
         item._title += '  ' + entry.fTitle;

      h._childs.push(item);

      if (typeof entry.fElements === 'undefined')
         continue;
      for (let l = 0; l < entry.fElements.arr.length; ++l) {
         const elem = entry.fElements.arr[l];
         if (!elem?.fName)
            continue;
         let _name = `${elem.fTypeName} ${elem.fName}`;
         const _title = `${elem.fTypeName} type:${elem.fType}`;
         if (elem.fArrayDim === 1)
            _name += `[${elem.fArrayLength}]`;
         else {
            for (let dim = 0; dim < elem.fArrayDim; ++dim)
               _name += `[${elem.fMaxIndex[dim]}]`;
         }
         if (elem.fBaseVersion === 4294967295)
            _name += ':-1';
         else if (elem.fBaseVersion !== undefined)
            _name += `:${elem.fBaseVersion}`;
         _name += ';';
         if (elem.fTitle)
            _name += ` // ${elem.fTitle}`;

         item._childs.push({ _name, _title, _kind: elem.fTypeName, _icon: (elem.fTypeName === kBaseClass) ? 'img_class' : 'img_member' });
      }
      if (!item._childs.length)
         delete item._childs;
   }

   return h;
}

/** @summary tag item in hierarchy painter as streamer info
  * @desc this function used on THttpServer to mark streamer infos list
  * as fictional TStreamerInfoList class, which has special draw function
  * @private */
function markAsStreamerInfo(h, item, obj) {
   if (obj?._typename === clTList)
      obj._typename = clTStreamerInfoList;
}


/** @summary Create hierarchy for object inspector
  * @private */
function createInspectorContent(obj) {
   const h = { _name: 'Object', _title: '', _click_action: kExpand, _nosimple: false, _do_context: true };

   if (isStr(obj.fName) && obj.fName)
      h._name = obj.fName;

   if (isStr(obj.fTitle) && obj.fTitle)
      h._title = obj.fTitle;

   if (obj._typename)
      h._title += `  type:${obj._typename}`;

   if (isRootCollection(obj)) {
      h._name = obj.name || obj._typename;
      listHierarchy(h, obj);
   } else
      objectHierarchy(h, obj);

   return h;
}


/** @summary Parse string value as array.
  * @desc It could be just simple string:  'value' or
  * array with or without string quotes:  [element], ['elem1',elem2]
  * @private */
function parseAsArray(val) {
   const res = [];

   if (!isStr(val))
      return res;

   val = val.trim();
   if (!val)
      return res;

   // return as array with single element
   if ((val.length < 2) || (val.at(0) !== '[') || (val.at(-1) !== ']')) {
      res.push(val);
      return res;
   }

   // try to split ourself, checking quotes and brackets
   let nbr = 0, nquotes = 0, ndouble = 0, last = 1;

   for (let indx = 1; indx < val.length; ++indx) {
      if (nquotes > 0) {
         if (val[indx] === '\'')
            nquotes--;
         continue;
      }
      if (ndouble > 0) {
         if (val[indx] === '"')
            ndouble--;
         continue;
      }
      switch (val[indx]) {
         case '\'':
            nquotes++;
            break;
         case '"':
            ndouble++;
            break;
         case '[':
            nbr++;
            break;
         case ']':
            if (indx < val.length - 1) {
               nbr--;
               break;
            }
         // eslint-disable-next-line  no-fallthrough
         case ',':
            if (nbr === 0) {
               let sub = val.substring(last, indx).trim();
               if ((sub.length > 1) && (sub.at(0) === sub.at(-1)) && ((sub[0] === '"') || (sub[0] === '\'')))
                  sub = sub.slice(1, sub.length - 1);
               res.push(sub);
               last = indx + 1;
            }
            break;
      }
   }

   if (!res.length)
      res.push(val.slice(1, val.length - 1).trim());

   return res;
}


/** @summary central function for expand of all online items
  * @private */
function onlineHierarchy(node, obj) {
   if (node && obj?._childs) {
      for (let n = 0; n < obj._childs.length; ++n) {
         if (obj._childs[n]._more || obj._childs[n]._childs)
            obj._childs[n]._expand = onlineHierarchy;
      }

      node._childs = obj._childs;
      obj._childs = null;
      return true;
   }

   return false;
}

/** @summary Check if draw handle for specified object can do expand
  * @private */
function canExpandHandle(handle) {
   return handle?.expand || handle?.get_expand || handle?.expand_item;
}

const kindTFile = getKindForType(clTFile);

/**
  * @summary Painter of hierarchical structures
  *
  * @example
  * import { HierarchyPainter } from 'https://root.cern/js/latest/modules/gui/HierarchyPainter.mjs';
  * // create hierarchy painter in 'myTreeDiv'
  * let h = new HierarchyPainter('example', 'myTreeDiv');
  * // configure 'simple' layout in 'myMainDiv'
  * // one also can specify 'grid2x2' or 'flex' or 'tabs'
  * h.setDisplay('simple', 'myMainDiv');
  * // open file and display element
  * h.openRootFile('https://root.cern/js/files/hsimple.root').then(() => h.display('hpxpy;1','colz')); */

class HierarchyPainter extends BasePainter {

   #monitoring_interval; // monitoring time interval
   #monitoring_on; // if monitoring enabled
   #monitoring_handle; // timer handle for monitoring
   #monitoring_frame; // animation frame for monitoring
   #one_by_one;  // process drop items one by one
   #topname; // top item name
   #cached_draw_object; // cached object for first draw

   /** @summary Create painter
     * @param {string} name - symbolic name
     * @param {string} frameid - element id where hierarchy is drawn
     * @param {string} [backgr] - background color */
   constructor(name, frameid, backgr) {
      super(frameid);
      this.name = name;
      this.h = null; // hierarchy
      this.with_icons = true;

      if (backgr === '__as_dark_mode__')
         this.setBasicColors();
      else
         this.background = backgr;
      this.files_monitoring = !frameid; // by default files monitored when nobrowser option specified
      this.nobrowser = (frameid === null);

      // remember only very first instance
      if (!getHPainter())
         setHPainter(this);
   }

   /** @summary Set basic colors
     * @private */
   setBasicColors() {
      this.background = settings.DarkMode ? 'black' : 'white';
      this.textcolor = settings.DarkMode ? '#eee' : '#111';
   }

   /** @summary Cleanup hierarchy painter
     * @desc clear drawing and browser */
   cleanup() {
      this.clearHierarchy(true);

      super.cleanup();

      if (getHPainter() === this)
         setHPainter(null);
   }

   /** @summary Create file hierarchy
     * @private */
   fileHierarchy(file, folder) {
      const painter = this;
      if (!folder)
         folder = {};

      folder._name = file.fFileName;
      folder._title = (file.fTitle ? file.fTitle + ', path: ' : '') + file.fFullURL + `, size: ${getSizeStr(file.fEND)}, version: ${getVersionStr(file.fVersion)}, modified: ${convertDate(getTDatime(file.fDatimeM))}`;
      folder._kind = kindTFile;
      folder._file = file;
      folder._fullurl = file.fFullURL;
      folder._localfile = file.fLocalFile;
      folder._had_direct_read = false;
      // this is central get method, item or itemname can be used, returns promise
      folder._get = function(item, itemname) {
         if (item?._readobj)
            return Promise.resolve(item._readobj);

         if (item)
            itemname = painter.itemFullName(item, this);

         const readFileObject = file2 => {
            if (!this._file)
               this._file = file2;

            if (!file2)
               return Promise.resolve(null);

            return file2.readObject(itemname).then(obj => {
               // if object was read even when item did not exist try to reconstruct new hierarchy
               if (!item && obj) {
                  // first try to found last read directory
                  const d = painter.findItem({ name: itemname, top: this, last_exists: true, check_keys: true });
                  if ((d?.last !== undefined) && (d.last !== this)) {
                     // reconstruct only sub-directory hierarchy
                     const dir = file2.getDir(painter.itemFullName(d.last, this));
                     if (dir) {
                        d.last._name = d.last._keyname;
                        const dirname = painter.itemFullName(d.last, this);
                        keysHierarchy(d.last, dir.fKeys, file2, dirname + '/');
                     }
                  } else {
                     // reconstruct full file hierarchy
                     keysHierarchy(this, file2.fKeys, file2, '');
                  }
                  item = painter.findItem({ name: itemname, top: this });
               }

               if (item) {
                  item._readobj = obj;
                  // remove cycle number for objects supporting expand
                  if ('_expand' in item)
                     item._name = item._keyname;
               }

               return obj;
            });
         };

         if (this._file)
            return readFileObject(this._file);
         if (this._localfile)
            return openFile(this._localfile).then(f => readFileObject(f));
         if (this._fullurl)
            return openFile(this._fullurl).then(f => readFileObject(f));
         return Promise.resolve(null);
      };

      keysHierarchy(folder, file.fKeys, file, '');

      return folder;
   }

   /** @summary Iterate over all items in hierarchy
     * @param {function} func - function called for every item
     * @param {object} [top] - top item to start from
     * @private */
   forEachItem(func, top) {
      function each_item(item, prnt) {
         if (!item)
            return;
         if (prnt)
            item._parent = prnt;
         func(item);
         const len = item._childs?.length ?? 0;
         for (let n = 0; n < len; ++n)
            each_item(item._childs[n], item);
      }

      if (isFunc(func))
         each_item(top || this.h);
   }

   /** @summary Search item in the hierarchy
     * @param {object|string} arg - item name or object with arguments
     * @param {string} arg.name -  item to search
     * @param {boolean} [arg.force] - specified elements will be created when not exists
     * @param {boolean} [arg.last_exists] -  when specified last parent element will be returned
     * @param {boolean} [arg.check_keys] - check TFile keys with cycle suffix
     * @param {boolean} [arg.allow_index] - let use sub-item indexes instead of name
     * @param {object} [arg.top] - element to start search from
     * @private */
   findItem(arg) {
      function find_in_hierarchy(top, fullname) {
         if (!fullname || !top)
            return top;

         let pos = fullname.length;

         if (!top._parent && (top._kind !== kTopFolder) && (fullname.indexOf(top._name) === 0)) {
            // it is allowed to provide item name, which includes top-parent like file.root/folder/item
            // but one could skip top-item name, if there are no other items
            if (fullname === top._name)
               return top;

            const len = top._name.length;
            if (fullname[len] === '/') {
               fullname = fullname.slice(len + 1);
               pos = fullname.length;
            }
         }

         function process_child(child, ignore_prnt) {
            // set parent pointer when searching child
            if (!ignore_prnt)
               child._parent = top;

            if ((pos >= fullname.length - 1) || (pos < 0))
               return child;

            return find_in_hierarchy(child, fullname.slice(pos + 1));
         }

         while (pos > 0) {
            // we try to find element with slashes inside - start from full name
            let localname = (pos >= fullname.length) ? fullname : fullname.slice(0, pos);

            if (top._childs) {
               // first try to find direct matched item
               for (let i = 0; i < top._childs.length; ++i) {
                  if (top._childs[i]._name === localname)
                     return process_child(top._childs[i]);
               }

               // if first child online, check its elements
               if ((top._kind === kTopFolder) && (top._childs[0]._online !== undefined)) {
                  for (let i = 0; i < top._childs[0]._childs.length; ++i) {
                     if (top._childs[0]._childs[i]._name === localname)
                        return process_child(top._childs[0]._childs[i], true);
                  }
               }

               // if allowed, try to found item with key
               if (arg.check_keys) {
                  let newest = null;
                  for (let i = 0; i < top._childs.length; ++i) {
                     if (top._childs[i]._keyname === localname) {
                        if (!newest || (newest._cycle < top._childs[i]._cycle))
                           newest = top._childs[i];
                     }
                  }
                  if (newest)
                     return process_child(newest);
               }

               let allow_index = arg.allow_index;
               if ((localname.at(0) === '[') && (localname.at(-1) === ']') &&
                    /^\d+$/.test(localname.slice(1, localname.length - 1))) {
                  allow_index = true;
                  localname = localname.slice(1, localname.length - 1);
               }

               // when search for the elements it could be allowed to check index
               if (allow_index && /^\d+$/.test(localname)) {
                  const indx = parseInt(localname);
                  if (Number.isInteger(indx) && (indx >= 0) && (indx < top._childs.length))
                     return process_child(top._childs[indx]);
               }
            }

            pos = fullname.lastIndexOf('/', pos - 1);
         }

         if (arg.force) {
            // if did not found element with given name we just generate it
            if (top._childs === undefined)
               top._childs = [];
            pos = fullname.indexOf('/');
            const child = { _name: ((pos < 0) ? fullname : fullname.slice(0, pos)) };
            top._childs.push(child);
            return process_child(child);
         }

         return arg.last_exists ? { last: top, rest: fullname } : null;
      }

      let top = this.h, itemname;

      if (isStr(arg)) {
         itemname = arg;
         arg = {};
      } else if (isObject(arg)) {
         itemname = arg.name;
         if ('top' in arg)
            top = arg.top;
      } else
         return null;

      if (itemname === '__top_folder__')
         return top;

      if (isStr(itemname) && (itemname.indexOf('img:') === 0))
         return null;

      return find_in_hierarchy(top, itemname);
   }

   /** @summary Produce full string name for item
     * @param {Object} node - item element
     * @param {Object} [uptoparent] - up to which parent to continue
     * @param {boolean} [compact] - if specified, top parent is not included
     * @return {string} produced name
     * @private */
   itemFullName(node, uptoparent, compact) {
      if (node?._kind === kTopFolder)
         return '__top_folder__';

      let res = '';

      while (node) {
         // online items never includes top-level folder
         if ((node._online !== undefined) && !uptoparent)
            return res;

         if ((node === uptoparent) || (node._kind === kTopFolder))
            break;
         if (compact && !node._parent)
            break; // in compact form top-parent is not included
         if (res)
            res = '/' + res;
         res = node._name + res;
         node = node._parent;
      }

      return res;
   }

   /** @summary Executes item marked as 'Command'
     * @desc If command requires additional arguments, they could be specified as extra arguments arg1, arg2, ...
     * @param {String} itemname - name of command item
     * @param {Object} [elem] - HTML element for command execution
     * @param [arg1] - first optional argument
     * @param [arg2] - second optional argument and so on
     * @return {Promise} with command result */
   async executeCommand(itemname, elem, ...userargs) {
      const hitem = this.findItem(itemname),
            url = this.getOnlineItemUrl(hitem) + '/cmd.json',
            d3node = select(elem),
            cmdargs = [];

      for (let n = 0; n < (hitem._numargs ?? 0); ++n)
         cmdargs.push(n < userargs.length ? userargs[n] : '');

      const promise = !cmdargs.length || !elem
                       ? Promise.resolve(cmdargs)
                       : createMenu().then(menu => menu.showCommandArgsDialog(hitem._name, cmdargs));

      return promise.then(args => {
         if (args === null)
            return false;

         let urlargs = '';
         for (let k = 0; k < args.length; ++k)
            urlargs += `${k > 0 ? '&' : '?'}arg${k + 1}=${args[k]}`;

         if (!d3node.empty()) {
            d3node.style('background', 'yellow');
            if (hitem._title)
               d3node.attr('title', 'Executing ' + hitem._title);
         }

         return httpRequest(url + urlargs, 'text').then(res => {
            if (d3node.empty())
               return res;
            const col = (res && (res !== 'false')) ? 'green' : 'red';
            d3node.style('background', col);
            if (hitem._title)
               d3node.attr('title', hitem._title + ' lastres=' + res);
            setTimeout(() => {
               d3node.style('background', null);
               if (hitem._icon && d3node.classed('jsroot_fastcmd_btn'))
                  d3node.style('background-image', `url('${hitem._icon}')`);
            }, 2000);
            if ((col === 'green') && ('_hreload' in hitem))
               this.reload();
            if ((col === 'green') && ('_update_item' in hitem))
               this.updateItems(hitem._update_item.split(';'));
            return res;
         });
      });
   }

   /** @summary Get object item with specified name
     * @desc depending from provided option, same item can generate different object types
     * @param {Object} arg - item name or config object
     * @param {string} arg.name - item name
     * @param {Object} arg.item - or item itself
     * @param {string} options - supposed draw options
     * @return {Promise} with object like { item, obj, itemname }
     * @private */
   async getObject(arg, options) {
      const result = { item: null, obj: null };
      let itemname, item;

      if (arg === null)
         return result;

      if (isStr(arg))
         itemname = arg;
      else if (isObject(arg)) {
         if ((arg._parent !== undefined) && (arg._name !== undefined) && (arg._kind !== undefined))
            item = arg;
         else if (arg.name !== undefined)
            itemname = arg.name;
         else if (arg.arg !== undefined)
            itemname = arg.arg;
         else if (arg.item !== undefined)
            item = arg.item;
      }

      if (isStr(itemname) && (itemname.indexOf('img:') === 0)) {
         // artificial class, can be created by users
         result.obj = { _typename: 'TJSImage', fName: itemname.slice(4) };
         return result;
      }

      if (item)
         itemname = this.itemFullName(item);
      else
         item = this.findItem({ name: itemname, allow_index: true, check_keys: true });

      // if item not found, try to find nearest parent which could allow us to get inside

      const d = item ? null : this.findItem({ name: itemname, last_exists: true, check_keys: true, allow_index: true });

      // if item not found, try to expand hierarchy central function
      // implements not process get in central method of hierarchy item (if exists)
      // if last_parent found, try to expand it
      if ((d !== null) && ('last' in d) && (d.last !== null)) {
         const parentname = this.itemFullName(d.last);

         // this is indication that expand does not give us better path to searched item
         if (isObject(arg) && ('rest' in arg)) {
            if ((arg.rest === d.rest) || (arg.rest.length <= d.rest.length))
               return result;
         }

         return this.expandItem(parentname, undefined, options !== 'hierarchy_expand_verbose').then(res => {
            if (!res)
               return result;
            let newparentname = this.itemFullName(d.last);
            if (newparentname)
               newparentname += '/';
            return this.getObject({ name: newparentname + d.rest, rest: d.rest }, options);
         });
      }

      result.item = item;

      if ((item !== null) && isObject(item._obj)) {
         result.obj = item._obj;
         return result;
      }

      // normally search _get method in the parent items
      let curr = item;
      while (curr) {
         if (isFunc(curr._get)) {
            return curr._get(item, null, options).then(obj => {
               result.obj = obj;
               return result;
            });
         }
         curr = curr._parent;
      }

      return result;
   }

   /** @summary returns true if item is last in parent childs list
     * @private */
   isLastSibling(hitem) {
      if (!hitem || !hitem._parent || !hitem._parent._childs)
         return false;
      const chlds = hitem._parent._childs;
      let indx = chlds.indexOf(hitem);
      if (indx < 0)
         return false;
      while (++indx < chlds.length) {
         if (!('_hidden' in chlds[indx]))
            return false;
      }
      return true;
   }

   /** @summary Create item html code
     * @private */
   addItemHtml(hitem, d3prnt, arg) {
      if (!hitem || ('_hidden' in hitem))
         return true;

      const isroot = (hitem === this.h),
            has_childs = ('_childs' in hitem),
            handle = getDrawHandle(hitem._kind),
            itemname = this.itemFullName(hitem);
      let img1 = '', img2 = '', can_click = false, break_list = false, d3cont;

      if (handle) {
         if ('icon' in handle)
            img1 = handle.icon;
         if ('icon2' in handle)
            img2 = handle.icon2;
         if (!img1 && isFunc(handle.icon_get))
            img1 = handle.icon_get(hitem, this);
         if (canDrawHandle(handle) || ('execute' in handle) || ('aslink' in handle) ||
             (canExpandHandle(handle) && (hitem._more !== false)))
            can_click = true;
      }

      if ('_icon' in hitem)
         img1 = hitem._icon;
      if ('_icon2' in hitem)
         img2 = hitem._icon2;
      if (!img1 && ('_online' in hitem))
         hitem._icon = img1 = 'img_globe';
      if (!img1 && isroot)
         hitem._icon = img1 = 'img_base';

      if (hitem._more || hitem._expand || hitem._player || hitem._can_draw)
         can_click = true;

      let can_menu = can_click;
      if (!can_menu && getTypeForKind(hitem._kind))
         can_menu = can_click = true;

      if (!img2)
         img2 = img1;
      if (!img1)
         img1 = (has_childs || hitem._more) ? 'img_folder' : 'img_page';
      if (!img2)
         img2 = (has_childs || hitem._more) ? 'img_folderopen' : 'img_page';

      if (arg === 'update') {
         d3prnt.selectAll('*').remove();
         d3cont = d3prnt;
      } else {
         d3cont = d3prnt.append('div');
         if (arg && (arg >= (hitem._parent._show_limit || settings.HierarchyLimit)))
            break_list = true;
      }

      hitem._d3cont = d3cont.node(); // set for direct referencing
      d3cont.attr('item', itemname);

      // line with all html elements for this item (excluding childs)
      const h = this, d3line = d3cont.append('div').attr('class', 'h_line');

      // build indent
      let prnt = isroot ? null : hitem._parent, upcnt = 1;
      while (prnt && (prnt !== this.h)) {
         const is_last = this.isLastSibling(prnt),
               d3icon = d3line.insert('div', ':first-child').attr('class', is_last ? 'img_empty' : 'img_line');
         if (!is_last)
            d3icon.style('cursor', 'pointer').property('upcnt', upcnt).on('click', function(evnt) { h.tree_click(evnt, this, 'parentminus'); });
         prnt = prnt._parent;
         upcnt++;
      }

      let icon_class = '', plusminus = false;

      if (isroot) ; else if ((has_childs && !break_list) || handle?.pm) {
         icon_class = hitem._isopen ? 'img_minus' : 'img_plus';
         plusminus = true;
      } else
         icon_class = 'img_join';

      if (icon_class) {
         if (break_list || this.isLastSibling(hitem))
            icon_class += 'bottom';
         const d3icon = d3line.append('div').attr('class', icon_class);
         if (plusminus)
            d3icon.style('cursor', 'pointer').on('click', function(evnt) { h.tree_click(evnt, this, kPM); });
      }

      // make node icons

      if (this.with_icons && !break_list) {
         const icon_name = hitem._isopen ? img2 : img1,
               d3img = (icon_name.indexOf('img_') === 0)
                 ? d3line.append('div')
                          .attr('class', icon_name)
                          .attr('title', hitem._kind)
                 : d3line.append('img')
                          .attr('src', icon_name)
                          .attr('alt', '')
                          .attr('title', hitem._kind)
                          .style('vertical-align', 'top')
                          .style('width', '18px')
                          .style('height', '18px');

         if (hitem._icon_click || handle?.icon_click)
            d3img.on('click', function(evnt) { h.tree_click(evnt, this, 'icon'); });
      }

      const d3a = d3line.append('a');
      if (can_click || has_childs || break_list)
         d3a.attr('class', cssItem).on('click', function(evnt) { h.tree_click(evnt, this); });

      if (break_list) {
         hitem._break_point = true; // indicate that list was broken here
         d3a.attr('title', 'there are ' + (hitem._parent._childs.length - arg) + ' more items')
            .text('...more...');
         return false;
      }

      if ('disp_kind' in h) {
         if (settings.DragAndDrop && can_click)
            this.enableDrag(d3a, itemname);

         if (settings.ContextMenu && can_menu)
            d3a.on('contextmenu', function(evnt) { h.tree_contextmenu(evnt, this); });

         d3a.on('mouseover', function() { h.tree_mouseover(true, this); })
            .on('mouseleave', function() { h.tree_mouseover(false, this); });
      } else if (hitem._direct_context && settings.ContextMenu)
         d3a.on('contextmenu', function(evnt) { h.direct_contextmenu(evnt, this); });

      let element_name = hitem._name, element_title = '';

      if ('_realname' in hitem)
         element_name = hitem._realname;

      if ('_title' in hitem)
         element_title = hitem._title;

      if ('_fullname' in hitem)
         element_title += '  fullname: ' + hitem._fullname;

      if (!element_title)
         element_title = element_name;

      d3a.attr('title', element_title)
         .text(element_name + ('_value' in hitem ? ':' : ''))
         .style('background', hitem._background ? hitem._background : null);

      if ('_value' in hitem) {
         const d3p = d3line.append('p');
         if ('_vclass' in hitem)
            d3p.attr('class', hitem._vclass);
         if (!hitem._isopen)
            d3p.text(hitem._value);
      }

      if (has_childs && (isroot || hitem._isopen)) {
         const d3chlds = d3cont.append('div').attr('class', 'h_childs');
         if (this.show_overflow)
            d3chlds.style('overflow', 'initial');
         for (let i = 0; i < hitem._childs.length; ++i) {
            const chld = hitem._childs[i];
            chld._parent = hitem;
            if (!this.addItemHtml(chld, d3chlds, i))
               break; // if too many items, skip rest
         }
      }

      return true;
   }

   /** @summary Toggle open state of the item
     * @desc Used with 'expand all' / 'collapse all' buttons in normal GUI
     * @param {boolean} isopen - if items should be expand or closed
     * @return {boolean} true when any item was changed */
   toggleOpenState(isopen, h, promises) {
      const hitem = h || this.h;

      if (hitem._childs === undefined) {
         if (!isopen)
            return false;

         if (this.with_icons) {
            // in normal hierarchy check precisely if item can be expand
            if (!hitem._more && !hitem._expand && !this.canExpandItem(hitem))
               return false;
         }

         const pr = this.expandItem(this.itemFullName(hitem));
         if (isPromise(pr) && isObject(promises))
            promises.push(pr);
         if (hitem._childs !== undefined)
            hitem._isopen = true;
         return hitem._isopen;
      }

      if ((hitem !== this.h) && isopen && !hitem._isopen) {
         // when there are childs and they are not see, simply show them
         hitem._isopen = true;
         return true;
      }

      let change_child = false;
      for (let i = 0; i < hitem._childs.length; ++i) {
         if (this.toggleOpenState(isopen, hitem._childs[i], promises))
            change_child = true;
      }

      if ((hitem !== this.h) && !isopen && hitem._isopen && !change_child) {
         // if none of the childs can be closed, than just close that item
         delete hitem._isopen;
         return true;
      }

      if (!h)
         this.refreshHtml();
      return false;
   }

   /** @summary Expand to specified level
     * @protected */
   async expandToLevel(level) {
      if (!level || !Number.isFinite(level) || (level < 0))
         return this;

      const promises = [];
      this.toggleOpenState(true, this.h, promises);
      return Promise.all(promises).then(() => this.expandToLevel(level - 1));
   }

   /** @summary Expand to specified level
    * @deprecated will be removed in version 8, kept only for backward compatibility
     * @protected */
   async exapndToLevel(level) { return this.expandToLevel(level); }

   /** @summary Refresh HTML code of hierarchy painter
     * @return {Promise} when done */
   async refreshHtml() {
      const d3elem = this.selectDom();
      if (d3elem.empty())
         return this;

      d3elem.html('')   // clear html - most simple way
            .style('overflow', this.show_overflow ? 'auto' : 'hidden')
            .style('display', 'flex')
            .style('flex-direction', 'column');

      injectHStyle(d3elem.node());

      const h = this, factcmds = [];
      let status_item = null;
      this.forEachItem(item => {
         delete item._d3cont; // remove html container
         if (('_fastcmd' in item) && (item._kind === 'Command'))
            factcmds.push(item);
         if (('_status' in item) && !status_item)
            status_item = item;
      });

      if (!this.h || d3elem.empty())
         return this;

      if (factcmds.length) {
         const fastbtns = d3elem.append('div').attr('style', 'display: inline; vertical-align: middle; white-space: nowrap;');
         for (let n = 0; n < factcmds.length; ++n) {
            const btn = fastbtns.append('button')
                       .text('')
                       .attr('class', 'jsroot_fastcmd_btn')
                       .attr('item', this.itemFullName(factcmds[n]))
                       .attr('title', factcmds[n]._title)
                       .on('click', function() { h.executeCommand(select(this).attr('item'), this); });

            if (factcmds[n]._icon)
               btn.style('background-image', `url('${factcmds[n]._icon}')`);
         }
      }

      const d3btns = d3elem.append('p').attr('class', 'jsroot').style('margin-bottom', '3px').style('margin-top', 0);
      d3btns.append('a').attr('class', cssButton).text('expand all')
            .attr('title', 'expand all items in the browser').on('click', () => this.toggleOpenState(true));
      d3btns.append('text').text(' | ');
      d3btns.append('a').attr('class', cssButton).text('collapse all')
            .attr('title', 'collapse all items in the browser').on('click', () => this.toggleOpenState(false));

      if (isFunc(this.storeAsJson)) {
         d3btns.append('text').text(' | ');
         d3btns.append('a').attr('class', cssButton).text('json')
               .attr('title', 'dump to json file').on('click', () => this.storeAsJson());
      }

      if (isFunc(this.removeInspector)) {
         d3btns.append('text').text(' | ');
         d3btns.append('a').attr('class', cssButton).text('remove')
               .attr('title', 'remove inspector').on('click', () => this.removeInspector());
      }

      if ('_online' in this.h) {
         d3btns.append('text').text(' | ');
         d3btns.append('a').attr('class', cssButton).text('reload')
               .attr('title', 'reload object list from the server').on('click', () => this.reload());
      }

      if ('disp_kind' in this) {
         d3btns.append('text').text(' | ');
         d3btns.append('a').attr('class', cssButton).text('clear')
               .attr('title', 'clear all drawn objects').on('click', () => this.clearHierarchy(false));
      }

      const maindiv =
         d3elem.append('div')
               .attr('class', 'jsroot')
               .style('font-size', this.with_icons ? '12px' : '15px')
               .style('flex', '1');

      if (!this.show_overflow)
         maindiv.style('overflow', 'auto');

      if (this.background) {
         // case of object inspector and streamer infos display
         maindiv.style('background-color', this.background)
                .style('margin', '2px').style('padding', '2px');
      }
      if (this.textcolor)
         maindiv.style('color', this.textcolor);

      this.addItemHtml(this.h, maindiv.append('div').attr('class', cssTree));

      this.setTopPainter(); // assign this hierarchy painter as top painter

      if (status_item && !this.status_disabled && !decodeUrl().has('nostatus')) {
         const func = findFunction(status_item._status);
         if (isFunc(func)) {
            return this.createStatusLine().then(sdiv => {
               if (sdiv)
                  func(sdiv, this.itemFullName(status_item));
            });
         }
      }

      return this;
   }

   /** @summary Update item node
     * @private */
   updateTreeNode(hitem, d3cont) {
      if ((d3cont === undefined) || d3cont.empty()) {
         d3cont = select(hitem._d3cont ? hitem._d3cont : null);
         const name = this.itemFullName(hitem);
         if (d3cont.empty())
            d3cont = this.selectDom().select(`[item='${name}']`);
         if (d3cont.empty() && ('_cycle' in hitem))
            d3cont = this.selectDom().select(`[item='${name};${hitem._cycle}']`);
         if (d3cont.empty())
            return;
      }

      this.addItemHtml(hitem, d3cont, 'update');

      this.brlayout?.adjustBrowserSize(true);
   }

   /** @summary Update item background
     * @private */
   updateBackground(hitem, scroll_into_view) {
      if (!hitem || !hitem._d3cont)
         return;

      const d3cont = select(hitem._d3cont);
      if (d3cont.empty())
         return;

      const d3a = d3cont.select(`.${cssItem}`);
      d3a.style('background', hitem._background ? hitem._background : null);

      if (scroll_into_view && hitem._background)
         d3a.node().scrollIntoView(false);
   }

   /** @summary Focus on hierarchy item
     * @param {Object|string} hitem - item to open or its name
     * @desc all parents to the item will be opened first
     * @return {Promise} when done
     * @private */
   async focusOnItem(hitem) {
      if (isStr(hitem))
         hitem = this.findItem(hitem);

      const name = hitem ? this.itemFullName(hitem) : '';
      if (!name)
         return false;

      let itm = hitem, need_refresh = false;

      while (itm) {
         if ((itm._childs !== undefined) && !itm._isopen) {
            itm._isopen = true;
            need_refresh = true;
         }
         itm = itm._parent;
      }

      const promise = need_refresh ? this.refreshHtml() : Promise.resolve(true);

      return promise.then(() => {
         const d3cont = this.selectDom().select(`[item='${name}']`);
         if (d3cont.empty())
            return false;
         d3cont.node().scrollIntoView();
         return true;
      });
   }

   /** @summary Handler for click event of item in the hierarchy
     * @private */
   tree_click(evnt, node, place) {
      if (!node)
         return;

      let d3cont = select(node.parentNode.parentNode),
          itemname = d3cont.attr('item'),
          hitem = itemname ? this.findItem(itemname) : null;
      if (!hitem)
         return;

      if (place === 'parentminus') {
         let upcnt = select(node).property('upcnt') || 1;
         while (upcnt-- > 0)
            hitem = hitem?._parent;
         if (!hitem)
            return;
         itemname = this.itemFullName(hitem);
         d3cont = select(hitem?._d3cont || null);
         place = kPM;
      }

      if (hitem._break_point) {
         // special case of more item

         delete hitem._break_point;

         // update item itself
         this.addItemHtml(hitem, d3cont, 'update');

         const prnt = hitem._parent, indx = prnt._childs.indexOf(hitem),
               d3chlds = select(d3cont.node().parentNode);

         if (indx < 0)
            return console.error('internal error');

         prnt._show_limit = (prnt._show_limit || settings.HierarchyLimit) * 2;

         for (let n = indx + 1; n < prnt._childs.length; ++n) {
            const chld = prnt._childs[n];
            chld._parent = prnt;
            if (!this.addItemHtml(chld, d3chlds, n))
               break; // if too many items, skip rest
         }

         return;
      }

      let prnt = hitem, dflt;
      while (prnt) {
         if ((dflt = prnt._click_action) !== undefined)
            break;
         prnt = prnt._parent;
      }

      if (!place)
         place = 'item';
      const selector = (hitem._kind === getKindForType(clTKey) && hitem._more) ? 'noinspect' : '',
            sett = getDrawSettings(hitem._kind, selector), handle = sett.handle;

      if (place === 'icon') {
         let func = null;
         if (isFunc(hitem._icon_click))
            func = hitem._icon_click;
         else if (isFunc(handle?.icon_click))
            func = handle.icon_click;
         if (func && func(hitem, this))
            this.updateTreeNode(hitem, d3cont);
         return;
      }

      // special feature - all items with '_expand' function are not drawn by click
      if ((place === 'item') && ('_expand' in hitem) && !hitem._expand_miss && !evnt.ctrlKey && !evnt.shiftKey)
         place = kPM;

      // special case - one should expand item
      if (((place === kPM) && !('_childs' in hitem) && hitem._more) ||
          ((place === kPM) && handle?.pm) ||
          ((place === 'item') && (dflt === kExpand)))
         return this.expandItem(itemname, d3cont);

      if (place === 'item') {
         if (hitem._player)
            return this.player(itemname);

         if (handle?.aslink)
            return window.open(itemname + '/');

         if (handle?.execute)
            return this.executeCommand(itemname, node.parentNode);

         if (handle?.ignore_online && this.isOnlineItem(hitem))
            return;

         const dflt_expand = (this.default_by_click === kExpand);
         let can_draw = hitem._can_draw,
             can_expand = hitem._more,
             drawopt = '';

         if (evnt.shiftKey) {
            drawopt = handle?.shift || kInspect;
            if (isStr(drawopt) && (drawopt.indexOf(kInspect) === 0) && handle?.noinspect)
               drawopt = '';
         }
         if (evnt.ctrlKey && handle?.ctrl)
            drawopt = handle.ctrl;

         if (!drawopt && !handle?.always_draw) {
            for (let pitem = hitem._parent; pitem; pitem = pitem._parent) {
               if (pitem._painter) {
                  can_draw = false;
                  if (can_expand === undefined)
                     can_expand = false;
                  break;
               }
            }
         }

         if (hitem._childs)
            can_expand = false;

         if (can_draw === undefined)
            can_draw = sett.draw;
         if (can_expand === undefined)
            can_expand = sett.expand || sett.get_expand;

         if (can_draw && can_expand && !drawopt) {
            // if default action specified as expand, disable drawing
            // if already displayed, try to expand
            if (dflt_expand || (handle?.dflt === kExpand) || (handle?.expand_after_draw && this.isItemDisplayed(itemname)))
               can_draw = false;
         }

         if (can_draw && !drawopt)
            drawopt = kDfltDrawOpt;

         if (can_draw)
            return this.display(itemname, drawopt, null, true);

         if (can_expand || dflt_expand)
            return this.expandItem(itemname, d3cont);

         // cannot draw, but can inspect ROOT objects
         if (getTypeForKind(hitem._kind) && sett.inspect && (can_draw !== false))
            return this.display(itemname, kInspect, null, true);

         if (!hitem._childs || (hitem === this.h))
            return;
      }

      if (hitem._isopen)
         delete hitem._isopen;
      else
         hitem._isopen = true;

      this.updateTreeNode(hitem, d3cont);
   }

   /** @summary Handler for mouse-over event
     * @private */
   tree_mouseover(on, elem) {
      const itemname = select(elem.parentNode.parentNode).attr('item'),
            hitem = this.findItem(itemname);
      if (!hitem)
         return;

      let painter, prnt = hitem;
      while (prnt && !painter) {
         painter = prnt._painter;
         prnt = prnt._parent;
      }

      if (isFunc(painter?.mouseOverHierarchy))
         painter.mouseOverHierarchy(on, itemname, hitem);
   }

   /** @summary alternative context menu, used in the object inspector
     * @private */
   direct_contextmenu(evnt, elem) {
      evnt.preventDefault();
      const itemname = select(elem.parentNode.parentNode).attr('item'),
            hitem = this.findItem(itemname);
      if (!hitem)
         return;

      if (isFunc(this.fill_context)) {
         createMenu(evnt, this).then(menu => {
            this.fill_context(menu, hitem);
            if (menu.size() > 0) {
               menu.tree_node = elem.parentNode;
               menu.show();
            }
         });
      }
   }

   /** @summary Fills settings menu items
     * @private */
   fillSettingsMenu(menu, alone) {
      menu.addSettingsMenu(true, alone, arg => {
         if (arg === 'refresh') {
            this.forEachRootFile(folder => keysHierarchy(folder, folder._file.fKeys, folder._file, ''));
            this.refreshHtml();
         } else if (arg === 'dark')
            this.changeDarkMode();
         else if (arg === 'width')
            this.brlayout?.adjustSeparators(settings.BrowserWidth, null);
      });
   }

   /** @summary Handle changes of dark mode
     * @private */
   changeDarkMode() {
      if (this.textcolor) {
         this.setBasicColors();
         this.refreshHtml();
      }

      this.brlayout?.createStyle();
      this.createButtons(); // recreate buttons
      if (isFunc(this.disp?.changeDarkMode))
         this.disp.changeDarkMode();
      this.disp?.forEachFrame(frame => {
         const p = getElementCanvPainter(frame) || getElementMainPainter(frame);
         if (isFunc(p?.changeDarkMode) && (p !== this))
            p.changeDarkMode();
      });
   }

   /** @summary Toggle dark mode
     * @private */
   toggleDarkMode() {
      settings.DarkMode = !settings.DarkMode;
      this.changeDarkMode();
   }

   /** @summary Handle context menu in the hierarchy
     * @private */
   tree_contextmenu(evnt, elem) {
      evnt.preventDefault();
      const itemname = select(elem.parentNode.parentNode).attr('item'),
            hitem = this.findItem(itemname);
      if (!hitem)
         return;

      const onlineprop = this.getOnlineProp(itemname),
            fileprop = this.getFileProp(itemname);

      function qualifyURL(url) {
         const escapeHTML = s => s.split('&').join('&amp;').split('<').join('&lt;').split('"').join('&quot;'),
               el = document.createElement('div');
         el.innerHTML = `<a href="${escapeHTML(url)}">x</a>`;
         return el.firstChild.href;
      }

      createMenu(evnt, this).then(menu => {
         if ((!itemname || !hitem._parent) && !('_jsonfile' in hitem)) {
            let addr = '', cnt = 0;
            const files = [], separ = () => { return (cnt++ > 0) ? '&' : '?'; };

            this.forEachRootFile(item => files.push(item._file.fFullURL));

            if (!this.getTopOnlineItem())
               addr = exports.source_dir + 'index.htm';

            if (this.isMonitoring())
               addr += separ() + 'monitoring=' + this.getMonitoringInterval();

            if (files.length === 1)
               addr += `${separ()}file=${files[0]}`;
            else if (files.length > 1)
               addr += `${separ()}files=${JSON.stringify(files)}`;

            if (this.disp_kind)
               addr += separ() + 'layout=' + this.disp_kind.replace(/ /g, '');

            const items = [], opts = [];

            this.disp?.forEachFrame(frame => {
               const dummy = new ObjectPainter(frame);
               let top = dummy.getTopPainter(),
                   item = top ? top.getItemName() : null, opt;

               if (item)
                  opt = top.getDrawOpt() || top.getItemDrawOpt();
               else {
                  top = null;
                  dummy.forEachPainter(p => {
                     const _item = p.getItemName();
                     if (!_item)
                        return;
                     let _opt = p.getDrawOpt() || p.getItemDrawOpt() || '';
                     if (!top) {
                        top = p;
                        item = _item;
                        opt = _opt;
                     } else if (top.getPadPainter() === p.getPadPainter()) {
                        if (_opt.indexOf('same ') === 0)
                           _opt = _opt.slice(5);
                        item += '+' + _item;
                        opt += '+' + _opt;
                     }
                  });
               }

               if (item) {
                  items.push(item);
                  opts.push(opt || '');
               }
            });

            if (items.length === 1)
               addr += separ() + 'item=' + items[0] + separ() + 'opt=' + opts[0];
            else if (items.length > 1)
               addr += separ() + 'items=' + JSON.stringify(items) + separ() + 'opts=' + JSON.stringify(opts);


            menu.add('Direct link', () => window.open(addr));
            menu.add('Only items', () => window.open(addr + '&nobrowser'));
            this.fillSettingsMenu(menu);
         } else if (onlineprop)
            this.fillOnlineMenu(menu, onlineprop, itemname);
         else {
            const sett = getDrawSettings(hitem._kind, 'nosame');

            // allow to draw item even if draw function is not defined
            if (hitem._can_draw) {
               if (!sett.opts)
                  sett.opts = [''];
               if (sett.opts.indexOf('') < 0)
                  sett.opts.unshift('');
            }

            if (sett.opts) {
               menu.addDrawMenu('Draw', sett.opts, arg => this.display(itemname, arg),
                                'Draw item in the new frame');

               const active_frame = this.disp?.getActiveFrame();

               if (!sett.noappend && active_frame && (getElementCanvPainter(active_frame) || getElementMainPainter(active_frame))) {
                  menu.addDrawMenu('Superimpose', sett.opts, arg => this.dropItem(itemname, active_frame, arg),
                                   'Superimpose item with drawing on active frame');
               }
            }

            if (fileprop && sett.opts && !fileprop.localfile) {
               const url = settings.NewTabUrl || exports.source_dir;
               let filepath = qualifyURL(fileprop.fileurl);
               if (filepath.indexOf(url) === 0)
                  filepath = filepath.slice(url.length);
               filepath = `${fileprop.kind}=${filepath}`;
               if (fileprop.itemname) {
                  let name = fileprop.itemname;
                  if (name.search(/\+| |,/) >= 0)
                     name = `'${name}'`;
                  filepath += `&item=${name}`;
               }

               let arg0 = 'nobrowser';
               if (settings.WithCredentials)
                  arg0 += '&with_credentials';
               if (settings.NewTabUrlPars)
                  arg0 += '&' + settings.NewTabUrlPars;
               if (settings.NewTabUrlExportSettings) {
                  if (gStyle.fOptStat !== 1111)
                     arg0 += `&optstat=${gStyle.fOptStat}`;
                  if (gStyle.fOptFit)
                     arg0 += `&optfit=${gStyle.fOptFit}`;
                  if (gStyle.fOptDate)
                     arg0 += `&optdate=${gStyle.fOptDate}`;
                  if (gStyle.fOptFile)
                     arg0 += `&optfile=${gStyle.fOptFile}`;
                  if (gStyle.fOptTitle !== 1)
                     arg0 += `&opttitle=${gStyle.fOptTitle}`;
                  if (settings.TimeZone === 'UTC')
                     arg0 += '&utc';
                  else if (settings.TimeZone === 'Europe/Berlin')
                     arg0 += '&cet';
                  else if (settings.TimeZone)
                     arg0 += `&timezone='${settings.TimeZone}'`;
                  if (Math.abs(gStyle.fDateX - 0.01) > 1e-3)
                     arg0 += `&datex=${gStyle.fDateX.toFixed(3)}`;
                  if (Math.abs(gStyle.fDateY - 0.01) > 1e-3)
                     arg0 += `&datey=${gStyle.fDateY.toFixed(3)}`;
                  if (gStyle.fHistMinimumZero)
                     arg0 += '&histzero';
                  if (settings.DarkMode)
                     arg0 += '&dark=on';
                  if (!settings.UseStamp)
                     arg0 += '&usestamp=off';
                  if (settings.OnlyLastCycle)
                     arg0 += '&lastcycle';
                  if (settings.OptimizeDraw !== 1)
                     arg0 += `&optimize=${settings.OptimizeDraw}`;
                  if (settings.MaxRanges !== 200)
                     arg0 += `&maxranges=${settings.MaxRanges}`;
                  if (settings.FuncAsCurve)
                     arg0 += '&tf1=curve';
                  if (!settings.ToolBar && !settings.Tooltip && !settings.ContextMenu && !settings.Zooming && !settings.MoveResize && !settings.DragAndDrop)
                     arg0 += '&interactive=0';
                  else if (!settings.ContextMenu)
                     arg0 += '&nomenu';
               }

               menu.addDrawMenu('Draw in new tab', sett.opts,
                                arg => window.open(`${url}?${arg0}&${filepath}&opt=${arg}`),
                                'Draw item in the new browser tab or window');
            }

            if ((sett.expand || sett.get_expand) && (hitem._more || hitem._more === undefined)) {
               if (hitem._childs === undefined)
                  menu.add('Expand', () => this.expandItem(itemname), 'Expand content of object');
               else {
                  menu.add('Unexpand', () => {
                     hitem._more = true;
                     delete hitem._childs;
                     delete hitem._isopen;
                     if (hitem.expand_item)
                        delete hitem._expand;
                     this.updateTreeNode(hitem);
                  }, 'Remove all childs from hierarchy');
               }
            }

            if (hitem._kind === getKindForType(clTStyle))
               menu.add('Apply', () => this.applyStyle(itemname));
         }

         if (isFunc(hitem._menu))
            hitem._menu(menu, hitem, this);

         if (menu.size() > 0) {
            menu.tree_node = elem.parentNode;
            if (menu.separ)
               menu.separator(); // add separator at the end
            menu.add('Close');
            menu.show();
         }
      }); // end menu creation

      return false;
   }

   /** @summary Starts player for specified item
     * @desc Same as 'Player' context menu
     * @param {string} itemname - item name for which player should be started
     * @param {string} [option] - extra options for the player
     * @return {Promise} when ready */
   async player(itemname, option) {
      const item = this.findItem(itemname);

      if (!isStr(item?._player))
         return null;

      let player_func;

      if (item._module) {
         const hh = await this.importModule(item._module);
         player_func = hh ? hh[item._player] : null;
      } else {
         if (item._prereq || (item._player.indexOf('JSROOT.') >= 0))
            await this.loadScripts('', item._prereq);
         player_func = findFunction(item._player);
      }

      if (!isFunc(player_func))
         return null;

      await this.createDisplay();
      return player_func(this, itemname, option);
   }

   /** @summary Checks if item can be displayed with given draw option
     * @private */
   canDisplay(item, drawopt) {
      if (!item)
         return false;
      if (item._player)
         return true;
      if (item._can_draw !== undefined)
         return item._can_draw;
      if (isStr(drawopt) && (drawopt.indexOf(kInspect) === 0))
         return true;
      const handle = getDrawHandle(item._kind, drawopt);
      return canDrawHandle(handle);
   }

   /** @summary Returns true if given item displayed
     * @param {string} itemname - item name */
   isItemDisplayed(itemname) {
      const mdi = this.getDisplay();
      return mdi?.findFrame(itemname) !== null;
   }

   /** @summary Display specified item
     * @param {string} itemname - item name
     * @param {string} [drawopt] - draw option for the item
     * @param {string|Object} [dom] - place where to draw item, same as for @ref draw function
     * @param {boolean} [interactive] - if display was called in interactive mode, will activate selected drawing
     * @return {Promise} with created painter object */
   async display(itemname, drawopt, dom = null, interactive = false) {
      const display_itemname = itemname;
      let painter = null,
          updating = false,
          item = null,
          frame_name = itemname;

      // only to support old API where dom was not there
      if ((dom === true) || (dom === false)) {
         interactive = dom;
         dom = null;
      }

      if (isStr(dom) && (dom.indexOf('frame:') === 0)) {
         frame_name = dom.slice(6);
         dom = null;
      }

      const complete = (respainter, err) => {
         if (err)
            console.log('When display ', itemname, 'got', err);

         if (updating && item)
            delete item._doing_update;
         if (!updating)
            showProgress();
         if (isFunc(respainter?.setItemName)) {
            respainter.setItemName(display_itemname, updating ? null : drawopt, this); // mark painter as created from hierarchy

            if (item && !item._painter)
               item._painter = respainter;
         }

         return respainter || painter;
      };

      return this.createDisplay().then(mdi => {
         if (!mdi)
            return complete();

         item = this.findItem(display_itemname);

         if (item?._player)
            return this.player(display_itemname, drawopt).then(res => complete(res));

         updating = isStr(drawopt) && (drawopt.indexOf('update:') === 0);

         if (updating) {
            drawopt = drawopt.slice(7);
            if (!item || item._doing_update)
               return complete();
            item._doing_update = true;
         }

         if (item && !this.canDisplay(item, drawopt))
            return complete();

         const use_dflt_opt = drawopt === kDfltDrawOpt;
         if (use_dflt_opt)
            drawopt = '';

         if (!updating)
            showProgress(`Loading ${display_itemname} ...`);

         return this.getObject(display_itemname, drawopt).then(result => {
            if (!updating)
               showProgress();

            if (!item)
               item = result.item;
            let obj = result.obj;

            if (!obj)
               return complete();

            if (!updating)
               showProgress(`Drawing ${display_itemname} ...`);

            let handle = obj._typename ? getDrawHandle(getKindForType(obj._typename)) : null;

            if (handle?.draw_field && obj[handle.draw_field]) {
               obj = obj[handle.draw_field];
               if (!drawopt)
                  drawopt = handle.draw_field_opt || '';
               handle = obj._typename ? getDrawHandle(getKindForType(obj._typename)) : null;
            }

            if (use_dflt_opt && !drawopt && handle?.dflt && (handle.dflt !== kExpand))
               drawopt = handle.dflt;

            if (dom) {
               const func = updating ? redraw : draw;
               return func(dom, obj, drawopt).then(p => complete(p)).catch(err => complete(null, err));
            }

            let did_activate = false;
            const arr = [];

            mdi.forEachPainter((p, frame) => {
               if (p.getItemName() !== display_itemname)
                  return;

               const itemopt = p.getItemDrawOpt();
               if (use_dflt_opt && interactive)
                  drawopt = itemopt;

               // verify that object was drawn with same option as specified now (if any)
               if (!updating && drawopt && (itemopt !== drawopt))
                  return;

               if (interactive && !did_activate) {
                  did_activate = true;
                  mdi.activateFrame(frame);
               }

               if (isFunc(p.redrawObject)) {
                  const pr = p.redrawObject(obj, drawopt);

                  if (pr) {
                     painter = p;
                     arr.push(pr);
                  }
               }
            });

            if (painter)
               return Promise.all(arr).then(() => complete());

            if (updating) {
               console.warn(`something went wrong - did not found painter when doing update of ${display_itemname}`);
               return complete();
            }

            const frame = mdi.findFrame(frame_name, true);
            cleanup(frame);
            mdi.activateFrame(frame);

            return draw(frame, obj, drawopt)
                         .then(p => complete(p))
                         .catch(err => complete(null, err));
         });
      });
   }

   /** @summary Enable drag of the element
     * @private  */
   enableDrag(d3elem /* , itemname */) {
      d3elem.attr('draggable', 'true').on('dragstart', function(ev) {
         const itemname = this.parentNode.parentNode.getAttribute('item');
         ev.dataTransfer.setData('item', itemname);
      });
   }

   /** @summary Enable drop on the frame
     * @private  */
   enableDrop(frame) {
      const h = this;
      select(frame).on('dragover', ev => {
         const itemname = ev.dataTransfer.getData('item'),
               ditem = h.findItem(itemname);
         if (getTypeForKind(ditem?._kind))
            ev.preventDefault(); // let accept drop, otherwise it will be refused
      }).on('dragenter', function() {
         select(this).classed('jsroot_drag_area', true);
      }).on('dragleave', function() {
         select(this).classed('jsroot_drag_area', false);
      }).on('drop', function(ev) {
         select(this).classed('jsroot_drag_area', false);
         const itemname = ev.dataTransfer.getData('item');
         if (!itemname)
            return;
         const painters = [], elements = [];
         let pad_painter = getElementCanvPainter(this),
             target = ev.target;
         pad_painter?.forEachPainter(pp => {
            painters.push(pp);
            elements.push(pp.getPadSvg().node());
         }, 'pads');
         // only if there are sub-pads - try to find them
         if (painters.length > 1) {
            while (target && (target !== this)) {
               const p = elements.indexOf(target);
               if (p > 0) {
                  pad_painter = painters[p];
                  break;
               }
               target = target.parentNode;
            }
         }
         h.dropItem(itemname, pad_painter || this);
      });
   }

   /** @summary Remove all drop handlers on the frame
     * @private  */
   clearDrop(frame) {
      select(frame).on('dragover', null).on('dragenter', null).on('dragleave', null).on('drop', null);
   }

   /** @summary Drop item on specified element for drawing
    * @return {Promise} when completed
    * @private */
   async dropItem(itemname, dom, opt) {
      if (!opt || !isStr(opt))
         opt = '';

      const drop_complete = (drop_painter, is_main) => {
         if (!is_main && isFunc(drop_painter?.setItemName))
            drop_painter.setItemName(itemname, null, this);
         return drop_painter;
      };

      if (itemname === '$legend') {
         const cp = getElementCanvPainter(dom);
         if (isFunc(cp?.buildLegend))
            return cp.buildLegend(0, 0, 0, 0, '', opt).then(lp => drop_complete(lp));
         console.error('Not possible to build legend');
         return drop_complete(null);
      }

      return this.getObject(itemname).then(res => {
         if (!res.obj)
            return null;

         const mp = getElementMainPainter(dom);

         if (isFunc(mp?.performDrop))
            return mp.performDrop(res.obj, itemname, res.item, opt).then(p => drop_complete(p, mp === p));

         const sett = res.obj._typename ? getDrawSettings(getKindForType(res.obj._typename)) : null;
         if (!sett?.draw)
            return null;

         const cp = getElementCanvPainter(dom);

         if (cp) {
            if (sett?.has_same && mp)
               opt = 'same ' + opt;
         } else
            this.cleanupFrame(dom);

         // if drop on sub-pad painter - call add pad buttons
         if (isFunc(dom?.addPadButtons))
            dom.addPadButtons();

         return draw(dom, res.obj, opt).then(p => drop_complete(p, mp === p));
      });
   }

   /** @summary Update specified items
     * @desc Method can be used to fetch new objects and update all existing drawings
     * @param {string|array|boolean} arg - either item name or array of items names to update or true if only automatic items will be updated
     * @return {Promise} when ready */
   async updateItems(arg) {
      if (!this.disp)
         return false;

      const allitems = [], options = [];
      let only_auto_items = false, want_update_all = false;

      if (isStr(arg))
         arg = [arg];
      else if (!isObject(arg)) {
         if (arg === undefined)
            arg = !this.isMonitoring();
         want_update_all = true;
         only_auto_items = Boolean(arg);
      }

      // first collect items
      this.disp.forEachPainter(p => {
         const itemname = p.getItemName();

         if (!isStr(itemname) || (allitems.indexOf(itemname) >= 0))
            return;

         if (want_update_all) {
            const item = this.findItem(itemname);
            if (!item || item._not_monitor || item._player)
               return;
            if (!('_always_monitor' in item)) {
               const handle = getDrawHandle(item._kind);
               let forced = false;
               if (handle?.monitor !== undefined) {
                  if ((handle.monitor === false) || (handle.monitor === 'never'))
                     return;
                  if (handle.monitor === 'always')
                     forced = true;
               }
               if (!forced && only_auto_items)
                  return;
            }
         } else if (arg.indexOf(itemname) < 0)
            return;

         allitems.push(itemname);
         options.push('update:' + p.getItemDrawOpt());
      }, true); // only visible panels are considered

      // force all files to read again (normally in non-browser mode)
      if (this.files_monitoring && !only_auto_items && want_update_all) {
         this.forEachRootFile(item => {
            this.forEachItem(fitem => { delete fitem._readobj; }, item);
            delete item._file;
         });
      }

      return this.displayItems(allitems, options);
   }

   /** @summary Display all provided elements
     * @return {Promise} when drawing finished
     * @private */
   async displayItems(items, options) {
      if (!items?.length)
         return true;

      const h = this;

      if (!options)
         options = [];
      while (options.length < items.length)
         options.push(kDfltDrawOpt);

      if ((options.length === 1) && (options[0] === 'iotest')) {
         this.clearHierarchy();
         select('#' + this.disp_frameid).html('').append('h2').text('Start I/O test');

         const tm0 = new Date();
         return this.getObject(items[0]).then(res => {
            const tm1 = new Date();
            select('#' + this.disp_frameid).append('h2').text(`Item ${items[0]} reading ` + (res?.obj ? `type ${res?.obj._typename} time = ${tm1.getTime() - tm0.getTime()}ms` : 'fail'));
            return true;
         });
      }

      const dropitems = new Array(items.length),
            dropopts = new Array(items.length),
            images = new Array(items.length);

      // First of all check that items are exists, look for cycle extension and plus sign
      for (let i = 0; i < items.length; ++i) {
         dropitems[i] = dropopts[i] = null;

         const item = items[i];
         let can_split = true;

         if (item?.indexOf('img:') === 0) {
            images[i] = true;
            continue;
         }

         if ((item?.length > 1) && (item.at(0) === '\'') && (item.at(-1) === '\'')) {
            items[i] = item.slice(1, item.length - 1);
            can_split = false;
         }

         let elem = h.findItem({ name: items[i], check_keys: true });
         if (elem) {
            items[i] = h.itemFullName(elem);
            continue;
         }

         if (can_split && (items[i].at(0) === '[') && (items[i].at(-1) === ']')) {
            dropitems[i] = parseAsArray(items[i]);
            items[i] = dropitems[i].shift();
         } else if (can_split && (items[i].indexOf('+') > 0)) {
            dropitems[i] = items[i].split('+');
            items[i] = dropitems[i].shift();
         }

         if (dropitems[i]?.length) {
            // allow to specify _same_ item in different file
            for (let j = 0; j < dropitems[i].length; ++j) {
               const pos = dropitems[i][j].indexOf('_same_');
               if ((pos > 0) && (h.findItem(dropitems[i][j]) === null))
                  dropitems[i][j] = dropitems[i][j].slice(0, pos) + items[i].slice(pos);

               elem = h.findItem({ name: dropitems[i][j], check_keys: true });
               if (elem)
                  dropitems[i][j] = h.itemFullName(elem);
            }

            if ((options[i].at(0) === '[') && (options[i].at(-1) === ']')) {
               dropopts[i] = parseAsArray(options[i]);
               options[i] = dropopts[i].shift();
            } else if (options[i].indexOf('+') > 0) {
               dropopts[i] = options[i].split('+');
               options[i] = dropopts[i].shift();
            } else
               dropopts[i] = [];


            while (dropopts[i].length < dropitems[i].length)
               dropopts[i].push('');
         }

         // also check if subsequent items has _same_, than use name from first item
         const pos = items[i].indexOf('_same_');
         if ((pos > 0) && !h.findItem(items[i]) && (i > 0))
            items[i] = items[i].slice(0, pos) + items[0].slice(pos);

         elem = h.findItem({ name: items[i], check_keys: true });
         if (elem)
            items[i] = h.itemFullName(elem);
      }

      // now check that items can be displayed
      for (let n = items.length - 1; n >= 0; --n) {
         if (images[n])
            continue;
         const hitem = h.findItem(items[n]);
         if (!hitem || h.canDisplay(hitem, options[n]))
            continue;
         // try to expand specified item
         h.expandItem(items[n], null, true);
         items.splice(n, 1);
         options.splice(n, 1);
         dropitems.splice(n, 1);
      }

      if (!items.length)
         return true;

      const frame_names = new Array(items.length), items_wait = new Array(items.length);
      for (let n = 0; n < items.length; ++n) {
         items_wait[n] = 0;
         let fname = items[n], k = 0;
         if (items.indexOf(fname) < n)
            items_wait[n] = true; // if same item specified, one should wait first drawing before start next
         const p = options[n].indexOf('frameid:');
         if (p >= 0) {
            fname = options[n].slice(p + 8);
            options[n] = options[n].slice(0, p);
         } else {
            while (frame_names.indexOf(fname) >= 0)
               fname = items[n] + '_' + k++;
         }
         frame_names[n] = fname;
      }

      // now check if several same items present - select only one for the drawing
      // if draw option includes 'main', such item will be drawn first
      for (let n = 0; n < items.length; ++n) {
         if (items_wait[n] !== 0)
            continue;
         let found_main = n;
         for (let k = 0; k < items.length; ++k) {
            if ((items[n] === items[k]) && (options[k].indexOf('main') >= 0))
               found_main = k;
         }
         for (let k = 0; k < items.length; ++k) {
            if (items[n] === items[k])
               items_wait[k] = (found_main !== k);
         }
      }

      return this.createDisplay().then(mdi => {
         if (!mdi)
            return false;

         const doms = new Array(items.length);

         // Than create empty frames for each item
         for (let i = 0; i < items.length; ++i) {
            if (options[i].indexOf('update:')) {
               mdi.createFrame(frame_names[i]);
               doms[i] = 'frame:' + frame_names[i];
            }
         }

         function dropNextItem(indx, painter) {
            if (painter && dropitems[indx]?.length)
               return h.dropItem(dropitems[indx].shift(), painter.getDrawDom(), dropopts[indx].shift()).then(() => dropNextItem(indx, painter));

            dropitems[indx] = null; // mark that all drop items are processed
            items[indx] = null; // mark item as ready

            for (let cnt = 0; cnt < items.length; ++cnt) {
               if (items[cnt] === null)
                  continue; // ignore completed item
               if (items_wait[cnt] && items.indexOf(items[cnt]) === cnt) {
                  items_wait[cnt] = false;
                  return h.display(items[cnt], options[cnt], doms[cnt]).then(drop_painter => dropNextItem(cnt, drop_painter));
               }
            }
         }

         const promises = [];

         if (this.#one_by_one) {
            function processNext(indx) {
               if (indx >= items.length)
                  return true;
               if (items_wait[indx])
                  return processNext(indx + 1);
               return h.display(items[indx], options[indx], doms[indx])
                       .then(painter => dropNextItem(indx, painter))
                       .then(() => processNext(indx + 1));
            }
            promises.push(processNext(0));
         } else {
            // We start display of all items parallel, but only if they are not the same
            for (let i = 0; i < items.length; ++i) {
               if (!items_wait[i])
                  promises.push(h.display(items[i], options[i], doms[i]).then(painter => dropNextItem(i, painter)));
            }
         }

         return Promise.all(promises).then(() => {
            if (mdi?.createFinalBatchFrame && isBatchMode() && !isNodeJs())
               mdi.createFinalBatchFrame();
         });
      });
   }

   /** @summary Reload hierarchy and refresh html code
     * @return {Promise} when completed */
   async reload() {
      if ('_online' in this.h)
         return this.openOnline(this.h._online).then(() => this.refreshHtml());
      return false;
   }

   /** @summary activate (select) specified item
     * @param {Array} items - array of items names
     * @param {boolean} [force] - if specified, all required sub-levels will be opened
     * @private */
   activateItems(items, force) {
      if (isStr(items))
         items = [items];

      const active = [], // array of elements to activate
            update = []; // array of elements to update
      this.forEachItem(item => {
         if (item._background) {
            active.push(item);
            delete item._background;
         }
      });

      const find_next = (itemname, prev_found) => {
         if (itemname === undefined) {
            // extract next element
            if (!items.length) {
               update.reverse().forEach(node => this.updateTreeNode(node));
               active.forEach(item => this.updateBackground(item, force));
               return;
            }
            itemname = items.shift();
         }

         let hitem = this.findItem(itemname);

         if (!hitem) {
            const d = this.findItem({ name: itemname, last_exists: true, check_keys: true, allow_index: true });
            if (!d || !d.last)
               return find_next();
            d.now_found = this.itemFullName(d.last);

            if (force) {
               // if after last expand no better solution found - skip it
               if ((prev_found !== undefined) && (d.now_found === prev_found))
                  return find_next();

               return this.expandItem(d.now_found).then(res => {
                  if (!res)
                     return find_next();
                  let newname = this.itemFullName(d.last);
                  if (newname)
                     newname += '/';
                  find_next(newname + d.rest, d.now_found);
               });
            }
            hitem = d.last;
         }

         if (hitem) {
            // check that item is visible (opened), otherwise should enable parent

            let prnt = hitem._parent;
            while (prnt) {
               if (!prnt._isopen) {
                  if (force) {
                     prnt._isopen = true;
                     if (update.indexOf(prnt) < 0)
                        update.push(prnt);
                  } else {
                     hitem = prnt; break;
                  }
               }
               prnt = prnt._parent;
            }

            hitem._background = 'LightSteelBlue';
            if (active.indexOf(hitem) < 0)
               active.push(hitem);
         }

         find_next();
      };

      if (force && this.brlayout) {
         if (!this.brlayout.browser_kind)
            return this.createBrowser('float', true).then(() => find_next());
         if (!this.brlayout.browser_visible)
            this.brlayout.toggleBrowserVisisbility();
      }

      // use recursion
      find_next();
   }

   /** @summary Check if item can be (potentially) expand
     * @private */
   canExpandItem(item) {
      if (!item)
         return false;
      if (item._expand)
         return true;
      const handle = getDrawHandle(item._kind, '::expand');
      return handle && canExpandHandle(handle);
   }

   /** @summary expand specified item
     * @param {String} itemname - item name
     * @return {Promise} when ready */
   async expandItem(itemname, d3cont, silent) {
      const hitem = this.findItem(itemname), hpainter = this;

      if (!hitem && d3cont)
         return;

      function doneExpandItem(_item) {
         if (_item._childs === undefined)
            _item._expand_miss = true;
         else {
            _item._isopen = true;
            if (_item._parent && !_item._parent._isopen) {
               _item._parent._isopen = true; // also show parent
               if (!silent)
                  hpainter.updateTreeNode(_item._parent);
            } else if (!silent)
               hpainter.updateTreeNode(_item, d3cont);
         }
         return _item;
      }

      async function doExpandItem(_item, _obj) {
         delete _item._expand_miss;

         if (isStr(_item._expand))
            _item._expand = findFunction(_item._expand);

         if (!isFunc(_item._expand)) {
            let handle = getDrawHandle(_item._kind, '::expand');

            // in inspector show all members
            if (handle?.expand_item && !hpainter._inspector) {
               _obj = _obj[handle.expand_item];
               _item.expand_item = handle.expand_item; // remember that was expand item
               handle = _obj?._typename ? getDrawHandle(getKindForType(_obj._typename), '::expand') : null;
            }

            if (handle?.expand || handle?.get_expand) {
               if (isFunc(handle.expand))
                  _item._expand = handle.expand;
               else if (isStr(handle.expand)) {
                  if (!internals.ignore_v6) {
                     const v6 = await exports._ensureJSROOT();
                     await v6.require(handle.prereq);
                     await v6._complete_loading();
                  }
                  _item._expand = handle.expand = findFunction(handle.expand);
               } else if (isFunc(handle.get_expand))
                  _item._expand = handle.expand = await handle.get_expand();
            }
         }

         // try to use expand function
         if (_obj && isFunc(_item._expand)) {
            const res = _item._expand(_item, _obj);
            if (res)
               return getPromise(res).then(() => doneExpandItem(_item));
         }

         if (_obj && objectHierarchy(_item, _obj))
            return doneExpandItem(_item);

         // mark as expand miss - behaves as normal object
         _item._expand_miss = true;
         return -1;
      }

      let promise = Promise.resolve(-1);

      if (hitem) {
         // item marked as it cannot be expanded, also top item cannot be changed
         if ((hitem._more === false) || (!hitem._parent && hitem._childs))
            return;

         // for the file expand always just toggle isopen flag
         if (hitem._childs && (hitem._isopen || hitem._file)) {
            hitem._isopen = !hitem._isopen;
            if (!silent)
               this.updateTreeNode(hitem, d3cont);
            return;
         }

         if (hitem._obj)
            promise = doExpandItem(hitem, hitem._obj);
      }

      return promise.then(res => {
         if (res !== -1)
            return res; // done

         showProgress('Loading ' + itemname);

         return this.getObject(itemname, silent ? 'hierarchy_expand' : 'hierarchy_expand_verbose').then(res2 => {
            showProgress();
            if (res2.obj)
               return doExpandItem(res2.item, res2.obj).then(res3 => { return res3 !== -1 ? res3 : undefined; });
         });
      });
   }

   /** @summary Return main online item
     * @private */
   getTopOnlineItem(item) {
      if (item) {
         while (item && (!('_online' in item)))
            item = item._parent;
         return item;
      }

      if (!this.h)
         return null;
      if (this.h._online)
         return this.h;
      if (this.h._childs && this.h._childs[0]?._online)
         return this.h._childs[0];
      return null;
   }

   /** @summary Call function for each item which corresponds to JSON file
     * @private */
   forEachJsonFile(func) {
      if (!this.h)
         return;

      if (this.h._jsonfile)
         return func(this.h);

      this.h._childs?.forEach(item => {
         if (item._jsonfile)
            func(item);
      });
   }

   /** @summary Open JSON file
     * @param {string} filepath - URL to JSON file
     * @return {Promise} when object ready */
   async openJsonFile(filepath) {
      let isfileopened = false;
      this.forEachJsonFile(item => {
         if (item._jsonfile === filepath)
            isfileopened = true;
      });
      if (isfileopened)
         return;

      return httpRequest(filepath, 'object').then(res2 => {
         if (!res2)
            return;
         const h1 = { _jsonfile: filepath, _kind: getKindForType(res2._typename), _jsontmp: res2, _name: filepath.split('/').pop() };
         if (res2.fTitle)
            h1._title = res2.fTitle;
         h1._get = function(item /* ,itemname */) {
            if (item._jsontmp)
               return Promise.resolve(item._jsontmp);
            return httpRequest(item._jsonfile, 'object')
                         .then(res3 => {
                            item._jsontmp = res3;
                            return res3;
                         });
         };
         if (!this.h)
            this.h = h1;
         else if (this.h._kind === kTopFolder)
            this.h._childs.push(h1);
         else {
            const h0 = this.h, topname = h0?._jsonfile ? 'Files' : 'Items';
            this.h = { _name: topname, _kind: kTopFolder, _childs: [h0, h1] };
         }

         return this.refreshHtml();
      });
   }

   /** @summary Call function for each item which corresponds to ROOT file
     * @private */
   forEachRootFile(func) {
      if (!this.h)
         return;
      if ((this.h._kind === kindTFile) && this.h._file)
         return func(this.h);

      this.h._childs?.forEach(item => {
         if ((item._kind === kindTFile) && item._fullurl)
            func(item);
      });
   }

   /** @summary Find ROOT file which corresponds to provided item name
     * @private */
   findRootFileForItem(itemname) {
      let item = this.findItem(itemname);
      while (item) {
         if ((item._kind === kindTFile) && item._fullurl && item._file)
            return item;
         item = item?._parent;
      }
      return null;
   }

   /** @summary Open ROOT file
     * @param {string} filepath - URL to ROOT file, argument for openFile
     * @return {Promise} when file is opened */
   async openRootFile(filepath) {
      let isfileopened = false;
      this.forEachRootFile(item => {
         if (item._fullurl === filepath)
            isfileopened = true;
      });
      if (isfileopened)
         return;

      const msg = isStr(filepath) ? filepath : 'file';

      showProgress(`Opening ${msg} ...`);

      return openFile(filepath).then(file => {
         const h1 = this.fileHierarchy(file);
         h1._isopen = true;
         if (!this.h) {
            this.h = h1;
            if (this.#topname)
               h1._name = this.#topname;
         } else if (this.h._kind === kTopFolder)
            this.h._childs.push(h1);
         else {
            const h0 = this.h, topname = (h0._kind === kindTFile) ? 'Files' : 'Items';
            this.h = { _name: topname, _kind: kTopFolder, _childs: [h0, h1], _isopen: true };
         }

         return this.refreshHtml();
      }).catch(() => {
         // make CORS warning
         if (isBatchMode())
            console.error(`Fail to open ${msg} - check CORS headers`);
         else if (!select('#gui_fileCORS').style('background', 'red').empty())
            setTimeout(() => select('#gui_fileCORS').style('background', ''), 5000);
         return false;
      }).finally(() => showProgress());
   }

   /** @summary Create list of files for specified directory */
   async listServerDir(dirname) {
      return httpRequest(dirname, 'text').then(res => {
         if (!res)
            return false;
         const h = { _name: 'Files', _kind: kTopFolder, _childs: [], _isopen: true }, fmap = {};
         let p = 0;
         while (p < res.length) {
            p = res.indexOf('a href="', p + 1);
            if (p < 0)
               break;
            p += 8;
            const p2 = res.indexOf('"', p + 1);
            if (p2 < 0)
               break;

            const fname = res.slice(p, p2);
            p = p2 + 1;

            if (fmap[fname])
               continue;
            fmap[fname] = true;

            if ((fname.lastIndexOf('.root') === fname.length - 5) && (fname.length > 5)) {
               h._childs.push({
                  _name: fname, _title: dirname + fname, _url: dirname + fname, _kind: kindTFile,
                  _click_action: kExpand, _more: true, _obj: {},
                  _expand: item => {
                     return openFile(item._url).then(file => {
                        if (!file)
                           return false;
                        delete item._expand;
                        delete item._more;
                        delete item._click_action;
                        delete item._obj;
                        item._isopen = true;
                        this.fileHierarchy(file, item);
                        this.updateTreeNode(item);
                     });
                  }
               });
            } else if (((fname.lastIndexOf('.json.gz') === fname.length - 8) && (fname.length > 8)) ||
                       ((fname.lastIndexOf('.json') === fname.length - 5) && (fname.length > 5))) {
               h._childs.push({
                  _name: fname, _title: dirname + fname, _jsonfile: dirname + fname, _can_draw: true,
                  _get: item => {
                     return httpRequest(item._jsonfile, 'object').then(res2 => {
                        if (res2) {
                           item._kind = getKindForType(res2._typename);
                           item._jsontmp = res2;
                           this.updateTreeNode(item);
                        }
                        return res2;
                     });
                  }
               });
            }
         }
         if (h._childs.length)
            this.h = h;
         return true;
      });
   }

   /** @summary Apply loaded TStyle object
     * @desc One also can specify item name of JSON file name where style is loaded
     * @param {object|string} style - either TStyle object of item name where object can be load */
   async applyStyle(style) {
      if (!style)
         return true;

      let pr = Promise.resolve(style);

      if (isStr(style)) {
         const item = this.findItem({ name: style, allow_index: true, check_keys: true });
         if (item !== null)
            pr = this.getObject(item).then(res => res.obj);
         else if (style.indexOf('.json') > 0)
            pr = httpRequest(style, 'object');
      }

      return pr.then(st => {
         if (st?._typename === clTStyle)
            Object.assign(gStyle, st);
      });
   }

   /** @summary Provides information about file item
     * @private */
   getFileProp(itemname) {
      let item = this.findItem(itemname);
      if (!item)
         return null;

      itemname = item._name;
      while (item._parent) {
         item = item._parent;
         if (item._file)
            return { kind: 'file', fileurl: item._file.fURL, itemname, localfile: Boolean(item._file.fLocalFile) };

         if (item._jsonfile)
            return { kind: 'json', fileurl: item._jsonfile, itemname };

         itemname = item._name + '/' + itemname;
      }

      return null;
   }

   /** @summary Provides URL for online item
     * @desc Such URL can be used  to request data from the server
     * @return string or null if item is not online
     * @private */
   getOnlineItemUrl(item) {
      if (isStr(item))
         item = this.findItem(item);
      let prnt = item;
      while (prnt && (prnt._online === undefined))
         prnt = prnt._parent;
      return prnt ? (prnt._online + this.itemFullName(item, prnt)) : null;
   }

   /** @summary Returns true if item is online
     * @private */
   isOnlineItem(item) {
      return this.getOnlineItemUrl(item) !== null;
   }

   /** @summary Dynamic module import, supports special shortcuts from core or draw_tree
     * @return {Promise} with module
     * @private */
   async importModule(module) {
      switch (module) {
         case 'core': return Promise.resolve().then(function () { return core; });
         case 'draw_tree': return Promise.resolve().then(function () { return TTree; });
         case 'hierarchy': return { HierarchyPainter, markAsStreamerInfo };
      }
      return import(/* webpackIgnore: true */ module);
   }

   /** @summary set cached object for gui drawing
     * @private */
   setCachedObject(obj) {
      this.#cached_draw_object = obj;
   }

   /** @summary method used to request object from the http server
     * @return {Promise} with requested object
     * @private */
   async getOnlineItem(item, itemname, option) {
      let url = itemname, h_get = false, req = '', req_kind = 'object', draw_handle = null;

      if (isStr(option) && (option.indexOf('hierarchy_expand') === 0)) {
         h_get = true;
         option = undefined;
      }

      if (item) {
         url = this.getOnlineItemUrl(item);
         let func = null;
         if (item._kind)
            draw_handle = getDrawHandle(item._kind);

         if (h_get) {
            req = 'h.json?compact=3';
            item._expand = onlineHierarchy; // use proper expand function
         } else if (item._make_request) {
            if (item._module) {
               const h = await this.importModule(item._module);
               func = h[item._make_request];
            } else
               func = findFunction(item._make_request);
         } else if (draw_handle?.make_request)
            func = draw_handle.make_request;


         if (isFunc(func)) {
            // ask to make request
            const dreq = func(this, item, url, option);
            // result can be simple string or object with req and kind fields
            if (dreq) {
               if (isStr(dreq))
                  req = dreq;
               else {
                  if (dreq.req)
                     req = dreq.req;
                  if (dreq.kind)
                     req_kind = dreq.kind;
               }
            }
         }

         if (!req && !getTypeForKind(item._kind))
            req = 'item.json.gz?compact=3';
      }

      if (!itemname && item && this.#cached_draw_object && !req) {
         // special handling for online draw when cashed
         const obj = this.#cached_draw_object;
         this.#cached_draw_object = undefined;
         return obj;
      }

      if (!req)
         req = 'root.json.gz?compact=23';

      if (url)
         url += '/';
      url += req;

      return new Promise(resolveFunc => {
         let itemreq = null;

         createHttpRequest(url, req_kind, obj => {
            const handleAfterRequest = func => {
               if (isFunc(func)) {
                  const res = func(this, item, obj, option, itemreq);
                  if (isObject(res))
                     obj = res;
               }
               resolveFunc(obj);
            };

            if (!h_get && item?._after_request) {
               if (item._module)
                  this.importModule(item._module).then(h => handleAfterRequest(h[item._after_request]));
               else
                  handleAfterRequest(findFunction(item._after_request)); // v6 support
            } else
               handleAfterRequest(draw_handle?.after_request);
         }, undefined, true).then(xhr => {
            itemreq = xhr;
            xhr.send(null);
         });
      });
   }

   /** @summary Access THttpServer with provided address
     * @param {string} server_address - URL to server like 'http://localhost:8090/'
     * @return {Promise} when ready */
   async openOnline(server_address) {
      const adoptHierarchy = async result => {
         this.h = result;
         if (!result)
            return Promise.resolve(null);

         if (this.h?._title && (typeof document !== 'undefined'))
            document.title = this.h._title;

         result._isopen = true;

         // mark top hierarchy as online data and
         this.h._online = server_address;

         this.h._get = (item, itemname, option) => this.getOnlineItem(item, itemname, option);

         this.h._expand = onlineHierarchy;

         const styles = [], scripts = [], v6_modules = [], v7_imports = [];
         this.forEachItem(item => {
            if (item._childs !== undefined)
               item._expand = onlineHierarchy;

            if (item._autoload) {
               const arr = item._autoload.split(';');
               arr.forEach(name => {
                  if ((name.length > 4) && (name.lastIndexOf('.mjs') === name.length - 4))
                     v7_imports.push(this.importModule(name));
                  else if ((name.length > 3) && (name.lastIndexOf('.js') === name.length - 3)) {
                     if (!scripts.find(elem => elem === name))
                        scripts.push(name);
                  } else if ((name.length > 4) && (name.lastIndexOf('.css') === name.length - 4)) {
                     if (!styles.find(elem => elem === name))
                        styles.push(name);
                  } else if (name && !v6_modules.find(elem => elem === name))
                     v6_modules.push(name);
               });
            }
         });

         return this.loadScripts(scripts, v6_modules)
               .then(() => loadScript(styles))
               .then(() => Promise.all(v7_imports))
               .then(() => {
                  this.forEachItem(item => {
                     if (!('_drawfunc' in item) || !('_kind' in item))
                        return;
                     const typename = getTypeForKind(item._kind) || `kind:${item._kind}`,
                           drawopt = item._drawopt;
                     if (!canDrawHandle(typename) || drawopt)
                        addDrawFunc({ name: typename, func: item._drawfunc, script: item._drawscript, opt: drawopt });
                  });

                  return this;
               });
      };

      if (!server_address)
         server_address = '';

      if (isObject(server_address)) {
         const h = server_address;
         server_address = '';
         return adoptHierarchy(h);
      }

      return httpRequest(server_address + 'h.json?compact=3', 'object').then(hh => adoptHierarchy(hh));
   }

   /** @summary Get properties for online item  - server name and relative name
     * @private */
   getOnlineProp(itemname) {
      let item = this.findItem(itemname);
      if (!item)
         return null;

      itemname = item._name;
      while (item._parent) {
         item = item._parent;

         if (item._online)
            return { server: item._online, itemname };
         itemname = item._name + '/' + itemname;
      }

      return null;
   }

   /** @summary Fill context menu for online item
     * @private */
   fillOnlineMenu(menu, onlineprop, itemname) {
      const node = this.findItem(itemname),
            sett = getDrawSettings(node._kind, 'nosame;noinspect'),
            handle = getDrawHandle(node._kind),
            root_type = getTypeForKind(node._kind);

      if (sett.opts && (node._can_draw !== false)) {
         sett.opts.push(kInspect);
         menu.addDrawMenu('Draw', sett.opts, arg => this.display(itemname, arg));
      }

      if (!node._childs && (node._more !== false) && (node._more || root_type || sett.expand || sett.get_expand))
         menu.add('Expand', () => this.expandItem(itemname));

      if (handle?.execute)
         menu.add('Execute', () => this.executeCommand(itemname, menu.tree_node));

      if (sett.opts && (node._can_draw !== false)) {
         menu.addDrawMenu('Draw in new window', sett.opts,
                          arg => window.open(onlineprop.server + `?nobrowser&item=${onlineprop.itemname}` +
                                              (this.isMonitoring() ? `&monitoring=${this.getMonitoringInterval()}` : '') +
                                              (arg ? `&opt=${arg}` : '')));
      }

      if (sett.opts?.length && root_type && (node._can_draw !== false)) {
         menu.addDrawMenu('Draw as png', sett.opts,
                          arg => window.open(onlineprop.server + onlineprop.itemname + '/root.png?w=600&h=400' + (arg ? '&opt=' + arg : '')),
                          'Request PNG image from the server');
      }

      if (node._player)
         menu.add('Player', () => this.player(itemname));
   }

   /** @summary Assign existing hierarchy to the painter and refresh HTML code
     * @private */
   setHierarchy(h) {
      this.h = h;
      this.refreshHtml();
   }

   /** @summary Configures monitoring interval
     * @param {number} interval - repetition interval in ms
     * @param {boolean} flag - initial monitoring state */
   setMonitoring(interval, monitor_on) {
      this.#runMonitoring('cleanup');

      if (interval) {
         interval = parseInt(interval);
         if (Number.isInteger(interval) && (interval > 0)) {
            this.#monitoring_interval = Math.max(100, interval);
            monitor_on = true;
         } else
            this.#monitoring_interval = 3000;
      }

      this.#monitoring_on = monitor_on;

      if (this.isMonitoring())
         this.#runMonitoring();
   }

   /** @summary Runs monitoring event loop
     * @private */
   #runMonitoring(arg) {
      if ((arg === 'cleanup') || !this.isMonitoring()) {
         if (this.#monitoring_handle) {
            clearTimeout(this.#monitoring_handle);
            this.#monitoring_handle = undefined;
         }

         if (this.#monitoring_frame) {
            cancelAnimationFrame(this.#monitoring_frame);
            this.#monitoring_frame = undefined;
         }
         return;
      }

      if (arg === 'frame') {
         // process of timeout, request animation frame
         this.#monitoring_handle = undefined;
         this.#monitoring_frame = requestAnimationFrame(() => this.#runMonitoring('draw'));
         return;
      }

      if (arg === 'draw') {
         this.#monitoring_frame = undefined;
         this.updateItems();
      }

      this.#monitoring_handle = setTimeout(() => this.#runMonitoring('frame'), this.getMonitoringInterval());
   }

   /** @summary Returns configured monitoring interval in ms */
   getMonitoringInterval() { return this.#monitoring_interval || 3000; }

   /** @summary Returns true when monitoring is enabled */
   isMonitoring() { return this.#monitoring_on; }

   /** @summary Assign default layout and place where drawing will be performed
     * @param {string} layout - layout like 'simple' or 'grid2x2'
     * @param {string} frameid - DOM element id where object drawing will be performed */
   setDisplay(layout, frameid) {
      if (!frameid && isObject(layout)) {
         this.disp = layout;
         this.disp_kind = 'custom';
         this.disp_frameid = null;
      } else {
         this.disp_kind = layout;
         this.disp_frameid = frameid;
      }

      if (!this.register_resize && (this.disp_kind !== 'batch')) {
         this.register_resize = true;
         registerForResize(this);
      }
   }

   /** @summary Returns configured layout */
   getLayout() {
      return this.disp_kind;
   }

   /** @summary Remove painter reference from hierarchy
     * @private */
   removePainter(obj_painter) {
      this.forEachItem(item => {
         if (item._painter === obj_painter) {
            // delete painter reference
            delete item._painter;
            // also clear data which could be associated with item
            if (isFunc(item.clear))
               item.clear();
         }
      });
   }

   /** @summary Cleanup all items in hierarchy
     * @private */
   clearHierarchy(withbrowser) {
      if (this.disp) {
         this.disp.cleanup();
         delete this.disp;
      }

      const plainarr = [];

      this.forEachItem(item => {
         delete item._painter; // remove reference on the painter
         // when only display cleared, try to clear all browser items
         if (!withbrowser && isFunc(item.clear))
            item.clear();
         if (withbrowser)
            plainarr.push(item);
      });

      if (withbrowser) {
         // cleanup all monitoring loops
         this.enableMonitoring(false);
         // simplify work for javascript and delete all (ok, most of) cross-references
         this.selectDom().html('');
         plainarr.forEach(d => {
            delete d._parent;
            delete d._childs;
            delete d._obj;
            delete d._d3cont;
         });
         delete this.h;
      }
   }

   /** @summary Returns actual MDI display object
     * @desc It should an instance of {@link MDIDisplay} class */
   getDisplay() {
      return this.disp;
   }

   /** @summary method called when MDI element is cleaned up
     * @desc hook to perform extra actions when frame is cleaned
     * @private */
   cleanupFrame(frame) {
      select(frame).attr('frame_title', null);

      this.clearDrop(frame);

      const lst = cleanup(frame);

      // we remove all painters references from items
      if (lst.length) {
         this.forEachItem(item => {
            if (item._painter && lst.indexOf(item._painter) >= 0)
               delete item._painter;
         });
      }
   }

   /** @summary Creates configured MDIDisplay object
     * @return {Promise} when ready
     * @private */
   async createDisplay() {
      if (this.disp) {
         if ((this.disp.numDraw() > 0) || (this.disp_kind === 'custom'))
            return this.disp;
         this.disp.cleanup();
         delete this.disp;
      }

      if (this.disp_kind === 'batch') {
         const pr = isNodeJs() ? _loadJSDOM() : Promise.resolve(null);
         return pr.then(handle => {
            this.disp = new BatchDisplay(1200, 800, handle?.body);
            return this.disp;
         });
      }

      // check that we can found frame where drawing should be done
      if (!document.getElementById(this.disp_frameid))
         return null;

      if (isBatchMode())
         this.disp = new BatchDisplay(settings.CanvasWidth, settings.CanvasHeight);
      else if ((this.disp_kind.indexOf('flex') === 0) || (this.disp_kind.indexOf('coll') === 0))
         this.disp = new FlexibleDisplay(this.disp_frameid);
      else if (this.disp_kind === 'tabs')
         this.disp = new TabsDisplay(this.disp_frameid);
      else
         this.disp = new GridDisplay(this.disp_frameid, this.disp_kind);

      this.disp.cleanupFrame = this.cleanupFrame.bind(this);
      if (settings.DragAndDrop)
         this.disp.setInitFrame(this.enableDrop.bind(this));

      return this.disp;
   }

   /** @summary If possible, creates custom MDIDisplay for given item
     * @param itemname - name of item, for which drawing is created
     * @param custom_kind - display kind
     * @return {Promise} with mdi object created
     * @private */
   async createCustomDisplay(itemname, custom_kind) {
      if (this.disp_kind !== 'simple')
         return this.createDisplay();

      this.disp_kind = custom_kind;

      // check if display can be erased
      if (this.disp) {
         const num = this.disp.numDraw();
         if ((num > 1) || ((num === 1) && !this.disp.findFrame(itemname)))
            return this.createDisplay();
         this.disp.cleanup();
         delete this.disp;
      }

      return this.createDisplay();
   }

   /** @summary function updates object drawings for other painters
     * @private */
   updateOnOtherFrames(painter, obj) {
      const handle = obj._typename ? getDrawHandle(getKindForType(obj._typename)) : null;
      if (handle?.draw_field && obj[handle?.draw_field])
         obj = obj[handle?.draw_field];

      let isany = false;
      this.disp?.forEachPainter((p /* , frame */) => {
         if ((p === painter) || (p.getItemName() !== painter.getItemName()))
            return;

         // do not activate frame when doing update
         // mdi.activateFrame(frame);
         if (isFunc(p.redrawObject) && p.redrawObject(obj))
            isany = true;
      });
      return isany;
   }

   /** @summary Process resize event
     * @private */
   checkResize(size) {
      this.disp?.checkMDIResize(null, size);
   }

   /** @summary Load and execute scripts, kept to support v6 applications
     * @private */
   async loadScripts(scripts, modules, use_inject) {
      if (!scripts?.length && !modules?.length)
         return true;

      if (use_inject && scripts.indexOf('.mjs') > 0)
         return loadModules(scripts.split(';'));

      if (use_inject && !globalThis.JSROOT) {
         globalThis.JSROOT = {
            version, gStyle, create: create$1, httpRequest, loadScript, decodeUrl,
            source_dir: exports.source_dir, settings, addUserStreamer, addDrawFunc,
            draw, redraw
         };
      }

      if (internals.ignore_v6 || use_inject)
         return loadScript(scripts);

      return exports._ensureJSROOT().then(v6 => {
         return v6.require(modules)
                  .then(() => loadScript(scripts))
                  .then(() => v6._complete_loading());
      });
   }

   /** @summary Start GUI
     * @return {Promise} when ready
     * @private */
   async startGUI(gui_div, url) {
      const d = decodeUrl(url),

            getOption = opt => {
               let res = d.get(opt, null);
               if ((res === null) && gui_div && !gui_div.empty() && gui_div.node().hasAttribute(opt))
                  res = gui_div.attr(opt);
               return res;
            },

            getUrlOptionAsArray = opt => {
               let res = [];

               while (opt) {
                  const separ = opt.indexOf(';');
                  let part = (separ > 0) ? opt.slice(0, separ) : opt;

                  opt = (separ > 0) ? opt.slice(separ + 1) : '';

                  let canarray = true;
                  if (part[0] === '#') {
                     part = part.slice(1);
                     canarray = false;
                  }

                  const val = d.get(part, null);

                  if (canarray)
                     res = res.concat(parseAsArray(val));
                  else if (val !== null)
                     res.push(val);
               }
               return res;
            },

            getOptionAsArray = opt => {
               let res = getUrlOptionAsArray(opt);
               if (res.length || !gui_div || gui_div.empty())
                  return res;
               while (opt) {
                  const separ = opt.indexOf(';');
                  let part = separ > 0 ? opt.slice(0, separ) : opt;
                  opt = separ > 0 ? opt.slice(separ + 1) : '';

                  let canarray = true;
                  if (part[0] === '#') {
                     part = part.slice(1);
                     canarray = false;
                  }
                  if (part === 'files' || !gui_div.node().hasAttribute(part))
                     continue;

                  const val = gui_div.attr(part);

                  if (canarray)
                     res = res.concat(parseAsArray(val));
                  else if (val !== null)
                     res.push(val);
               }
               return res;
            },

            filesdir = d.get('path') || '', // path used in normal gui
            jsonarr = getOptionAsArray('#json;jsons'),
            expanditems = getOptionAsArray('expand'),
            focusitem = getOption('focus'),
            layout = getOption('layout'),
            style = getOptionAsArray('#style'),
            title = getOption('title');

      this.#one_by_one = settings.drop_items_one_by_one ?? (getOption('one_by_one') !== null);

      let prereq = getOption('prereq') || '',
          load = getOption('load'),
          dir = getOption('dir'),
          inject = getOption('inject'),
          filesarr = getOptionAsArray('#file;files'),
          itemsarr = getOptionAsArray('#item;items'),
          optionsarr = getOptionAsArray('#opt;opts'),
          monitor = getOption('monitoring'),
          statush = 0, status = getOption('status'),
          browser_kind = getOption('browser'),
          browser_configured = Boolean(browser_kind);

      if (monitor === null)
         monitor = 0;
      else if (monitor === '')
         monitor = 3000;
      else
         monitor = parseInt(monitor);

      if (getOption('float') !== null) {
         browser_kind = 'float';
         browser_configured = true;
      } else if (getOption('fix') !== null) {
         browser_kind = 'fix';
         browser_configured = true;
      }

      if (!browser_configured && (browser.screenWidth <= 640))
         browser_kind = 'float';

      this.no_select = getOption('noselect');
      this.top_info = getOption('info');

      if (getOption('files_monitoring') !== null)
         this.files_monitoring = true;

      if (title && (typeof document !== 'undefined'))
         document.title = title;

      if (!expanditems.length && (getOption('expand') === ''))
         expanditems.push('');

      if (filesdir) {
         for (let i = 0; i < filesarr.length; ++i)
            filesarr[i] = filesdir + filesarr[i];
         for (let i = 0; i < jsonarr.length; ++i)
            jsonarr[i] = filesdir + jsonarr[i];
      }

      if (!itemsarr.length && ((getOption('item') === '') || (jsonarr.length === 1 && !expanditems.length)))
         itemsarr.push('');

      if (!this.disp_kind) {
         if (isStr(layout) && layout)
            this.disp_kind = layout;
         else if (settings.DislpayKind && settings.DislpayKind !== 'simple')
            this.disp_kind = settings.DislpayKind;
         else {
            const _kinds = ['simple', 'simple', 'vert2', 'vert21', 'vert22', 'vert32',
                            'vert222', 'vert322', 'vert332', 'vert333'];
            this.disp_kind = _kinds[itemsarr.length] || 'flex';
         }
      }

      if (status === 'no')
         status = null;
      else if (status === 'off') {
         this.status_disabled = true;
         status = null;
      } else if (status === 'on')
         status = true;
      else if (status !== null) {
         statush = parseInt(status);
         if (!Number.isInteger(statush) || (statush < 5))
            statush = 0;
         status = true;
      }
      if (this.no_select === '')
         this.no_select = true;

      if (!browser_kind)
         browser_kind = 'fix';
      else if (browser_kind === 'no')
         browser_kind = '';
      else if (browser_kind === 'off') {
         browser_kind = '';
         status = null;
         this.exclude_browser = true;
      }
      if (getOption('nofloat') !== null)
         this.float_browser_disabled = true;

      if (this.start_without_browser)
         browser_kind = '';

      this.#topname = getOption('topname');

      const openAllFiles = () => {
         let promise;

         if (load || prereq) {
            promise = this.loadScripts(load, prereq);
            load = prereq = '';
         } else if (inject) {
            promise = this.loadScripts(inject, '', true);
            inject = '';
         } else if (browser_kind) {
            promise = this.createBrowser(browser_kind);
            browser_kind = '';
         } else if (status !== null) {
            promise = this.createStatusLine(statush, status);
            status = null;
         } else if (jsonarr.length)
            promise = this.openJsonFile(jsonarr.shift());
         else if (filesarr.length)
            promise = this.openRootFile(filesarr.shift());
         else if (dir) {
            promise = this.listServerDir(dir);
            dir = '';
         } else if (expanditems.length)
            promise = this.expandItem(expanditems.shift());
         else if (style.length)
            promise = this.applyStyle(style.shift());
         else {
            return this.refreshHtml()
                   .then(() => this.displayItems(itemsarr, optionsarr))
                   .then(() => { return focusitem ? this.focusOnItem(focusitem) : this; })
                   .then(() => {
                      this.setMonitoring(monitor);
                      return itemsarr ? this.refreshHtml() : this; // this is final return
                   });
         }

         return promise.then(openAllFiles);
      };

      let h0 = null;
      if (this.is_online) {
         const func = internals.getCachedHierarchy || findFunction('GetCachedHierarchy');
         if (isFunc(func))
            h0 = func();
         if (!isObject(h0))
            h0 = '';

         if ((this.is_online === 'draw') && !itemsarr.length)
            itemsarr.push('');
      }

      if (h0 !== null) {
         return this.openOnline(h0).then(() => {
            // check if server enables monitoring
            if (!this.exclude_browser && !browser_configured && this.h._browser) {
               browser_kind = this.h._browser;
               if (browser_kind === 'no')
                  browser_kind = '';
               else if (browser_kind === 'off') {
                  browser_kind = '';
                  status = null;
                  this.exclude_browser = true;
               }
            }

            if (('_monitoring' in this.h) && !monitor)
               monitor = this.h._monitoring;

            if (this.h._loadfile && !filesarr.length)
               filesarr = parseAsArray(this.h._loadfile);

            if (('_drawitem' in this.h) && !itemsarr.length) {
               itemsarr = parseAsArray(this.h._drawitem);
               optionsarr = parseAsArray(this.h._drawopt);
            }

            if (this.h._layout && !layout && ((this.is_online !== 'draw') || (itemsarr.length > 1)))
               this.disp_kind = this.h._layout;

            if (('_toptitle' in this.h) && this.exclude_browser && (typeof document !== 'undefined'))
               document.title = this.h._toptitle;

            if (gui_div)
               this.prepareGuiDiv(gui_div.attr('id'), this.disp_kind);

            return openAllFiles();
         });
      }

      if (gui_div)
         this.prepareGuiDiv(gui_div.attr('id'), this.disp_kind);

      return openAllFiles();
   }

   /** @summary Prepare div element - create layout and buttons
     * @private */
   prepareGuiDiv(myDiv, layout) {
      this.gui_div = isStr(myDiv) ? myDiv : myDiv.attr('id');

      this.brlayout = new BrowserLayout(this.gui_div, this);

      this.brlayout.create(!this.exclude_browser);

      this.createButtons();

      this.setDisplay(layout, this.brlayout.drawing_divid());
   }

   /** @summary Create shortcut buttons */
   createButtons() {
      if (this.exclude_browser)
         return;

      const btns = this.brlayout?.createBrowserBtns();
      if (!btns)
         return;

      ToolbarIcons.createSVG(btns, ToolbarIcons.diamand, 15, 'toggle fix-pos browser', 'browser')
                  .style('margin', '3px').on('click', () => this.createBrowser('fix', true));

      if (!this.float_browser_disabled) {
         ToolbarIcons.createSVG(btns, ToolbarIcons.circle, 15, 'toggle float browser', 'browser')
                     .style('margin', '3px').on('click', () => this.createBrowser('float', true));
      }

      if (!this.status_disabled) {
         ToolbarIcons.createSVG(btns, ToolbarIcons.three_circles, 15, 'toggle status line', 'browser')
                     .style('margin', '3px').on('click', () => this.createStatusLine(0, 'toggle'));
      }
   }

   /** @summary Returns true if status is exists */
   hasStatusLine() {
      if (this.status_disabled || !this.gui_div || !this.brlayout)
         return false;
      return this.brlayout.hasStatus();
   }

   /** @summary Create status line
     * @param {number} [height] - size of the status line
     * @param [mode] - false / true / 'toggle'
     * @return {Promise} when ready */
   async createStatusLine(height, mode) {
      if (this.status_disabled || !this.gui_div || !this.brlayout)
         return '';
      return this.brlayout.createStatusLine(height, mode);
   }

   /** @summary Redraw hierarchy
     * @desc works only when inspector or streamer info is displayed
     * @private */
   redrawObject(obj) {
      if (!this._inspector && !this._streamer_info)
         return false;
      if (this._streamer_info)
         this.h = createStreamerInfoContent(obj);
      else
         this.h = createInspectorContent(obj);
      return this.refreshHtml().then(() => this.setTopPainter());
   }

   /** @summary Create browser elements
     * @return {Promise} when completed */
   async createBrowser(browser_kind, update_html) {
      if (!this.gui_div || this.exclude_browser || !this.brlayout)
         return false;

      const main = select(`#${this.gui_div} .jsroot_browser`);
      // one requires top-level container
      if (main.empty())
         return false;

      if ((browser_kind === 'float') && this.float_browser_disabled)
         browser_kind = 'fix';

      if (!main.select('.jsroot_browser_area').empty()) {
         // this is case when browser created,
         // if update_html specified, hidden state will be toggled

         if (update_html)
            this.brlayout.toggleKind(browser_kind);

         return true;
      }

      let guiCode = `<p class="jsroot_browser_version"><a href="https://root.cern/js/">JSROOT</a> version <span style="color:green"><b>${version}</b></span></p>`;

      if (this.is_online) {
         guiCode += '<p> Hierarchy in <a href="h.json">json</a> and <a href="h.xml">xml</a> format</p>' +
                    '<div style="display:inline; vertical-align:middle; white-space: nowrap;">' +
                    '<label style="margin-right:5px"><input type="checkbox" name="monitoring" class="gui_monitoring"/>Monitoring</label>';
      } else if (!this.no_select) {
         const myDiv = select('#' + this.gui_div),
               files = myDiv.attr('files') || '../files/hsimple.root',
               path = decodeUrl().get('path') || myDiv.attr('path') || '',
               arrFiles = files.split(';');

         guiCode += '<input type="text" value="" style="width:95%; margin:5px;border:2px;" class="gui_urlToLoad" title="input file name"/>' +
                    '<div style="display:flex;flex-direction:row;padding-top:5px">' +
                    '<select class="gui_selectFileName" style="flex:1;padding:2px;" title="select file name"' +
                    '<option value="" selected="selected"></option>';
         arrFiles.forEach(fname => { guiCode += `<option value="${path + fname}">${fname}</option>`; });
         guiCode += '</select>' +
                    '<input type="file" class="gui_localFile" accept=".root" style="display:none"/><output id="list" style="display:none"></output>' +
                    '<input type="button" value="..." class="gui_fileBtn" style="min-width:3em;padding:3px;margin-left:5px;margin-right:5px;" title="select local file for reading"/><br/>' +
                    '</div>' +
                    '<p id="gui_fileCORS"><small><a href="https://github.com/root-project/jsroot/blob/master/docs/JSROOT.md#reading-root-files-from-other-servers">Read docu</a>' +
                    ' how to open files from other servers.</small></p>' +
                    '<div style="display:flex;flex-direction:row">' +
                    '<input style="padding:3px;margin-right:5px" class="gui_ReadFileBtn" type="button" title="Read the Selected File" value="Load"/>' +
                    '<input style="padding:3px;margin-right:5px" class="gui_ResetUIBtn" type="button" title="Close all opened files and clear drawings" value="Reset"/>';
      } else if (this.no_select === 'file')
         guiCode += '<div style="display:flex;flex-direction:row">';


      if (this.is_online || !this.no_select || this.no_select === 'file') {
         guiCode += '<select style="padding:2px;margin-right:5px;" title="layout kind" class="gui_layout"></select>' +
                    '</div>';
      }

      guiCode += `<div id="${this.gui_div}_browser_hierarchy" class="jsroot_browser_hierarchy"></div>`;

      this.brlayout.setBrowserContent(guiCode);

      const title_elem = this.brlayout.setBrowserTitle(this.top_info || (this.is_online ? 'ROOT online server' : 'Read a ROOT file'));
      title_elem?.on('contextmenu', evnt => {
         evnt.preventDefault();
         createMenu(evnt).then(menu => {
            this.fillSettingsMenu(menu, true);
            menu.show();
         });
      }).on('dblclick', () => {
         this.createBrowser(this.brlayout?.browser_kind === 'float' ? 'fix' : 'float', true);
      });

      if (!this.is_online && !this.no_select) {
         this.readSelectedFile = function() {
            const filename = main.select('.gui_urlToLoad').property('value').trim();
            if (!filename)
               return;

            if (filename.toLowerCase().lastIndexOf('.json') === filename.length - 5)
               this.openJsonFile(filename);
            else
               this.openRootFile(filename);
         };

         main.select('.gui_selectFileName').property('value', '')
              .on('change', evnt => main.select('.gui_urlToLoad').property('value', evnt.target.value));
         main.select('.gui_fileBtn').on('click', () => main.select('.gui_localFile').node().click());

         main.select('.gui_ReadFileBtn').on('click', () => this.readSelectedFile());

         main.select('.gui_ResetUIBtn').on('click', () => this.clearHierarchy(true));

         main.select('.gui_urlToLoad').on('keyup', evnt => {
            if (evnt.code === 'Enter')
               this.readSelectedFile();
         });

         main.select('.gui_localFile').on('change', evnt => {
            const files = evnt.target.files;

            for (let n = 0; n < files.length; ++n) {
               const f = files[n];
               main.select('.gui_urlToLoad').property('value', f.name);
               this.openRootFile(f);
            }
         });
      }

      const layout = main.select('.gui_layout');
      if (!layout.empty()) {
         ['simple', 'vert2', 'vert3', 'vert231', 'horiz2', 'horiz32', 'flex', 'tabs',
          'grid 2x2', 'grid 1x3', 'grid 2x3', 'grid 3x3', 'grid 4x4'].forEach(kind => layout.append('option').attr('value', kind).text(kind));

         layout.on('change', ev => {
            const kind = ev.target.value || 'flex';
            this.setDisplay(kind, this.gui_div + '_drawing');
            settings.DislpayKind = kind;
         });
      }

      this.setDom(this.gui_div + '_browser_hierarchy');

      if (update_html) {
         this.refreshHtml();
         this.initializeBrowser();
      }

      return this.brlayout.toggleBrowserKind(browser_kind || 'fix');
   }

   /** @summary Initialize browser elements */
   initializeBrowser() {
      const main = select(`#${this.gui_div} .jsroot_browser`);
      if (main.empty() || !this.brlayout)
         return;

      this.brlayout.adjustBrowserSize();

      const selects = main.select('.gui_layout').node();

      if (selects) {
         let found = false;
         for (const i in selects.options) {
            const s = selects.options[i].text;
            if (!isStr(s))
               continue;
            if ((s === this.getLayout()) || (s.replace(/ /g, '') === this.getLayout())) {
               selects.selectedIndex = i;
               found = true;
               break;
            }
         }
         if (!found) {
            const opt = document.createElement('option');
            opt.innerText = opt.value = this.getLayout();
            selects.appendChild(opt);
            selects.selectedIndex = selects.options.length - 1;
         }
      }

      if (this.is_online) {
         if (this.h?._toptitle)
            this.brlayout.setBrowserTitle(this.h._toptitle);
         main.select('.gui_monitoring')
           .property('checked', this.isMonitoring())
           .on('click', evnt => {
              this.enableMonitoring(evnt.target.checked);
              this.updateItems();
           });
      } else if (!this.no_select) {
         let fname = '';
         this.forEachRootFile(item => {
            if (!fname)
               fname = item._fullurl;
         });
         main.select('.gui_urlToLoad').property('value', fname);
      }
   }

   /** @summary Enable monitoring mode */
   enableMonitoring(on) {
      this.setMonitoring(undefined, on);

      const chkbox = select(`#${this.gui_div} .jsroot_browser .gui_monitoring`);
      if (!chkbox.empty() && (chkbox.property('checked') !== on))
         chkbox.property('checked', on);
   }

} // class HierarchyPainter

// ======================================================================================


/** @summary Display streamer info
  * @private */
async function drawStreamerInfo(dom, lst) {
   const painter = new HierarchyPainter('sinfo', dom, '__as_dark_mode__');

   // in batch mode HTML drawing is not possible, just keep object reference for a minute
   if (isBatchMode()) {
      painter.selectDom().property('_json_object_', lst);
      return painter;
   }

   painter._streamer_info = true;
   painter.h = createStreamerInfoContent(lst);

   // painter.selectDom().style('overflow','auto');

   return painter.refreshHtml().then(() => {
      painter.setTopPainter();
      return painter;
   });
}

/** @summary Display inspector
  * @private */
async function drawInspector(dom, obj, opt) {
   cleanup(dom);
   const painter = new HierarchyPainter('inspector', dom, '__as_dark_mode__');

   // in batch mode HTML drawing is not possible, just keep object reference for a minute
   if (isBatchMode()) {
      painter.selectDom().property('_json_object_', obj);
      return painter;
   }

   painter.default_by_click = kExpand; // default action
   painter.with_icons = false;
   painter._inspector = true; // keep
   let expand_level = 0;

   if (isStr(opt) && opt.indexOf(kInspect) === 0) {
      opt = opt.slice(kInspect.length);
      if (opt)
         expand_level = Number.parseInt(opt);
   }

   if (painter.selectDom().classed('jsroot_inspector')) {
      painter.removeInspector = function() {
         this.selectDom().remove();
      };

      if (!browser.qt6 && !browser.cef3) {
         painter.storeAsJson = function() {
            const json = toJSON(obj, 2),
                  fname = obj.fName || 'file';
            saveFile(`${fname}.json`, prJSON + encodeURIComponent(json));
         };
      }
   }

   painter.fill_context = function(menu, hitem) {
      const sett = getDrawSettings(hitem._kind, 'nosame');
      if (sett.opts) {
         menu.addDrawMenu('nosub:Draw', sett.opts, arg => {
            if (!hitem?._obj)
               return;
            const obj2 = hitem._obj;
            let ddom = this.selectDom().node();
            if (isFunc(this.removeInspector)) {
               ddom = ddom.parentNode;
               this.removeInspector();
               if (arg.indexOf(kInspect) === 0)
                  return this.showInspector(arg, obj2);
            }
            cleanup(ddom);
            draw(ddom, obj2, arg);
         });
      }
   };

   painter.h = createInspectorContent(obj);

   return painter.refreshHtml().then(() => {
      painter.setTopPainter();
      return painter.expandToLevel(expand_level);
   });
}


/** @summary Show object in inspector for provided object
  * @protected */
ObjectPainter.prototype.showInspector = function(opt, obj) {
   if (opt === 'check')
      return true;

   const main = this.selectDom(),
         rect = getElementRect(main),
         w = Math.round(rect.width * 0.05) + 'px',
         h = Math.round(rect.height * 0.05) + 'px',
         id = 'root_inspector_' + internals.id_counter++;

   main.append('div')
       .attr('id', id)
       .attr('class', 'jsroot_inspector')
       .style('position', 'absolute')
       .style('top', h)
       .style('bottom', h)
       .style('left', w)
       .style('right', w);

   if (!obj?._typename)
      obj = isFunc(this.getPrimaryObject) ? this.getPrimaryObject() : this.getObject();

   return drawInspector(id, obj, opt);
};


internals.drawInspector = drawInspector;

var HierarchyPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
HierarchyPainter: HierarchyPainter,
drawInspector: drawInspector,
drawList: drawList,
drawStreamerInfo: drawStreamerInfo,
folderHierarchy: folderHierarchy,
keysHierarchy: keysHierarchy,
listHierarchy: listHierarchy,
markAsStreamerInfo: markAsStreamerInfo,
objectHierarchy: objectHierarchy,
taskHierarchy: taskHierarchy
});

/** @summary Read style and settings from URL
  * @private */
function readStyleFromURL(url) {
   // first try to read settings from local storage
   const d = decodeUrl(url),
         prefix = d.get('storage_prefix');

   if (isStr(prefix) && prefix)
      setStoragePrefix(prefix);

   if (readSettings())
      setDefaultDrawOpt(settings._dflt_drawopt);

   readStyle();

   function get_bool(name, field, special) {
      if (d.has(name)) {
         const val = d.get(name);
         if (special && (val === special))
            settings[field] = special;
         else
            settings[field] = (val !== '0') && (val !== 'false') && (val !== 'off');
      }
   }

   if (d.has('optimize')) {
      settings.OptimizeDraw = 2;
      let optimize = d.get('optimize');
      if (optimize) {
         optimize = parseInt(optimize);
         if (Number.isInteger(optimize))
            settings.OptimizeDraw = optimize;
      }
   }

   if (d.has('scale')) {
      const s = parseInt(d.get('scale'));
      settings.CanvasScale = Number.isInteger(s) ? s : 2;
   }

   const b = d.get('batch');
   if (b !== undefined) {
      setBatchMode(d !== 'off');
      if (b === 'png')
         internals.batch_png = true;
   }

   get_bool('lastcycle', 'OnlyLastCycle');
   get_bool('usestamp', 'UseStamp');
   get_bool('dark', 'DarkMode');
   get_bool('approx_text_size', 'ApproxTextSize');

   let mr = d.get('maxranges');
   if (mr) {
      mr = parseInt(mr);
      if (Number.isInteger(mr))
         settings.MaxRanges = mr;
   }

   if (d.has('wrong_http_response'))
      settings.HandleWrongHttpResponse = true;

   if (d.has('prefer_saved_points'))
      settings.PreferSavedPoints = true;

   const tf1_style = d.get('tf1');
   if (tf1_style === 'curve')
      settings.FuncAsCurve = true;
   else if (tf1_style === 'line')
      settings.FuncAsCurve = false;

   if (d.has('with_credentials'))
      settings.WithCredentials = true;

   let inter = d.get('interactive');
   if (inter === 'nomenu')
      settings.ContextMenu = false;
   else if (inter !== undefined) {
      if (!inter || (inter === '1'))
         inter = '111111';
      else if (inter === '0')
         inter = '000000';
      if (inter.length === 6) {
         switch (inter[0]) {
            case '0':
               settings.ToolBar = false;
               break;
            case '1':
               settings.ToolBar = 'popup';
               break;
            case '2':
               settings.ToolBar = true;
               break;
         }
         inter = inter.slice(1);
      }
      if (inter.length === 5) {
         settings.Tooltip = parseInt(inter[0]);
         settings.ContextMenu = (inter[1] !== '0');
         settings.Zooming = (inter[2] !== '0');
         settings.MoveResize = (inter[3] !== '0');
         settings.DragAndDrop = (inter[4] !== '0');
      }
   }

   get_bool('tooltip', 'Tooltip');

   const mathjax = d.get('mathjax', null);
   let latex = d.get('latex', null);
   if ((mathjax !== null) && (mathjax !== '0') && (latex === null))
      latex = 'math';
   if (latex !== null)
      settings.Latex = constants$1.Latex.fromString(latex);

   if (d.has('nomenu'))
      settings.ContextMenu = false;
   if (d.has('noprogress'))
      settings.ProgressBox = false;
   else
      get_bool('progress', 'ProgressBox', 'modal');

   if (d.has('notouch'))
      browser.touches = false;
   if (d.has('adjframe'))
      settings.CanAdjustFrame = true;

   const has_toolbar = d.has('toolbar');
   if (has_toolbar) {
      const toolbar = d.get('toolbar', '');
      let val = null;
      if (toolbar.indexOf('popup') >= 0)
         val = 'popup';
      if (toolbar.indexOf('left') >= 0) {
         settings.ToolBarSide = 'left';
         val = 'popup';
      }
      if (toolbar.indexOf('right') >= 0) {
         settings.ToolBarSide = 'right';
         val = 'popup';
      }
      if (toolbar.indexOf('vert') >= 0) {
         settings.ToolBarVert = true;
         val = 'popup';
      }
      if (toolbar.indexOf('show') >= 0)
         val = true;
      settings.ToolBar = val || ((toolbar.indexOf('0') < 0) && (toolbar.indexOf('false') < 0) && (toolbar.indexOf('off') < 0));
   }

   get_bool('skipsi', 'SkipStreamerInfos');
   get_bool('skipstreamerinfos', 'SkipStreamerInfos');

   if (d.has('nodraggraphs'))
      settings.DragGraphs = false;

   if (d.has('palette')) {
      const palette = parseInt(d.get('palette'));
      if (Number.isInteger(palette) && (palette > 0) && (palette < 113))
         settings.Palette = palette;
   }

   const render3d = d.get('render3d'), embed3d = d.get('embed3d'), geosegm = d.get('geosegm');
   if (render3d)
      settings.Render3D = constants$1.Render3D.fromString(render3d);
   if (embed3d)
      settings.Embed3D = constants$1.Embed3D.fromString(embed3d);
   if (geosegm)
      settings.GeoGradPerSegm = Math.max(2, parseInt(geosegm));
   get_bool('geocomp', 'GeoCompressComp');

   if (d.has('hlimit'))
      settings.HierarchyLimit = parseInt(d.get('hlimit'));

   function get_int_style(name, field, dflt) {
      if (!d.has(name))
         return;
      const val = d.get(name);
      if (!val || (val === 'true') || (val === 'on'))
         gStyle[field] = dflt;
      else if ((val === 'false') || (val === 'off'))
         gStyle[field] = 0;
      else
         gStyle[field] = parseInt(val);
      return gStyle[field];
   }
   function get_float_style(name, field) {
      if (!d.has(name))
         return;
      const val = d.get(name),
            flt = Number.parseFloat(val);
      if (Number.isFinite(flt))
         gStyle[field] = flt;
   }

   if (d.has('histzero'))
      gStyle.fHistMinimumZero = true;
   if (d.has('histmargin'))
      gStyle.fHistTopMargin = parseFloat(d.get('histmargin'));
   get_int_style('optstat', 'fOptStat', 1111);
   get_int_style('optfit', 'fOptFit', 0);
   const has_date = get_int_style('optdate', 'fOptDate', 1),
         has_file = get_int_style('optfile', 'fOptFile', 1);
   if ((has_date || has_file) && !has_toolbar)
      settings.ToolBarVert = true;
   get_float_style('datex', 'fDateX');
   get_float_style('datey', 'fDateY');
   get_float_style('barwidth', 'fBarWidth');
   get_float_style('baroffset', 'fBarOffset');

   get_int_style('opttitle', 'fOptTitle', 1);
   if (d.has('utc'))
      settings.TimeZone = 'UTC';
   if (d.has('cet'))
      settings.TimeZone = 'Europe/Berlin';
   else if (d.has('timezone')) {
      settings.TimeZone = d.get('timezone');
      if ((settings.TimeZone === 'default') || (settings.TimeZone === 'dflt'))
         settings.TimeZone = 'Europe/Berlin';
      else if (settings.TimeZone === 'local')
         settings.TimeZone = '';
   }

   gStyle.fStatFormat = d.get('statfmt', gStyle.fStatFormat);
   gStyle.fFitFormat = d.get('fitfmt', gStyle.fFitFormat);
}


/** @summary Build main GUI
  * @desc Used in many HTML files to create JSROOT GUI elements
  * @param {String} gui_element - id of the `<div>` element
  * @param {String} gui_kind - either 'online', 'nobrowser', 'draw'
  * @return {Promise} with {@link HierarchyPainter} instance
  * @example
  * import { buildGUI } from 'https://root.cern/js/latest/modules/gui.mjs';
  * buildGUI('guiDiv'); */
async function buildGUI(gui_element, gui_kind = '') {
   const myDiv = select(isStr(gui_element) ? `#${gui_element}` : gui_element);
   if (myDiv.empty())
      return Promise.reject(Error('no div for gui found'));

   myDiv.html(''); // clear element

   const d = decodeUrl(), getSize = name => {
      const res = d.has(name) ? d.get(name).split('x') : [];
      if (res.length !== 2)
         return null;
      res[0] = parseInt(res[0]);
      res[1] = parseInt(res[1]);
      return res[0] > 0 && res[1] > 0 ? res : null;
   };
   let online = (gui_kind === 'online'), nobrowser = false, drawing = false;

   if (gui_kind === 'draw')
      online = drawing = nobrowser = true;
   else if ((gui_kind === 'nobrowser') || d.has('nobrowser') || (myDiv.attr('nobrowser') && myDiv.attr('nobrowser') !== 'false'))
      nobrowser = true;

   if (myDiv.attr('ignoreurl') === 'true')
      settings.IgnoreUrlOptions = true;

   readStyleFromURL();

   if (isBatchMode())
      nobrowser = true;

   const divsize = getSize('divsize'), canvsize = getSize('canvsize'), smallpad = getSize('smallpad');
   if (divsize)
      myDiv.style('position', 'relative').style('width', divsize[0] + 'px').style('height', divsize[1] + 'px');
   else if (!isBatchMode()) {
      select('html').style('height', '100%');
      select('body').style('min-height', '100%').style('margin', 0).style('overflow', 'hidden');
      myDiv.style('position', 'absolute').style('left', 0).style('top', 0).style('bottom', 0).style('right', 0).style('padding', '1px');
   }
   if (canvsize) {
      settings.CanvasWidth = canvsize[0];
      settings.CanvasHeight = canvsize[1];
   }
   if (smallpad) {
      settings.SmallPad.width = smallpad[0];
      settings.SmallPad.height = smallpad[1];
   }

   const hpainter = new HierarchyPainter('root', null);
   if (online)
      hpainter.is_online = drawing ? 'draw' : 'online';
   if (drawing || isBatchMode())
      hpainter.exclude_browser = true;
   hpainter.start_without_browser = nobrowser;

   return hpainter.startGUI(myDiv).then(() => {
      if (!nobrowser)
         return hpainter.initializeBrowser();
      if (!drawing)
         return;
      const func = internals.getCachedObject || findFunction('GetCachedObject'),
            obj = isFunc(func) ? parse$1(func()) : undefined;
      if (isObject(obj))
         hpainter.setCachedObject(obj);
      let opt = d.get('opt', '');
      if (d.has('websocket'))
         opt += ';websocket';
      return hpainter.display('', opt);
   }).then(() => hpainter);
}

var _rollup_plugin_ignore_empty_module_placeholder = {};

var _rollup_plugin_ignore_empty_module_placeholder$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
default: _rollup_plugin_ignore_empty_module_placeholder
});

/** @summary Draw TEllipse
  * @private */
function drawEllipse() {
   const ellipse = this.getObject(),
         closed_ellipse = (ellipse.fPhimin === 0) && (ellipse.fPhimax === 360),
         is_crown = (ellipse._typename === 'TCrown');

   this.createAttLine({ attr: ellipse });
   this.createAttFill({ attr: ellipse });

   const g = this.createG(),
         funcs = this.getAxisToSvgFunc(),
         x = funcs.x(ellipse.fX1),
         y = funcs.y(ellipse.fY1),
         rx = is_crown && (ellipse.fR1 <= 0) ? (funcs.x(ellipse.fX1 + ellipse.fR2) - x) : (funcs.x(ellipse.fX1 + ellipse.fR1) - x),
         ry = y - funcs.y(ellipse.fY1 + ellipse.fR2),
         dr = Math.PI / 180;

   let path = '';

   if (is_crown && (ellipse.fR1 > 0)) {
      const ratio = ellipse.fYXRatio ?? 1,
            rx1 = rx, ry2 = ratio * ry,
            ry1 = ratio * (y - funcs.y(ellipse.fY1 + ellipse.fR1)),
            rx2 = funcs.x(ellipse.fX1 + ellipse.fR2) - x;

      if (closed_ellipse) {
         path = `M${-rx1},0A${rx1},${ry1},0,1,0,${rx1},0A${rx1},${ry1},0,1,0,${-rx1},0` +
                `M${-rx2},0A${rx2},${ry2},0,1,0,${rx2},0A${rx2},${ry2},0,1,0,${-rx2},0`;
      } else {
         const large_arc = (ellipse.fPhimax - ellipse.fPhimin >= 180) ? 1 : 0,
               a1 = ellipse.fPhimin * dr, a2 = ellipse.fPhimax * dr,
               dx1 = Math.round(rx1 * Math.cos(a1)), dy1 = Math.round(ry1 * Math.sin(a1)),
               dx2 = Math.round(rx1 * Math.cos(a2)), dy2 = Math.round(ry1 * Math.sin(a2)),
               dx3 = Math.round(rx2 * Math.cos(a1)), dy3 = Math.round(ry2 * Math.sin(a1)),
               dx4 = Math.round(rx2 * Math.cos(a2)), dy4 = Math.round(ry2 * Math.sin(a2));

         path = `M${dx2},${dy2}A${rx1},${ry1},0,${large_arc},0,${dx1},${dy1}` +
                `L${dx3},${dy3}A${rx2},${ry2},0,${large_arc},1,${dx4},${dy4}Z`;
      }
   } else if (ellipse.fTheta === 0) {
      if (closed_ellipse)
         path = `M${-rx},0A${rx},${ry},0,1,0,${rx},0A${rx},${ry},0,1,0,${-rx},0Z`;
      else {
         const x1 = Math.round(rx * Math.cos(ellipse.fPhimin * dr)),
               y1 = Math.round(ry * Math.sin(ellipse.fPhimin * dr)),
               x2 = Math.round(rx * Math.cos(ellipse.fPhimax * dr)),
               y2 = Math.round(ry * Math.sin(ellipse.fPhimax * dr));
         path = `M0,0L${x1},${y1}A${rx},${ry},0,1,1,${x2},${y2}Z`;
      }
   } else {
      const ct = Math.cos(ellipse.fTheta * dr),
            st = Math.sin(ellipse.fTheta * dr),
            phi1 = ellipse.fPhimin * dr,
            phi2 = ellipse.fPhimax * dr,
            np = 200,
            dphi = (phi2 - phi1) / (np - (closed_ellipse ? 0 : 1));
      let lastx = 0, lasty = 0;
      if (!closed_ellipse)
         path = 'M0,0';
      for (let n = 0; n < np; ++n) {
         const angle = phi1 + n * dphi,
               dx = ellipse.fR1 * Math.cos(angle),
               dy = ellipse.fR2 * Math.sin(angle),
               px = funcs.x(ellipse.fX1 + dx * ct - dy * st) - x,
               py = funcs.y(ellipse.fY1 + dx * st + dy * ct) - y;
         if (!path)
            path = `M${px},${py}`;
         else if (lastx === px)
            path += `v${py - lasty}`;
         else if (lasty === py)
            path += `h${px - lastx}`;
         else
            path += `l${px - lastx},${py - lasty}`;
         lastx = px;
         lasty = py;
      }
      path += 'Z';
   }

   this.x = x;
   this.y = y;

   makeTranslate(g.append('svg:path'), x, y)
      .attr('d', path)
      .call(this.lineatt.func)
      .call(this.fillatt.func);

   assignContextMenu(this);

   addMoveHandler(this);

   this.moveDrag = function(dx, dy) {
      this.x += dx;
      this.y += dy;
      makeTranslate(this.getG().select('path'), this.x, this.y);
   };

   this.moveEnd = function(not_changed) {
      if (not_changed)
         return;
      const ell = this.getObject();
      ell.fX1 = this.svgToAxis('x', this.x);
      ell.fY1 = this.svgToAxis('y', this.y);
      this.submitCanvExec(`SetX1(${ell.fX1});;SetY1(${ell.fY1});;Notify();;`);
   };
}

/** @summary Draw TPie
  * @private */
function drawPie() {
   const g = this.createG(),
         pie = this.getObject(),
         nb = pie.fPieSlices.length,
         xc = this.axisToSvg('x', pie.fX),
         yc = this.axisToSvg('y', pie.fY),
         rx = this.axisToSvg('x', pie.fX + pie.fRadius) - xc,
         ry = this.axisToSvg('y', pie.fY + pie.fRadius) - yc;

   makeTranslate(g, xc, yc);

   // Draw the slices
   let total = 0,
       af = (pie.fAngularOffset * Math.PI) / 180,
       x1 = Math.round(rx * Math.cos(af)),
       y1 = Math.round(ry * Math.sin(af));

   for (let n = 0; n < nb; n++)
      total += pie.fPieSlices[n].fValue;

   for (let n = 0; n < nb; n++) {
      const slice = pie.fPieSlices[n];

      this.createAttLine({ attr: slice });
      this.createAttFill({ attr: slice });

      af += slice.fValue / total * 2 * Math.PI;
      const x2 = Math.round(rx * Math.cos(af)),
            y2 = Math.round(ry * Math.sin(af));

      g.append('svg:path')
       .attr('d', `M0,0L${x1},${y1}A${rx},${ry},0,0,0,${x2},${y2}z`)
       .call(this.lineatt.func)
       .call(this.fillatt.func);
      x1 = x2;
      y1 = y2;
   }
}

/** @summary Draw TMarker
  * @private */
function drawMarker$1() {
   const marker = this.getObject(),
         kMarkerNDC = BIT(14);

   this.isndc = marker.TestBit(kMarkerNDC);

   const d = new DrawOptions(this.getDrawOpt()),
         use_frame = this.isndc ? false : d.check('FRAME'),
         swap_xy = use_frame && this.getFramePainter()?.swap_xy();

   this.createAttMarker({ attr: marker });

   const g = this.createG(use_frame ? 'frame2d' : undefined);

   let x = this.axisToSvg('x', marker.fX, this.isndc),
       y = this.axisToSvg('y', marker.fY, this.isndc);
   if (swap_xy)
      [x, y] = [y, x];

   const path = this.markeratt.create(x, y);

   if (path) {
      g.append('svg:path')
       .attr('d', path)
       .call(this.markeratt.func);
   }

   if (d.check('NO_INTERACTIVE'))
      return;

   assignContextMenu(this);

   addMoveHandler(this);

   this.dx = this.dy = 0;

   this.moveDrag = function(dx, dy) {
      this.dx += dx;
      this.dy += dy;
      if (this.getG())
         makeTranslate(this.getG().select('path'), this.dx, this.dy);
   };

   this.moveEnd = function(not_changed) {
      if (not_changed || !this.getG())
         return;
      const mrk = this.getObject();
      let fx = this.svgToAxis('x', this.axisToSvg('x', mrk.fX, this.isndc) + this.dx, this.isndc),
          fy = this.svgToAxis('y', this.axisToSvg('y', mrk.fY, this.isndc) + this.dy, this.isndc);
      if (swap_xy)
         [fx, fy] = [fy, fx];
      mrk.fX = fx;
      mrk.fY = fy;
      this.submitCanvExec(`SetX(${fx});;SetY(${fy});;Notify();;`);
      this.redraw();
   };
}

/** @summary Draw TPolyMarker
  * @private */
function drawPolyMarker() {
   const poly = this.getObject(),
         func = this.getAxisToSvgFunc();

   this.createAttMarker({ attr: poly });

   const g = this.createG();

   let path = '';
   for (let n = 0; n <= poly.fLastPoint; ++n)
      path += this.markeratt.create(func.x(poly.fX[n]), func.y(poly.fY[n]));

   if (path) {
      g.append('svg:path')
       .attr('d', path)
       .call(this.markeratt.func);
   }

   assignContextMenu(this);

   addMoveHandler(this);

   this.dx = this.dy = 0;

   this.moveDrag = function(dx, dy) {
      this.dx += dx;
      this.dy += dy;
      if (this.getG())
         makeTranslate(this.getG().select('path'), this.dx, this.dy);
   };

   this.moveEnd = function(not_changed) {
      if (not_changed || !this.getG())
         return;
      const poly2 = this.getObject(),
            func2 = this.getAxisToSvgFunc();
      let exec = '';
      for (let n = 0; n <= poly2.fLastPoint; ++n) {
         const x = this.svgToAxis('x', func2.x(poly2.fX[n]) + this.dx),
               y = this.svgToAxis('y', func2.y(poly2.fY[n]) + this.dy);
         poly2.fX[n] = x;
         poly2.fY[n] = y;
         exec += `SetPoint(${n},${x},${y});;`;
      }
      this.submitCanvExec(exec + 'Notify();;');
      this.redraw();
   };
}

/** @summary Draw JS image
  * @private */
function drawJSImage(dom, obj, opt) {
   const painter = new BasePainter(dom),
         main = painter.selectDom(),
         img = main.append('img').attr('src', obj.fName).attr('title', obj.fTitle || obj.fName);

   if (opt && opt.indexOf('scale') >= 0)
      img.style('width', '100%').style('height', '100%');
   else if (opt && opt.indexOf('center') >= 0) {
      main.style('position', 'relative');
      img.attr('style', 'margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);');
   }

   painter.setTopPainter();

   return painter;
}

var more = /*#__PURE__*/Object.freeze({
__proto__: null,
drawEllipse: drawEllipse,
drawJSImage: drawJSImage,
drawMarker: drawMarker$1,
drawPie: drawPie,
drawPolyMarker: drawPolyMarker
});

/** @summary direct draw function for TPolyMarker3D object
  * @private */
async function drawPolyMarker3D$1() {
   const fp = this.$fp || this.getFramePainter();

   delete this.$fp;

   if (!isObject(fp) || !fp.grx || !fp.gry || !fp.grz)
      return this;

   const poly = this.getObject(), sizelimit = 50000, fP = poly.fP;
   let step = 1, numselect = 0;

   for (let i = 0; i < fP.length; i += 3) {
      if ((fP[i] < fp.scale_xmin) || (fP[i] > fp.scale_xmax) ||
          (fP[i + 1] < fp.scale_ymin) || (fP[i + 1] > fp.scale_ymax) ||
          (fP[i + 2] < fp.scale_zmin) || (fP[i + 2] > fp.scale_zmax))
         continue;
      ++numselect;
   }

   if ((settings.OptimizeDraw > 0) && (numselect > sizelimit))
      step = Math.max(2, Math.floor(numselect / sizelimit));

   const size = Math.floor(numselect / step),
         pnts = new PointsCreator(size, fp.webgl, fp.size_x3d / 100),
         index = new Int32Array(size);
   let select = 0, icnt = 0;

   for (let i = 0; i < fP.length; i += 3) {
      if ((fP[i] < fp.scale_xmin) || (fP[i] > fp.scale_xmax) ||
          (fP[i + 1] < fp.scale_ymin) || (fP[i + 1] > fp.scale_ymax) ||
          (fP[i + 2] < fp.scale_zmin) || (fP[i + 2] > fp.scale_zmax))
         continue;

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

      index[icnt++] = i;

      pnts.addPoint(fp.grx(fP[i]), fp.gry(fP[i + 1]), fp.grz(fP[i + 2]));
   }

   return pnts.createPoints({ color: this.getColor(poly.fMarkerColor), style: poly.fMarkerStyle }).then(mesh => {
      mesh.tip_color = (poly.fMarkerColor === 3) ? 0xFF0000 : 0x00FF00;
      mesh.tip_name = poly.fName || 'Poly3D';
      mesh.poly = poly;
      mesh.fp = fp;
      mesh.scale0 = 0.7 * pnts.scale;
      mesh.index = index;

      fp.add3DMesh(mesh, this, true);

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

         indx = this.index[indx];

         const fp2 = this.fp,
               grx = fp2.grx(this.poly.fP[indx]),
               gry = fp2.gry(this.poly.fP[indx + 1]),
               grz = fp2.grz(this.poly.fP[indx + 2]);

         return {
            x1: grx - this.scale0,
            x2: grx + this.scale0,
            y1: gry - this.scale0,
            y2: gry + this.scale0,
            z1: grz - this.scale0,
            z2: grz + this.scale0,
            color: this.tip_color,
            lines: [
               this.tip_name,
               'pnt: ' + indx / 3,
               'x: ' + fp2.axisAsText('x', this.poly.fP[indx]),
               'y: ' + fp2.axisAsText('y', this.poly.fP[indx + 1]),
               'z: ' + fp2.axisAsText('z', this.poly.fP[indx + 2])
            ]
         };
      };

      fp.render3D(100); // set timeout to be able draw other points

      return this;
   });
}

/** @summary Show TTree::Draw progress during processing
  * @private */
TDrawSelector.prototype.ShowProgress = function(value) {
   let msg, ret;
   if ((value === undefined) || !Number.isFinite(value))
      msg = ret = '';
   else if (this._break) {
      msg = 'Breaking ... ';
      ret = 'break';
   } else {
      if (this.last_progress !== value) {
         const diff = value - this.last_progress;
         if (!this.aver_diff)
            this.aver_diff = diff;
         this.aver_diff = diff * 0.3 + this.aver_diff * 0.7;
      }

      this.last_progress = value;

      let ndig = 0;
      if (this.aver_diff <= 0)
         ndig = 0;
      else if (this.aver_diff < 0.0001)
         ndig = 3;
      else if (this.aver_diff < 0.001)
         ndig = 2;
      else if (this.aver_diff < 0.01)
         ndig = 1;
      msg = `TTree draw ${(value * 100).toFixed(ndig)} % `;
   }

   showProgress(msg, 0, () => { this._break = 1; });
   return ret;
};

/** @summary Draw result of tree drawing
  * @private */
async function drawTreeDrawResult(dom, obj, opt) {
   const typ = obj?._typename;

   if (!typ || !isStr(typ))
      return Promise.reject(Error('Object without type cannot be draw with TTree'));

   if (typ.indexOf(clTH1) === 0)
      return TH1Painter.draw(dom, obj, opt);
   if (typ.indexOf(clTH2) === 0)
      return TH2Painter.draw(dom, obj, opt);
   if (typ.indexOf(clTH3) === 0)
      return TH3Painter.draw(dom, obj, opt);
   if (typ.indexOf(clTGraph) === 0)
      return TGraphPainter.draw(dom, obj, opt);
   if ((typ === clTPolyMarker3D) && obj.$hist) {
      return TH3Painter.draw(dom, obj.$hist, opt).then(() => {
         const p2 = new ObjectPainter(dom, obj, opt);
         p2.addToPadPrimitives();
         p2.redraw = drawPolyMarker3D$1;
         return p2.redraw();
      });
   }

   return Promise.reject(Error(`Object of type ${typ} cannot be draw with TTree`));
}


/** @summary Handle callback function with progress of tree draw
  * @private */
async function treeDrawProgress(obj, final) {
   // no need to update drawing if previous is not yet completed
   if (!final && !this.last_pr)
      return;

   if (this.dump || this.dump_entries || this.testio) {
      if (!final)
         return;
      if (isBatchMode()) {
         const painter = new BasePainter(this.drawid);
         painter.selectDom().property('_json_object_', obj);
         return painter;
      }
      if (isFunc(internals.drawInspector))
         return internals.drawInspector(this.drawid, obj);
      const str = create$1(clTObjString);
      str.fString = toJSON(obj, 2);
      return drawRawText(this.drawid, str);
   }

   // complex logic with intermediate update
   // while TTree reading not synchronized with drawing,
   // next portion can appear before previous is drawn
   // critical is last drawing which should wait for previous one
   // therefore last_pr is kept as indication that promise is not yet processed

   if (!this.last_pr)
      this.last_pr = Promise.resolve(true);

   return this.last_pr.then(() => {
      if (this.obj_painter)
         this.last_pr = getPromise(this.obj_painter.redrawObject(obj)).then(() => this.obj_painter);
      else if (!obj) {
         if (final)
            console.log('no result after tree drawing');
         this.last_pr = false; // return false indicating no drawing is done
      } else {
         this.last_pr = drawTreeDrawResult(this.drawid, obj, this.drawopt).then(p => {
            this.obj_painter = p;
            if (!final)
               this.last_pr = null;
            return p; // return painter for histogram
         });
      }

      return final ? this.last_pr : null;
   });
}


/** @summary Create painter to perform tree drawing on server side
  * @private */
function createTreePlayer(player) {
   player.draw_first = true;

   player.configureOnline = function(itemname, url, askey, root_version, expr) {
      this.setItemName(itemname, '', this);
      this.url = url;
      this.root_version = root_version;
      this.askey = askey;
      this.draw_expr = expr;
   };

   player.configureTree = function(tree) {
      this.local_tree = tree;
   };

   player.showExtraButtons = function(args) {
      const main = this.selectDom(),
            numentries = this.local_tree?.fEntries || 0;

      main.select('.treedraw_more').remove(); // remove more button first

      main.select('.treedraw_buttons').node().innerHTML +=
          'Cut: <input class="treedraw_cut ui-corner-all ui-widget" style="width:8em;margin-left:5px" title="cut expression"></input>' +
          'Opt: <input class="treedraw_opt ui-corner-all ui-widget" style="width:5em;margin-left:5px" title="histogram draw options"></input>' +
          `Num: <input class="treedraw_number" type='number' min="0" max="${numentries}" step="1000" style="width:7em;margin-left:5px" title="number of entries to process (default all)"></input>` +
          `First: <input class="treedraw_first" type='number' min="0" max="${numentries}" step="1000" style="width:7em;margin-left:5px" title="first entry to process (default first)"></input>` +
          '<button class="treedraw_clear" title="Clear drawing">Clear</button>';

      main.select('.treedraw_exe').on('click', () => this.performDraw());
      main.select('.treedraw_cut').property('value', args?.parse_cut || '').on('change', () => this.performDraw());
      main.select('.treedraw_opt').property('value', args?.drawopt || '').on('change', () => this.performDraw());
      main.select('.treedraw_number').attr('value', args?.numentries || ''); // .on('change', () => this.performDraw());
      main.select('.treedraw_first').attr('value', args?.firstentry || ''); // .on('change', () => this.performDraw());
      main.select('.treedraw_clear').on('click', () => cleanup(this.drawid));
   };

   player.showPlayer = function(args) {
      const main = this.selectDom();

      this.drawid = 'jsroot_tree_player_' + internals.id_counter++ + '_draw';

      const show_extra = args?.parse_cut || args?.numentries || args?.firstentry;

      main.html('<div style="display:flex; flex-flow:column; height:100%; width:100%;">' +
                   '<div class="treedraw_buttons" style="flex: 0 1 auto;margin-top:0.2em;">' +
                      '<button class="treedraw_exe" title="Execute draw expression" style="margin-left:0.5em">Draw</button>' +
                      'Expr:<input class="treedraw_varexp treedraw_varexp_info" style="width:12em;margin-left:5px" title="draw expression"></input>' +
                      '<label class="treedraw_varexp_info">\u24D8</label>' +
                     '<button class="treedraw_more">More</button>' +
                   '</div>' +
                   '<div style="flex: 0 1 auto"><hr/></div>' +
                   `<div id="${this.drawid}" style="flex: 1 1 auto; overflow:hidden;"></div>` +
                '</div>');

      // only when main html element created, one can set painter
      // ObjectPainter allow such usage of methods from BasePainter
      this.setTopPainter();

      if (this.local_tree) {
         main.select('.treedraw_buttons')
             .attr('title', 'Tree draw player for: ' + this.local_tree.fName);
      }
      main.select('.treedraw_exe').on('click', () => this.performDraw());
      main.select('.treedraw_varexp')
          .attr('value', args?.parse_expr || this.draw_expr || 'px:py')
          .on('change', () => this.performDraw());
      main.select('.treedraw_varexp_info')
          .attr('title', 'Example of valid draw expressions:\n' +
                         '  px - 1-dim draw\n' +
                         '  px:py - 2-dim draw\n' +
                         '  px:py:pz - 3-dim draw\n' +
                         '  px+py:px-py - use any expressions\n' +
                         '  px:py>>Graph - create and draw TGraph\n' +
                         '  px:py>>dump - dump extracted variables\n' +
                         '  px:py>>h(50,-5,5,50,-5,5) - custom histogram\n' +
                         '  px:py;hbins:100 - custom number of bins');

      if (show_extra)
         this.showExtraButtons(args);
      else
         main.select('.treedraw_more').on('click', () => this.showExtraButtons(args));

      this.checkResize();

      registerForResize(this);
   };

   player.getValue = function(sel) {
      const elem = this.selectDom().select(sel);
      if (elem.empty())
         return;
      return elem.property('value') ?? elem.attr('value');
   };

   player.performLocalDraw = function() {
      if (!this.local_tree)
         return;

      const frame = this.selectDom(),
            args = { expr: this.getValue('.treedraw_varexp') };

      if (frame.select('.treedraw_more').empty()) {
         args.cut = this.getValue('.treedraw_cut');
         if (!args.cut)
            delete args.cut;

         args.drawopt = this.getValue('.treedraw_opt');
         if (args.drawopt === 'dump') {
            args.dump = true;
            args.drawopt = '';
         }
         if (!args.drawopt)
            delete args.drawopt;

         args.numentries = parseInt(this.getValue('.treedraw_number'));
         if (!Number.isInteger(args.numentries))
            delete args.numentries;

         args.firstentry = parseInt(this.getValue('.treedraw_first'));
         if (!Number.isInteger(args.firstentry))
            delete args.firstentry;
      }

      cleanup(this.drawid);

      args.drawid = this.drawid;

      args.progress = treeDrawProgress.bind(args);

      treeDraw(this.local_tree, args).then(obj => args.progress(obj, true));
   };

   player.getDrawOpt = function() {
      let res = 'player';
      const expr = this.getValue('.treedraw_varexp');
      if (expr)
         res += ':' + expr;
      return res;
   };

   player.performDraw = function() {
      if (this.local_tree)
         return this.performLocalDraw();

      const frame = this.selectDom();
      let url = this.url + '/exe.json.gz?compact=3&method=Draw',
          expr = this.getValue('.treedraw_varexp'),
          hname = 'h_tree_draw', option = '';
      const pos = expr.indexOf('>>');

      if (pos < 0)
         expr += `>>${hname}`;
      else {
         hname = expr.slice(pos + 2);
         if (hname[0] === '+')
            hname = hname.slice(1);
         const pos2 = hname.indexOf('(');
         if (pos2 > 0)
            hname = hname.slice(0, pos2);
      }

      if (frame.select('.treedraw_more').empty()) {
         const cut = this.getValue('.treedraw_cut');
         let nentries = this.getValue('.treedraw_number'),
             firstentry = this.getValue('.treedraw_first');

         option = this.getValue('.treedraw_opt');

         url += `&prototype="const char*,const char*,Option_t*,Long64_t,Long64_t"&varexp="${expr}"&selection="${cut}"`;

         // provide all optional arguments - default value kMaxEntries not works properly in ROOT6
         if (!nentries)
            nentries = 'TTree::kMaxEntries'; // kMaxEntries available since ROOT 6.05/03
         if (!firstentry)
            firstentry = '0';
         url += `&option="${option}"&nentries=${nentries}&firstentry=${firstentry}`;
      } else
         url += `&prototype="Option_t*"&opt="${expr}"`;

      url += `&_ret_object_=${hname}`;

      const submitDrawRequest = () => {
         httpRequest(url, 'object').then(res => {
            cleanup(this.drawid);
            drawTreeDrawResult(this.drawid, res, option);
         });
      };

      this.draw_expr = expr;

      if (this.askey) {
         // first let read tree from the file
         this.askey = false;
         httpRequest(this.url + '/root.json.gz?compact=3', 'text').then(submitDrawRequest);
      } else
         submitDrawRequest();
   };

   player.checkResize = function(/* arg */) {
      resize(this.drawid);
   };

   return player;
}


/** @summary function used with THttpServer to assign player for the TTree object
  * @private */
function drawTreePlayer(hpainter, itemname, askey, asleaf) {
   let item = hpainter.findItem(itemname),
       expr = '', leaf_cnt = 0;
   const top = hpainter.getTopOnlineItem(item);
   if (!item || !top)
      return null;

   if (asleaf) {
      expr = item._name;
      while (item && !item._ttree)
         item = item._parent;
      if (!item)
         return null;
      itemname = hpainter.itemFullName(item);
   }

   const url = hpainter.getOnlineItemUrl(itemname);
   if (!url)
      return null;

   const root_version = top._root_version || 400129, // by default use version number 6-27-01
         mdi = hpainter.getDisplay();
   if (!mdi)
      return null;

   const frame = mdi.findFrame(itemname, true);
   if (!frame)
      return null;

   const divid = select(frame).attr('id'),
         player = new BasePainter(divid);

   if (item._childs && !asleaf) {
      for (let n = 0; n < item._childs.length; ++n) {
         const leaf = item._childs[n];
         if (isStr(leaf?._kind) && (leaf._kind.indexOf(getKindForType('TLeaf')) === 0) && (leaf_cnt < 2)) {
            if (leaf_cnt++ > 0)
               expr += ':';
            expr += leaf._name;
         }
      }
   }

   createTreePlayer(player);
   player.configureOnline(itemname, url, askey, root_version, expr);
   player.showPlayer();

   return player;
}

/** @summary function used with THttpServer when tree is not yet loaded
  * @private */
function drawTreePlayerKey(hpainter, itemname) {
   return drawTreePlayer(hpainter, itemname, true);
}

/** @summary function used with THttpServer when tree is not yet loaded
  * @private */
function drawLeafPlayer(hpainter, itemname) {
   return drawTreePlayer(hpainter, itemname, false, true);
}

/** @summary function called from draw()
  * @desc just envelope for real TTree::Draw method which do the main job
  * Can be also used for the branch and leaf object
  * @private */
async function drawTree(dom, obj, opt) {
   let tree = obj, args = opt;

   if (obj._typename === clTBranchFunc) {
      // fictional object, created only in browser
      args = { expr: `.${obj.func}()`, branch: obj.branch };
      if (opt && opt.indexOf('dump') === 0)
         args.expr += '>>' + opt;
      else if (opt)
         args.expr += opt;
      tree = obj.branch.$tree;
   } else if (obj.$branch) {
      // this is drawing of the single leaf from the branch
      args = { expr: `.${obj.fName}${opt || ''}`, branch: obj.$branch };
      if ((args.branch.fType === kClonesNode) || (args.branch.fType === kSTLNode)) {
         // special case of size
         args.expr = opt;
         args.direct_branch = true;
      }

      tree = obj.$branch.$tree;
   } else if (obj.$tree) {
      // this is drawing of the branch

      // if generic object tried to be drawn without specifying any options, it will be just dump
      if (!opt && obj.fStreamerType && (obj.fStreamerType !== kTString) &&
          (obj.fStreamerType >= kObject) && (obj.fStreamerType <= kAnyP))
         opt = 'dump';

      args = { expr: opt, branch: obj };
      tree = obj.$tree;
   } else {
      if (!args)
         args = 'player';
      if (isStr(args))
         args = { expr: args };
   }

   if (!tree)
      throw Error('No TTree object available for TTree::Draw');

   if (isStr(args.expr)) {
      const p = args.expr.indexOf('player');
      if (p === 0) {
         args.player = true;
         args.expr = args.expr.slice(6);
         if (args.expr[0] === ':')
            args.expr = args.expr.slice(1);
      } else if ((p >= 0) && (p === args.expr.length - 6)) {
         args.player = true;
         args.expr = args.expr.slice(0, p);
         if ((p > 0) && (args.expr[p - 1] === ';'))
            args.expr = args.expr.slice(0, p - 1);
      }
   }

   let painter;

   if (args.player) {
      painter = new ObjectPainter(dom, obj, opt);
      createTreePlayer(painter);
      painter.configureTree(tree);
      painter.showPlayer(args);
      args.drawid = painter.drawid;
   } else
      args.drawid = dom;


   // use in result handling same function as for progress handling

   args.progress = treeDrawProgress.bind(args);

   let pr;
   if (args.expr === 'testio') {
      args.testio = true;
      args.showProgress = msg => showProgress(msg, -1, () => { args._break = 1; });
      pr = treeIOTest(tree, args);
   } else if (args.expr || args.branch)
      pr = treeDraw(tree, args);
   else
      return painter;

   return pr.then(res => args.progress(res, true));
}

var TTree = /*#__PURE__*/Object.freeze({
__proto__: null,
drawLeafPlayer: drawLeafPlayer,
drawTree: drawTree,
drawTreePlayer: drawTreePlayer,
drawTreePlayerKey: drawTreePlayerKey,
treeDrawProgress: treeDrawProgress
});

const kTextNDC = BIT(14);

class TTextPainter extends ObjectPainter {

   async _redrawText(x, y, annot) {
      const text = this.getObject(),
            pp = this.getPadPainter(),
            fp = this.getFramePainter(),
            is_url = text.fName.startsWith('http://') || text.fName.startsWith('https://');
      let fact = 1, use_frame = false;

      this.createAttText({ attr: text });

      if ((annot === '3d') || text.TestBit(kTextNDC))
         this.isndc = true;
      else if (!annot && pp?.getRootPad(true)) {
         // force pad coordinates
         const d = new DrawOptions(this.getDrawOpt());
         use_frame = d.check('FRAME');
      } else if (!annot) {
         // place in the middle
         this.isndc = true;
         x = y = 0.5;
         text.fTextAlign = 22;
      }

      const g = this.createG(use_frame ? 'frame2d' : undefined, is_url);

      g.attr('transform', null); // remove transform from interactive changes

      x = this.axisToSvg('x', x, this.isndc);
      y = this.axisToSvg('y', y, this.isndc);
      this.swap_xy = use_frame && fp?.swap_xy();

      if (this.swap_xy)
         [x, y] = [y, x];

      const arg = this.textatt.createArg({ x, y, text: text.fTitle, latex: 0 });

      if (this.matchObjectType(clTLatex) || annot)
         arg.latex = 1;
      else if (this.matchObjectType(clTMathText)) {
         arg.latex = 2;
         fact = 0.8;
      }

      if (is_url) {
         g.attr('href', text.fName);
         if (!this.isBatchMode())
            g.append('svg:title').text(`link on ${text.fName}`);
      }

      return this.startTextDrawingAsync(this.textatt.font, this.textatt.getSize(pp, fact))
               .then(() => this.drawText(arg))
               .then(() => this.finishTextDrawing())
               .then(() => {
                  if (this.isBatchMode())
                     return this;

                  if (pp.isButton() && !pp.isEditable()) {
                     g.on('click', () => this.getCanvPainter().selectActivePad(pp));
                     return this;
                  }

                  Object.assign(this, { pos_x: x, pos_y: y, pos_dx: 0, pos_dy: 0 });

                  if (annot !== '3d')
                     addMoveHandler(this, true, is_url);

                  assignContextMenu(this);

                  if (this.matchObjectType(clTLink))
                     g.style('cursor', 'pointer').on('click', () => this.submitCanvExec('ExecuteEvent(kButton1Up, 0, 0);;'));

                  return this;
               });
   }

   async redraw() {
      return this._redrawText(this.getObject().fX, this.getObject().fY);
   }

   moveDrag(dx, dy) {
      this.pos_dx += dx;
      this.pos_dy += dy;
      makeTranslate(this.getG(), this.pos_dx, this.pos_dy);
   }

   moveEnd(not_changed) {
      if (not_changed)
         return;
      const txt = this.getObject();
      let fx = this.svgToAxis('x', this.pos_x + this.pos_dx, this.isndc),
          fy = this.svgToAxis('y', this.pos_y + this.pos_dy, this.isndc);
      if (this.swap_xy)
         [fx, fy] = [fy, fx];

      txt.fX = fx;
      txt.fY = fy;
      this.submitCanvExec(`SetX(${fx});;SetY(${fy});;`);
   }

   fillContextMenuItems(menu) {
      const text = this.getObject();
      menu.add('Change text', () => menu.input('Enter new text', text.fTitle).then(t => {
         text.fTitle = t;
         this.interactiveRedraw('pad', `exec:SetTitle("${t}")`);
      }));
   }

   /** @summary draw TText-derived object */
   static async draw(dom, obj, opt) {
      const painter = new TTextPainter(dom, obj, opt);
      return ensureTCanvas(painter, false).then(() => painter.redraw());
   }

} // class TTextPainter

var TTextPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TTextPainter: TTextPainter
});

function getRotation(camera, mesh) {
   const dx = camera.position.x - mesh.position.x,
         dy = camera.position.y - mesh.position.y;
   return Math.atan2(dy, dx) + Math.PI / 2;
}

class TAnnotation3DPainter extends TTextPainter {

   /** @summary Redraw annotation
    * @desc handle 3d and 2d mode */

   async redraw() {
      const fp = this.getFramePainter(),
            text = this.getObject();

      if (fp?.mode3d && !this.use_2d) {
         const mesh = build3dlatex(text, '', this, fp);
         mesh.traverse(o => o.geometry?.rotateX(Math.PI / 2));
         mesh.position.set(fp.grx(text.fX), fp.gry(text.fY), fp.grz(text.fZ));
         mesh.rotation.set(0, 0, getRotation(fp.camera, mesh));
         fp.processRender3D = true;
         fp.add3DMesh(mesh, this, true);
         fp.render3D(100);
         return this;
      }

      const mode = fp?.mode3d && isFunc(fp?.convert3DtoPadNDC) ? '3d' : '2d';
      let x = text.fX, y = text.fY;

      if (mode === '3d') {
         const pos = fp.convert3DtoPadNDC(text.fX, text.fY, text.fZ);
         x = pos.x;
         y = pos.y;
      }

      return this._redrawText(x, y, mode).then(() => {
         fp.processRender3D = mode === '3d';
         return this;
      });
   }

   /** @summary Extra handling during 3d rendering
     * @desc Allows to reposition annotation when rotate/zoom drawing */
   handleRender3D() {
      const text = this.getObject(),
            fp = this.getFramePainter();
      if (this.use_2d) {
         const pos = fp.convert3DtoPadNDC(text.fX, text.fY, text.fZ),
               new_x = this.axisToSvg('x', pos.x, true),
               new_y = this.axisToSvg('y', pos.y, true);
         makeTranslate(this.getG(), new_x - this.pos_x, new_y - this.pos_y);
      } else
         fp.get3DMeshes(this).forEach(mesh => mesh.rotation.set(0, 0, getRotation(fp.camera, mesh)));
   }

   /** @summary draw TAnnotation3D object */
   static async draw(dom, obj, opt) {
      const painter = new TAnnotation3DPainter(dom, obj, opt);
      painter.use_2d = (opt === '2d') || (opt === '2D');
      return ensureTCanvas(painter, painter.use_2d ? true : '3d').then(() => painter.redraw());
   }

} // class TAnnotation3DPainter

var TAnnotation3DPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TAnnotation3DPainter: TAnnotation3DPainter
});

const kIsZoomed = BIT(16); // bit set when zooming on Y axis

/**
 * @summary Painter class for THStack
 *
 * @private
 */

let THStackPainter$2 = class THStackPainter extends ObjectPainter {

   #firstpainter;  // first painter on stack
   #painters;      // array of sub-painters
   #stack;         // internal stack of histograms
   #did_update;    // flag used in update

   /** @summary constructor
     * @param {object|string} dom - DOM element for drawing or element id
     * @param {object} stack - THStack object
     * @param {string} [opt] - draw options */
   constructor(dom, stack, opt) {
      super(dom, stack, opt);
      this.#firstpainter = null;
      this.#painters = []; // keep painters to be able update objects
   }

   /** @summary Cleanup THStack painter */
   cleanup() {
      this.getPadPainter()?.cleanPrimitives(objp => { return (objp === this.#firstpainter) || (this.#painters.indexOf(objp) >= 0); });
      this.#firstpainter = null;
      this.#painters = [];
      this.#stack = undefined;
      super.cleanup();
   }

   /** @summary Build sum of all histograms
     * @desc Build a separate #stack containing the running sum of all histograms */
   buildStack(stack, pp) {
      this.#stack = null;

      if (!stack.fHists)
         return false;
      const nhists = stack.fHists.arr.length;
      if (nhists <= 0)
         return false;

      let arr = pp?.findInPrimitives(undefined, clTObjArray);
      if ((arr?.arr.length === nhists) && (arr?.name === stack.fName)) {
         this.#stack = arr;
         return true;
      }

      arr = create$1(clTObjArray);
      let hprev = clone(stack.fHists.arr[0]);
      arr.arr.push(hprev);
      for (let i = 1; i < nhists; ++i) {
         const hnext = clone(stack.fHists.arr[i]),
               xnext = hnext.fXaxis, xprev = hprev.fXaxis;

         let match = (xnext.fNbins === xprev.fNbins) &&
                     (xnext.fXmin === xprev.fXmin) &&
                     (xnext.fXmax === xprev.fXmax);

         if (!match && (xnext.fNbins > 0) && (xnext.fNbins < xprev.fNbins) && (xnext.fXmin === xprev.fXmin) &&
             (Math.abs((xnext.fXmax - xnext.fXmin) / xnext.fNbins - (xprev.fXmax - xprev.fXmin) / xprev.fNbins) < 0.0001)) {
            // simple extension of histogram to make sum
            const arr2 = new Array(hprev.fNcells).fill(0);
            for (let n = 1; n <= xnext.fNbins; ++n)
               arr2[n] = hnext.fArray[n];
            hnext.fNcells = hprev.fNcells;
            Object.assign(xnext, xprev);
            hnext.fArray = arr2;
            match = true;
         }
         if (!match) {
            console.warn(`When drawing THStack, cannot sum-up histograms ${hnext.fName} and ${hprev.fName}`);
            return false;
         }

         // trivial sum of histograms
         for (let n = 0; n < hnext.fArray.length; ++n)
            hnext.fArray[n] += hprev.fArray[n];

         arr.arr.push(hnext);
         hprev = hnext;
      }
      this.#stack = arr;
      return true;
   }

   /** @summary Returns stack min/max values */
   getMinMax(iserr) {
      const stack = this.getObject(),
            o = this.getOptions(),
            pad = this.getPadPainter()?.getRootPad(true),
            logscale = pad?.fLogv ?? (o.ndim === 1 ? pad?.fLogy : pad?.fLogz);
      let themin = 0, themax = 0;

      const getHistMinMax = (hist, witherr) => {
         const res = { min: 0, max: 0 };
         let domin = true, domax = true;
         if (hist.fMinimum !== kNoZoom) {
            res.min = hist.fMinimum;
            domin = false;
         }
         if (hist.fMaximum !== kNoZoom) {
            res.max = hist.fMaximum;
            domax = false;
         }

         if (!domin && !domax)
            return res;

         let i1 = 1, i2 = hist.fXaxis.fNbins, j1 = 1, j2 = 1, first = true;

         if (hist.fXaxis.TestBit(EAxisBits.kAxisRange)) {
            i1 = hist.fXaxis.fFirst;
            i2 = hist.fXaxis.fLast;
         }

         if (hist._typename.indexOf(clTH2) === 0) {
            j2 = hist.fYaxis.fNbins;
            if (hist.fYaxis.TestBit(EAxisBits.kAxisRange)) {
               j1 = hist.fYaxis.fFirst;
               j2 = hist.fYaxis.fLast;
            }
         }
         let err = 0;
         for (let j = j1; j <= j2; ++j) {
            for (let i = i1; i <= i2; ++i) {
               const val = hist.getBinContent(i, j);
               if (witherr)
                  err = hist.getBinError(hist.getBin(i, j));
               if (logscale && (val - err <= 0))
                  continue;
               if (domin && (first || (val - err < res.min)))
                  res.min = val - err;
               if (domax && (first || (val + err > res.max)))
                  res.max = val + err;
               first = false;
            }
         }

         return res;
      };

      if (o.nostack) {
         for (let i = 0; i < stack.fHists.arr.length; ++i) {
            const resh = getHistMinMax(stack.fHists.arr[i], iserr);
            if (i === 0) {
               themin = resh.min;
               themax = resh.max;
            } else {
               themin = Math.min(themin, resh.min);
               themax = Math.max(themax, resh.max);
            }
         }
      } else {
         themin = getHistMinMax(this.#stack.arr.at(0), iserr).min;
         themax = getHistMinMax(this.#stack.arr.at(-1), iserr).max;
      }

      if (logscale)
         themin = (themin > 0) ? themin * 0.9 : themax * 1e-3;
      else if (themin > 0)
         themin = 0;

      if (stack.fMaximum !== kNoZoom)
         themax = stack.fMaximum;

      if (stack.fMinimum !== kNoZoom)
         themin = stack.fMinimum;

      // redo code from THStack::BuildAndPaint

      if (!o.nostack || (stack.fMaximum === kNoZoom)) {
         if (logscale) {
            if (themin > 0)
               themax *= (1 + 0.2 * Math.log10(themax / themin));
         } else if (stack.fMaximum === kNoZoom)
            themax *= (1 + gStyle.fHistTopMargin);
      }
      if (!o.nostack || (stack.fMinimum === kNoZoom)) {
         if (logscale)
            themin = (themin > 0) ? themin / (1 + 0.5 * Math.log10(themax / themin)) : 1e-3 * themax;
      }

      const res = { min: themin, max: themax, hopt: `;hmin:${themin};hmax:${themax}` };
      if (stack.fHistogram?.TestBit(kIsZoomed))
         res.hopt += ';zoom_min_max';

      return res;
   }

   /** @summary Provide draw options for the histogram */
   getHistDrawOption(hist, opt) {
      const o = this.getOptions();
      let hopt = opt || hist.fOption || o.hopt;
      if (hopt.toUpperCase().indexOf(o.hopt) < 0)
         hopt += ' ' + o.hopt;
      if (o.draw_errors && !hopt)
         hopt = 'E';
      if (o.zscale) {
         const p = hopt.toUpperCase().indexOf('COLZ');
         if (p >= 0)
            hopt = hopt.slice(0, p + 3) + hopt.slice(p + 4);
      }
      if (!o.pads)
         hopt += ' same nostat' + o.auto;
      return hopt;
   }

   /** @summary Draw next stack histogram */
   async drawNextHisto(indx, pad_painter) {
      const stack = this.getObject(),
            o = this.getOptions(),
            hlst = o.nostack ? stack.fHists : this.#stack,
            nhists = hlst?.arr?.length || 0;

      if (indx >= nhists)
         return this;

      const rindx = o.horder ? indx : nhists - indx - 1,
            subid = o.nostack ? `hists_${rindx}` : `stack_${rindx}`,
            hist = hlst.arr[rindx],
            hopt = this.getHistDrawOption(hist, stack.fHists.opt[rindx]);

      // handling of 'pads' draw option
      if (pad_painter) {
         const subpad_painter = pad_painter.getSubPadPainter(indx + 1);
         if (!subpad_painter)
            return this;

         subpad_painter.cleanPrimitives(true);

         return this.drawHist(subpad_painter, hist, hopt).then(subp => {
            if (subp) {
               subp.setSecondaryId(this, subid);
               this.#painters.push(subp);
            }
            return this.drawNextHisto(indx + 1, pad_painter);
         });
      }

      // special handling of stacked histograms
      // also used to provide tooltips
      if ((rindx > 0) && !o.nostack)
         hist.$baseh = hlst.arr[rindx - 1];
      // this number used for auto colors creation
      if (o.auto)
         hist.$num_histos = nhists;

      const dom = this.#firstpainter?.getPadPainter() || this.getDrawDom();

      return this.drawHist(dom, hist, hopt).then(subp => {
         subp.setSecondaryId(this, subid);
         this.#painters.push(subp);
         return this.drawNextHisto(indx + 1, pad_painter);
      });
   }

   /** @summary Decode draw options of THStack painter */
   decodeOptions(opt) {
      const o = this.setOptions({ ndim: 1, nostack: false, same: false, horder: true, has_errors: false, draw_errors: false, hopt: '', auto: '' }),
            stack = this.getObject(),
            hist = stack.fHistogram || stack.fHists?.arr[0] || this.#stack?.arr[0];

      if (hist?._typename.indexOf(clTH2) === 0)
         o.ndim = 2;

      if ((o.ndim === 2) && !opt)
         opt = 'lego1';

      if (!o.nostack) {
         stack.fHists?.arr.forEach(h => {
            const len = h.fSumw2?.length ?? 0;
            for (let n = 0; n < len; ++n) {
               if (h.fSumw2[n] > 0) {
                  o.has_errors = true;
                  break;
               }
            }
         });
      }

      o.nhist = stack.fHists?.arr?.length ?? 1;

      const d = new DrawOptions(opt);

      o.nostack = d.check('NOSTACK');
      if (d.check('STACK'))
         o.nostack = false;
      o.same = d.check('SAME');

      d.check('NOCLEAR'); // ignore option

      ['PFC', 'PLC', 'PMC'].forEach(f => {
         if (d.check(f))
            o.auto += ' ' + f;
      });

      o.pads = d.check('PADS');
      if (o.pads)
         o.nostack = true;

      o.hopt = d.remain().trim(); // use remaining draw options for histogram draw

      const dolego = d.check('LEGO');

      o.errors = d.check('E');

      o.zscale = d.check('COLZ');

      // if any histogram appears with pre-calculated errors, use E for all histograms
      if (!o.nostack && o.has_errors && !dolego && !d.check('HIST') && (o.hopt.indexOf('E') < 0))
         o.draw_errors = true;

      o.horder = o.nostack || dolego;
   }

   /** @summary Create main histogram for THStack axis drawing */
   createHistogram(stack) {
      const o = this.getOptions(),
            histos = stack.fHists,
            numhistos = histos?.arr.length ?? 0;

      if (!numhistos) {
         const histo = createHistogram(clTH1F, 100);
         setHistogramTitle(histo, stack.fTitle);
         histo.fBits |= kNoStats;
         return histo;
      }

      const h0 = histos.arr[0],
            histo = createHistogram((o.ndim === 1) ? clTH1F : clTH2F, h0.fXaxis.fNbins, h0.fYaxis.fNbins);

      histo.fName = 'axis_hist';
      histo.fBits |= kNoStats;
      Object.assign(histo.fXaxis, h0.fXaxis);
      if (o.ndim === 2)
         Object.assign(histo.fYaxis, h0.fYaxis);

      // this code is not exists in ROOT painter, can be skipped?
      for (let n = 1; n < numhistos; ++n) {
         const h = histos.arr[n];

         if (!histo.fXaxis.fLabels) {
            histo.fXaxis.fXmin = Math.min(histo.fXaxis.fXmin, h.fXaxis.fXmin);
            histo.fXaxis.fXmax = Math.max(histo.fXaxis.fXmax, h.fXaxis.fXmax);
         }

         if ((o.ndim === 2) && !histo.fYaxis.fLabels) {
            histo.fYaxis.fXmin = Math.min(histo.fYaxis.fXmin, h.fYaxis.fXmin);
            histo.fYaxis.fXmax = Math.max(histo.fYaxis.fXmax, h.fYaxis.fXmax);
         }
      }

      histo.fTitle = stack.fTitle;

      return histo;
   }

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

      const stack = this.getObject(),
            pp = this.getPadPainter(),
            o = this.getOptions();

      stack.fHists = obj.fHists;
      stack.fTitle = obj.fTitle;
      stack.fMinimum = obj.fMinimum;
      stack.fMaximum = obj.fMaximum;

      if (!o.nostack)
         o.nostack = !this.buildStack(stack, pp);

      if (this.#firstpainter) {
         let src = obj.fHistogram;
         if (!src)
            src = stack.fHistogram = this.createHistogram(stack);

         const mm = this.getMinMax(o.errors || o.draw_errors);
         this.#firstpainter.options.hmin = mm.min;
         this.#firstpainter.options.hmax = mm.max;

         this.#firstpainter._checked_zooming = false; // force to check 3d zooming

         if (o.ndim === 1) {
            this.#firstpainter.ymin = mm.min;
            this.#firstpainter.ymax = mm.max;
         } else {
            this.#firstpainter.zmin = mm.min;
            this.#firstpainter.zmax = mm.max;
         }

         this.#firstpainter.updateObject(src);

         this.#firstpainter.options.zoom_min_max = src.TestBit(kIsZoomed);
      }

      // and now update histograms
      const hlst = o.nostack ? stack.fHists : this.#stack,
            nhists = hlst?.arr?.length ?? 0;

      if (nhists !== this.#painters.length) {
         this.#did_update = 1;
         pp?.cleanPrimitives(objp => this.#painters.indexOf(objp) >= 0);
         this.#painters = [];
      } else {
         this.#did_update = 2;
         for (let indx = 0; indx < nhists; ++indx) {
            const rindx = o.horder ? indx : nhists - indx - 1,
                  hist = hlst.arr[rindx];
            this.#painters[indx].updateObject(hist, this.getHistDrawOption(hist, stack.fHists.opt[rindx]));
         }
      }

      return true;
   }

   /** @summary Redraw THStack
     * @desc Do something if previous update had changed number of histograms */
   redraw(reason) {
      if (!this.#did_update)
         return;

      const full_redraw = this.#did_update === 1;
      this.#did_update = undefined;

      let pr = Promise.resolve(this);

      const o = this.getOptions();

      if (this.#firstpainter) {
         const mm = this.getMinMax(o.errors || o.draw_errors);
         this.#firstpainter.decodeOptions(o.hopt + mm.hopt);
         pr = this.#firstpainter.redraw(reason);
      }

      return pr.then(() => {
         if (full_redraw)
            return this.drawNextHisto(0, o.pads ? this.getPadPainter() : null);

         const redrawSub = indx => {
            if (indx >= this.#painters.length)
               return this;
            return this.#painters[indx].redraw(reason).then(() => redrawSub(indx + 1));
         };
         return redrawSub(0);
      });
   }

   /** @summary Fill THStack context menu */
   fillContextMenuItems(menu) {
      const o = this.getOptions();
      menu.addRedrawMenu(this);
      if (!o.pads) {
         menu.addchk(o.draw_errors, 'Draw errors', flag => {
            o.draw_errors = flag;
            const stack = this.getObject(),
                  hlst = o.nostack ? stack.fHists : this.#stack,
                  nhists = hlst?.arr?.length ?? 0;
            for (let indx = 0; indx < nhists; ++indx) {
               const rindx = o.horder ? indx : nhists - indx - 1,
                     hist = hlst.arr[rindx];
               this.#painters[indx].decodeOptions(this.getHistDrawOption(hist, stack.fHists.opt[rindx]));
            }
            this.redrawPad();
         }, 'Change draw erros in the stack');
      }
   }

   /** @summary Invoke histogram drawing */
   drawHist(dom, hist, hopt) {
      const func = (this.getOptions().ndim === 1) ? TH1Painter$2.draw : TH2Painter$2.draw;
      return func(dom, hist, hopt);
   }

   /** @summary Access or modify histogram min/max
    * @private */
   accessMM(ismin, v) {
      const name = ismin ? 'fMinimum' : 'fMaximum',
            stack = this.getObject();
      if (v === undefined)
         return stack[name];

      this.#did_update = 2;

      stack[name] = v;

      this.interactiveRedraw('pad', ismin ? `exec:SetMinimum(${v})` : `exec:SetMaximum(${v})`);
   }

   /** @summary Full stack redraw with specified draw option */
   async redrawWith(opt, skip_cleanup) {
      const pp = this.getPadPainter(),
            o = this.getOptions();

      if (!skip_cleanup && pp) {
         this.#firstpainter = null;
         this.#painters = [];
         if (o.pads)
            pp.divide(0, 0);
         pp.removePrimitive(this, true);
      }

      this.decodeOptions(opt);

      const stack = this.getObject();

      let pr = Promise.resolve(this), pad_painter = null;

      if (o.pads) {
         pr = ensureTCanvas(this, false).then(() => {
            pad_painter = this.getPadPainter();
            return pad_painter.divide(o.nhist, 0, true);
         });
      } else {
         if (!o.nostack)
            o.nostack = !this.buildStack(stack, pp);

         if (!o.same && stack.fHists?.arr.length) {
            if (!stack.fHistogram)
               stack.fHistogram = this.createHistogram(stack);

            const mm = this.getMinMax(o.errors || o.draw_errors);

            pr = this.drawHist(this.getDrawDom(), stack.fHistogram, o.hopt + mm.hopt).then(subp => {
               this.#firstpainter = subp;
               subp.$stack_hist = true;
               subp.setSecondaryId(this, 'hist'); // mark hist painter as created by THStack
            });
         }
      }

      return pr.then(() => this.drawNextHisto(0, pad_painter)).then(() => {
         if (!o.pads)
            this.addToPadPrimitives();
         return this;
      });
   }

   /** @summary draw THStack object in 2D only */
   static async draw(dom, stack, opt) {
      if (!stack.fHists || !stack.fHists.arr)
         return null; // drawing not needed

      const painter = new THStackPainter(dom, stack, opt);

      return painter.redrawWith(opt, true);
   }

}; // class THStackPainter

class THStackPainter extends THStackPainter$2 {

   /** @summary Invoke histogram drawing */
   drawHist(dom, hist, hopt) {
      const func = (this.getOptions().ndim === 1) ? TH1Painter.draw : TH2Painter.draw;
      return func(dom, hist, hopt);
   }

   /** @summary draw THStack object */
   static async draw(dom, stack, opt) {
      if (!stack.fHists || !stack.fHists.arr)
         return null; // drawing not needed

      const painter = new THStackPainter(dom, stack, opt);

      return painter.redrawWith(opt, true);
   }

} // class THStackPainter

var THStackPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
THStackPainter: THStackPainter
});

/** @summary Prepare frame painter for 3D drawing
  * @private */
function before3DDraw(painter) {
   const fp = painter.getFramePainter();

   if (!fp?.mode3d || !painter.getObject())
      return null;

   if (fp?.toplevel)
      return fp;

   const main = painter.getMainPainter();

   if (main && !isFunc(main.drawExtras))
      return null;

   const pr = main ? Promise.resolve(main) : drawDummy3DGeom(painter);

   return pr.then(geop => {
      const pp = painter.getPadPainter();
      if (pp)
         pp.options._disable_dragging = true;

      if (geop.options.dummy && isFunc(painter.get3DBox))
         geop.extendCustomBoundingBox(painter.get3DBox());
      return geop.drawExtras(painter.getObject(), '', true, true);
   });
}

/** @summary Function to extract 3DBox for poly marker and line
  * @private */
function get3DBox() {
   const obj = this.getObject();
   if (!obj?.fP.length)
      return null;
   const box = { min: { x: 0, y: 0, z: 0 }, max: { x: 0, y: 0, z: 0 } };

   for (let k = 0; k < obj.fP.length; k += 3) {
      const x = obj.fP[k],
            y = obj.fP[k + 1],
            z = obj.fP[k + 2];
      if (k === 0) {
         box.min.x = box.max.x = x;
         box.min.y = box.max.y = y;
         box.min.z = box.max.z = z;
      } else {
         box.min.x = Math.min(x, box.min.x);
         box.max.x = Math.max(x, box.max.x);
         box.min.y = Math.min(y, box.min.y);
         box.max.y = Math.max(y, box.max.y);
         box.min.z = Math.min(z, box.min.z);
         box.max.z = Math.max(z, box.max.z);
      }
   }

   return box;
}


/** @summary direct draw function for TPolyMarker3D object (optionally with geo painter)
  * @private */
async function drawPolyMarker3D() {
   this.get3DBox = get3DBox;

   const fp = before3DDraw(this);

   if (!isObject(fp) || !fp.grx || !fp.gry || !fp.grz)
      return fp;

   this.$fp = fp;

   return drawPolyMarker3D$1.bind(this)();
}

/** @summary Direct draw function for TPolyLine3D object
  * @desc Takes into account dashed properties
  * @private */
async function drawPolyLine3D() {
   this.get3DBox = get3DBox;

   const line = this.getObject(),
         fp = before3DDraw(this);

   if (!isObject(fp) || !fp.grx || !fp.gry || !fp.grz)
      return fp;

   const limit = 3 * line.fN, p = line.fP, pnts = [];

   for (let n = 3; n < limit; n += 3) {
      pnts.push(fp.grx(p[n - 3]), fp.gry(p[n - 2]), fp.grz(p[n - 1]),
                fp.grx(p[n]), fp.gry(p[n + 1]), fp.grz(p[n + 2]));
   }

   const lines = createLineSegments(pnts, create3DLineMaterial(this, line));

   fp.add3DMesh(lines, this, true);

   fp.render3D(100);

   return true;
}

var draw3d = /*#__PURE__*/Object.freeze({
__proto__: null,
drawPolyLine3D: drawPolyLine3D,
drawPolyMarker3D: drawPolyMarker3D
});

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

class TGraphTimePainter extends ObjectPainter {

   #step; // step number
   #selfid; // use to identify primitives which should be clean
   #wait_animation_frame; // animation flag
   #running_timeout; // timeout handle

   constructor(dom, gr, opt) {
      super(dom, gr, opt);
      this.decodeOptions(opt);
      this.#selfid = 'grtime_' + internals.id_counter++;
   }

   /** @summary Redraw object */
   redraw() {
      if (this.#step === undefined)
         this.startDrawing();
   }

   /** @summary Decode drawing options */
   decodeOptions(opt) {
      const d = new DrawOptions(opt || 'REPEAT');

      this.setOptions({
         once: d.check('ONCE'),
         repeat: d.check('REPEAT'),
         first: d.check('FIRST')
      });

      this.storeDrawOpt(opt);
   }

   /** @summary Draw primitives */
   async drawPrimitives(indx) {
      const lst = this.getObject()?.fSteps.arr[this.#step];

      if (!lst || (indx >= lst.arr.length))
         return;

      const obj = lst.arr[indx],
            opt = lst.opt[indx] + (obj._typename === clTMarker ? ';no_interactive' : '');

      return draw(this.getPadPainter(), obj, opt).then(p => {
         if (p) {
            p.$grtimeid = this.#selfid; // indicator that painter created by ourself
            p.$grstep = this.#step; // remember step
         }
         return this.drawPrimitives(indx + 1);
      });
   }

   /** @summary Continue drawing */
   continueDrawing() {
      const gr = this.getObject(),
            o = this.getOptions();

      if (o.first) {
         // draw only single frame, cancel all others
         this.#step = undefined;
         return;
      }

      if (this.#wait_animation_frame) {
         this.#wait_animation_frame = undefined;

         // clear pad
         const pp = this.getPadPainter();
         if (!pp) {
            // most probably, pad is cleared
            this.#step = undefined;
            return;
         }

         // draw primitives again
         this.drawPrimitives(0).then(() => {
            // clear primitives produced by previous drawing to avoid flicking
            pp.cleanPrimitives(p => { return (p.$grtimeid === this.#selfid) && (p.$grstep !== this.#step); });

            this.continueDrawing();
         });
      } else if (this.#running_timeout) {
         clearTimeout(this.#running_timeout);
         this.#running_timeout = undefined;

         this.#wait_animation_frame = true;
         // use animation frame to disable update in inactive form
         requestAnimationFrame(() => this.continueDrawing());
      } else {
         let sleeptime = Math.max(gr.fSleepTime, 10);

         if (++this.#step > gr.fSteps.arr.length) {
            if (o.repeat) {
               this.#step = 0; // start again
               sleeptime = Math.max(5000, 5 * sleeptime); // increase sleep time
            } else {
               this.#step = undefined;    // clear indicator that animation running
               return;
            }
         }

         this.#running_timeout = setTimeout(() => this.continueDrawing(), sleeptime);
      }
   }

   /** @summary Start drawing of TGraphTime */
   startDrawing() {
      this.#step = 0;

      return this.drawPrimitives(0).then(() => {
         this.continueDrawing();
         return this;
      });
   }

   /** @summary Draw TGraphTime object */
   static async draw(dom, gr, opt) {
      if (!gr.fFrame) {
         console.error('Frame histogram not exists');
         return null;
      }

      const painter = new TGraphTimePainter(dom, gr, opt);

      if (painter.getMainPainter()) {
         console.error('Cannot draw graph time on top of other histograms');
         return null;
      }

      if (!gr.fFrame.fTitle && gr.fTitle) {
         const arr = gr.fTitle.split(';');
         gr.fFrame.fTitle = arr[0];
         if (arr[1])
            gr.fFrame.fXaxis.fTitle = arr[1];
         if (arr[2])
            gr.fFrame.fYaxis.fTitle = arr[2];
      }

      return TH1Painter$2.draw(dom, gr.fFrame, '').then(() => {
         painter.addToPadPrimitives();
         return painter.startDrawing();
      });
   }

} // class TGraphTimePainter


/** @summary Draw TRooPlot
  * @private */
async function drawRooPlot(dom, plot) {
   return draw(dom, plot._hist, 'hist').then(async hp => {
      const arr = [];
      for (let i = 0; i < plot._items.arr.length; ++i)
         arr.push(draw(dom, plot._items.arr[i], plot._items.opt[i]));
      return Promise.all(arr).then(() => hp);
   });
}

var TGraphTimePainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TGraphTimePainter: TGraphTimePainter,
drawRooPlot: drawRooPlot
});

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

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

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

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

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

class TGraphDelaunay {

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

      this.SetMaxIter();
   }


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

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

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

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

      return zz;
   }


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

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


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

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

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

      return oddNodes;
   }


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

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

      // order the vertices before storing them
      do {
         swap = false;
         if (ns > ps) {
            [ns, ps] = [ps, ns];
            swap = true;
         }
         if (ms > ns) {
            [ms, ns] = [ns, ms];
            swap = true;
         }
      } while (swap);

      // store a new Delaunay triangle
      this.fNdt++;
      this.fPTried.push(ps);
      this.fNTried.push(ns);
      this.fMTried.push(ms);
   }


   // Attempt to find all the Delaunay triangles of the point set. It is not
   // guaranteed that it will fully succeed, and no check is made that it has
   // fully succeeded (such a check would be possible by referencing the points
   // that make up the convex hull). The method is to check if each triangle
   // shares all three of its sides with other triangles. If not, a point is
   // generated just outside the triangle on the side(s) not shared, and a new
   // triangle is found for that point. If this method is not working properly
   // (many triangles are not being found) it's probably because the new points
   // are too far beyond or too close to the non-shared sides. Fiddling with
   // the size of the `alittlebit' parameter may help.

   FindAllTriangles() {
      if (this.fAllTri)
         return;

      this.fAllTri = true;

      let xcntr, ycntr, xm, ym, xx, yy,
          sx, sy, nx, ny, mx, my, mdotn, nn, a,
          t1, t2, pa, na, ma, pb, nb, mb, p1 = 0, p2 = 0, m, n, p3 = 0;
      const s = [false, false, false],
            alittlebit = 0.0001;

      this.Initialize();

      // start with a point that is guaranteed to be inside the hull (the
      // centre of the hull). The starting point is shifted "a little bit"
      // otherwise, in case of triangles aligned on a regular grid, we may
      // found none of them.
      xcntr = 0;
      ycntr = 0;
      for (n = 1; n <= this.fNhull; n++) {
         xcntr += this.fXN[this.fHullPoints[n - 1]];
         ycntr += this.fYN[this.fHullPoints[n - 1]];
      }
      xcntr = xcntr / this.fNhull + alittlebit;
      ycntr = ycntr / this.fNhull + alittlebit;
      // and calculate it's triangle
      this.Interpolate(xcntr, ycntr);

      // loop over all Delaunay triangles (including those constantly being
      // produced within the loop) and check to see if their 3 sides also
      // correspond to the sides of other Delaunay triangles, i.e. that they
      // have all their neighbors.
      t1 = 1;
      while (t1 <= this.fNdt) {
         // get the three points that make up this triangle
         pa = this.fPTried[t1 - 1];
         na = this.fNTried[t1 - 1];
         ma = this.fMTried[t1 - 1];

         // produce three integers which will represent the three sides
         s[0] = false;
         s[1] = false;
         s[2] = false;
         // loop over all other Delaunay triangles
         for (t2 = 1; t2 <= this.fNdt; t2++) {
            if (t2 !== t1) {
               // get the points that make up this triangle
               pb = this.fPTried[t2 - 1];
               nb = this.fNTried[t2 - 1];
               mb = this.fMTried[t2 - 1];
               // do triangles t1 and t2 share a side?
               if ((pa === pb && na === nb) || (pa === pb && na === mb) || (pa === nb && na === mb)) {
                  // they share side 1
                  s[0] = true;
               } else if ((pa === pb && ma === nb) || (pa === pb && ma === mb) || (pa === nb && ma === mb)) {
                  // they share side 2
                  s[1] = true;
               } else if ((na === pb && ma === nb) || (na === pb && ma === mb) || (na === nb && ma === mb)) {
                  // they share side 3
                  s[2] = true;
               }
            }
            // if t1 shares all its sides with other Delaunay triangles then
            // forget about it
            if (s[0] && s[1] && s[2])
               continue;
         }
         // Looks like t1 is missing a neighbor on at least one side.
         // For each side, take a point a little bit beyond it and calculate
         // the Delaunay triangle for that point, this should be the triangle
         // which shares the side.
         for (m = 1; m <= 3; m++) {
            if (!s[m - 1]) {
               // get the two points that make up this side
               if (m === 1) {
                  p1 = pa;
                  p2 = na;
                  p3 = ma;
               } else if (m === 2) {
                  p1 = pa;
                  p2 = ma;
                  p3 = na;
               } else if (m === 3) {
                  p1 = na;
                  p2 = ma;
                  p3 = pa;
               }
               // get the coordinates of the centre of this side
               xm = (this.fXN[p1] + this.fXN[p2]) / 2.0;
               ym = (this.fYN[p1] + this.fYN[p2]) / 2.0;
               // we want to add a little to these coordinates to get a point just
               // outside the triangle; (sx,sy) will be the vector that represents
               // the side
               sx = this.fXN[p1] - this.fXN[p2];
               sy = this.fYN[p1] - this.fYN[p2];
               // (nx,ny) will be the normal to the side, but don't know if it's
               // pointing in or out yet
               nx = sy;
               ny = -sx;
               nn = Math.sqrt(nx * nx + ny * ny);
               nx /= nn;
               ny /= nn;
               mx = this.fXN[p3] - xm;
               my = this.fYN[p3] - ym;
               mdotn = mx * nx + my * ny;
               if (mdotn > 0) {
                  // (nx,ny) is pointing in, we want it pointing out
                  nx = -nx;
                  ny = -ny;
               }
               // increase/decrease xm and ym a little to produce a point
               // just outside the triangle (ensuring that the amount added will
               // be large enough such that it won't be lost in rounding errors)
               a = Math.abs(Math.max(alittlebit * xm, alittlebit * ym));
               xx = xm + nx * a;
               yy = ym + ny * a;
               // try and find a new Delaunay triangle for this point
               this.Interpolate(xx, yy);

               // this side of t1 should now, hopefully, if it's not part of the
               // hull, be shared with a new Delaunay triangle just calculated by Interpolate
            }
         }
         t1++;
      }
   }

   // Finds those points which make up the convex hull of the set. If the xy
   // plane were a sheet of wood, and the points were nails hammered into it
   // at the respective coordinates, then if an elastic band were stretched
   // over all the nails it would form the shape of the convex hull. Those
   // nails in contact with it are the points that make up the hull.

   FindHull() {
      if (!this.fHullPoints)
         this.fHullPoints = new Array(this.fNpoints);

      let nhull_tmp = 0;
      for (let n = 1; n <= this.fNpoints; n++) {
         // if the point is not inside the hull of the set of all points
         // bar it, then it is part of the hull of the set of all points
         // including it
         const is_in = this.InHull(n, n);
         if (!is_in) {
            // cannot increment fNhull directly - InHull needs to know that
            // the hull has not yet been completely found
            nhull_tmp++;
            this.fHullPoints[nhull_tmp - 1] = n;
         }
      }
      this.fNhull = nhull_tmp;
   }


   // Is point e inside the hull defined by all points apart from x ?

   InHull(e, x) {
      let n1, n2, n, m, ntry,
          lastdphi, dd1, dd2, dx1, dx2, dx3, dy1, dy2, dy3,
          u, v, vNv1, vNv2, phi1, phi2, dphi,
          deTinhull = false;

      const xx = this.fXN[e],
            yy = this.fYN[e];

      if (this.fNhull > 0) {
         //  The hull has been found - no need to use any points other than
         //  those that make up the hull
         ntry = this.fNhull;
      } else {
         //  The hull has not yet been found, will have to try every point
         ntry = this.fNpoints;
      }

      //  n1 and n2 will represent the two points most separated by angle
      //  from point e. Initially the angle between them will be <180 degrees.
      //  But subsequent points will increase the n1-e-n2 angle. If it
      //  increases above 180 degrees then point e must be surrounded by
      //  points - it is not part of the hull.
      n1 = 1;
      n2 = 2;
      if (n1 === x) {
         n1 = n2;
         n2++;
      } else if (n2 === x)
         n2++;


      //  Get the angle n1-e-n2 and set it to lastdphi
      dx1 = xx - this.fXN[n1];
      dy1 = yy - this.fYN[n1];
      dx2 = xx - this.fXN[n2];
      dy2 = yy - this.fYN[n2];
      phi1 = Math.atan2(dy1, dx1);
      phi2 = Math.atan2(dy2, dx2);
      dphi = (phi1 - phi2) - (Math.floor((phi1 - phi2) / (Math.PI * 2)) * Math.PI * 2);
      if (dphi < 0)
         dphi += Math.PI * 2;
      lastdphi = dphi;
      for (n = 1; n <= ntry; n++) {
         if (this.fNhull > 0) {
            // Try hull point n
            m = this.fHullPoints[n - 1];
         } else
            m = n;

         if ((m !== n1) && (m !== n2) && (m !== x)) {
            // Can the vector e->m be represented as a sum with positive
            // coefficients of vectors e->n1 and e->n2?
            dx1 = xx - this.fXN[n1];
            dy1 = yy - this.fYN[n1];
            dx2 = xx - this.fXN[n2];
            dy2 = yy - this.fYN[n2];
            dx3 = xx - this.fXN[m];
            dy3 = yy - this.fYN[m];

            dd1 = (dx2 * dy1 - dx1 * dy2);
            dd2 = (dx1 * dy2 - dx2 * dy1);

            if (dd1 * dd2) {
               u = (dx2 * dy3 - dx3 * dy2) / dd1;
               v = (dx1 * dy3 - dx3 * dy1) / dd2;
               if ((u < 0) || (v < 0)) {
                  // No, it cannot - point m does not lie in-between n1 and n2 as
                  // viewed from e. Replace either n1 or n2 to increase the
                  // n1-e-n2 angle. The one to replace is the one which makes the
                  // smallest angle with e->m
                  vNv1 = (dx1 * dx3 + dy1 * dy3) / Math.sqrt(dx1 * dx1 + dy1 * dy1);
                  vNv2 = (dx2 * dx3 + dy2 * dy3) / Math.sqrt(dx2 * dx2 + dy2 * dy2);
                  if (vNv1 > vNv2) {
                     n1 = m;
                     phi1 = Math.atan2(dy3, dx3);
                     phi2 = Math.atan2(dy2, dx2);
                  } else {
                     n2 = m;
                     phi1 = Math.atan2(dy1, dx1);
                     phi2 = Math.atan2(dy3, dx3);
                  }
                  dphi = (phi1 - phi2) - (Math.floor((phi1 - phi2) / (Math.PI * 2)) * Math.PI * 2);
                  if (dphi < 0)
                     dphi += Math.PI * 2;
                  if ((dphi - Math.PI) * (lastdphi - Math.PI) < 0) {
                     // The addition of point m means the angle n1-e-n2 has risen
                     // above 180 degrees, the point is in the hull.
                     deTinhull = true;
                     return deTinhull;
                  }
                  lastdphi = dphi;
               }
            }
         }
      }
      // Point e is not surrounded by points - it is not in the hull.
      return deTinhull;
   }

   // Finds the z-value at point e given that it lies
   // on the plane defined by t1,t2,t3

   InterpolateOnPlane(TI1, TI2, TI3, e) {
      let swap, t1 = TI1, t2 = TI2, t3 = TI3;

      // order the vertices
      do {
         swap = false;
         if (t2 > t1) {
            [t1, t2] = [t2, t1];
            swap = true;
         }
         if (t3 > t2) {
            [t2, t3] = [t3, t2];
            swap = true;
         }
      } while (swap);

      const x1 = this.fXN[t1],
            x2 = this.fXN[t2],
            x3 = this.fXN[t3],
            y1 = this.fYN[t1],
            y2 = this.fYN[t2],
            y3 = this.fYN[t3],
            f1 = this.fZ[t1 - 1],
            f2 = this.fZ[t2 - 1],
            f3 = this.fZ[t3 - 1],
            u = (f1 * (y2 - y3) + f2 * (y3 - y1) + f3 * (y1 - y2)) / (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)),
            v = (f1 * (x2 - x3) + f2 * (x3 - x1) + f3 * (x1 - x2)) / (y1 * (x2 - x3) + y2 * (x3 - x1) + y3 * (x1 - x2)),
            w = f1 - u * x1 - v * y1;

      return u * this.fXN[e] + v * this.fYN[e] + w;
   }

   // Finds the Delaunay triangle that the point (xi,yi) sits in (if any) and
   // calculate a z-value for it by linearly interpolating the z-values that
   // make up that triangle.

   Interpolate(xx, yy) {
      let thevalue,
          it, ntris_tried, p, n, m,
          i, j, k, l, z, f, d, o1, o2, a, b, t1, t2, t3,
          /* eslint-disable-next-line no-useless-assignment */
          ndegen = 0, degen = 0, fdegen = 0, o1degen = 0, o2degen = 0,
          vxN, vyN,
          d1, d2, d3, c1, c2, dko1, dko2, dfo1,
          dfo2, sin_sum, cfo1k, co2o1k, co2o1f,
          dx1, dx2, dx3, dy1, dy2, dy3, u, v;
      const dxz = [0, 0, 0], dyz = [0, 0, 0];

      // initialize the Delaunay algorithm if needed
      this.Initialize();

      // create vectors needed for sorting
      if (!this.fOrder) {
         this.fOrder = new Array(this.fNpoints);
         this.fDist = new Array(this.fNpoints);
      }

      // the input point will be point zero.
      this.fXN[0] = xx;
      this.fYN[0] = yy;

      // set the output value to the default value for now
      thevalue = this.fZout;

      // some counting
      ntris_tried = 0;

      // no point in proceeding if xx or yy are silly
      if ((xx > this.fXNmax) || (xx < this.fXNmin) || (yy > this.fYNmax) || (yy < this.fYNmin))
         return thevalue;

      // check existing Delaunay triangles for a good one
      for (it = 1; it <= this.fNdt; it++) {
         p = this.fPTried[it - 1];
         n = this.fNTried[it - 1];
         m = this.fMTried[it - 1];
         // p, n and m form a previously found Delaunay triangle, does it
         // enclose the point?
         if (this.Enclose(p, n, m, 0)) {
            // yes, we have the triangle
            thevalue = this.InterpolateOnPlane(p, n, m, 0);
            return thevalue;
         }
      }

      // is this point inside the convex hull?
      const shouldbein = this.InHull(0, -1);
      if (!shouldbein)
         return thevalue;

      // it must be in a Delaunay triangle - find it...

      // order mass points by distance in mass plane from desired point
      for (it = 1; it <= this.fNpoints; it++) {
         vxN = this.fXN[it];
         vyN = this.fYN[it];
         this.fDist[it - 1] = Math.sqrt((xx - vxN) * (xx - vxN) + (yy - vyN) * (yy - vyN));
      }

      // sort array 'fDist' to find closest points
      TMath_Sort(this.fNpoints, this.fDist, this.fOrder /* , false */);
      for (it = 0; it < this.fNpoints; it++)
         this.fOrder[it]++;

      // loop over triplets of close points to try to find a triangle that
      // encloses the point.
      for (k = 3; k <= this.fNpoints; k++) {
         m = this.fOrder[k - 1];
         for (j = 2; j <= k - 1; j++) {
            n = this.fOrder[j - 1];
            for (i = 1; i <= j - 1; i++) {
               let skip_this_triangle = false; // used instead of goto L90
               p = this.fOrder[i - 1];
               if (ntris_tried > this.fMaxIter) {
                  // perhaps this point isn't in the hull after all
                  /* Warning("Interpolate",
                             "Abandoning the effort to find a Delaunay triangle (and thus interpolated z-value) for point %g %g"
                              ,xx,yy); */
                  return thevalue;
               }
               ntris_tried++;
               // check the points aren't colinear
               d1 = Math.sqrt((this.fXN[p] - this.fXN[n]) ** 2 + (this.fYN[p] - this.fYN[n]) ** 2);
               d2 = Math.sqrt((this.fXN[p] - this.fXN[m]) ** 2 + (this.fYN[p] - this.fYN[m]) ** 2);
               d3 = Math.sqrt((this.fXN[n] - this.fXN[m]) ** 2 + (this.fYN[n] - this.fYN[m]) ** 2);
               if ((d1 + d2 <= d3) || (d1 + d3 <= d2) || (d2 + d3 <= d1))
                  continue;

               // does the triangle enclose the point?
               if (!this.Enclose(p, n, m, 0))
                  continue;

               // is it a Delaunay triangle? (ie. are there any other points
               // inside the circle that is defined by its vertices?)

               // test the triangle for Delaunay'ness

               // loop over all other points testing each to see if it's
               // inside the triangle's circle
               ndegen = 0;
               for (z = 1; z <= this.fNpoints; z++) {
                  if ((z === p) || (z === n) || (z === m))
                     continue; // goto L50;
                  // An easy first check is to see if point z is inside the triangle
                  // (if it's in the triangle it's also in the circle)

                  // point z cannot be inside the triangle if it's further from (xx,yy)
                  // than the furthest pointing making up the triangle - test this
                  for (l = 1; l <= this.fNpoints; l++) {
                     if (this.fOrder[l - 1] === z) {
                        if ((l < i) || (l < j) || (l < k)) {
                           // point z is nearer to (xx,yy) than m, n or p - it could be in the
                           // triangle so call enclose to find out

                           // if it is inside the triangle this can't be a Delaunay triangle
                           if (this.Enclose(p, n, m, z)) {
                              skip_this_triangle = true;
                              break;
                           } // goto L90;
                        } else {
                           // there's no way it could be in the triangle so there's no point
                           // calling enclose
                           break; // goto L1;
                        }
                     }
                  }

                  if (skip_this_triangle)
                     break;

                  // is point z colinear with any pair of the triangle points?
                  // L1:
                  if (((this.fXN[p] - this.fXN[z]) * (this.fYN[p] - this.fYN[n])) === ((this.fYN[p] - this.fYN[z]) * (this.fXN[p] - this.fXN[n]))) {
                     // z is colinear with p and n
                     a = p;
                     b = n;
                  } else if (((this.fXN[p] - this.fXN[z]) * (this.fYN[p] - this.fYN[m])) === ((this.fYN[p] - this.fYN[z]) * (this.fXN[p] - this.fXN[m]))) {
                     // z is colinear with p and m
                     a = p;
                     b = m;
                  } else if (((this.fXN[n] - this.fXN[z]) * (this.fYN[n] - this.fYN[m])) === ((this.fYN[n] - this.fYN[z]) * (this.fXN[n] - this.fXN[m]))) {
                     // z is colinear with n and m
                     a = n;
                     b = m;
                  } else {
                     a = 0;
                     b = 0;
                  }
                  if (a) {
                     // point z is colinear with 2 of the triangle points, if it lies
                     // between them it's in the circle otherwise it's outside
                     if (this.fXN[a] !== this.fXN[b]) {
                        if (((this.fXN[z] - this.fXN[a]) * (this.fXN[z] - this.fXN[b])) < 0) {
                           skip_this_triangle = true;
                           break;
                           // goto L90;
                        } else if (((this.fXN[z] - this.fXN[a]) * (this.fXN[z] - this.fXN[b])) === 0) {
                           // At least two points are sitting on top of each other, we will
                           // treat these as one and not consider this a 'multiple points lying
                           // on a common circle' situation. It is a sign something could be
                           // wrong though, especially if the two coincident points have
                           // different fZ's. If they don't then this is harmless.
                           console.warn(`Interpolate Two of these three points are coincident ${a} ${b} ${z}`);
                        }
                     } else if (((this.fYN[z] - this.fYN[a]) * (this.fYN[z] - this.fYN[b])) < 0) {
                        skip_this_triangle = true;
                        break;
                        // goto L90;
                     } else if (((this.fYN[z] - this.fYN[a]) * (this.fYN[z] - this.fYN[b])) === 0) {
                        // At least two points are sitting on top of each other - see above.
                        console.warn(`Interpolate Two of these three points are coincident ${a} ${b} ${z}`);
                     }
                     // point is outside the circle, move to next point
                     continue; // goto L50;
                  }

                  if (skip_this_triangle)
                     break;

                  /* Error("Interpolate", "Should not get to here"); */
                  // may as well soldier on
                  // SL: initialize before try to find better values
                  f = m;
                  o1 = p;
                  o2 = n;

                  // if point z were to look at the triangle, which point would it see
                  // lying between the other two? (we're going to form a quadrilateral
                  // from the points, and then demand certain properties of that
                  // quadrilateral)
                  dxz[0] = this.fXN[p] - this.fXN[z];
                  dyz[0] = this.fYN[p] - this.fYN[z];
                  dxz[1] = this.fXN[n] - this.fXN[z];
                  dyz[1] = this.fYN[n] - this.fYN[z];
                  dxz[2] = this.fXN[m] - this.fXN[z];
                  dyz[2] = this.fYN[m] - this.fYN[z];
                  for (l = 1; l <= 3; l++) {
                     dx1 = dxz[l - 1];
                     dx2 = dxz[l % 3];
                     dx3 = dxz[(l + 1) % 3];
                     dy1 = dyz[l - 1];
                     dy2 = dyz[l % 3];
                     dy3 = dyz[(l + 1) % 3];

                     // u et v are used only to know their sign. The previous
                     // code computed them with a division which was long and
                     // might be a division by 0. It is now replaced by a
                     // multiplication.
                     u = (dy3 * dx2 - dx3 * dy2) * (dy1 * dx2 - dx1 * dy2);
                     v = (dy3 * dx1 - dx3 * dy1) * (dy2 * dx1 - dx2 * dy1);

                     if ((u >= 0) && (v >= 0)) {
                        // vector (dx3,dy3) is expressible as a sum of the other two vectors
                        // with positive coefficients -> i.e. it lies between the other two vectors
                        if (l === 1) {
                           f = m;
                           o1 = p;
                           o2 = n;
                        } else if (l === 2) {
                           f = p;
                           o1 = n;
                           o2 = m;
                        } else {
                           f = n;
                           o1 = m;
                           o2 = p;
                        }
                        break; // goto L2;
                     }
                  }
                  // L2:
                  // this is not a valid quadrilateral if the diagonals don't cross,
                  // check that points f and z lie on opposite side of the line o1-o2,
                  // this is true if the angle f-o1-z is greater than o2-o1-z and o2-o1-f
                  cfo1k = ((this.fXN[f] - this.fXN[o1]) * (this.fXN[z] - this.fXN[o1]) + (this.fYN[f] - this.fYN[o1]) * (this.fYN[z] - this.fYN[o1])) /
                           Math.sqrt(((this.fXN[f] - this.fXN[o1]) * (this.fXN[f] - this.fXN[o1]) + (this.fYN[f] - this.fYN[o1]) * (this.fYN[f] - this.fYN[o1])) *
                           ((this.fXN[z] - this.fXN[o1]) * (this.fXN[z] - this.fXN[o1]) + (this.fYN[z] - this.fYN[o1]) * (this.fYN[z] - this.fYN[o1])));
                  co2o1k = ((this.fXN[o2] - this.fXN[o1]) * (this.fXN[z] - this.fXN[o1]) + (this.fYN[o2] - this.fYN[o1]) * (this.fYN[z] - this.fYN[o1])) /
                           Math.sqrt(((this.fXN[o2] - this.fXN[o1]) * (this.fXN[o2] - this.fXN[o1]) + (this.fYN[o2] - this.fYN[o1]) * (this.fYN[o2] - this.fYN[o1])) *
                           ((this.fXN[z] - this.fXN[o1]) * (this.fXN[z] - this.fXN[o1]) + (this.fYN[z] - this.fYN[o1]) * (this.fYN[z] - this.fYN[o1])));
                  co2o1f = ((this.fXN[o2] - this.fXN[o1]) * (this.fXN[f] - this.fXN[o1]) + (this.fYN[o2] - this.fYN[o1]) * (this.fYN[f] - this.fYN[o1])) /
                           Math.sqrt(((this.fXN[o2] - this.fXN[o1]) * (this.fXN[o2] - this.fXN[o1]) + (this.fYN[o2] - this.fYN[o1]) * (this.fYN[o2] - this.fYN[o1])) *
                           ((this.fXN[f] - this.fXN[o1]) * (this.fXN[f] - this.fXN[o1]) + (this.fYN[f] - this.fYN[o1]) * (this.fYN[f] - this.fYN[o1])));
                  if ((cfo1k > co2o1k) || (cfo1k > co2o1f)) {
                     // not a valid quadrilateral - point z is definitely outside the circle
                     continue; // goto L50;
                  }
                  // calculate the 2 internal angles of the quadrangle formed by joining
                  // points z and f to points o1 and o2, at z and f. If they sum to less
                  // than 180 degrees then z lies outside the circle
                  dko1 = Math.sqrt((this.fXN[z] - this.fXN[o1]) * (this.fXN[z] - this.fXN[o1]) + (this.fYN[z] - this.fYN[o1]) * (this.fYN[z] - this.fYN[o1]));
                  dko2 = Math.sqrt((this.fXN[z] - this.fXN[o2]) * (this.fXN[z] - this.fXN[o2]) + (this.fYN[z] - this.fYN[o2]) * (this.fYN[z] - this.fYN[o2]));
                  dfo1 = Math.sqrt((this.fXN[f] - this.fXN[o1]) * (this.fXN[f] - this.fXN[o1]) + (this.fYN[f] - this.fYN[o1]) * (this.fYN[f] - this.fYN[o1]));
                  dfo2 = Math.sqrt((this.fXN[f] - this.fXN[o2]) * (this.fXN[f] - this.fXN[o2]) + (this.fYN[f] - this.fYN[o2]) * (this.fYN[f] - this.fYN[o2]));
                  c1 = ((this.fXN[z] - this.fXN[o1]) * (this.fXN[z] - this.fXN[o2]) + (this.fYN[z] - this.fYN[o1]) * (this.fYN[z] - this.fYN[o2])) / dko1 / dko2;
                  c2 = ((this.fXN[f] - this.fXN[o1]) * (this.fXN[f] - this.fXN[o2]) + (this.fYN[f] - this.fYN[o1]) * (this.fYN[f] - this.fYN[o2])) / dfo1 / dfo2;
                  sin_sum = c1 * Math.sqrt(1 - c2 * c2) + c2 * Math.sqrt(1 - c1 * c1);

                  // sin_sum doesn't always come out as zero when it should do.
                  if (sin_sum < -1e-6) {
                     // z is inside the circle, this is not a Delaunay triangle
                     skip_this_triangle = true;
                     break;
                     // goto L90;
                  } else if (Math.abs(sin_sum) <= 1.e-6) {
                     // point z lies on the circumference of the circle (within rounding errors)
                     // defined by the triangle, so there is potential for degeneracy in the
                     // triangle set (Delaunay triangulation does not give a unique way to split
                     // a polygon whose points lie on a circle into constituent triangles). Make
                     // a note of the additional point number.
                     ndegen++;
                     degen = z;
                     fdegen = f;
                     o1degen = o1;
                     o2degen = o2;
                  }

                  // L50: continue;
               } // end of for ( z = 1 ...) loop

               if (skip_this_triangle)
                  continue;

               // This is a good triangle
               if (ndegen > 0) {
                  // but is degenerate with at least one other,
                  // haven't figured out what to do if more than 4 points are involved
                  /* if (ndegen > 1) {
                     Error("Interpolate",
                            "More than 4 points lying on a circle. No decision making process formulated for triangulating this region in a non-arbitrary way %d %d %d %d",
                             p,n,m,degen);
                     return thevalue;
                  } */

                  // we have a quadrilateral which can be split down either diagonal
                  // (d<->f or o1<->o2) to form valid Delaunay triangles. Choose diagonal
                  // with highest average z-value. Whichever we choose we will have
                  // verified two triangles as good and two as bad, only note the good ones
                  d = degen;
                  f = fdegen;
                  o1 = o1degen;
                  o2 = o2degen;
                  if ((this.fZ[o1 - 1] + this.fZ[o2 - 1]) > (this.fZ[d - 1] + this.fZ[f - 1])) {
                     // best diagonalisation of quadrilateral is current one, we have
                     // the triangle
                     t1 = p;
                     t2 = n;
                     t3 = m;
                     // file the good triangles
                     this.FileIt(p, n, m);
                     this.FileIt(d, o1, o2);
                  } else {
                     // use other diagonal to split quadrilateral, use triangle formed by
                     // point f, the degnerate point d and whichever of o1 and o2 create
                     // an enclosing triangle
                     t1 = f;
                     t2 = d;
                     if (this.Enclose(f, d, o1, 0))
                        t3 = o1;
                     else
                        t3 = o2;

                     // file the good triangles
                     this.FileIt(f, d, o1);
                     this.FileIt(f, d, o2);
                  }
               } else {
                  // this is a Delaunay triangle, file it
                  this.FileIt(p, n, m);
                  t1 = p;
                  t2 = n;
                  t3 = m;
               }
               // do the interpolation
               thevalue = this.InterpolateOnPlane(t1, t2, t3, 0);
               return thevalue;

               // L90:      continue;
            }
         }
      }
      if (shouldbein)
         console.error(`Interpolate Point outside hull when expected inside: this point could be dodgy ${xx}  ${yy} ${ntris_tried}`);
      return thevalue;
   }

   /** @summary Defines the number of triangles tested for a Delaunay triangle
    * @desc (number of iterations) before abandoning the search */
   SetMaxIter(n = 100000) {
      this.fAllTri = false;
      this.fMaxIter = n;
   }

   /** @summary Sets the histogram bin height for points lying outside the convex hull ie:
     * @desc the bins in the margin. */
   SetMarginBinsContent(z) {
      this.fZout = z;
   }

   /** @summary Returns the X and Y graphs building a contour.
    * @desc A contour level may consist in several parts not connected to each other.
    * This function finds them and returns them in a graphs' list. */
   GetContourList(contour) {
      if (!this.fNdt)
         return null;

      let graph,     // current graph
          // Find all the segments making the contour
          r21, r20, r10, p0, p1, p2, x0, y0, z0, x1, y1, z1, x2, y2, z2,
          it, i0, i1, i2, nbSeg = 0,
          // Allocate space to store the segments. They cannot be more than the
          // number of triangles.
          xs0c, ys0c, xs1c, ys1c;

      const t = [0, 0, 0],
            xs0 = new Array(this.fNdt).fill(0),
            ys0 = new Array(this.fNdt).fill(0),
            xs1 = new Array(this.fNdt).fill(0),
            ys1 = new Array(this.fNdt).fill(0);

      // Loop over all the triangles in order to find all the line segments
      // making the contour.

      // old implementation
      for (it = 0; it < this.fNdt; it++) {
         t[0] = this.fPTried[it];
         t[1] = this.fNTried[it];
         t[2] = this.fMTried[it];
         p0 = t[0] - 1;
         p1 = t[1] - 1;
         p2 = t[2] - 1;
         x0 = this.fX[p0];
         x2 = this.fX[p0];
         y0 = this.fY[p0];
         y2 = this.fY[p0];
         z0 = this.fZ[p0];
         z2 = this.fZ[p0];

         // Order along Z axis the points (xi,yi,zi) where "i" belongs to {0,1,2}
         // After this z0 < z1 < z2
         /* eslint-disable-next-line no-useless-assignment */
         i0 = i1 = i2 = 0;
         if (this.fZ[p1] <= z0) {
            z0 = this.fZ[p1];
            x0 = this.fX[p1];
            y0 = this.fY[p1];
            i0 = 1;
         }
         if (this.fZ[p1] > z2) {
            z2 = this.fZ[p1];
            x2 = this.fX[p1];
            y2 = this.fY[p1];
            i2 = 1;
         }
         if (this.fZ[p2] <= z0) {
            z0 = this.fZ[p2];
            x0 = this.fX[p2];
            y0 = this.fY[p2];
            i0 = 2;
         }
         if (this.fZ[p2] > z2) {
            z2 = this.fZ[p2];
            x2 = this.fX[p2];
            y2 = this.fY[p2];
            i2 = 2;
         }
         if (i0 === 0 && i2 === 0) {
            console.error('GetContourList: wrong vertices ordering');
            return null;
         }

         i1 = 3 - i2 - i0;

         x1 = this.fX[t[i1] - 1];
         y1 = this.fY[t[i1] - 1];
         z1 = this.fZ[t[i1] - 1];

         if (contour >= z0 && contour <= z2) {
            r20 = (contour - z0) / (z2 - z0);
            xs0c = r20 * (x2 - x0) + x0;
            ys0c = r20 * (y2 - y0) + y0;
            if (contour >= z1 && contour <= z2) {
               r21 = (contour - z1) / (z2 - z1);
               xs1c = r21 * (x2 - x1) + x1;
               ys1c = r21 * (y2 - y1) + y1;
            } else {
               r10 = (contour - z0) / (z1 - z0);
               xs1c = r10 * (x1 - x0) + x0;
               ys1c = r10 * (y1 - y0) + y0;
            }
            // do not take the segments equal to a point
            if (xs0c !== xs1c || ys0c !== ys1c) {
               nbSeg++;
               xs0[nbSeg - 1] = xs0c;
               ys0[nbSeg - 1] = ys0c;
               xs1[nbSeg - 1] = xs1c;
               ys1[nbSeg - 1] = ys1c;
            }
         }
      }

      const list = [], // list holding all the graphs
            segUsed = new Array(this.fNdt).fill(false);

      // Find all the graphs making the contour. There is two kind of graphs,
      // either they are "opened" or they are "closed"

      // Find the opened graphs
      let xc = 0, yc = 0, xnc = 0, ync = 0,
          findNew, s0, s1, is, js;

      for (is = 0; is < nbSeg; is++) {
         if (segUsed[is])
            continue;
         s0 = s1 = false;

         // Find to which segment is is connected. It can be connected
         // via 0, 1 or 2 vertices.
         for (js = 0; js < nbSeg; js++) {
            if (is === js)
               continue;
            if (xs0[is] === xs0[js] && ys0[is] === ys0[js])
               s0 = true;
            if (xs0[is] === xs1[js] && ys0[is] === ys1[js])
               s0 = true;
            if (xs1[is] === xs0[js] && ys1[is] === ys0[js])
               s1 = true;
            if (xs1[is] === xs1[js] && ys1[is] === ys1[js])
               s1 = true;
         }

         // Segment is is alone, not connected. It is stored in the
         // list and the next segment is examined.
         if (!s0 && !s1) {
            graph = [];
            graph.push(xs0[is], ys0[is]);
            graph.push(xs1[is], ys1[is]);
            segUsed[is] = true;
            list.push(graph);
            continue;
         }

         // Segment is is connected via 1 vertex only and can be considered
         // as the starting point of an opened contour.
         if (!s0 || !s1) {
            // Find all the segments connected to segment is
            graph = [];
            if (s0) {
               xc = xs0[is];
               yc = ys0[is];
               xnc = xs1[is];
               ync = ys1[is];
            }
            if (s1) {
               xc = xs1[is];
               yc = ys1[is];
               xnc = xs0[is];
               ync = ys0[is];
            }
            graph.push(xnc, ync);
            segUsed[is] = true;
            js = 0;

            while (true) {
               findNew = false;
               while (js < nbSeg && segUsed[js])
                  js++;

               if (xc === xs0[js] && yc === ys0[js]) {
                  xc = xs1[js];
                  yc = ys1[js];
                  findNew = true;
               } else if (xc === xs1[js] && yc === ys1[js]) {
                  xc = xs0[js];
                  yc = ys0[js];
                  findNew = true;
               }
               if (findNew) {
                  segUsed[js] = true;
                  graph.push(xc, yc);
                  js = 0;
               } else if (++js >= nbSeg)
                  break;
            }
            list.push(graph);
         }
      }


      // Find the closed graphs. At this point all the remaining graphs
      // are closed. Any segment can be used to start the search.
      for (is = 0; is < nbSeg; is++) {
         if (segUsed[is])
            continue;

         // Find all the segments connected to segment is
         graph = [];
         segUsed[is] = true;
         xc = xs0[is];
         yc = ys0[is];
         js = 0;
         graph.push(xc, yc);
         while (true) {
            while (js < nbSeg && segUsed[js])
               js++;
            findNew = false;
            if (xc === xs0[js] && yc === ys0[js]) {
               xc = xs1[js];
               yc = ys1[js];
               findNew = true;
            } else if (xc === xs1[js] && yc === ys1[js]) {
               xc = xs0[js];
               yc = ys0[js];
               findNew = true;
            }
            if (findNew) {
               segUsed[js] = true;
               graph.push(xc, yc);
               js = 0;
            } else if (++js >= nbSeg)
               break;
         }
         graph.push(xs0[is], ys0[is]);
         list.push(graph);
      }

      return list;
   }

} // class TGraphDelaunay

/** @summary Function handles tooltips in the mesh */
function _graph2DTooltip(intersect) {
   let indx = Math.floor(intersect.index / this.nvertex);
   if ((indx < 0) || (indx >= this.index.length))
      return null;
   const sqr = v => v * v;

   indx = this.index[indx];

   const fp = this.fp, gr = this.graph;
   let grx = fp.grx(gr.fX[indx]),
       gry = fp.gry(gr.fY[indx]),
       grz = fp.grz(gr.fZ[indx]);

   if (this.check_next && indx + 1 < gr.fX.length) {
      const d = intersect.point,
            grx1 = fp.grx(gr.fX[indx + 1]),
            gry1 = fp.gry(gr.fY[indx + 1]),
            grz1 = fp.grz(gr.fZ[indx + 1]);
      if (sqr(d.x - grx1) + sqr(d.y - gry1) + sqr(d.z - grz1) < sqr(d.x - grx) + sqr(d.y - gry) + sqr(d.z - grz)) {
         grx = grx1;
         gry = gry1;
         grz = grz1;
         indx++;
      }
   }

   return {
      x1: grx - this.scale0,
      x2: grx + this.scale0,
      y1: gry - this.scale0,
      y2: gry + this.scale0,
      z1: grz - this.scale0,
      z2: grz + this.scale0,
      color: this.tip_color,
      lines: [this.tip_name,
              'pnt: ' + indx,
              'x: ' + fp.axisAsText('x', gr.fX[indx]),
              'y: ' + fp.axisAsText('y', gr.fY[indx]),
              'z: ' + fp.axisAsText('z', gr.fZ[indx])]
   };
}


/**
 * @summary Painter for TGraph2D classes
 * @private
 */

class TGraph2DPainter extends ObjectPainter {

   #redraw_hist; // painter to redraw histogram
   #delaunay; // used delaunay instance

   /** @summary Decode options string  */
   decodeOptions(opt) {
      const d = new DrawOptions(opt),
            gr2d = this.getObject();

      if (d.check('FILL_', 'color') && gr2d)
         gr2d.fFillColor = d.color;

      if (d.check('LINE_', 'color') && gr2d)
         gr2d.fLineColor = d.color;

      d.check('SAME');

      let Triangles = 0, Contour = 0;

      if (d.check('CONT5'))
         Contour = 15;
      else if (d.check('TRI1'))
         Triangles = 11; // wire-frame and colors
      else if (d.check('TRI2'))
         Triangles = 10; // only color triangles
      else if (d.check('TRIW'))
         Triangles = 1;
      else if (d.check('TRI'))
         Triangles = 2;

      const res = this.setOptions({
         Triangles,
         Contour,
         Line: d.check('LINE'),
         Error: d.check('ERR') && (this.matchObjectType(clTGraph2DErrors) || this.matchObjectType(clTGraph2DAsymmErrors))
      });

      if (d.check('P0COL'))
         res.Color = res.Circles = res.Markers = true;
      else {
         res.Color = d.check('COL');
         res.Circles = d.check('P0');
         res.Markers = d.check('P');
      }

      if (!res.Markers)
         res.Color = false;

      if (res.Color || res.Triangles >= 10 || res.Contour)
         res.Zscale = d.check('Z');

      res.isAny = function() {
         return this.Markers || this.Error || this.Circles || this.Line || this.Triangles || res.Contour;
      };

      if (res.Contour)
         res.Axis = '';
      else if (res.isAny()) {
         res.Axis = 'lego2';
         if (res.Zscale)
            res.Axis += 'z';
      } else
         res.Axis = opt;

      this.storeDrawOpt(opt);
   }

   /** @summary Create histogram for axes drawing */
   createHistogram() {
      const gr = this.getObject(),
            o = this.getOptions(),
            asymm = this.matchObjectType(clTGraph2DAsymmErrors);
      let xmin = gr.fX[0], xmax = xmin,
          ymin = gr.fY[0], ymax = ymin,
          zmin = gr.fZ[0], zmax = zmin;

      for (let p = 0; p < gr.fNpoints; ++p) {
         const x = gr.fX[p], y = gr.fY[p], z = gr.fZ[p];

         if (o.Error) {
            xmin = Math.min(xmin, x - (asymm ? gr.fEXlow[p] : gr.fEX[p]));
            xmax = Math.max(xmax, x + (asymm ? gr.fEXhigh[p] : gr.fEX[p]));
            ymin = Math.min(ymin, y - (asymm ? gr.fEYlow[p] : gr.fEY[p]));
            ymax = Math.max(ymax, y + (asymm ? gr.fEYhigh[p] : gr.fEY[p]));
            zmin = Math.min(zmin, z - (asymm ? gr.fEZlow[p] : gr.fEZ[p]));
            zmax = Math.max(zmax, z + (asymm ? gr.fEZhigh[p] : gr.fEZ[p]));
         } else {
            xmin = Math.min(xmin, x);
            xmax = Math.max(xmax, x);
            ymin = Math.min(ymin, y);
            ymax = Math.max(ymax, y);
            zmin = Math.min(zmin, z);
            zmax = Math.max(zmax, z);
         }
      }

      function calc_delta(min, max, margin) {
         if (min < max)
            return margin * (max - min);
         return Math.abs(min) < 1e5 ? 0.02 : 0.02 * Math.abs(min);
      }
      const dx = calc_delta(xmin, xmax, gr.fMargin),
            dy = calc_delta(ymin, ymax, gr.fMargin),
            dz = calc_delta(zmin, zmax, 0);
      let uxmin = xmin - dx, uxmax = xmax + dx,
          uymin = ymin - dy, uymax = ymax + dy,
          uzmin = zmin - dz, uzmax = zmax + dz;

      if ((uxmin < 0) && (xmin >= 0))
         uxmin = xmin * 0.98;
      if ((uxmax > 0) && (xmax <= 0))
         uxmax = 0;

      if ((uymin < 0) && (ymin >= 0))
         uymin = ymin * 0.98;
      if ((uymax > 0) && (ymax <= 0))
         uymax = 0;

      if ((uzmin < 0) && (zmin >= 0))
         uzmin = zmin * 0.98;
      if ((uzmax > 0) && (zmax <= 0))
         uzmax = 0;

      const graph = this.getObject();

      if (graph.fMinimum !== kNoZoom)
         uzmin = graph.fMinimum;
      if (graph.fMaximum !== kNoZoom)
         uzmax = graph.fMaximum;

      const histo = createHistogram(clTH2D, graph.fNpx, graph.fNpy);
      histo.fName = graph.fName + '_h';
      setHistogramTitle(histo, graph.fTitle);
      histo.fXaxis.fXmin = uxmin;
      histo.fXaxis.fXmax = uxmax;
      histo.fYaxis.fXmin = uymin;
      histo.fYaxis.fXmax = uymax;
      histo.fZaxis.fXmin = uzmin;
      histo.fZaxis.fXmax = uzmax;
      histo.fMinimum = uzmin;
      histo.fMaximum = uzmax;
      histo.fBits |= kNoStats;

      if (!o.isAny()) {
         const dulaunay = this.buildDelaunay(graph);
         if (dulaunay) {
            for (let i = 0; i < graph.fNpx; ++i) {
               const xx = uxmin + (i + 0.5) / graph.fNpx * (uxmax - uxmin);
               for (let j = 0; j < graph.fNpy; ++j) {
                  const yy = uymin + (j + 0.5) / graph.fNpy * (uymax - uymin),
                        zz = dulaunay.ComputeZ(xx, yy);
                  histo.fArray[histo.getBin(i + 1, j + 1)] = zz;
               }
            }
         }
      }

      return histo;
   }

   buildDelaunay(graph) {
      if (!this.#delaunay) {
         this.#delaunay = new TGraphDelaunay(graph);
         this.#delaunay.FindAllTriangles();
         if (!this.#delaunay.fNdt)
            this.#delaunay = undefined;
      }
      return this.#delaunay;
   }

   drawTriangles(fp, graph, levels, palette) {
      const dulaunay = this.buildDelaunay(graph);
      if (!dulaunay)
         return;

      const main_grz = !fp.logz ? fp.grz : value => { return (value < fp.scale_zmin) ? -0.1 : fp.grz(value); },
            o = this.getOptions(),
            plain_mode = o.Triangles === 2,
            do_faces = (o.Triangles >= 10) || plain_mode,
            do_lines = (o.Triangles % 10 === 1) || (plain_mode && (graph.fLineColor !== graph.fFillColor)),
            triangles = new Triangles3DHandler(levels, main_grz, 0, 2 * fp.size_z3d, do_lines);

      for (triangles.loop = 0; triangles.loop < 2; ++triangles.loop) {
         triangles.createBuffers();

         for (let t = 0; t < dulaunay.fNdt; ++t) {
            const points = [dulaunay.fPTried[t], dulaunay.fNTried[t], dulaunay.fMTried[t]],
                  coord = [];
            let use_triangle = true;
            for (let i = 0; i < 3; ++i) {
               const pnt = points[i] - 1;
               coord.push(fp.grx(graph.fX[pnt]), fp.gry(graph.fY[pnt]), main_grz(graph.fZ[pnt]));

               if ((graph.fX[pnt] < fp.scale_xmin) || (graph.fX[pnt] > fp.scale_xmax) ||
                    (graph.fY[pnt] < fp.scale_ymin) || (graph.fY[pnt] > fp.scale_ymax))
                  use_triangle = false;
            }

            if (do_faces && use_triangle)
               triangles.addMainTriangle(...coord);

            if (do_lines && use_triangle) {
               triangles.addLineSegment(coord[0], coord[1], coord[2], coord[3], coord[4], coord[5]);
               triangles.addLineSegment(coord[3], coord[4], coord[5], coord[6], coord[7], coord[8]);
               triangles.addLineSegment(coord[6], coord[7], coord[8], coord[0], coord[1], coord[2]);
            }
         }
      }

      triangles.callFuncs((lvl, pos) => {
         const geometry = createLegoGeom(this.getMainPainter(), pos, null, 100, 100),
               color = plain_mode ? this.getColor(graph.fFillColor) : palette.calcColor(lvl, levels.length),
               material = new THREE.MeshBasicMaterial(getMaterialArgs(color, { side: THREE.DoubleSide, vertexColors: false })),
               mesh = new THREE.Mesh(geometry, material);

         fp.add3DMesh(mesh, this);

         mesh.painter = this; // to let use it with context menu
      }, (_isgrid, lpos) => {
         const lcolor = this.getColor(graph.fLineColor),
               material = new THREE.LineBasicMaterial({ color: new THREE.Color(lcolor), linewidth: graph.fLineWidth }),
               linemesh = createLineSegments(convertLegoBuf(this.getMainPainter(), lpos, 100, 100), material);
         fp.add3DMesh(linemesh, this);
      });
   }

   /** @summary Update TGraph2D object */
   updateObject(obj, opt) {
      if (!this.matchObjectType(obj))
         return false;

      const o = this.getOptions();

      if (opt && (opt !== o.original))
         this.decodeOptions(opt, obj);

      Object.assign(this.getObject(), obj);

      this.#delaunay = undefined; // rebuild triangles

      this.#redraw_hist = undefined;

      // if our own histogram was used as axis drawing, we need update histogram as well
      if (this.axes_draw) {
         const hist_painter = this.getMainPainter();
         hist_painter?.updateObject(this.createHistogram(), o.Axis);
         this.#redraw_hist = hist_painter;
      }

      return true;
   }

   /** @summary Redraw TGraph2D object
     * @desc Update histogram drawing if necessary
     * @return {Promise} for drawing ready */
   async redraw() {
      const promise = getPromise(this.#redraw_hist?.redraw());

      this.#redraw_hist = undefined;

      return promise.then(() => this.drawGraph2D());
   }

   async drawContour(fp, main, graph) {
      const dulaunay = this.buildDelaunay(graph);
      if (!dulaunay)
         return this;

      const cntr = main.getContour(),
            palette = main.getHistPalette(),
            levels = cntr.getLevels(),
            funcs = fp.getGrFuncs(),
            g = this.createG(true);

      this.createAttLine({ attr: graph, nocolor: true });

      for (let k = 0; k < levels.length; ++k) {
         const lst = dulaunay.GetContourList(levels[k]),
               color = cntr.getPaletteColor(palette, levels[k]);
         let path = '';
         for (let i = 0; i < lst.length; ++i) {
            const gr = lst[i], arr = [];
            for (let n = 0; n < gr.length; n += 2)
               arr.push({ grx: funcs.grx(gr[n]), gry: funcs.gry(gr[n + 1]) });
            path += buildSvgCurve(arr, { cmd: 'M', line: true });
         }

         this.lineatt.color = color;

         g.append('svg:path')
          .attr('d', path)
          .style('fill', 'none')
          .call(this.lineatt.func);
      }

      return this;
   }

   /** @summary Actual drawing of TGraph2D object
     * @return {Promise} for drawing ready */
   async drawGraph2D() {
      const fp = this.getFramePainter(),
            main = this.getMainPainter(),
            graph = this.getObject(),
            o = this.getOptions();

      if (!graph || !main || !fp)
         return this;

      if (o.Contour)
         return this.drawContour(fp, main, graph);

      if (!fp.mode3d)
         return this;

      fp.remove3DMeshes(this);

      if (!o.isAny()) {
         // no need to draw smoothing if histogram content was drawn
         if (main.draw_content)
            return this;
         if ((graph.fMarkerSize === 1) && (graph.fMarkerStyle === 1))
            o.Circles = true;
         else
            o.Markers = true;
      }

      const countSelected = (zmin, zmax) => {
         let cnt = 0;
         for (let i = 0; i < graph.fNpoints; ++i) {
            if ((graph.fX[i] >= fp.scale_xmin) && (graph.fX[i] <= fp.scale_xmax) &&
                (graph.fY[i] >= fp.scale_ymin) && (graph.fY[i] <= fp.scale_ymax) &&
                (graph.fZ[i] >= zmin) && (graph.fZ[i] < zmax)) ++cnt;
         }
         return cnt;
      };

      // try to define scale-down factor
      let step = 1;
      if ((settings.OptimizeDraw > 0) && !fp.webgl) {
         const numselected = countSelected(fp.scale_zmin, fp.scale_zmax),
               sizelimit = 50000;

         if (numselected > sizelimit)
            step = Math.max(2, Math.floor(numselected / sizelimit));
      }

      const markeratt = this.createAttMarker({ attr: graph, std: false }),
            promises = [];
      let palette = null,
          levels = [fp.scale_zmin, fp.scale_zmax],
          scale = fp.size_x3d / 100 * markeratt.getFullSize();

      if (o.Circles)
         scale = 0.06 * fp.size_x3d;

      if (fp.usesvg)
         scale *= 0.3;

      const fw = fp.getFrameWidth(), fh = fp.getFrameHeight();

      if ((fw > 10) && (fh > 10))
         scale *= 7 * Math.max(fp.size_x3d / fw, fp.size_z3d / fh);

      if (o.Color || (o.Triangles >= 10)) {
         levels = main.getContourLevels(true);
         palette = main.getHistPalette();
      }

      if (o.Triangles)
         this.drawTriangles(fp, graph, levels, palette);

      for (let lvl = 0; lvl < levels.length - 1; ++lvl) {
         const lvl_zmin = Math.max(levels[lvl], fp.scale_zmin),
               lvl_zmax = Math.min(levels[lvl + 1], fp.scale_zmax);

         if (lvl_zmin >= lvl_zmax)
            continue;

         const size = Math.floor(countSelected(lvl_zmin, lvl_zmax) / step),
               index = new Int32Array(size),
               pnts = o.Markers || o.Circles ? new PointsCreator(size, fp.webgl, scale / 3) : null,
               err = o.Error ? new Float32Array(size * 6 * 3) : null,
               asymm = err && this.matchObjectType(clTGraph2DAsymmErrors),
               line = o.Line ? new Float32Array((size - 1) * 6) : null;

         let select = 0, icnt = 0, ierr = 0, iline = 0;

         for (let i = 0; i < graph.fNpoints; ++i) {
            if ((graph.fX[i] < fp.scale_xmin) || (graph.fX[i] > fp.scale_xmax) ||
                (graph.fY[i] < fp.scale_ymin) || (graph.fY[i] > fp.scale_ymax) ||
                (graph.fZ[i] < lvl_zmin) || (graph.fZ[i] >= lvl_zmax))
               continue;

            if (step > 1) {
               select = (select + 1) % step;
               if (select)
                  continue;
            }

            index[icnt++] = i; // remember point index for tooltip

            const x = fp.grx(graph.fX[i]), y = fp.gry(graph.fY[i]), z = fp.grz(graph.fZ[i]);

            pnts?.addPoint(x, y, z);

            if (err) {
               err[ierr] = fp.grx(graph.fX[i] - (asymm ? graph.fEXlow[i] : graph.fEX[i]));
               err[ierr + 1] = y;
               err[ierr + 2] = z;
               err[ierr + 3] = fp.grx(graph.fX[i] + (asymm ? graph.fEXhigh[i] : graph.fEX[i]));
               err[ierr + 4] = y;
               err[ierr + 5] = z;
               ierr += 6;
               err[ierr] = x;
               err[ierr + 1] = fp.gry(graph.fY[i] - (asymm ? graph.fEYlow[i] : graph.fEY[i]));
               err[ierr + 2] = z;
               err[ierr + 3] = x;
               err[ierr + 4] = fp.gry(graph.fY[i] + (asymm ? graph.fEYhigh[i] : graph.fEY[i]));
               err[ierr + 5] = z;
               ierr += 6;
               err[ierr] = x;
               err[ierr + 1] = y;
               err[ierr + 2] = fp.grz(graph.fZ[i] - (asymm ? graph.fEZlow[i] : graph.fEZ[i]));
               err[ierr + 3] = x;
               err[ierr + 4] = y;
               err[ierr + 5] = fp.grz(graph.fZ[i] + (asymm ? graph.fEZhigh[i] : graph.fEZ[i]));
               ierr += 6;
            }

            if (line) {
               if (iline >= 6) {
                  line[iline] = line[iline - 3];
                  line[iline + 1] = line[iline - 2];
                  line[iline + 2] = line[iline - 1];
                  iline += 3;
               }
               line[iline] = x;
               line[iline + 1] = y;
               line[iline + 2] = z;
               iline += 3;
            }
         }

         if (line && (iline > 3) && (line.length === iline)) {
            const lcolor = this.getColor(graph.fLineColor),
                  material = new THREE.LineBasicMaterial({ color: new THREE.Color(lcolor), linewidth: graph.fLineWidth }),
                  linemesh = createLineSegments(line, material);
            fp.add3DMesh(linemesh, this);

            linemesh.graph = graph;
            linemesh.index = index;
            linemesh.fp = fp;
            linemesh.scale0 = 0.7 * scale;
            linemesh.tip_name = this.getObjectHint();
            linemesh.tip_color = (graph.fMarkerColor === 3) ? 0xFF0000 : 0x00FF00;
            linemesh.nvertex = 2;
            linemesh.check_next = true;
            linemesh.tooltip = _graph2DTooltip;
         }

         if (err) {
            const lcolor = this.getColor(graph.fLineColor),
                  material = new THREE.LineBasicMaterial({ color: new THREE.Color(lcolor), linewidth: graph.fLineWidth }),
                  errmesh = createLineSegments(err, material);
            fp.add3DMesh(errmesh, this);

            errmesh.graph = graph;
            errmesh.index = index;
            errmesh.fp = fp;
            errmesh.scale0 = 0.7 * scale;
            errmesh.tip_name = this.getObjectHint();
            errmesh.tip_color = (graph.fMarkerColor === 3) ? 0xFF0000 : 0x00FF00;
            errmesh.nvertex = 6;
            errmesh.tooltip = _graph2DTooltip;
         }

         if (pnts) {
            let color = 'blue';

            if (!o.Circles || o.Color)
               color = palette?.calcColor(lvl, levels.length) ?? this.getColor(graph.fMarkerColor);

            const pr = pnts.createPoints({ color, fill: o.Circles ? 'white' : undefined, style: o.Circles ? 4 : graph.fMarkerStyle }).then(mesh => {
               mesh.graph = graph;
               mesh.fp = fp;
               mesh.tip_color = (graph.fMarkerColor === 3) ? 0xFF0000 : 0x00FF00;
               mesh.scale0 = 0.3 * scale;
               mesh.index = index;
               mesh.tip_name = this.getObjectHint();
               mesh.tooltip = _graph2DTooltip;
               fp.add3DMesh(mesh, this);
            });

            promises.push(pr);
         }
      }

      return Promise.all(promises).then(() => {
         const main2 = this.getMainPainter(),
               handle_palette = this.axes_draw || (main2?.draw_content === false);
         if (!handle_palette)
            return;

         const pal = main2?.findFunction(clTPaletteAxis),
               pal_painter = this.getPadPainter()?.findPainterFor(pal);
         if (!pal_painter)
            return;

         pal_painter.Enabled = o.Zscale;

         if (o.Zscale)
            return pal_painter.drawPave();

         pal_painter.removeG(); // completely remove drawing without need to redraw complete pad
      }).then(() => {
         fp.render3D(100);
         return this;
      });
   }

   /** @summary Build three.js of TGraph2D object */
   static async build3d(gr, opt) {
      const painter = new TGraph2DPainter(null, gr);
      painter.decodeOptions(opt, gr);

      if (painter.options.Contour) {
         console.error('Contour plot is not 3d');
         return null;
      }

      return TH2Painter.build3d(painter.createHistogram(), painter.options.Axis, true).then(hist_painter => {
         painter.axes_draw = true;
         const fp = hist_painter.getFramePainter();

         painter.getFramePainter = () => fp;
         painter.getMainPainter = () => hist_painter;

         return painter.drawGraph2D().then(() => fp.create3DScene(-1, true));
      });
   }

   /** @summary draw TGraph2D object */
   static async draw(dom, gr, opt) {
      const painter = new TGraph2DPainter(dom, gr);
      painter.decodeOptions(opt, gr);

      let promise = Promise.resolve(null);

      if (!painter.getMainPainter()) {
         // histogram is not preserved in TGraph2D
         promise = TH2Painter.draw(dom, painter.createHistogram(), painter.options.Axis);
         painter.axes_draw = true;
      }

      return promise.then(() => {
         painter.addToPadPrimitives();
         return painter.drawGraph2D();
      });
   }

} // class TGraph2DPainter

var TGraph2DPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TGraph2DPainter: TGraph2DPainter
});

const kNoTitle = BIT(17);

/**
 * @summary Painter for TGraphPolargram objects.
 *
 * @private */

class TGraphPolargramPainter extends TooltipHandler {

   /** @summary Create painter
     * @param {object|string} dom - DOM element for drawing or element id
     * @param {object} polargram - object to draw */
   constructor(dom, polargram, opt) {
      super(dom, polargram, opt);
      this.$polargram = true; // indicate that this is polargram
      this.zoom_rmin = this.zoom_rmax = 0;
      this.t0 = 0;
      this.mult = 1;
      this.decodeOptions(opt);
      this.setTooltipEnabled(true);
   }

   /** @summary Returns true if fixed coordinates are configured */
   isNormalAngles() {
      const polar = this.getObject();
      return polar?.fRadian || polar?.fGrad || polar?.fDegree;
   }

   /** @summary Decode draw options */
   decodeOptions(opt) {
      const d = new DrawOptions(opt);

      this.setOptions({
         rdot: d.check('RDOT'),
         rangle: d.check('RANGLE', true) ? d.partAsInt() : 0,
         NoLabels: d.check('N'),
         OrthoLabels: d.check('O')
      });

      this.storeDrawOpt(opt);
   }

   /** @summary Set angles range displayed by the polargram */
   setAnglesRange(tmin, tmax, set_obj) {
      if (tmin >= tmax)
         tmax = tmin + 1;
      if (set_obj) {
         const polar = this.getObject();
         polar.fRwtmin = tmin;
         polar.fRwtmax = tmax;
      }
      this.t0 = tmin;
      this.mult = 2 * Math.PI / (tmax - tmin);
   }

   /** @summary Translate coordinates */
   translate(input_angle, radius, keep_float) {
      // recalculate angle
      const angle = (input_angle - this.t0) * this.mult;
      let rx = this.r(radius),
          ry = rx / this.szx * this.szy,
          grx = rx * Math.cos(-angle),
          gry = ry * Math.sin(-angle);

      if (!keep_float) {
         grx = Math.round(grx);
         gry = Math.round(gry);
         rx = Math.round(rx);
         ry = Math.round(ry);
      }
      return { grx, gry, rx, ry };
   }

   /** @summary format label for radius ticks */
   format(radius) {
      if (radius === Math.round(radius))
         return radius.toString();
      if (this.ndig > 10)
         return radius.toExponential(4);
      return radius.toFixed((this.ndig > 0) ? this.ndig : 0);
   }

   /** @summary Convert axis values to text */
   axisAsText(axis, value) {
      if (axis === 'r') {
         if (value === Math.round(value))
            return value.toString();
         if (this.ndig > 10)
            return value.toExponential(4);
         return value.toFixed(this.ndig + 2);
      }

      value *= 180 / Math.PI;
      return (value === Math.round(value)) ? value.toString() : value.toFixed(1);
   }

   /** @summary Returns coordinate of frame - without using frame itself */
   getFrameRect() {
      const pp = this.getPadPainter(),
            pad = pp.getRootPad(true),
            w = pp.getPadWidth(),
            h = pp.getPadHeight(),
            rect = {};

      if (pad) {
         rect.szx = Math.round(Math.max(0.1, 0.5 - Math.max(pad.fLeftMargin, pad.fRightMargin)) * w);
         rect.szy = Math.round(Math.max(0.1, 0.5 - Math.max(pad.fBottomMargin, 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 Process mouse event */
   mouseEvent(kind, evnt) {
      let pnt = null;
      if (kind !== 'leave') {
         const pos = pointer(evnt, this.getG()?.node());
         pnt = { x: pos[0], y: pos[1], touch: false };
      }
      this.processFrameTooltipEvent(pnt);
   }

   /** @summary Process mouse wheel event */
   mouseWheel(evnt) {
      evnt.stopPropagation();
      evnt.preventDefault();

      this.processFrameTooltipEvent(null); // remove all tooltips

      const polar = this.getObject();
      if (!polar)
         return;

      let delta = evnt.wheelDelta ? -evnt.wheelDelta : (evnt.deltaY || evnt.detail);
      if (!delta)
         return;

      delta = (delta < 0) ? -0.2 : 0.2;

      let rmin = this.scale_rmin, rmax = this.scale_rmax;
      const range = rmax - rmin;

      // rmin -= delta*range;
      rmax += delta * range;

      if ((rmin < polar.fRwrmin) || (rmax > polar.fRwrmax))
         rmin = rmax = 0;

      if ((this.zoom_rmin !== rmin) || (this.zoom_rmax !== rmax)) {
         this.zoom_rmin = rmin;
         this.zoom_rmax = rmax;
         this.redrawPad();
      }
   }

   /** @summary Process mouse double click event */
   mouseDoubleClick() {
      if (this.zoom_rmin || this.zoom_rmax) {
         this.zoom_rmin = this.zoom_rmax = 0;
         this.redrawPad();
      }
   }

   /** @summary Draw polargram polar labels */
   async drawPolarLabels(polar, nmajor) {
      const fontsize = Math.round(polar.fPolarTextSize * this.szy * 2),
            o = this.getOptions();

      return this.startTextDrawingAsync(polar.fPolarLabelFont, fontsize).then(() => {
         const lbls = (nmajor === 8) ? ['0', '#frac{#pi}{4}', '#frac{#pi}{2}', '#frac{3#pi}{4}', '#pi', '#frac{5#pi}{4}', '#frac{3#pi}{2}', '#frac{7#pi}{4}'] : ['0', '#frac{2#pi}{3}', '#frac{4#pi}{3}'],
               aligns = [12, 11, 21, 31, 32, 33, 23, 13];

         for (let n = 0; n < nmajor; ++n) {
            const angle = -n * 2 * Math.PI / nmajor;
            this.getG().append('svg:path')
               .attr('d', `M0,0L${Math.round(this.szx * Math.cos(angle))},${Math.round(this.szy * Math.sin(angle))}`)
               .call(this.lineatt.func);

            let align = 12, rotate = 0;

            if (o.OrthoLabels) {
               rotate = -n / nmajor * 360;
               if ((rotate > -271) && (rotate < -91)) {
                  align = 32;
                  rotate += 180;
               }
            } else {
               const aindx = Math.round(16 - angle / Math.PI * 4) % 8; // index in align table, here absolute angle is important
               align = aligns[aindx];
            }

            this.drawText({
               align, rotate,
               x: Math.round((this.szx + fontsize) * Math.cos(angle)),
               y: Math.round((this.szy + fontsize / this.szx * this.szy) * (Math.sin(angle))),
               text: lbls[n],
               color: this.getColor(polar.fPolarLabelColor), latex: 1
            });
         }

         return this.finishTextDrawing();
      });
   }

   /** @summary Redraw polargram */
   async redraw() {
      if (!this.isMainPainter())
         return;

      const polar = this.getObject(),
            o = this.getOptions(),
            rect = this.getPadPainter().getFrameRect(),
            g = this.createG();

      makeTranslate(g, Math.round(rect.x + rect.width / 2), Math.round(rect.y + rect.height / 2));
      this.szx = rect.szx;
      this.szy = rect.szy;

      this.scale_rmin = polar.fRwrmin;
      this.scale_rmax = polar.fRwrmax;
      if (this.zoom_rmin !== this.zoom_rmax) {
         this.scale_rmin = this.zoom_rmin;
         this.scale_rmax = this.zoom_rmax;
      }

      this.r = linear().domain([this.scale_rmin, this.scale_rmax]).range([0, this.szx]);

      if (polar.fRadian) {
         polar.fRwtmin = 0;
         polar.fRwtmax = 2 * Math.PI;
      } else if (polar.fDegree) {
         polar.fRwtmin = 0;
         polar.fRwtmax = 360;
      } else if (polar.fGrad) {
         polar.fRwtmin = 0;
         polar.fRwtmax = 200;
      }

      this.setAnglesRange(polar.fRwtmin, polar.fRwtmax);

      const ticks = this.r.ticks(5);
      let nminor = Math.floor((polar.fNdivRad % 10000) / 100),
          nmajor = polar.fNdivPol % 100;
      if (nmajor !== 3)
         nmajor = 8;

      this.createAttLine({ attr: polar });
      if (!this.gridatt)
         this.gridatt = this.createAttLine({ color: polar.fLineColor, style: 2, width: 1, std: false });

      const range = Math.abs(polar.fRwrmax - polar.fRwrmin);
      this.ndig = (range <= 0) ? -3 : Math.round(Math.log10(ticks.length / range));

      // verify that all radius labels are unique
      let lbls = [], indx = 0;
      while (indx < ticks.length) {
         const lbl = this.format(ticks[indx]);
         if (lbls.indexOf(lbl) >= 0) {
            if (++this.ndig > 10)
               break;
            lbls = [];
            indx = 0;
            continue;
         }
         lbls.push(lbl);
         indx++;
      }

      let exclude_last = false;
      const pointer_events = this.isBatchMode() ? null : 'visibleFill';

      if ((ticks.at(-1) < polar.fRwrmax) && (this.zoom_rmin === this.zoom_rmax)) {
         ticks.push(polar.fRwrmax);
         exclude_last = true;
      }

      return this.startTextDrawingAsync(polar.fRadialLabelFont, Math.round(polar.fRadialTextSize * this.szy * 2)).then(() => {
         const axis_angle = - (o.rangle || polar.fAxisAngle) / 180 * Math.PI,
               ca = Math.cos(axis_angle),
               sa = Math.sin(axis_angle);
         for (let n = 0; n < ticks.length; ++n) {
            let rx = this.r(ticks[n]),
                ry = rx / this.szx * this.szy;
            g.append('ellipse')
             .attr('cx', 0)
             .attr('cy', 0)
             .attr('rx', Math.round(rx))
             .attr('ry', Math.round(ry))
             .style('fill', 'none')
             .style('pointer-events', pointer_events)
             .call(this.lineatt.func);

            if ((n < ticks.length - 1) || !exclude_last) {
               const halign = ca > 0.7 ? 1 : (ca > 0 ? 3 : (ca > -0.7 ? 1 : 3)),
                     valign = Math.abs(ca) < 0.7 ? 1 : 3;
               this.drawText({
                  align: 10 * halign + valign,
                  x: Math.round(rx * ca),
                  y: Math.round(ry * sa),
                  text: this.format(ticks[n]),
                  color: this.getColor(polar.fRadialLabelColor), latex: 0
               });
               if (o.rdot) {
                  g.append('ellipse')
                   .attr('cx', Math.round(rx * ca))
                   .attr('cy', Math.round(ry * sa))
                   .attr('rx', 3)
                   .attr('ry', 3)
                   .style('fill', 'red');
               }
            }

            if ((nminor > 1) && ((n < ticks.length - 1) || !exclude_last)) {
               const dr = (ticks[1] - ticks[0]) / nminor;
               for (let nn = 1; nn < nminor; ++nn) {
                  const gridr = ticks[n] + dr * nn;
                  if (gridr > this.scale_rmax)
                     break;
                  rx = this.r(gridr);
                  ry = rx / this.szx * this.szy;
                  g.append('ellipse')
                   .attr('cx', 0)
                   .attr('cy', 0)
                   .attr('rx', Math.round(rx))
                   .attr('ry', Math.round(ry))
                   .style('fill', 'none')
                   .style('pointer-events', pointer_events)
                   .call(this.gridatt.func);
               }
            }
         }

         if (ca < 0.999) {
            g.append('path')
             .attr('d', `M0,0L${Math.round(this.szx * ca)},${Math.round(this.szy * sa)}`)
             .style('pointer-events', pointer_events)
             .call(this.lineatt.func);
         }

         return this.finishTextDrawing();
      }).then(() => {
         return o.NoLabels ? true : this.drawPolarLabels(polar, nmajor);
      }).then(() => {
         nminor = Math.floor((polar.fNdivPol % 10000) / 100);

         if (nminor > 1) {
            for (let n = 0; n < nmajor * nminor; ++n) {
               if (n % nminor === 0)
                  continue;
               const angle = -n * 2 * Math.PI / nmajor / nminor;
               g.append('svg:path')
                .attr('d', `M0,0L${Math.round(this.szx * Math.cos(angle))},${Math.round(this.szy * Math.sin(angle))}`)
                .call(this.gridatt.func);
            }
         }

         if (this.isBatchMode())
            return;

         assignContextMenu(this, kNoReorder);

         this.assignZoomHandler(g);
      });
   }

   /** @summary Fill TGraphPolargram context menu */
   fillContextMenuItems(menu) {
      const pp = this.getObject(), o = this.getOptions();
      menu.sub('Axis range');
      menu.addchk(pp.fRadian, 'Radian', flag => {
         pp.fRadian = flag;
         pp.fDegree = pp.fGrad = false;
         this.interactiveRedraw('pad', flag ? 'exec:SetToRadian()' : 'exec:SetTwoPi()');
      }, 'Handle data angles as radian range 0..2*Pi');
      menu.addchk(pp.fDegree, 'Degree', flag => {
         pp.fDegree = flag;
         pp.fRadian = pp.fGrad = false;
         this.interactiveRedraw('pad', flag ? 'exec:SetToDegree()' : 'exec:SetTwoPi()');
      }, 'Handle data angles as degree range 0..360');
      menu.addchk(pp.fGrad, 'Grad', flag => {
         pp.fGrad = flag;
         pp.fRadian = pp.fDegree = false;
         this.interactiveRedraw('pad', flag ? 'exec:SetToGrad()' : 'exec:SetTwoPi()');
      }, 'Handle data angles as grad range 0..200');
      menu.endsub();
      menu.addSizeMenu('Axis angle', 0, 315, 45, o.rangle || pp.fAxisAngle, v => {
         o.rangle = pp.fAxisAngle = v;
         this.interactiveRedraw('pad', `exec:SetAxisAngle(${v})`);
      });
   }

   /** @summary Assign zoom handler to element
    * @private */
   assignZoomHandler(elem) {
      elem.on('mouseenter', evnt => this.mouseEvent('enter', evnt))
          .on('mousemove', evnt => this.mouseEvent('move', evnt))
          .on('mouseleave', evnt => this.mouseEvent('leave', evnt));

      if (settings.Zooming)
         elem.on('dblclick', evnt => this.mouseDoubleClick(evnt));

      if (settings.Zooming && settings.ZoomWheel)
         elem.on('wheel', evnt => this.mouseWheel(evnt));
   }

   /** @summary Draw TGraphPolargram */
   static async draw(dom, polargram, opt) {
      const main = getElementMainPainter(dom);
      if (main) {
         if (main.getObject() === polargram)
            return main;
         throw Error('Cannot superimpose TGraphPolargram with any other drawings');
      }

      const painter = new TGraphPolargramPainter(dom, polargram, opt);
      return ensureTCanvas(painter, false).then(() => {
         painter.setAsMainPainter();
         return painter.redraw();
      }).then(() => painter);
   }

} // class TGraphPolargramPainter


/**
 * @summary Painter for TGraphPolar objects.
 *
 * @private
 */

class TGraphPolarPainter extends ObjectPainter {

   /** @summary Decode options for drawing TGraphPolar */
   decodeOptions(opt) {
      const d = new DrawOptions(opt || 'L'),
            rdot = d.check('RDOT'),
            rangle = d.check('RANGLE', true) ? d.partAsInt() : 0,
            o = this.setOptions({
               mark: d.check('P'),
               err: d.check('E'),
               fill: d.check('F'),
               line: d.check('L'),
               curve: d.check('C'),
               radian: d.check('R'),
               degree: d.check('D'),
               grad: d.check('G'),
               Axis: d.check('N') ? 'N' : ''
            }, opt);

      if (d.check('O'))
         o.Axis += 'O';
      if (rdot)
         o.Axis += '_rdot';
      if (rangle)
         o.Axis += `_rangle${rangle}`;

      this.storeDrawOpt(opt);
   }

   /** @summary Update TGraphPolar with polargram */
   updateObject(obj, opt) {
      if (!this.matchObjectType(obj))
         return false;

      if (opt && (opt !== this.getOptions().original))
         this.decodeOptions(opt);

      if (this._draw_axis && obj.fPolargram)
         this.getMainPainter().updateObject(obj.fPolargram);

      delete obj.fPolargram;
      // copy all properties but not polargram
      Object.assign(this.getObject(), obj);
      return true;
   }

   /** @summary Redraw TGraphPolar */
   redraw() {
      return this.drawGraphPolar().then(() => this.updateTitle());
   }

   /** @summary Drawing TGraphPolar */
   async drawGraphPolar() {
      const graph = this.getObject(),
            o = this.getOptions(),
            main = this.getMainPainter();

      if (!graph || !main?.$polargram)
         return;

      if (o.mark)
         this.createAttMarker({ attr: graph });
      if (o.err || o.line || o.curve)
         this.createAttLine({ attr: graph });
      if (o.fill)
         this.createAttFill({ attr: graph });

      const g = this.createG();

      if (this._draw_axis && !main.isNormalAngles()) {
         const has_err = graph.fEX?.length;
         let rwtmin = graph.fX[0],
             rwtmax = graph.fX[0];
         for (let n = 0; n < graph.fNpoints; ++n) {
            rwtmin = Math.min(rwtmin, graph.fX[n] - (has_err ? graph.fEX[n] : 0));
            rwtmax = Math.max(rwtmax, graph.fX[n] + (has_err ? graph.fEX[n] : 0));
         }
         rwtmax += (rwtmax - rwtmin) / graph.fNpoints;
         main.setAnglesRange(rwtmin, rwtmax, true);
      }

      g.attr('transform', main.getG().attr('transform'));

      let mpath = '', epath = '';
      const bins = [], pointer_events = this.isBatchMode() ? null : 'visibleFill';

      for (let n = 0; n < graph.fNpoints; ++n) {
         if (graph.fY[n] > main.scale_rmax)
            continue;

         if (o.err) {
            const p1 = main.translate(graph.fX[n], graph.fY[n] - graph.fEY[n]),
                  p2 = main.translate(graph.fX[n], graph.fY[n] + graph.fEY[n]),
                  p3 = main.translate(graph.fX[n] + graph.fEX[n], graph.fY[n]),
                  p4 = main.translate(graph.fX[n] - graph.fEX[n], graph.fY[n]);

            epath += `M${p1.grx},${p1.gry}L${p2.grx},${p2.gry}` +
                     `M${p3.grx},${p3.gry}A${p4.rx},${p4.ry},0,0,1,${p4.grx},${p4.gry}`;
         }

         const pos = main.translate(graph.fX[n], graph.fY[n]);

         if (o.mark)
            mpath += this.markeratt.create(pos.grx, pos.gry);

         if (o.curve || o.line || o.fill)
            bins.push(pos);
      }

      if ((o.fill || o.line) && bins.length) {
         const lpath = buildSvgCurve(bins, { line: true });
         if (o.fill) {
            g.append('svg:path')
             .attr('d', lpath + 'Z')
             .style('pointer-events', pointer_events)
             .call(this.fillatt.func);
         }

         if (o.line) {
            g.append('svg:path')
             .attr('d', lpath)
             .style('fill', 'none')
             .style('pointer-events', pointer_events)
             .call(this.lineatt.func);
         }
      }

      if (o.curve && bins.length) {
         g.append('svg:path')
          .attr('d', buildSvgCurve(bins))
          .style('fill', 'none')
          .style('pointer-events', pointer_events)
          .call(this.lineatt.func);
      }

      if (epath) {
         g.append('svg:path')
          .attr('d', epath)
          .style('fill', 'none')
          .style('pointer-events', pointer_events)
          .call(this.lineatt.func);
      }

      if (mpath) {
         g.append('svg:path')
          .attr('d', mpath)
          .style('pointer-events', pointer_events)
          .call(this.markeratt.func);
      }

      if (!this.isBatchMode()) {
         assignContextMenu(this, kNoReorder);
         main.assignZoomHandler(g);
      }
   }

   /** @summary Create polargram object */
   createPolargram(gr) {
      const o = this.getOptions();
      if (!gr.fPolargram) {
         gr.fPolargram = create$1('TGraphPolargram');
         if (o.radian)
            gr.fPolargram.fRadian = true;
         else if (o.degree)
            gr.fPolargram.fDegree = true;
         else if (o.grad)
            gr.fPolargram.fGrad = true;
      }

      let rmin = gr.fY[0] || 0, rmax = rmin;
      const has_err = gr.fEY?.length;
      for (let n = 0; n < gr.fNpoints; ++n) {
         rmin = Math.min(rmin, gr.fY[n] - (has_err ? gr.fEY[n] : 0));
         rmax = Math.max(rmax, gr.fY[n] + (has_err ? gr.fEY[n] : 0));
      }

      gr.fPolargram.fRwrmin = rmin - (rmax - rmin) * 0.1;
      gr.fPolargram.fRwrmax = rmax + (rmax - rmin) * 0.1;

      return gr.fPolargram;
   }

   /** @summary Provide tooltip at specified point */
   extractTooltip(pnt) {
      if (!pnt)
         return null;

      const graph = this.getObject(),
            main = this.getMainPainter();
      let best_dist2 = 1e10, bestindx = -1, bestpos = null;

      for (let n = 0; n < graph.fNpoints; ++n) {
         const pos = main.translate(graph.fX[n], graph.fY[n]),
               dist2 = (pos.grx - pnt.x) ** 2 + (pos.gry - pnt.y) ** 2;
         if (dist2 < best_dist2) {
            best_dist2 = dist2;
            bestindx = n;
            bestpos = pos;
         }
      }

      let match_distance = 5;
      if (this.markeratt?.used)
         match_distance = this.markeratt.getFullSize();

      if (Math.sqrt(best_dist2) > match_distance)
         return null;

      const res = {
         name: this.getObject().fName, title: this.getObject().fTitle,
         x: bestpos.grx, y: bestpos.gry,
         color1: (this.markeratt?.used ? this.markeratt.color : undefined) ?? (this.fillatt?.used ? this.fillatt.color : undefined) ?? this.lineatt?.color,
         exact: Math.sqrt(best_dist2) < 4,
         lines: [this.getObjectHint()],
         binindx: bestindx,
         menu_dist: match_distance,
         radius: match_distance
      };

      res.lines.push(`r = ${main.axisAsText('r', graph.fY[bestindx])}`,
                     `phi = ${main.axisAsText('phi', graph.fX[bestindx])}`);

      if (graph.fEY && graph.fEY[bestindx])
         res.lines.push(`error r = ${main.axisAsText('r', graph.fEY[bestindx])}`);

      if (graph.fEX && graph.fEX[bestindx])
         res.lines.push(`error phi = ${main.axisAsText('phi', graph.fEX[bestindx])}`);

      return res;
   }

   /** @summary Only redraw histogram title
     * @return {Promise} with painter */
   async updateTitle() {
      // case when histogram drawn over other histogram (same option)
      if (!this._draw_axis)
         return this;

      const tpainter = this.getPadPainter()?.findPainterFor(null, kTitle, clTPaveText),
            pt = tpainter?.getObject();

      if (!tpainter || !pt)
         return this;

      const gr = this.getObject(),
            draw_title = !gr.TestBit(kNoTitle) && (gStyle.fOptTitle > 0);

      pt.Clear();
      if (draw_title)
         pt.AddText(gr.fTitle);
      return tpainter.redraw().then(() => this);
   }


   /** @summary Draw histogram title
     * @return {Promise} with painter */
   async drawTitle() {
      // case when histogram drawn over other histogram (same option)
      if (!this._draw_axis)
         return this;

      const gr = this.getObject(),
            st = gStyle,
            draw_title = !gr.TestBit(kNoTitle) && (st.fOptTitle > 0),
            pp = this.getPadPainter();

      let pt = pp.findInPrimitives(kTitle, clTPaveText);

      if (pt) {
         pt.Clear();
         if (draw_title)
            pt.AddText(gr.fTitle);
         return this;
      }

      pt = create$1(clTPaveText);
      Object.assign(pt, {
         fName: kTitle, fFillColor: st.fTitleColor, fFillStyle: st.fTitleStyle, fBorderSize: st.fTitleBorderSize,
         fTextFont: st.fTitleFont, fTextSize: st.fTitleFontSize, fTextColor: st.fTitleTextColor, fTextAlign: 22
      });

      if (draw_title)
         pt.AddText(gr.fTitle);
      return TPavePainter.draw(pp, pt, kPosTitle).then(p => {
         p?.setSecondaryId(this, kTitle);
         return this;
      });
   }

   /** @summary Show tooltip */
   showTooltip(hint) {
      let ttcircle = this.getG()?.selectChild('.tooltip_bin');

      if (!hint || !this.getG()) {
         ttcircle?.remove();
         return;
      }

      if (ttcircle.empty()) {
         ttcircle = this.getG().append('svg:ellipse')
                             .attr('class', 'tooltip_bin')
                             .style('pointer-events', 'none');
      }

      hint.changed = ttcircle.property('current_bin') !== hint.binindx;

      if (hint.changed) {
         ttcircle.attr('cx', hint.x)
               .attr('cy', hint.y)
               .attr('rx', Math.round(hint.radius))
               .attr('ry', Math.round(hint.radius))
               .style('fill', 'none')
               .style('stroke', hint.color1)
               .property('current_bin', hint.binindx);
      }
   }

   /** @summary Process tooltip event */
   processTooltipEvent(pnt) {
      const hint = this.extractTooltip(pnt);
      if (!pnt || !pnt.disabled)
         this.showTooltip(hint);
      return hint;
   }

   /** @summary Draw TGraphPolar */
   static async draw(dom, graph, opt) {
      const painter = new TGraphPolarPainter(dom, graph, opt);
      painter.decodeOptions(opt);

      const main = painter.getMainPainter();
      if (main && !main.$polargram) {
         console.error('Cannot superimpose TGraphPolar with plain histograms');
         return null;
      }

      let pr = Promise.resolve(null);
      if (!main) {
         // indicate that axis defined by this graph
         painter._draw_axis = true;
         pr = TGraphPolargramPainter.draw(dom, painter.createPolargram(graph), painter.options.Axis);
      }

      return pr.then(gram_painter => {
         gram_painter?.setSecondaryId(painter, 'polargram');
         painter.addToPadPrimitives();
         return painter.drawGraphPolar();
      }).then(() => painter.drawTitle());
   }

} // class TGraphPolarPainter

var TGraphPolarPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TGraphPolarPainter: TGraphPolarPainter,
TGraphPolargramPainter: TGraphPolargramPainter
});

/** @summary Create log scale for axis bins
  * @private */
function produceTAxisLogScale(axis, num, min, max) {
   let lmin, lmax;

   if (max > 0) {
      lmax = Math.log(max);
      lmin = min > 0 ? Math.log(min) : lmax - 5;
   } else {
      lmax = -10;
      lmin = -15;
   }

   axis.fNbins = num;
   axis.fXbins = new Array(num + 1);
   for (let i = 0; i <= num; ++i)
      axis.fXbins[i] = Math.exp(lmin + i / num * (lmax - lmin));
   axis.fXmin = Math.exp(lmin);
   axis.fXmax = Math.exp(lmax);
}

function scanTF1Options(opt) {
   if (!isStr(opt))
      opt = '';
   let p = opt.indexOf(';webcanv_hist'), _webcanv_hist = false, _use_saved = 0;
   if (p >= 0) {
      _webcanv_hist = true;
      opt = opt.slice(0, p);
   }
   p = opt.indexOf(';force_saved');
   if (p >= 0) {
      _use_saved = 2;
      opt = opt.slice(0, p);
   }
   p = opt.indexOf(';prefer_saved');
   if (p >= 0) {
      _use_saved = 1;
      opt = opt.slice(0, p);
   }
   return { opt, _webcanv_hist, _use_saved };
}


/**
  * @summary Painter for TF1 object
  *
  * @private
  */

class TF1Painter extends TH1Painter$2 {

   #use_saved_points; // use saved points for drawing
   #func; // func object
   #tmp_tooltip; // temporary tooltip
   #fail_eval; // fail evaluation of function

   /** @summary Assign function  */
   setFunc(f) { this.#func = f; }

   /** @summary Returns drawn object name */
   getObjectName() { return this.#func?.fName ?? 'func'; }

   /** @summary Returns drawn object class name */
   getClassName() { return this.#func?._typename ?? clTF1; }

   /** @summary Returns true while function is drawn */
   isTF1() { return true; }

   isTF12() { return this.getClassName() === clTF12; }

   /** @summary Returns primary function which was then drawn as histogram */
   getPrimaryObject() { return this.#func; }

   /** @summary Update function */
   updateObject(obj /* , opt */) {
      if (!obj || (this.getClassName() !== obj._typename))
         return false;
      delete obj.evalPar;
      const histo = this.getHisto();

      if (this._webcanv_hist) {
         const h0 = this.getPadPainter()?.findInPrimitives('Func', clTH1D);
         if (h0)
            this.updateAxes(histo, h0, this.getFramePainter());
      }

      this.setFunc(obj);
      this.createTF1Histogram(obj, histo);
      this.scanContent();
      return true;
   }

   /** @summary Redraw TF1
     * @private */
   redraw(reason) {
      if (!this.#use_saved_points && (reason === 'logx' || reason === 'zoom')) {
         this.createTF1Histogram(this.#func, this.getHisto());
         this.scanContent();
      }

      return super.redraw(reason);
   }

   /** @summary Create histogram for TF1 drawing
     * @private */
   createTF1Histogram(tf1, hist) {
      const fp = this.getFramePainter(),
            pad = this.getPadPainter()?.getRootPad(true),
            logx = pad?.fLogx,
            gr = fp?.getGrFuncs(this.second_x, this.second_y);
      let xmin = tf1.fXmin, xmax = tf1.fXmax, np = Math.max(tf1.fNpx, 100);

      if (gr?.zoom_xmin !== gr?.zoom_xmax) {
         const dx = (xmax - xmin) / np;
         if ((xmin < gr.zoom_xmin) && (gr.zoom_xmin < xmax))
            xmin = Math.max(xmin, gr.zoom_xmin - dx);
         if ((xmin < gr.zoom_xmax) && (gr.zoom_xmax < xmax))
            xmax = Math.min(xmax, gr.zoom_xmax + dx);
      }

      this.#use_saved_points = (tf1.fSave.length > 3) && (settings.PreferSavedPoints || (this._use_saved > 1));

      const ensureBins = num => {
         if (hist.fNcells !== num + 2) {
            hist.fNcells = num + 2;
            hist.fArray = new Float32Array(hist.fNcells);
         }
         hist.fArray.fill(0);
         hist.fXaxis.fNbins = num;
         hist.fXaxis.fXbins = [];
      };

      this.#fail_eval = undefined;

      // this.#use_saved_points = true;

      if (!this.#use_saved_points) {
         let iserror = false;

         if (!tf1.evalPar) {
            try {
               if (this.isTF12()) {
                  if (proivdeEvalPar(tf1.fF2)) {
                     tf1.evalPar = function(x) {
                        return this.fCase ? this.fF2.evalPar(x, this.fXY) : this.fF2.evalPar(this.fXY, x);
                     };
                  } else
                     iserror = true;
               } else if (!proivdeEvalPar(tf1))
                  iserror = true;
            } catch {
               iserror = true;
            }
         }

         ensureBins(np);

         if (logx)
            produceTAxisLogScale(hist.fXaxis, np, xmin, xmax);
         else {
            hist.fXaxis.fXmin = xmin;
            hist.fXaxis.fXmax = xmax;
         }

         for (let n = 0; (n < np) && !iserror; n++) {
            const x = hist.fXaxis.GetBinCenter(n + 1);
            let y = 0;
            try {
               y = tf1.evalPar(x);
            } catch {
               iserror = true;
            }

            if (!iserror)
               hist.setBinContent(n + 1, Number.isFinite(y) ? y : 0);
         }

         if (iserror)
            this.#fail_eval = true;

         if (iserror && (tf1.fSave.length > 3))
            this.#use_saved_points = true;
      }

      // in the case there were points have saved and we cannot calculate function
      // if we don't have the user's function
      if (this.#use_saved_points) {
         np = tf1.fSave.length - 3;
         let custom_xaxis = null;
         xmin = tf1.fSave[np + 1];
         xmax = tf1.fSave[np + 2];

         if (xmin === xmax) {
            const mp = this.getMainPainter();
            if (isFunc(mp?.getHisto))
               custom_xaxis = mp?.getHisto()?.fXaxis;
         }

         if (custom_xaxis) {
            ensureBins(hist.fXaxis.fNbins);
            Object.assign(hist.fXaxis, custom_xaxis);
            // TODO: find first bin

            for (let n = 0; n < np; ++n) {
               const y = tf1.fSave[n];
               hist.setBinContent(n + 1, Number.isFinite(y) ? y : 0);
            }
         } else {
            ensureBins(tf1.fNpx);
            hist.fXaxis.fXmin = tf1.fXmin;
            hist.fXaxis.fXmax = tf1.fXmax;

            for (let n = 0; n < tf1.fNpx; ++n) {
               const y = _getTF1Save(tf1, hist.fXaxis.GetBinCenter(n + 1));
               hist.setBinContent(n + 1, Number.isFinite(y) ? y : 0);
            }
         }
      }

      hist.fName = 'Func';
      setHistogramTitle(hist, tf1.fTitle);
      hist.fMinimum = tf1.fMinimum;
      hist.fMaximum = tf1.fMaximum;
      hist.fLineColor = tf1.fLineColor;
      hist.fLineStyle = tf1.fLineStyle;
      hist.fLineWidth = tf1.fLineWidth;
      hist.fFillColor = tf1.fFillColor;
      hist.fFillStyle = tf1.fFillStyle;
      hist.fMarkerColor = tf1.fMarkerColor;
      hist.fMarkerStyle = tf1.fMarkerStyle;
      hist.fMarkerSize = tf1.fMarkerSize;
      hist.fBits |= kNoStats;
   }

   /** @summary Extract function ranges */
   extractAxesProperties(ndim) {
      super.extractAxesProperties(ndim);

      const func = this.#func, nsave = func?.fSave.length ?? 0;

      if (nsave > 3 && this.#use_saved_points) {
         this.xmin = Math.min(this.xmin, func.fSave[nsave - 2]);
         this.xmax = Math.max(this.xmax, func.fSave[nsave - 1]);
      }
      if (func) {
         this.xmin = Math.min(this.xmin, func.fXmin);
         this.xmax = Math.max(this.xmax, func.fXmax);
      }
   }

   /** @summary Checks if it makes sense to zoom inside specified axis range */
   canZoomInside(axis, min, max) {
      const nsave = this.#func?.fSave.length ?? 0;
      if ((nsave > 3) && this.#use_saved_points && (axis === 'x')) {
         // in the case where the points have been saved, useful for example
         // if we don't have the user's function
         const nb_points = nsave - 2,
               xmin = this.#func.fSave[nsave - 2],
               xmax = this.#func.fSave[nsave - 1];

         return Math.abs(xmax - xmin) / nb_points < Math.abs(max - min);
      }

      // if function calculated, one always could zoom inside
      return (axis === 'x') || (axis === 'y');
   }

   /** @summary return tooltips for TF2 */
   getTF1Tooltips(pnt) {
      this.#tmp_tooltip = undefined;
      const lines = [this.getObjectHint()],
            o = this.getOptions(),
            funcs = this.getFramePainter()?.getGrFuncs(o.second_x, o.second_y);

      if (!funcs || !isFunc(this.#func?.evalPar)) {
         lines.push('grx = ' + pnt.x, 'gry = ' + pnt.y);
         return lines;
      }

      const x = funcs.revertAxis('x', pnt.x);
      let y = 0, gry = 0, iserror = false;

      try {
         y = this.#func.evalPar(x);
         gry = Math.round(funcs.gry(y));
      } catch {
         iserror = true;
      }

      lines.push('x = ' + funcs.axisAsText('x', x),
                 'value = ' + (iserror ? '<fail>' : floatToString(y, gStyle.fStatFormat)));

      if (!iserror)
         this.#tmp_tooltip = { y, gry };
      return lines;
   }

   /** @summary process tooltip event for TF1 object */
   processTooltipEvent(pnt) {
      if (this.#use_saved_points)
         return super.processTooltipEvent(pnt);

      let ttrect = this.getG()?.selectChild('.tooltip_bin');

      if (!this.getG() || !pnt) {
         ttrect?.remove();
         return null;
      }

      const res = {
         name: this.#func?.fName, title: this.#func?.fTitle,
         x: pnt.x, y: pnt.y,
         color1: this.lineatt?.color ?? 'green',
         color2: this.fillatt?.getFillColorAlt('blue') ?? 'blue',
         lines: this.getTF1Tooltips(pnt), exact: true, menu: true
      };

      if (pnt.disabled)
         ttrect.remove();
      else {
         if (ttrect.empty()) {
            ttrect = this.getG().append('svg:circle')
                         .attr('class', 'tooltip_bin')
                         .style('pointer-events', 'none')
                         .style('fill', 'none')
                         .attr('r', (this.lineatt?.width ?? 1) + 4);
         }

         ttrect.attr('cx', pnt.x)
               .attr('cy', this.#tmp_tooltip?.gry ?? pnt.y);
         if (this.lineatt)
            ttrect.call(this.lineatt.func);
      }

      return res;
   }

   /** @summary fill information for TWebCanvas
    * @desc Used to inform web canvas when evaluation failed
     * @private */
   fillWebObjectOptions(opt) {
      opt.fcust = this.#fail_eval && !this._use_saved ? 'func_fail' : '';
   }

   /** @summary draw TF1 object */
   static async draw(dom, tf1, opt) {
      const web = scanTF1Options(opt);
      opt = web.opt;
      delete web.opt;
      let hist;

      if (web._webcanv_hist)
         hist = getElementPadPainter(dom)?.findInPrimitives('Func', clTH1D);

      if (!hist) {
         hist = createHistogram(clTH1D, 100);
         hist.fBits |= kNoStats;
      }

      if (!opt && getElementMainPainter(dom))
         opt = 'same';

      const painter = new TF1Painter(dom, hist);

      painter.setFunc(tf1);
      Object.assign(painter, web);

      painter.createTF1Histogram(tf1, hist);

      return THistPainter._drawHist(painter, opt);
   }

} // class TF1Painter

var TF1Painter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TF1Painter: TF1Painter,
produceTAxisLogScale: produceTAxisLogScale,
scanTF1Options: scanTF1Options
});

const kIsBayesian = BIT(14),  // Bayesian statistics are used
      kPosteriorMode = BIT(15),  // Use posterior mean for best estimate (Bayesian statistics)
   //   kShortestInterval = BIT(16),  // Use shortest interval, not implemented - too complicated
      kUseBinPrior = BIT(17),  // Use a different prior for each bin
      kUseWeights = BIT(18),  // Use weights
      getBetaAlpha = (obj, bin) => { return (obj.fBeta_bin_params.length > bin) ? obj.fBeta_bin_params[bin].first : obj.fBeta_alpha; },
      getBetaBeta = (obj, bin) => { return (obj.fBeta_bin_params.length > bin) ? obj.fBeta_bin_params[bin].second : obj.fBeta_beta; };

/**
 * @summary Painter for TEfficiency object
 *
 * @private
 */

class TEfficiencyPainter extends ObjectPainter {

   /** @summary Calculate efficiency */
   getEfficiency(obj, bin) {
      const BetaMean = (a, b) => { return (a <= 0 || b <= 0) ? 0 : a / (a + b); },
            BetaMode = (a, b) => {
               if (a <= 0 || b <= 0)
                  return 0;
               if (a <= 1 || b <= 1) {
                  if (a < b)
                     return 0;
                  if (a > b)
                     return 1;
                  if (a === b)
                     return 0.5; // cannot do otherwise
               }
               return (a - 1.0) / (a + b - 2.0);
            },
            total = obj.fTotalHistogram.fArray[bin], // should work for both 1-d and 2-d
            passed = obj.fPassedHistogram.fArray[bin]; // should work for both 1-d and 2-d

      if (obj.TestBit(kIsBayesian)) {
         // parameters for the beta prior distribution
         const alpha = obj.TestBit(kUseBinPrior) ? getBetaAlpha(obj, bin) : obj.fBeta_alpha,
               beta = obj.TestBit(kUseBinPrior) ? getBetaBeta(obj, bin) : obj.fBeta_beta;

         let aa, bb;
         if (obj.TestBit(kUseWeights)) {
            const tw = total, // fTotalHistogram->GetBinContent(bin);
                  tw2 = obj.fTotalHistogram.fSumw2 ? obj.fTotalHistogram.fSumw2[bin] : Math.abs(total),
                  pw = passed; // fPassedHistogram->GetBinContent(bin);

            if (tw2 <= 0)
               return pw / tw;

            // tw/tw2 re-normalize the weights
            const norm = tw / tw2;
            aa = pw * norm + alpha;
            bb = (tw - pw) * norm + beta;
         } else {
            aa = passed + alpha;
            bb = total - passed + beta;
         }

         return !obj.TestBit(kPosteriorMode) ? BetaMean(aa, bb) : BetaMode(aa, bb);
      }

      return total ? passed / total : 0;
   }

   /** @summary Calculate efficiency error low */
   getEfficiencyErrorLow(obj, bin, value) {
      const total = obj.fTotalHistogram.fArray[bin],
            passed = obj.fPassedHistogram.fArray[bin];
      let alpha = 0, beta = 0;
      if (obj.TestBit(kIsBayesian)) {
         alpha = obj.TestBit(kUseBinPrior) ? getBetaAlpha(obj, bin) : obj.fBeta_alpha;
         beta = obj.TestBit(kUseBinPrior) ? getBetaBeta(obj, bin) : obj.fBeta_beta;
      }

      return value - this.fBoundary(total, passed, obj.fConfLevel, false, alpha, beta);
   }

   /** @summary Calculate efficiency error low up */
   getEfficiencyErrorUp(obj, bin, value) {
      const total = obj.fTotalHistogram.fArray[bin],
            passed = obj.fPassedHistogram.fArray[bin];
      let alpha = 0, beta = 0;
      if (obj.TestBit(kIsBayesian)) {
         alpha = obj.TestBit(kUseBinPrior) ? getBetaAlpha(obj, bin) : obj.fBeta_alpha;
         beta = obj.TestBit(kUseBinPrior) ? getBetaBeta(obj, bin) : obj.fBeta_beta;
      }

      return this.fBoundary(total, passed, obj.fConfLevel, true, alpha, beta) - value;
   }

   /** @summary Copy drawing attributes */
   copyAttributes(obj, eff) {
      ['fLineColor', 'fLineStyle', 'fLineWidth', 'fFillColor', 'fFillStyle', 'fMarkerColor', 'fMarkerStyle', 'fMarkerSize'].forEach(name => { obj[name] = eff[name]; });
   }

   /** @summary Create graph for the drawing of 1-dim TEfficiency */
   createGraph(/* eff */) {
      const gr = create$1(clTGraphAsymmErrors);
      gr.fName = 'eff_graph';
      return gr;
   }

   /** @summary Create histogram for the drawing of 2-dim TEfficiency */
   createHisto(eff) {
      const nbinsx = eff.fTotalHistogram.fXaxis.fNbins,
            nbinsy = eff.fTotalHistogram.fYaxis.fNbins,
            hist = createHistogram(clTH2F, nbinsx, nbinsy);
      Object.assign(hist.fXaxis, eff.fTotalHistogram.fXaxis);
      Object.assign(hist.fYaxis, eff.fTotalHistogram.fYaxis);
      hist.fName = 'eff_histo';
      return hist;
   }

   /** @summary Fill graph with points from efficiency object */
   fillGraph(gr, opt) {
      const eff = this.getObject(),
            xaxis = eff.fTotalHistogram.fXaxis,
            npoints = xaxis.fNbins,
            plot0Bins = (opt.indexOf('e0') >= 0);

      for (let n = 0, j = 0; n < npoints; ++n) {
         if (!plot0Bins && eff.fTotalHistogram.getBinContent(n + 1) === 0)
            continue;

         const value = this.getEfficiency(eff, n + 1);

         gr.fX[j] = xaxis.GetBinCenter(n + 1);
         gr.fY[j] = value;
         gr.fEXlow[j] = xaxis.GetBinCenter(n + 1) - xaxis.GetBinLowEdge(n + 1);
         gr.fEXhigh[j] = xaxis.GetBinLowEdge(n + 2) - xaxis.GetBinCenter(n + 1);
         gr.fEYlow[j] = this.getEfficiencyErrorLow(eff, n + 1, value);
         gr.fEYhigh[j] = this.getEfficiencyErrorUp(eff, n + 1, value);

         gr.fNpoints = ++j;
      }

      gr.fTitle = eff.fTitle;
      this.copyAttributes(gr, eff);
   }

   /** @summary Fill graph with points from efficiency object */
   fillHisto(hist) {
      const eff = this.getObject(),
            nbinsx = hist.fXaxis.fNbins,
            nbinsy = hist.fYaxis.fNbins;

      for (let i = 0; i < nbinsx + 2; ++i) {
         for (let j = 0; j < nbinsy + 2; ++j) {
            const bin = hist.getBin(i, j);
            hist.fArray[bin] = this.getEfficiency(eff, bin);
         }
      }

      hist.fTitle = eff.fTitle;
      hist.fBits |= kNoStats;
      this.copyAttributes(hist, eff);
   }

   /** @summary Draw function */
   drawFunction(indx) {
      const eff = this.getObject();

      if (!eff?.fFunctions || (indx >= eff.fFunctions.arr.length))
         return this;

      return TF1Painter.draw(this.getPadPainter(), eff.fFunctions.arr[indx], eff.fFunctions.opt[indx])
                        .then(funcp => {
                           funcp?.setSecondaryId(this, `func_${indx}`);
                           return this.drawFunction(indx + 1);
                        });
   }

   /** @summary Fill context menu */
   fillContextMenuItems(menu) {
      menu.addRedrawMenu(this);
   }

   /** @summary Fully redraw efficiency with new draw options */
   async redrawWith(opt, skip_cleanup) {
      if (!skip_cleanup)
         this.getPadPainter()?.removePrimitive(this, true);

      if (!opt || !isStr(opt))
         opt = '';
      opt = opt.toLowerCase();

      let promise, draw_total = false;

      const eff = this.getObject(),
            dom = this.getDrawDom();

      if (opt[0] === 'b') {
         draw_total = true;
         promise = (this.ndim === 1 ? TH1Painter : TH2Painter).draw(dom, eff.fTotalHistogram, opt.slice(1));
      } else if (this.ndim === 1) {
         if (!opt)
            opt = 'ap';
         if ((opt.indexOf('same') < 0) && (opt.indexOf('a') < 0))
            opt += 'a';
         if (opt.indexOf('p') < 0)
            opt += 'p';

         const gr = this.createGraph(eff);
         this.fillGraph(gr, opt);
         promise = TGraphPainter$1.draw(dom, gr, opt);
      } else {
         if (!opt)
            opt = 'col';
         const hist = this.createHisto(eff);
         this.fillHisto(hist, opt);
         promise = TH2Painter.draw(dom, hist, opt);
      }

      return promise.then(subp => {
         subp?.setSecondaryId(this, 'eff');
         this.addToPadPrimitives();
         return draw_total ? this : this.drawFunction(0);
      });
   }

   /** @summary Draw TEfficiency object */
   static async draw(dom, eff, opt) {
      if (!eff || !eff.fTotalHistogram)
         return null;

      const painter = new TEfficiencyPainter(dom, eff);

      if (eff.fTotalHistogram._typename.indexOf(clTH1) === 0)
         painter.ndim = 1;
      else if (eff.fTotalHistogram._typename.indexOf(clTH2) === 0)
         painter.ndim = 2;
      else
         return null;

      painter.fBoundary = getTEfficiencyBoundaryFunc(eff.fStatisticOption, eff.TestBit(kIsBayesian));

      return painter.redrawWith(opt, true);
   }

} // class TEfficiencyPainter

var TEfficiencyPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TEfficiencyPainter: TEfficiencyPainter
});

/**
 * @summary Painter for TScatter object.
 *
 * @private
 */

class TScatterPainter extends TGraphPainter$1 {

   #color_palette; // color palette
   #contour; // colors contour

   /** @summary Cleanup painter */
   cleanup() {
      this.clearHistPalette();
      this.#contour = undefined;
      super.cleanup();
   }

   /** @summary Return drawn graph object */
   getGraph() { return this.getObject()?.fGraph; }

   /** @summary Return colors contour */
   getContour() { return this.#contour; }

   /** @summary Is TScatter object */
   isScatter() { return true; }

   /** @summary Return margins for histogram ranges */
   getHistRangeMargin() { return this.getObject()?.fMargin ?? 0.1; }

   /** @summary Draw axis histogram
     * @private */
   async drawAxisHisto() {
      const set_x = this.isDummyHistogram('x'),
            set_y = this.isDummyHistogram('y'),
            histo = this.createHistogram(set_x, set_y);
      return TH2Painter$2.draw(this.getDrawDom(), histo, this.getOptions().Axis + ';IGNORE_PALETTE');
   }

   /** @summary Provide palette, create if necessary
     * @private */
   getPalette() {
      const gr = this.getGraph();
      let pal = gr?.fFunctions?.arr?.find(func => (func._typename === clTPaletteAxis));

      if (!pal && gr) {
         pal = create$1(clTPaletteAxis);

         const fp = this.get_fp();
         Object.assign(pal, { fX1NDC: fp.fX2NDC + 0.005, fX2NDC: fp.fX2NDC + 0.05, fY1NDC: fp.fY1NDC, fY2NDC: fp.fY2NDC, fInit: 1, $can_move: true });
         Object.assign(pal.fAxis, { fChopt: '+', fLineColor: 1, fLineSyle: 1, fLineWidth: 1, fTextAngle: 0, fTextAlign: 11, fNdiv: 510 });
         gr.fFunctions.AddFirst(pal, '');
      }

      return pal;
   }

   /** @summary Update TScatter members
    * @private */
   _updateMembers(scatter, obj) {
      scatter.fBits = obj.fBits;
      scatter.fTitle = obj.fTitle;
      scatter.fNpoints = obj.fNpoints;
      scatter.fColor = obj.fColor;
      scatter.fSize = obj.fSize;
      scatter.fMargin = obj.fMargin;
      scatter.fMinMarkerSize = obj.fMinMarkerSize;
      scatter.fMaxMarkerSize = obj.fMaxMarkerSize;
      return super._updateMembers(scatter.fGraph, obj.fGraph);
   }

   /** @summary Return Z axis used for palette drawing
    * @private */
   getZaxis() {
      return this.getHistogram()?.fZaxis;
   }

   /** @summary Checks if it makes sense to zoom inside specified axis range */
   canZoomInside(axis, min, max) {
      if (axis !== 'z')
         return super.canZoomInside(axis, min, max);

      const levels = this.#contour?.getLevels();
      if (!levels)
         return false;
      // match at least full color level inside
      for (let i = 0; i < levels.length - 1; ++i) {
         if ((min <= levels[i]) && (max >= levels[i + 1]))
            return true;
      }
      return false;
   }

   /** @summary Returns color palette associated with histogram
    * @desc Create if required, checks pad and canvas for custom palette */
   getHistPalette(force) {
      let pal = force ? null : this.#color_palette;
      if (pal)
         return pal;
      const pp = this.getPadPainter();
      if (isFunc(pp?.getCustomPalette))
         pal = pp.getCustomPalette();
      if (!pal)
         pal = getColorPalette(this.getOptions().Palette, pp?.isGrayscale());
      this.#color_palette = pal;
      return pal;
   }

   /** @summary Remove palette */
   clearHistPalette() {
      this.#color_palette = undefined;
   }

   /** @summary Actual drawing of TScatter */
   async drawGraph() {
      const fp = this.get_fp(),
            hpainter = this.getMainPainter(),
            scatter = this.getObject(),
            hist = this.getHistogram();

      let scale = 1, offset = 0, palette;
      if (!fp || !hpainter || !scatter)
         return;

      if (scatter.fColor) {
         const pal = this.getPalette();
         if (pal)
            pal.$main_painter = this;

         palette = this.getHistPalette();

         let minc = scatter.fColor[0], maxc = scatter.fColor[0];
         for (let i = 1; i < scatter.fColor.length; ++i) {
            minc = Math.min(minc, scatter.fColor[i]);
            maxc = Math.max(maxc, scatter.fColor[i]);
         }
         if (maxc <= minc)
            maxc = minc < 0 ? 0.9 * minc : (minc > 0 ? 1.1 * minc : 1);
         else if ((minc > 0) && (minc < 0.3 * maxc))
            minc = 0;
         this.#contour = new HistContour(minc, maxc);
         this.#contour.createNormal(30);
         this.#contour.configIndicies(0, 0);

         fp.zmin = minc;
         fp.zmax = maxc;

         if (!fp.zoomChangedInteractive('z') && hist && hist.fMinimum !== kNoZoom && hist.fMaximum !== kNoZoom) {
            fp.zoom_zmin = hist.fMinimum;
            fp.zoom_zmax = hist.fMaximum;
         }
      }

      if (scatter.fSize) {
         let mins = scatter.fSize[0], maxs = scatter.fSize[0];

         for (let i = 1; i < scatter.fSize.length; ++i) {
            mins = Math.min(mins, scatter.fSize[i]);
            maxs = Math.max(maxs, scatter.fSize[i]);
         }

         if (maxs <= mins)
            maxs = mins < 0 ? 0.9 * mins : (mins > 0 ? 1.1 * mins : 1);

         scale = (scatter.fMaxMarkerSize - scatter.fMinMarkerSize) / (maxs - mins);
         offset = mins;
      }

      const g = this.createG(!fp.pad_layer),
            funcs = fp.getGrFuncs(),
            is_zoom = (fp.zoom_zmin !== fp.zoom_zmax) && scatter.fColor,
            bins = this._getBins();

      for (let i = 0; i < bins.length; ++i) {
         if (is_zoom && ((scatter.fColor[i] < fp.zoom_zmin) || (scatter.fColor[i] > fp.zoom_zmax)))
            continue;

         const pnt = bins[i],
               grx = funcs.grx(pnt.x),
               gry = funcs.gry(pnt.y),
               size = scatter.fSize ? scatter.fMinMarkerSize + scale * (scatter.fSize[i] - offset) : scatter.fMarkerSize,
               color = scatter.fColor ? this.#contour.getPaletteColor(palette, scatter.fColor[i]) : this.getColor(scatter.fMarkerColor),
               handle = new TAttMarkerHandler({ color, size, style: scatter.fMarkerStyle });

         g.append('svg:path')
          .attr('d', handle.create(grx, gry))
          .call(handle.func);
      }

      return this;
   }

   /** @summary Draw TScatter object */
   static async draw(dom, obj, opt) {
      return TGraphPainter$1._drawGraph(new TScatterPainter(dom, obj), opt);
   }

} // class TScatterPainter

var TScatterPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TScatterPainter: TScatterPainter
});

const kLineNDC = BIT(14);

/**
 * @summary Painter for TLine class
 * @private
 */

class TLinePainter extends ObjectPainter {

   #side; // side which is interactively moved

   /** @summary Start interactive moving */
   moveStart(x, y) {
      const fullsize = Math.max(1, Math.sqrt((this.x1 - this.x2) ** 2 + (this.y1 - this.y2) ** 2)),
            sz1 = Math.sqrt((x - this.x1) ** 2 + (y - this.y1) ** 2) / fullsize,
            sz2 = Math.sqrt((x - this.x2) ** 2 + (y - this.y2) ** 2) / fullsize;
      if (sz1 > 0.9)
         this.#side = 1;
      else if (sz2 > 0.9)
         this.#side = -1;
      else
         this.#side = 0;
   }

   /** @summary Continue interactive moving */
   moveDrag(dx, dy) {
      if (this.#side !== 1) {
         this.x1 += dx;
         this.y1 += dy;
      }
      if (this.#side !== -1) {
         this.x2 += dx;
         this.y2 += dy;
      }
      this.getG().select('path').attr('d', this.createPath());
   }

   /** @summary Finish interactive moving */
   moveEnd(not_changed) {
      if (not_changed)
         return;
      const line = this.getObject();
      let exec = '',
          fx1 = this.svgToAxis('x', this.x1, this.isndc),
          fx2 = this.svgToAxis('x', this.x2, this.isndc),
          fy1 = this.svgToAxis('y', this.y1, this.isndc),
          fy2 = this.svgToAxis('y', this.y2, this.isndc);
      if (this.swap_xy)
         [fx1, fy1, fx2, fy2] = [fy1, fx1, fy2, fx2];
      line.fX1 = fx1;
      line.fX2 = fx2;
      line.fY1 = fy1;
      line.fY2 = fy2;
      if (this.#side !== 1)
         exec += `SetX1(${fx1});;SetY1(${fy1});;`;
      if (this.#side !== -1)
         exec += `SetX2(${fx2});;SetY2(${fy2});;`;
      this.submitCanvExec(exec + 'Notify();;');
   }

   /** @summary Returns object ranges
     * @desc Can be used for newly created canvas */
   getUserRanges() {
      const line = this.getObject(),
            isndc = line.TestBit(kLineNDC);
      if (isndc)
         return null;
      const minx = Math.min(line.fX1, line.fX2),
            maxx = Math.max(line.fX1, line.fX2),
            miny = Math.min(line.fY1, line.fY2),
            maxy = Math.max(line.fY1, line.fY2);
      return { minx, miny, maxx, maxy };
   }

   /** @summary Calculate line coordinates */
   prepareDraw() {
      const line = this.getObject();

      this.isndc = line.TestBit(kLineNDC);

      const use_frame = this.isndc ? false : new DrawOptions(this.getDrawOpt()).check('FRAME');

      this.createG(use_frame ? 'frame2d' : undefined);

      this.swap_xy = use_frame && this.getFramePainter()?.swap_xy();

      const func = this.getAxisToSvgFunc(this.isndc, true);

      this.x1 = func.x(line.fX1);
      this.y1 = func.y(line.fY1);
      this.x2 = func.x(line.fX2);
      this.y2 = func.y(line.fY2);

      if (this.swap_xy)
         [this.x1, this.y1, this.x2, this.y2] = [this.y1, this.x1, this.y2, this.x2];

      this.createAttLine({ attr: line });
   }

   /** @summary Create path */
   createPath() {
      const x1 = Math.round(this.x1), x2 = Math.round(this.x2), y1 = Math.round(this.y1), y2 = Math.round(this.y2);
      return `M${x1},${y1}` + (x1 === x2 ? `V${y2}` : (y1 === y2 ? `H${x2}` : `L${x2},${y2}`));
   }

   /** @summary Add extras - used for TArrow */
   addExtras() {}

   /** @summary Redraw line */
   redraw() {
      this.prepareDraw();

      const elem = this.appendPath(this.createPath())
                       .call(this.lineatt.func);

      if (this.getObject()?.$do_not_draw)
         elem.remove();
      else {
         this.addExtras(elem);
         addMoveHandler(this);
         assignContextMenu(this);
      }

      return this;
   }

   /** @summary Draw TLine object */
   static async draw(dom, obj, opt) {
      const painter = new TLinePainter(dom, obj, opt);
      return ensureTCanvas(painter, false).then(() => painter.redraw());
   }

} // class TLinePainter

var TLinePainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TLinePainter: TLinePainter
});

const k_upper_pad = 'upper_pad', k_lower_pad = 'lower_pad', k_top_pad = 'top_pad';


/**
 * @summary Painter class for TRatioPlot
 *
 * @private
 */

class TRatioPlotPainter extends ObjectPainter {

   #oldratio; // old ratio plot

   /** @summary Set grids range */
   setGridsRange(xmin, xmax, ymin, ymax, low_p) {
      const ratio = this.getObject();
      if (xmin === xmax) {
         const x_handle = this.getPadPainter()?.findPainterFor(ratio.fLowerPad, k_lower_pad, clTPad)?.getFramePainter()?.x_handle;
         if (!x_handle)
            return;
         if (xmin === 0) {
            // in case of unzoom full range should be used
            xmin = x_handle.full_min;
            xmax = x_handle.full_max;
         } else {
            // in case of y-scale zooming actual range has to be used
            xmin = x_handle.scale_min;
            xmax = x_handle.scale_max;
         }
      }
      ratio.fGridlines.forEach(line => {
         line.fX1 = xmin;
         line.fX2 = xmax;
      });
      const nlines = Math.min(ratio.fGridlines.length, ratio.fGridlinePositions.length);
      for (let i = 0; i < nlines; ++i) {
         const y = ratio.fGridlinePositions[i],
               line = ratio.fGridlines[i];
         if (ymin !== 'ignorey') {
            line.$do_not_draw = (ymin !== ymax) && ((y < ymin) || (y > ymax));
            line.fY1 = line.fY2 = y;
         }
         low_p?.findPainterFor(line)?.redraw();
      }
   }

   /** @summary Configure custom interactive handlers for ratio plot
    * @desc Should work for both new and old code */
   configureInteractive() {
      const ratio = this.getObject(),
            pp = this.getPadPainter(),
            up_p = pp.findPainterFor(ratio.fUpperPad, k_upper_pad, clTPad),
            up_fp = up_p?.getFramePainter(),
            low_p = pp.findPainterFor(ratio.fLowerPad, k_lower_pad, clTPad),
            low_fp = low_p?.getFramePainter();

      if (!up_p || !low_p)
         return;

      low_p.forEachPainterInPad(objp => {
         if (isFunc(objp?.testEditable))
            objp.testEditable(false);
      });

      this.setGridsRange(low_fp.scale_xmin, low_fp.scale_xmax, low_fp.scale_ymin, low_fp.scale_ymax, low_p);

      if (up_p._ratio_interactive && low_p._ratio_interactive)
         return;

      up_p._ratio_interactive = true;
      low_p._ratio_interactive = true;

      up_fp.o_zoom = up_fp.zoom;
      up_fp._ratio_low_fp = low_fp;
      up_fp._ratio_painter = this;

      up_fp.zoom = function(xmin, xmax, ymin, ymax, zmin, zmax) {
         return this.o_zoom(xmin, xmax, ymin, ymax, zmin, zmax).then(res => {
            this._ratio_painter.setGridsRange(up_fp.scale_xmin, up_fp.scale_xmax, 'ignory');
            return this._ratio_low_fp.o_zoom(up_fp.scale_xmin, up_fp.scale_xmax).then(() => res);
         });
      };

      up_fp.o_sizeChanged = up_fp.sizeChanged;
      up_fp.sizeChanged = function() {
         this.o_sizeChanged();
         this._ratio_low_fp.fX1NDC = this.fX1NDC;
         this._ratio_low_fp.fX2NDC = this.fX2NDC;
         this._ratio_low_fp.o_sizeChanged();
      };

      low_fp.o_zoom = low_fp.zoom;
      low_fp._ratio_up_fp = up_fp;
      low_fp._ratio_painter = this;

      low_fp.zoom = function(xmin, xmax, ymin, ymax, zmin, zmax) {
         if (xmin === xmax) {
            xmin = up_fp.xmin;
            xmax = up_fp.xmax;
         } else {
            xmin = Math.min(xmin, up_fp.xmin);
            xmax = Math.max(xmax, up_fp.xmax);
         }
         this._ratio_painter.setGridsRange(xmin, xmax, ymin, ymax);
         return this._ratio_up_fp.o_zoom(xmin, xmax).then(() => this.o_zoom(xmin, xmax, ymin, ymax, zmin, zmax));
      };

      low_fp.o_sizeChanged = low_fp.sizeChanged;
      low_fp.sizeChanged = function() {
         this.o_sizeChanged();
         this._ratio_up_fp.fX1NDC = this.fX1NDC;
         this._ratio_up_fp.fX2NDC = this.fX2NDC;
         this._ratio_up_fp.o_sizeChanged();
      };
   }

   /** @summary Redraw old TRatioPlot where object was in very end of list of primitives */
   async redrawOld() {
      const ratio = this.getObject(),
            pp = this.getPadPainter(),
            top_p = pp.findPainterFor(ratio.fTopPad, k_top_pad, clTPad),
            pad = pp.getRootPad(),
            mirrow_axis = (pad.fFrameFillStyle === 0) ? 1 : 0,
            tick_x = pad.fTickx || mirrow_axis,
            tick_y = pad.fTicky || mirrow_axis;

      top_p?.disablePadDrawing();

      const up_p = pp.findPainterFor(ratio.fUpperPad, k_upper_pad, clTPad),
            up_main = up_p?.getMainPainter(),
            up_fp = up_p?.getFramePainter(),
            low_p = pp.findPainterFor(ratio.fLowerPad, k_lower_pad, clTPad),
            low_main = low_p?.getMainPainter(),
            low_fp = low_p?.getFramePainter();
      let promise_up = Promise.resolve(true);

      if (up_p && up_main && up_fp && low_fp && !up_p._ratio_configured) {
         up_p._ratio_configured = true;

         up_main.options.Axis = 0; // draw both axes

         const h = up_main.getHisto();

         h.fYaxis.$use_top_pad = true; // workaround to use same scaling
         h.fXaxis.fLabelSize = 0; // do not draw X axis labels
         h.fXaxis.fTitle = ''; // do not draw X axis title

         up_p.getRootPad().fTickx = tick_x;
         up_p.getRootPad().fTicky = tick_y;

         promise_up = up_p.redrawPad();
      }

      return promise_up.then(() => {
         if (!low_p || !low_main || !low_fp || !up_fp || low_p._ratio_configured)
            return this;

         low_p._ratio_configured = true;
         low_main.options.Axis = 0; // draw both axes
         const h = low_main.getHisto();
         h.fXaxis.fTitle = 'x';

         h.fXaxis.$use_top_pad = true;
         h.fYaxis.$use_top_pad = true;
         low_p.getRootPad().fTickx = tick_x;
         low_p.getRootPad().fTicky = tick_y;

         const arr = [];

         // add missing lines in old ratio painter
         if (ratio.fGridlinePositions.length && (ratio.fGridlines.length < ratio.fGridlinePositions.length)) {
            ratio.fGridlinePositions.forEach(gridy => {
               let found = false;
               ratio.fGridlines.forEach(line => {
                  if ((line.fY1 === line.fY2) && (Math.abs(line.fY1 - gridy) < 1e-6))
                     found = true;
               });
               if (!found) {
                  const line = create$1(clTLine);
                  line.fX1 = up_fp.scale_xmin;
                  line.fX2 = up_fp.scale_xmax;
                  line.fY1 = line.fY2 = gridy;
                  line.fLineStyle = 2;
                  ratio.fGridlines.push(line);
                  arr.push(TLinePainter.draw(low_p, line));
               }
            });
         }

         return Promise.all(arr)
                       .then(() => low_fp.zoomSingle('x', up_fp.scale_xmin, up_fp.scale_xmax))
                       .then(changed => { return changed ? true : low_p.redrawPad(); })
                       .then(() => this);
      });
   }

   /** @summary Redraw TRatioPlot */
   async redraw() {
      const ratio = this.getObject(),
            pp = this.getPadPainter();

      if (this.#oldratio === undefined)
         this.#oldratio = Boolean(pp.findPainterFor(ratio.fTopPad, k_top_pad, clTPad));

      // configure ratio interactive at the end
      pp.$userInteractive = () => this.configureInteractive();

      if (this.#oldratio)
         return this.redrawOld();

      const pad = pp.getRootPad(),
            mirrow_axis = (pad.fFrameFillStyle === 0) ? 1 : 0,
            tick_x = pad.fTickx || mirrow_axis,
            tick_y = pad.fTicky || mirrow_axis;

      // do not draw primitives and pad itself
      ratio.fTopPad.$disable_drawing = true;

      ratio.fUpperPad.$ratio_pad = 'up'; // indicate drawing of the axes for main painter
      ratio.fUpperPad.fTickx = tick_x;
      ratio.fUpperPad.fTicky = tick_y;

      ratio.fLowerPad.$ratio_pad = 'low'; // indicate drawing of the axes for main painter
      ratio.fLowerPad.fTickx = tick_x;
      ratio.fLowerPad.fTicky = tick_y;

      return this;
   }

   /** @summary Draw TRatioPlot */
   static async draw(dom, ratio, opt) {
      const painter = new TRatioPlotPainter(dom, ratio, opt);
      return ensureTCanvas(painter, false).then(() => painter.redraw());
   }

} // class TRatioPlotPainter

var TRatioPlotPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TRatioPlotPainter: TRatioPlotPainter
});

const kResetHisto = BIT(17);

/**
 * @summary Painter for TMultiGraph object.
 *
 * @private
 */

let TMultiGraphPainter$2 = class TMultiGraphPainter extends ObjectPainter {

   #firstpainter; // first painter
   #painters; // array of sub-painters
   #funcs_handler; // special instance for functions drawing
   #restopt; // remaining part of draw options
   #auto; // extra options for auto colors
   #is3d; // if 3d drawing
   #pads;  // pads draw option

   /** @summary Create painter
     * @param {object|string} dom - DOM element for drawing or element id
     * @param {object} obj - TMultiGraph object to draw */
   constructor(dom, mgraph) {
      super(dom, mgraph);
      this.#firstpainter = null;
      this.#painters = []; // keep painters to be able update objects
   }

   /** @summary Cleanup TMultiGraph painter */
   cleanup() {
      this.#painters = [];
      this.#is3d = undefined;
      this.#pads = undefined;
      this.#auto = undefined;
      this.#restopt = undefined;
      super.cleanup();
   }

   /** @summary Return true if 3D drawing is used */
   is3d() { return this.#is3d; }

   /** @summary Update TMultiGraph object */
   updateObject(obj) {
      if (!this.matchObjectType(obj))
         return false;

      const mgraph = this.getObject(),
            graphs = obj.fGraphs,
            pp = this.getPadPainter();

      mgraph.fTitle = obj.fTitle;

      let isany = false;
      if (this.#firstpainter) {
         const histo = this.scanGraphsRange(graphs, obj.fHistogram, pp?.getRootPad(true), true);
         if (this.#firstpainter.updateObject(histo))
            isany = true;
      }

      const ngr = Math.min(graphs.arr.length, this.#painters.length);

      // TODO: handle changing number of graphs
      for (let i = 0; i < ngr; ++i) {
         if (this.#painters[i].updateObject(graphs.arr[i], (graphs.opt[i] || this.#restopt) + this.#auto))
            isany = true;
      }

      this.#funcs_handler = new FunctionsHandler(this, pp, obj.fFunctions);

      return isany;
   }

   /** @summary Redraw TMultiGraph
     * @desc may redraw histogram which was used to draw axes
     * @return {Promise} for ready */
   async redraw(reason) {
      const promise = this.#firstpainter?.redraw(reason) ?? Promise.resolve(true),
            redrawNext = async indx => {
               if (indx >= this.#painters.length)
                  return this;
               return this.#painters[indx].redraw(reason).then(() => redrawNext(indx + 1));
            };

      return promise.then(() => redrawNext(0)).then(() => {
         const res = this.#funcs_handler?.drawNext(0) ?? this;
         this.#funcs_handler = undefined;
         return res;
      });
   }

   /** @summary Scan graphs range
     * @return {object} histogram for axes drawing */
   scanGraphsRange(graphs, histo, pad, reset_histo) {
      const mgraph = this.getObject(),
            rw = { xmin: 0, xmax: 0, ymin: 0, ymax: 0, first: true },
            test = (v1, v2) => { return Math.abs(v2 - v1) < 1e-6; };
      let maximum, minimum, logx = false, logy = false,
          src_hist, dummy_histo = false;

      if (pad) {
         logx = pad.fLogx;
         logy = pad.fLogv ?? pad.fLogy;
      }

      // ignore existing histogram in 3d case
      if (this.is3d() && histo && !histo.fXaxis.fLabels)
         histo = null;

      if (!histo)
         src_hist = graphs.arr[0]?.fHistogram;
      else {
         dummy_histo = test(histo.fMinimum, -0.05) && test(histo.fMaximum, 1.05) &&
                       test(histo.fXaxis.fXmin, -0.05) && test(histo.fXaxis.fXmax, 1.05);
         src_hist = histo;
      }

      graphs.arr.forEach(gr => {
         if (gr.fNpoints === 0)
            return;
         if (gr.TestBit(kResetHisto))
            reset_histo = true;
         if (rw.first) {
            rw.xmin = rw.xmax = gr.fX[0];
            rw.ymin = rw.ymax = gr.fY[0];
            rw.first = false;
         }
         for (let i = 0; i < gr.fNpoints; ++i) {
            rw.xmin = Math.min(rw.xmin, gr.fX[i]);
            rw.xmax = Math.max(rw.xmax, gr.fX[i]);
            rw.ymin = Math.min(rw.ymin, gr.fY[i]);
            rw.ymax = Math.max(rw.ymax, gr.fY[i]);
         }
      });

      if (rw.xmin === rw.xmax)
         rw.xmax += 1;
      if (rw.ymin === rw.ymax)
         rw.ymax += 1;
      const dx = 0.05 * (rw.xmax - rw.xmin),
            dy = 0.05 * (rw.ymax - rw.ymin);

      let uxmin = rw.xmin - dx,
          uxmax = rw.xmax + dx;
      if (logy) {
         if (rw.ymin <= 0)
            rw.ymin = 0.001 * rw.ymax;
         minimum = rw.ymin / (1 + 0.5 * Math.log10(rw.ymax / rw.ymin));
         maximum = rw.ymax * (1 + 0.2 * Math.log10(rw.ymax / rw.ymin));
      } else {
         minimum = rw.ymin - dy;
         maximum = rw.ymax + dy;
      }
      if (minimum < 0 && rw.ymin >= 0)
         minimum = 0;
      if (maximum > 0 && rw.ymax <= 0)
         maximum = 0;

      const glob_minimum = minimum, glob_maximum = maximum;

      if (uxmin < 0 && rw.xmin >= 0)
         uxmin = logx ? 0.9 * rw.xmin : 0;
      if (uxmax > 0 && rw.xmax <= 0)
         uxmax = logx ? 1.1 * rw.xmax : 0;

      if (mgraph.fMinimum !== kNoZoom)
         rw.ymin = minimum = mgraph.fMinimum;
      if (mgraph.fMaximum !== kNoZoom)
         rw.ymax = maximum = mgraph.fMaximum;

      if (minimum < 0 && rw.ymin >= 0 && logy)
         minimum = 0.9 * rw.ymin;
      if (maximum > 0 && rw.ymax <= 0 && logy)
         maximum = 1.1 * rw.ymax;
      if (minimum <= 0 && logy)
         minimum = 0.001 * maximum;
      if (!logy && minimum > 0 && minimum < 0.05 * maximum)
         minimum = 0;
      if (uxmin <= 0 && logx)
         uxmin = (uxmax > 1000) ? 1 : 0.001 * uxmax;

      // Create a temporary histogram to draw the axis (if necessary)
      if (!histo || reset_histo || dummy_histo) {
         let xaxis, yaxis;
         if (this.is3d()) {
            histo = createHistogram(clTH2F, graphs.arr.length, 10);
            xaxis = histo.fXaxis;
            xaxis.fXmin = 0;
            xaxis.fXmax = graphs.arr.length;
            xaxis.fLabels = create$1(clTHashList);
            for (let i = 0; i < graphs.arr.length; i++) {
               const lbl = create$1(clTObjString);
               lbl.fString = graphs.arr[i].fTitle || `gr${i}`;
               lbl.fUniqueID = graphs.arr.length - i; // graphs drawn in reverse order
               xaxis.fLabels.Add(lbl, '');
            }
            xaxis = histo.fYaxis;
            yaxis = histo.fZaxis;
         } else {
            histo = createHistogram(src_hist?._typename ?? clTH1F, src_hist?.fXaxis.fNbins ?? 10);
            xaxis = histo.fXaxis;
            yaxis = histo.fYaxis;
         }

         if (src_hist) {
            Object.assign(xaxis, src_hist.fXaxis);
            yaxis.fTitle = src_hist.fYaxis.fTitle;
         }

         histo.fTitle = mgraph.fTitle;
         if (histo.fTitle.indexOf(';') >= 0) {
            const t = histo.fTitle.split(';');
            histo.fTitle = t[0];
            if (t[1])
               xaxis.fTitle = t[1];
            if (t[2])
               yaxis.fTitle = t[2];
         }
         if (!xaxis.fLabels) {
            xaxis.fXmin = uxmin;
            xaxis.fXmax = uxmax;
         }
      }

      const axis = this.is3d() ? histo.fZaxis : histo.fYaxis;
      axis.fXmin = Math.min(minimum, glob_minimum);
      axis.fXmax = Math.max(maximum, glob_maximum);
      if (histo.fMinimum === kNoZoom)
         histo.fMinimum = minimum;
      if (histo.fMaximum === kNoZoom)
         histo.fMaximum = maximum;
      histo.fBits |= kNoStats;

      return histo;
   }

   /** @summary draw special histogram for axis
     * @return {Promise} when ready */
   async drawAxisHist(histo, hopt) {
      return TH1Painter$2.draw(this.getDrawDom(), histo, hopt);
   }

   /** @summary Draw graph  */
   async drawGraph(dom, gr, opt /* , pos3d */) {
      return TGraphPainter$1.draw(dom, gr, opt);
   }

   /** @summary method draws next graph  */
   async drawNextGraph(indx, pad_painter) {
      const graphs = this.getObject().fGraphs;

      // at the end of graphs drawing draw functions (if any)
      if (indx >= graphs.arr.length)
         return this;

      const gr = graphs.arr[indx],
            draw_opt = (graphs.opt[indx] || this.#restopt) + this.#auto,
            pos3d = graphs.arr.length - indx,
            subid = `graphs_${indx}`;

      // handling of 'pads' draw option
      if (pad_painter) {
         const subpad_painter = pad_painter.getSubPadPainter(indx + 1);
         if (!subpad_painter)
            return this;

         subpad_painter.cleanPrimitives(true);

         return this.drawGraph(subpad_painter, gr, draw_opt, pos3d).then(subp => {
            if (subp) {
               subp.setSecondaryId(this, subid);
               this.#painters.push(subp);
            }
            return this.drawNextGraph(indx + 1, pad_painter);
         });
      }

      // used in automatic colors numbering
      if (this.#auto)
         gr.$num_graphs = graphs.arr.length;

      return this.drawGraph(this.getPadPainter(), gr, draw_opt, pos3d).then(subp => {
         if (subp) {
            subp.setSecondaryId(this, subid);
            this.#painters.push(subp);
         }
         return this.drawNextGraph(indx + 1);
      });
   }

   /** @summary Fill TMultiGraph context menu */
   fillContextMenuItems(menu) {
      menu.addRedrawMenu(this);
   }

   /** @summary Redraw TMultiGraph object using provided option
     * @private */
   async redrawWith(opt, skip_cleanup) {
      if (!skip_cleanup) {
         this.#firstpainter = null;
         this.#painters = [];
         const pp = this.getPadPainter();
         pp?.removePrimitive(this, true);
         if (this.#pads)
            pp?.divide(0, 0);
      }

      const d = new DrawOptions(opt),
            mgraph = this.getObject();

      this.#is3d = d.check('3D');
      this.#auto = '';
      this.#pads = d.check('PADS');
      ['PFC', 'PLC', 'PMC'].forEach(f => {
         if (d.check(f))
            this.#auto += ' ' + f;
      });

      let hopt = '', pad_painter = null;
      if (d.check('FB') && this.is3d())
         hopt += 'FB'; // will be directly combined with LEGO
      PadDrawOptions.forEach(name => {
         if (d.check(name))
            hopt += ';' + name;
      });

      this.#restopt = d.remain();

      let promise = Promise.resolve(true);
      if (this.#pads) {
         promise = ensureTCanvas(this, false).then(() => {
            pad_painter = this.getPadPainter();
            return pad_painter.divide(mgraph.fGraphs.arr.length, 0, true);
         });
      } else if (d.check('A') || !this.getMainPainter()) {
         const histo = this.scanGraphsRange(mgraph.fGraphs, mgraph.fHistogram, this.getPadPainter()?.getRootPad(true));
         promise = this.drawAxisHist(histo, hopt).then(ap => {
            ap.setSecondaryId(this, 'hist'); // mark that axis painter generated from mg
            this.#firstpainter = ap;
         });
      }

      return promise.then(() => {
         this.addToPadPrimitives();
         return this.drawNextGraph(0, pad_painter);
      }).then(() => {
         if (this.#pads)
            return this;
         const handler = new FunctionsHandler(this, this.getPadPainter(), this.getObject().fFunctions, true);
         return handler.drawNext(0); // returns painter
      });
   }

   /** @summary Draw TMultiGraph object in 2D only */
   static async draw(dom, mgraph, opt) {
      const painter = new TMultiGraphPainter(dom, mgraph, opt);
      return painter.redrawWith(opt, true);
   }

}; // class TMultiGraphPainter

class TMultiGraphPainter extends TMultiGraphPainter$2 {

   /** @summary draw special histogram for axis
     * @return {Promise} when ready */
   async drawAxisHist(histo, hopt) {
      const dom = this.getDrawDom();
      return this.is3d() ? TH2Painter.draw(dom, histo, 'LEGO' + hopt)
                         : TH1Painter$2.draw(dom, histo, hopt);
   }

   /** @summary draw multi graph in 3D */
   async drawGraph(dom, gr, opt, pos3d) {
      if (this.is3d())
         opt += `pos3d_${pos3d}`;
      return TGraphPainter.draw(dom, gr, opt);
   }

   /** @summary Draw TMultiGraph object */
   static async draw(dom, mgraph, opt) {
      const painter = new TMultiGraphPainter(dom, mgraph, opt);
      return painter.redrawWith(opt, true);
   }

} // class TMultiGraphPainter

var TMultiGraphPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TMultiGraphPainter: TMultiGraphPainter
});

/**
 * @summary Painter for TWebPainting classes.
 *
 * @private
 */

class TWebPaintingPainter extends ObjectPainter {

   /** @summary Update TWebPainting object */
   updateObject(obj) {
      if (!this.matchObjectType(obj))
         return false;
      this.assignObject(obj);
      return true;
   }

   /** @summary Provides menu header */
   getMenuHeader() {
      return this.getObject()?.fClassName || 'TWebPainting';
   }

   /** @summary Fill context menu
    * @desc Create only header, items will be requested from server */
   fillContextMenu(menu) {
      const cl = this.getMenuHeader();
      menu.header(cl, `${urlClassPrefix}${cl}.html`);
      return true;
   }

   /** @summary Mouse click handler
    * @desc Redirect mouse click events to the ROOT application
    * @private */
   handleMouseClick(evnt) {
      const pp = this.getPadPainter(),
            rect = pp?.getPadRect();

      if (pp && rect && this.getSnapId()) {
         const pos = pointer(evnt, this.getG().node());
         pp.selectObjectPainter(this, { x: pos[0] + rect.x, y: pos[1] + rect.y });
      }
   }

   /** @summary draw TWebPainting object */
   async redraw() {
      const obj = this.getObject(), func = this.getAxisToSvgFunc();

      if (!obj?.fOper || !func)
         return this;

      let indx = 0, attr = {}, lastpath = null, lastkind = 'none', d = '',
          oper, npoints, n;

      /* eslint-disable one-var */
      const g = this.createG(),
            arr = obj.fOper.split(';');
      const check_attributes = kind => {
         if (kind === lastkind)
            return;

         if (lastpath) {
            lastpath.attr('d', d); // flush previous
            d = '';
            lastpath = null;
            lastkind = 'none';
         }

         if (!kind)
            return;

         lastkind = kind;
         lastpath = g.append('svg:path').attr('d', ''); // placeholder for 'd' to have it always in front
         switch (kind) {
            case 'f':
               lastpath.call(this.fillatt.func);
               break;
            case 'l':
               lastpath.call(this.lineatt.func).style('fill', 'none');
               break;
            case 'm':
               lastpath.call(this.markeratt.func);
               break;
         }
      };
      const read_attr = (str, names) => {
         let lastp = 0;
         const obj2 = { _typename: 'any' };
         for (let k = 0; k < names.length; ++k) {
            const p = str.indexOf(':', lastp + 1);
            obj2[names[k]] = parseInt(str.slice(lastp + 1, p > lastp ? p : undefined));
            lastp = p;
         }
         return obj2;
      };
      const process = k => {
         while (++k < arr.length) {
            oper = arr[k][0];
            switch (oper) {
               case 'z':
                  this.createAttLine({ attr: read_attr(arr[k], ['fLineColor', 'fLineStyle', 'fLineWidth']), force: true });
                  check_attributes();
                  continue;
               case 'y':
                  this.createAttFill({ attr: read_attr(arr[k], ['fFillColor', 'fFillStyle']), force: true });
                  check_attributes();
                  continue;
               case 'x':
                  this.createAttMarker({ attr: read_attr(arr[k], ['fMarkerColor', 'fMarkerStyle', 'fMarkerSize']), force: true });
                  check_attributes();
                  continue;
               case 'o':
                  attr = read_attr(arr[k], ['fTextColor', 'fTextFont', 'fTextSize', 'fTextAlign', 'fTextAngle']);
                  if (attr.fTextSize < 0)
                     attr.fTextSize *= -1e-3;
                  check_attributes();
                  continue;
               case 'r':
               case 'b': {
                  check_attributes((oper === 'b') ? 'f' : 'l');

                  const x1 = func.x(obj.fBuf[indx++]),
                        y1 = func.y(obj.fBuf[indx++]),
                        x2 = func.x(obj.fBuf[indx++]),
                        y2 = func.y(obj.fBuf[indx++]);

                  d += `M${x1},${y1}h${x2 - x1}v${y2 - y1}h${x1 - x2}z`;
                  continue;
               }
               case 'l':
               case 'f': {
                  check_attributes(oper);

                  npoints = parseInt(arr[k].slice(1));

                  for (n = 0; n < npoints; ++n)
                     d += `${(n > 0) ? 'L' : 'M'}${func.x(obj.fBuf[indx++])},${func.y(obj.fBuf[indx++])}`;

                  if (oper === 'f')
                     d += 'Z';

                  continue;
               }

               case 'm': {
                  check_attributes(oper);

                  npoints = parseInt(arr[k].slice(1));

                  this.markeratt.resetPos();
                  for (n = 0; n < npoints; ++n)
                     d += this.markeratt.create(func.x(obj.fBuf[indx++]), func.y(obj.fBuf[indx++]));

                  continue;
               }

               case 'h':
               case 't': {
                  if (attr.fTextSize) {
                     check_attributes();

                     const height = (attr.fTextSize > 1) ? attr.fTextSize : this.getPadPainter().getPadHeight() * attr.fTextSize,
                           group = g.append('svg:g');

                     return this.startTextDrawingAsync(attr.fTextFont, height, group).then(() => {
                        let text = arr[k].slice(1),
                            angle = attr.fTextAngle;
                        if (angle >= 360)
                           angle -= Math.floor(angle / 360) * 360;

                        if (oper === 'h') {
                           let res = '';
                           for (n = 0; n < text.length; n += 2)
                              res += String.fromCharCode(parseInt(text.slice(n, n + 2), 16));
                           text = res;
                        }

                        // todo - correct support of angle
                        this.drawText({
                           align: attr.fTextAlign,
                           x: func.x(obj.fBuf[indx++]),
                           y: func.y(obj.fBuf[indx++]),
                           rotate: -angle,
                           text,
                           color: getColor(attr.fTextColor),
                           latex: 0, draw_g: group
                        });

                        return this.finishTextDrawing(group);
                     }).then(() => process(k));
                  }
                  continue;
               }

               default:
                  console.log(`unsupported operation ${oper}`);
            }
         }

         return Promise.resolve(true);
      };


      return process(-1).then(() => {
         check_attributes();
         assignContextMenu(this);
         if (!this.isBatchMode())
            g.on('click', evnt => this.handleMouseClick(evnt));
         return this;
      });
   }

   static async draw(dom, obj) {
      const painter = new TWebPaintingPainter(dom, obj);
      painter.addToPadPrimitives();
      return painter.redraw();
   }

} // class TWebPaintingPainter

var TWebPaintingPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TWebPaintingPainter: TWebPaintingPainter
});

/**
  * @summary Painter for TF2 object
  *
  * @private
  */

class TF2Painter extends TH2Painter {

   #use_saved_points; // use saved points for drawing
   #func; // func object
   #fail_eval; // fail evaluation of function

   /** @summary Assign function  */
   setFunc(f) { this.#func = f; }

   /** @summary Returns drawn object name */
   getObjectName() { return this.#func?.fName ?? 'func'; }

   /** @summary Returns drawn object class name */
   getClassName() { return this.#func?._typename ?? clTF2; }

   /** @summary Returns true while function is drawn */
   isTF1() { return true; }

   /** @summary Returns primary function which was then drawn as histogram */
   getPrimaryObject() { return this.#func; }

   /** @summary Update histogram */
   updateObject(obj /* , opt */) {
      if (!obj || (this.getClassName() !== obj._typename))
         return false;
      delete obj.evalPar;
      const histo = this.getHisto();

      if (this._webcanv_hist) {
         const h0 = this.getPadPainter()?.findInPrimitives('Func', clTH2F);
         if (h0)
            this.updateAxes(histo, h0, this.getFramePainter());
      }

      this.setFunc(obj);
      this.createTF2Histogram(obj, histo);
      this.scanContent();
      return true;
   }

   /** @summary Redraw TF2
     * @private */
   redraw(reason) {
      if (!this.#use_saved_points && (reason === 'logx' || reason === 'logy' || reason === 'zoom')) {
         this.createTF2Histogram(this.#func, this.getHisto());
         this.scanContent();
      }

      return super.redraw(reason);
   }

   /** @summary Create histogram for TF2 drawing
     * @private */
   createTF2Histogram(func, hist) {
      let nsave = func.fSave.length - 6;
      if ((nsave > 0) && (nsave !== (func.fSave[nsave + 4] + 1) * (func.fSave[nsave + 5] + 1)))
         nsave = 0;

      this.#use_saved_points = (nsave > 0) && (settings.PreferSavedPoints || (this._use_saved > 1));

      const fp = this.getFramePainter(),
            pad = this.getPadPainter()?.getRootPad(true),
            logx = pad?.fLogx, logy = pad?.fLogy,
            gr = fp?.getGrFuncs(this.second_x, this.second_y);
      let xmin = func.fXmin, xmax = func.fXmax,
          ymin = func.fYmin, ymax = func.fYmax,
          npx = Math.max(func.fNpx, 20),
          npy = Math.max(func.fNpy, 20);

      if (gr?.zoom_xmin !== gr?.zoom_xmax) {
         const dx = (xmax - xmin) / npx;
         if ((xmin < gr.zoom_xmin) && (gr.zoom_xmin < xmax))
            xmin = Math.max(xmin, gr.zoom_xmin - dx);
         if ((xmin < gr.zoom_xmax) && (gr.zoom_xmax < xmax))
            xmax = Math.min(xmax, gr.zoom_xmax + dx);
      }

      if (gr?.zoom_ymin !== gr?.zoom_ymax) {
         const dy = (ymax - ymin) / npy;
         if ((ymin < gr.zoom_ymin) && (gr.zoom_ymin < ymax))
            ymin = Math.max(ymin, gr.zoom_ymin - dy);
         if ((ymin < gr.zoom_ymax) && (gr.zoom_ymax < ymax))
            ymax = Math.min(ymax, gr.zoom_ymax + dy);
      }

      const ensureBins = (nx, ny) => {
         if (hist.fNcells !== (nx + 2) * (ny + 2)) {
            hist.fNcells = (nx + 2) * (ny + 2);
            hist.fArray = new Float32Array(hist.fNcells);
         }
         hist.fArray.fill(0);
         hist.fXaxis.fNbins = nx;
         hist.fXaxis.fXbins = [];
         hist.fYaxis.fNbins = ny;
         hist.fYaxis.fXbins = [];
      };

      this.#fail_eval = undefined;

      if (!this.#use_saved_points) {
         let iserror = false;

         if (!func.evalPar && !proivdeEvalPar(func))
            iserror = true;

         ensureBins(npx, npy);
         hist.fXaxis.fXmin = xmin;
         hist.fXaxis.fXmax = xmax;
         hist.fYaxis.fXmin = ymin;
         hist.fYaxis.fXmax = ymax;

         if (logx)
            produceTAxisLogScale(hist.fXaxis, npx, xmin, xmax);
         if (logy)
            produceTAxisLogScale(hist.fYaxis, npy, ymin, ymax);

         for (let j = 0; (j < npy) && !iserror; ++j) {
            for (let i = 0; (i < npx) && !iserror; ++i) {
               const x = hist.fXaxis.GetBinCenter(i + 1),
                     y = hist.fYaxis.GetBinCenter(j + 1);
               let z = 0;

               try {
                  z = func.evalPar(x, y);
               } catch {
                  iserror = true;
               }

               if (!iserror)
                  hist.setBinContent(hist.getBin(i + 1, j + 1), Number.isFinite(z) ? z : 0);
            }
         }

         if (iserror)
            this.#fail_eval = true;

         if (iserror && (nsave > 6))
            this.#use_saved_points = true;
      }

      if (this.#use_saved_points) {
         npx = Math.round(func.fSave[nsave + 4]);
         npy = Math.round(func.fSave[nsave + 5]);
         xmin = func.fSave[nsave];
         xmax = func.fSave[nsave + 1];
         ymin = func.fSave[nsave + 2];
         ymax = func.fSave[nsave + 3];
         const dx = (xmax - xmin) / npx, dy = (ymax - ymin) / npy, getSave = (x, y) => {
            if (x < xmin || x > xmax || dx <= 0)
               return 0;
            if (y < ymin || y > ymax || dy <= 0)
               return 0;
            const ibin = Math.min(npx - 1, Math.floor((x - xmin) / dx)),
                  jbin = Math.min(npy - 1, Math.floor((y - ymin) / dy)),
                  xlow = xmin + ibin * dx,
                  ylow = ymin + jbin * dy,
                  t = (x - xlow) / dx,
                  u = (y - ylow) / dy,
                  k1 = jbin * (npx + 1) + ibin,
                  k2 = jbin * (npx + 1) + ibin + 1,
                  k3 = (jbin + 1) * (npx + 1) + ibin + 1,
                  k4 = (jbin + 1) * (npx + 1) + ibin;
            return (1 - t) * (1 - u) * func.fSave[k1] + t * (1 - u) * func.fSave[k2] + t * u * func.fSave[k3] + (1 - t) * u * func.fSave[k4];
         };

         ensureBins(func.fNpx, func.fNpy);
         hist.fXaxis.fXmin = func.fXmin;
         hist.fXaxis.fXmax = func.fXmax;
         hist.fYaxis.fXmin = func.fYmin;
         hist.fYaxis.fXmax = func.fYmax;

         for (let j = 0; j < func.fNpy; ++j) {
            const y = hist.fYaxis.GetBinCenter(j + 1);
            for (let i = 0; i < func.fNpx; ++i) {
               const x = hist.fXaxis.GetBinCenter(i + 1),
                     z = getSave(x, y);
               hist.setBinContent(hist.getBin(i + 1, j + 1), Number.isFinite(z) ? z : 0);
            }
         }
      }

      hist.fName = 'Func';
      setHistogramTitle(hist, func.fTitle);
      hist.fMinimum = func.fMinimum;
      hist.fMaximum = func.fMaximum;
      // fHistogram->SetContour(fContour.fN, levels);
      hist.fLineColor = func.fLineColor;
      hist.fLineStyle = func.fLineStyle;
      hist.fLineWidth = func.fLineWidth;
      hist.fFillColor = func.fFillColor;
      hist.fFillStyle = func.fFillStyle;
      hist.fMarkerColor = func.fMarkerColor;
      hist.fMarkerStyle = func.fMarkerStyle;
      hist.fMarkerSize = func.fMarkerSize;
      hist.fBits |= kNoStats;

      return hist;
   }

   /** @summary Extract function ranges */
   extractAxesProperties(ndim) {
      super.extractAxesProperties(ndim);

      const func = this.#func, nsave = func?.fSave.length ?? 0;

      if (nsave > 6 && this.#use_saved_points) {
         this.xmin = Math.min(this.xmin, func.fSave[nsave - 6]);
         this.xmax = Math.max(this.xmax, func.fSave[nsave - 5]);
         this.ymin = Math.min(this.ymin, func.fSave[nsave - 4]);
         this.ymax = Math.max(this.ymax, func.fSave[nsave - 3]);
      }
      if (func) {
         this.xmin = Math.min(this.xmin, func.fXmin);
         this.xmax = Math.max(this.xmax, func.fXmax);
         this.ymin = Math.min(this.ymin, func.fYmin);
         this.ymax = Math.max(this.ymax, func.fYmax);
      }
   }

   /** @summary return tooltips for TF2 */
   getTF2Tooltips(pnt) {
      const lines = [this.getObjectHint()],
            o = this.getOptions(),
            funcs = this.getFramePainter()?.getGrFuncs(o.second_x, o.second_y);

      if (!funcs || !isFunc(this.#func?.evalPar)) {
         lines.push('grx = ' + pnt.x, 'gry = ' + pnt.y);
         return lines;
      }

      const x = funcs.revertAxis('x', pnt.x),
            y = funcs.revertAxis('y', pnt.y);
      let z = 0, iserror = false;

      try {
         z = this.#func.evalPar(x, y);
      } catch {
         iserror = true;
      }

      lines.push('x = ' + funcs.axisAsText('x', x),
                 'y = ' + funcs.axisAsText('y', y),
                 'value = ' + (iserror ? '<fail>' : floatToString(z, gStyle.fStatFormat)));
      return lines;
   }

   /** @summary process tooltip event for TF2 object */
   processTooltipEvent(pnt) {
      if (this.#use_saved_points)
         return super.processTooltipEvent(pnt);

      let ttrect = this.getG()?.selectChild('.tooltip_bin');

      if (!this.getG() || !pnt) {
         ttrect?.remove();
         return null;
      }

      const res = {
         name: this.#func?.fName, title: this.#func?.fTitle,
         x: pnt.x, y: pnt.y,
         color1: this.lineatt?.color ?? 'green',
         color2: this.fillatt?.getFillColorAlt('blue') ?? 'blue',
         lines: this.getTF2Tooltips(pnt), exact: true, menu: true
      };

      if (pnt.disabled)
         ttrect.remove();
      else {
         if (ttrect.empty()) {
            ttrect = this.getG().append('svg:circle')
                                .attr('class', 'tooltip_bin')
                                .style('pointer-events', 'none')
                                .style('fill', 'none')
                                .attr('r', (this.lineatt?.width ?? 1) + 4);
         }

         ttrect.attr('cx', pnt.x)
               .attr('cy', pnt.y);
         if (this.lineatt)
            ttrect.call(this.lineatt.func);
      }

      return res;
   }

   /** @summary fill information for TWebCanvas
     * @desc Used to inform web canvas when evaluation failed
     * @private */
   fillWebObjectOptions(opt) {
      opt.fcust = this.#fail_eval && !this._use_saved ? 'func_fail' : '';
   }

   /** @summary draw TF2 object */
   static async draw(dom, tf2, opt) {
      const web = scanTF1Options(opt);
      opt = web.opt;
      delete web.opt;

      const d = new DrawOptions(opt);
      if (d.empty())
         opt = 'cont3';
      else if (d.opt === 'SAME')
         opt = 'cont2 same';

      let hist;

      if (web._webcanv_hist)
         hist = getElementPadPainter(dom)?.findInPrimitives('Func', clTH2F);

      if (!hist) {
         hist = createHistogram(clTH2F, 20, 20);
         hist.fBits |= kNoStats;
      }

      const painter = new TF2Painter(dom, hist);

      painter.setFunc(tf2);
      Object.assign(painter, web);
      painter.createTF2Histogram(tf2, hist);

      return THistPainter._drawHist(painter, opt);
   }

} // class TF2Painter

var TF2Painter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TF2Painter: TF2Painter
});

function findZValue(arrz, arrv, cross = 0) {
   for (let i = arrz.length - 2; i >= 0; --i) {
      const v1 = arrv[i], v2 = arrv[i + 1],
            z1 = arrz[i], z2 = arrz[i + 1];
      if (v1 === cross)
         return z1;
      if (v2 === cross)
         return z2;
      if ((v1 < cross) !== (v2 < cross))
         return z1 + (cross - v1) / (v2 - v1) * (z2 - z1);
   }
   return arrz[0] - 1;
}


/**
  * @summary Painter for TF3 object
  *
  * @private
  */

class TF3Painter extends TH2Painter {

   #use_saved_points; // use saved points for drawing
   #func; // func object
   #fail_eval; // fail evaluation of function

   /** @summary Assign function  */
   setFunc(f) { this.#func = f; }

   /** @summary Returns drawn object name */
   getObjectName() { return this.#func?.fName ?? 'func'; }

   /** @summary Returns drawn object class name */
   getClassName() { return this.#func?._typename ?? clTF3; }

   /** @summary Returns true while function is drawn */
   isTF1() { return true; }

   /** @summary Returns primary function which was then drawn as histogram */
   getPrimaryObject() { return this.#func; }

   /** @summary Update histogram */
   updateObject(obj /* , opt */) {
      if (!obj || (this.getClassName() !== obj._typename))
         return false;
      delete obj.evalPar;
      const histo = this.getHisto();

      if (this._webcanv_hist) {
         const h0 = this.getPadPainter()?.findInPrimitives('Func', clTH2F);
         if (h0)
            this.updateAxes(histo, h0, this.getFramePainter());
      }

      this.setFunc(obj);
      this.createTF3Histogram(obj, histo);
      this.scanContent();
      return true;
   }

   /** @summary Redraw TF2
     * @private */
   redraw(reason) {
      if (!this.#use_saved_points && (reason === 'logx' || reason === 'logy' || reason === 'logy' || reason === 'zoom')) {
         this.createTF3Histogram(this.#func, this.getHisto());
         this.scanContent();
      }

      return super.redraw(reason);
   }

   /** @summary Create histogram for TF3 drawing
     * @private */
   createTF3Histogram(func, hist) {
      const nsave = func.fSave.length - 9;

      this.#use_saved_points = (nsave > 0) && (settings.PreferSavedPoints || (this._use_saved > 1));

      const fp = this.getFramePainter(),
            pad = this.getPadPainter()?.getRootPad(true),
            logx = pad?.fLogx, logy = pad?.fLogy,
            gr = fp?.getGrFuncs(this.second_x, this.second_y);
      let xmin = func.fXmin, xmax = func.fXmax,
          ymin = func.fYmin, ymax = func.fYmax,
          zmin = func.fZmin, zmax = func.fZmax,
          npx = Math.max(func.fNpx, 20),
          npy = Math.max(func.fNpy, 20),
          npz = Math.max(func.fNpz, 20);

      if (gr?.zoom_xmin !== gr?.zoom_xmax) {
         const dx = (xmax - xmin) / npx;
         if ((xmin < gr.zoom_xmin) && (gr.zoom_xmin < xmax))
            xmin = Math.max(xmin, gr.zoom_xmin - dx);
         if ((xmin < gr.zoom_xmax) && (gr.zoom_xmax < xmax))
            xmax = Math.min(xmax, gr.zoom_xmax + dx);
      }

      if (gr?.zoom_ymin !== gr?.zoom_ymax) {
         const dy = (ymax - ymin) / npy;
         if ((ymin < gr.zoom_ymin) && (gr.zoom_ymin < ymax))
            ymin = Math.max(ymin, gr.zoom_ymin - dy);
         if ((ymin < gr.zoom_ymax) && (gr.zoom_ymax < ymax))
            ymax = Math.min(ymax, gr.zoom_ymax + dy);
      }

      if (gr?.zoom_zmin !== gr?.zoom_zmax) {
         // no need for dz here - TH2 is not binned over Z axis
         if ((zmin < gr.zoom_zmin) && (gr.zoom_zmin < zmax))
            zmin = gr.zoom_zmin;
         if ((zmin < gr.zoom_zmax) && (gr.zoom_zmax < zmax))
            zmax = gr.zoom_zmax;
      }

      const ensureBins = (nx, ny) => {
         if (hist.fNcells !== (nx + 2) * (ny + 2)) {
            hist.fNcells = (nx + 2) * (ny + 2);
            hist.fArray = new Float32Array(hist.fNcells);
         }
         hist.fArray.fill(0);
         hist.fXaxis.fNbins = nx;
         hist.fXaxis.fXbins = [];
         hist.fYaxis.fNbins = ny;
         hist.fYaxis.fXbins = [];
         hist.fXaxis.fXmin = xmin;
         hist.fXaxis.fXmax = xmax;
         hist.fYaxis.fXmin = ymin;
         hist.fYaxis.fXmax = ymax;
         hist.fMinimum = zmin;
         hist.fMaximum = zmax;
      };

      this.#fail_eval = undefined;

      if (!this.#use_saved_points) {
         let iserror = false;

         if (!func.evalPar && !proivdeEvalPar(func))
            iserror = true;

         ensureBins(npx, npy);

         if (logx)
            produceTAxisLogScale(hist.fXaxis, npx, xmin, xmax);
         if (logy)
            produceTAxisLogScale(hist.fYaxis, npy, ymin, ymax);

         const arrv = new Array(npz), arrz = new Array(npz);
         for (let k = 0; k < npz; ++k)
            arrz[k] = zmin + k / (npz - 1) * (zmax - zmin);

         for (let j = 0; (j < npy) && !iserror; ++j) {
            for (let i = 0; (i < npx) && !iserror; ++i) {
               const x = hist.fXaxis.GetBinCenter(i + 1),
                     y = hist.fYaxis.GetBinCenter(j + 1);
               let z = 0;

               try {
                  for (let k = 0; k < npz; ++k)
                     arrv[k] = func.evalPar(x, y, arrz[k]);

                  z = findZValue(arrz, arrv);
               } catch {
                  iserror = true;
               }

               if (!iserror)
                  hist.setBinContent(hist.getBin(i + 1, j + 1), Number.isFinite(z) ? z : 0);
            }
         }

         if (iserror)
            this.#fail_eval = true;

         if (iserror && (nsave > 0))
            this.#use_saved_points = true;
      }

      if (this.#use_saved_points) {
         xmin = func.fSave[nsave];
         xmax = func.fSave[nsave + 1];
         ymin = func.fSave[nsave + 2];
         ymax = func.fSave[nsave + 3];
         zmin = func.fSave[nsave + 4];
         zmax = func.fSave[nsave + 5];
         npx = Math.round(func.fSave[nsave + 6]);
         npy = Math.round(func.fSave[nsave + 7]);
         npz = Math.round(func.fSave[nsave + 8]);

         const dz = (zmax - zmin) / npz;

         ensureBins(npx + 1, npy + 1);

         const arrv = new Array(npz + 1),
               arrz = new Array(npz + 1);
         for (let k = 0; k <= npz; k++)
            arrz[k] = zmin + k * dz;

         for (let i = 0; i <= npx; ++i) {
            for (let j = 0; j <= npy; ++j) {
               for (let k = 0; k <= npz; k++)
                  arrv[k] = func.fSave[i + (npx + 1) * (j + (npy + 1) * k)];
               const z = findZValue(arrz, arrv);
               hist.setBinContent(hist.getBin(i + 1, j + 1), Number.isFinite(z) ? z : 0);
            }
         }
      }

      hist.fName = 'Func';
      setHistogramTitle(hist, func.fTitle);


      // hist.fMinimum = func.fMinimum;
      // hist.fMaximum = func.fMaximum;
      // fHistogram->SetContour(fContour.fN, levels);
      hist.fLineColor = func.fLineColor;
      hist.fLineStyle = func.fLineStyle;
      hist.fLineWidth = func.fLineWidth;
      hist.fFillColor = func.fFillColor;
      hist.fFillStyle = func.fFillStyle;
      hist.fMarkerColor = func.fMarkerColor;
      hist.fMarkerStyle = func.fMarkerStyle;
      hist.fMarkerSize = func.fMarkerSize;
      hist.fBits |= kNoStats;

      return hist;
   }

   /** @summary Extract function ranges */
   extractAxesProperties(ndim) {
      super.extractAxesProperties(ndim);

      const func = this.#func, nsave = func?.fSave.length ?? 0;

      if (nsave > 9 && this.#use_saved_points) {
         this.xmin = Math.min(this.xmin, func.fSave[nsave - 9]);
         this.xmax = Math.max(this.xmax, func.fSave[nsave - 8]);
         this.ymin = Math.min(this.ymin, func.fSave[nsave - 7]);
         this.ymax = Math.max(this.ymax, func.fSave[nsave - 6]);
         this.zmin = Math.min(this.zmin, func.fSave[nsave - 5]);
         this.zmax = Math.max(this.zmax, func.fSave[nsave - 4]);
      }
      if (func) {
         this.xmin = Math.min(this.xmin, func.fXmin);
         this.xmax = Math.max(this.xmax, func.fXmax);
         this.ymin = Math.min(this.ymin, func.fYmin);
         this.ymax = Math.max(this.ymax, func.fYmax);
         this.zmin = Math.min(this.zmin, func.fZmin);
         this.zmax = Math.max(this.zmax, func.fZmax);
      }
   }

   /** @summary fill information for TWebCanvas
     * @desc Used to inform web canvas when evaluation failed
     * @private */
   fillWebObjectOptions(opt) {
      opt.fcust = this.#fail_eval && !this._use_saved ? 'func_fail' : '';
   }

   /** @summary draw TF3 object */
   static async draw(dom, tf3, opt) {
      const web = scanTF1Options(opt);
      opt = web.opt;
      delete web.opt;

      const d = new DrawOptions(opt);
      if (d.empty() || (opt === 'gl'))
         opt = 'surf1';
      else if (d.opt === 'SAME')
         opt = 'surf1 same';

      let hist;

      if (web._webcanv_hist)
         hist = getElementPadPainter(dom)?.findInPrimitives('Func', clTH2F);

      if (!hist) {
         hist = createHistogram(clTH2F, 20, 20);
         hist.fBits |= kNoStats;
      }

      const painter = new TF3Painter(dom, hist);

      painter.setFunc(tf3, clTF3);
      Object.assign(painter, web);
      painter.createTF3Histogram(tf3, hist);
      return THistPainter._drawHist(painter, opt);
   }

} // class TF3Painter

var TF3Painter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TF3Painter: TF3Painter
});

/**
 * @summary Painter for TSpline classes.
 *
 * @private
 */

class TSplinePainter extends ObjectPainter {

   #knot_size; // graphical size of each knot

   /** @summary Update TSpline object
     * @private */
   updateObject(obj, opt) {
      const spline = this.getObject();

      if (spline._typename !== obj._typename)
         return false;

      if (spline !== obj)
         Object.assign(spline, obj);

      if (opt !== undefined)
         this.decodeOptions(opt);

      return true;
   }

   /** @summary Evaluate spline at given position
     * @private */
   eval(knot, x) {
      const dx = x - knot.fX;

      if (knot._typename === 'TSplinePoly3')
         return knot.fY + dx * (knot.fB + dx * (knot.fC + dx * knot.fD));

      if (knot._typename === 'TSplinePoly5')
         return knot.fY + dx * (knot.fB + dx * (knot.fC + dx * (knot.fD + dx * (knot.fE + dx * knot.fF))));

      return knot.fY + dx;
   }

   /** @summary Find idex for x value
     * @private */
   findX(x) {
      const spline = this.getObject();
      let klow = 0, khig = spline.fNp - 1;

      if (x <= spline.fXmin)
         return klow;
      if (x >= spline.fXmax)
         return khig;

      if (spline.fKstep) {
         // Equidistant knots, use histogram
         klow = Math.round((x - spline.fXmin) / spline.fDelta);
         // Correction for rounding errors
         if (x < spline.fPoly[klow].fX)
            klow = Math.max(klow - 1, 0);
         else if ((klow < khig) && (x > spline.fPoly[klow + 1].fX))
            ++klow;
      } else {
         // Non equidistant knots, binary search
         while (khig - klow > 1) {
            const khalf = Math.round((klow + khig) / 2);
            if (x > spline.fPoly[khalf].fX)
               klow = khalf;
            else
               khig = khalf;
         }
      }
      return klow;
   }

   /** @summary Create histogram for axes drawing
     * @private */
   createDummyHisto() {
      const spline = this.getObject();
      let xmin = 0, xmax = 1, ymin = 0, ymax = 1;

      if (spline.fPoly) {
         xmin = xmax = spline.fPoly[0].fX;
         ymin = ymax = spline.fPoly[0].fY;

         spline.fPoly.forEach(knot => {
            xmin = Math.min(knot.fX, xmin);
            xmax = Math.max(knot.fX, xmax);
            ymin = Math.min(knot.fY, ymin);
            ymax = Math.max(knot.fY, ymax);
         });

         if (ymax > 0)
            ymax *= (1 + gStyle.fHistTopMargin);
         if (ymin < 0)
            ymin *= (1 + gStyle.fHistTopMargin);
      }

      const histo = createHistogram(clTH1I, 10);

      histo.fName = spline.fName + '_hist';
      histo.fTitle = spline.fTitle;
      histo.fBits |= kNoStats;

      histo.fXaxis.fXmin = xmin;
      histo.fXaxis.fXmax = xmax;
      histo.fYaxis.fXmin = ymin;
      histo.fYaxis.fXmax = ymax;
      histo.fMinimum = ymin;
      histo.fMaximum = ymax;

      return histo;
   }

   /** @summary Process tooltip event
     * @private */
   processTooltipEvent(pnt) {
      const spline = this.getObject(),
            o = this.getOptions(),
            funcs = this.getFramePainter()?.getGrFuncs(o.second_x, o.second_y);
      let cleanup = false, xx, yy, knot = null, indx = 0;

      if ((pnt === null) || !spline || !funcs)
         cleanup = true;
      else {
         xx = funcs.revertAxis('x', pnt.x);
         indx = this.findX(xx);
         knot = spline.fPoly[indx];
         yy = this.eval(knot, xx);

         if ((indx < spline.fN - 1) && (Math.abs(spline.fPoly[indx + 1].fX - xx) < Math.abs(xx - knot.fX)))
            knot = spline.fPoly[++indx];

         if (Math.abs(funcs.grx(knot.fX) - pnt.x) < 0.5 * this.#knot_size) {
            xx = knot.fX;
            yy = knot.fY;
         } else {
            knot = null;
            if ((xx < spline.fXmin) || (xx > spline.fXmax))
               cleanup = true;
         }
      }

      let gbin = this.getG()?.selectChild('.tooltip_bin');
      const radius = this.lineatt.width + 3;

      if (cleanup || !this.getG()) {
         gbin?.remove();
         return null;
      }

      if (gbin.empty()) {
         gbin = this.getG().append('svg:circle')
                           .attr('class', 'tooltip_bin')
                           .style('pointer-events', 'none')
                           .attr('r', radius)
                           .style('fill', 'none')
                           .call(this.lineatt.func);
      }

      const res = {
         name: this.getObject().fName,
         title: this.getObject().fTitle,
         x: funcs.grx(xx),
         y: funcs.gry(yy),
         color1: this.lineatt.color,
         lines: [],
         exact: (knot !== null) || (Math.abs(funcs.gry(yy) - pnt.y) < radius)
      };

      res.changed = gbin.property('current_xx') !== xx;
      res.menu = res.exact;
      res.menu_dist = Math.sqrt((res.x - pnt.x) ** 2 + (res.y - pnt.y) ** 2);

      if (res.changed) {
         gbin.attr('cx', Math.round(res.x))
             .attr('cy', Math.round(res.y))
             .property('current_xx', xx);
      }

      const name = this.getObjectHint();
      if (name)
         res.lines.push(name);
      res.lines.push(`x = ${funcs.axisAsText('x', xx)}`,
                     `y = ${funcs.axisAsText('y', yy)}`);
      if (knot !== null) {
         res.lines.push(`knot = ${indx}`,
                        `B = ${floatToString(knot.fB, gStyle.fStatFormat)}`,
                        `C = ${floatToString(knot.fC, gStyle.fStatFormat)}`,
                        `D = ${floatToString(knot.fD, gStyle.fStatFormat)}`);
         if ((knot.fE !== undefined) && (knot.fF !== undefined)) {
            res.lines.push(`E = ${floatToString(knot.fE, gStyle.fStatFormat)}`,
                           `F = ${floatToString(knot.fF, gStyle.fStatFormat)}`);
         }
      }

      return res;
   }

   /** @summary Redraw object
     * @private */
   redraw() {
      const spline = this.getObject(),
            o = this.getOptions(),
            funcs = this.getFramePainter().getGrFuncs(o.second_x, o.second_y),
            w = funcs.getFrameWidth(),
            h = funcs.getFrameHeight(),
            g = this.createG(true);

      this.#knot_size = 5; // used in tooltip handling

      this.createAttLine({ attr: spline });

      if (o.Line || o.Curve) {
         const npx = Math.max(10, spline.fNpx), bins = []; // index of current knot
         let xmin = Math.max(funcs.scale_xmin, spline.fXmin),
             xmax = Math.min(funcs.scale_xmax, spline.fXmax),
             indx = this.findX(xmin);

         if (funcs.logx) {
            xmin = Math.log(xmin);
            xmax = Math.log(xmax);
         }

         for (let n = 0; n < npx; ++n) {
            let x = xmin + (xmax - xmin) / npx * (n - 1);
            if (funcs.logx)
               x = Math.exp(x);

            while ((indx < spline.fNp - 1) && (x > spline.fPoly[indx + 1].fX))
               ++indx;

            const y = this.eval(spline.fPoly[indx], x);

            bins.push({ x, y, grx: funcs.grx(x), gry: funcs.gry(y) });
         }

         g.append('svg:path')
          .attr('class', 'line')
          .attr('d', buildSvgCurve(bins))
          .style('fill', 'none')
          .call(this.lineatt.func);
      }

      if (o.Mark) {
         // for tooltips use markers only if nodes where not created

         this.createAttMarker({ attr: spline });

         this.markeratt.resetPos();

         this.#knot_size = this.markeratt.getFullSize();

         let path = '';

         for (let n = 0; n < spline.fPoly.length; n++) {
            const knot = spline.fPoly[n],
                  grx = funcs.grx(knot.fX);
            if ((grx > -this.#knot_size) && (grx < w + this.#knot_size)) {
               const gry = funcs.gry(knot.fY);
               if ((gry > -this.#knot_size) && (gry < h + this.#knot_size))
                  path += this.markeratt.create(grx, gry);
            }
         }

         if (path) {
            g.append('svg:path')
             .attr('d', path)
             .call(this.markeratt.func);
         }
      }
   }

   /** @summary Checks if it makes sense to zoom inside specified axis range */
   canZoomInside(axis /* , min, max */) {
      // spline can always be calculated and therefore one can zoom inside
      return (axis !== 'x') ? false : Boolean(this.getObject());
   }

   /** @summary Decode options for TSpline drawing */
   decodeOptions(opt) {
      const d = new DrawOptions(opt),
            o = this.setOptions({
               Same: d.check('SAME'),
               Line: d.check('L'),
               Curve: d.check('C'),
               Mark: d.check('P'),
               Hopt: '',
               second_x: false,
               second_y: false
            });

      if (!o.Line && !o.Curve && !o.Mark)
         o.Curve = true;

      if (d.check('X+')) {
         o.Hopt += 'X+';
         o.second_x = Boolean(this.getMainPainter());
      }
      if (d.check('Y+')) {
         o.Hopt += 'Y+';
         o.second_y = Boolean(this.getMainPainter());
      }

      this.storeDrawOpt(opt);
   }

   /** @summary Draw TSpline */
   static async draw(dom, spline, opt) {
      const painter = new TSplinePainter(dom, spline);
      painter.decodeOptions(opt);

      const no_main = !painter.getMainPainter();
      let promise = Promise.resolve();
      if (no_main || painter.options.second_x || painter.options.second_y) {
         if (painter.options.Same && no_main) {
            console.warn('TSpline painter requires histogram to be drawn');
            return null;
         }
         const histo = painter.createDummyHisto();
         promise = TH1Painter.draw(dom, histo, painter.options.Hopt);
      }

      return promise.then(() => {
         painter.addToPadPrimitives();
         painter.redraw();
         return painter;
      });
   }

} // class TSplinePainter

var TSplinePainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TSplinePainter: TSplinePainter
});

/**
 * @summary Painter for TArrow class
 * @private
 */

class TArrowPainter extends TLinePainter {

   #beg;
   #mid;
   #end;
   #angle2; // half of angle in rad
   #wsize; // arrow size

   /** @summary Create line segment with rotation */
   rotate(angle, x0, y0) {
      let dx = this.#wsize * Math.cos(angle), dy = this.#wsize * Math.sin(angle), res = '';
      if ((x0 !== undefined) && (y0 !== undefined))
         res = `M${Math.round(x0 - dx)},${Math.round(y0 - dy)}`;
      else {
         dx = -dx;
         dy = -dy;
      }
      res += `l${Math.round(dx)},${Math.round(dy)}`;
      if (x0 && (y0 === undefined))
         res += 'z';
      return res;
   }

   /** @summary Create SVG path for the arrow */
   createPath() {
      const angle = Math.atan2(this.y2 - this.y1, this.x2 - this.x1),
            dlen = this.#wsize * Math.cos(this.#angle2),
            dx = dlen * Math.cos(angle), dy = dlen * Math.sin(angle);

      let path = '';
      if (this.#beg) {
         path += this.rotate(angle - Math.PI - this.#angle2, this.x1, this.y1) +
                 this.rotate(angle - Math.PI + this.#angle2, this.#beg > 10);
      }

      if (this.#mid % 10 === 2) {
         path += this.rotate(angle - Math.PI - this.#angle2, (this.x1 + this.x2 - dx) / 2, (this.y1 + this.y2 - dy) / 2) +
                 this.rotate(angle - Math.PI + this.#angle2, this.#mid > 10);
      }

      if (this.#mid % 10 === 1) {
         path += this.rotate(angle - this.#angle2, (this.x1 + this.x2 + dx) / 2, (this.y1 + this.y2 + dy) / 2) +
                 this.rotate(angle + this.#angle2, this.#mid > 10);
      }

      if (this.#end) {
         path += this.rotate(angle - this.#angle2, this.x2, this.y2) +
                 this.rotate(angle + this.#angle2, this.#end > 10);
      }

      return `M${Math.round(this.x1 + (this.#beg > 10 ? dx : 0))},${Math.round(this.y1 + (this.#beg > 10 ? dy : 0))}` +
             `L${Math.round(this.x2 - (this.#end > 10 ? dx : 0))},${Math.round(this.y2 - (this.#end > 10 ? dy : 0))}` +
              path;
   }

   /** @summary calculate all TArrow coordinates */
   prepareDraw() {
      super.prepareDraw();

      const arrow = this.getObject(),
            oo = arrow.fOption,
            rect = this.getPadPainter().getPadRect();

      this.#wsize = Math.max(3, Math.round(Math.max(rect.width, rect.height) * arrow.fArrowSize * 0.8));
      this.#angle2 = arrow.fAngle / 2 / 180 * Math.PI;
      this.#beg = this.#mid = this.#end = 0;

      if (oo.indexOf('<') === 0)
         this.#beg = (oo.indexOf('<|') === 0) ? 12 : 2;
      if (oo.indexOf('->-') >= 0)
         this.#mid = 1;
      else if (oo.indexOf('-|>-') >= 0)
         this.#mid = 11;
      else if (oo.indexOf('-<-') >= 0)
         this.#mid = 2;
      else if (oo.indexOf('-<|-') >= 0)
         this.#mid = 12;

      const p1 = oo.lastIndexOf('>'), p2 = oo.lastIndexOf('|>'), len = oo.length;
      if ((p1 >= 0) && (p1 === len - 1))
         this.#end = ((p2 >= 0) && (p2 === len - 2)) ? 11 : 1;

      this.createAttFill({ attr: arrow, enable: (this.#beg > 10) || (this.#end > 10) });
   }

   /** @summary Add extras to path for TArrow */
   addExtras(elem) {
      elem.call(this.fillatt.func);
   }

   /** @summary Draw TArrow object */
   static async draw(dom, obj, opt) {
      const painter = new TArrowPainter(dom, obj, opt);
      return ensureTCanvas(painter, false).then(() => painter.redraw());
   }

} // class TArrowPainter

var TArrowPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TArrowPainter: TArrowPainter
});

const kPolyLineNDC = BIT(14);

/**
 * @summary Painter for TPolyLine class
 * @private
 */

class TPolyLinePainter extends ObjectPainter {

   #dx; // interactive change
   #dy; // interactive change
   #isndc; // if NDC coordinates used

   /** @summary Dragging object
    *  @private */
   moveDrag(dx, dy) {
      this.#dx += dx;
      this.#dy += dy;
      makeTranslate(this.getG().select('path'), this.#dx, this.#dy);
   }

   /** @summary End dragging object
    * @private */
   moveEnd(not_changed) {
      if (not_changed)
         return;
      const polyline = this.getObject(),
            func = this.getAxisToSvgFunc(this.#isndc);
      let exec = '';

      for (let n = 0; n <= polyline.fLastPoint; ++n) {
         const x = this.svgToAxis('x', func.x(polyline.fX[n]) + this.#dx, this.#isndc),
               y = this.svgToAxis('y', func.y(polyline.fY[n]) + this.#dy, this.#isndc);
         polyline.fX[n] = x;
         polyline.fY[n] = y;
         exec += `SetPoint(${n},${x},${y});;`;
      }
      this.submitCanvExec(exec + 'Notify();;');
      this.redraw();
   }

   /** @summary Returns object ranges
    * @desc Can be used for newly created canvas */
   getUserRanges() {
      const polyline = this.getObject(),
            isndc = polyline.TestBit(kPolyLineNDC);
      if (isndc || !polyline.fLastPoint)
         return null;
      let minx = polyline.fX[0], maxx = minx,
          miny = polyline.fY[0], maxy = miny;
      for (let n = 1; n <= polyline.fLastPoint; ++n) {
         minx = Math.min(minx, polyline.fX[n]);
         maxx = Math.max(maxx, polyline.fX[n]);
         miny = Math.min(miny, polyline.fY[n]);
         maxy = Math.max(maxy, polyline.fY[n]);
      }
      return { minx, miny, maxx, maxy };
   }

   /** @summary Redraw poly line */
   redraw() {
      const g = this.createG(),
            polyline = this.getObject(),
            isndc = polyline.TestBit(kPolyLineNDC),
            opt = this.getDrawOpt() || polyline.fOption,
            dofill = (polyline._typename === clTPolyLine) && (isStr(opt) && opt.toLowerCase().indexOf('f') >= 0),
            func = this.getAxisToSvgFunc(isndc);

      this.createAttLine({ attr: polyline });
      this.createAttFill({ attr: polyline, enable: dofill });

      let cmd = '';
      for (let n = 0; n <= polyline.fLastPoint; ++n)
         cmd += `${n > 0 ? 'L' : 'M'}${func.x(polyline.fX[n])},${func.y(polyline.fY[n])}`;

      g.append('svg:path')
       .attr('d', cmd + (dofill ? 'Z' : ''))
       .call(dofill ? () => {} : this.lineatt.func)
       .call(this.fillatt.func);

      assignContextMenu(this);

      addMoveHandler(this);

      this.#dx = this.#dy = 0;
      this.#isndc = isndc;

      return this;
   }

   /** @summary Draw TPolyLine object */
   static async draw(dom, obj, opt) {
      const painter = new TPolyLinePainter(dom, obj, opt);
      return ensureTCanvas(painter, false).then(() => painter.redraw());
   }

} // class TPolyLinePainter

var TPolyLinePainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TPolyLinePainter: TPolyLinePainter
});

/** @summary Drawing TGaxis
  * @private */
class TGaxisPainter extends TAxisPainter {

   /** @summary Convert TGaxis position into NDC to fix it when frame zoomed */
   convertTo(opt) {
      const gaxis = this.getObject(),
            x1 = this.axisToSvg('x', gaxis.fX1),
            y1 = this.axisToSvg('y', gaxis.fY1),
            x2 = this.axisToSvg('x', gaxis.fX2),
            y2 = this.axisToSvg('y', gaxis.fY2);

      if (opt === 'ndc') {
         const pw = this.getPadPainter().getPadWidth(),
               ph = this.getPadPainter().getPadHeight();

         gaxis.fX1 = x1 / pw;
         gaxis.fX2 = x2 / pw;
         gaxis.fY1 = (ph - y1) / ph;
         gaxis.fY2 = (ph - y2) / ph;
         this.use_ndc = true;
      } else if (opt === 'frame') {
         const rect = this.getFramePainter().getFrameRect();
         gaxis.fX1 = (x1 - rect.x) / rect.width;
         gaxis.fX2 = (x2 - rect.x) / rect.width;
         gaxis.fY1 = (y1 - rect.y) / rect.height;
         gaxis.fY2 = (y2 - rect.y) / rect.height;
         this.bind_frame = true;
      }
   }

   /** @summary Drag moving handle */
   moveDrag(dx, dy) {
      this.gaxis_x += dx;
      this.gaxis_y += dy;
      makeTranslate(this.getG(), this.gaxis_x, this.gaxis_y);
   }

   /** @summary Drag end handle */
   moveEnd(not_changed) {
      if (not_changed)
         return;

      const gaxis = this.getObject();

      let fx, fy;
      if (this.bind_frame) {
         const rect = this.getFramePainter().getFrameRect();
         fx = (this.gaxis_x - rect.x) / rect.width;
         fy = (this.gaxis_y - rect.y) / rect.height;
      } else {
         fx = this.svgToAxis('x', this.gaxis_x, this.use_ndc);
         fy = this.svgToAxis('y', this.gaxis_y, this.use_ndc);
      }

      if (this.vertical) {
         gaxis.fX1 = gaxis.fX2 = fx;
         if (this.reverse) {
            gaxis.fY2 = fy + (gaxis.fY2 - gaxis.fY1);
            gaxis.fY1 = fy;
         } else {
            gaxis.fY1 = fy + (gaxis.fY1 - gaxis.fY2);
            gaxis.fY2 = fy;
         }
      } else {
         if (this.reverse) {
            gaxis.fX1 = fx + (gaxis.fX1 - gaxis.fX2);
            gaxis.fX2 = fx;
         } else {
            gaxis.fX2 = fx + (gaxis.fX2 - gaxis.fX1);
            gaxis.fX1 = fx;
         }
         gaxis.fY1 = gaxis.fY2 = fy;
      }

      this.submitAxisExec(`SetX1(${gaxis.fX1});;SetX2(${gaxis.fX2});;SetY1(${gaxis.fY1});;SetY2(${gaxis.fY2})`, true);
   }

   /** @summary Redraw axis, used in standalone mode for TGaxis */
   redraw() {
      const gaxis = this.getObject(),
            min = gaxis.fWmin,
            max = gaxis.fWmax;
      let x1, y1, x2, y2;

      if (this.bind_frame) {
         const rect = this.getFramePainter().getFrameRect();
         x1 = Math.round(rect.x + gaxis.fX1 * rect.width);
         x2 = Math.round(rect.x + gaxis.fX2 * rect.width);
         y1 = Math.round(rect.y + gaxis.fY1 * rect.height);
         y2 = Math.round(rect.y + gaxis.fY2 * rect.height);
      } else {
         x1 = this.axisToSvg('x', gaxis.fX1, this.use_ndc);
         y1 = this.axisToSvg('y', gaxis.fY1, this.use_ndc);
         x2 = this.axisToSvg('x', gaxis.fX2, this.use_ndc);
         y2 = this.axisToSvg('y', gaxis.fY2, this.use_ndc);
      }

      const w = x2 - x1, h = y1 - y2,
            vertical = Math.abs(w) < Math.abs(h);
      let sz = vertical ? h : w, reverse = false;

      if (sz < 0) {
         reverse = true;
         sz = -sz;
         if (vertical)
            y2 = y1;
         else
            x1 = x2;
      }

      this.configureAxis(vertical ? 'yaxis' : 'xaxis', min, max, min, max, vertical, [0, sz], {
         time_scale: gaxis.fChopt.indexOf('t') >= 0,
         log: (gaxis.fChopt.indexOf('G') >= 0) ? 1 : 0,
         reverse,
         swap_side: reverse,
         axis_func: this.axis_func
      });

      this.gaxis_x = x1;
      this.gaxis_y = y2;

      return this.drawAxis(this.createG(), Math.abs(w), Math.abs(h), makeTranslate(this.gaxis_x, this.gaxis_y) || '').then(() => {
         addMoveHandler(this);
         assignContextMenu(this, kNoReorder);
         return this;
      });
   }

   /** @summary Fill TGaxis context menu items */
   fillContextMenuItems(menu) {
      menu.addTAxisMenu(EAxisBits, this, this.getObject(), '');
   }

   /** @summary Check if there is function for TGaxis can be found */
   async checkFuncion() {
      const gaxis = this.getObject();
      if (!gaxis.fFunctionName) {
         this.axis_func = null;
         return;
      }
      const func = this.getPadPainter()?.findInPrimitives(gaxis.fFunctionName, clTF1);

      let promise = Promise.resolve(func);
      if (!func) {
         const h = getHPainter(),
               item = h?.findItem({ name: gaxis.fFunctionName, check_keys: true });
         if (item) {
            promise = h.getObject({ item }).then(res => {
               return res?.obj?._typename === clTF1 ? res.obj : null;
            });
         }
      }

      return promise.then(f => {
         this.axis_func = f;
         if (f)
            proivdeEvalPar(f);
      });
   }

   /** @summary Create handle for custom function in the axis */
   createFuncHandle(func, logbase, smin, smax) {
      const res = function(v) { return res.toGraph(v); };
      res._func = func;
      res._domain = [smin, smax];
      res._scale = logbase ? log().base(logbase) : linear();
      res._scale.domain(res._domain).range([0, 100]);
      res.eval = function(v) {
         try {
            v = res._func.evalPar(v);
         } catch {
            v = 0;
         }
         return Number.isFinite(v) ? v : 0;
      };

      const vmin = res.eval(smin), vmax = res.eval(smax);
      if ((vmin < vmax) === (smin < smax)) {
         res._vmin = vmin;
         res._vk = 1 / (vmax - vmin);
      } else if (vmin === vmax) {
         res._vmin = 0;
         res._vk = 1;
      } else {
         res._vmin = vmax;
         res._vk = 1 / (vmin - vmax);
      }
      res._range = [0, 100];
      res.range = function(arr) {
         if (arr) {
            res._range = arr;
            return res;
         }
         return res._range;
      };

      res.domain = function() { return res._domain; };

      res.toGraph = function(v) {
         const rel = (res.eval(v) - res._vmin) * res._vk;
         return res._range[0] * (1 - rel) + res._range[1] * rel;
      };

      res.ticks = function(arg) { return res._scale.ticks(arg); };

      return res;
   }

   /** @summary Draw TGaxis object */
   static async draw(dom, obj, opt) {
      const painter = new TGaxisPainter(dom, obj, false);

      return ensureTCanvas(painter, false).then(() => {
         if (opt)
            painter.convertTo(opt);
         return painter.checkFuncion();
      }).then(() => painter.redraw());
   }

} // class TGaxisPainter

var TGaxisPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TGaxisPainter: TGaxisPainter
});

/**
 * @summary Painter for TBox class
 * @private
 */

class TBoxPainter extends ObjectPainter {

   #border_mode;
   #border_size;

   /** @summary start of drag handler
     * @private */
   moveStart(x, y) {
      const ww = Math.abs(this.x2 - this.x1), hh = Math.abs(this.y1 - this.y2);

      this.c_x1 = Math.abs(x - this.x2) > ww * 0.1;
      this.c_x2 = Math.abs(x - this.x1) > ww * 0.1;
      this.c_y1 = Math.abs(y - this.y2) > hh * 0.1;
      this.c_y2 = Math.abs(y - this.y1) > hh * 0.1;
      if (this.c_x1 !== this.c_x2 && this.c_y1 && this.c_y2)
         this.c_y1 = this.c_y2 = false;
      if (this.c_y1 !== this.c_y2 && this.c_x1 && this.c_x2)
         this.c_x1 = this.c_x2 = false;
   }

   /** @summary drag handler
     * @private */
   moveDrag(dx, dy) {
      this.x1 += this.c_x1 ? dx : 0;
      this.x2 += this.c_x2 ? dx : 0;
      this.y1 += this.c_y1 ? dy : 0;
      this.y2 += this.c_y2 ? dy : 0;

      const nodes = this.getG().selectAll('path').nodes(),
            pathes = this.getPathes();

      pathes.forEach((path, i) => select(nodes[i]).attr('d', path));
   }

   /** @summary end of drag handler
     * @private */
   moveEnd(not_changed) {
      if (not_changed)
         return;
      const box = this.getObject(), X = this.swap_xy ? 'Y' : 'X', Y = this.swap_xy ? 'X' : 'Y';
      let exec = '';
      if (this.c_x1) {
         const v = this.svgToAxis('x', this.x1);
         box[`f${X}1`] = v;
         exec += `Set${X}1(${v});;`;
      }
      if (this.c_x2) {
         const v = this.svgToAxis('x', this.x2);
         box[`f${X}2`] = v;
         exec += `Set${X}2(${v});;`;
      }
      if (this.c_y1) {
         const v = this.svgToAxis('y', this.y1);
         box[`f${Y}1`] = v;
         exec += `Set${Y}1(${v});;`;
      }
      if (this.c_y2) {
         const v = this.svgToAxis('y', this.y2);
         box[`f${Y}2`] = v;
         exec += `Set${Y}2(${v});;`;
      }
      this.submitCanvExec(exec + 'Notify();;');
   }

   /** @summary Returns object ranges
     * @desc Can be used for newly created canvas */
   getUserRanges() {
      const box = this.getObject(),
            minx = Math.min(box.fX1, box.fX2),
            maxx = Math.max(box.fX1, box.fX2),
            miny = Math.min(box.fY1, box.fY2),
            maxy = Math.max(box.fY1, box.fY2);
      return { minx, miny, maxx, maxy };
   }

   /** @summary Create path */
   getPathes() {
      const xx = Math.round(Math.min(this.x1, this.x2)),
            yy = Math.round(Math.min(this.y1, this.y2)),
            ww = Math.round(Math.abs(this.x2 - this.x1)),
            hh = Math.round(Math.abs(this.y1 - this.y2)),
            path = `M${xx},${yy}h${ww}v${hh}h${-ww}z`;
      if (!this.#border_mode)
         return [path];
      return [path].concat(getBoxDecorations(xx, yy, ww, hh, this.#border_mode, this.#border_size, this.#border_size));
   }

   /** @summary Redraw box */
   redraw() {
      const box = this.getObject(),
            d = new DrawOptions(this.getDrawOpt()),
            fp = d.check('FRAME') ? this.getFramePainter() : null,
            draw_line = d.check('L');

      this.createAttLine({ attr: box });
      this.createAttFill({ attr: box });

      this.swap_xy = fp?.swap_xy();

      // if box filled, contour line drawn only with 'L' draw option:
      if (!this.fillatt.empty() && !draw_line)
         this.lineatt.color = 'none';

      const g = this.createG(fp);

      this.x1 = this.axisToSvg('x', box.fX1);
      this.x2 = this.axisToSvg('x', box.fX2);
      this.y1 = this.axisToSvg('y', box.fY1);
      this.y2 = this.axisToSvg('y', box.fY2);

      if (this.swap_xy)
         [this.x1, this.x2, this.y1, this.y2] = [this.y1, this.y2, this.x1, this.x2];

      this.#border_mode = (box.fBorderMode && this.fillatt.hasColor()) ? box.fBorderMode : 0;
      this.#border_size = box.fBorderSize || 2;

      const paths = this.getPathes();

      g.append('svg:path')
       .attr('d', paths[0])
       .call(this.lineatt.func)
       .call(this.fillatt.func);

      if (this.#border_mode) {
         g.append('svg:path')
          .attr('d', paths[1])
          .call(this.fillatt.func)
          .style('fill', rgb(this.fillatt.color).brighter(0.5).formatRgb());

         g.append('svg:path')
          .attr('d', paths[2])
          .call(this.fillatt.func)
          .style('fill', rgb(this.fillatt.color).darker(0.5).formatRgb());
      }

      assignContextMenu(this);

      addMoveHandler(this);

      return this;
   }

   /** @summary Draw TLine object */
   static async draw(dom, obj, opt) {
      const painter = new TBoxPainter(dom, obj, opt);
      return ensureTCanvas(painter, false).then(() => painter.redraw());
   }

} // class TBoxPainter

var TBoxPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TBoxPainter: TBoxPainter
});

/**
 * @summary Painter for TASImage object.
 *
 * @private
 */

class TASImagePainter extends ObjectPainter {

   #contour;

   /** @summary Decode options string  */
   decodeOptions(opt) {
      const d = new DrawOptions(opt),
            obj = this.getObject();

      if (d.check('CONST') && obj)
         obj.fConstRatio = true;

      this.setOptions({ Zscale: d.check('Z') });
   }

   /** @summary Create RGBA buffers */
   createRGBA(nlevels) {
      const obj = this.getObject(),
            pal = obj?.fPalette;
      if (!pal)
         return null;

      const rgba = new Array((nlevels + 1) * 4).fill(0); // precalculated colors

      for (let lvl = 0, indx = 1; lvl <= nlevels; ++lvl) {
         const l = lvl / nlevels;
         while ((pal.fPoints[indx] < l) && (indx < pal.fPoints.length - 1))
            indx++;

         const r1 = (pal.fPoints[indx] - l) / (pal.fPoints[indx] - pal.fPoints[indx - 1]),
               r2 = (l - pal.fPoints[indx - 1]) / (pal.fPoints[indx] - pal.fPoints[indx - 1]);

         rgba[lvl * 4] = Math.min(255, Math.round((pal.fColorRed[indx - 1] * r1 + pal.fColorRed[indx] * r2) / 256));
         rgba[lvl * 4 + 1] = Math.min(255, Math.round((pal.fColorGreen[indx - 1] * r1 + pal.fColorGreen[indx] * r2) / 256));
         rgba[lvl * 4 + 2] = Math.min(255, Math.round((pal.fColorBlue[indx - 1] * r1 + pal.fColorBlue[indx] * r2) / 256));
         rgba[lvl * 4 + 3] = Math.min(255, Math.round((pal.fColorAlpha[indx - 1] * r1 + pal.fColorAlpha[indx] * r2) / 256));
      }

      return rgba;
   }

   /** @summary Cleanup painter
     * @private */
   cleanup() {
      this.#contour = undefined;
      super.cleanup();
   }

   /** @summary Return colors contour
     * @private */
   getContour() { return this.#contour; }

   /** @summary Create url using image buffer
     * @private */
   async makeUrlFromImageBuf(obj, fp) {
      const nlevels = 1000;
      this.rgba = this.createRGBA(nlevels); // precalculated colors

      let min = obj.fImgBuf[0], max = obj.fImgBuf[0];
      for (let k = 1; k < obj.fImgBuf.length; ++k) {
         const v = obj.fImgBuf[k];
         min = Math.min(v, min);
         max = Math.max(v, max);
      }

      // does not work properly in Node.js, causes 'Maximum call stack size exceeded' error
      // min = Math.min.apply(null, obj.fImgBuf),
      // max = Math.max.apply(null, obj.fImgBuf);

      // create contour like in hist painter to allow palette drawing
      this.#contour = {
         arr: new Array(200),
         rgba: this.rgba,
         getLevels() { return this.arr; },
         getPaletteColor(pal, zval) {
            if (!this.arr || !this.rgba)
               return 'white';
            const indx = Math.round((zval - this.arr[0]) / (this.arr.at(-1) - this.arr.at(0)) * (this.rgba.length - 4) / 4) * 4;
            return toColor(this.rgba[indx] / 255, this.rgba[indx + 1] / 255, this.rgba[indx + 2] / 255, this.rgba[indx + 3] / 255);
         }
      };
      for (let k = 0; k < 200; k++)
         this.#contour.arr[k] = min + (max - min) / (200 - 1) * k;

      if (min >= max)
         max = min + 1;

      const z = this.getImageZoomRange(fp, obj.fConstRatio, obj.fWidth, obj.fHeight),
            pr = isNodeJs()
                 ? Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }).then(h => h.default.createCanvas(z.xmax - z.xmin, z.ymax - z.ymin))
                 : new Promise(resolveFunc => {
                    const c = document.createElement('canvas');
                    c.width = z.xmax - z.xmin;
                    c.height = z.ymax - z.ymin;
                    resolveFunc(c);
                 });

      return pr.then(canvas => {
         const context = canvas.getContext('2d'),
               imageData = context.getImageData(0, 0, canvas.width, canvas.height),
               arr = imageData.data;

         for (let i = z.ymin; i < z.ymax; ++i) {
            let dst = (z.ymax - i - 1) * (z.xmax - z.xmin) * 4;
            const row = i * obj.fWidth;
            for (let j = z.xmin; j < z.xmax; ++j) {
               let iii = Math.round((obj.fImgBuf[row + j] - min) / (max - min) * nlevels) * 4;
               // copy rgba value for specified point
               arr[dst++] = this.rgba[iii++];
               arr[dst++] = this.rgba[iii++];
               arr[dst++] = this.rgba[iii++];
               arr[dst++] = this.rgba[iii];
            }
         }

         context.putImageData(imageData, 0, 0);

         return { url: canvas.toDataURL(), constRatio: obj.fConstRatio, can_zoom: true };
      });
   }

   getImageZoomRange(fp, constRatio, width, height) {
      const res = { xmin: 0, xmax: width, ymin: 0, ymax: height };
      if (!fp)
         return res;

      let offx = 0, offy = 0, sizex = width, sizey = height;

      if (constRatio) {
         const image_ratio = height / width,
               frame_ratio = fp.getFrameHeight() / fp.getFrameWidth();

         if (image_ratio > frame_ratio) {
            const w2 = height / frame_ratio;
            offx = Math.round((w2 - width) / 2);
            sizex = Math.round(w2);
         } else {
            const h2 = frame_ratio * width;
            offy = Math.round((h2 - height) / 2);
            sizey = Math.round(h2);
         }
      }

      if (fp.zoom_xmin !== fp.zoom_xmax) {
         res.xmin = Math.min(width, Math.max(0, Math.round(fp.zoom_xmin * sizex) - offx));
         res.xmax = Math.min(width, Math.max(0, Math.round(fp.zoom_xmax * sizex) - offx));
      }
      if (fp.zoom_ymin !== fp.zoom_ymax) {
         res.ymin = Math.min(height, Math.max(0, Math.round(fp.zoom_ymin * sizey) - offy));
         res.ymax = Math.min(height, Math.max(0, Math.round(fp.zoom_ymax * sizey) - offy));
      }
      return res;
   }

   /** @summary Produce data url from png buffer */
   async makeUrlFromPngBuf(obj, fp) {
      const buf = obj.fPngBuf;
      let pngbuf = '';

      if (isStr(buf))
         pngbuf = buf;
      else {
         for (let k = 0; k < buf.length; ++k)
            pngbuf += String.fromCharCode(buf[k] < 0 ? 256 + buf[k] : buf[k]);
      }

      const res = { url: 'data:image/png;base64,' + btoa_func(pngbuf), constRatio: obj.fConstRatio, can_zoom: fp && !isNodeJs() },
            doc = getDocument();

      if (!res.can_zoom || ((fp?.zoom_xmin === fp?.zoom_xmax) && (fp?.zoom_ymin === fp?.zoom_ymax)))
         return res;

      return new Promise(resolveFunc => {
         const image = doc.createElement('img');

         image.onload = () => {
            const canvas = doc.createElement('canvas');
            canvas.width = image.width;
            canvas.height = image.height;

            const context = canvas.getContext('2d');
            context.drawImage(image, 0, 0);

            const arr = context.getImageData(0, 0, image.width, image.height).data,
                  z = this.getImageZoomRange(fp, res.constRatio, image.width, image.height),
                  canvas2 = doc.createElement('canvas');
            canvas2.width = z.xmax - z.xmin;
            canvas2.height = z.ymax - z.ymin;

            const context2 = canvas2.getContext('2d'),
                  imageData2 = context2.getImageData(0, 0, canvas2.width, canvas2.height),
                  arr2 = imageData2.data;

            for (let i = z.ymin; i < z.ymax; ++i) {
               let dst = (z.ymax - i - 1) * (z.xmax - z.xmin) * 4,
                   src = ((image.height - i - 1) * image.width + z.xmin) * 4;
               for (let j = z.xmin; j < z.xmax; ++j) {
                  // copy rgba value for specified point
                  arr2[dst++] = arr[src++];
                  arr2[dst++] = arr[src++];
                  arr2[dst++] = arr[src++];
                  arr2[dst++] = arr[src++];
               }
            }

            context2.putImageData(imageData2, 0, 0);

            res.url = canvas2.toDataURL();

            resolveFunc(res);
         };

         image.onerror = () => resolveFunc(res);

         image.src = res.url;
      });
   }

   /** @summary Use in frame painter to check zoom Y is allowed
    * @protected */
   get _wheel_zoomy() { return true; }

   /** @summary Draw image */
   async drawImage() {
      const obj = this.getObject(),
            fp = this.getFramePainter(),
            rect = fp?.getFrameRect() ?? this.getPadPainter().getPadRect();

      if (obj._blob) {
         // try to process blob data due to custom streamer
         if ((obj._blob.length === 15) && !obj._blob[0]) {
            obj.fImageQuality = obj._blob[1];
            obj.fImageCompression = obj._blob[2];
            obj.fConstRatio = obj._blob[3];
            obj.fPalette = {
               _typename: clTImagePalette,
               fUniqueID: obj._blob[4],
               fBits: obj._blob[5],
               fNumPoints: obj._blob[6],
               fPoints: obj._blob[7],
               fColorRed: obj._blob[8],
               fColorGreen: obj._blob[9],
               fColorBlue: obj._blob[10],
               fColorAlpha: obj._blob[11]
            };

            obj.fWidth = obj._blob[12];
            obj.fHeight = obj._blob[13];
            obj.fImgBuf = obj._blob[14];

            if ((obj.fWidth * obj.fHeight !== obj.fImgBuf.length) ||
                  (obj.fPalette.fNumPoints !== obj.fPalette.fPoints.length)) {
               console.error(`TASImage _blob decoding error ${obj.fWidth * obj.fHeight} != ${obj.fImgBuf.length} ${obj.fPalette.fNumPoints} != ${obj.fPalette.fPoints.length}`);
               delete obj.fImgBuf;
               delete obj.fPalette;
            }
         } else if ((obj._blob.length === 3) && obj._blob[0]) {
            obj.fPngBuf = obj._blob[2];
            if (obj.fPngBuf?.length !== obj._blob[1]) {
               console.error(`TASImage with png buffer _blob error ${obj._blob[1]} != ${obj.fPngBuf?.length}`);
               delete obj.fPngBuf;
            }
         } else
            console.error(`TASImage _blob len ${obj._blob.length} not recognized`);

         delete obj._blob;
      }

      let promise;

      if (obj.fImgBuf && obj.fPalette)
         promise = this.makeUrlFromImageBuf(obj, fp);
      else if (obj.fPngBuf)
         promise = this.makeUrlFromPngBuf(obj, fp);
      else
         promise = Promise.resolve(null);

      return promise.then(res => {
         if (!res?.url)
            return this;

         const img = this.createG(fp)
             .append('image')
             .attr('href', res.url)
             .attr('width', rect.width)
             .attr('height', rect.height)
             .attr('preserveAspectRatio', res.constRatio ? null : 'none');

         if (!this.isBatchMode()) {
            if (settings.MoveResize || settings.ContextMenu)
               img.style('pointer-events', 'visibleFill');

            if (res.can_zoom)
               img.style('cursor', 'pointer');
         }

         assignContextMenu(this, kNoReorder);

         if (!fp || !res.can_zoom)
            return this;

         return this.drawColorPalette(this.getOptions().Zscale, true).then(() => {
            fp.setAxesRanges(create$1(clTAxis), 0, 1, create$1(clTAxis), 0, 1, null, 0, 0);
            fp.createXY({ ndim: 2, check_pad_range: false });
            return fp.addInteractivity();
         });
      });
   }

   /** @summary Fill TASImage context menu */
   fillContextMenuItems(menu) {
      const obj = this.getObject(), o = this.getOptions();
      if (obj) {
         menu.addchk(obj.fConstRatio, 'Const ratio', flag => {
            obj.fConstRatio = flag;
            this.interactiveRedraw('pad', `exec:SetConstRatio(${flag})`);
         }, 'Change const ratio flag of image');
      }
      if (obj?.fPalette) {
         menu.addchk(o.Zscale, 'Color palette', flag => {
            o.Zscale = flag;
            this.drawColorPalette(flag, true);
         }, 'Toggle color palette');
      }
   }

   /** @summary Checks if it makes sense to zoom inside specified axis range */
   canZoomInside(axis, min, max) {
      const obj = this.getObject();

      if (!obj)
         return false;

      if (((axis === 'x') || (axis === 'y')) && (max - min > 0.01))
         return true;

      return false;
   }

   /** @summary Return palette - dummy here
     * @private */
   getHistPalette() { return true; }

   /** @summary Draw color palette
     * @private */
   async drawColorPalette(enabled, can_move) {
      if (!this.isMainPainter())
         return null;

      if (!this.draw_palette) {
         const pal = create$1(clTPaletteAxis);
         Object.assign(pal, { fX1NDC: 0.91, fX2NDC: 0.95, fY1NDC: 0.1, fY2NDC: 0.9, fInit: 1 });
         pal.fAxis.fChopt = '+';
         this.draw_palette = pal;
      }

      let pal_painter = this.getPadPainter().findPainterFor(this.draw_palette);

      if (!enabled) {
         if (pal_painter) {
            pal_painter.Enabled = false;
            pal_painter.removeG(); // completely remove drawing without need to redraw complete pad
         }
         return null;
      }

      const fp = this.getFramePainter();

      // keep palette width
      if (can_move && fp) {
         const pal = this.draw_palette;
         pal.fX2NDC = fp.fX2NDC + 0.01 + (pal.fX2NDC - pal.fX1NDC);
         pal.fX1NDC = fp.fX2NDC + 0.01;
         pal.fY1NDC = fp.fY1NDC;
         pal.fY2NDC = fp.fY2NDC;
      }

      if (pal_painter) {
         pal_painter.Enabled = true;
         return pal_painter.drawPave('');
      }

      return TPavePainter.draw(this.getPadPainter(), this.draw_palette).then(p => {
         pal_painter = p;

         // mark painter as secondary - not in list of TCanvas primitives
         pal_painter.setSecondaryId(this);

         // make dummy redraw, palette will be updated only from histogram painter
         pal_painter.redraw = function() {};
      });
   }

   /** @summary Toggle colz draw option
     * @private */
   toggleColz() {
      if (this.getObject()?.fPalette) {
         const o = this.getOptions();
         o.Zscale = !o.Zscale;
         return this.drawColorPalette(o.Zscale, true);
      }
   }

   /** @summary Redraw image */
   redraw() {
      return this.drawImage();
   }

   /** @summary Process click on TASImage-defined buttons
     * @desc may return promise or simply false */
   clickButton(funcname) {
      if (this.isMainPainter() && funcname === 'ToggleColorZ')
         return this.toggleColz();

      return false;
   }

   /** @summary Fill pad toolbar for TASImage */
   fillToolbar() {
      const pp = this.getPadPainter();
      if (pp && this.getObject()?.fPalette) {
         pp.addPadButton('th2colorz', 'Toggle color palette', 'ToggleColorZ');
         pp.showPadButtons();
      }
   }

   /** @summary Draw TASImage object */
   static async draw(dom, obj, opt) {
      const painter = new TASImagePainter(dom, obj, opt);
      painter.setAsMainPainter();
      painter.decodeOptions(opt);
      return ensureTCanvas(painter, false)
                 .then(() => painter.drawImage())
                 .then(() => {
                    painter.fillToolbar();
                    return painter;
                 });
   }

} // class TASImagePainter

var TASImagePainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
TASImagePainter: TASImagePainter
});

const LITTLE_ENDIAN = true;
class RBufferReader {

   constructor(buffer) {
      if (buffer instanceof ArrayBuffer) {
         this.buffer = buffer;
         this.byteOffset = 0;
         this.byteLength = buffer.byteLength;
      } else if (ArrayBuffer.isView(buffer)) {
         this.buffer = buffer.buffer;
         this.byteOffset = buffer.byteOffset;
         this.byteLength = buffer.byteLength;
      } else
         throw new TypeError('Invalid buffer type');

      this.view = new DataView(this.buffer);
      // important - offset should start from actual place in the buffer
      this.offset = this.byteOffset;
   }

   // Move to a specific position in the buffer
   seek(position) {
      if (typeof position === 'bigint') {
         if (position > BigInt(Number.MAX_SAFE_INTEGER))
            throw new Error(`Offset too large to seek safely: ${position}`);
         this.offset = Number(position);
      } else
         this.offset = position;
   }


   // Read unsigned 8-bit integer (1 BYTE)
   readU8() {
      const val = this.view.getUint8(this.offset);
      this.offset += 1;
      return val;
   }

   // Read unsigned 16-bit integer (2 BYTES)
   readU16() {
      const val = this.view.getUint16(this.offset, LITTLE_ENDIAN);
      this.offset += 2;
      return val;
   }

   // Read unsigned 32-bit integer (4 BYTES)
   readU32() {
      const val = this.view.getUint32(this.offset, LITTLE_ENDIAN);
      this.offset += 4;
      return val;
   }

   // Read signed 8-bit integer (1 BYTE)
   readS8() {
      const val = this.view.getInt8(this.offset);
      this.offset += 1;
      return val;
   }

   // Read signed 16-bit integer (2 BYTES)
   readS16() {
      const val = this.view.getInt16(this.offset, LITTLE_ENDIAN);
      this.offset += 2;
      return val;
   }

   // Read signed 32-bit integer (4 BYTES)
   readS32() {
      const val = this.view.getInt32(this.offset, LITTLE_ENDIAN);
      this.offset += 4;
      return val;
   }

   // Read 32-bit float (4 BYTES)
   readF32() {
      const val = this.view.getFloat32(this.offset, LITTLE_ENDIAN);
      this.offset += 4;
      return val;
   }

   // Read 64-bit float (8 BYTES)
   readF64() {
      const val = this.view.getFloat64(this.offset, LITTLE_ENDIAN);
      this.offset += 8;
      return val;
   }

   // Read a string with 32-bit length prefix
   readString() {
      const length = this.readU32();
      let str = '';
      for (let i = 0; i < length; i++)
         str += String.fromCharCode(this.readU8());
      return str;
   }

   // Read unsigned 64-bit integer (8 BYTES)
   readU64() {
      const val = this.view.getBigUint64(this.offset, LITTLE_ENDIAN);
      this.offset += 8;
      return val;
   }

   // Read signed 64-bit integer (8 BYTES)
   readS64() {
      const val = this.view.getBigInt64(this.offset, LITTLE_ENDIAN);
      this.offset += 8;
      return val;
   }

}

const ENTupleColumnType = {
   kBit: 0x00,
   kByte: 0x01,
   kChar: 0x02,
   kInt8: 0x03,
   kUInt8: 0x04,
   kInt16: 0x05,
   kUInt16: 0x06,
   kInt32: 0x07,
   kUInt32: 0x08,
   kInt64: 0x09,
   kUInt64: 0x0A,
   kReal16: 0x0B,
   kReal32: 0x0C,
   kReal64: 0x0D,
   kIndex32: 0x0E,
   kIndex64: 0x0F,
   kSplitInt16: 0x11,
   kSplitUInt16: 0x12,
   kSplitInt32: 0x13,
   kSplitUInt32: 0x14,
   kSplitInt64: 0x15,
   kSplitUInt64: 0x16,
   kSplitReal16: 0x17,
   kSplitReal32: 0x18,
   kSplitReal64: 0x19,
   kSplitIndex32: 0x1A,
   kSplitIndex64: 0x1B};


/**
 * @summary Rearrange bytes from split format to normal format (row-wise) for decoding
 */
function recontructUnsplitBuffer(blob, columnDescriptor) {
   const { coltype } = columnDescriptor;

   if (
      coltype === ENTupleColumnType.kSplitUInt16 ||
        coltype === ENTupleColumnType.kSplitUInt32 ||
        coltype === ENTupleColumnType.kSplitUInt64 ||
        coltype === ENTupleColumnType.kSplitReal16 ||
        coltype === ENTupleColumnType.kSplitReal32 ||
        coltype === ENTupleColumnType.kSplitReal64 ||
        coltype === ENTupleColumnType.kSplitIndex32 ||
        coltype === ENTupleColumnType.kSplitIndex64 ||
        coltype === ENTupleColumnType.kSplitInt16 ||
        coltype === ENTupleColumnType.kSplitInt32 ||
        coltype === ENTupleColumnType.kSplitInt64
   ) {
      // Determine byte size based on column type
      let byteSize;
      switch (coltype) {
         case ENTupleColumnType.kSplitReal64:
         case ENTupleColumnType.kSplitInt64:
         case ENTupleColumnType.kSplitUInt64:
         case ENTupleColumnType.kSplitIndex64:
            byteSize = 8;
            break;
         case ENTupleColumnType.kSplitReal32:
         case ENTupleColumnType.kSplitInt32:
         case ENTupleColumnType.kSplitIndex32:
         case ENTupleColumnType.kSplitUInt32:
            byteSize = 4;
            break;
         case ENTupleColumnType.kSplitInt16:
         case ENTupleColumnType.kSplitUInt16:
         case ENTupleColumnType.kSplitReal16:
            byteSize = 2;
            break;
         default:
            throw new Error(`Unsupported split coltype: ${coltype} (0x${coltype.toString(16).padStart(2, '0')})`);
      }

      const splitView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength),
            count = blob.byteLength / byteSize,
            outBuffer = new ArrayBuffer(blob.byteLength),
            outBytes = new Uint8Array(outBuffer);

      for (let i = 0; i < count; ++i) {
         for (let b = 0; b < byteSize; ++b) {
            const splitIndex = b * count + i,
                  byte = splitView.getUint8(splitIndex),
                  writeIndex = i * byteSize + b;
            outBytes[writeIndex] = byte;
         }
      }

      // Return updated blob and remapped coltype
      const newBlob = outBuffer;
      let newColtype;
      switch (coltype) {
         case ENTupleColumnType.kSplitUInt16:
            newColtype = ENTupleColumnType.kUInt16;
            break;
         case ENTupleColumnType.kSplitUInt32:
            newColtype = ENTupleColumnType.kUInt32;
            break;
         case ENTupleColumnType.kSplitUInt64:
            newColtype = ENTupleColumnType.kUInt64;
            break;
         case ENTupleColumnType.kSplitIndex32:
            newColtype = ENTupleColumnType.kIndex32;
            break;
         case ENTupleColumnType.kSplitIndex64:
            newColtype = ENTupleColumnType.kIndex64;
            break;
         case ENTupleColumnType.kSplitReal16:
            newColtype = ENTupleColumnType.kReal16;
            break;
         case ENTupleColumnType.kSplitReal32:
            newColtype = ENTupleColumnType.kReal32;
            break;
         case ENTupleColumnType.kSplitReal64:
            newColtype = ENTupleColumnType.kReal64;
            break;
         case ENTupleColumnType.kSplitInt16:
            newColtype = ENTupleColumnType.kInt16;
            break;
         case ENTupleColumnType.kSplitInt32:
            newColtype = ENTupleColumnType.kInt32;
            break;
         case ENTupleColumnType.kSplitInt64:
            newColtype = ENTupleColumnType.kInt64;
            break;
         default:
            throw new Error(`Unsupported split coltype for reassembly: ${coltype}`);
      }

      return { blob: newBlob, coltype: newColtype };
   }

   // If no split type, return original blob and coltype
   return { blob, coltype };
}


/**
 * @summary Decode a reconstructed index buffer (32- or 64-bit deltas to absolute indices)
 */
function DecodeDeltaIndex(blob, coltype) {
   let deltas, result;

   if (coltype === ENTupleColumnType.kIndex32) {
      deltas = new Int32Array(blob.buffer || blob, blob.byteOffset || 0, blob.byteLength / 4);
      result = new Int32Array(deltas.length);
   } else if (coltype === ENTupleColumnType.kIndex64) {
      deltas = new BigInt64Array(blob.buffer || blob, blob.byteOffset || 0, blob.byteLength / 8);
      result = new BigInt64Array(deltas.length);
   } else
      throw new Error(`DecodeDeltaIndex: unsupported column type ${coltype}`);

   if (deltas.length > 0)
      result[0] = deltas[0];
   for (let i = 1; i < deltas.length; ++i)
      result[i] = result[i - 1] + deltas[i];

   return { blob: result, coltype };
}

/**
 * @summary Decode a reconstructed signed integer buffer using ZigZag encoding
  */
function decodeZigzag(blob, coltype) {
   let zigzag, result;

   if (coltype === ENTupleColumnType.kInt16) {
      zigzag = new Uint16Array(blob.buffer || blob, blob.byteOffset || 0, blob.byteLength / 2);
      result = new Int16Array(zigzag.length);
   } else if (coltype === ENTupleColumnType.kInt32) {
      zigzag = new Uint32Array(blob.buffer || blob, blob.byteOffset || 0, blob.byteLength / 4);
      result = new Int32Array(zigzag.length);
   } else if (coltype === ENTupleColumnType.kInt64) {
      zigzag = new BigUint64Array(blob.buffer || blob, blob.byteOffset || 0, blob.byteLength / 8);
      result = new BigInt64Array(zigzag.length);
   } else
      throw new Error(`decodeZigzag: unsupported column type ${coltype}`);

   for (let i = 0; i < zigzag.length; ++i) {
      // ZigZag decode: (x >>> 1) ^ (-(x & 1))
      const x = zigzag[i];
      result[i] = (x >>> 1) ^ (-(x & 1));
   }

   return { blob: result, coltype };
}

// Envelope Types
// TODO: Define usage logic for envelope types in future
// const kEnvelopeTypeHeader = 0x01,
//       kEnvelopeTypeFooter = 0x02,
//       kEnvelopeTypePageList = 0x03,

// Field Flags
const kFlagRepetitiveField = 0x01,
      kFlagProjectedField = 0x02,
      kFlagHasTypeChecksum = 0x04,

   // Column Flags
      kFlagDeferredColumn = 0x01,
      kFlagHasValueRange = 0x02;

class RNTupleDescriptorBuilder {

   deserializeHeader(header_blob) {
      if (!header_blob)
         return;

      const reader = new RBufferReader(header_blob),

            payloadStart = reader.offset,
            // Read the envelope metadata
            {
               envelopeLength
            } = this._readEnvelopeMetadata(reader),

            // Seek to end of envelope to get checksum
            checksumPos = payloadStart + envelopeLength - 8,
            currentPos = reader.offset;

      reader.seek(checksumPos);
      this.headerEnvelopeChecksum = reader.readU64();

      reader.seek(currentPos);

      //  Read feature flags list (may span multiple 64-bit words)
      this._readFeatureFlags(reader);

      //  Read metadata strings
      this.name = reader.readString();
      this.description = reader.readString();
      this.writer = reader.readString();

      // 4 list frames inside the header envelope
      this._readSchemaDescription(reader);
   }

   deserializeFooter(footer_blob) {
      if (!footer_blob)
         return;

      const reader = new RBufferReader(footer_blob);

      // Read the envelope metadata
      this._readEnvelopeMetadata(reader);


      // Feature flag(32 bits)
      this._readFeatureFlags(reader);
      // Header checksum (64-bit xxhash3)
      const headerChecksumFromFooter = reader.readU64();
      if (headerChecksumFromFooter !== this.headerEnvelopeChecksum)
         throw new Error('RNTuple corrupted: header checksum does not match footer checksum.');

      const schemaExtensionSize = reader.readS64();

      if (schemaExtensionSize < 0)
         throw new Error('Schema extension frame is not a record frame, which is unexpected.');

      // Schema extension record frame (4 list frames inside)
      this._readSchemaDescription(reader);

      // Cluster Group record frame
      this._readClusterGroups(reader);
   }


   _readEnvelopeMetadata(reader) {
      const typeAndLength = reader.readU64(),

            // Envelope metadata
            // The 16 bits are the envelope type ID, and the 48 bits are the envelope length
            envelopeType = Number(typeAndLength & 0xFFFFn),
            envelopeLength = Number((typeAndLength >> 16n) & 0xFFFFFFFFFFFFn);

      return {
         envelopeType,
         envelopeLength
      };
   }

   _readSchemaDescription(reader) {
      // Reading new descriptor arrays from the input
      const newFields = this._readFieldDescriptors(reader),
            newColumns = this._readColumnDescriptors(reader),
            newAliases = this._readAliasColumn(reader),
            newExtra = this._readExtraTypeInformation(reader);

      // Merging these new arrays into existing arrays
      this.fieldDescriptors = (this.fieldDescriptors || []).concat(newFields);
      this.columnDescriptors = (this.columnDescriptors || []).concat(newColumns);
      this.aliasColumns = (this.aliasColumns || []).concat(newAliases);
      this.extraTypeInfo = (this.extraTypeInfo || []).concat(newExtra);
   }


   _readFeatureFlags(reader) {
      this.featureFlags = [];
      while (true) {
         const val = reader.readU64();
         this.featureFlags.push(val);
         if ((val & 0x8000000000000000n) === 0n)
            break; // MSB not set: end of list
      }

      // verify all feature flags are zero
      if (this.featureFlags.some(v => v !== 0n))
         throw new Error('Unexpected non-zero feature flags: ' + this.featureFlags);
   }

   _readFieldDescriptors(reader) {
      const startOffset = BigInt(reader.offset),
            fieldListSize = reader.readS64(), // signed 64-bit
            fieldListIsList = fieldListSize < 0;


      if (!fieldListIsList)
         throw new Error('Field list frame is not a list frame, which is required.');

      const fieldListCount = reader.readU32(), // number of field entries
         // List frame: list of field record frames

            fieldDescriptors = [];
      for (let i = 0; i < fieldListCount; ++i) {
         const recordStart = BigInt(reader.offset),
               fieldRecordSize = reader.readS64(),
               fieldVersion = reader.readU32(),
               typeVersion = reader.readU32(),
               parentFieldId = reader.readU32(),
               structRole = reader.readU16(),
               flags = reader.readU16(),

               fieldName = reader.readString(),
               typeName = reader.readString(),
               typeAlias = reader.readString(),
               description = reader.readString();
         let arraySize = null,
             sourceFieldId = null,
             checksum = null;

         if (flags & kFlagRepetitiveField)
            arraySize = reader.readU64();

         if (flags & kFlagProjectedField)
            sourceFieldId = reader.readU32();

         if (flags & kFlagHasTypeChecksum)
            checksum = reader.readU32();


         fieldDescriptors.push({
            fieldVersion,
            typeVersion,
            parentFieldId,
            structRole,
            flags,
            fieldName,
            typeName,
            typeAlias,
            description,
            arraySize,
            sourceFieldId,
            checksum
         });
         reader.seek(Number(recordStart + fieldRecordSize));
      }
      reader.seek(Number(startOffset - fieldListSize));
      return fieldDescriptors;
   }

   _readColumnDescriptors(reader) {
      const startOffset = BigInt(reader.offset),
            columnListSize = reader.readS64(),
            columnListIsList = columnListSize < 0;
      if (!columnListIsList)
         throw new Error('Column list frame is not a list frame, which is required.');
      const columnListCount = reader.readU32(), // number of column entries
            columnDescriptors = [];
      for (let i = 0; i < columnListCount; ++i) {
         const recordStart = BigInt(reader.offset),
               columnRecordSize = reader.readS64(),
               coltype = reader.readU16(),
               bitsOnStorage = reader.readU16(),
               fieldId = reader.readU32(),
               flags = reader.readU16(),
               representationIndex = reader.readU16();
         let firstElementIndex = null,
             minValue = null,
             maxValue = null;

         if (flags & kFlagDeferredColumn)
            firstElementIndex = reader.readU64();

         if (flags & kFlagHasValueRange) {
            minValue = reader.readF64();
            maxValue = reader.readF64();
         }


         const column = {
            coltype,
            bitsOnStorage,
            fieldId,
            flags,
            representationIndex,
            firstElementIndex,
            minValue,
            maxValue,
            index: i
         };
         column.isDeferred = function() {
            return (this.flags & RNTupleDescriptorBuilder.kFlagDeferredColumn) !== 0;
         };
         column.isSuppressed = function() {
            return this.firstElementIndex !== null && this.firstElementIndex < 0;
         };

         columnDescriptors.push(column);
         reader.seek(Number(recordStart + columnRecordSize));
      }
      reader.seek(Number(startOffset - columnListSize));
      return columnDescriptors;
   }
   _readAliasColumn(reader) {
      const startOffset = BigInt(reader.offset),
            aliasColumnListSize = reader.readS64(),
            aliasListisList = aliasColumnListSize < 0;
      if (!aliasListisList)
         throw new Error('Alias column list frame is not a list frame, which is required.');
      const aliasColumnCount = reader.readU32(), // number of alias column entries
            aliasColumns = [];
      for (let i = 0; i < aliasColumnCount; ++i) {
         const recordStart = BigInt(reader.offset),
               aliasColumnRecordSize = reader.readS64(),
               physicalColumnId = reader.readU32(),
               fieldId = reader.readU32();
         aliasColumns.push({
            physicalColumnId,
            fieldId
         });
         reader.seek(Number(recordStart + aliasColumnRecordSize));
      }
      reader.seek(Number(startOffset - aliasColumnListSize));
      return aliasColumns;
   }
   _readExtraTypeInformation(reader) {
      const startOffset = BigInt(reader.offset),
            extraTypeInfoListSize = reader.readS64(),
            isList = extraTypeInfoListSize < 0;

      if (!isList)
         throw new Error('Extra type info frame is not a list frame, which is required.');

      const entryCount = reader.readU32(),

            extraTypeInfo = [];
      for (let i = 0; i < entryCount; ++i) {
         const recordStart = BigInt(reader.offset),
               extraTypeInfoRecordSize = reader.readS64(),
               contentId = reader.readU32(),
               typeVersion = reader.readU32();
         extraTypeInfo.push({
            contentId,
            typeVersion
         });
         reader.seek(Number(recordStart + extraTypeInfoRecordSize));
      }
      reader.seek(Number(startOffset - extraTypeInfoListSize));
      return extraTypeInfo;
   }
   _readClusterGroups(reader) {
      const startOffset = BigInt(reader.offset),
            clusterGroupListSize = reader.readS64(),
            isList = clusterGroupListSize < 0;
      if (!isList)
         throw new Error('Cluster group frame is not a list frame');

      const groupCount = reader.readU32(),

            clusterGroups = [];

      for (let i = 0; i < groupCount; ++i) {
         const recordStart = BigInt(reader.offset),
               clusterRecordSize = reader.readS64(),
               minEntry = reader.readU64(),
               entrySpan = reader.readU64(),
               numClusters = reader.readU32(),
               pageListLength = reader.readU64(),


            // Locator method to get the page list locator offset
               pageListLocator = this._readLocator(reader),


               group = {
                  minEntry,
                  entrySpan,
                  numClusters,
                  pageListLocator,
                  pageListLength
               };
         clusterGroups.push(group);
         reader.seek(Number(recordStart + clusterRecordSize));
      }
      reader.seek(Number(startOffset - clusterGroupListSize));
      this.clusterGroups = clusterGroups;
   }

   _readLocator(reader) {
      const sizeAndType = reader.readU32(); // 4 bytes: size + T bit
      if ((sizeAndType | 0) < 0) // | makes the sizeAndType as signed
         throw new Error('Non-standard locators (T=1) not supported yet');
      const size = sizeAndType,
            offset = reader.readU64(); // 8 bytes: offset
      return {
         size,
         offset
      };
   }
   deserializePageList(page_list_blob) {
      if (!page_list_blob)
         throw new Error('deserializePageList: received an invalid or empty page list blob');

      const reader = new RBufferReader(page_list_blob);
      this._readEnvelopeMetadata(reader);
      // Page list checksum (64-bit xxhash3)
      const pageListHeaderChecksum = reader.readU64();
      if (pageListHeaderChecksum !== this.headerEnvelopeChecksum)
         throw new Error('RNTuple corrupted: header checksum does not match Page List Header checksum.');

      const listStartOffset = BigInt(reader.offset),
            // Read cluster summaries list frame
            clusterSummaryListSize = reader.readS64();
      if (clusterSummaryListSize >= 0)
         throw new Error('Expected a list frame for cluster summaries');
      const clusterSummaryCount = reader.readU32(),

            clusterSummaries = [];

      for (let i = 0; i < clusterSummaryCount; ++i) {
         const recordStart = BigInt(reader.offset),
               clusterSummaryRecordSize = reader.readS64(),
               firstEntry = reader.readU64(),
               combined = reader.readU64(),
               flags = combined >> 56n;
         if (flags & 0x01n)
            throw new Error('Cluster summary uses unsupported sharded flag (0x01)');
         const numEntries = Number(combined & 0x00FFFFFFFFFFFFFFn);
         clusterSummaries.push({
            firstEntry,
            numEntries,
            flags
         });
         reader.seek(Number(recordStart + clusterSummaryRecordSize));
      }
      reader.seek(Number(listStartOffset - clusterSummaryListSize));
      this.clusterSummaries = clusterSummaries;
      this._readNestedFrames(reader);

      /* const checksumPagelist = */ reader.readU64();
   }

   _readNestedFrames(reader) {
      const clusterPageLocations = [],
            numListClusters = reader.readS64();
      if (numListClusters >= 0)
         throw new Error('Expected list frame for clusters');
      const numRecordCluster = reader.readU32();

      for (let i = 0; i < numRecordCluster; ++i) {
         const outerListSize = reader.readS64();
         if (outerListSize >= 0)
            throw new Error('Expected outer list frame for columns');

         const numColumns = reader.readU32(),
               columns = [];

         for (let c = 0; c < numColumns; ++c) {
            const innerListSize = reader.readS64();
            if (innerListSize >= 0)
               throw new Error('Expected inner list frame for pages');

            const numPages = reader.readU32(),
                  pages = [];

            for (let p = 0; p < numPages; ++p) {
               const numElementsWithBit = reader.readS32(),
                     hasChecksum = numElementsWithBit < 0,
                     numElements = BigInt(Math.abs(Number(numElementsWithBit))),

                     locator = this._readLocator(reader);
               pages.push({
                  numElements,
                  hasChecksum,
                  locator
               });
            }

            const elementOffset = reader.readS64(),
                  isSuppressed = elementOffset < 0;

            let compression = null;
            if (!isSuppressed)
               compression = reader.readU32();

            columns.push({
               pages,
               elementOffset,
               isSuppressed,
               compression
            });
         }

         clusterPageLocations.push(columns);
      }

      this.pageLocations = clusterPageLocations;
   }

   // Example Of Deserializing Page Content
   deserializePage(blob, columnDescriptor, pageInfo) {
      const originalColtype = columnDescriptor.coltype,
            {
               coltype
            } = recontructUnsplitBuffer(blob, columnDescriptor);
      let {
         blob: processedBlob
      } = recontructUnsplitBuffer(blob, columnDescriptor);


      // Handle split index types
      if (originalColtype === ENTupleColumnType.kSplitIndex32 || originalColtype === ENTupleColumnType.kSplitIndex64) {
         const {
            blob: decodedArray
         } = DecodeDeltaIndex(processedBlob, coltype);
         processedBlob = decodedArray;
      }

      // Handle Split Signed Int types
      if (originalColtype === ENTupleColumnType.kSplitInt16 || originalColtype === ENTupleColumnType.kSplitInt32 || originalColtype === ENTupleColumnType.kSplitInt64) {
         const {
            blob: decodedArray
         } = decodeZigzag(processedBlob, coltype);
         processedBlob = decodedArray;
      }

      const reader = new RBufferReader(processedBlob),
            values = [],

            // Use numElements from pageInfo parameter
            numValues = Number(pageInfo.numElements),
            // Helper for all simple types
            extractValues = (readFunc) => {
               for (let i = 0; i < numValues; ++i)
                  values.push(readFunc());
            };
      switch (coltype) {
         case ENTupleColumnType.kBit: {
            let bitCount = 0;
            const totalBitsInBuffer = processedBlob.byteLength * 8;
            if (totalBitsInBuffer < numValues)
               throw new Error(`kBit: Not enough bits in buffer (${totalBitsInBuffer}) for numValues (${numValues})`);

            for (let byteIndex = 0; byteIndex < processedBlob.byteLength; ++byteIndex) {
               const byte = reader.readU8();

               // Extract 8 bits from this byte
               for (let bitPos = 0; bitPos < 8 && bitCount < numValues; ++bitPos, ++bitCount) {
                  const bitValue = (byte >>> bitPos) & 1,
                        boolValue = bitValue === 1;
                  values.push(boolValue);
               }
            }
            break;
         }

         case ENTupleColumnType.kReal64:
            extractValues(reader.readF64.bind(reader));
            break;
         case ENTupleColumnType.kReal32:
            extractValues(reader.readF32.bind(reader));
            break;
         case ENTupleColumnType.kInt64:
            extractValues(reader.readS64.bind(reader));
            break;
         case ENTupleColumnType.kUInt64:
            extractValues(reader.readU64.bind(reader));
            break;
         case ENTupleColumnType.kInt32:
            extractValues(reader.readS32.bind(reader));
            break;
         case ENTupleColumnType.kUInt32:
            extractValues(reader.readU32.bind(reader));
            break;
         case ENTupleColumnType.kInt16:
            extractValues(reader.readS16.bind(reader));
            break;
         case ENTupleColumnType.kUInt16:
            extractValues(reader.readU16.bind(reader));
            break;
         case ENTupleColumnType.kInt8:
            extractValues(reader.readS8.bind(reader));
            break;
         case ENTupleColumnType.kUInt8:
         case ENTupleColumnType.kByte:
            extractValues(reader.readU8.bind(reader));
            break;
         case ENTupleColumnType.kChar:
            extractValues(() => String.fromCharCode(reader.readS8()));
            break;
         case ENTupleColumnType.kIndex32:
            extractValues(reader.readS32.bind(reader));
            break;
         case ENTupleColumnType.kIndex64:
            extractValues(reader.readS64.bind(reader));
            break;
         default:
            throw new Error(`Unsupported column type: ${columnDescriptor.coltype}`);
      }
      return values;
   }

} // class RNTupleDescriptorBuilder


/** @summary Very preliminary function to read header/footer from RNTuple
 * @private */
async function readHeaderFooter(tuple) {
   // if already read - return immediately, make possible to call several times
   if (tuple?.builder)
      return true;

   if (!tuple.$file)
      return false;

   // request header and footer buffers from the file
   return tuple.$file.readBuffer([tuple.fSeekHeader, tuple.fNBytesHeader, tuple.fSeekFooter, tuple.fNBytesFooter]).then(blobs => {
      if (blobs?.length !== 2)
         return false;

      // Handle both compressed and uncompressed cases
      const processBlob = (blob, uncompressedSize) => {
         // If uncompressedSize matches blob size, it's uncompressed
         if (blob.byteLength === uncompressedSize)
            return Promise.resolve(blob);
         return R__unzip(blob, uncompressedSize);
      };

      return Promise.all([
         processBlob(blobs[0], tuple.fLenHeader),
         processBlob(blobs[1], tuple.fLenFooter)
      ]).then(unzip_blobs => {
         const [header_blob, footer_blob] = unzip_blobs;
         if (!header_blob || !footer_blob)
            return false;

         tuple.builder = new RNTupleDescriptorBuilder;
         tuple.builder.deserializeHeader(header_blob);
         tuple.builder.deserializeFooter(footer_blob);

         // Build fieldToColumns mapping
         tuple.fieldToColumns = {};
         for (const colDesc of tuple.builder.columnDescriptors) {
            const fieldDesc = tuple.builder.fieldDescriptors[colDesc.fieldId],
                  fieldName = fieldDesc.fieldName;
            if (!tuple.fieldToColumns[fieldName])
               tuple.fieldToColumns[fieldName] = [];
            tuple.fieldToColumns[fieldName].push(colDesc);
         }

         // Deserialize Page List
         const group = tuple.builder.clusterGroups?.[0];
         if (!group || !group.pageListLocator)
            throw new Error('No valid cluster group or page list locator found');

         const offset = Number(group.pageListLocator.offset),
               size = Number(group.pageListLocator.size),
               uncompressedSize = Number(group.pageListLength);

         return tuple.$file.readBuffer([offset, size]).then(page_list_blob => {
            if (!(page_list_blob instanceof DataView))
               throw new Error(`Expected DataView from readBuffer, got ${Object.prototype.toString.call(page_list_blob)}`);

            // Check if page list data is uncompressed
            if (page_list_blob.byteLength === uncompressedSize) {
               // Data is uncompressed, use directly
               tuple.builder.deserializePageList(page_list_blob);
               return true;
            }
            // Attempt to decompress the page list
            return R__unzip(page_list_blob, uncompressedSize).then(unzipped_blob => {
               if (!(unzipped_blob instanceof DataView))
                  throw new Error(`Unzipped page list is not a DataView, got ${Object.prototype.toString.call(unzipped_blob)}`);

               tuple.builder.deserializePageList(unzipped_blob);
               return true;
            });
         });
      });
   }).catch(err => {
      console.error('Error during readHeaderFooter execution:', err);
      throw err;
   });
}

function readEntry(rntuple, fieldName, clusterIndex, entryIndex) {
   const builder = rntuple.builder,
         field = builder.fieldDescriptors.find(f => f.fieldName === fieldName),
         columns = rntuple.fieldToColumns[fieldName];

   if (!field)
      throw new Error(`No descriptor for field ${fieldName}`);
   if (!columns)
      throw new Error(`No columns field ${fieldName}`);


   const pages = builder.pageLocations[clusterIndex]?.[columns[0].index]?.pages;
   if (!pages)
      throw new Error(`No pages found ${fieldName}`);

   let pageid = 0;
   while ((pageid < pages.length - 1) && (entryIndex >= Number(pages[pageid].numElements))) {
      entryIndex -= Number(pages[pageid].numElements);
      pageid++;
   }

   if (field.typeName === 'std::string') {
      // string extracted from two columns
      const offsets = rntuple._clusterData[columns[0].index][pageid],
            payload = rntuple._clusterData[columns[1].index][pageid],
            start = entryIndex === 0 ? 0 : Number(offsets[entryIndex - 1]),
            end = Number(offsets[entryIndex]);
      return payload.slice(start, end).join(''); // Convert to string
   }
   const values = rntuple._clusterData[columns[0].index];
   return values[pageid][entryIndex];
}

/** @summary Return field name for specified branch index
 * @desc API let use field name in selector or field object itself */
function getSelectorFieldName(selector, i) {
   const br = selector.getBranch(i);
   return isStr(br) ? br : br?.fieldName;
}

// Read and process the next data cluster from the RNTuple
async function readNextCluster(rntuple, selector) {
   const builder = rntuple.builder;

   // Add validation
   if (!builder.clusterSummaries)
      throw new Error('No cluster summaries available - possibly incomplete file reading');

   const clusterIndex = selector.currentCluster,
         clusterSummary = builder.clusterSummaries[clusterIndex],
      // Gather all pages for this cluster from selected fields only
         pages = [],
      // Collect only selected field names from selector
         selectedFields = [];

   if (!clusterSummary) {
      selector.Terminate(clusterIndex > 0);
      return false;
   }

   for (let i = 0; i < selector.numBranches(); ++i)
      selectedFields.push(getSelectorFieldName(selector, i));

   // For each selected field, collect its columns' pages
   for (const fieldName of selectedFields) {
      const columns = rntuple.fieldToColumns[fieldName];
      if (!columns)
         throw new Error(`Selected field '${fieldName}' not found in RNTuple`);

      for (const colDesc of columns) {
         const colEntry = builder.pageLocations[clusterIndex]?.[colDesc.index];

         // When the data is missing or broken
         if (!colEntry || !colEntry.pages)
            throw new Error(`No pages for column ${colDesc.index} in cluster ${clusterIndex}`);

         for (const page of colEntry.pages)
            pages.push({ page, colDesc, fieldName });
      }
   }

   selector.currentCluster++;

   // Early exit if no pages to read (i.e., no selected fields matched)
   if (pages.length === 0) {
      selector.Terminate(false);
      return false;
   }

   // Build flat array of [offset, size, offset, size, ...] to read pages
   const dataToRead = pages.flatMap(p =>
      [Number(p.page.locator.offset), Number(p.page.locator.size)]
   );

   return rntuple.$file.readBuffer(dataToRead).then(blobsRaw => {
      const blobs = Array.isArray(blobsRaw) ? blobsRaw : [blobsRaw],
            unzipPromises = blobs.map((blob, idx) => {
               const { page, colDesc } = pages[idx],
                     colEntry = builder.pageLocations[clusterIndex][colDesc.index], // Access column entry
                     numElements = Number(page.numElements),
                     elementSize = colDesc.bitsOnStorage / 8;

               // Check if data is compressed
               if (colEntry.compression === 0)
                  return Promise.resolve(blob); // Uncompressed: use blob directly
               const expectedSize = numElements * elementSize;

               // Special handling for boolean fields
               if (colDesc.coltype === ENTupleColumnType.kBit) {
                  const expectedBoolSize = Math.ceil(numElements / 8);
                  if (blob.byteLength === expectedBoolSize)
                     return Promise.resolve(blob);
                  // Try decompression but catch errors for boolean fields
                  return R__unzip(blob, expectedBoolSize).catch(err => {
                     throw new Error(`Failed to unzip boolean page ${idx}: ${err.message}`);
                  });
               }

               // If the blob is already the expected size, treat as uncompressed
               if (blob.byteLength === expectedSize)
                  return Promise.resolve(blob);

               // Try decompression
               return R__unzip(blob, expectedSize).then(result => {
                  if (!result)
                     return blob; // Fallback to original blob
                  return result;
               }).catch(err => {
                  throw new Error(`Failed to unzip page ${idx}: ${err.message}`);
               });
            });

      return Promise.all(unzipPromises).then(unzipBlobs => {
         rntuple._clusterData = {}; // store deserialized data per column index

         for (let i = 0; i < unzipBlobs.length; ++i) {
            const blob = unzipBlobs[i];
            // Ensure blob is a DataView
            if (!(blob instanceof DataView))
               throw new Error(`Invalid blob type for page ${i}: ${Object.prototype.toString.call(blob)}`);
            const colDesc = pages[i].colDesc,
                  values = builder.deserializePage(blob, colDesc, pages[i].page);

            // Support multiple representations (e.g., string fields with offsets + payload)
            if (!rntuple._clusterData[colDesc.index])
               rntuple._clusterData[colDesc.index] = [];

            rntuple._clusterData[colDesc.index].push(values);
         }

         const numEntries = clusterSummary.numEntries;
         for (let i = 0; i < numEntries; ++i) {
            for (let b = 0; b < selector.numBranches(); ++b) {
               const fieldName = getSelectorFieldName(selector, b),
                     tgtName = selector.nameOfBranch(b);

               selector.tgtobj[tgtName] = readEntry(rntuple, fieldName, clusterIndex, i);
            }
            selector.Process(selector.currentEntry++);
         }

         return readNextCluster(rntuple, selector);
      });
   });
}

// TODO args can later be used to filter fields, limit entries, etc.
// Create reader and deserialize doubles from the buffer
function rntupleProcess(rntuple, selector, args) {
   return readHeaderFooter(rntuple).then(() => {
      selector.Begin();
      selector.currentCluster = 0;
      selector.currentEntry = 0;
      return readNextCluster(rntuple, selector);
   }).then(() => selector);
}

class TDrawSelectorTuple extends TDrawSelector {

   /** @summary Return total number of entries
     * @desc TODO: check implementation details ! */
   getNumEntries(tuple) {
      let cnt = 0;
      tuple?.builder.clusterSummaries.forEach(summary => { cnt += summary.numEntries; });
      return cnt;
   }

   /** @summary Search for field in tuple
     * @desc TODO: Can be more complex when name includes extra parts referencing member or collection size or more  */
   findBranch(tuple, name) {
      return tuple.builder?.fieldDescriptors.find(field => {
         return field.fieldName === name;
      });
   }

   /** @summary Returns true if field can be used as array */
   isArrayBranch(/* tuple, br */) { return false; }

   /** @summary Begin of processing */
   Begin(tree) {
      super.Begin(tree);
      this._first_entry = true;
      this._has_bigint = false;
   }

   /** @summary Process entry */
   Process(entry) {
      if (this._first_entry || this._has_bigint) {
         for (let n = 0; n < this.numBranches(); ++n) {
            const name = this.nameOfBranch(n);
            if (typeof this.tgtobj[name] === 'bigint') {
               this.tgtobj[name] = Number(this.tgtobj[name]);
               this._has_bigint = true;
            }
         }
      }

      this._first_entry = false;

      super.Process(entry);
   }

} // class TDrawSelectorTuple


/** @summary implementation of drawing for RNTuple
 * @param {object|string} args - different setting or simply draw expression
 * @param {string} args.expr - draw expression
 * @param {string} [args.cut=undefined] - cut expression (also can be part of 'expr' after '::')
 * @param {string} [args.drawopt=undefined] - draw options for result histogram
 * @param {number} [args.firstentry=0] - first entry to process
 * @param {number} [args.numentries=undefined] - number of entries to process, all by default
 * @param {Array} [args.elist=undefined] - array of entries id to process, all by default
 * @param {boolean} [args.staged] - staged processing, first apply cut to select entries and then perform drawing for selected entries
 * @param {object} [args.branch=undefined] - TBranch object from TTree itself for the direct drawing
 * @param {function} [args.progress=undefined] - function called during histogram accumulation with obj argument
 * @return {Promise} with produced object */

async function rntupleDraw(rntuple, args) {
   if (isStr(args))
      args = { expr: args };
   else if (!isObject(args))
      args = {};

   args.SelectorClass = TDrawSelectorTuple;
   args.processFunction = rntupleProcess;

   return readHeaderFooter(rntuple).then(res_header_footer => {
      return res_header_footer ? treeDraw(rntuple, args) : null;
   });
}


/** @summary Create hierarchy of ROOT::RNTuple object
 * @desc Used by hierarchy painter to explore sub-elements
 * @private */
async function tupleHierarchy(tuple_node, tuple) {
   tuple_node._childs = [];
   // tuple_node._tuple = tuple;  // set reference, will be used later by RNTuple::Draw

   return readHeaderFooter(tuple).then(res => {
      if (!res)
         return res;

      tuple.builder?.fieldDescriptors.forEach(field => {
         const item = {
            _name: field.fieldName,
            _typename: 'ROOT::RNTupleField', // pseudo class name, used in draw.mjs
            _kind: 'ROOT::RNTupleField',
            _title: `Filed of type ${field.typeName}`,
            $tuple: tuple, // reference on tuple, need for drawing
            $field: field
         };

         item._obj = item;

         tuple_node._childs.push(item);
      });

      return true;
   });
}

var rntuple = /*#__PURE__*/Object.freeze({
__proto__: null,
RBufferReader: RBufferReader,
readEntry: readEntry,
readHeaderFooter: readHeaderFooter,
rntupleDraw: rntupleDraw,
rntupleProcess: rntupleProcess,
tupleHierarchy: tupleHierarchy
});

/** @summary function called from draw()
  * @desc just envelope for real TTree::Draw method which do the main job
  * Can be also used for the branch and leaf object
  * @private */
async function drawRNTuple(dom, obj, opt) {
   const args = {};
   let tuple;

   if (obj?.$tuple) {
      // case of fictional ROOT::RNTupleField
      tuple = obj.$tuple;
      args.expr = obj._name;
      if (isStr(opt) && opt.indexOf('dump') === 0)
         args.expr += '>>' + opt;
      else if (opt)
         args.expr += opt;
   } else {
      tuple = obj;
      args.expr = opt;
   }

   if (!tuple)
      throw Error('No RNTuple object available for drawing');

   args.drawid = dom;

   args.progress = treeDrawProgress.bind(args);

   return rntupleDraw(tuple, args).then(res => args.progress(res, true));
}

var RNTuple = /*#__PURE__*/Object.freeze({
__proto__: null,
drawRNTuple: drawRNTuple
});

const kNormal = 1, /* kLessTraffic = 2, */ kOffline = 3;

class RObjectPainter extends ObjectPainter {

   #pending_request;
   #auto_colors; // handle for auto colors

   constructor(dom, obj, opt, csstype) {
      super(dom, obj, opt);
      this.csstype = csstype;
   }

   /** @summary Add painter to pad list of painters
    * @desc For RCanvas also handles common style
    * @protected */
   addToPadPrimitives(pad_painter) {
      const pp = super.addToPadPrimitives(pad_painter);

      if (pp && !this.rstyle && pp.next_rstyle)
         this.rstyle = pp.next_rstyle;

      return pp;
   }

   /** @summary Evaluate v7 attributes using fAttr storage and configured RStyle */
   v7EvalAttr(name, dflt) {
      const obj = this.getObject();
      if (!obj)
         return dflt;
      if (this.cssprefix)
         name = this.cssprefix + name;

      const type_check = res => {
         if (dflt === undefined)
            return res;
         const typ1 = typeof dflt, typ2 = typeof res;
         if (typ1 === typ2)
            return res;
         if (typ1 === 'boolean') {
            if (typ2 === 'string')
               return (res !== '') && (res !== '0') && (res !== 'no') && (res !== 'off');
            return Boolean(res);
         }
         if ((typ1 === 'number') && (typ2 === 'string'))
            return parseFloat(res);
         return res;
      };

      if (obj.fAttr?.m) {
         const value = obj.fAttr.m[name];
         if (value)
            return type_check(value.v); // found value direct in attributes
      }

      if (this.rstyle?.fBlocks) {
         const blks = this.rstyle.fBlocks;
         for (let k = 0; k < blks.length; ++k) {
            const block = blks[k],
                  match = (this.csstype && (block.selector === this.csstype)) ||
                          (obj.fId && (block.selector === ('#' + obj.fId))) ||
                          (obj.fCssClass && (block.selector === ('.' + obj.fCssClass)));

            if (match && block.map?.m) {
               const value = block.map.m[name.toLowerCase()];
               if (value)
                  return type_check(value.v);
            }
         }
      }

      return dflt;
   }

   /** @summary Set v7 attributes value */
   v7SetAttr(name, value) {
      const obj = this.getObject();
      if (this.cssprefix)
         name = this.cssprefix + name;

      if (obj?.fAttr?.m)
         obj.fAttr.m[name] = { v: value };
   }

   /** @summary Decode pad length from string, return pixel value */
   v7EvalLength(name, sizepx, dflt) {
      if (sizepx <= 0)
         sizepx = 1;

      const value = this.v7EvalAttr(name);

      if (value === undefined)
         return Math.round(dflt * sizepx);

      if (typeof value === 'number')
         return Math.round(value * sizepx);

      if (value === null)
         return 0;

      let norm = 0, px = 0, val = value, operand = 0, pos = 0;

      while (val) {
         // skip empty spaces
         while ((pos < val.length) && ((val[pos] === ' ') || (val[pos] === '\t')))
            ++pos;

         if (pos >= val.length)
            break;

         if ((val[pos] === '-') || (val[pos] === '+')) {
            if (operand) {
               console.log(`Fail to parse RPadLength ${value}`);
               return dflt;
            }
            operand = (val[pos] === '-') ? -1 : 1;
            pos++;
            continue;
         }

         if (pos > 0) {
            val = val.slice(pos);
            pos = 0;
         }

         while ((pos < val.length) && (((val[pos] >= '0') && (val[pos] <= '9')) || (val[pos] === '.')))
            pos++;

         const v = parseFloat(val.slice(0, pos));
         if (!Number.isFinite(v)) {
            console.log(`Fail to parse RPadLength ${value}`);
            return Math.round(dflt * sizepx);
         }

         val = val.slice(pos);
         pos = 0;
         if (!operand)
            operand = 1;
         if (val && (val[0] === '%')) {
            val = val.slice(1);
            norm += operand * v * 0.01;
         } else if ((val.length > 1) && (val[0] === 'p') && (val[1] === 'x')) {
            val = val.slice(2);
            px += operand * v;
         } else
            norm += operand * v;

         operand = 0;
      }

      return Math.round(norm * sizepx + px);
   }

   /** @summary Evaluate RColor using attribute storage and configured RStyle */
   v7EvalColor(name, dflt) {
      let val = this.v7EvalAttr(name, '');
      if (!val || !isStr(val))
         return dflt;

      if (val === 'auto') {
         const pp = this.getPadPainter();
         if (pp) {
            val = pp.getAutoColor();
            if (!this.#auto_colors)
               this.#auto_colors = {};
            this.#auto_colors[name] = val;
         } else if (this.#auto_colors && this.#auto_colors[name])
            val = this.#auto_colors[name];
         else {
            console.error(`Autocolor ${name} not defined yet - please check code`);
            val = '';
         }
      } else if (val[0] === '[') {
         const ordinal = parseFloat(val.slice(1, val.length - 1));
         val = 'black';
         if (Number.isFinite(ordinal)) {
            const pal = this.getPadPainter()?.getHistPalette();
            if (pal)
               val = pal.getColorOrdinal(ordinal);
         }
      }

      // to make colors similar in node and in pupperteer
      if ((val[0] === '#') && (isNodeJs() || (isBatchMode() && settings.ApproxTextSize))) {
         const col = color(val);
         if (col.opacity !== 1)
            col.opacity = col.opacity.toFixed(2);
         return col.formatRgb();
      }

      return val;
   }

   /** @summary Evaluate RAttrText properties
     * @return {Object} FontHandler, can be used directly for the text drawing */
   v7EvalFont(name, dflts, fontScale) {
      if (!dflts)
         dflts = {};
      else if (typeof dflts === 'number')
         dflts = { size: dflts };

      const pp = this.getPadPainter(),
            rfont = pp?._dfltRFont || { fFamily: 'Arial', fStyle: '', fWeight: '' },
            text_angle = this.v7EvalAttr(name + '_angle', 0),
            text_align = this.v7EvalAttr(name + '_align', dflts.align || 'none'),
            text_color = this.v7EvalColor(name + '_color', dflts.color || 'none'),
            font_family = this.v7EvalAttr(name + '_font_family', rfont.fFamily || 'Arial'),
            font_style = this.v7EvalAttr(name + '_font_style', rfont.fStyle || ''),
            font_weight = this.v7EvalAttr(name + '_font_weight', rfont.fWeight || '');
      let text_size = this.v7EvalAttr(name + '_size', dflts.size || 12);

      if (isStr(text_size))
         text_size = parseFloat(text_size);
      if (!Number.isFinite(text_size) || (text_size <= 0))
         text_size = 12;
      if (!fontScale)
         fontScale = pp?.getPadHeight() || 100;

      const handler = new FontHandler(null, text_size, fontScale);
      handler.setNameStyleWeight(font_family, font_style, font_weight);

      if (text_angle)
         handler.setAngle(360 - text_angle);
      if (text_align !== 'none')
         handler.setAlign(text_align);
      if (text_color !== 'none')
         handler.setColor(text_color);

      return handler;
   }

   /** @summary Create this.fillatt object based on v7 fill attributes */
   createv7AttFill(prefix) {
      if (!prefix || !isStr(prefix))
         prefix = 'fill_';

      const color = this.v7EvalColor(prefix + 'color', ''),
            pattern = this.v7EvalAttr(prefix + 'style', 0);

      this.createAttFill({ pattern, color, color_as_svg: true });
   }

   /** @summary Create this.lineatt object based on v7 line attributes */
   createv7AttLine(prefix) {
      if (!prefix || !isStr(prefix))
         prefix = 'line_';

      const color = this.v7EvalColor(prefix + 'color', 'black'),
            width = this.v7EvalAttr(prefix + 'width', 1),
            style = this.v7EvalAttr(prefix + 'style', 1);
      let pattern = this.v7EvalAttr(prefix + 'pattern');
      if (pattern && isNodeJs())
         pattern = pattern.split(',').join(', ');

      this.createAttLine({ color, width, style, pattern });

      if (prefix === 'border_')
         this.lineatt.setBorder(this.v7EvalAttr(prefix + 'rx', 0), this.v7EvalAttr(prefix + 'ry', 0));
   }

   /** @summary Create this.markeratt object based on v7 attributes */
   createv7AttMarker(prefix) {
      if (!prefix || !isStr(prefix))
         prefix = 'marker_';

      const color = this.v7EvalColor(prefix + 'color', 'black'),
            size = this.v7EvalAttr(prefix + 'size', 0.01),
            style = this.v7EvalAttr(prefix + 'style', 1),
            refsize = (size >= 1) ? 1 : (this.getPadPainter()?.getPadHeight() || 100);

      this.createAttMarker({ color, size, style, refsize });
   }

   /** @summary Create RChangeAttr, which can be applied on the server side
     * @private */
   v7AttrChange(req, name, value, kind) {
      if (!this.getSnapId())
         return false;

      if (!req._typename) {
         req._typename = `${nsREX}RChangeAttrRequest`;
         req.ids = [];
         req.names = [];
         req.values = [];
         req.update = true;
      }

      if (this.cssprefix)
         name = this.cssprefix + name;
      req.ids.push(this.getSnapId());
      req.names.push(name);

      if ((value === null) || (value === undefined)) {
         if (!kind)
            kind = 'none';
         if (kind !== 'none')
            console.error(`Trying to set ${kind} for none value`);
      }

      if (!kind) {
         switch (typeof value) {
            case 'number': kind = 'double'; break;
            case 'boolean': kind = 'boolean'; break;
         }
      }

      const obj = { _typename: `${nsREX}RAttrMap::` };
      switch (kind) {
         case 'none':
            obj._typename += 'NoValue_t';
            break;
         case 'boolean':
            obj._typename += 'BoolValue_t';
            obj.v = Boolean(value);
            break;
         case 'int':
            obj._typename += 'IntValue_t';
            obj.v = parseInt(value);
            break;
         case 'double':
            obj._typename += 'DoubleValue_t';
            obj.v = parseFloat(value);
            break;
         default:
            obj._typename += 'StringValue_t';
            obj.v = isStr(value) ? value : JSON.stringify(value);
            break;
      }

      req.values.push(obj);
      return true;
   }

   /** @summary Sends accumulated attribute changes to server */
   v7SendAttrChanges(req, do_update) {
      const canp = this.getCanvPainter();
      if (canp && req?._typename) {
         if (do_update !== undefined)
            req.update = Boolean(do_update);
         canp.v7SubmitRequest('', req);
      }
   }

   /** @summary Submit request to server-side drawable
    * @param kind defines request kind, only single request a time can be submitted
    * @param req is object derived from DrawableRequest, including correct _typename
    * @param method is method of painter object which will be called when getting reply */
   v7SubmitRequest(kind, req, method) {
      const canp = this.getCanvPainter();
      if (!isFunc(canp?.submitDrawableRequest))
         return null;

      // special situation when snapid not yet assigned - just keep ref until snapid is there
      // maybe keep full list - for now not clear if really needed
      if (!this.getSnapId()) {
         this.#pending_request = { kind, req, method };
         return req;
      }

      return canp.submitDrawableRequest(kind, req, this, method);
   }

   /** @summary Assign snapid to the painter
     * @desc Overwrite default method */
   assignSnapId(id) {
      super.assignSnapId(id);
      if (this.getSnapId() && this.#pending_request) {
         const p = this.#pending_request;
         this.#pending_request = undefined;
         this.v7SubmitRequest(p.kind, p.req, p.method);
      }
   }

   /** @summary Return communication mode with the server
     * @desc
     * kOffline means no server there,
     * kLessTraffic advise not to send commands if offline functionality available
     * kNormal is standard functionality with RCanvas on server side */
   v7CommMode() {
      const canp = this.getCanvPainter();
      if (!canp || !canp.submitDrawableRequest || !canp.getWebsocket())
         return kOffline;

      return kNormal;
   }

   v7NormalMode() { return this.v7CommMode() === kNormal; }

   v7OfflineMode() { return this.v7CommMode() === kOffline; }

} // class RObjectPainter

/**
 * @summary Axis painter for v7
 *
 * @private
 */

class RAxisPainter extends RObjectPainter {

   /** @summary constructor */
   constructor(dom, arg1, axis, cssprefix) {
      const drawable = cssprefix ? arg1.getObject() : arg1;
      super(dom, drawable, '', cssprefix ? arg1.csstype : 'axis');
      Object.assign(this, AxisPainterMethods);
      this.initAxisPainter();

      this.axis = axis;
      if (cssprefix) { // drawing from the frame
         this.embedded = true; // indicate that painter embedded into the histo painter
         // this.csstype = arg1.csstype; // for the moment only via frame one can set axis attributes
         this.cssprefix = cssprefix;
         this.rstyle = arg1.rstyle;
      } else {
         // this.csstype = 'axis';
         this.cssprefix = 'axis_';
      }
   }

   /** @summary cleanup painter */
   cleanup() {
      delete this.axis;
      delete this.axis_g;
      this.cleanupAxisPainter();
      super.cleanup();
   }

   /** @summary Use in GED to identify kind of axis */
   getAxisType() { return 'RAttrAxis'; }

   /** @summary Configure only base parameters, later same handle will be used for drawing  */
   configureZAxis(name, fp) {
      this.name = name;
      this.kind = kAxisNormal;
      this.log = false;
      const _log = this.v7EvalAttr('log', 0);
      if (_log) {
         this.log = true;
         this.logbase = 10;
         if (Math.abs(_log - Math.exp(1)) < 0.1)
            this.logbase = Math.exp(1);
         else if (_log > 1.9)
            this.logbase = Math.round(_log);
      }
      fp.logz = this.log;
   }

   /** @summary Configure axis painter
     * @desc Axis can be drawn inside frame <g> group with offset to 0 point for the frame
     * Therefore one should distinguish when calculated coordinates used for axis drawing itself or for calculation of frame coordinates
     * @private */
   configureAxis(name, min, max, smin, smax, vertical, frame_range, axis_range, opts) {
      if (!opts)
         opts = {};
      this.name = name;
      this.full_min = min;
      this.full_max = max;
      this.kind = kAxisNormal;
      this.vertical = vertical;
      this.log = false;
      const _log = this.v7EvalAttr('log', 0),
            _symlog = this.v7EvalAttr('symlog', 0);
      this.reverse = opts.reverse || false;

      if (this.v7EvalAttr('time')) {
         this.kind = kAxisTime;
         this.timeoffset = 0;
         let toffset = this.v7EvalAttr('timeOffset');
         if (toffset !== undefined) {
            toffset = parseFloat(toffset);
            if (Number.isFinite(toffset))
               this.timeoffset = toffset * 1000;
         }
      } else if (this.axis?.fLabelsIndex) {
         this.kind = kAxisLabels;
         delete this.own_labels;
      } else if (opts.labels)
         this.kind = kAxisLabels;
      else
         this.kind = kAxisNormal;

      if (this.kind === kAxisTime)
         this.func = time().domain([this.convertDate(smin), this.convertDate(smax)]);
      else if (_symlog && (_symlog > 0)) {
         this.symlog = _symlog;
         this.func = symlog().constant(_symlog).domain([smin, smax]);
      } else if (_log) {
         if (smax <= 0)
            smax = 1;
         if ((smin <= 0) || (smin >= smax))
            smin = smax * 0.0001;
         this.log = true;
         this.logbase = 10;
         if (Math.abs(_log - Math.exp(1)) < 0.1)
            this.logbase = Math.exp(1);
         else if (_log > 1.9)
            this.logbase = Math.round(_log);
         this.func = log().base(this.logbase).domain([smin, smax]);
      } else
         this.func = linear().domain([smin, smax]);

      this.scale_min = smin;
      this.scale_max = smax;

      this.gr_range = axis_range || 1000; // when not specified, one can ignore it

      const range = frame_range ?? [0, this.gr_range];

      this.axis_shift = range[1] - this.gr_range;

      if (this.reverse)
         this.func.range([range[1], range[0]]);
      else
         this.func.range(range);

      if (this.kind === kAxisTime)
         this.gr = val => this.func(this.convertDate(val));
      else if (this.log)
         this.gr = val => { return (val < this.scale_min) ? (this.vertical ? this.func.range()[0] + 5 : -5) : this.func(val); };
      else
         this.gr = this.func;

      delete this.format;// remove formatting func

      const ndiv = this.v7EvalAttr('ndiv', 508);

      this.nticks = ndiv % 100;
      this.nticks2 = (ndiv % 10000 - this.nticks) / 100;
      this.nticks3 = Math.floor(ndiv / 10000);

      this.nticks = Math.min(this.nticks, 20);

      const gr_range = Math.abs(this.gr_range) || 100;

      if (this.kind === kAxisTime) {
         this.nticks = Math.min(this.nticks, 8);

         const scale_range = this.scale_max - this.scale_min,
               tf2 = chooseTimeFormat(scale_range / gr_range, false);
         let tf1 = this.v7EvalAttr('timeFormat', '');

         if (!tf1 || (scale_range < 0.1 * (this.full_max - this.full_min)))
            tf1 = chooseTimeFormat(scale_range / this.nticks, true);

         this.tfunc1 = this.tfunc2 = timeFormat(tf1);
         if (tf2 !== tf1)
            this.tfunc2 = timeFormat(tf2);

         this.format = this.formatTime;
      } else if (this.log) {
         if (this.nticks2 > 1) {
            this.nticks *= this.nticks2; // all log ticks (major or minor) created centrally
            this.nticks2 = 1;
         }
         this.noexp = this.v7EvalAttr('noexp', false);
         if ((this.scale_max < 300) && (this.scale_min > 0.3) && (this.logbase === 10))
            this.noexp = true;
         this.moreloglabels = this.v7EvalAttr('moreloglbls', false);

         this.format = this.formatLog;
      } else if (this.kind === kAxisLabels) {
         this.nticks = 50; // for text output allow max 50 names
         const scale_range = this.scale_max - this.scale_min;
         if (this.nticks > scale_range)
            this.nticks = Math.round(scale_range);
         this.nticks2 = 1;

         this.format = this.formatLabels;
      } else {
         this.order = 0;
         this.ndig = 0;
         this.format = this.formatNormal;
      }
   }

   /** @summary Return scale min */
   getScaleMin() {
      return this.func ? this.func.domain()[0] : 0;
   }

   /** @summary Return scale max */
   getScaleMax() {
      return this.func ? this.func.domain()[1] : 0;
   }

   /** @summary Provide label for axis value */
   formatLabels(d) {
      const indx = Math.round(d);
      if (this.axis?.fLabelsIndex) {
         if ((indx < 0) || (indx >= this.axis.fNBinsNoOver))
            return null;
         for (let i = 0; i < this.axis.fLabelsIndex.length; ++i) {
            const pair = this.axis.fLabelsIndex[i];
            if (pair.second === indx)
               return pair.first;
         }
      } else {
         const labels = this.getObject().fLabels;
         if (labels && (indx >= 0) && (indx < labels.length))
            return labels[indx];
      }
      return null;
   }

   /** @summary Creates array with minor/middle/major ticks */
   createTicks(only_major_as_array, optionNoexp, optionNoopt, optionInt) {
      if (optionNoopt && this.nticks && (this.kind === kAxisNormal))
         this.noticksopt = true;

      const ticks = this.produceTicks(this.nticks),
            handle = { nminor: 0, nmiddle: 0, nmajor: 0, func: this.func, minor: ticks, middle: ticks, major: ticks };

      if (only_major_as_array) {
         const res = handle.major, delta = (this.scale_max - this.scale_min) * 1e-5;
         if (res.at(0) > this.scale_min + delta)
            res.unshift(this.scale_min);
         if (res.at(-1) < this.scale_max - delta)
            res.push(this.scale_max);
         return res;
      }

      if ((this.nticks2 > 1) && (!this.log || (this.logbase === 10))) {
         handle.minor = handle.middle = this.produceTicks(handle.major.length, this.nticks2);

         const gr_range = Math.abs(this.func.range()[1] - this.func.range()[0]);

         // avoid black filling by middle-size
         if ((handle.middle.length <= handle.major.length) || (handle.middle.length > gr_range))
            handle.minor = handle.middle = handle.major;
         else if ((this.nticks3 > 1) && !this.log) {
            handle.minor = this.produceTicks(handle.middle.length, this.nticks3);
            if ((handle.minor.length <= handle.middle.length) || (handle.minor.length > gr_range))
               handle.minor = handle.middle;
         }
      }

      handle.reset = function() {
         this.nminor = this.nmiddle = this.nmajor = 0;
      };

      handle.next = function(doround) {
         if (this.nminor >= this.minor.length)
            return false;

         this.tick = this.minor[this.nminor++];
         this.grpos = this.func(this.tick);
         if (doround)
            this.grpos = Math.round(this.grpos);
         this.kind = 3;

         if ((this.nmiddle < this.middle.length) && (Math.abs(this.grpos - this.func(this.middle[this.nmiddle])) < 1)) {
            this.nmiddle++;
            this.kind = 2;
         }

         if ((this.nmajor < this.major.length) && (Math.abs(this.grpos - this.func(this.major[this.nmajor])) < 1)) {
            this.nmajor++;
            this.kind = 1;
         }
         return true;
      };

      handle.last_major = function() {
         return (this.kind !== 1) ? false : this.nmajor === this.major.length;
      };

      handle.next_major_grpos = function() {
         return this.nmajor >= this.major.length ? null : this.func(this.major[this.nmajor]);
      };

      handle.get_modifier = function() { return null; };

      this.order = 0;
      this.ndig = 0;

      // at the moment when drawing labels, we can try to find most optimal text representation for them

      if ((this.kind === kAxisNormal) && !this.log && handle.major.length) {
         let maxorder = 0, minorder = 0, exclorder3 = false;

         if (!optionNoexp) {
            const maxtick = Math.max(Math.abs(handle.major.at(0)), Math.abs(handle.major.at(-1))),
                  mintick = Math.min(Math.abs(handle.major.at(0)), Math.abs(handle.major.at(-1))),
                  ord1 = (maxtick > 0) ? Math.round(Math.log10(maxtick) / 3) * 3 : 0,
                  ord2 = (mintick > 0) ? Math.round(Math.log10(mintick) / 3) * 3 : 0;

            exclorder3 = (maxtick < 2e4); // do not show 10^3 for values below 20000

            if (maxtick || mintick) {
               maxorder = Math.max(ord1, ord2) + 3;
               minorder = Math.min(ord1, ord2) - 3;
            }
         }

         // now try to find best combination of order and ndig for labels

         let bestorder = 0, bestndig = this.ndig, bestlen = 1e10;

         for (let order = minorder; order <= maxorder; order += 3) {
            if (exclorder3 && (order === 3))
               continue;
            this.order = order;
            this.ndig = 0;
            let lbls = [], indx = 0, totallen = 0;
            while (indx < handle.major.length) {
               const v0 = handle.major[indx],
                     lbl = this.format(v0, true);

               let bad_value = lbls.indexOf(lbl) >= 0;
               if (!bad_value) {
                  try {
                     const v1 = parseFloat(lbl) * Math.pow(10, order);
                     bad_value = (Math.abs(v0) > 1e-30) && (Math.abs(v1 - v0) / Math.abs(v0) > 1e-8);
                  } catch {
                     console.warn('Failure by parsing of', lbl);
                     bad_value = true;
                  }
               }
               if (bad_value) {
                  if (++this.ndig > 15) {
                     totallen += 1e10;
                     break; // not too many digits, anyway it will be exponential
                  }
                  lbls = [];
                  indx = totallen = 0;
               } else {
                  lbls.push(lbl);
                  totallen += lbl.length;
                  indx++;
               }
            }

            // for order === 0 we should virtually remove '0.' and extra label on top
            if (!order && (this.ndig < 4))
               totallen -= (handle.major.length * 2 + 3);

            if (totallen < bestlen) {
               bestlen = totallen;
               bestorder = this.order;
               bestndig = this.ndig;
            }
         }

         this.order = bestorder;
         this.ndig = bestndig;

         if (optionInt) {
            if (this.order)
               console.warn(`Axis painter - integer labels are configured, but axis order ${this.order} is preferable`);
            if (this.ndig)
               console.warn(`Axis painter - integer labels are configured, but ${this.ndig} decimal digits are required`);
            this.ndig = 0;
            this.order = 0;
         }
      }

      return handle;
   }

   /** @summary Is labels should be centered */
   isCenteredLabels() {
      if (this.kind === kAxisLabels)
         return true;
      if (this.kind === 'log')
         return false;
      return this.v7EvalAttr('labels_center', false);
   }

   /** @summary Is labels should be rotated */
   isRotateLabels() { return false; }

   /** @summary Used to move axis labels instead of zooming
     * @private */
   processLabelsMove(arg, pos) {
      if (this.optionUnlab || !this.axis_g)
         return false;

      const label_g = this.axis_g.select('.axis_labels');
      if (!label_g || (label_g.size() !== 1))
         return false;

      if (arg === 'start') {
         // no moving without labels
         const box = label_g.node().getBBox();

         label_g.append('rect')
                 .classed('drag', true)
                 .attr('x', box.x)
                 .attr('y', box.y)
                 .attr('width', box.width)
                 .attr('height', box.height)
                 .style('cursor', 'move')
                 .call(addHighlightStyle, true);
         if (this.vertical)
            this.drag_pos0 = pos[0];
         else
            this.drag_pos0 = pos[1];


         return true;
      }

      let offset = label_g.property('fix_offset');

      if (this.vertical) {
         offset += Math.round(pos[0] - this.drag_pos0);
         makeTranslate(label_g, offset);
      } else {
         offset += Math.round(pos[1] - this.drag_pos0);
         makeTranslate(label_g, 0, offset);
      }
      if (!offset)
         makeTranslate(label_g);

      if (arg === 'stop') {
         label_g.select('rect.drag').remove();
         delete this.drag_pos0;
         if (offset !== label_g.property('fix_offset')) {
            label_g.property('fix_offset', offset);
            const side = label_g.property('side') || 1;
            this.labelsOffset = offset / (this.vertical ? -side : side);
            this.changeAxisAttr(1, 'labels_offset', this.labelsOffset / this.scalingSize);
         }
      }

      return true;
   }

   /** @summary Add interactive elements to draw axes title */
   addTitleDrag(title_g, side) {
      if (!settings.MoveResize || this.isBatchMode())
         return;

      let drag_rect = null,
          acc_x, acc_y, new_x, new_y, alt_pos, curr_indx;

      const drag_move = drag().subject(Object);

      drag_move
         .on('start', evnt => {
            evnt.sourceEvent.preventDefault();
            evnt.sourceEvent.stopPropagation();

            const box = title_g.node().getBBox(), // check that elements visible, request precise value
                  title_length = this.vertical ? box.height : box.width;

            new_x = acc_x = title_g.property('shift_x');
            new_y = acc_y = title_g.property('shift_y');

            if (this.titlePos === 'center')
               curr_indx = 1;
            else
               curr_indx = (this.titlePos === 'left') ? 0 : 2;

            // let d = ((this.gr_range > 0) && this.vertical) ? title_length : 0;
            alt_pos = [0, this.gr_range / 2, this.gr_range]; // possible positions
            const off = this.vertical ? -title_length : title_length,
                  swap = this.isReverseAxis() ? 2 : 0;
            if (this.title_align === 'middle') {
               alt_pos[swap] += off / 2;
               alt_pos[2 - swap] -= off / 2;
            } else if ((this.title_align === 'begin') ^ this.isTitleRotated()) {
               alt_pos[1] -= off / 2;
               alt_pos[2 - swap] -= off;
            } else { // end
               alt_pos[swap] += off;
               alt_pos[1] += off / 2;
            }

            alt_pos[curr_indx] = this.vertical ? acc_y : acc_x;

            drag_rect = title_g.append('rect')
                 .attr('x', box.x)
                 .attr('y', box.y)
                 .attr('width', box.width)
                 .attr('height', box.height)
                 .style('cursor', 'move')
                 .call(addHighlightStyle, true);
            // .style('pointer-events','none'); // let forward double click to underlying elements
         }).on('drag', evnt => {
            if (!drag_rect)
               return;

            evnt.sourceEvent.preventDefault();
            evnt.sourceEvent.stopPropagation();

            acc_x += evnt.dx;
            acc_y += evnt.dy;

            const p = this.vertical ? acc_y : acc_x;
            let set_x, set_y, besti = 0;

            for (let i = 1; i < 3; ++i) {
               if (Math.abs(p - alt_pos[i]) < Math.abs(p - alt_pos[besti]))
                  besti = i;
            }

            if (this.vertical) {
               set_x = acc_x;
               set_y = alt_pos[besti];
            } else {
               set_x = alt_pos[besti];
               set_y = acc_y;
            }

            new_x = set_x;
            new_y = set_y;
            curr_indx = besti;
            makeTranslate(title_g, new_x, new_y);
         }).on('end', evnt => {
            if (!drag_rect)
               return;

            evnt.sourceEvent.preventDefault();
            evnt.sourceEvent.stopPropagation();

            const basepos = title_g.property('basepos') || 0;

            title_g.property('shift_x', new_x)
                      .property('shift_y', new_y);

            this.titleOffset = (this.vertical ? basepos - new_x : new_y - basepos) * side;

            if (curr_indx === 1)
               this.titlePos = 'center';
            else if (curr_indx === 0)
               this.titlePos = 'left';
            else
               this.titlePos = 'right';

            this.changeAxisAttr(0, 'title_position', this.titlePos, 'title_offset', this.titleOffset / this.scalingSize);

            drag_rect.remove();
            drag_rect = null;
         });

      title_g.style('cursor', 'move').call(drag_move);
   }

   /** @summary checks if value inside graphical range, taking into account delta */
   isInsideGrRange(pos, delta1, delta2) {
      if (!delta1)
         delta1 = 0;
      if (delta2 === undefined)
         delta2 = delta1;
      if (this.gr_range < 0)
         return (pos >= this.gr_range - delta2) && (pos <= delta1);
      return (pos >= -delta1) && (pos <= this.gr_range + delta2);
   }

   /** @summary returns graphical range */
   getGrRange(delta) {
      if (!delta)
         delta = 0;
      if (this.gr_range < 0)
         return this.gr_range - delta;
      return this.gr_range + delta;
   }

   /** @summary If axis direction is negative coordinates direction */
   isReverseAxis() {
      return !this.vertical !== (this.getGrRange() > 0);
   }

   /** @summary Draw axis ticks
     * @private */
   drawMainLine(axis_g) {
      let ending = '';

      if (this.endingSize && this.endingStyle) {
         let sz = (this.gr_range > 0) ? -this.endingSize : this.endingSize;
         const sz7 = Math.round(sz * 0.7);
         sz = Math.round(sz);
         if (this.vertical)
            ending = `l${sz7},${sz}M0,${this.gr_range}l${-sz7},${sz}`;
         else
            ending = `l${sz},${sz7}M${this.gr_range},0l${sz},${-sz7}`;
      }

      axis_g.append('svg:path')
            .attr('d', 'M0,0' + (this.vertical ? 'v' : 'h') + this.gr_range + ending)
            .call(this.lineatt.func)
            .style('fill', ending ? 'none' : null);
   }

   /** @summary Draw axis ticks
     * @return {Object} with gaps on left and right side
     * @private */
   drawTicks(axis_g, side, main_draw) {
      if (main_draw)
         this.ticks = [];

      this.handle.reset();

      let res = '', ticks_plusminus = 0;
      if (this.ticksSide === 'both') {
         side = 1;
         ticks_plusminus = 1;
      }

      while (this.handle.next(true)) {
         let h1 = Math.round(this.ticksSize / 4), h2;

         if (this.handle.kind < 3)
            h1 = Math.round(this.ticksSize / 2);

         const grpos = this.handle.grpos - this.axis_shift;

         if ((this.startingSize || this.endingSize) && !this.isInsideGrRange(grpos, -Math.abs(this.startingSize), -Math.abs(this.endingSize)))
            continue;

         if (this.handle.kind === 1) {
            // if not showing labels, not show large tick
            if ((this.kind === kAxisLabels) || (this.format(this.handle.tick, true) !== null))
               h1 = this.ticksSize;

            if (main_draw)
               this.ticks.push(grpos); // keep graphical positions of major ticks
         }

         if (ticks_plusminus > 0)
            h2 = -h1;
         else if (side < 0) {
            h2 = -h1;
            h1 = 0;
         } else
            h2 = 0;

         res += this.vertical ? `M${h1},${grpos}H${h2}` : `M${grpos},${-h1}V${-h2}`;
      }

      if (res) {
         axis_g.append('svg:path')
               .attr('d', res)
               .style('stroke', this.ticksColor || this.lineatt.color)
               .style('stroke-width', !this.ticksWidth || (this.ticksWidth === 1) ? null : this.ticksWidth);
      }

      const gap0 = Math.round(0.25 * this.ticksSize), gap = Math.round(1.25 * this.ticksSize);
      return {
         '-1': (side > 0) || ticks_plusminus ? gap : gap0,
         1: (side < 0) || ticks_plusminus ? gap : gap0
      };
   }

   /** @summary Performs labels drawing
     * @return {Promise} with gaps in both direction */
   async drawLabels(axis_g, side, gaps) {
      const center_lbls = this.isCenteredLabels(),
            rotate_lbls = Boolean(this.labelsFont.angle),
            label_g = axis_g.append('svg:g').attr('class', 'axis_labels').property('side', side),
            lbl_pos = this.handle.lbl_pos || this.handle.major;
      let textscale = 1, maxtextlen = 0, lbls_tilt = false,
          max_lbl_width = 0, max_lbl_height = 0;

      // function called when text is drawn to analyze width, required to correctly scale all labels
      function process_drawtext_ready(painter) {
         max_lbl_width = Math.max(max_lbl_width, this.result_width);
         max_lbl_height = Math.max(max_lbl_height, this.result_height);

         const textwidth = this.result_width;

         if (textwidth && ((!painter.vertical && !rotate_lbls) || (painter.vertical && rotate_lbls)) && !painter.log) {
            const maxwidth = !this.gap_before ? 0.9 * this.gap_after : (!this.gap_after ? 0.9 * this.gap_before : this.gap_before * 0.45 + this.gap_after * 0.45);
            textscale = Math.min(textscale, maxwidth / textwidth);
         }

         if ((textscale > 0.0001) && (textscale < 0.8) && !painter.vertical && !rotate_lbls && (maxtextlen > 5) && (side > 0))
            lbls_tilt = true;

         const scale = textscale * (lbls_tilt ? 3 : 1);
         if ((scale > 0.0001) && (scale < 1))
            painter.scaleTextDrawing(1 / scale, label_g);
      }

      const fix_offset = Math.round((this.vertical ? -side : side) * this.labelsOffset),
            fix_coord = Math.round((this.vertical ? -side : side) * gaps[side]);
      let lastpos = 0;

      if (fix_offset)
         makeTranslate(label_g, this.vertical ? fix_offset : 0, this.vertical ? 0 : fix_offset);

      label_g.property('fix_offset', fix_offset);

      return this.startTextDrawingAsync(this.labelsFont, 'font', label_g).then(() => {
         for (let nmajor = 0; nmajor < lbl_pos.length; ++nmajor) {
            const lbl = this.format(lbl_pos[nmajor], true);
            if (lbl === null)
               continue;

            const arg = { text: lbl, latex: 1, draw_g: label_g };
            let pos = Math.round(this.func(lbl_pos[nmajor]));

            arg.gap_before = (nmajor > 0) ? Math.abs(Math.round(pos - this.func(lbl_pos[nmajor - 1]))) : 0;
            arg.gap_after = (nmajor < lbl_pos.length - 1) ? Math.abs(Math.round(this.func(lbl_pos[nmajor + 1]) - pos)) : 0;

            if (center_lbls) {
               const gap = arg.gap_after || arg.gap_before;
               pos = Math.round(pos - (this.vertical ? 0.5 * gap : -0.5 * gap));
               if (!this.isInsideGrRange(pos, 5))
                  continue;
            }

            maxtextlen = Math.max(maxtextlen, lbl.length);

            pos -= this.axis_shift;

            if ((this.startingSize || this.endingSize) && !this.isInsideGrRange(pos, -Math.abs(this.startingSize), -Math.abs(this.endingSize)))
               continue;

            if (this.vertical) {
               arg.x = fix_coord;
               arg.y = pos;
               arg.align = rotate_lbls ? ((side < 0) ? 23 : 20) : ((side < 0) ? 12 : 32);
            } else {
               arg.x = pos;
               arg.y = fix_coord;
               arg.align = rotate_lbls ? ((side < 0) ? 12 : 32) : ((side < 0) ? 20 : 23);
               if (this.log && !this.noexp && !this.vertical && arg.align === 23) {
                  arg.align = 21;
                  arg.y += this.labelsFont.size;
               }
            }

            arg.post_process = process_drawtext_ready;

            this.drawText(arg);

            if (lastpos && (pos !== lastpos) && ((this.vertical && !rotate_lbls) || (!this.vertical && rotate_lbls))) {
               const axis_step = Math.abs(pos - lastpos);
               textscale = Math.min(textscale, 0.9 * axis_step / this.labelsFont.size);
            }

            lastpos = pos;
         }

         if (this.order) {
            this.drawText({ x: this.vertical ? side * 5 : this.getGrRange(5),
               y: this.has_obstacle ? fix_coord : (this.vertical ? this.getGrRange(3) : -3 * side),
               align: this.vertical ? ((side < 0) ? 30 : 10) : ((this.has_obstacle ^ (side < 0)) ? 13 : 10),
               latex: 1,
               text: '#times' + this.formatExp(10, this.order),
               draw_g: label_g });
         }

         return this.finishTextDrawing(label_g);
      }).then(() => {
         if (lbls_tilt) {
            label_g.selectAll('text').each(function() {
               const txt = select(this), tr = txt.attr('transform');
               txt.attr('transform', tr + ' rotate(25)').style('text-anchor', 'start');
            });
         }

         if (this.vertical)
            gaps[side] += Math.round(rotate_lbls ? 1.2 * max_lbl_height : max_lbl_width + 0.4 * this.labelsFont.size) - side * fix_offset;
         else {
            const tilt_height = lbls_tilt ? max_lbl_width * Math.sin(25 / 180 * Math.PI) + max_lbl_height * (Math.cos(25 / 180 * Math.PI) + 0.2) : 0;

            gaps[side] += Math.round(Math.max(rotate_lbls ? max_lbl_width + 0.4 * this.labelsFont.size : 1.2 * max_lbl_height, 1.2 * this.labelsFont.size, tilt_height)) + fix_offset;
         }

         return gaps;
      });
   }

   /** @summary Add zooming rect to axis drawing */
   addZoomingRect(axis_g, side, lgaps) {
      if (settings.Zooming && !this.disable_zooming && !this.isBatchMode()) {
         const sz = Math.max(lgaps[side], 10),
               d = this.vertical ? `v${this.gr_range}h${-side * sz}v${-this.gr_range}` : `h${this.gr_range}v${side * sz}h${-this.gr_range}`;
         axis_g.append('svg:path')
               .attr('d', `M0,0${d}z`)
               .attr('class', 'axis_zoom')
               .style('opacity', '0')
               .style('cursor', 'crosshair');
      }
   }

   /** @summary Returns true if axis title is rotated */
   isTitleRotated() {
      return this.titleFont && (this.titleFont.angle !== (this.vertical ? 270 : 0));
   }

   /** @summary Draw axis title */
   async drawTitle(axis_g, side, lgaps) {
      if (!this.fTitle)
         return this;

      const title_g = axis_g.append('svg:g').attr('class', 'axis_title'),
            rotated = this.isTitleRotated();

      return this.startTextDrawingAsync(this.titleFont, 'font', title_g).then(() => {
         let title_shift_x, title_shift_y, title_basepos;

         this.title_align = this.titleCenter ? 'middle' : (this.titleOpposite ^ (this.isReverseAxis() || rotated) ? 'begin' : 'end');

         if (this.vertical) {
            title_basepos = Math.round(-side * (lgaps[side]));
            title_shift_x = title_basepos + Math.round(-side * this.titleOffset);
            title_shift_y = Math.round(this.titleCenter ? this.gr_range / 2 : (this.titleOpposite ? 0 : this.gr_range));
            this.drawText({
               align: [this.title_align, ((side < 0) ^ rotated ? 'top' : 'bottom')],
               text: this.fTitle, draw_g: title_g
            });
         } else {
            title_shift_x = Math.round(this.titleCenter ? this.gr_range / 2 : (this.titleOpposite ? 0 : this.gr_range));
            title_basepos = Math.round(side * lgaps[side]);
            title_shift_y = title_basepos + Math.round(side * this.titleOffset);
            this.drawText({
               align: [this.title_align, ((side > 0) ^ rotated ? 'top' : 'bottom')],
               text: this.fTitle, draw_g: title_g
            });
         }

         makeTranslate(title_g, title_shift_x, title_shift_y)
                     .property('basepos', title_basepos)
                     .property('shift_x', title_shift_x)
                     .property('shift_y', title_shift_y);

         this.addTitleDrag(title_g, side);

         return this.finishTextDrawing(title_g);
      });
   }

   /** @summary Extract major draw attributes, which are also used in interactive operations
     * @private  */
   extractDrawAttributes(scalingSize) {
      const pp = this.getPadPainter(),
            rect = pp?.getPadRect() || { width: 10, height: 10 };

      this.scalingSize = scalingSize || (this.vertical ? rect.width : rect.height);

      this.createv7AttLine('line_');

      this.optionUnlab = this.v7EvalAttr('labels_hide', false);

      this.endingStyle = this.v7EvalAttr('ending_style', '');
      this.endingSize = Math.round(this.v7EvalLength('ending_size', this.scalingSize, this.endingStyle ? 0.02 : 0));
      this.startingSize = Math.round(this.v7EvalLength('starting_size', this.scalingSize, 0));
      this.ticksSize = this.v7EvalLength('ticks_size', this.scalingSize, 0.02);
      this.ticksSide = this.v7EvalAttr('ticks_side', 'normal');
      this.ticksColor = this.v7EvalColor('ticks_color', '');
      this.ticksWidth = this.v7EvalAttr('ticks_width', 1);
      if (scalingSize && (this.ticksSize < 0))
         this.ticksSize = -this.ticksSize;

      this.fTitle = this.v7EvalAttr('title_value', '');

      if (this.fTitle) {
         this.titleFont = this.v7EvalFont('title', { size: 0.03 }, scalingSize || pp?.getPadHeight() || 10);
         this.titleFont.roundAngle(180, this.vertical ? 270 : 0);

         this.titleOffset = this.v7EvalLength('title_offset', this.scalingSize, 0);
         this.titlePos = this.v7EvalAttr('title_position', 'right');
         this.titleCenter = (this.titlePos === 'center');
         this.titleOpposite = (this.titlePos === 'left');
      } else {
         delete this.titleFont;
         delete this.titleOffset;
         delete this.titlePos;
      }

      // TODO: remove old scaling factors for labels and ticks
      this.labelsFont = this.v7EvalFont('labels', { size: scalingSize ? 0.05 : 0.03 });
      this.labelsFont.roundAngle(180);
      if (this.labelsFont.angle)
         this.labelsFont.angle = 270;
      this.labelsOffset = this.v7EvalLength('labels_offset', this.scalingSize, 0);

      if (scalingSize)
         this.ticksSize = this.labelsFont.size * 0.5; // old lego scaling factor

      if (this.maxTickSize && (this.ticksSize > this.maxTickSize))
         this.ticksSize = this.maxTickSize;
   }

   /** @summary Performs axis drawing
     * @return {Promise} which resolved when drawing is completed */
   async drawAxis(layer, transform, side) {
      let axis_g = layer;

      if (side === undefined)
         side = 1;

      if (!this.standalone) {
         axis_g = layer.selectChild(`.${this.name}_container`);
         if (axis_g.empty())
            axis_g = layer.append('svg:g').attr('class', `${this.name}_container`);
         else
            axis_g.selectAll('*').remove();
      }

      axis_g.attr('transform', transform);

      this.extractDrawAttributes();
      this.axis_g = axis_g;
      this.side = side;

      if (this.ticksSide === 'invert')
         side = -side;

      if (this.standalone)
         this.drawMainLine(axis_g);

      const optionNoopt = false,  // no ticks position optimization
            optionInt = false,    // integer labels
            optionNoexp = false;  // do not create exp

      this.handle = this.createTicks(false, optionNoexp, optionNoopt, optionInt);

      // first draw ticks
      const tgaps = this.drawTicks(axis_g, side, true),
            labelsPromise = this.optionUnlab ? Promise.resolve(tgaps) : this.drawLabels(axis_g, side, tgaps); // draw labels

      return labelsPromise.then(lgaps => {
         // when drawing axis on frame, zoom rect should be always outside
         this.addZoomingRect(axis_g, this.standalone ? side : this.side, lgaps);

         return this.drawTitle(axis_g, side, lgaps);
      });
   }

   /** @summary Assign handler, which is called when axis redraw by interactive changes
     * @desc Used by palette painter to reassign interactive handlers
     * @private */
   setAfterDrawHandler(handler) {
      this._afterDrawAgain = handler;
   }

   /** @summary Draw axis with the same settings, used by interactive changes */
   drawAxisAgain() {
      if (!this.axis_g || !this.side)
         return;

      this.axis_g.selectAll('*').remove();

      this.extractDrawAttributes();

      let side = this.side;
      if (this.ticksSide === 'invert')
         side = -side;

      if (this.standalone)
         this.drawMainLine(this.axis_g);

      // first draw ticks
      const tgaps = this.drawTicks(this.axis_g, side, false),
            labelsPromise = this.optionUnlab ? Promise.resolve(tgaps) : this.drawLabels(this.axis_g, side, tgaps);

      return labelsPromise.then(lgaps => {
         // when drawing axis on frame, zoom rect should be always outside
         this.addZoomingRect(this.axis_g, this.standalone ? side : this.side, lgaps);

         return this.drawTitle(this.axis_g, side, lgaps);
      }).then(() => {
         if (isFunc(this._afterDrawAgain))
            this._afterDrawAgain();
      });
   }

   /** @summary Draw axis again on opposite frame size */
   drawAxisOtherPlace(layer, transform, side, only_ticks) {
      let axis_g = layer.selectChild(`.${this.name}_container2`);
      if (axis_g.empty())
         axis_g = layer.append('svg:g').attr('class', `${this.name}_container2`);
      else
         axis_g.selectAll('*').remove();

      axis_g.attr('transform', transform);

      if (this.ticksSide === 'invert')
         side = -side;

      // draw ticks and labels again
      const tgaps = this.drawTicks(axis_g, side, false),
            promise = this.optionUnlab || only_ticks ? Promise.resolve(tgaps) : this.drawLabels(axis_g, side, tgaps);

      return promise.then(lgaps => {
         this.addZoomingRect(axis_g, side, lgaps);
         return true;
      });
   }

   /** @summary Change zooming in standalone mode */
   zoomStandalone(min, max) {
      return this.changeAxisAttr(1, 'zoomMin', min, 'zoomMax', max);
   }

   /** @summary Redraw axis, used in standalone mode for RAxisDrawable */
   redraw() {
      const drawable = this.getObject(),
            pp = this.getPadPainter(),
            pos = pp.getCoordinate(drawable.fPos),
            reverse = this.v7EvalAttr('reverse', false),
            labels_len = drawable.fLabels.length,
            min = (labels_len > 0) ? 0 : this.v7EvalAttr('min', 0),
            max = (labels_len > 0) ? labels_len : this.v7EvalAttr('max', 100);
      let len = pp.getPadLength(drawable.fVertical, drawable.fLength),
          smin = this.v7EvalAttr('zoomMin'),
          smax = this.v7EvalAttr('zoomMax');

      // in vertical direction axis drawn in negative direction
      if (drawable.fVertical)
         len -= pp.getPadHeight();

      if (smin === smax) {
         smin = min;
         smax = max;
      }

      this.configureAxis('axis', min, max, smin, smax, drawable.fVertical, undefined, len, { reverse, labels: labels_len > 0 });

      const g = this.createG();

      this.standalone = true;  // no need to clean axis container

      const promise = this.drawAxis(g, makeTranslate(pos.x, pos.y));

      if (this.isBatchMode())
         return promise;

      return promise.then(() => {
         if (settings.ContextMenu) {
            g.on('contextmenu', evnt => {
               evnt.stopPropagation(); // disable main context menu
               evnt.preventDefault();  // disable browser context menu
               createMenu(evnt, this).then(menu => {
                  menu.header('RAxisDrawable', `${urlClassPrefix}ROOT_1_1Experimental_1_1RAxisBase.html`);
                  menu.add('Unzoom', () => this.zoomStandalone());
                  this.fillAxisContextMenu(menu, '');
                  menu.show();
               });
            });
         }

         addDragHandler(this, {
            x: pos.x, y: pos.y, width: this.vertical ? 10 : len, height: this.vertical ? len : 10,
            only_move: true, redraw: d => this.positionChanged(d)
         });

         g.on('dblclick', () => this.zoomStandalone());

         if (settings.ZoomWheel) {
            g.on('wheel', evnt => {
               evnt.stopPropagation();
               evnt.preventDefault();

               const pos2 = pointer(evnt, this.getG().node()),
                     coord = this.vertical ? (1 - pos2[1] / len) : pos2[0] / len,
                     item = this.analyzeWheelEvent(evnt, coord);

               if (item.changed)
                  this.zoomStandalone(item.min, item.max);
            });
         }
      });
   }

   /** @summary Process interactive moving of the axis drawing */
   positionChanged(drag) {
      const drawable = this.getObject(),
            rect = this.getPadPainter().getPadRect(),
            xn = drag.x / rect.width,
            yn = 1 - drag.y / rect.height;

      drawable.fPos.fHoriz.fArr = [xn];
      drawable.fPos.fVert.fArr = [yn];

      this.submitCanvExec(`SetPos({${xn.toFixed(4)},${yn.toFixed(4)}})`);
   }

   /** @summary Change axis attribute, submit changes to server and redraw axis when specified
     * @desc Arguments as redraw_mode, name1, value1, name2, value2, ... */
   changeAxisAttr(redraw_mode, ...args) {
      const changes = {};
      let indx = 0;
      while (indx < args.length) {
         this.v7AttrChange(changes, args[indx], args[indx + 1]);
         this.v7SetAttr(args[indx], args[indx + 1]);
         indx += 2;
      }
      this.v7SendAttrChanges(changes, false); // do not invoke canvas update on the server
      if (redraw_mode === 1) {
         if (this.standalone)
            this.redraw();
         else
            this.drawAxisAgain();
      } else if (redraw_mode)
         this.redrawPad();
   }

   /** @summary Change axis log scale kind */
   changeAxisLog(arg) {
      if ((this.kind === kAxisLabels) || (this.kind === kAxisTime))
         return;
      if (arg === 'toggle')
         arg = this.log ? 0 : 10;

      arg = parseFloat(arg);
      if (Number.isFinite(arg))
         this.changeAxisAttr(2, 'log', arg, 'symlog', 0);
   }

   /** @summary Provide context menu for axis */
   fillAxisContextMenu(menu, kind) {
      if (kind)
         menu.add('Unzoom', () => this.getFramePainter().unzoom(kind));

      menu.sub('Log scale', () => this.changeAxisLog('toggle'));
      menu.addchk(!this.log && !this.symlog, 'linear', 0, arg => this.changeAxisLog(arg));
      menu.addchk(this.log && !this.symlog && (this.logbase === 10), 'log10', () => this.changeAxisLog(10));
      menu.addchk(this.log && !this.symlog && (this.logbase === 2), 'log2', () => this.changeAxisLog(2));
      menu.addchk(this.log && !this.symlog && Math.abs(this.logbase - Math.exp(1)) < 0.1, 'ln', () => this.changeAxisLog(Math.exp(1)));
      menu.addchk(!this.log && this.symlog, 'symlog', 0, () =>
         menu.input('set symlog constant', this.symlog || 10, 'float').then(v => this.changeAxisAttr(2, 'symlog', v)));
      menu.endsub();

      menu.add('Divisions', () => menu.input('Set axis devisions', this.v7EvalAttr('ndiv', 508), 'int').then(val => this.changeAxisAttr(2, 'ndiv', val)));

      menu.sub('Ticks');
      menu.addRColorMenu('color', this.ticksColor, col => this.changeAxisAttr(1, 'ticks_color', col));
      menu.addSizeMenu('size', 0, 0.05, 0.01, this.ticksSize / this.scalingSize, sz => this.changeAxisAttr(1, 'ticks_size', sz));
      menu.addSelectMenu('side', ['normal', 'invert', 'both'], this.ticksSide, side => this.changeAxisAttr(1, 'ticks_side', side));
      menu.endsub();

      if (!this.optionUnlab && this.labelsFont) {
         menu.sub('Labels');
         menu.addSizeMenu('offset', -0.05, 0.05, 0.01, this.labelsOffset / this.scalingSize,
                          offset => this.changeAxisAttr(1, 'labels_offset', offset));
         menu.addRAttrTextItems(this.labelsFont, { noangle: 1, noalign: 1 },
                                change => this.changeAxisAttr(1, 'labels_' + change.name, change.value));
         menu.addchk(this.labelsFont.angle, 'rotate', res => this.changeAxisAttr(1, 'labels_angle', res ? 180 : 0));
         menu.endsub();
      }

      menu.sub('Title', () => menu.input('Enter axis title', this.fTitle).then(t => this.changeAxisAttr(1, 'title_value', t)));

      if (this.fTitle) {
         menu.addSizeMenu('offset', -0.05, 0.05, 0.01, this.titleOffset / this.scalingSize,
                          offset => this.changeAxisAttr(1, 'title_offset', offset));

         menu.addSelectMenu('position', ['left', 'center', 'right'], this.titlePos,
                            pos => this.changeAxisAttr(1, 'title_position', pos));

         menu.addchk(this.isTitleRotated(), 'rotate', flag => this.changeAxisAttr(1, 'title_angle', flag ? 180 : 0));

         menu.addRAttrTextItems(this.titleFont, { noangle: 1, noalign: 1 }, change => this.changeAxisAttr(1, 'title_' + change.name, change.value));
      }

      menu.endsub();
      return true;
   }

} // class RAxisPainter

/**
 * @summary Painter class for RFrame, main handler for interactivity
 *
 * @private
 */

class RFramePainter extends RObjectPainter {

   #frame_x; // frame X coordinate
   #frame_y; // frame Y coordinate
   #frame_width; // frame width
   #frame_height; // frame height
   #frame_trans; // transform of frame element
   #swap_xy;  // swap X/Y axis on the frame
   #reverse_x; // reverse X axis
   #reverse_y; // reverse Y axis
   #axes_drawn; // when axes are drawn
   #projection; // id of projection function
   #click_handler; // handle for click events
   #dblclick_handler; // handle for double click events
   #keys_handler; // assigned handler for keyboard events
   #enabled_keys;  // when keyboard press handling enabled
   #last_event_pos; // position of last event

   /** @summary constructor
     * @param {object|string} dom - DOM element for drawing or element id
     * @param {object} frame - RFrame object */
   constructor(dom, frame) {
      super(dom, frame, '', 'frame');
      this.mode3d = false;
      this.xmin = this.xmax = 0; // no scale specified, wait for objects drawing
      this.ymin = this.ymax = 0; // no scale specified, wait for objects drawing
      this.#axes_drawn = false;
      this.#projection = 0; // different projections
      this.v7_frame = true; // indicator of v7, used in interactive part
   }

   /** @summary Returns frame painter - object itself */
   getFramePainter() { return this; }

   /** @summary Returns true if it is ROOT6 frame
    * @private */
   is_root6() { return false; }

   /** @summary Set active flag for frame - can block some events
    * @private */
   setFrameActive(on) {
      this.#enabled_keys = on && settings.HandleKeys;
      // used only in 3D mode
      if (this.control)
         this.control.enableKeys = this.#enabled_keys;
   }

   /** @summary Returns true if keys handling enabled
   * @private */
   isEnabledKeys() { return this.#enabled_keys; }

   /** @summary Returns true if X/Y axis swapped */
   swap_xy() { return this.#swap_xy; }

   /** @summary Is reverse x */
   reverse_x() { return this.#reverse_x; }

   /** @summary Is reverse x */
   reverse_y() { return this.#reverse_y; }

   /** @summary Set position of last context menu event
    * @private */
   setLastEventPos(pnt) { this.#last_event_pos = pnt; }

   /** @summary Return position of last event
     * @private */
   getLastEventPos() { return this.#last_event_pos; }

   /** @summary Update graphical attributes */
   updateAttributes(force) {
      if ((this.fX1NDC === undefined) || (force && !this.$modifiedNDC)) {
         const rect = this.getPadPainter().getPadRect();
         this.fX1NDC = this.v7EvalLength('margins_left', rect.width, gStyle.fPadLeftMargin) / rect.width;
         this.fY1NDC = this.v7EvalLength('margins_bottom', rect.height, gStyle.fPadBottomMargin) / rect.height;
         this.fX2NDC = 1 - this.v7EvalLength('margins_right', rect.width, gStyle.fPadRightMargin) / rect.width;
         this.fY2NDC = 1 - this.v7EvalLength('margins_top', rect.height, gStyle.fPadTopMargin) / rect.height;
      }

      if (!this.fillatt)
         this.createv7AttFill();

      this.createv7AttLine('border_');
   }

   /** @summary Returns coordinates transformation func */
   getProjectionFunc() { return getEarthProjectionFunc(this.#projection); }

   /** @summary Recalculate frame ranges using specified projection functions
     * @desc Not yet used in v7 */
   recalculateRange(Proj) {
      this.#projection = Proj || 0;

      if ((this.#projection === 2) && ((this.scale_ymin <= -90) || (this.scale_ymax >= 90))) {
         console.warn(`Mercator Projection: latitude out of range ${this.scale_ymin} ${this.scale_ymax}`);
         this.#projection = 0;
      }

      const func = this.getProjectionFunc();
      if (!func)
         return;

      const pnts = [func(this.scale_xmin, this.scale_ymin),
                    func(this.scale_xmin, this.scale_ymax),
                    func(this.scale_xmax, this.scale_ymax),
                    func(this.scale_xmax, this.scale_ymin)];
      if (this.scale_xmin < 0 && this.scale_xmax > 0) {
         pnts.push(func(0, this.scale_ymin));
         pnts.push(func(0, this.scale_ymax));
      }
      if (this.scale_ymin < 0 && this.scale_ymax > 0) {
         pnts.push(func(this.scale_xmin, 0));
         pnts.push(func(this.scale_xmax, 0));
      }

      this.original_xmin = this.scale_xmin;
      this.original_xmax = this.scale_xmax;
      this.original_ymin = this.scale_ymin;
      this.original_ymax = this.scale_ymax;

      this.scale_xmin = this.scale_xmax = pnts[0].x;
      this.scale_ymin = this.scale_ymax = pnts[0].y;

      for (let n = 1; n < pnts.length; ++n) {
         this.scale_xmin = Math.min(this.scale_xmin, pnts[n].x);
         this.scale_xmax = Math.max(this.scale_xmax, pnts[n].x);
         this.scale_ymin = Math.min(this.scale_ymin, pnts[n].y);
         this.scale_ymax = Math.max(this.scale_ymax, pnts[n].y);
      }
   }

   getFrameSvg() { return this.getPadPainter().getFrameSvg(); }


   /** @summary Draw axes grids
     * @desc Called immediately after axes drawing */
   drawGrids() {
      const layer = this.getFrameSvg().selectChild('.axis_layer');

      layer.selectAll('.xgrid').remove();
      layer.selectAll('.ygrid').remove();

      const h = this.getFrameHeight(),
            w = this.getFrameWidth(),
            gridx = this.v7EvalAttr('gridX', false),
            gridy = this.v7EvalAttr('gridY', false),
            grid_style = getSvgLineStyle(gStyle.fGridStyle),
            grid_color = (gStyle.fGridColor > 0) ? this.getColor(gStyle.fGridColor) : 'black';

      if (this.x_handle)
         this.x_handle.draw_grid = gridx;

      // add a grid on x axis, if the option is set
      if (this.x_handle?.draw_grid) {
         let grid = '';
         for (let n = 0; n < this.x_handle.ticks.length; ++n) {
            grid += this.#swap_xy
                  ? `M0,${h + this.x_handle.ticks[n]}h${w}`
                  : `M${this.x_handle.ticks[n]},0v${h}`;
         }

         if (grid) {
            layer.append('svg:path')
                 .attr('class', 'xgrid')
                 .attr('d', grid)
                 .style('stroke', grid_color)
                 .style('stroke-width', gStyle.fGridWidth)
                 .style('stroke-dasharray', grid_style);
         }
      }

      if (this.y_handle)
         this.y_handle.draw_grid = gridy;

      // add a grid on y axis, if the option is set
      if (this.y_handle?.draw_grid) {
         let grid = '';
         for (let n = 0; n < this.y_handle.ticks.length; ++n) {
            grid += this.#swap_xy
                     ? `M${this.y_handle.ticks[n]},0v${h}`
                     : `M0,${h + this.y_handle.ticks[n]}h${w}`;
         }

         if (grid) {
            layer.append('svg:path')
               .attr('class', 'ygrid')
               .attr('d', grid)
               .style('stroke', grid_color)
               .style('stroke-width', gStyle.fGridWidth)
               .style('stroke-dasharray', grid_style);
         }
      }
   }

   /** @summary Converts 'raw' axis value into text */
   axisAsText(axis, value) {
      const handle = this[`${axis}_handle`];

      return handle ? handle.axisAsText(value, settings[axis.toUpperCase() + 'ValuesFormat']) : value.toPrecision(4);
   }

   /** @summary Set axis range */
   _setAxisRange(prefix, vmin, vmax) {
      const nmin = `${prefix}min`, nmax = `${prefix}max`;
      if (this[nmin] !== this[nmax])
         return;
      let min = this.v7EvalAttr(`${prefix}_min`),
          max = this.v7EvalAttr(`${prefix}_max`);

      if (min !== undefined)
         vmin = min;
      if (max !== undefined)
         vmax = max;

      if (vmin < vmax) {
         this[nmin] = vmin;
         this[nmax] = vmax;
      }

      const nzmin = `zoom_${prefix}min`, nzmax = `zoom_${prefix}max`;

      if ((this[nzmin] === this[nzmax]) && !this.zoomChangedInteractive(prefix)) {
         min = this.v7EvalAttr(`${prefix}_zoomMin`);
         max = this.v7EvalAttr(`${prefix}_zoomMax`);

         if ((min !== undefined) || (max !== undefined)) {
            this[nzmin] = (min === undefined) ? this[nmin] : min;
            this[nzmax] = (max === undefined) ? this[nmax] : max;
         }
      }
   }

   /** @summary Set axes ranges for drawing, check configured attributes if range already specified */
   setAxesRanges(xaxis, xmin, xmax, yaxis, ymin, ymax, zaxis, zmin, zmax) {
      if (this.#axes_drawn)
         return;
      this.xaxis = xaxis;
      this._setAxisRange('x', xmin, xmax);
      this.yaxis = yaxis;
      this._setAxisRange('y', ymin, ymax);
      this.zaxis = zaxis;
      this._setAxisRange('z', zmin, zmax);
   }

   /** @summary Set secondary axes ranges */
   setAxes2Ranges(second_x, xaxis, xmin, xmax, second_y, yaxis, ymin, ymax) {
      if (second_x) {
         this.x2axis = xaxis;
         this._setAxisRange('x2', xmin, xmax);
      }
      if (second_y) {
         this.y2axis = yaxis;
         this._setAxisRange('y2', ymin, ymax);
      }
   }

   /** @summary Create x,y objects which maps user coordinates into pixels
     * @desc Must be used only for v6 objects, see TFramePainter for more details
     * @private */
   createXY(opts) {
      if (this.self_drawaxes)
         return;

      this.cleanXY(); // remove all previous configurations

      if (!opts)
         opts = { ndim: 1 };

      this.v6axes = true;
      this.#swap_xy = opts.swap_xy || false;
      this.#reverse_x = opts.reverse_x || false;
      this.#reverse_y = opts.reverse_y || false;

      this.logx = this.v7EvalAttr('x_log', 0);
      this.logy = this.v7EvalAttr('y_log', 0);

      const w = this.getFrameWidth(), h = this.getFrameHeight(), pp = this.getPadPainter();

      this.scales_ndim = opts.ndim;

      this.scale_xmin = this.xmin;
      this.scale_xmax = this.xmax;

      this.scale_ymin = this.ymin;
      this.scale_ymax = this.ymax;

      if (opts.extra_y_space) {
         const log_scale = this.#swap_xy ? this.logx : this.logy;
         if (log_scale && (this.scale_ymax > 0))
            this.scale_ymax = Math.exp(Math.log(this.scale_ymax) * 1.1);
         else
            this.scale_ymax += (this.scale_ymax - this.scale_ymin) * 0.1;
      }

      if ((opts.zoom_xmin !== opts.zoom_xmax) && ((this.zoom_xmin === this.zoom_xmax) || !this.zoomChangedInteractive('x'))) {
         this.zoom_xmin = opts.zoom_xmin;
         this.zoom_xmax = opts.zoom_xmax;
      }

      if ((opts.zoom_ymin !== opts.zoom_ymax) && ((this.zoom_ymin === this.zoom_ymax) || !this.zoomChangedInteractive('y'))) {
         this.zoom_ymin = opts.zoom_ymin;
         this.zoom_ymax = opts.zoom_ymax;
      }

      if (this.zoom_xmin !== this.zoom_xmax) {
         this.scale_xmin = this.zoom_xmin;
         this.scale_xmax = this.zoom_xmax;
      }

      if (this.zoom_ymin !== this.zoom_ymax) {
         this.scale_ymin = this.zoom_ymin;
         this.scale_ymax = this.zoom_ymax;
      }

      let xaxis = this.xaxis, yaxis = this.yaxis;
      if (xaxis?._typename !== clTAxis)
         xaxis = create$1(clTAxis);
      if (yaxis?._typename !== clTAxis)
         yaxis = create$1(clTAxis);

      this.x_handle = new TAxisPainter(pp, xaxis, true);
      this.x_handle.optionUnlab = this.v7EvalAttr('x_labels_hide', false);

      this.x_handle.configureAxis('xaxis', this.xmin, this.xmax, this.scale_xmin, this.scale_xmax, this.#swap_xy, this.#swap_xy ? [0, h] : [0, w], {
         reverse: this.#reverse_x,
         log: this.#swap_xy ? this.logy : this.logx,
         symlog: this.#swap_xy ? opts.symlog_y : opts.symlog_x,
         logcheckmin: (opts.ndim > 1) || !this.#swap_xy,
         logminfactor: 0.0001
      });

      this.x_handle.assignFrameMembers(this, 'x');

      this.y_handle = new TAxisPainter(pp, yaxis, true);
      this.y_handle.optionUnlab = this.v7EvalAttr('y_labels_hide', false);

      this.y_handle.configureAxis('yaxis', this.ymin, this.ymax, this.scale_ymin, this.scale_ymax, !this.#swap_xy, this.#swap_xy ? [0, w] : [0, h], {
         reverse: this.#reverse_y,
         log: this.#swap_xy ? this.logx : this.logy,
         symlog: this.#swap_xy ? opts.symlog_x : opts.symlog_y,
         logcheckmin: (opts.ndim > 1) || this.#swap_xy,
         log_min_nz: opts.ymin_nz && (opts.ymin_nz < this.ymax) ? 0.5 * opts.ymin_nz : 0,
         logminfactor: 3e-4
      });

      this.y_handle.assignFrameMembers(this, 'y');
   }

   /** @summary Identify if requested axes are drawn
     * @desc Checks if x/y axes are drawn. Also if second side is already there */
   hasDrawnAxes(second_x, second_y) { return !second_x && !second_y ? this.#axes_drawn : false; }

   /** @summary Draw configured axes on the frame
     * @desc axes can be drawn only for main histogram  */
   async drawAxes() {
      if (this.#axes_drawn || (this.xmin === this.xmax) || (this.ymin === this.ymax))
         return this.#axes_drawn;

      const ticksx = this.v7EvalAttr('ticksX', 1),
            ticksy = this.v7EvalAttr('ticksY', 1),
            sidex = this.v7EvalAttr('swapX', false) ? -1 : 1,
            sidey = this.v7EvalAttr('swapY', false) ? -1 : 1,
            w = this.getFrameWidth(),
            h = this.getFrameHeight(),
            pp = this.getPadPainter();

      if (!this.v6axes) {
         // this is partially same as v6 createXY method

         this.cleanupAxes();

         this.#swap_xy = false;

         if (this.zoom_xmin !== this.zoom_xmax) {
            this.scale_xmin = this.zoom_xmin;
            this.scale_xmax = this.zoom_xmax;
         } else {
            this.scale_xmin = this.xmin;
            this.scale_xmax = this.xmax;
         }

         if (this.zoom_ymin !== this.zoom_ymax) {
            this.scale_ymin = this.zoom_ymin;
            this.scale_ymax = this.zoom_ymax;
         } else {
            this.scale_ymin = this.ymin;
            this.scale_ymax = this.ymax;
         }

         this.recalculateRange(0);

         this.x_handle = new RAxisPainter(pp, this, this.xaxis, 'x_');
         this.x_handle.assignSnapId(this.getSnapId());
         this.x_handle.draw_swapside = (sidex < 0);
         this.x_handle.draw_ticks = ticksx;

         this.y_handle = new RAxisPainter(pp, this, this.yaxis, 'y_');
         this.y_handle.assignSnapId(this.getSnapId());
         this.y_handle.draw_swapside = (sidey < 0);
         this.y_handle.draw_ticks = ticksy;

         this.z_handle = new RAxisPainter(pp, this, this.zaxis, 'z_');
         this.z_handle.assignSnapId(this.getSnapId());

         this.x_handle.configureAxis('xaxis', this.xmin, this.xmax, this.scale_xmin, this.scale_xmax, false, [0, w], w, { reverse: false });
         this.x_handle.assignFrameMembers(this, 'x');

         this.y_handle.configureAxis('yaxis', this.ymin, this.ymax, this.scale_ymin, this.scale_ymax, true, [h, 0], -h, { reverse: false });
         this.y_handle.assignFrameMembers(this, 'y');

         // only get basic properties like log scale
         this.z_handle.configureZAxis('zaxis', this);
      }

      const layer = this.getFrameSvg().selectChild('.axis_layer');

      this.x_handle.has_obstacle = false;

      const draw_horiz = this.#swap_xy ? this.y_handle : this.x_handle,
            draw_vertical = this.#swap_xy ? this.x_handle : this.y_handle;
      let pr;

      if (this.getPadPainter()?.isFastDrawing())
         pr = Promise.resolve(true); // do nothing
      else if (this.v6axes) {
         // in v7 ticksx/y values shifted by 1 relative to v6
         // In v7 ticksx === 0 means no ticks, ticksx === 1 equivalent to === 0 in v6

         const can_adjust_frame = false, disable_x_draw = false, disable_y_draw = false;

         draw_horiz.disable_ticks = (ticksx <= 0);
         draw_vertical.disable_ticks = (ticksy <= 0);

         const pr1 = draw_horiz.drawAxis(layer, w, h,
                                         draw_horiz.invert_side ? null : `translate(0,${h})`,
                                         (ticksx > 1) ? -h : 0, disable_x_draw,
                                         undefined, false, this.getPadPainter().getPadHeight() - h - this.getFrameY()),

               pr2 = draw_vertical.drawAxis(layer, w, h,
                                            draw_vertical.invert_side ? `translate(${w})` : null,
                                            (ticksy > 1) ? w : 0, disable_y_draw,
                                            draw_vertical.invert_side ? 0 : this.#frame_x, can_adjust_frame);

         pr = Promise.all([pr1, pr2]).then(() => this.drawGrids());
      } else {
         let arr = [];

         if (ticksx > 0)
            arr.push(draw_horiz.drawAxis(layer, makeTranslate(0, sidex > 0 ? h : 0), sidex));

         if (ticksy > 0)
            arr.push(draw_vertical.drawAxis(layer, makeTranslate(sidey > 0 ? 0 : w, h), sidey));

         pr = Promise.all(arr).then(() => {
            arr = [];
            if (ticksx > 1)
               arr.push(draw_horiz.drawAxisOtherPlace(layer, makeTranslate(0, sidex < 0 ? h : 0), -sidex, ticksx === 2));

            if (ticksy > 1)
               arr.push(draw_vertical.drawAxisOtherPlace(layer, makeTranslate(sidey < 0 ? 0 : w, h), -sidey, ticksy === 2));
            return Promise.all(arr);
         }).then(() => this.drawGrids());
      }

      return pr.then(() => {
         this.#axes_drawn = true;
         return true;
      });
   }

   /** @summary Draw secondary configured axes */
   drawAxes2(second_x, second_y) {
      const w = this.getFrameWidth(), h = this.getFrameHeight(),
            pp = this.getPadPainter(),
            layer = this.getFrameSvg().selectChild('.axis_layer');
      let pr1, pr2;

      if (second_x) {
         if (this.zoom_x2min !== this.zoom_x2max) {
            this.scale_x2min = this.zoom_x2min;
            this.scale_x2max = this.zoom_x2max;
         } else {
            this.scale_x2min = this.x2min;
            this.scale_x2max = this.x2max;
         }
         this.x2_handle = new RAxisPainter(pp, this, this.x2axis, 'x2_');
         this.x2_handle.assignSnapId(this.getSnapId());

         this.x2_handle.configureAxis('x2axis', this.x2min, this.x2max, this.scale_x2min, this.scale_x2max, false, [0, w], w, { reverse: false });
         this.x2_handle.assignFrameMembers(this, 'x2');

         pr1 = this.x2_handle.drawAxis(layer, null, -1);
      }

      if (second_y) {
         if (this.zoom_y2min !== this.zoom_y2max) {
            this.scale_y2min = this.zoom_y2min;
            this.scale_y2max = this.zoom_y2max;
         } else {
            this.scale_y2min = this.y2min;
            this.scale_y2max = this.y2max;
         }

         this.y2_handle = new RAxisPainter(pp, this, this.y2axis, 'y2_');
         this.y2_handle.assignSnapId(this.getSnapId());

         this.y2_handle.configureAxis('y2axis', this.y2min, this.y2max, this.scale_y2min, this.scale_y2max, true, [h, 0], -h, { reverse: false });
         this.y2_handle.assignFrameMembers(this, 'y2');

         pr2 = this.y2_handle.drawAxis(layer, makeTranslate(w, h), -1);
      }

      return Promise.all([pr1, pr2]);
   }

   /** @summary Return functions to create x/y points based on coordinates
     * @desc In default case returns frame painter itself
     * @private */
   getGrFuncs(second_x, second_y) {
      const use_x2 = second_x && this.grx2,
            use_y2 = second_y && this.gry2;
      if (!use_x2 && !use_y2)
         return this;

      return {
         use_x2,
         grx: use_x2 ? this.grx2 : this.grx,
         x_handle: use_x2 ? this.x2_handle : this.x_handle,
         logx: use_x2 ? this.x2_handle.log : this.x_handle.log,
         scale_xmin: use_x2 ? this.scale_x2min : this.scale_xmin,
         scale_xmax: use_x2 ? this.scale_x2max : this.scale_xmax,
         use_y2,
         gry: use_y2 ? this.gry2 : this.gry,
         y_handle: use_y2 ? this.y2_handle : this.y_handle,
         logy: use_y2 ? this.y2_handle.log : this.y_handle.log,
         scale_ymin: use_y2 ? this.scale_y2min : this.scale_ymin,
         scale_ymax: use_y2 ? this.scale_y2max : this.scale_ymax,
         fp: this,
         swap_xy() { return this.fp.swap_xy(); },
         revertAxis(name, v) {
            if ((name === 'x') && this.use_x2)
               name = 'x2';
            if ((name === 'y') && this.use_y2)
               name = 'y2';
            return this.fp.revertAxis(name, v);
         },
         axisAsText(name, v) {
            if ((name === 'x') && this.use_x2)
               name = 'x2';
            if ((name === 'y') && this.use_y2)
               name = 'y2';
            return this.fp.axisAsText(name, v);
         }
      };
   }

   /** @summary function called at the end of resize of frame
     * @desc Used to update attributes on the server
     * @private */
   sizeChanged() {
      const changes = {};
      this.v7AttrChange(changes, 'margins_left', this.fX1NDC);
      this.v7AttrChange(changes, 'margins_bottom', this.fY1NDC);
      this.v7AttrChange(changes, 'margins_right', 1 - this.fX2NDC);
      this.v7AttrChange(changes, 'margins_top', 1 - this.fY2NDC);
      this.v7SendAttrChanges(changes, false); // do not invoke canvas update on the server

      this.redrawPad();
   }

   /** @summary Remove all x/y functions
     * @private */
   cleanXY() {
      // remove all axes drawings
      const clean = (name, grname) => {
         this[name]?.cleanup();
         delete this[name];
         delete this[grname];
      };

      clean('x_handle', 'grx');
      clean('y_handle', 'gry');
      clean('z_handle', 'grz');
      clean('x2_handle', 'grx2');
      clean('y2_handle', 'gry2');

      delete this.v6axes; // marker that v6 axes are used
   }

   /** @summary Remove all axes drawings
     * @private */
   cleanupAxes() {
      this.cleanXY();

      this.getG()?.selectChild('.axis_layer').selectAll('*').remove();
      this.#axes_drawn = false;
   }

   /** @summary Removes all drawn elements of the frame
     * @private */
   cleanFrameDrawings() {
      // cleanup all 3D drawings if any
      if (isFunc(this.create3DScene))
         this.create3DScene(-1);

      this.cleanupAxes();

      const clean = (name) => {
         this[name + 'min'] = this[name + 'max'] = 0;
         this[`zoom_${name}min`] = this[`zoom_${name}max`] = 0;
         this[`scale_${name}min`] = this[`scale_${name}max`] = 0;
      };

      clean('x');
      clean('y');
      clean('z');
      clean('x2');
      clean('y2');

      this.getG()?.selectChild('.main_layer').selectAll('*').remove();
      this.getG()?.selectChild('.upper_layer').selectAll('*').remove();
   }

   /** @summary Fully cleanup frame
     * @private */
   cleanup() {
      this.cleanFrameDrawings();

      this.getG()?.selectAll('*').remove();
      this.getG()?.on('mousedown', null)
                  .on('dblclick', null)
                  .on('wheel', null)
                  .on('contextmenu', null)
                  .property('interactive_set', null);

      if (this.#keys_handler) {
         window.removeEventListener('keydown', this.#keys_handler, false);
         this.#keys_handler = undefined;
      }
      this.#enabled_keys = undefined;
      delete this.self_drawaxes;

      delete this.xaxis;
      delete this.yaxis;
      delete this.zaxis;
      delete this.x2axis;
      delete this.y2axis;

      this.setG(undefined); // frame <g> element managed by the pad

      this.#click_handler = undefined;
      this.#dblclick_handler = undefined;

      this.getPadPainter()?.setFramePainter(this, false);

      super.cleanup();
   }

   /** @summary Redraw frame
     * @private */
   redraw() {
      const pp = this.getPadPainter();
      pp?.setFramePainter(this, true);

      // first update all attributes from objects
      this.updateAttributes();

      const rect = pp?.getPadRect() ?? { width: 10, height: 10 },
            lm = Math.round(rect.width * this.fX1NDC),
            tm = Math.round(rect.height * (1 - this.fY2NDC)),
            rotate = pp?.options?.RotateFrame,
            w = Math.round(rect.width * (this.fX2NDC - this.fX1NDC)),
            h = Math.round(rect.height * (this.fY2NDC - this.fY1NDC));

      // update values here to let access even when frame is not really updated
      this.#frame_x = lm;
      this.#frame_y = tm;
      this.#frame_width = rotate ? h : w;
      this.#frame_height = rotate ? w : h;
      this.#frame_trans = rotate ? `rotate(-90,${lm},${tm}) translate(${lm - h},${tm})` : makeTranslate(lm, tm);
      this.$can_drag = !rotate && !pp?.options?.FixFrame;

      return this.mode3d ? this : this.createFrameG();
   }

   /** @summary Create frame element and update all attributes
     * @private */
   createFrameG() {
      // this is svg:g object - container for every other items belonging to frame
      let g = this.setG(this.getFrameSvg()),
          top_rect, main_svg;

      if (g.empty()) {
         g = this.setG(this.getPadPainter().getLayerSvg('primitives_layer').append('svg:g').attr('class', 'root_frame'));

         if (!this.isBatchMode())
            g.append('svg:title').text('');

         top_rect = g.append('svg:rect');

         main_svg = g.append('svg:svg')
                     .attr('class', 'main_layer')
                     .attr('x', 0)
                     .attr('y', 0)
                     .attr('overflow', 'hidden');

         g.append('svg:g').attr('class', 'axis_layer');
         g.append('svg:g').attr('class', 'upper_layer');
      } else {
         top_rect = g.selectChild('rect');
         main_svg = g.selectChild('.main_layer');
      }

      this.#axes_drawn = false;

      g.attr('transform', this.#frame_trans);

      top_rect.attr('x', 0)
              .attr('y', 0)
              .attr('width', this.#frame_width)
              .attr('height', this.#frame_height)
              .attr('rx', this.lineatt.rx || null)
              .attr('ry', this.lineatt.ry || null)
              .call(this.fillatt.func)
              .call(this.lineatt.func);

      main_svg.attr('width', this.#frame_width)
              .attr('height', this.#frame_height)
              .attr('viewBox', `0 0 ${this.#frame_width} ${this.#frame_height}`);

      let pr = Promise.resolve(true);

      if (this.v7EvalAttr('drawAxes')) {
         this.self_drawaxes = true;
         this.setAxesRanges();
         pr = this.drawAxes().then(() => this.addInteractivity());
      }

      return pr.then(() => { return this; });
   }

   /** @summary Returns frame X position */
   getFrameX() { return this.#frame_x || 0; }

   /** @summary Returns frame Y position */
   getFrameY() { return this.#frame_y || 0; }

   /** @summary Returns frame width */
   getFrameWidth() { return this.#frame_width || 0; }

   /** @summary Returns frame height */
   getFrameHeight() { return this.#frame_height || 0; }

   /** @summary Returns frame rectangle plus extra info for hint display */
   getFrameRect() {
      return {
         x: this.#frame_x || 0,
         y: this.#frame_y || 0,
         width: this.getFrameWidth(),
         height: this.getFrameHeight(),
         transform: this.getG()?.attr('transform') || '',
         hint_delta_x: 0,
         hint_delta_y: 0
      };
   }

   /** @summary Returns palette associated with frame */
   getHistPalette() {
      return this.getPadPainter().getHistPalette();
   }

   /** @summary Configure user-defined click handler
     * @desc Function will be called every time when frame click was performed
     * As argument, tooltip object with selected bins will be provided
     * If handler function returns true, default handling of click will be disabled */
   configureUserClickHandler(handler) {
      this.#click_handler = isFunc(handler) ? handler : null;
   }

   /** @summary Returns actual click handler */
   getClickHandler() { return this.#click_handler; }

   /** @summary Configure user-defined dblclick handler
     * @desc Function will be called every time when double click was called
     * As argument, tooltip object with selected bins will be provided
     * If handler function returns true, default handling of dblclick (unzoom) will be disabled */
   configureUserDblclickHandler(handler) {
      this.#dblclick_handler = isFunc(handler) ? handler : null;
   }

   /** @summary Returns actual double-click handler */
   getDblclickHandler() { return this.#dblclick_handler; }

   /** @summary function can be used for zooming into specified range
     * @desc if both limits for each axis 0 (like xmin === xmax === 0), axis will be unzoomed
     * @return {Promise} with boolean flag if zoom operation was performed */
   async zoom(xmin, xmax, ymin, ymax, zmin, zmax, interactive) {
      // disable zooming when axis conversion is enabled
      if (this.#projection)
         return false;

      if (xmin === 'x') {
         xmin = xmax;
         xmax = ymin;
         interactive = ymax;
         ymin = ymax = undefined;
      } else if (xmin === 'y') {
         interactive = ymax;
         ymax = ymin;
         ymin = xmax;
         xmin = xmax = undefined;
      } else if (xmin === 'z') {
         interactive = ymax;
         zmin = xmax;
         zmax = ymin;
         xmin = xmax = ymin = ymax = undefined;
      }

      let zoom_x = (xmin !== xmax), zoom_y = (ymin !== ymax), zoom_z = (zmin !== zmax),
          unzoom_x = false, unzoom_y = false, unzoom_z = false;

      if (zoom_x) {
         let cnt = 0;
         if (xmin <= this.xmin) {
            xmin = this.xmin;
            cnt++;
         }
         if (xmax >= this.xmax) {
            xmax = this.xmax;
            cnt++;
         }
         if (cnt === 2) {
            zoom_x = false;
            unzoom_x = true;
         }
      } else
         unzoom_x = (xmin === xmax) && (xmin === 0);

      if (zoom_y) {
         let cnt = 0;
         if (ymin <= this.ymin) {
            ymin = this.ymin;
            cnt++;
         }
         if (ymax >= this.ymax) {
            ymax = this.ymax;
            cnt++;
         }
         if (cnt === 2) {
            zoom_y = false;
            unzoom_y = true;
         }
      } else
         unzoom_y = (ymin === ymax) && (ymin === 0);

      if (zoom_z) {
         let cnt = 0;
         if (zmin <= this.zmin) {
            zmin = this.zmin;
            cnt++;
         }
         if (zmax >= this.zmax) {
            zmax = this.zmax;
            cnt++;
         }
         if (cnt === 2) {
            zoom_z = false;
            unzoom_z = true;
         }
      } else
         unzoom_z = (zmin === zmax) && (zmin === 0);

      let changed = false, r_x = '', r_y = '', r_z = '', is_any_check = false;
      const req = {
         _typename: `${nsREX}RFrame::RUserRanges`,
         values: [0, 0, 0, 0, 0, 0],
         flags: [false, false, false, false, false, false]
      }, checkZooming = (painter, force) => {
         if (!force && !isFunc(painter.canZoomInside))
            return;

         is_any_check = true;

         if (zoom_x && (force || painter.canZoomInside('x', xmin, xmax))) {
            this.zoom_xmin = xmin;
            this.zoom_xmax = xmax;
            changed = true;
            r_x = '0';
            zoom_x = false;
            req.values[0] = xmin;
            req.values[1] = xmax;
            req.flags[0] = req.flags[1] = true;
            if (interactive)
               this.zoomChangedInteractive('x', interactive);
         }
         if (zoom_y && (force || painter.canZoomInside('y', ymin, ymax))) {
            this.zoom_ymin = ymin;
            this.zoom_ymax = ymax;
            changed = true;
            r_y = '1';
            zoom_y = false;
            req.values[2] = ymin;
            req.values[3] = ymax;
            req.flags[2] = req.flags[3] = true;
            if (interactive)
               this.zoomChangedInteractive('y', interactive);
         }
         if (zoom_z && (force || painter.canZoomInside('z', zmin, zmax))) {
            this.zoom_zmin = zmin;
            this.zoom_zmax = zmax;
            changed = true;
            r_z = '2';
            zoom_z = false;
            req.values[4] = zmin;
            req.values[5] = zmax;
            req.flags[4] = req.flags[5] = true;
            if (interactive)
               this.zoomChangedInteractive('z', interactive);
         }
      };

      // first process zooming (if any)
      if (zoom_x || zoom_y || zoom_z)
         this.forEachPainter(painter => checkZooming(painter));

      // force zooming when no any other painter can verify zoom range
      if (!is_any_check && this.self_drawaxes)
         checkZooming(null, true);

      // and process unzoom, if any
      if (unzoom_x || unzoom_y || unzoom_z) {
         if (unzoom_x) {
            if (this.zoom_xmin !== this.zoom_xmax) {
               changed = true;
               r_x = '0';
            }
            this.zoom_xmin = this.zoom_xmax = 0;
            req.values[0] = req.values[1] = -1;
            if (interactive)
               this.zoomChangedInteractive('x', interactive);
         }
         if (unzoom_y) {
            if (this.zoom_ymin !== this.zoom_ymax) {
               changed = true;
               r_y = '1';
            }
            this.zoom_ymin = this.zoom_ymax = 0;
            req.values[2] = req.values[3] = -1;
            if (interactive)
               this.zoomChangedInteractive('y', interactive);
         }
         if (unzoom_z) {
            if (this.zoom_zmin !== this.zoom_zmax) {
               changed = true;
               r_z = '2';
            }
            this.zoom_zmin = this.zoom_zmax = 0;
            req.values[4] = req.values[5] = -1;
            if (interactive)
               this.zoomChangedInteractive('z', interactive);
         }
      }

      if (!changed)
         return false;

      if (this.v7NormalMode())
         this.v7SubmitRequest('zoom', { _typename: `${nsREX}RFrame::RZoomRequest`, ranges: req });

      return this.interactiveRedraw('pad', 'zoom' + r_x + r_y + r_z).then(() => true);
   }

   /** @summary Zooming of single axis
     * @param {String} name - axis name like x/y/z but also second axis x2 or y2
     * @param {Number} vmin - axis minimal value, 0 for unzoom
     * @param {Number} vmax - axis maximal value, 0 for unzoom
     * @param {Boolean} [interactive] - if change was performed interactively
     * @protected */
   async zoomSingle(name, vmin, vmax, interactive) {
      const names = ['x', 'y', 'z', 'x2', 'y2'], indx = names.indexOf(name);

      // disable zooming when axis conversion is enabled
      if (this.#projection || (!this[`${name}_handle`] && (name !== 'z')) || (indx < 0))
         return false;

      let zoom_v = (vmin !== vmax), unzoom_v = false;

      if (zoom_v) {
         let cnt = 0;
         if (vmin <= this[name + 'min']) {
            vmin = this[name + 'min'];
            cnt++;
         }
         if (vmax >= this[name + 'max']) {
            vmax = this[name + 'max'];
            cnt++;
         }
         if (cnt === 2) {
            zoom_v = false;
            unzoom_v = true;
         }
      } else
         unzoom_v = (vmin === vmax) && (vmin === 0);


      let changed = false, is_any_check = false;
      const req = {
         _typename: `${nsREX}RFrame::RUserRanges`,
         values: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
         flags: [false, false, false, false, false, false, false, false, false, false]
      };

      // eslint-disable-next-line one-var
      const checkZooming = (painter, force) => {
         if (!force && !isFunc(painter?.canZoomInside))
            return;

         is_any_check = true;

         if (zoom_v && (force || painter.canZoomInside(name[0], vmin, vmax))) {
            this[`zoom_${name}min`] = vmin;
            this[`zoom_${name}max`] = vmax;
            changed = true;
            zoom_v = false;
            req.values[indx * 2] = vmin;
            req.values[indx * 2 + 1] = vmax;
            req.flags[indx * 2] = req.flags[indx * 2 + 1] = true;
         }
      };

      // first process zooming (if any)
      if (zoom_v)
         this.forEachPainter(painter => checkZooming(painter));

      // force zooming when no any other painter can verify zoom range
      if (!is_any_check && this.self_drawaxes)
         checkZooming(null, true);

      if (unzoom_v) {
         if (this[`zoom_${name}min`] !== this[`zoom_${name}max`])
            changed = true;
         this[`zoom_${name}min`] = this[`zoom_${name}max`] = 0;
         req.values[indx * 2] = req.values[indx * 2 + 1] = -1;
      }

      if (!changed)
         return false;

      if (interactive)
         this.zoomChangedInteractive(name, interactive);

      if (this.v7NormalMode())
         this.v7SubmitRequest('zoom', { _typename: `${nsREX}RFrame::RZoomRequest`, ranges: req });

      return this.interactiveRedraw('pad', `zoom${indx}`).then(() => true);
   }

   /** @summary Unzoom single axis */
   async unzoomSingle(name, interactive) {
      return this.zoomSingle(name, 0, 0, typeof interactive === 'undefined' ? 'unzoom' : interactive);
   }

   /** @summary Checks if specified axis zoomed */
   isAxisZoomed(axis) {
      return this[`zoom_${axis}min`] !== this[`zoom_${axis}max`];
   }

   /** @summary Unzoom specified axes
     * @return {Promise} with boolean flag if zoom is changed */
   async unzoom(dox, doy, doz) {
      if (dox === 'all')
         return this.unzoom('x2').then(() => this.unzoom('y2')).then(() => this.unzoom('xyz'));

      if ((dox === 'x2') || (dox === 'y2'))
         return this.unzoomSingle(dox);

      if (typeof dox === 'undefined')
         dox = doy = doz = true;
      else if (isStr(dox)) {
         doz = dox.indexOf('z') >= 0;
         doy = dox.indexOf('y') >= 0;
         dox = dox.indexOf('x') >= 0;
      }

      return this.zoom(dox ? 0 : undefined, dox ? 0 : undefined,
                       doy ? 0 : undefined, doy ? 0 : undefined,
                       doz ? 0 : undefined, doz ? 0 : undefined,
                       'unzoom');
   }

   /** @summary Reset all zoom attributes
    * @private */
   resetZoom() {
      ['x', 'y', 'z', 'x2', 'y2'].forEach(n => {
         this[`zoom_${n}min`] = undefined;
         this[`zoom_${n}max`] = undefined;
         this[`zoom_changed_${n}`] = undefined;
      });
   }

   /** @summary Mark/check if zoom for specific axis was changed interactively
     * @private */
   zoomChangedInteractive(axis, value) {
      if (axis === 'reset') {
         this.zoom_changed_x = this.zoom_changed_y = this.zoom_changed_z = undefined;
         return;
      }
      if (!axis || axis === 'any')
         return this.zoom_changed_x || this.zoom_changed_y || this.zoom_changed_z;

      if ((axis !== 'x') && (axis !== 'y') && (axis !== 'z'))
         return;

      const fld = 'zoom_changed_' + axis;
      if (value === undefined)
         return this[fld];

      if (value === 'unzoom') {
         // special handling of unzoom, only if was never changed before flag set to true
         this[fld] = (this[fld] === undefined);
         return;
      }

      if (value)
         this[fld] = true;
   }

   /** @summary Fill menu for frame when server is not there */
   fillObjectOfflineMenu(menu, kind) {
      if ((kind !== 'x') && (kind !== 'y'))
         return;

      menu.add('Unzoom', () => this.unzoom(kind));

      // here should be all axes attributes in offline
   }

   /** @summary Set grid drawing for specified axis */
   changeFrameAttr(attr, value) {
      const changes = {};
      this.v7AttrChange(changes, attr, value);
      this.v7SetAttr(attr, value);
      this.v7SendAttrChanges(changes, false); // do not invoke canvas update on the server
      this.redrawPad();
   }

   /** @summary Fill context menu */
   fillContextMenu(menu, kind, obj) {
      if (kind === 'pal')
         kind = 'z';

      if ((kind === 'x') || (kind === 'y') || (kind === 'x2') || (kind === 'y2')) {
         const handle = this[kind + '_handle'],
               faxis = obj || this[kind + 'axis'];
         if (!handle)
            return false;
         menu.header(`${kind.toUpperCase()} axis`, `${urlClassPrefix}ROOT_1_1Experimental_1_1RAxisBase.html`);

         if (isFunc(faxis?.TestBit)) {
            const main = this.getMainPainter(true);
            menu.addTAxisMenu(EAxisBits, main || this, faxis, kind);
            return true;
         }

         return handle.fillAxisContextMenu(menu, kind);
      }

      const alone = menu.size() === 0;

      if (alone)
         menu.header('Frame', `${urlClassPrefix}ROOT_1_1Experimental_1_1RFrame.html`);
      else
         menu.separator();

      if (this.zoom_xmin !== this.zoom_xmax)
         menu.add('Unzoom X', () => this.unzoom('x'));
      if (this.zoom_ymin !== this.zoom_ymax)
         menu.add('Unzoom Y', () => this.unzoom('y'));
      if (this.zoom_zmin !== this.zoom_zmax)
         menu.add('Unzoom Z', () => this.unzoom('z'));
      if (this.zoom_x2min !== this.zoom_x2max)
         menu.add('Unzoom X2', () => this.unzoom('x2'));
      if (this.zoom_y2min !== this.zoom_y2max)
         menu.add('Unzoom Y2', () => this.unzoom('y2'));
      menu.add('Unzoom all', () => this.unzoom('all'));

      menu.separator();

      menu.addchk(this.isTooltipAllowed(), 'Show tooltips', () => this.setTooltipAllowed('toggle'));

      if (this.x_handle)
         menu.addchk(this.x_handle.draw_grid, 'Grid x', flag => this.changeFrameAttr('gridX', flag));
      if (this.y_handle)
         menu.addchk(this.y_handle.draw_grid, 'Grid y', flag => this.changeFrameAttr('gridY', flag));
      if (this.x_handle && !this.x2_handle)
         menu.addchk(this.x_handle.draw_swapside, 'Swap x', flag => this.changeFrameAttr('swapX', flag));
      if (this.y_handle && !this.y2_handle)
         menu.addchk(this.y_handle.draw_swapside, 'Swap y', flag => this.changeFrameAttr('swapY', flag));
      if (this.x_handle && !this.x2_handle) {
         menu.sub('Ticks x');
         menu.addchk(this.x_handle.draw_ticks === 0, 'off', () => this.changeFrameAttr('ticksX', 0));
         menu.addchk(this.x_handle.draw_ticks === 1, 'normal', () => this.changeFrameAttr('ticksX', 1));
         menu.addchk(this.x_handle.draw_ticks === 2, 'ticks on both sides', () => this.changeFrameAttr('ticksX', 2));
         menu.addchk(this.x_handle.draw_ticks === 3, 'labels on both sides', () => this.changeFrameAttr('ticksX', 3));
         menu.endsub();
      }
      if (this.y_handle && !this.y2_handle) {
         menu.sub('Ticks y');
         menu.addchk(this.y_handle.draw_ticks === 0, 'off', () => this.changeFrameAttr('ticksY', 0));
         menu.addchk(this.y_handle.draw_ticks === 1, 'normal', () => this.changeFrameAttr('ticksY', 1));
         menu.addchk(this.y_handle.draw_ticks === 2, 'ticks on both sides', () => this.changeFrameAttr('ticksY', 2));
         menu.addchk(this.y_handle.draw_ticks === 3, 'labels on both sides', () => this.changeFrameAttr('ticksY', 3));
         menu.endsub();
      }

      menu.addAttributesMenu(this, alone ? '' : 'Frame ');
      menu.separator();

      menu.sub('Save as');
      const fmts = ['svg', 'png', 'jpeg', 'webp'];
      if (internals.makePDF)
         fmts.push('pdf');
      fmts.forEach(fmt => menu.add(`frame.${fmt}`, () => this.getPadPainter().saveAs(fmt, 'frame', `frame.${fmt}`)));
      menu.endsub();

      return true;
   }

   /** @summary Convert graphical coordinate into axis value */
   revertAxis(axis, pnt) { return this[`${axis}_handle`]?.revertPoint(pnt) ?? 0; }

   /** @summary Show axis status message
     * @desc method called normally when mouse enter main object element
     * @private */
   showAxisStatus(axis_name, evnt) {
      const hint_name = axis_name, hint_title = 'axis',
            m = pointer(evnt, this.getFrameSvg().node());
      let id = (axis_name === 'x') ? 0 : 1;

      if (this.#swap_xy)
         id = 1 - id;

      const axis_value = this.revertAxis(axis_name, m[id]);

      this.showObjectStatus(hint_name, hint_title, `${axis_name} : ${this.axisAsText(axis_name, axis_value)}`, `${Math.round(m[0])},${Math.round(m[1])}`);
   }

   /** @summary Add interactive keys handlers
    * @private */
   addKeysHandler() {
      if (this.isBatchMode() || this.#keys_handler || (typeof window === 'undefined'))
         return;

      FrameInteractive.assign(this);

      this.#keys_handler = evnt => this.processKeyPress(evnt);

      window.addEventListener('keydown', this.#keys_handler, false);
   }

   /** @summary Add interactive functionality to the frame
    * @private */
   addInteractivity(for_second_axes) {
      if (this.isBatchMode() || (!settings.Zooming && !settings.ContextMenu))
         return true;

      FrameInteractive.assign(this);
      if (!for_second_axes)
         this.addBasicInteractivity();
      return this.addFrameInteractivity(for_second_axes);
   }

   /** @summary Set selected range back to pad object - to be implemented
     * @private */
   setRootPadRange(/* pad, is3d */) {
      // TODO: change of pad range and send back to root application
   }

   /** @summary Toggle log scale on the specified axes */
   toggleAxisLog(axis) {
      const handle = this[axis + '_handle'];
      return handle?.changeAxisLog('toggle');
   }

} // class RFramePainter

/**
 * @summary Painter class for RPad
 *
 * @private
 */

class RPadPainter extends RObjectPainter {

   #iscan;      // is canvas flag
   #pad_name;   // name of the pad
   #pad;        // RPad object
   #painters;   // painters in the pad
   #pad_scale;  // scaling factor of the pad
   #pad_x;      // pad x coordinate
   #pad_y;      // pad y coordinate
   #pad_width;  // pad width
   #pad_height; // pad height
   #doing_draw; // drawing handles
   #custom_palette; // custom palette
   #frame_painter_ref; // frame painter
   #main_painter_ref; // main painter on the pad
   #num_primitives; // number of primitives
   #auto_color_cnt;  // counter for auto colors
   #fixed_size;  // fixed size flag
   #has_canvas;  // when top-level canvas exists
   #fast_drawing; // fast drawing mode
   #resize_tmout; // timeout handle for resize
   #start_draw_tm;  // time when start drawing primitives

   /** @summary constructor */
   constructor(dom, pad, opt, iscan, add_to_primitives) {
      super(dom, pad, '', 'pad');
      this.#pad = pad;
      this.#iscan = iscan; // indicate if working with canvas
      this.#pad_name = '';
      if (!iscan && pad) {
         if (pad.fObjectID)
            this.#pad_name = 'pad' + pad.fObjectID; // use objectid as pad name
         else
            this.#pad_name = 'ppp' + internals.id_counter++; // artificial name
      }
      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;

      if (opt !== undefined)
         this.decodeOptions(opt);

      if (add_to_primitives) {
         if ((add_to_primitives !== 'webpad') && this.getCanvSvg().empty()) {
            this.#has_canvas = false;
            this.#pad_name = '';
            this.setTopPainter();
         } else
            this.addToPadPrimitives(); // must be here due to pad painter
      }
   }

   /** @summary Returns pad name
     * @protected */
   getPadName() { return this.#pad_name; }

   /** @summary Indicates that drawing runs in batch mode
     * @private */
   isBatchMode() {
      if (this.batch_mode !== undefined)
         return this.batch_mode;

      if (isBatchMode())
         return true;

      return this.isTopPad() ? false : this.getCanvPainter()?.isBatchMode();
   }

   /** @summary Indicates that is not Root6 pad painter
    * @private */
   isRoot6() { return false; }

   /** @summary Returns true if pad is editable */
   isEditable() { return true; }

   /** @summary Returns true if button */
   isButton() { return false; }

   /** @summary Returns true if it is canvas
    * @param {Boolean} [is_online = false] - if specified, checked if it is canvas with configured connection to server */
   isCanvas(is_online = false) {
      if (!this.#iscan)
         return false;
      if (is_online === true)
         return isFunc(this.getWebsocket) && this.getWebsocket();
      return isStr(is_online) ? this.#iscan === is_online : true;
   }

   /** @summary Returns true if it is canvas or top pad without canvas */
   isTopPad() { return this.isCanvas() || !this.#has_canvas; }

   /** @summary returns pad painter
     * @protected */
   getPadPainter() { return this.isTopPad() ? null : super.getPadPainter(); }

   /** @summary returns canvas painter
     * @protected */
   getCanvPainter(try_select) { return this.isTopPad() ? this : super.getCanvPainter(try_select); }

   /** @summary Canvas main svg element
     * @return {object} d3 selection with canvas svg
     * @protected */
   getCanvSvg() { return this.selectDom().select('.root_canvas'); }

   /** @summary Pad svg element
     * @return {object} d3 selection with pad svg
     * @protected */
   getPadSvg() {
      const c = this.getCanvSvg();
      if (!this.#pad_name || c.empty())
         return c;

      return c.select('.primitives_layer .__root_pad_' + this.#pad_name);
   }

   /** @summary Method selects immediate layer under canvas/pad main element
     * @param {string} name - layer name like 'primitives_layer', 'btns_layer', 'info_layer'
     * @protected */
   getLayerSvg(name) { return this.getPadSvg().selectChild('.' + name); }

   /** @summary Returns svg element for the frame in current pad
     * @protected */
   getFrameSvg() {
      const layer = this.getLayerSvg('primitives_layer');
      if (layer.empty())
         return layer;
      let node = layer.node().firstChild;
      while (node) {
         const elem = select(node);
         if (elem.classed('root_frame'))
            return elem;
         node = node.nextSibling;
      }
      return select(null);
   }

   /** @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
    * @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.getPadSvg();
      if (!svg_p.empty()) {
         svg_p.property('pad_painter', null);
         if (!this.isCanvas())
            svg_p.remove();
      }

      this.#main_painter_ref = undefined;
      this.#frame_painter_ref = undefined;
      this.#pad_x = this.#pad_y = this.#pad_width = this.#pad_height = undefined;
      this.#doing_draw = undefined;
      delete this._dfltRFont;

      this.#painters = [];
      this.#pad = undefined;
      this.assignObject(null);
      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 Assign actual frame painter
     * @private */
   setFramePainter(fp, on) {
      if (on)
         this.#frame_painter_ref = fp;
      else if (this.#frame_painter_ref === fp)
         this.#frame_painter_ref = undefined;
   }

   /** @summary get pad width */
   getPadWidth() { return this.#pad_width || 0; }

   /** @summary get pad height */
   getPadHeight() { return this.#pad_height || 0; }

   /** @summary get pad height */
   getPadScale() { return this.#pad_scale || 1; }

   /** @summary return pad log state x or y are allowed */
   getPadLog(/* name */) { return false; }

   /** @summary get pad rect */
   getPadRect() {
      return {
         x: this.#pad_x || 0,
         y: this.#pad_y || 0,
         width: this.getPadWidth(),
         height: this.getPadHeight()
      };
   }

   /** @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 = {};

      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
    * @private */
   cleanPrimitives(selector) {
      // remove all primitives
      if (selector === true)
         selector = () => true;

      if (!isFunc(selector))
         return false;

      let is_any = false;

      for (let k = this.#painters.length - 1; k >= 0; --k) {
         const subp = this.#painters[k];
         if (selector(subp)) {
            subp.cleanup();
            this.#painters.splice(k, 1);
            is_any = true;
         }
      }

      return is_any;
   }

   /** @summary Divide pad on sub-pads */
   async divide(/* nx, ny, use_existing */) {
      if (settings.Debug)
         console.warn('RPadPainter.divide not implemented');
      return this;
   }

   /** @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, 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 = [];
      let resindx = indx - 1; // object removed itself
      arr.push(prim);
      this.#painters.splice(indx, 1);

      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.getMainPainter() === painter) {
            delete this.setMainPainter(undefined, true);
            resindx = -111;
         }
      });

      return resindx;
   }

   /** @summary try to find object by name in list of pad primitives
     * @desc used to find title drawing
     * @private */
   findInPrimitives(/* objname, objtype */) {
      if (settings.Debug)
         console.warn('findInPrimitives not implemented for RPad');
      return null;
   }

   /** @summary Try to find painter for specified object
     * @desc can be used to find painter for some special objects, registered as
     * histogram functions
     * @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 Returns palette associated with pad.
     * @desc Either from existing palette painter or just default palette */
   getHistPalette() {
      const pp = this.findPainterFor(undefined, undefined, `${nsREX}RPaletteDrawable`);

      if (pp)
         return pp.getHistPalette();

      if (!this.fDfltPalette) {
         this.fDfltPalette = {
            _typename: `${nsREX}RPalette`,
            fColors: [{ fOrdinal: 0, fColor: { fColor: 'rgb(53, 42, 135)' } },
                      { fOrdinal: 0.125, fColor: { fColor: 'rgb(15, 92, 221)' } },
                      { fOrdinal: 0.25, fColor: { fColor: 'rgb(20, 129, 214)' } },
                      { fOrdinal: 0.375, fColor: { fColor: 'rgb(6, 164, 202)' } },
                      { fOrdinal: 0.5, fColor: { fColor: 'rgb(46, 183, 164)' } },
                      { fOrdinal: 0.625, fColor: { fColor: 'rgb(135, 191, 119)' } },
                      { fOrdinal: 0.75, fColor: { fColor: 'rgb(209, 187, 89)' } },
                      { fOrdinal: 0.875, fColor: { fColor: 'rgb(254, 200, 50)' } },
                      { fOrdinal: 1, fColor: { fColor: 'rgb(249, 251, 14)' } }],
            fInterpolate: true,
            fNormalized: true
         };
         exports.addMethods(this.fDfltPalette, `${nsREX}RPalette`);
      }

      return this.fDfltPalette;
   }

   /** @summary Returns custom palette
     * @private */
   getCustomPalette(no_recursion) {
      return this.#custom_palette || (no_recursion ? null : this.getCanvPainter()?.getCustomPalette(true));
   }

   /** @summary Returns number of painters
     * @protected */
   getNumPainters() { return this.#painters.length; }

   /** @summary Add painter to pad list of painters
     * @protected */
   addToPrimitives(painter) {
      if (this.#painters.indexOf(painter) < 0)
         this.#painters.push(painter);
      return this;
   }

   /** @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
     * @private */
   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 canp = this.isTopPad() ? this : this.getCanvPainter();

      if (painter === undefined)
         painter = this;

      if (pos && !this.isTopPad())
         pos = getAbsPosInCanvas(this.getPadSvg(), pos);

      selectActivePad({ pp: this, active: true });

      canp.producePadEvent('select', this, painter, pos);
   }

   /** @summary Set fast drawing property depending on the size
     * @private */
   setFastDrawing(w, h) {
      const was_fast = this.#fast_drawing;
      this.#fast_drawing = !this.hasSnapId() && settings.SmallPad && ((w < settings.SmallPad.width) || (h < settings.SmallPad.height));
      if (was_fast !== this.#fast_drawing)
         this.showPadButtons();
   }

   /** @summary Return fast drawing flag
   * @private */
   isFastDrawing() { return this.#fast_drawing; }

   /** @summary Returns true if canvas configured with grayscale
     * @private */
   isGrayscale() { return false; }

   /** @summary Set grayscale mode for the canvas
     * @private */
   setGrayscale(/* flag */) {
      console.error('grayscale mode not implemented for RCanvas');
   }

   /** @summary Returns true if default pad range is configured
     * @private */
   isDefaultPadRange() {
      return true;
   }

   /** @summary Create SVG element for the canvas */
   createCanvasSvg(check_resize, new_size) {
      const lmt = 5;
      let factor, svg, rect, btns, 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 (!this.isBatchMode())
            btns = this.getLayerSvg('btns_layer');

         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 (!this.isBatchMode() && !this.online_canvas)
            svg.append('svg:title').text('ROOT canvas');

         if (!this.isBatchMode())
            svg.style('user-select', settings.UserSelect || null);

         frect = svg.append('svg:path').attr('class', 'canvas_fillrect');
         if (!this.isBatchMode()) {
            frect.style('pointer-events', 'visibleFill')
                 .on('dblclick', evnt => this.enlargePad(evnt, true))
                 .on('click', () => this.selectObjectPainter(this, null))
                 .on('mouseenter', () => this.showObjectStatus())
                 .on('contextmenu', settings.ContextMenu ? evnt => this.padContextMenu(evnt) : null);
         }

         svg.append('svg:g').attr('class', 'primitives_layer');
         svg.append('svg:g').attr('class', 'info_layer');
         if (!this.isBatchMode()) {
            btns = svg.append('svg:g')
                      .attr('class', 'btns_layer')
                      .property('leftside', settings.ToolBarSide === 'left')
                      .property('vertical', settings.ToolBarVert);
         }

         factor = 0.66;
         if (this.#pad?.fWinSize[0] && this.#pad.fWinSize[1]) {
            factor = this.#pad.fWinSize[1] / this.#pad.fWinSize[0];
            if ((factor < 0.1) || (factor > 10))
               factor = 0.66;
         }

         if (this.#fixed_size) {
            render_to.style('overflow', 'auto');
            rect = { width: this.#pad.fWinSize[0], height: this.#pad.fWinSize[1] };
            if (!rect.width || !rect.height)
               rect = getElementRect(render_to);
         } else
            rect = this.testMainResize(2, new_size, factor);
      }

      this.createAttFill({ pattern: 1001, color: 0 });

      if ((rect.width <= lmt) || (rect.height <= lmt)) {
         if (!this.hasSnapId()) {
            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);

      if (this.#fixed_size) {
         svg.attr('x', 0)
            .attr('y', 0)
            .attr('width', rect.width)
            .attr('height', rect.height)
            .style('position', 'absolute');
      } else {
         svg.attr('x', 0)
            .attr('y', 0)
            .style('width', '100%')
            .style('height', '100%')
            .style('position', 'absolute')
            .style('left', 0).style('top', 0).style('bottom', 0).style('right', 0);
      }

      svg.style('filter', settings.DarkMode ? '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;

      frect.attr('d', `M0,0H${rect.width}V${rect.height}H0Z`)
           .call(this.fillatt.func);

      this.setFastDrawing(rect.width, rect.height);

      if (isFunc(this.alignButtons) && btns)
         this.alignButtons(btns, rect.width, rect.height);

      return true;
   }

   /** @summary Draw item name on canvas, dummy for RPad
     * @private */
   drawItemNameOnCanvas() {
   }

   /** @summary Enlarge pad draw element when possible */
   enlargePad(evnt, is_dblclick, is_escape) {
      evnt?.preventDefault();
      evnt?.stopPropagation();

      // ignore double click on online canvas itself for enlarge
      if (is_dblclick && this.isCanvas(true) && (this.enlargeMain('state') === 'off'))
         return;

      const svg_can = this.getCanvSvg(),
            pad_enlarged = svg_can.property('pad_enlarged');

      if (this.isTopPad() || (!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 SVG element for the pad
     * @return true when pad is displayed and all its items should be redrawn */
   createPadSvg(only_resize) {
      if (this.isTopPad()) {
         this.createCanvasSvg(only_resize ? 2 : 0);
         return true;
      }

      const svg_parent = this.getPadPainter()?.getPadSvg(),
            svg_can = this.getCanvSvg(),
            width = svg_parent.property('draw_width'),
            height = svg_parent.property('draw_height'),
            pad_enlarged = svg_can.property('pad_enlarged');
      let pad_visible = true,
          w = width, h = height, x = 0, y = 0,
          svg_pad, svg_rect, btns = null;

      if (this.#pad?.fPos && this.#pad?.fSize) {
         x = Math.round(width * this.#pad.fPos.fHoriz.fArr[0]);
         y = Math.round(height * this.#pad.fPos.fVert.fArr[0]);
         w = Math.round(width * this.#pad.fSize.fHoriz.fArr[0]);
         h = Math.round(height * this.#pad.fSize.fVert.fArr[0]);
      }

      if (pad_enlarged) {
         pad_visible = false;
         if (pad_enlarged === this.#pad)
            pad_visible = true;
         else {
            this.forEachPainterInPad(pp => {
               if (pp.getObject() === pad_enlarged)
                  pad_visible = true;
            }, 'pads');
         }

         if (pad_visible) {
            w = width;
            h = height;
            x = y = 0;
         }
      }

      if (only_resize) {
         svg_pad = this.getPadSvg();
         svg_rect = svg_pad.selectChild('.root_pad_border');
         if (!this.isBatchMode())
            btns = this.getLayerSvg('btns_layer');
         this.addPadInteractive(true);
      } else {
         svg_pad = svg_parent.selectChild('.primitives_layer')
             .append('svg:svg') // here was g before, svg used to blend all drawings outside
             .classed('__root_pad_' + this.#pad_name, true)
             .attr('pad', this.#pad_name) // set extra attribute  to mark pad name
             .property('pad_painter', this); // this is custom property

         if (!this.isBatchMode())
            svg_pad.append('svg:title').text('ROOT subpad');

         svg_rect = svg_pad.append('svg:path').attr('class', 'root_pad_border');

         svg_pad.append('svg:g').attr('class', 'primitives_layer');
         if (!this.isBatchMode()) {
            btns = svg_pad.append('svg:g')
                          .attr('class', 'btns_layer')
                          .property('leftside', settings.ToolBarSide !== 'left')
                          .property('vertical', settings.ToolBarVert);
         }

         if (settings.ContextMenu)
            svg_rect.on('contextmenu', evnt => this.padContextMenu(evnt));

         if (!this.isBatchMode()) {
            svg_rect.style('pointer-events', 'visibleFill') // get events also for not visible rect
                    .on('dblclick', evnt => this.enlargePad(evnt, true))
                    .on('click', () => this.selectObjectPainter(this, null))
                    .on('mouseenter', () => this.showObjectStatus());
         }
      }

      this.createAttFill({ attr: this.#pad });

      this.createAttLine({ attr: this.#pad, color0: this.#pad.fBorderMode === 0 ? '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;

      svg_rect.attr('d', `M0,0H${w}V${h}H0Z`)
              .call(this.fillatt.func)
              .call(this.lineatt.func);

      this.setFastDrawing(w, h);

      // special case of 3D canvas overlay
      if (svg_pad.property('can3d') === constants$1.Embed3D.Overlay) {
         this.selectDom().select('.draw3d_' + 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())
      //   return;
   }

   /** @summary returns true if any objects beside sub-pads exists in the pad */
   hasObjectsToDraw() {
      return this.#pad?.fPrimitives?.find(obj => obj._typename !== `${nsREX}RPadDisplayItem`);
   }

   /** @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 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)
         this.#doing_draw = undefined;
      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
     * @private */
   async drawPrimitives(indx) {
      if (indx === undefined) {
         if (this.isCanvas())
            this.#start_draw_tm = new Date().getTime();

         // set number of primitives
         this.#num_primitives = this.#pad?.fPrimitives?.length ?? 0;

         return this.syncDraw(true).then(() => this.drawPrimitives(0));
      }

      if (!this.#pad || (indx >= this.#num_primitives)) {
         this.confirmDraw();

         if (this.#start_draw_tm) {
            const spenttm = new Date().getTime() - this.#start_draw_tm;
            if (spenttm > 3000)
               console.log(`Canvas drawing took ${(spenttm * 1e-3).toFixed(2)}s`);
            this.#start_draw_tm = undefined;
         }

         return;
      }

      // handle used to invoke callback only when necessary
      return this.drawObject(this, this.#pad.fPrimitives[indx], '').then(op => {
         // mark painter as belonging to primitives
         if (isObject(op))
            op._primitive = true;

         return this.drawPrimitives(indx + 1);
      });
   }

   /** @summary Provide autocolor
     * @private */
   getAutoColor() {
      const pal = this.getHistPalette(),
            cnt = this.#auto_color_cnt++,
            num = Math.max(this.#num_primitives - 1, 2);
      return pal?.getColorOrdinal((cnt % num) / num) ?? 'blue';
   }

   /** @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) {
      const clname = this.isCanvas() ? 'RCanvas' : 'RPad';

      menu.header(clname, `${urlClassPrefix}ROOT_1_1Experimental_1_1${clname}.html`);

      menu.addchk(this.isTooltipAllowed(), 'Show tooltips', () => this.setTooltipAllowed('toggle'));

      if (!this.isCanvas(true)) {
         // if not online canvas
         menu.addAttributesMenu(this);
         if (this.isCanvas()) {
            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.isTopPad() && this.hasObjectsToDraw()))
         menu.addchk((this.enlargeMain('state') === 'on'), 'Enlarge ' + (this.isCanvas() ? 'canvas' : 'pad'), () => this.enlargePad());

      const fname = this.#pad_name || (this.isCanvas() ? 'canvas' : 'pad');
      menu.sub('Save as');
      ['svg', 'png', 'jpeg', 'pdf', 'webp'].forEach(fmt => menu.add(`${fname}.${fmt}`, () => this.saveAs(fmt, this.isCanvas(), `${fname}.${fmt}`)));
      menu.endsub();

      return true;
   }

   /** @summary Show pad context menu
     * @private */
   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();
      }

      createMenu(evnt, this).then(menu => {
         this.fillContextMenu(menu);
         return this.fillObjectExecMenu(menu);
      }).then(menu => menu.show());
   }

   /** @summary Redraw legend object
    * @desc Used when object attributes are changed to ensure that legend is up to date
    * @private */
   async redrawLegend() {}

   /** @summary Deliver mouse move or click event to the web canvas
     * @private */
   deliverWebCanvasEvent() {}

   /** @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 RPad redrawing');
         return false;
      }

      let showsubitems = true;
      const redrawNext = indx => {
         while (indx < this.#painters.length) {
            const sub = this.#painters[indx++];
            let res = 0;
            if (showsubitems || isPadPainter(sub))
               res = sub.redraw(reason);

            if (isPromise(res))
               return res.then(() => redrawNext(indx));
         }
         return true;
      };

      return sync_promise.then(() => {
         if (this.isCanvas())
            this.createCanvasSvg(2);
         else
            showsubitems = this.createPadSvg(true);

         return redrawNext(0);
      }).then(() => {
         this.addPadInteractive();
         if (getActivePad() === this)
            this.getCanvPainter()?.producePadEvent('padredraw', this);
         this.confirmDraw();
         return true;
      });
   }

   /** @summary redraw pad */
   redraw(reason) {
      return this.redrawPad(reason);
   }


   /** @summary Checks if pad should be redrawn by resize
     * @private */
   needRedrawByResize() {
      const elem = this.getPadSvg();
      if (!elem.empty() && elem.property('can3d') === constants$1.Embed3D.Overlay)
         return true;

      for (let i = 0; i < this.#painters.length; ++i) {
         if (isFunc(this.#painters[i].needRedrawByResize)) {
            if (this.#painters[i].needRedrawByResize())
               return true;
         }
      }

      return false;
   }

   /** @summary Check resize of canvas */
   checkCanvasResize(size, force) {
      if (this._ignore_resize || !this.isTopPad())
         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(() => {
         changed = this.createCanvasSvg(force ? 2 : 1, size);

         if (changed && this.isCanvas() && this.#pad && this.online_canvas && !this.embed_canvas && !this.isBatchMode()) {
            if (this.#resize_tmout)
               clearTimeout(this.#resize_tmout);
            this.#resize_tmout = setTimeout(() => {
               this.#resize_tmout = undefined;
               if (!this.#pad?.fWinSize)
                  return;
               const cw = this.getPadWidth(), ch = this.getPadHeight();
               if ((cw > 0) && (ch > 0) && ((this.#pad.fWinSize[0] !== cw) || (this.#pad.fWinSize[1] !== ch))) {
                  this.#pad.fWinSize[0] = cw;
                  this.#pad.fWinSize[1] = ch;
                  this.sendWebsocket(`RESIZED:[${cw},${ch}]`);
               }
            }, 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 RPad object
     * @private */
   updateObject(obj) {
      if (!obj)
         return false;

      this.#pad.fStyle = obj.fStyle;
      this.#pad.fAttr = obj.fAttr;

      if (this.isCanvas()) {
         this.#pad.fTitle = obj.fTitle;
         this.#pad.fWinSize = obj.fWinSize;
      } else {
         this.#pad.fPos = obj.fPos;
         this.#pad.fSize = obj.fSize;
      }

      return true;
   }

   /** @summary Add object painter to list of primitives
     * @private */
   addObjectPainter(objpainter, lst, indx) {
      if (objpainter && lst && lst[indx] && !objpainter.hasSnapId()) {
         // keep snap id in painter, will be used for the
         if (this.#painters.indexOf(objpainter) < 0)
            this.#painters.push(objpainter);
         objpainter.assignSnapId(lst[indx].fObjectID);
         if (!objpainter.rstyle)
            objpainter.rstyle = lst[indx].fStyle || this.rstyle;
      }
   }

   /** @summary Extract properties from TObjectDisplayItem */
   extractTObjectProp(snap) {
      if (snap.fColIndex && snap.fColValue) {
         const colors = this.getColors() || getRootColors();
         for (let k = 0; k < snap.fColIndex.length; ++k)
            colors[snap.fColIndex[k]] = convertColor(snap.fColValue[k]);
      }

      // painter used only for evaluation of attributes
      const pattr = new RObjectPainter(), obj = snap.fObject;
      pattr.assignObject(snap);
      pattr.csstype = snap.fCssType;
      pattr.rstyle = snap.fStyle;

      snap.fOption = pattr.v7EvalAttr('options', '');

      const extract_color = (member_name, attr_name) => {
         const col = pattr.v7EvalColor(attr_name, '');
         if (col)
            obj[member_name] = addColor(col, this.getColors());
      };

      // handle TAttLine
      if ((obj.fLineColor !== undefined) && (obj.fLineWidth !== undefined) && (obj.fLineStyle !== undefined)) {
         extract_color('fLineColor', 'line_color');
         obj.fLineWidth = pattr.v7EvalAttr('line_width', obj.fLineWidth);
         obj.fLineStyle = pattr.v7EvalAttr('line_style', obj.fLineStyle);
      }

      // handle TAttFill
      if ((obj.fFillColor !== undefined) && (obj.fFillStyle !== undefined)) {
         extract_color('fFillColor', 'fill_color');
         obj.fFillStyle = pattr.v7EvalAttr('fill_style', obj.fFillStyle);
      }

      // handle TAttMarker
      if ((obj.fMarkerColor !== undefined) && (obj.fMarkerStyle !== undefined) && (obj.fMarkerSize !== undefined)) {
         extract_color('fMarkerColor', 'marker_color');
         obj.fMarkerStyle = pattr.v7EvalAttr('marker_style', obj.fMarkerStyle);
         obj.fMarkerSize = pattr.v7EvalAttr('marker_size', obj.fMarkerSize);
      }

      // handle TAttText
      if ((obj.fTextColor !== undefined) && (obj.fTextAlign !== undefined) && (obj.fTextAngle !== undefined) && (obj.fTextSize !== undefined)) {
         extract_color('fTextColor', 'text_color');
         obj.fTextAlign = pattr.v7EvalAttr('text_align', obj.fTextAlign);
         obj.fTextAngle = pattr.v7EvalAttr('text_angle', obj.fTextAngle);
         obj.fTextSize = pattr.v7EvalAttr('text_size', obj.fTextSize);
         // TODO: v7 font handling differs much from v6, ignore for the moment
      }
   }

   /** @summary Function called when drawing next snapshot from the list
     * @return {Promise} with pad painter when ready
     * @private */
   async drawNextSnap(lst, pindx, indx) {
      if (indx === undefined) {
         indx = -1;
         // flag used to prevent immediate pad redraw during first draw
         this.#num_primitives = lst?.length ?? 0;
         this.#auto_color_cnt = 0;
      }

      delete this.next_rstyle;

      ++indx; // change to the next snap

      if (!lst || indx >= lst.length) {
         this.#auto_color_cnt = undefined;
         return this;
      }

      const snap = lst[indx], is_subpad = snap._typename === `${nsREX}RPadDisplayItem`;

      // empty object, no need to do something, take next
      if (snap.fDummy)
         return this.drawNextSnap(lst, pindx + 1, indx);

      if (snap._typename === `${nsREX}TObjectDisplayItem`) {
         // identifier used in TObjectDrawable

         if (snap.fKind === webSnapIds.kStyle) {
            Object.assign(gStyle, snap.fObject);
            return this.drawNextSnap(lst, pindx, indx);
         }

         if (snap.fKind === webSnapIds.kColors) {
            const colors = [], arr = snap.fObject.arr;
            for (let n = 0; n < arr.length; ++n) {
               const name = arr[n].fString, p = name.indexOf('=');
               if (p > 0)
                  colors[parseInt(name.slice(0, p))] = convertColor(name.slice(p + 1));
            }

            this.setColors(colors);
            // set global list of colors
            // adoptRootColors(ListOfColors);
            return this.drawNextSnap(lst, pindx, indx);
         }

         if (snap.fKind === webSnapIds.kPalette) {
            const arr = snap.fObject.arr, palette = [];
            for (let n = 0; n < arr.length; ++n)
               palette[n] = arr[n].fString;
            this.#custom_palette = new ColorPalette(palette);
            return this.drawNextSnap(lst, pindx, indx);
         }

         if (snap.fKind === webSnapIds.kFont)
            return this.drawNextSnap(lst, pindx, indx);

         if (!this.getFramePainter()) {
            // draw dummy frame which is not provided by RCanvas
            return this.drawObject(this, { _typename: clTFrame, $dummy: true }, '')
                       .then(() => this.drawNextSnap(lst, pindx, indx - 1));
         }

         this.extractTObjectProp(snap);
      }

      // try to locate existing object painter, only allowed when redrawing pad snap
      let objpainter, promise;

      while ((pindx !== undefined) && (pindx < this.#painters.length)) {
         const subp = this.#painters[pindx++];

         if (subp.getSnapId() === snap.fObjectID) {
            objpainter = subp;
            break;
         } else if (subp.getSnapId() && !subp.isSecondary() && !is_subpad) {
            console.warn(`Mismatch in snapid between painter ${subp.getSnapId()} secondary: ${subp.isSecondary()} type: ${subp.getClassName()} and primitive ${snap.fObjectID} kind ${snap.fKind} type ${snap.fDrawable?._typename}`);
            break;
         }
      }

      if (objpainter) {
         if (is_subpad)
            promise = objpainter.redrawPadSnap(snap);
         else if (objpainter.updateObject(snap.fDrawable || snap.fObject || snap, snap.fOption || '', true))
            promise = objpainter.redraw();
      } else if (is_subpad) {
         const padpainter = new RPadPainter(this, snap, '', false, 'webpad');
         padpainter.assignSnapId(snap.fObjectID);
         padpainter.rstyle = snap.fStyle;

         padpainter.createPadSvg();

         if (snap.fPrimitives?.length)
            padpainter.addPadButtons();

         pindx++; // new painter will be add
         promise = padpainter.drawNextSnap(snap.fPrimitives).then(() => padpainter.addPadInteractive());
      } else {
         // will be used in addToPadPrimitives to assign style to sub-painters
         this.next_rstyle = snap.fStyle || this.rstyle;
         pindx++; // new painter will be add

         // TODO - fDrawable is v7, fObject from v6, maybe use same data member?
         promise = this.drawObject(this, snap.fDrawable || snap.fObject || snap, snap.fOption || '')
                       .then(objp => this.addObjectPainter(objp, lst, indx));
      }

      return getPromise(promise).then(() => this.drawNextSnap(lst, pindx, indx)); // call next
   }

   /** @summary Search painter with specified snapid, also sub-pads are checked
     * @private */
   findSnap(snapid, onlyid) {
      function check(checkid) {
         if (!checkid || !isStr(checkid))
            return false;
         if (checkid === snapid)
            return true;
         return onlyid && (checkid.length > snapid.length) &&
                (checkid.indexOf(snapid) === (checkid.length - snapid.length));
      }

      if (check(this.getSnapId()))
         return this;

      if (!this.#painters)
         return null;

      for (let k = 0; k < this.#painters.length; ++k) {
         let sub = this.#painters[k];

         if (!onlyid && isFunc(sub.findSnap))
            sub = sub.findSnap(snapid);
         else if (!check(sub.getSnapId()))
            sub = null;

         if (sub)
            return sub;
      }

      return null;
   }

   /** @summary Redraw pad snap
     * @desc Online version of drawing pad primitives
     * @return {Promise} with pad painter */
   async redrawPadSnap(snap) {
      // for the pad/canvas display item contains list of primitives plus pad attributes

      if (!snap || !snap.fPrimitives)
         return this;

      if (this.isCanvas(true) && snap.fTitle && !this.embed_canvas && (typeof document !== 'undefined'))
         document.title = snap.fTitle;

      if (!this.hasSnapId()) {
         // first time getting snap, create all gui elements first

         this.assignSnapId(snap.fObjectID);

         this.assignObject(snap);
         this.#pad = snap;

         if (this.isBatchMode() && this.isCanvas())
            this.#fixed_size = true;

         const mainid = this.selectDom().attr('id');

         if (!this.isBatchMode() && this.online_canvas && !this.use_openui && !this.brlayout && mainid && isStr(mainid) && !getHPainter()) {
            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.createCanvasSvg(0);
         this.addPadButtons(true);

         return this.drawNextSnap(snap.fPrimitives).then(() => {
            if (isFunc(this.onCanvasUpdated))
               this.onCanvasUpdated(this);
            return this;
         });
      }

      // update only pad/canvas attributes
      this.updateObject(snap);

      // apply all changes in the object (pad or canvas)
      if (this.isCanvas())
         this.createCanvasSvg(2);
      else
         this.createPadSvg(true);

      let missmatch = false, i = 0, k = 0;

      // match painters with new list of primitives
      while (k < this.#painters.length) {
         const sub = this.#painters[k];

         // skip check secondary painters or painters without snapid
         // also frame painter will be excluded here
         if (!sub.hasSnapId() || sub.isSecondary()) {
            k++;
            continue; // look only for painters with snapid
         }

         if (i >= snap.fPrimitives.length)
            break;


         const prim = snap.fPrimitives[i];

         if (prim.fObjectID === sub.getSnapId()) {
            i++;
            k++;
         } else if (prim.fDummy || !prim.fObjectID || ((prim._typename === `${nsREX}TObjectDisplayItem`) && ((prim.fKind === webSnapIds.kStyle) || (prim.fKind === webSnapIds.kColors) || (prim.fKind === webSnapIds.kPalette) || (prim.fKind === webSnapIds.kFont)))) {
            // ignore primitives without snapid or which are not produce drawings
            i++;
         } else {
            missmatch = true;
            break;
         }
      }

      let cnt = 1000;
      // remove painters without primitives, limit number of checks
      while (!missmatch && (k < this.#painters.length) && (--cnt >= 0)) {
         if (this.removePrimitive(k) === -111)
            missmatch = true;
      }
      if (cnt < 0)
         missmatch = true;

      if (missmatch) {
         const old_painters = this.#painters;
         this.#painters = [];
         old_painters.forEach(objp => objp.cleanup());
         this.setMainPainter(undefined, true);
         if (isFunc(this.removePadButtons))
            this.removePadButtons();
         this.addPadButtons(true);
      }

      return this.drawNextSnap(snap.fPrimitives, missmatch ? undefined : 0).then(() => {
         this.addPadInteractive();
         if (getActivePad() === this)
            this.getCanvPainter()?.producePadEvent('padredraw', this);
         if (isFunc(this.onCanvasUpdated))
            this.onCanvasUpdated(this);
         return this;
      });
   }

   /** @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 Show context menu for specified item
     * @private */
   itemContextMenu(name) {
      const rrr = this.getPadSvg().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.getMainPainter();
            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.getSnapId())
            selp.fillObjectExecMenu(menu, selkind).then(() => postponePromise(() => menu.show(), 50));
      });
   }

   /** @summary Save pad in specified format
     * @desc Used from context menu */
   saveAs(kind, full_canvas, filename) {
      if (!filename)
         filename = (this.#pad_name || (this.isCanvas() ? 'canvas' : 'pad')) + '.' + kind;

      this.produceImage(full_canvas, kind).then(imgdata => {
         if (!imgdata)
            return console.error(`Fail to produce image ${filename}`);

         if ((browser.qt6 || browser.cef3) && this.getSnapId()) {
            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
            saveFile(filename, (kind !== 'svg') ? imgdata : prSVG + encodeURIComponent(imgdata));
      });
   }

   /** @summary Search active pad
     * @return {Object} pad painter for active pad */
   findActivePad() { return null; }

   /** @summary Produce image for the pad
     * @return {Promise} with created image */
   async produceImage(full_canvas, file_format, args) {
      const use_frame = (full_canvas === 'frame'),
            elem = use_frame ? this.getFrameSvg() : (full_canvas ? this.getCanvSvg() : this.getPadSvg()),
            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);
         }
      }

      if (!use_frame) {
         // do not make transformations for the frame
         painter.forEachPainterInPad(pp => {
            const item = { prnt: pp.getPadSvg() };
            items.push(item);

            // remove buttons from each sub-pad
            const btns = pp.getLayerSvg('btns_layer');
            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$1.Embed3D.Overlay) && (can3d !== constants$1.Embed3D.Embed))
               return;

            const main = isFunc(fp.getRenderer) ? fp : fp.getMainPainter(),
                  canvas = isFunc(main.getRenderer) ? main.getRenderer()?.domElement : null;
            if (!isFunc(main?.render3D) || !isObject(canvas))
               return;

            const sz2 = fp.getSizeFor3d(constants$1.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$1.Embed3D.Embed) {
               item.foreign = item.prnt.select('.' + sz2.clname);
               item.foreign.remove();
            }

            const svg_frame = fp.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 => {
         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');

            menu.add(this.isCanvas() ? 'Canvas' : 'Pad', 'pad', this.itemContextMenu);

            if (this.getFramePainter())
               menu.add('Frame', 'frame', this.itemContextMenu);

            const main = this.getMainPainter(); // here hist painter methods

            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) || pp.isSecondary())
                     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 });

      if (!this.isTopPad() && funcname.indexOf('Pad') && (funcname !== 'enlargePad')) {
         const cp = this.getCanvPainter();
         if (cp && (cp !== this))
            cp.addPadButton(btn, tooltip, funcname);
      }
   }

   /** @summary Add buttons for pad or canvas
     * @private */
   addPadButtons(is_online) {
      this.addPadButton('camera', 'Create PNG', this.isCanvas() ? 'CanvasSnapShot' : 'PadSnapShot', 'Ctrl PrintScreen');

      if (settings.ContextMenu)
         this.addPadButton('question', 'Access context menus', 'PadContextMenus');

      const add_enlarge = !this.isTopPad() && 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 Show pad buttons
     * @private */
   showPadButtons() {
      if (!this._buttons)
         return;

      PadButtonsHandler.assign(this);
      this.showPadButtons();
   }

   /** @summary Calculates RPadLength value */
   getPadLength(vertical, len, frame_painter) {
      let rect, res;
      const sign = vertical ? -1 : 1,
            getV = (indx, dflt) => { return (indx < len.fArr.length) ? len.fArr[indx] : dflt; },
            getRect = () => {
               if (!rect)
                  rect = frame_painter ? frame_painter.getFrameRect() : this.getPadRect();
               return rect;
            };

      if (frame_painter) {
         const user = getV(2), func = vertical ? 'gry' : 'grx';
         if ((user !== undefined) && frame_painter[func])
            res = frame_painter[func](user);
      }

      if (res === undefined)
         res = vertical ? getRect().height : 0;

      const norm = getV(0, 0), pixel = getV(1, 0);

      res += sign * pixel;

      if (norm)
         res += sign * (vertical ? getRect().height : getRect().width) * norm;

      return Math.round(res);
   }


   /** @summary Calculates pad position for RPadPos values
     * @param {object} pos - instance of RPadPos
     * @param {object} frame_painter - if drawing will be performed inside frame, frame painter */
   getCoordinate(pos, frame_painter) {
      return {
         x: this.getPadLength(false, pos.fHoriz, frame_painter),
         y: this.getPadLength(true, pos.fVert, frame_painter)
      };
   }

   /** @summary Decode pad draw options */
   decodeOptions(opt) {
      const pad = this.getObject();
      if (!pad)
         return;

      const d = new DrawOptions(opt),
            o = this.setOptions({ GlobalColors: true, LocalColors: false, IgnorePalette: false, RotateFrame: false, FixFrame: false });

      if (d.check('NOCOLORS') || d.check('NOCOL'))
         o.GlobalColors = o.LocalColors = false;
      if (d.check('LCOLORS') || d.check('LCOL')) {
         o.GlobalColors = false;
         o.LocalColors = true;
      }
      if (d.check('NOPALETTE') || d.check('NOPAL'))
         o.IgnorePalette = true;
      if (d.check('ROTATE'))
         o.RotateFrame = true;
      if (d.check('FIXFRAME'))
         o.FixFrame = true;

      if (d.check('WHITE'))
         pad.fFillColor = 0;
      if (d.check('LOGX'))
         pad.fLogx = 1;
      if (d.check('LOGY'))
         pad.fLogy = 1;
      if (d.check('LOGZ'))
         pad.fLogz = 1;
      if (d.check('LOG'))
         pad.fLogx = pad.fLogy = pad.fLogz = 1;
      if (d.check('GRIDX'))
         pad.fGridx = 1;
      if (d.check('GRIDY'))
         pad.fGridy = 1;
      if (d.check('GRID'))
         pad.fGridx = pad.fGridy = 1;
      if (d.check('TICKX'))
         pad.fTickx = 1;
      if (d.check('TICKY'))
         pad.fTicky = 1;
      if (d.check('TICK'))
         pad.fTickx = pad.fTicky = 1;
   }

   /** @summary draw RPad object */
   static async draw(dom, pad, opt) {
      const painter = new RPadPainter(dom, pad, opt, false, true);

      painter.createPadSvg();

      if (painter.matchObjectType(nsREX + 'RPad') && (painter.isTopPad() || painter.hasObjectsToDraw()))
         painter.addPadButtons();

      selectActivePad({ pp: painter, active: false });

      // flag used to prevent immediate pad redraw during first draw
      return painter.drawPrimitives().then(() => {
         painter.addPadInteractive();
         painter.showPadButtons();
         return painter;
      });
   }

} // class RPadPainter

/**
 * @summary Painter class for RCanvas
 *
 * @private
 */

class RCanvasPainter extends RPadPainter {

   #websocket; // WebWindow handle used for communication with server
   #changed_layout; // modified layout
   #submreq;  // submitted requests
   #nextreqid; // id of next request

   /** @summary constructor */
   constructor(dom, canvas, opt) {
      super(dom, canvas, opt, true);
      this.#websocket = null;
      this.#submreq = {};
      this.tooltip_allowed = settings.Tooltip;
      this.v7canvas = true;
   }

   /** @summary Cleanup canvas painter */
   cleanup() {
      this.#websocket = undefined;
      this.#submreq = {};

      if (this.#changed_layout)
         this.setLayoutKind('simple');
      this.#changed_layout = undefined;

      super.cleanup();
   }

   /** @summary Returns readonly flag */
   isReadonly() { return false; }

   /** @summary Returns canvas name */
   getCanvasName() {
      const title = this.getRootPad()?.fTitle;
      return (!title || !isStr(title)) ? 'rcanvas' : title.replace(/ /g, '_');
   }

   /** @summary Returns layout kind */
   getLayoutKind() {
      const origin = this.selectDom('origin'),
            layout = origin.empty() ? '' : origin.property('layout');
      return layout || 'simple';
   }

   /** @summary Set canvas layout kind */
   setLayoutKind(kind, main_selector) {
      const origin = this.selectDom('origin');
      if (!origin.empty()) {
         if (!kind)
            kind = 'simple';
         origin.property('layout', kind);
         origin.property('layout_selector', (kind !== 'simple') && main_selector ? main_selector : null);
         this.#changed_layout = (kind !== 'simple'); // use in cleanup
      }
   }

   /** @summary Changes layout
     * @return {Promise} indicating when finished */
   async changeLayout(layout_kind, mainid) {
      const current = this.getLayoutKind();
      if (current === layout_kind)
         return true;

      const origin = this.selectDom('origin'),
            sidebar2 = origin.select('.side_panel2'),
            lst = [];
      let sidebar = origin.select('.side_panel'),
          main = this.selectDom(), force;

      while (main.node().firstChild)
         lst.push(main.node().removeChild(main.node().firstChild));

      if (!sidebar.empty())
         cleanup(sidebar.node());
      if (!sidebar2.empty())
         cleanup(sidebar2.node());

      this.setLayoutKind('simple'); // restore defaults
      origin.html(''); // cleanup origin

      if (layout_kind === 'simple') {
         main = origin;
         for (let k = 0; k < lst.length; ++k)
            main.node().appendChild(lst[k]);
         this.setLayoutKind(layout_kind);
         force = true;
      } else {
         const grid = new GridDisplay(origin.node(), layout_kind);

         if (mainid === undefined)
            mainid = (layout_kind.indexOf('vert') === 0) ? 0 : 1;

         main = select(grid.getGridFrame(mainid));
         main.classed('central_panel', true).style('position', 'relative');

         if (mainid === 2) {
            // left panel for Y
            sidebar = select(grid.getGridFrame(0));
            sidebar.classed('side_panel2', true).style('position', 'relative');
            // bottom panel for X
            sidebar = select(grid.getGridFrame(3));
            sidebar.classed('side_panel', true).style('position', 'relative');
         } else {
            sidebar = select(grid.getGridFrame(1 - mainid));
            sidebar.classed('side_panel', true).style('position', 'relative');
         }

         // now append all childs to the new main
         for (let k = 0; k < lst.length; ++k)
            main.node().appendChild(lst[k]);

         this.setLayoutKind(layout_kind, '.central_panel');

         // remove reference to MDIDisplay, solves resize problem
         origin.property('mdi', null);
      }

      // resize main drawing and let draw extras
      resize(main.node(), force);
      return true;
   }

   /** @summary Toggle projection
     * @return {Promise} indicating when ready
     * @private */
   async toggleProjection(kind) {
      delete this.proj_painter;

      if (kind)
         this.proj_painter = { X: false, Y: false }; // just indicator that drawing can be preformed

      if (isFunc(this.showUI5ProjectionArea))
         return this.showUI5ProjectionArea(kind);

      let layout = 'simple', mainid;

      switch (kind) {
         case 'XY':
            layout = 'projxy';
            mainid = 2;
            break;
         case 'X':
         case 'bottom':
            layout = 'vert2_31';
            mainid = 0;
            break;
         case 'Y':
         case 'left':
            layout = 'horiz2_13';
            mainid = 1;
            break;
         case 'top':
            layout = 'vert2_13';
            mainid = 1;
            break;
         case 'right':
            layout = 'horiz2_31';
            mainid = 0;
            break;
      }

      return this.changeLayout(layout, mainid);
   }

   /** @summary Draw projection for specified histogram
     * @private */
   async drawProjection(/* kind, hist, hopt */) {
      // dummy for the moment
      return false;
   }

   /** @summary Draw in side panel
     * @private */
   async drawInSidePanel(canv, opt, kind) {
      const sel = ((this.getLayoutKind() === 'projxy') && (kind === 'Y')) ? '.side_panel2' : '.side_panel',
            side = this.selectDom('origin').select(sel);
      return side.empty() ? null : this.drawObject(side.node(), canv, opt);
   }

   /** @summary Checks if canvas shown inside ui5 widget
     * @desc Function should be used only from the func which supposed to be replaced by ui5
     * @private */
   testUI5() {
      return this.use_openui ?? false;
   }

   /** @summary Show message
     * @desc Used normally with web-based canvas and handled in ui5
     * @private */
   showMessage(msg) {
      if (!this.testUI5())
         showProgress(msg, 7000);
   }

   /** @summary Function called when canvas menu item Save is called */
   saveCanvasAsFile(fname) {
      const pnt = fname.indexOf('.');
      this.createImage(fname.slice(pnt + 1))
          .then(res => this.sendWebsocket(`SAVE:${fname}:${res}`));
   }

   /** @summary Send command to server to save canvas with specified name
     * @desc Should be only used in web-based canvas
     * @private */
   sendSaveCommand(fname) { this.sendWebsocket('PRODUCE:' + fname); }

   /** @summary Return assigned web socket
    * @private */
   getWebsocket() { return this.#websocket; }

   /** @summary Return true if message can be send via web socket
    * @private */
   canSendWebsocket(noper = 1) { return this.#websocket?.canSend(noper); }

   /** @summary Send message via web socket
     * @private */
   sendWebsocket(msg) {
      if (this.#websocket?.canSend()) {
         this.#websocket.send(msg);
         return true;
      }

      return false;
   }

   /** @summary Close websocket connection to canvas
     * @private */
   closeWebsocket(force) {
      if (this.#websocket) {
         this.#websocket.close(force);
         this.#websocket.cleanup();
         this.#websocket = undefined;
      }
   }

   /** @summary Use provided connection for the web canvas
     * @private */
   useWebsocket(handle) {
      this.closeWebsocket();

      this.#websocket = handle;
      this.#websocket.setReceiver(this);
      this.#websocket.connect();
   }

   /** @summary set, test or reset timeout of specified name
     * @desc Used to prevent overloading of websocket for specific function */
   websocketTimeout(name, tm) {
      if (!this.#websocket)
         return;
      if (!this.#websocket._tmouts)
         this.#websocket._tmouts = {};

      const handle = this.#websocket._tmouts[name];
      if (tm === undefined)
         return handle !== undefined;

      if (tm === 'reset') {
         if (handle) {
            clearTimeout(handle);
            delete this.#websocket._tmouts[name];
         }
      } else if (!handle && Number.isInteger(tm))
         this.#websocket._tmouts[name] = setTimeout(() => { delete this.#websocket._tmouts[name]; }, tm);
   }

   /** @summary Handler for websocket open event
     * @private */
   onWebsocketOpened(/* handle */) {
   }

   /** @summary Handler for websocket close event
     * @private */
   onWebsocketClosed(/* handle */) {
      if (!this.embed_canvas)
         closeCurrentWindow();
   }

   /** @summary Handler for websocket message
     * @private */
   onWebsocketMsg(handle, msg) {
      // console.log('GET_MSG ' + msg.slice(0,30));

      if (msg === 'CLOSE') {
         this.onWebsocketClosed();
         this.closeWebsocket(true);
      } else if (msg.slice(0, 5) === 'SNAP:') {
         msg = msg.slice(5);
         const p1 = msg.indexOf(':'),
               snapid = msg.slice(0, p1),
               snap = parse$1(msg.slice(p1 + 1));
         this.syncDraw(true)
             .then(() => {
                if (!this.getSnapId() && snap?.fWinSize)
                   this.resizeBrowser(snap.fWinSize[0], snap.fWinSize[1]);
             }).then(() => this.redrawPadSnap(snap))
             .then(() => {
                this.addPadInteractive();
                handle.send(`SNAPDONE:${snapid}`); // send ready message back when drawing completed
                this.confirmDraw();
             }).catch(err => {
                if (isFunc(this.showConsoleError))
                   this.showConsoleError(err);
                else
                   console.log(err);
             });
      } else if (msg.slice(0, 4) === 'JSON') {
         const obj = parse$1(msg.slice(4));
         this.redrawObject(obj);
      } else if (msg.slice(0, 9) === 'REPL_REQ:')
         this.processDrawableReply(msg.slice(9));
      else if (msg.slice(0, 4) === 'CMD:') {
         msg = msg.slice(4);
         const p1 = msg.indexOf(':'),
               cmdid = msg.slice(0, p1),
               cmd = msg.slice(p1 + 1),
               reply = `REPLY:${cmdid}:`;
         if ((cmd === 'SVG') || (cmd === 'PNG') || (cmd === 'JPEG') || (cmd === 'WEBP') || (cmd === 'PDF')) {
            this.createImage(cmd.toLowerCase())
                .then(res => handle.send(reply + res));
         } else if (cmd.indexOf('ADDPANEL:') === 0) {
            if (!isFunc(this.showUI5Panel))
               handle.send(reply + 'false');
            else {
               const window_path = cmd.slice(9),
                     conn = handle.createNewInstance(window_path);

               // set interim receiver until first message arrives
               conn.setReceiver({
                  cpainter: this,

                  onWebsocketOpened() {
                  },

                  onWebsocketMsg(panel_handle, msg2) {
                     const panel_name = (msg2.indexOf('SHOWPANEL:') === 0) ? msg2.slice(10) : '';
                     this.cpainter.showUI5Panel(panel_name, panel_handle)
                                  .then(res => handle.send(reply + (res ? 'true' : 'false')));
                  },

                  onWebsocketClosed() {
                     // if connection failed,
                     handle.send(reply + 'false');
                  },

                  onWebsocketError() {
                     // if connection failed,
                     handle.send(reply + 'false');
                  }

               });

               // only when connection established, panel will be activated
               conn.connect();
            }
         } else {
            console.log('Unrecognized command ' + cmd);
            handle.send(reply);
         }
      } else if ((msg.slice(0, 7) === 'DXPROJ:') || (msg.slice(0, 7) === 'DYPROJ:')) {
         const kind = msg[1],
               hist = parse$1(msg.slice(7));
         this.drawProjection(kind, hist);
      } else if (msg.slice(0, 5) === 'SHOW:') {
         const that = msg.slice(5),
               on = that.at(-1) === '1';
         this.showSection(that.slice(0, that.length - 2), on);
      } else
         console.log(`unrecognized msg len: ${msg.length} msg: ${msg.slice(0, 30)}`);
   }

   /** @summary Submit request to RDrawable object on server side */
   submitDrawableRequest(kind, req, painter, method) {
      if (!this.getWebsocket() || !req?._typename || !painter.getSnapId())
         return null;

      if (kind && method) {
         // if kind specified - check if such request already was submitted
         if (!painter._requests)
            painter._requests = {};

         const prevreq = painter._requests[kind];

         if (prevreq) {
            const tm = new Date().getTime();
            if (!prevreq._tm || (tm - prevreq._tm < 5000)) {
               prevreq._nextreq = req; // submit when got reply
               return false;
            }
            delete painter._requests[kind]; // let submit new request after timeout
         }

         painter._requests[kind] = req; // keep reference on the request
      }

      req.id = painter.getSnapId();

      if (method) {
         if (!this.#nextreqid)
            this.#nextreqid = 1;
         req.reqid = this.#nextreqid++;
      } else
         req.reqid = 0; // request will not be replied


      const msg = JSON.stringify(req);

      if (req.reqid) {
         req._kind = kind;
         req._painter = painter;
         req._method = method;
         req._tm = new Date().getTime();

         this.#submreq[req.reqid] = req; // fast access to submitted requests
      }

      this.sendWebsocket('REQ:' + msg);
      return req;
   }

   /** @summary Submit menu request
     * @private */
   async submitMenuRequest(painter, menukind, reqid) {
      return new Promise(resolveFunc => {
         this.submitDrawableRequest('', {
            _typename: `${nsREX}RDrawableMenuRequest`,
            menukind: menukind || '',
            menureqid: reqid // used to identify menu request
         }, painter, resolveFunc);
      });
   }

   /** @summary Submit executable command for given painter */
   submitExec(painter, exec, subelem) {
      if (subelem && isStr(subelem)) {
         const len = subelem.length;
         if ((len > 2) && (subelem.indexOf('#x') === len - 2))
            subelem = 'x';
         else if ((len > 2) && (subelem.indexOf('#y') === len - 2))
            subelem = 'y';
         else if ((len > 2) && (subelem.indexOf('#z') === len - 2))
            subelem = 'z';

         if ((subelem === 'x') || (subelem === 'y') || (subelem === 'z'))
            exec = subelem + 'axis#' + exec;
         else
            return console.log(`not recoginzed subelem ${subelem} in submitExec`);
      }

      this.submitDrawableRequest('', { _typename: `${nsREX}RDrawableExecRequest`, exec }, painter);
   }

   /** @summary Process reply from request to RDrawable */
   processDrawableReply(msg) {
      const reply = parse$1(msg);
      if (!reply?.reqid)
         return false;

      const req = this.#submreq[reply.reqid];
      if (!req)
         return false;

      // remove reference first
      this.#submreq[reply.reqid] = undefined;

      // remove blocking reference for that kind
      if (req._kind && req._painter?._requests) {
         if (req._painter._requests[req._kind] === req)
            delete req._painter._requests[req._kind];
      }

      if (req._method)
         req._method(reply, req);

      // resubmit last request of that kind
      if (req._nextreq && !req._painter._requests[req._kind])
         this.submitDrawableRequest(req._kind, req._nextreq, req._painter, req._method);
   }

   /** @summary Show specified section in canvas */
   async showSection(that, on) {
      switch (that) {
         case 'Menu': break;
         case 'StatusBar': break;
         case 'Editor': break;
         case 'ToolBar': break;
         case 'ToolTips': this.setTooltipAllowed(on); break;
      }
      return true;
   }

   /** @summary Method informs that something was changed in the canvas
     * @desc used to update information on the server (when used with web6gui)
     * @private */
   processChanges(kind, painter, subelem) {
      // check if we could send at least one message more - for some meaningful actions
      if (!this.canSendWebsocket(2) || !isStr(kind))
         return;
      if (!painter)
         painter = this;
      switch (kind) {
         case 'sbits':
            console.log('Status bits in RCanvas are changed - that to do?');
            break;
         case 'frame': // when moving frame
         case 'zoom':  // when changing zoom inside frame
            console.log('Frame moved or zoom is changed - that to do?');
            break;
         case 'pave_moved':
            console.log('TPave is moved inside RCanvas - that to do?');
            break;
         default:
            if ((kind.slice(0, 5) === 'exec:') && painter?.getSnapId())
               this.submitExec(painter, kind.slice(5), subelem);
            else
               console.log('UNPROCESSED CHANGES', kind);
      }
   }

   /** @summary Handle pad button click event
     * @private */
   clickPadButton(funcname, evnt) {
      if (funcname === 'ToggleGed')
         return this.activateGed(this, null, 'toggle');
      if (funcname === 'ToggleStatus')
         return this.activateStatusBar('toggle');
      return super.clickPadButton(funcname, evnt);
   }

   /** @summary returns true when event status area exist for the canvas */
   hasEventStatus() {
      if (this.testUI5())
         return false;
      if (this.brlayout)
         return this.brlayout.hasStatus();
      const hp = getHPainter();
      return hp ? hp.hasStatusLine() : false;
   }

   /** @summary Check if status bar can be toggled
     * @private */
   canStatusBar() {
      return this.testUI5() || this.brlayout || getHPainter();
   }

   /** @summary Show/toggle event status bar
     * @private */
   activateStatusBar(state) {
      if (this.testUI5())
         return;
      if (this.brlayout)
         this.brlayout.createStatusLine(23, state);
      else
         getHPainter()?.createStatusLine(23, state);

      this.processChanges('sbits', this);
   }

   /** @summary Show online canvas status
     * @private */
   showCanvasStatus(...msgs) {
      if (this.testUI5())
         return;

      const br = this.brlayout || getHPainter()?.brlayout;
      br?.showStatus(...msgs);
   }

   /** @summary Returns true if GED is present on the canvas */
   hasGed() {
      if (this.testUI5())
         return false;
      return this.brlayout?.hasContent() ?? false;
   }

   /** @summary Function used to de-activate GED
     * @private */
   removeGed() {
      if (this.testUI5())
         return;

      this.registerForPadEvents(null);

      if (this.ged_view) {
         this.ged_view.getController().cleanupGed();
         this.ged_view.destroy();
         delete this.ged_view;
      }
      this.brlayout?.deleteContent(true);
      this.processChanges('sbits', this);
   }

   /** @summary Get view data for ui5 panel
     * @private */
   getUi5PanelData(/* panel_name */) {
      return { jsroot: { settings, create: create$1, parse: parse$1, toJSON, loadScript, EAxisBits, getColorExec } };
   }

   /** @summary Function used to activate GED
     * @return {Promise} when GED is there
     * @private */
   async activateGed(objpainter, kind, mode) {
      if (this.testUI5() || !this.brlayout)
         return false;

      if (this.brlayout.hasContent()) {
         if ((mode === 'toggle') || (mode === false))
            this.removeGed();
         else
            objpainter?.getPadPainter()?.selectObjectPainter(objpainter);

         return true;
      }

      if (mode === false)
         return false;

      const btns = this.brlayout.createBrowserBtns();

      ToolbarIcons.createSVG(btns, ToolbarIcons.diamand, 15, 'toggle fix-pos mode', 'browser')
                  .style('margin', '3px').on('click', () => this.brlayout.toggleKind('fix'));

      ToolbarIcons.createSVG(btns, ToolbarIcons.circle, 15, 'toggle float mode', 'browser')
                  .style('margin', '3px').on('click', () => this.brlayout.toggleKind('float'));

      ToolbarIcons.createSVG(btns, ToolbarIcons.cross, 15, 'delete GED', 'browser')
                  .style('margin', '3px').on('click', () => this.removeGed());

      // be aware, that jsroot_browser_hierarchy required for flexible layout that element use full browser area
      this.brlayout.setBrowserContent('<div class=\'jsroot_browser_hierarchy\' id=\'ged_placeholder\'>Loading GED ...</div>');
      this.brlayout.setBrowserTitle('GED');
      this.brlayout.toggleBrowserKind(kind || 'float');

      return new Promise(resolveFunc => {
         loadOpenui5.then(sap => {
            select('#ged_placeholder').text('');

            sap.ui.require(['sap/ui/model/json/JSONModel', 'sap/ui/core/mvc/XMLView'], (JSONModel, XMLView) => {
               const oModel = new JSONModel({ handle: null });

               XMLView.create({
                  viewName: 'rootui5.canv.view.Ged',
                  viewData: this.getUi5PanelData('Ged')
               }).then(oGed => {
                  oGed.setModel(oModel);

                  oGed.placeAt('ged_placeholder');

                  this.ged_view = oGed;

                  // TODO: should be moved into Ged controller - it must be able to detect canvas painter itself
                  this.registerForPadEvents(oGed.getController().padEventsReceiver.bind(oGed.getController()));

                  objpainter?.getPadPainter()?.selectObjectPainter(objpainter);

                  this.processChanges('sbits', this);

                  resolveFunc(true);
               });
            });
         });
      });
   }

   /** @summary produce JSON for RCanvas, which can be used to display canvas once again
     * @private */
   produceJSON() {
      console.error('RCanvasPainter.produceJSON not yet implemented');
      return '';
   }

   /** @summary resize browser window to get requested canvas sizes */
   resizeBrowser(fullW, fullH) {
      if (!fullW || !fullH || this.isBatchMode() || this.embed_canvas || this.batch_mode)
         return;
      this.getWebsocket()?.resizeWindow(fullW, fullH);
   }

   /** @summary draw RCanvas object */
   static async draw(dom, can, opt) {
      const nocanvas = !can;
      if (nocanvas)
         can = create$1(`${nsREX}RCanvas`);

      const painter = new RCanvasPainter(dom, can, opt);
      painter.createCanvasSvg(0);

      selectActivePad({ pp: painter, active: false });

      return painter.drawPrimitives().then(() => {
         painter.addPadInteractive();
         painter.addPadButtons();
         painter.showPadButtons();
         return painter;
      });
   }

} // class RCanvasPainter


/** @summary draw RPadSnapshot object
  * @private */
function drawRPadSnapshot(dom, snap, opt) {
   const painter = new RCanvasPainter(dom, null, opt);
   painter.batch_mode = isBatchMode();
   return painter.syncDraw(true).then(() => painter.redrawPadSnap(snap)).then(() => {
      painter.confirmDraw();
      painter.showPadButtons();
      return painter;
   });
}

/** @summary Ensure RCanvas and RFrame for the painter object
  * @param {Object} painter  - painter object to process
  * @param {string|boolean} frame_kind  - false for no frame or '3d' for special 3D mode
  * @desc Assigns DOM, creates and draw RCanvas and RFrame if necessary, add painter to pad list of painters
  * @return {Promise} for ready
  * @private */
async function ensureRCanvas(painter /* , frame_kind */) {
   if (!painter)
      return Promise.reject(Error('Painter not provided in ensureRCanvas'));

   // simple check - if canvas there, can use painter
   const pad_painter = painter.getPadPainter(),
         pr = pad_painter ? Promise.resolve(pad_painter) :
              RCanvasPainter.draw(painter.getDom(), null /* noframe */);

   return pr.then(pp => {
      // if ((frame_kind !== false) && pp.getFrameSvg().selectChild('.main_layer').empty())
      //   return RFramePainter.draw(painter.getDom(), null, isStr(frame_kind) ? frame_kind : '');
      painter.addToPadPrimitives(pp);
      return painter;
   });
}


/** @summary Function used for direct draw of RFrameTitle
  * @private */
function drawRFrameTitle(reason, drag) {
   const fp = this.getFramePainter();
   if (!fp)
      return console.log('no frame painter - no title');

   const rect = fp.getFrameRect(),
         fx = rect.x,
         fy = rect.y,
         fw = rect.width,
         // fh           = rect.height,
         ph = this.getPadPainter().getPadHeight(),
         title = this.getObject(),
         title_width = fw,
         textFont = this.v7EvalFont('text', { size: 0.07, color: 'black', align: 22 });
   let title_margin = this.v7EvalLength('margin', ph, 0.02),
       title_height = this.v7EvalLength('height', ph, 0.05);

   if (reason === 'drag') {
      title_height = drag.height;
      title_margin = fy - drag.y - drag.height;
      const changes = {};
      this.v7AttrChange(changes, 'margin', title_margin / ph);
      this.v7AttrChange(changes, 'height', title_height / ph);
      this.v7SendAttrChanges(changes, false); // do not invoke canvas update on the server
   }

   const g = this.createG();

   makeTranslate(g, fx, Math.round(fy - title_margin - title_height));

   return this.startTextDrawingAsync(textFont, 'font').then(() => {
      this.drawText({ x: title_width / 2, y: title_height / 2, text: title.fText, latex: 1 });
      return this.finishTextDrawing();
   }).then(() => {
      addDragHandler(this, {
         x: fx, y: Math.round(fy - title_margin - title_height), width: title_width, height: title_height,
         minwidth: 20, minheight: 20, no_change_x: true, redraw: d => this.redraw('drag', d)
      });
   });
}

// ==========================================================

registerMethods(`${nsREX}RPalette`, {

   extractRColor(rcolor) {
      const col = rcolor.fColor || 'black';
      return convertColor(col);
   },

   getColor(indx) {
      return this.palette[indx];
   },

   getContourIndex(zc) {
      const cntr = this.fContour;
      let l = 0, r = cntr.length - 1;

      if (zc < cntr[0])
         return -1;
      if (zc >= cntr[r])
         return r - 1;

      if (this.fCustomContour) {
         while (l < r - 1) {
            const mid = Math.round((l + r) / 2);
            if (cntr[mid] > zc)
               r = mid;
            else
               l = mid;
         }
         return l;
      }

      // last color in palette starts from level cntr[r-1]
      return Math.floor((zc - cntr[0]) / (cntr[r - 1] - cntr[0]) * (r - 1));
   },

   getContourColor(zc) {
      const zindx = this.getContourIndex(zc);
      return (zindx < 0) ? '' : this.getColor(zindx);
   },

   getContour() {
      return this.fContour && (this.fContour.length > 1) ? this.fContour : null;
   },

   deleteContour() {
      this.fContour = undefined;
   },

   calcColor(value, entry1, entry2) {
      const dist = entry2.fOrdinal - entry1.fOrdinal,
            r1 = entry2.fOrdinal - value,
            r2 = value - entry1.fOrdinal;

      if (!this.fInterpolate || (dist <= 0))
         return convertColor((r1 < r2) ? entry2.fColor : entry1.fColor);

      // interpolate
      const col1 = rgb(this.extractRColor(entry1.fColor)),
            col2 = rgb(this.extractRColor(entry2.fColor)),
            color = rgb(Math.round((col1.r * r1 + col2.r * r2) / dist),
                           Math.round((col1.g * r1 + col2.g * r2) / dist),
                           Math.round((col1.b * r1 + col2.b * r2) / dist));

      return color.formatRgb();
   },

   createPaletteColors(len) {
      const arr = [];
      let indx = 0;

      while (arr.length < len) {
         const value = arr.length / (len - 1),
               entry = this.fColors[indx];

         if ((Math.abs(entry.fOrdinal - value) < 0.0001) || (indx === this.fColors.length - 1)) {
            arr.push(this.extractRColor(entry.fColor));
            continue;
         }

         const next = this.fColors[indx + 1];
         if (next.fOrdinal <= value)
            indx++;
         else
            arr.push(this.calcColor(value, entry, next));
      }

      return arr;
   },

   getColorOrdinal(value) {
      // extract color with ordinal value between 0 and 1
      if (!this.fColors)
         return 'black';
      if ((typeof value !== 'number') || (value < 0))
         value = 0;
      else if (value > 1)
         value = 1;

      // TODO: implement better way to find index

      let entry, next = this.fColors[0];
      for (let indx = 0; indx < this.fColors.length - 1; ++indx) {
         entry = next;

         if (Math.abs(entry.fOrdinal - value) < 0.0001)
            return this.extractRColor(entry.fColor);

         next = this.fColors[indx + 1];
         if (next.fOrdinal > value)
            return this.calcColor(value, entry, next);
      }

      return this.extractRColor(next.fColor);
   },

   setFullRange(min, max) {
      // set full z scale range, used in zooming
      this.full_min = min;
      this.full_max = max;
   },

   createContour(logz, nlevels, zmin, zmax, zminpositive) {
      this.fContour = [];
      delete this.fCustomContour;
      this.colzmin = zmin;
      this.colzmax = zmax;

      if (logz) {
         if (this.colzmax <= 0)
            this.colzmax = 1.0;
         if (this.colzmin <= 0) {
            if ((zminpositive === undefined) || (zminpositive <= 0))
               this.colzmin = 0.0001 * this.colzmax;
            else
               this.colzmin = ((zminpositive < 3) || (zminpositive > 100)) ? 0.3 * zminpositive : 1;
         }
         if (this.colzmin >= this.colzmax)
            this.colzmin = 0.0001 * this.colzmax;

         const logmin = Math.log(this.colzmin) / Math.log(10),
               logmax = Math.log(this.colzmax) / Math.log(10),
               dz = (logmax - logmin) / nlevels;
         this.fContour.push(this.colzmin);
         for (let level = 1; level < nlevels; level++)
            this.fContour.push(Math.exp((logmin + dz * level) * Math.log(10)));
         this.fContour.push(this.colzmax);
         this.fCustomContour = true;
      } else {
         if ((this.colzmin === this.colzmax) && this.colzmin) {
            this.colzmax += 0.01 * Math.abs(this.colzmax);
            this.colzmin -= 0.01 * Math.abs(this.colzmin);
         }
         const dz = (this.colzmax - this.colzmin) / nlevels;
         for (let level = 0; level <= nlevels; level++)
            this.fContour.push(this.colzmin + dz * level);
      }

      if (!this.palette || (this.palette.length !== nlevels))
         this.palette = this.createPaletteColors(nlevels);
   }

});

/** @summary draw RFont object
  * @private */
function drawRFont() {
   const font = this.getObject(),
         svg = this.getCanvSvg(),
         clname = 'custom_font_' + font.fFamily + font.fWeight + font.fStyle;
   let defs = svg.selectChild('.canvas_defs');

   if (defs.empty())
      defs = svg.insert('svg:defs', ':first-child').attr('class', 'canvas_defs');

   let entry = defs.selectChild('.' + clname);
   if (entry.empty()) {
      entry = defs.append('style')
                  .attr('type', 'text/css')
                  .attr('class', clname)
                  .text(`@font-face { font-family: "${font.fFamily}"; font-weight: ${font.fWeight ? font.fWeight : 'normal'}; font-style: ${font.fStyle ? font.fStyle : 'normal'}; src: ${font.fSrc}; }`);
      const p1 = font.fSrc.indexOf('base64,'),
            p2 = font.fSrc.lastIndexOf(' format(');
      if (p1 > 0 && p2 > p1) {
         const base64 = font.fSrc.slice(p1 + 7, p2 - 2),
               is_ttf = font.fSrc.indexOf('data:application/font-ttf') > 0;
         // TODO: for the moment only ttf format supported by jsPDF
         if (is_ttf)
            entry.property('$fontcfg', { n: font.fFamily, base64 });
      }
   }

   if (font.fDefault)
      this.getPadPainter()._dfltRFont = font;

   return true;
}

/** @summary draw RAxis object
  * @private */
function drawRAxis(dom, obj, opt) {
   const painter = new RAxisPainter(dom, obj, opt);
   painter.disable_zooming = true;
   return ensureRCanvas(painter)
           .then(() => painter.redraw())
           .then(() => painter);
}

/** @summary draw RFrame object
  * @private */
function drawRFrame(dom, obj, opt) {
   const p = new RFramePainter(dom, obj);
   if (opt === '3d')
      p.mode3d = true;
   return ensureRCanvas(p).then(() => p.redraw());
}

var RCanvasPainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
RCanvasPainter: RCanvasPainter,
RObjectPainter: RObjectPainter,
RPadPainter: RPadPainter,
drawRAxis: drawRAxis,
drawRFont: drawRFont,
drawRFrame: drawRFrame,
drawRFrameTitle: drawRFrameTitle,
drawRPadSnapshot: drawRPadSnapshot,
ensureRCanvas: ensureRCanvas
});

/** @summary draw RText object
  * @private */
function drawText() {
   const text = this.getObject(),
         pp = this.getPadPainter(),
         onframe = this.v7EvalAttr('onFrame', false) ? pp.getFramePainter() : null,
         clipping = onframe ? this.v7EvalAttr('clipping', false) : false,
         p = pp.getCoordinate(text.fPos, onframe),
         textFont = this.v7EvalFont('text', { size: 12, color: 'black', align: 22 });

   this.createG(clipping ? 'main_layer' : (onframe ? 'upper_layer' : false));

   return this.startTextDrawingAsync(textFont, 'font').then(() => {
      this.drawText({ x: p.x, y: p.y, text: text.fText, latex: 1 });
      return this.finishTextDrawing();
   });
}

/** @summary draw RLine object
  * @private */
function drawLine() {
   const line = this.getObject(),
         pp = this.getPadPainter(),
         onframe = this.v7EvalAttr('onFrame', false) ? pp.getFramePainter() : null,
         clipping = onframe ? this.v7EvalAttr('clipping', false) : false,
         p1 = pp.getCoordinate(line.fP1, onframe),
         p2 = pp.getCoordinate(line.fP2, onframe),
         g = this.createG(clipping ? 'main_layer' : (onframe ? 'upper_layer' : false));

   this.createv7AttLine();

   g.append('svg:path')
    .attr('d', `M${p1.x},${p1.y}L${p2.x},${p2.y}`)
    .call(this.lineatt.func);
}

/** @summary draw RBox object
  * @private */
function drawBox() {
   const box = this.getObject(),
         pp = this.getPadPainter(),
         onframe = this.v7EvalAttr('onFrame', false) ? pp.getFramePainter() : null,
         clipping = onframe ? this.v7EvalAttr('clipping', false) : false,
         p1 = pp.getCoordinate(box.fP1, onframe),
         p2 = pp.getCoordinate(box.fP2, onframe),
         g = this.createG(clipping ? 'main_layer' : (onframe ? 'upper_layer' : false));

   this.createv7AttLine('border_');

   this.createv7AttFill();

   g.append('svg:path')
    .attr('d', `M${p1.x},${p1.y}H${p2.x}V${p2.y}H${p1.x}Z`)
    .call(this.lineatt.func)
    .call(this.fillatt.func);
}

/** @summary draw RMarker object
  * @private */
function drawMarker() {
   const marker = this.getObject(),
         pp = this.getPadPainter(),
         onframe = this.v7EvalAttr('onFrame', false) ? pp.getFramePainter() : null,
         clipping = onframe ? this.v7EvalAttr('clipping', false) : false,
         p = pp.getCoordinate(marker.fP, onframe),
         g = this.createG(clipping ? 'main_layer' : (onframe ? 'upper_layer' : false));

   this.createv7AttMarker();

   const path = this.markeratt.create(p.x, p.y);

   if (path) {
      g.append('svg:path')
       .attr('d', path)
       .call(this.markeratt.func);
   }
}

/** @summary painter for RPalette
 *
 * @private
 */

class RPalettePainter extends RObjectPainter {

   /** @summary get palette */
   getHistPalette() {
      const pal = this.getObject()?.fPalette;

      if (pal && !isFunc(pal.getColor))
         exports.addMethods(pal, `${nsREX}RPalette`);

      return pal;
   }

   /** @summary Draw palette */
   drawPalette(drag) {
      const palette = this.getHistPalette(),
            contour = palette.getContour(),
            fp = this.getFramePainter();

      if (!contour)
         return console.log('no contour - no palette');

      // frame painter must  be there
      if (!fp)
         return console.log('no frame painter - no palette');

      const zmin = contour.at(0),
            zmax = contour.at(-1),
            rect = fp.getFrameRect(),
            pad_width = this.getPadPainter().getPadWidth(),
            pad_height = this.getPadPainter().getPadHeight(),
            visible = this.v7EvalAttr('visible', true),
            vertical = this.v7EvalAttr('vertical', true),
            g = this.getG();
      let gmin = palette.full_min,
          gmax = palette.full_max,
          palette_x, palette_y, palette_width, palette_height;

      if (drag) {
         palette_width = drag.width;
         palette_height = drag.height;

         const changes = {};
         if (vertical) {
            this.v7AttrChange(changes, 'margin', (drag.x - rect.x - rect.width) / pad_width);
            this.v7AttrChange(changes, 'width', palette_width / pad_width);
         } else {
            this.v7AttrChange(changes, 'margin', (drag.y - rect.y - rect.height) / pad_width);
            this.v7AttrChange(changes, 'width', palette_height / pad_height);
         }
         this.v7SendAttrChanges(changes, false); // do not invoke canvas update on the server
      } else {
         if (vertical) {
            const margin = this.v7EvalLength('margin', pad_width, 0.02);
            palette_x = Math.round(rect.x + rect.width + margin);
            palette_width = this.v7EvalLength('width', pad_width, 0.05);
            palette_y = rect.y;
            palette_height = rect.height;
         } else {
            const margin = this.v7EvalLength('margin', pad_height, 0.02);
            palette_x = rect.x;
            palette_width = rect.width;
            palette_y = Math.round(rect.y + rect.height + margin);
            palette_height = this.v7EvalLength('width', pad_height, 0.05);
         }

         // x,y,width,height attributes used for drag functionality
         makeTranslate(g, palette_x, palette_y);
      }

      let g_btns = g.selectChild('.colbtns');
      if (g_btns.empty())
         g_btns = g.append('svg:g').attr('class', 'colbtns');
      else
         g_btns.selectAll('*').remove();

      if (!visible)
         return;

      g_btns.append('svg:path')
          .attr('d', `M0,0H${palette_width}V${palette_height}H0Z`)
          .style('stroke', 'black')
          .style('fill', 'none');

      if ((gmin === undefined) || (gmax === undefined)) {
         gmin = zmin;
         gmax = zmax;
      }

      if (vertical)
         fp.z_handle.configureAxis('zaxis', gmin, gmax, zmin, zmax, true, [palette_height, 0], -palette_height, { reverse: false });
      else
         fp.z_handle.configureAxis('zaxis', gmin, gmax, zmin, zmax, false, [0, palette_width], palette_width, { reverse: false });

      for (let i = 0; i < contour.length - 1; ++i) {
         const z0 = Math.round(fp.z_handle.gr(contour[i])),
               z1 = Math.round(fp.z_handle.gr(contour[i + 1])),
               col = palette.getContourColor((contour[i] + contour[i + 1]) / 2),
               r = g_btns.append('svg:path')
                         .attr('d', vertical ? `M0,${z1}H${palette_width}V${z0}H0Z` : `M${z0},0V${palette_height}H${z1}V0Z`)
                         .style('fill', col)
                         .style('stroke', col)
                         .property('fill0', col)
                         .property('fill1', rgb(col).darker(0.5).formatRgb());

         if (this.isBatchMode())
            continue;

         if (this.isTooltipAllowed()) {
            r.on('mouseover', function() {
               select(this).transition().duration(100).style('fill', select(this).property('fill1'));
            }).on('mouseout', function() {
               select(this).transition().duration(100).style('fill', select(this).property('fill0'));
            }).append('svg:title').text(contour[i].toFixed(2) + ' - ' + contour[i + 1].toFixed(2));
         }

         if (settings.Zooming)
            r.on('dblclick', () => fp.unzoom('z'));
      }

      fp.z_handle.maxTickSize = Math.round(palette_width * 0.3);

      const promise = fp.z_handle.drawAxis(g, makeTranslate(vertical ? palette_width : 0, palette_height), vertical ? -1 : 1);

      if (this.isBatchMode() || drag)
         return promise;

      return promise.then(() => {
         if (settings.ContextMenu) {
            g.on('contextmenu', evnt => {
               evnt.stopPropagation(); // disable main context menu
               evnt.preventDefault();  // disable browser context menu
               createMenu(evnt, this).then(menu => {
                  menu.header('Palette');
                  menu.addchk(vertical, 'Vertical', flag => {
                     this.v7SetAttr('vertical', flag);
                     this.redrawPad();
                  });
                  fp.z_handle.fillAxisContextMenu(menu, 'z');
                  menu.show();
               });
            });
         }

         addDragHandler(this, {
            x: palette_x, y: palette_y, width: palette_width, height: palette_height,
            minwidth: 20, minheight: 20, no_change_x: !vertical, no_change_y: vertical, redraw: d => this.drawPalette(d)
         });

         if (!settings.Zooming)
            return;

         let doing_zoom = false, sel1 = 0, sel2 = 0, zoom_rect, zoom_rect_visible, moving_labels, last_pos;

         const moveRectSel = evnt => {
            if (!doing_zoom)
               return;
            evnt.preventDefault();

            last_pos = pointer(evnt, this.getG().node());

            if (moving_labels)
               return fp.z_handle.processLabelsMove('move', last_pos);

            if (vertical)
               sel2 = Math.min(Math.max(last_pos[1], 0), palette_height);
            else
               sel2 = Math.min(Math.max(last_pos[0], 0), palette_width);

            const sz = Math.abs(sel2 - sel1);

            if (!zoom_rect_visible && (sz > 1)) {
               zoom_rect.style('display', null);
               zoom_rect_visible = true;
            }

            if (vertical)
               zoom_rect.attr('y', Math.min(sel1, sel2)).attr('height', sz);
            else
               zoom_rect.attr('x', Math.min(sel1, sel2)).attr('width', sz);
         }, endRectSel = evnt => {
            if (!doing_zoom)
               return;

            evnt.preventDefault();
            select(window).on('mousemove.colzoomRect', null)
                             .on('mouseup.colzoomRect', null);
            zoom_rect.remove();
            zoom_rect = null;
            doing_zoom = false;

            if (moving_labels)
               fp.z_handle.processLabelsMove('stop', last_pos);
            else {
               const z = fp.z_handle.func, z1 = z.invert(sel1), z2 = z.invert(sel2);
               fp.zoomSingle('z', Math.min(z1, z2), Math.max(z1, z2));
            }
         }, startRectSel = evnt => {
            // ignore when touch selection is activated
            if (doing_zoom)
               return;
            doing_zoom = true;

            evnt.preventDefault();
            evnt.stopPropagation();

            last_pos = pointer(evnt, this.getG().node());
            sel1 = sel2 = last_pos[vertical ? 1 : 0];
            zoom_rect_visible = false;
            moving_labels = false;
            zoom_rect = g_btns
                 .append('svg:rect')
                 .attr('class', 'zoom')
                 .attr('id', 'colzoomRect')
                 .style('display', 'none');
            if (vertical)
               zoom_rect.attr('x', 0).attr('width', palette_width).attr('y', sel1).attr('height', 1);
            else
               zoom_rect.attr('x', sel1).attr('width', 1).attr('y', 0).attr('height', palette_height);

            select(window).on('mousemove.colzoomRect', moveRectSel)
                             .on('mouseup.colzoomRect', endRectSel, true);

            setTimeout(() => {
               if (!zoom_rect_visible && doing_zoom)
                  moving_labels = fp.z_handle.processLabelsMove('start', last_pos);
            }, 500);
         }, assignHandlers = () => {
            this.getG().selectAll('.axis_zoom, .axis_labels')
                       .on('mousedown', startRectSel)
                       .on('dblclick', () => fp.unzoom('z'));

            if (settings.ZoomWheel) {
               this.getG().on('wheel', evnt => {
                  evnt.stopPropagation();
                  evnt.preventDefault();

                  const pos = pointer(evnt, this.getG().node()),
                        coord = vertical ? (1 - pos[1] / palette_height) : pos[0] / palette_width,

                        item = fp.z_handle.analyzeWheelEvent(evnt, coord);
                  if (item.changed)
                     fp.zoomSingle('z', item.min, item.max);
               });
            }
         };

         fp.z_handle.setAfterDrawHandler(assignHandlers);

         assignHandlers();
      });
   }

   /** @summary draw RPalette object */
   static async draw(dom, palette, opt) {
      const painter = new RPalettePainter(dom, palette, opt, 'palette');
      return ensureRCanvas(painter).then(() => {
         painter.createG(); // just create container, real drawing will be done by histogram
         return painter;
      });
   }

} // class RPalettePainter

var v7more = /*#__PURE__*/Object.freeze({
__proto__: null,
RPalettePainter: RPalettePainter,
drawBox: drawBox,
drawLine: drawLine,
drawMarker: drawMarker,
drawText: drawText
});

const ECorner = { kTopLeft: 1, kTopRight: 2, kBottomLeft: 3, kBottomRight: 4 };

/**
 * @summary Painter for RPave class
 *
 * @private
 */

class RPavePainter extends RObjectPainter {

   /** @summary Draw pave content
     * @desc assigned depending on pave class */
   async drawContent() { return this; }

   /** @summary Draw pave */
   async drawPave() {
      const rect = this.getPadPainter().getPadRect(),
            fp = this.getFramePainter();

      this.onFrame = fp && this.v7EvalAttr('onFrame', true);
      this.corner = this.v7EvalAttr('corner', ECorner.kTopRight);

      const visible = this.v7EvalAttr('visible', true),
            offsetx = this.v7EvalLength('offsetX', rect.width, 0.02),
            offsety = this.v7EvalLength('offsetY', rect.height, 0.02),
            pave_width = this.v7EvalLength('width', rect.width, 0.3),
            pave_height = this.v7EvalLength('height', rect.height, 0.3),
            g = this.createG();

      g.classed('most_upper_primitives', true); // this primitive will remain on top of list

      if (!visible)
         return this;

      this.createv7AttLine('border_');

      this.createv7AttFill();

      const fr = this.onFrame ? fp.getFrameRect() : rect;
      let pave_x = 0, pave_y = 0;
      switch (this.corner) {
         case ECorner.kTopLeft:
            pave_x = fr.x + offsetx;
            pave_y = fr.y + offsety;
            break;
         case ECorner.kBottomLeft:
            pave_x = fr.x + offsetx;
            pave_y = fr.y + fr.height - offsety - pave_height;
            break;
         case ECorner.kBottomRight:
            pave_x = fr.x + fr.width - offsetx - pave_width;
            pave_y = fr.y + fr.height - offsety - pave_height;
            break;
         case ECorner.kTopRight:
         default:
            pave_x = fr.x + fr.width - offsetx - pave_width;
            pave_y = fr.y + offsety;
      }

      makeTranslate(g, pave_x, pave_y);

      g.append('svg:rect')
       .attr('x', 0)
       .attr('width', pave_width)
       .attr('y', 0)
       .attr('height', pave_height)
       .call(this.lineatt.func)
       .call(this.fillatt.func);

      this.pave_width = pave_width;
      this.pave_height = pave_height;

      // here should be fill and draw of text

      return this.drawContent().then(() => {
         if (!this.isBatchMode()) {
            // TODO: provide pave context menu as in v6
            if (settings.ContextMenu && this.paveContextMenu)
               g.on('contextmenu', evnt => this.paveContextMenu(evnt));

            addDragHandler(this, {
               x: pave_x, y: pave_y, width: pave_width, height: pave_height,
               minwidth: 20, minheight: 20, redraw: d => this.sizeChanged(d)
            });
         }

         return this;
      });
   }

   /** @summary Process interactive moving of the stats box */
   sizeChanged(drag) {
      this.pave_width = drag.width;
      this.pave_height = drag.height;

      const pave_x = drag.x,
            pave_y = drag.y,
            rect = this.getPadPainter().getPadRect(),
            fr = this.onFrame ? this.getFramePainter().getFrameRect() : rect,
            changes = {};
      let offsetx, offsety;

      switch (this.corner) {
         case ECorner.kTopLeft:
            offsetx = pave_x - fr.x;
            offsety = pave_y - fr.y;
            break;
         case ECorner.kBottomLeft:
            offsetx = pave_x - fr.x;
            offsety = fr.y + fr.height - pave_y - this.pave_height;
            break;
         case ECorner.kBottomRight:
            offsetx = fr.x + fr.width - pave_x - this.pave_width;
            offsety = fr.y + fr.height - pave_y - this.pave_height;
            break;
         case ECorner.kTopRight:
         default:
            offsetx = fr.x + fr.width - pave_x - this.pave_width;
            offsety = pave_y - fr.y;
      }

      this.v7AttrChange(changes, 'offsetX', offsetx / rect.width);
      this.v7AttrChange(changes, 'offsetY', offsety / rect.height);
      this.v7AttrChange(changes, 'width', this.pave_width / rect.width);
      this.v7AttrChange(changes, 'height', this.pave_height / rect.height);
      this.v7SendAttrChanges(changes, false); // do not invoke canvas update on the server

      this.getG().selectChild('rect')
                 .attr('width', this.pave_width)
                 .attr('height', this.pave_height);

      this.drawContent();
   }

   /** @summary Redraw RPave object */
   async redraw(/* reason */) {
      return this.drawPave();
   }

   /** @summary draw RPave object */
   static async draw(dom, pave, opt) {
      const painter = new RPavePainter(dom, pave, opt, 'pave');
      return ensureRCanvas(painter).then(() => painter.drawPave());
   }

}


/**
 * @summary Painter for RLegend class
 *
 * @private
 */

class RLegendPainter extends RPavePainter {

   /** @summary draw RLegend content */
   async drawContent() {
      const legend = this.getObject(),
            textFont = this.v7EvalFont('text', { size: 12, color: 'black', align: 22 }),
            width = this.pave_width,
            height = this.pave_height,
            pp = this.getPadPainter();

      let nlines = legend.fEntries.length;
      if (legend.fTitle)
         nlines++;

      if (!nlines || !pp)
         return this;

      const stepy = height / nlines, margin_x = 0.02 * width;

      textFont.setSize(height / (nlines * 1.2));
      return this.startTextDrawingAsync(textFont, 'font').then(() => {
         let posy = 0;

         if (legend.fTitle) {
            this.drawText({ latex: 1, width: width - 2 * margin_x, height: stepy, x: margin_x, y: posy, text: legend.fTitle });
            posy += stepy;
         }

         for (let i = 0; i < legend.fEntries.length; ++i) {
            const entry = legend.fEntries[i], w4 = Math.round(width / 4);
            let objp = null;

            this.drawText({ latex: 1, width: 0.75 * width - 3 * margin_x, height: stepy, x: 2 * margin_x + w4, y: posy, text: entry.fLabel });

            if (entry.fDrawableId !== 'custom')
               objp = pp.findSnap(entry.fDrawableId, true);
            else if (entry.fDrawable.fIO) {
               objp = new RObjectPainter(this.getPadPainter(), entry.fDrawable.fIO);
               if (entry.fLine)
                  objp.createv7AttLine();
               if (entry.fFill)
                  objp.createv7AttFill();
               if (entry.fMarker)
                  objp.createv7AttMarker();
            }

            if (entry.fFill && objp?.fillatt) {
               this.appendPath(`M${Math.round(margin_x)},${Math.round(posy + stepy * 0.1)}h${w4}v${Math.round(stepy * 0.8)}h${-w4}z`)
                   .call(objp.fillatt.func);
            }

            if (entry.fLine && objp?.lineatt) {
               this.appendPath(`M${Math.round(margin_x)},${Math.round(posy + stepy / 2)}h${w4}`)
                   .call(objp.lineatt.func);
            }

            if (entry.fError && objp?.lineatt) {
               this.appendPath(`M${Math.round(margin_x + width / 8)},${Math.round(posy + stepy * 0.2)}v${Math.round(stepy * 0.6)}`)
                   .call(objp.lineatt.func);
            }

            if (entry.fMarker && objp?.markeratt) {
               this.appendPath(objp.markeratt.create(margin_x + width / 8, posy + stepy / 2))
                   .call(objp.markeratt.func);
            }

            posy += stepy;
         }

         return this.finishTextDrawing();
      });
   }

   /** @summary draw RLegend object */
   static async draw(dom, legend, opt) {
      const painter = new RLegendPainter(dom, legend, opt, 'legend');
      return ensureRCanvas(painter).then(() => painter.drawPave());
   }

} // class RLegendPainter


/**
 * @summary Painter for RPaveText class
 *
 * @private
 */

class RPaveTextPainter extends RPavePainter {

   /** @summary draw RPaveText content */
   async drawContent() {
      const pavetext = this.getObject(),
            nlines = pavetext?.fText.length;

      if (!nlines)
         return;

      const textFont = this.v7EvalFont('text', { size: 12, color: 'black', align: 22 }),
            width = this.pave_width,
            height = this.pave_height,
            stepy = height / nlines, margin_x = 0.02 * width;

      textFont.setSize(height / (nlines * 1.2));

      return this.startTextDrawingAsync(textFont, 'font').then(() => {
         for (let i = 0, posy = 0; i < pavetext.fText.length; ++i, posy += stepy)
            this.drawText({ latex: 1, width: width - 2 * margin_x, height: stepy, x: margin_x, y: posy, text: pavetext.fText[i] });

         return this.finishTextDrawing(undefined, true);
      });
   }

   /** @summary draw RPaveText object */
   static async draw(dom, pave, opt) {
      const painter = new RPaveTextPainter(dom, pave, opt, 'pavetext');
      return ensureRCanvas(painter).then(() => painter.drawPave());
   }

} // class RPaveTextPainter

var RPavePainter$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
RLegendPainter: RLegendPainter,
RPavePainter: RPavePainter,
RPaveTextPainter: RPaveTextPainter
});

exports.BIT = BIT;
exports.BasePainter = BasePainter;
exports.BatchDisplay = BatchDisplay;
exports.BrowserLayout = BrowserLayout;
exports.CustomDisplay = CustomDisplay;
exports.DrawOptions = DrawOptions;
exports.EAxisBits = EAxisBits;
exports.FileProxy = FileProxy;
exports.FlexibleDisplay = FlexibleDisplay;
exports.GridDisplay = GridDisplay;
exports.HierarchyPainter = HierarchyPainter;
exports.MDIDisplay = MDIDisplay;
exports.ObjectPainter = ObjectPainter;
exports.RTreeMapPainter = RTreeMapPainter;
exports.TCanvasPainter = TCanvasPainter;
exports.TGeoPainter = TGeoPainter;
exports.TGraphPainter = TGraphPainter;
exports.TH1Painter = TH1Painter;
exports.TH2Painter = TH2Painter;
exports.TH3Painter = TH3Painter;
exports.THREE = THREE;
exports.TPadPainter = TPadPainter;
exports.TRandom = TRandom;
exports.TSelector = TSelector;
exports.TabsDisplay = TabsDisplay;
exports._loadJSDOM = _loadJSDOM;
exports.addDrawFunc = addDrawFunc;
exports.addHighlightStyle = addHighlightStyle;
exports.addMoveHandler = addMoveHandler;
exports.addUserStreamer = addUserStreamer;
exports.assignContextMenu = assignContextMenu;
exports.atob_func = atob_func;
exports.browser = browser;
exports.btoa_func = btoa_func;
exports.build3d = build3d;
exports.buildGUI = buildGUI;
exports.buildSvgCurve = buildSvgCurve;
exports.clTAnnotation = clTAnnotation;
exports.clTAttCanvas = clTAttCanvas;
exports.clTAttFill = clTAttFill;
exports.clTAttLine = clTAttLine;
exports.clTAttMarker = clTAttMarker;
exports.clTAttText = clTAttText;
exports.clTAxis = clTAxis;
exports.clTBox = clTBox;
exports.clTCanvas = clTCanvas;
exports.clTClonesArray = clTClonesArray;
exports.clTColor = clTColor;
exports.clTCutG = clTCutG;
exports.clTDiamond = clTDiamond;
exports.clTF1 = clTF1;
exports.clTF12 = clTF12;
exports.clTF2 = clTF2;
exports.clTF3 = clTF3;
exports.clTFile = clTFile;
exports.clTFrame = clTFrame;
exports.clTGaxis = clTGaxis;
exports.clTGeoNode = clTGeoNode;
exports.clTGeoNodeMatrix = clTGeoNodeMatrix;
exports.clTGeoVolume = clTGeoVolume;
exports.clTGraph = clTGraph;
exports.clTGraph2DAsymmErrors = clTGraph2DAsymmErrors;
exports.clTGraph2DErrors = clTGraph2DErrors;
exports.clTGraphPolar = clTGraphPolar;
exports.clTGraphPolargram = clTGraphPolargram;
exports.clTGraphTime = clTGraphTime;
exports.clTH1 = clTH1;
exports.clTH1D = clTH1D;
exports.clTH1F = clTH1F;
exports.clTH1I = clTH1I;
exports.clTH2 = clTH2;
exports.clTH2D = clTH2D;
exports.clTH2F = clTH2F;
exports.clTH2I = clTH2I;
exports.clTH3 = clTH3;
exports.clTHStack = clTHStack;
exports.clTHashList = clTHashList;
exports.clTImagePalette = clTImagePalette;
exports.clTKey = clTKey;
exports.clTLatex = clTLatex;
exports.clTLegend = clTLegend;
exports.clTLegendEntry = clTLegendEntry;
exports.clTLine = clTLine;
exports.clTLink = clTLink;
exports.clTList = clTList;
exports.clTMap = clTMap;
exports.clTMarker = clTMarker;
exports.clTMathText = clTMathText;
exports.clTMultiGraph = clTMultiGraph;
exports.clTNamed = clTNamed;
exports.clTObjArray = clTObjArray;
exports.clTObjString = clTObjString;
exports.clTObject = clTObject;
exports.clTPad = clTPad;
exports.clTPaletteAxis = clTPaletteAxis;
exports.clTPave = clTPave;
exports.clTPaveClass = clTPaveClass;
exports.clTPaveLabel = clTPaveLabel;
exports.clTPaveStats = clTPaveStats;
exports.clTPaveText = clTPaveText;
exports.clTPavesText = clTPavesText;
exports.clTPolyLine = clTPolyLine;
exports.clTPolyLine3D = clTPolyLine3D;
exports.clTPolyMarker3D = clTPolyMarker3D;
exports.clTProfile = clTProfile;
exports.clTProfile2D = clTProfile2D;
exports.clTProfile3D = clTProfile3D;
exports.clTString = clTString;
exports.clTStyle = clTStyle;
exports.clTText = clTText;
exports.clTTree = clTTree;
exports.cleanup = cleanup;
exports.clone = clone;
exports.closeMenu = closeMenu;
exports.compressSVG = compressSVG;
exports.constants = constants$1;
exports.convertDate = convertDate;
exports.create = create$1;
exports.createGeoPainter = createGeoPainter;
exports.createHistogram = createHistogram;
exports.createHttpRequest = createHttpRequest;
exports.createMenu = createMenu;
exports.createRootColors = createRootColors;
exports.createTGraph = createTGraph;
exports.createTHStack = createTHStack;
exports.createTMultiGraph = createTMultiGraph;
exports.createTPolyLine = createTPolyLine;
exports.d3_select = select;
exports.decodeUrl = decodeUrl;
exports.draw = draw;
exports.drawRawText = drawRawText;
exports.drawTFrame = drawTFrame;
exports.drawTPadSnapshot = drawTPadSnapshot;
exports.drawingJSON = drawingJSON;
exports.ensureTCanvas = ensureTCanvas;
exports.extendRootColors = extendRootColors;
exports.findFunction = findFunction;
exports.floatToString = floatToString;
exports.gStyle = gStyle;
exports.geoCfg = geoCfg;
exports.getAbsPosInCanvas = getAbsPosInCanvas;
exports.getActivePad = getActivePad;
exports.getBoxDecorations = getBoxDecorations;
exports.getColor = getColor;
exports.getDocument = getDocument;
exports.getDomCanvasPainter = getDomCanvasPainter;
exports.getElementCanvPainter = getElementCanvPainter;
exports.getElementMainPainter = getElementMainPainter;
exports.getElementPadPainter = getElementPadPainter;
exports.getElementRect = getElementRect;
exports.getHPainter = getHPainter;
exports.getKindForType = getKindForType;
exports.getMethods = getMethods;
exports.getPromise = getPromise;
exports.getTDatime = getTDatime;
exports.getTypeForKind = getTypeForKind;
exports.hasMenu = hasMenu;
exports.httpRequest = httpRequest;
exports.injectCode = injectCode;
exports.internals = internals;
exports.isArrayProto = isArrayProto;
exports.isBatchMode = isBatchMode;
exports.isFunc = isFunc;
exports.isNodeJs = isNodeJs;
exports.isObject = isObject;
exports.isPadPainter = isPadPainter;
exports.isPromise = isPromise;
exports.isRootCollection = isRootCollection;
exports.isStr = isStr;
exports.kAxisFunc = kAxisFunc;
exports.kAxisLabels = kAxisLabels;
exports.kAxisNormal = kAxisNormal;
exports.kAxisTime = kAxisTime;
exports.kInspect = kInspect;
exports.kNoReorder = kNoReorder;
exports.kNoStats = kNoStats;
exports.kNoZoom = kNoZoom;
exports.kTitle = kTitle;
exports.kToFront = kToFront;
exports.loadMathjax = loadMathjax;
exports.loadModules = loadModules;
exports.loadOpenui5 = loadOpenui5;
exports.loadScript = loadScript;
exports.makeImage = makeImage;
exports.makeSVG = makeSVG;
exports.makeTranslate = makeTranslate;
exports.nsREX = nsREX;
exports.nsROOT = nsROOT;
exports.nsSVG = nsSVG;
exports.openFile = openFile;
exports.parse = parse$1;
exports.parseMulti = parseMulti;
exports.postponePromise = postponePromise;
exports.prJSON = prJSON;
exports.prROOT = prROOT;
exports.prSVG = prSVG;
exports.readStyleFromURL = readStyleFromURL;
exports.redraw = redraw;
exports.registerForResize = registerForResize;
exports.registerMethods = registerMethods;
exports.resize = resize;
exports.selectActivePad = selectActivePad;
exports.setBatchMode = setBatchMode;
exports.setDefaultDrawOpt = setDefaultDrawOpt;
exports.setHPainter = setHPainter;
exports.setHistogramTitle = setHistogramTitle;
exports.setSaveFile = setSaveFile;
exports.settings = settings;
exports.showPainterMenu = showPainterMenu;
exports.svgToImage = svgToImage;
exports.toJSON = toJSON;
exports.treeDraw = treeDraw;
exports.treeProcess = treeProcess;
exports.unzipJSON = unzipJSON;
exports.urlClassPrefix = urlClassPrefix;
exports.version = version;
exports.version_date = version_date;
exports.version_id = version_id;

}));
