// https://root.cern/js/ v7.9.1 (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; /** @summary version id * @desc For the JSROOT release the string in format 'major.minor.patch' like '7.0.0' */ const version_id = '7.9.x', /** @summary version date * @desc Release date in format day/month/year like '14/04/2022' */ version_date = '19/05/2025', /** @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 ') !== 0)) 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, fromString(s) { if ((s === 'webgl') || (s === 'gl')) return this.WebGL; if (s === 'img') return this.WebGLImage; if (s === 'svg') return this.SVG; 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 }, /** @namespace * @summary Insiance of TStyle object like in ROOT * @desc Includes default draw styles, can be changed after loading of JSRoot.core.js * 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, fOptDate: 0, fOptFile: 0, fDateX: 0.01, fDateY: 0.01, /** @summary Draw histogram title */ fOptTitle: 1, /** @summary Canvas fill color */ fCanvasColor: 0, /** @summary Pad fill color */ fPadColor: 0, fPadBottomMargin: 0.1, fPadTopMargin: 0.1, fPadLeftMargin: 0.1, fPadRightMargin: 0.1, /** @summary TPad.fGridx default value */ fPadGridX: false, /** @summary TPad.fGridy default value */ fPadGridY: false, fPadTickX: 0, fPadTickY: 0, fPadBorderSize: 2, fPadBorderMode: 0, fCanvasBorderSize: 2, 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', fStatX: 0.98, fStatY: 0.935, fStatW: 0.2, fStatH: 0.16, fTitleAlign: 23, fTitleColor: 0, fTitleTextColor: 1, fTitleBorderSize: 0, fTitleFont: 42, fTitleFontSize: 0.05, 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', fOptStat: 1111, fOptFit: 0, fNumberContours: 20, fGridColor: 0, fGridStyle: 3, fGridWidth: 1, fFrameFillColor: 0, fFrameFillStyle: 1001, fFrameLineColor: 1, fFrameLineWidth: 1, fFrameLineStyle: 1, fFrameBorderSize: 1, 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, fHistFillColor: 0, fHistFillStyle: 1001, fHistLineColor: 602, fHistLineStyle: 1, fHistLineWidth: 1, /** @summary format for bin content */ fPaintTextFormat: 'g', /** @summary default time offset, UTC time at 01/01/95 */ fTimeOffset: 788918400, fLegendBorderSize: 1, fLegendFont: 42, fLegendTextSize: 0, fLegendFillColor: 0, fLegendFillStyle: 1001, 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, fBarWidth: 1 }; /** @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:') !== 0)) 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#a2ecf0daacdad801e60b8093a404c897d} 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) { const v = this.opts[opt]; return v !== undefined ? v : 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 !== 0) || ((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].innerHTML === 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.innerHTML = 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 === 0) 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 = () => { if (!scripts.length) return true; return 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', 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', 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', nsREX = 'ROOT::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 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) 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 (this.fBits & f) !== 0; }; 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) return; 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]; if (this.fParams) return this.fParams[n]; return 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) { 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; let y1 = y2 - dy + 2*ymargin; if (y1 < 0) y1 = 0; 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; } 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 !== 0) 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('ROOT::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('ROOT::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 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, 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, 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, getMethods: getMethods, getPromise: getPromise, 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, 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() { return []; } function selectorAll(selector) { return selector == null ? empty : 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; 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() { 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$1 = 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$1(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$1() { 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$1() : +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$1(); // 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$1(), 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 !== 0) { 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) { this.opt = isStr(opt) ? opt.toUpperCase().trim() : ''; this.part = ''; } /** @summary Returns true if remaining options are empty or contain only separators symbols. */ empty() { if (this.opt.length === 0) return true; return this.opt.replace(/[ ;_,]/g, '').length === 0; } /** @summary Returns remaining part of the draw options. */ remain() { return this.opt; } /** @summary Checks if given option exists */ check(name, postpart) { const pos = this.opt.indexOf(name); if (pos < 0) return false; this.opt = this.opt.slice(0, pos) + this.opt.slice(pos + name.length); this.part = ''; if (!postpart) return true; let pos2 = pos; 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.opt = this.opt.slice(0, pos) + this.opt.slice(pos2); } if (postpart !== 'color') 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 remaining part of found option as integer. */ partAsInt(offset, dflt) { let mult = 1; const last = this.part ? this.part[this.part.length - 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\("#(\w+)"\)/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>/g, '') // remove all empty groups with transform .replace(/<\/g>/g, '') // remove hidden title .replace(/<\/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>/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 /** @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 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) 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); delete this._hitemname; delete this._hdrawopt; delete this._hpainter; } /** @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') if (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 (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 > 0) 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() && enlarge.node().childNodes.length > 0) 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) { if (isStr(name)) this._hitemname = name; else delete this._hitemname; // only update draw option, never delete. if (isStr(opt)) this._hdrawopt = opt; this._hpainter = 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('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 = ''; 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 === 0) { 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 => 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); } 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 */ 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 */ 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 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 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 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 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], 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) + `${String.fromCharCode(newcode)} `; 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 together with normal 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; }, shiftX = dx => { curr.x += Math.round(dx); }, 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; }, 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 */ 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 */ 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); }, 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 !== 0) && (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 !== 0) { 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; }, 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'); }, 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 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); delete 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] === '_') { label = label.slice(1); res.low = !res.low ? extractSubLabel('_') : -1; if (res.low === -1) { console.log(`error with ${found.name} low limit`); return false; } } else if (label[0] === '^') { label = label.slice(1); res.up = !res.up ? extractSubLabel('^') : -1; 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; if (subpos.deco) { 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()) { 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},${-h+f2}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 together with normal 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 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'; 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', ' ': '\\;' }, 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' }, 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 }, 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 !== 0) { 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 !== 0) 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 { /** @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._disable) { 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) { if ((on === undefined) || on) delete this._disable; else this._disable = 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 ? painter.getCanvSvg() : null, 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 = 12; 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: w = h = 10; lines = 'M0,2h10M0,7h10M2,0v2M7,2v5M2,7v3'; break; // bricks 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: w = h = 8; lines = 'M8,2h-2v4h-4v2M2,0v2h-2'; lfill = 'none'; break; // left stairs case 3022: w = h = 8; lines = 'M0,2h2v4h4v2M6,0v2h2'; lfill = 'none'; break; // right stairs 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}`; } }, 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; } }, 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 !== 0)) 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 Painter class for ROOT objects * */ class ObjectPainter extends BasePainter { #draw_object; // drawn object #main_painter; // WeakRef to main painter in the pad #primary_ref; // reference of primary painter - if any #secondary_id; // id of this painter in relation to primary painter #options_store; // stored draw options used to check changes /** @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) { let pp = null; if (isFunc(dom?.forEachPainterInPad) && (dom?.this_pad_name !== undefined)) { pp = dom; dom = pp.getDom(); } super(dom); // this.draw_g = undefined; // container for all drawn objects this.pad_name = pp?.this_pad_name ?? ''; // name of pad where object is drawn 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 Assigns pad name where element will be drawn * @desc Should happened before first draw of element is performed, only for special use case * @param {string} [pad_name] - on which sub-pad element should be draw, if not specified - use current * @protected * @deprecated to be removed in v8 */ setPadName(pad_name) { // console.warn('setPadName is deprecated, to be removed in v8'); this.pad_name = isStr(pad_name) ? pad_name : ''; } /** @summary Returns pad name where object is drawn */ getPadName() { return this.pad_name || ''; } /** @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 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.normal_canvas === false)) keep_origin = false; } // cleanup all existing references delete this.pad_name; this.#main_painter = null; this.#draw_object = null; delete this.snapid; this._is_primary = undefined; this.#primary_ref = undefined; this.#secondary_id = undefined; // remove attributes objects (if any) delete this.fillatt; delete this.lineatt; delete this.markeratt; delete this._root_colors; delete this.options; 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 Store actual this.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?._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(prROOT + 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 = getDocument(), current = doc.body.style.cursor; document.body.style.cursor = 'wait'; const res = this.redrawPad(); 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 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) { if (!this._root_colors) this._root_colors = this.getCanvPainter()?._root_colors || getRootColors(); return this._root_colors[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) { if (!this._root_colors) this._root_colors = this.getCanvPainter()?._root_colors || getRootColors(); const indx = this._root_colors.indexOf(color); if (indx >= 0) return indx; this._root_colors.push(color); return this._root_colors.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) { if (on === undefined) 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 element with object drawing * @desc generic method to delete all graphical elements, associated with the painter * @protected */ removeG() { this.draw_g?.remove(); delete this.draw_g; } /** @summary Returns created element used for object drawing * @desc Element should be created by {@link ObjectPainter#createG} * @protected */ getG() { return this.draw_g; } /** @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, element will be created inside frame layer, otherwise in the pad * @protected */ createG(frame_layer, use_a = false) { let layer; if (frame_layer === 'frame2d') { const fp = this.getFramePainter(); frame_layer = fp && !fp.mode3d; } if (frame_layer) { const frame = this.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 = this.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.snapid) return; const pp = this.getPadPainter(); if (!pp?.snapid) return; this.getCanvPainter()?.sendWebsocket('POPOBJ:'+JSON.stringify([pp.snapid.toString(), this.snapid.toString()])); } /** @summary Canvas main svg element * @return {object} d3 selection with canvas svg * @protected */ getCanvSvg() { return this.selectDom().select('.root_canvas'); } /** @summary Pad svg element * @param {string} [pad_name] - pad name to select, if not specified - pad where object is drawn * @return {object} d3 selection with pad svg * @protected */ getPadSvg(pad_name) { if (pad_name === undefined) pad_name = this.pad_name; let c = this.getCanvSvg(); if (!pad_name || c.empty()) return c; const cp = c.property('pad_painter'); if (cp?.pads_cache && cp.pads_cache[pad_name]) return select(cp.pads_cache[pad_name]); c = c.select('.primitives_layer .__root_pad_' + pad_name); if (cp) { if (!cp.pads_cache) cp.pads_cache = {}; cp.pads_cache[pad_name] = c.node(); } return c; } /** @summary Assign secondary id * @private */ setSecondaryId(primary, name) { primary._is_primary = 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 Provides identifier on server for requested sub-element */ getSnapId(subelem) { if (!this.snapid) return ''; return this.snapid.toString() + (subelem ? '#'+subelem : ''); } /** @summary Method selects immediate layer under canvas/pad main element * @param {string} name - layer name, exits 'primitives_layer', 'btns_layer', 'info_layer' * @param {string} [pad_name] - pad name; current pad name used by default * @protected */ getLayerSvg(name, pad_name) { let svg = this.getPadSvg(pad_name); if (svg.empty()) return svg; if (name.indexOf('prim#') === 0) { svg = svg.selectChild('.primitives_layer'); name = name.slice(5); } return svg.selectChild('.' + name); } /** @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 returns pad painter * @param {string} [pad_name] pad name or use current pad by default * @protected */ getPadPainter(pad_name) { const elem = this.getPadSvg(isStr(pad_name) ? pad_name : undefined); return elem.empty() ? null : elem.property('pad_painter'); } /** @summary returns canvas painter * @protected */ getCanvPainter() { const elem = this.getCanvSvg(); return elem.empty() ? null : elem.property('pad_painter'); } /** @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.draw_g?.property('in_frame'); if (use_frame || (use_frame_coordinates && !isndc)) func.main = this.getFramePainter(); if (func.main?.grx && func.main?.gry) { func.x0 = (use_frame_coordinates && !isndc) ? func.main.getFrameX() : 0; func.y0 = (use_frame_coordinates && !isndc) ? func.main.getFrameY() : 0; if (nornd) { func.x = function(x) { return this.x0 + this.main.grx(x); }; func.y = function(y) { return this.y0 + this.main.gry(y); }; } else { func.x = function(x) { return this.x0 + Math.round(this.main.grx(x)); }; func.y = function(y) { return this.y0 + Math.round(this.main.gry(y)); }; } } else if (!use_frame) { const pp = this.getPadPainter(); if (!isndc) func.pad = 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.draw_g?.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 svg element for the frame in current pad * @protected */ getFrameSvg(pad_name) { const layer = this.getLayerSvg('primitives_layer', pad_name); 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 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() { const pp = this.getPadPainter(); if (!pp || (pp === this)) return null; if (pp.painters.indexOf(this) < 0) pp.painters.push(this); return pp; } /** @summary Remove painter from pad list of painters * @protected */ removeFromPadPrimitives() { const pp = this.getPadPainter(); if (!pp || (pp === this)) return false; const k = pp.painters.indexOf(this); if (k >= 0) pp.painters.splice(k, 1); return true; } /** @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; } /** @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 const pp = this.getPadPainter(); if (pp) pp.forEachPainterInPad(userfunc, kind); else { const painter = this.getTopPainter(); if (painter && (kind !== 'pads')) userfunc(painter); } } /** @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:') !== 0)) 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() { delete this.lineatt; delete this.fillatt; delete this.markeratt; } /** @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 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.draw_g; 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()?._fast_drawing ?? 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 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.draw_g; 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.draw_g; 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] - element for text drawing, this.draw_g used when not specified * @return {Promise} when text drawing completed * @protected */ async finishTextDrawing(draw_g, try_optimize) { if (!draw_g) draw_g = this.draw_g; 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) { if (!fillmenu_func || !isFunc(fillmenu_func)) delete this._userContextMenuFunc; else this._userContextMenuFunc = fillmenu_func; } /** @summary Fill object menu in web canvas * @private */ async fillObjectExecMenu(menu, kind) { if (isFunc(this._userContextMenuFunc)) return this._userContextMenuFunc(menu, kind); const canvp = this.getCanvPainter(); if (!this.snapid || !canvp || canvp?._readonly || !canvp?._websocket) 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) return; if (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) { if (!handler || !isFunc(handler)) { delete this._user_tooltip_handler; delete this._user_tooltip_timeout; } else { this._user_tooltip_handler = handler; this._user_tooltip_timeout = tmout || 100; } } /** @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_tooltip_handle) { clearTimeout(this._user_tooltip_handle); delete this._user_tooltip_handle; } if (!data) return this._user_tooltip_handler(data); // only after timeout user function will be called this._user_tooltip_handle = setTimeout(() => { delete this._user_tooltip_handle; 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.getFrameSvg(); if (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 = function(obj) { this.txt = obj; this.drawText(); return true; }; painter.drawText = async function() { let stxt = (this.txt._typename === clTObjString) ? this.txt.fString : this.txt.value; if (!isStr(stxt)) stxt = ''; const mathjax = this.txt.mathjax || (settings.Latex === constants$1.Latex.AlwaysMathJax); if (!mathjax && !('as_is' in this.txt)) { const arr = stxt.split('\n'); stxt = ''; for (let i = 0; i < arr.length; ++i) stxt += `
${arr[i]}
`; } const 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'); main.html(stxt); // (re) set painter to first child element, base painter not requires canvas this.setTopPainter(); if (mathjax) typesetMathjax(frame.node()); return this; }; return painter.drawText(); } /** @summary Returns canvas painter (if any) for specified HTML element * @param {string|object} dom - id or DOM element * @private */ function getElementCanvPainter(dom) { return new ObjectPainter(dom).getCanvPainter(); } /** @summary Returns main painter (if any) for specified HTML element - typically histogram painter * @param {string|object} dom - id or DOM element * @private */ function getElementMainPainter(dom) { return 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 = '174'; const MOUSE = { ROTATE: 0, DOLLY: 1, PAN: 2 }; const TOUCH = { ROTATE: 0, PAN: 1, DOLLY_PAN: 2, DOLLY_ROTATE: 3 }; const CullFaceNone = 0; const CullFaceBack = 1; const CullFaceFront = 2; const PCFShadowMap = 1; const PCFSoftShadowMap = 2; const VSMShadowMap = 3; const FrontSide = 0; const BackSide = 1; const DoubleSide = 2; const NoBlending = 0; const NormalBlending = 1; const AdditiveBlending = 2; const SubtractiveBlending = 3; const MultiplyBlending = 4; const CustomBlending = 5; const AddEquation = 100; const SubtractEquation = 101; const ReverseSubtractEquation = 102; const MinEquation = 103; const MaxEquation = 104; const ZeroFactor = 200; const OneFactor = 201; const SrcColorFactor = 202; const OneMinusSrcColorFactor = 203; const SrcAlphaFactor = 204; const OneMinusSrcAlphaFactor = 205; const DstAlphaFactor = 206; const OneMinusDstAlphaFactor = 207; const DstColorFactor = 208; const OneMinusDstColorFactor = 209; const SrcAlphaSaturateFactor = 210; const ConstantColorFactor = 211; const OneMinusConstantColorFactor = 212; const ConstantAlphaFactor = 213; const OneMinusConstantAlphaFactor = 214; const NeverDepth = 0; const AlwaysDepth = 1; const LessDepth = 2; const LessEqualDepth = 3; const EqualDepth = 4; const GreaterEqualDepth = 5; const GreaterDepth = 6; const NotEqualDepth = 7; const MultiplyOperation = 0; const MixOperation = 1; const AddOperation = 2; const NoToneMapping = 0; const LinearToneMapping = 1; const ReinhardToneMapping = 2; const CineonToneMapping = 3; const ACESFilmicToneMapping = 4; const CustomToneMapping = 5; const AgXToneMapping = 6; const NeutralToneMapping = 7; const UVMapping = 300; const CubeReflectionMapping = 301; const CubeRefractionMapping = 302; const EquirectangularReflectionMapping = 303; const EquirectangularRefractionMapping = 304; const CubeUVReflectionMapping = 306; const RepeatWrapping = 1000; const ClampToEdgeWrapping = 1001; const MirroredRepeatWrapping = 1002; const NearestFilter = 1003; const NearestMipmapNearestFilter = 1004; const NearestMipmapLinearFilter = 1005; const LinearFilter = 1006; const LinearMipmapNearestFilter = 1007; const LinearMipmapLinearFilter = 1008; const UnsignedByteType = 1009; const ByteType = 1010; const ShortType = 1011; const UnsignedShortType = 1012; const IntType = 1013; const UnsignedIntType = 1014; const FloatType = 1015; const HalfFloatType = 1016; const UnsignedShort4444Type = 1017; const UnsignedShort5551Type = 1018; const UnsignedInt248Type = 1020; const UnsignedInt5999Type = 35902; const AlphaFormat = 1021; const RGBFormat = 1022; const RGBAFormat = 1023; const LuminanceFormat = 1024; const LuminanceAlphaFormat = 1025; const DepthFormat = 1026; const DepthStencilFormat = 1027; const RedFormat = 1028; const RedIntegerFormat = 1029; const RGFormat = 1030; const RGIntegerFormat = 1031; const RGBAIntegerFormat = 1033; const RGB_S3TC_DXT1_Format = 33776; const RGBA_S3TC_DXT1_Format = 33777; const RGBA_S3TC_DXT3_Format = 33778; const RGBA_S3TC_DXT5_Format = 33779; const RGB_PVRTC_4BPPV1_Format = 35840; const RGB_PVRTC_2BPPV1_Format = 35841; const RGBA_PVRTC_4BPPV1_Format = 35842; const RGBA_PVRTC_2BPPV1_Format = 35843; const RGB_ETC1_Format = 36196; const RGB_ETC2_Format = 37492; const RGBA_ETC2_EAC_Format = 37496; const RGBA_ASTC_4x4_Format = 37808; const RGBA_ASTC_5x4_Format = 37809; const RGBA_ASTC_5x5_Format = 37810; const RGBA_ASTC_6x5_Format = 37811; const RGBA_ASTC_6x6_Format = 37812; const RGBA_ASTC_8x5_Format = 37813; const RGBA_ASTC_8x6_Format = 37814; const RGBA_ASTC_8x8_Format = 37815; const RGBA_ASTC_10x5_Format = 37816; const RGBA_ASTC_10x6_Format = 37817; const RGBA_ASTC_10x8_Format = 37818; const RGBA_ASTC_10x10_Format = 37819; const RGBA_ASTC_12x10_Format = 37820; const RGBA_ASTC_12x12_Format = 37821; const RGBA_BPTC_Format = 36492; const RGB_BPTC_SIGNED_Format = 36494; const RGB_BPTC_UNSIGNED_Format = 36495; const RED_RGTC1_Format = 36283; const SIGNED_RED_RGTC1_Format = 36284; const RED_GREEN_RGTC2_Format = 36285; const SIGNED_RED_GREEN_RGTC2_Format = 36286; const BasicDepthPacking = 3200; const RGBADepthPacking = 3201; const TangentSpaceNormalMap = 0; const ObjectSpaceNormalMap = 1; // Color space string identifiers, matching CSS Color Module Level 4 and WebGPU names where available. const NoColorSpace = ''; const SRGBColorSpace = 'srgb'; const LinearSRGBColorSpace = 'srgb-linear'; const LinearTransfer = 'linear'; const SRGBTransfer = 'srgb'; const KeepStencilOp = 7680; const AlwaysStencilFunc = 519; const NeverCompare = 512; const LessCompare = 513; const EqualCompare = 514; const LessEqualCompare = 515; const GreaterCompare = 516; const NotEqualCompare = 517; const GreaterEqualCompare = 518; const AlwaysCompare = 519; const StaticDrawUsage = 35044; const GLSL3 = '300 es'; const WebGLCoordinateSystem = 2000; const WebGPUCoordinateSystem = 2001; /** * 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 `` to range `` * 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 `` 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 `` 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.' ); } } const MathUtils = { DEG2RAD: DEG2RAD, RAD2DEG: RAD2DEG, generateUUID: generateUUID, clamp: clamp, euclideanModulo: euclideanModulo, mapLinear: mapLinear, inverseLerp: inverseLerp, lerp: lerp, damp: damp, pingpong: pingpong, smoothstep: smoothstep, smootherstep: smootherstep, randInt: randInt, randFloat: randFloat, randFloatSpread: randFloatSpread, seededRandom: seededRandom, degToRad: degToRad, radToDeg: radToDeg, isPowerOfTwo: isPowerOfTwo, ceilPowerOfTwo: ceilPowerOfTwo, floorPowerOfTwo: floorPowerOfTwo, setQuaternionFromProperEuler: setQuaternionFromProperEuler, normalize: normalize$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} 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} [array=[]] - The target array holding the vector components. * @param {number} [offset=0] - Index of the first element in the array. * @return {Array} 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; } } /** * 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} */ 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} 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} 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} [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} 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 ); } ); } function toNormalizedProjectionMatrix( projectionMatrix ) { const m = projectionMatrix.elements; // Convert [-1, 1] to [0, 1] projection matrix m[ 2 ] = 0.5 * m[ 2 ] + 0.5 * m[ 3 ]; m[ 6 ] = 0.5 * m[ 6 ] + 0.5 * m[ 7 ]; m[ 10 ] = 0.5 * m[ 10 ] + 0.5 * m[ 11 ]; m[ 14 ] = 0.5 * m[ 14 ] + 0.5 * m[ 15 ]; } function toReversedProjectionMatrix( projectionMatrix ) { const m = projectionMatrix.elements; const isPerspectiveMatrix = m[ 11 ] === -1; // Reverse [0, 1] projection matrix if ( isPerspectiveMatrix ) { m[ 10 ] = - m[ 10 ] - 1; m[ 14 ] = - m[ 14 ]; } else { m[ 10 ] = - m[ 10 ]; m[ 14 ] = - m[ 14 ] + 1; } } 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 } * - 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; }, fromWorkingColorSpace: function ( color, targetColorSpace ) { return this.convert( color, this.workingColorSpace, targetColorSpace ); }, toWorkingColorSpace: 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; }, 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; } }; /****************************************************************************** * 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. * @return {string} The data URI. */ static getDataURL( image ) { 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( 'image/png' ); } /** * 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; class Source { constructor( data = null ) { this.isSource = true; Object.defineProperty( this, 'id', { value: _sourceId ++ } ); this.uuid = generateUUID(); this.data = data; this.dataReady = true; this.version = 0; } set needsUpdate( value ) { if ( value === true ) this.version ++; } 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; /** * 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=LinearFilter] - The min filter value. * @param {number} [format=RGBAFormat] - The min filter value. * @param {number} [type=UnsignedByteType] - The min filter value. * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The min filter value. * @param {string} [colorSpace=NoColorSpace] - The min filter value. */ 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} */ 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 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 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 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 ); } /** * 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.userData = JSON.parse( JSON.stringify( source.userData ) ); this.needsUpdate = true; return this; } /** * 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.6, 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} 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} [array=[]] - The target array holding the vector components. * @param {number} [offset=0] - Index of the first element in the array. * @return {Array} 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; } } /* In options, we can specify: * Texture parameters for an auto-generated target texture * depthBuffer/stencilBuffer: Booleans to indicate if we should generate these buffers */ class RenderTarget extends EventDispatcher { constructor( width = 1, height = 1, options = {} ) { super(); this.isRenderTarget = true; this.width = width; this.height = height; this.depth = 1; this.scissor = new Vector4( 0, 0, width, height ); this.scissorTest = false; this.viewport = new Vector4( 0, 0, width, height ); const image = { width: width, height: height, depth: 1 }; options = Object.assign( { generateMipmaps: false, internalFormat: null, minFilter: LinearFilter, depthBuffer: true, stencilBuffer: false, resolveDepthBuffer: true, resolveStencilBuffer: true, depthTexture: null, samples: 0, count: 1 }, options ); const texture = new Texture( image, options.mapping, options.wrapS, options.wrapT, options.magFilter, options.minFilter, options.format, options.type, options.anisotropy, options.colorSpace ); texture.flipY = false; texture.generateMipmaps = options.generateMipmaps; texture.internalFormat = options.internalFormat; 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.depthBuffer = options.depthBuffer; this.stencilBuffer = options.stencilBuffer; this.resolveDepthBuffer = options.resolveDepthBuffer; this.resolveStencilBuffer = options.resolveStencilBuffer; this._depthTexture = null; this.depthTexture = options.depthTexture; this.samples = options.samples; } 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; } get depthTexture() { return this._depthTexture; } 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.dispose(); } this.viewport.set( 0, 0, width, height ); this.scissor.set( 0, 0, width, height ); } clone() { return new this.constructor().copy( this ); } 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; } dispose() { this.dispatchEvent( { type: 'dispose' } ); } } class WebGLRenderTarget extends RenderTarget { constructor( width = 1, height = 1, options = {} ) { super( width, height, options ); 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 working * color space, 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 Color management. 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.toWorkingColorSpace( 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.toWorkingColorSpace( 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.toWorkingColorSpace( 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.fromWorkingColorSpace( _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:0,s:0,l:0}} 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.fromWorkingColorSpace( _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.fromWorkingColorSpace( _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.fromWorkingColorSpace( _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} 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} [array=[]] - The target array holding the color components. * @param {number} [offset=0] - Index of the first element in the array. * @return {Array} 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; /** * 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} dst - The destination array. * @param {number} dstOffset - An offset into the destination array. * @param {Array} src0 - The source array of the first quaternion. * @param {number} srcOffset0 - An offset into the first source array. * @param {Array} 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} dst - The destination array. * @param {number} dstOffset - An offset into the destination array. * @param {Array} src0 - The source array of the first quaternion. * @param {number} srcOffset0 - An offset into the first source array. * @param {Array} src1 - The source array of the second quaternion. * @param {number} srcOffset1 - An offset into the second source array. * @return {Array} 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 < Number.EPSILON ) { // 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} 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} [array=[]] - The target array holding the quaternion components. * @param {number} [offset=0] - Index of the first element in the array. * @return {Array} 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} 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} 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} [array=[]] - The target array holding the vector components. * @param {number} [offset=0] - Index of the first element in the array. * @return {Array} 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 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} 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} 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 ); } } 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} 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 ); } } 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 _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} */ 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. * @return {Frustum} A reference to this frustum. */ setFromProjectionMatrix( m, coordinateSystem = WebGLCoordinateSystem ) { 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(); planes[ 4 ].setComponents( me3 - me2, me7 - me6, me11 - me10, me15 - me14 ).normalize(); if ( coordinateSystem === WebGLCoordinateSystem ) { planes[ 5 ].setComponents( me3 + me2, me7 + me6, me11 + me10, me15 + me14 ).normalize(); } else if ( coordinateSystem === WebGPUCoordinateSystem ) { planes[ 5 ].setComponents( me2, me6, me10, me14 ).normalize(); } 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 ); _sphere$6.radius = 0.7071067811865476; _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} */ 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. * @return {Matrix4} A reference to this matrix. */ makePerspective( left, right, top, bottom, near, far, coordinateSystem = WebGLCoordinateSystem ) { 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 ( 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. * @return {Matrix4} A reference to this matrix. */ makeOrthographic( left, right, top, bottom, near, far, coordinateSystem = WebGLCoordinateSystem ) { const te = this.elements; const w = 1.0 / ( right - left ); const h = 1.0 / ( top - bottom ); const p = 1.0 / ( far - near ); const x = ( right + left ) * w; const y = ( top + bottom ) * h; let z, zInv; if ( coordinateSystem === WebGLCoordinateSystem ) { z = ( far + near ) * p; zInv = -2 * p; } else if ( coordinateSystem === WebGPUCoordinateSystem ) { z = near * p; zInv = -1 * p; } else { throw new Error( 'THREE.Matrix4.makeOrthographic(): Invalid coordinate system: ' + coordinateSystem ); } te[ 0 ] = 2 * w; te[ 4 ] = 0; te[ 8 ] = 0; te[ 12 ] = - x; te[ 1 ] = 0; te[ 5 ] = 2 * h; te[ 9 ] = 0; te[ 13 ] = - y; te[ 2 ] = 0; te[ 6 ] = 0; te[ 10 ] = zInv; te[ 14 ] = - z; 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} 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} [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} 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 ( 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; class BufferAttribute { constructor( array, itemSize, normalized = false ) { if ( Array.isArray( array ) ) { throw new TypeError( 'THREE.BufferAttribute: array should be a Typed Array.' ); } this.isBufferAttribute = true; Object.defineProperty( this, 'id', { value: _id$2 ++ } ); this.name = ''; this.array = array; this.itemSize = itemSize; this.count = array !== undefined ? array.length / itemSize : 0; this.normalized = normalized; this.usage = StaticDrawUsage; this.updateRanges = []; this.gpuType = FloatType; this.version = 0; } onUploadCallback() {} set needsUpdate( value ) { if ( value === true ) this.version ++; } setUsage( value ) { this.usage = value; return this; } addUpdateRange( start, count ) { this.updateRanges.push( { start, count } ); } clearUpdateRanges() { this.updateRanges.length = 0; } 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; } 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; } copyArray( array ) { this.array.set( array ); return this; } 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; } 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; } 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; } 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; } set( value, offset = 0 ) { // Matching BufferAttribute constructor, do not normalize the array. this.array.set( value, offset ); return this; } getComponent( index, component ) { let value = this.array[ index * this.itemSize + component ]; if ( this.normalized ) value = denormalize( value, this.array ); return value; } setComponent( index, component, value ) { if ( this.normalized ) value = normalize$1( value, this.array ); this.array[ index * this.itemSize + component ] = value; return this; } getX( index ) { let x = this.array[ index * this.itemSize ]; if ( this.normalized ) x = denormalize( x, this.array ); return x; } setX( index, x ) { if ( this.normalized ) x = normalize$1( x, this.array ); this.array[ index * this.itemSize ] = x; return this; } getY( index ) { let y = this.array[ index * this.itemSize + 1 ]; if ( this.normalized ) y = denormalize( y, this.array ); return y; } setY( index, y ) { if ( this.normalized ) y = normalize$1( y, this.array ); this.array[ index * this.itemSize + 1 ] = y; return this; } getZ( index ) { let z = this.array[ index * this.itemSize + 2 ]; if ( this.normalized ) z = denormalize( z, this.array ); return z; } setZ( index, z ) { if ( this.normalized ) z = normalize$1( z, this.array ); this.array[ index * this.itemSize + 2 ] = z; return this; } getW( index ) { let w = this.array[ index * this.itemSize + 3 ]; if ( this.normalized ) w = denormalize( w, this.array ); return w; } setW( index, w ) { if ( this.normalized ) w = normalize$1( w, this.array ); this.array[ index * this.itemSize + 3 ] = w; return this; } 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; } 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; } 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; } onUpload( callback ) { this.onUploadCallback = callback; return this; } clone() { return new this.constructor( this.array, this.itemSize ).copy( this ); } 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; } } class Uint16BufferAttribute extends BufferAttribute { constructor( array, itemSize, normalized ) { super( new Uint16Array( array ), itemSize, normalized ); } } class Uint32BufferAttribute extends BufferAttribute { constructor( array, itemSize, normalized ) { super( new Uint32Array( array ), itemSize, normalized ); } } class Float32BufferAttribute extends BufferAttribute { 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} 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} [array=[]] - The target array holding the Euler components. * @param {number} [offset=0] - Index of the first element in the array. * @return {Array} 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'; class Layers { constructor() { this.mask = 1 | 0; } set( channel ) { this.mask = ( 1 << channel | 0 ) >>> 0; } enable( channel ) { this.mask |= 1 << channel | 0; } enableAll() { this.mask = 0xffffffff | 0; } toggle( channel ) { this.mask ^= 1 << channel | 0; } disable( channel ) { this.mask &= ~ ( 1 << channel | 0 ); } disableAll() { this.mask = 0; } test( layers ) { return ( this.mask & layers.mask ) !== 0; } isEnabled( channel ) { return ( this.mask & ( 1 << channel | 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 new child object has been added. * * @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} */ 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} */ this.animations = []; /** * 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} result - The method stores the result in this array. * @return {Array} 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} 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.6, 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.visibility = this._visibility; object.active = this._active; object.bounds = this._bounds.map( bound => ( { boxInitialized: bound.boxInitialized, boxMin: bound.box.min.toArray(), boxMax: bound.box.max.toArray(), sphereInitialized: bound.sphereInitialized, sphereRadius: bound.sphere.radius, sphereCenter: bound.sphere.center.toArray() } ) ); object.maxInstanceCount = this._maxInstanceCount; object.maxVertexCount = this._maxVertexCount; object.maxIndexCount = this._maxIndexCount; object.geometryInitialized = this._geometryInitialized; object.geometryCount = this._geometryCount; object.matricesTexture = this._matricesTexture.toJSON( meta ); if ( this._colorsTexture !== null ) object.colorsTexture = this._colorsTexture.toJSON( meta ); if ( this.boundingSphere !== null ) { object.boundingSphere = { center: object.boundingSphere.center.toArray(), radius: object.boundingSphere.radius }; } if ( this.boundingBox !== null ) { object.boundingBox = { min: object.boundingBox.min.toArray(), max: object.boundingBox.max.toArray() }; } } // 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(); class BufferGeometry extends EventDispatcher { constructor() { super(); this.isBufferGeometry = true; Object.defineProperty( this, 'id', { value: _id$1 ++ } ); this.uuid = generateUUID(); this.name = ''; this.type = 'BufferGeometry'; this.index = null; this.indirect = null; this.attributes = {}; this.morphAttributes = {}; this.morphTargetsRelative = false; this.groups = []; this.boundingBox = null; this.boundingSphere = null; this.drawRange = { start: 0, count: Infinity }; this.userData = {}; } getIndex() { return this.index; } setIndex( index ) { if ( Array.isArray( index ) ) { this.index = new ( arrayNeedsUint32( index ) ? Uint32BufferAttribute : Uint16BufferAttribute )( index, 1 ); } else { this.index = index; } return this; } setIndirect( indirect ) { this.indirect = indirect; return this; } getIndirect() { return this.indirect; } getAttribute( name ) { return this.attributes[ name ]; } setAttribute( name, attribute ) { this.attributes[ name ] = attribute; return this; } deleteAttribute( name ) { delete this.attributes[ name ]; return this; } hasAttribute( name ) { return this.attributes[ name ] !== undefined; } addGroup( start, count, materialIndex = 0 ) { this.groups.push( { start: start, count: count, materialIndex: materialIndex } ); } clearGroups() { this.groups = []; } setDrawRange( start, count ) { this.drawRange.start = start; this.drawRange.count = count; } 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; } applyQuaternion( q ) { _m1$2.makeRotationFromQuaternion( q ); this.applyMatrix4( _m1$2 ); return this; } rotateX( angle ) { // rotate geometry around world x-axis _m1$2.makeRotationX( angle ); this.applyMatrix4( _m1$2 ); return this; } rotateY( angle ) { // rotate geometry around world y-axis _m1$2.makeRotationY( angle ); this.applyMatrix4( _m1$2 ); return this; } rotateZ( angle ) { // rotate geometry around world z-axis _m1$2.makeRotationZ( angle ); this.applyMatrix4( _m1$2 ); return this; } translate( x, y, z ) { // translate geometry _m1$2.makeTranslation( x, y, z ); this.applyMatrix4( _m1$2 ); return this; } scale( x, y, z ) { // scale geometry _m1$2.makeScale( x, y, z ); this.applyMatrix4( _m1$2 ); return this; } lookAt( vector ) { _obj.lookAt( vector ); _obj.updateMatrix(); this.applyMatrix4( _obj.matrix ); return this; } center() { this.computeBoundingBox(); this.boundingBox.getCenter( _offset ).negate(); this.translate( _offset.x, _offset.y, _offset.z ); return this; } 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; } 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 ); } } 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 ); } } } 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 ) ); } } } 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; } } 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 ); } } 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; } toJSON() { const data = { metadata: { version: 4.6, 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 = { center: boundingSphere.center.toArray(), radius: boundingSphere.radius }; } return data; } clone() { return new this.constructor().copy( this ); } 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( data ) ); } // 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; } 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 OneMinusSrcAlphaFactor */ 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 OneMinusSrcAlphaFactor */ 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} * @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; /** * 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(); } 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.6, 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.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 ++; } onBuild( /* shaderobject, renderer */ ) { console.warn( 'Material: onBuild() has been removed.' ); // @deprecated, r166 } } /** * 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}"; class ShaderMaterial extends Material { constructor( parameters ) { super(); this.isShaderMaterial = true; this.type = 'ShaderMaterial'; this.defines = {}; this.uniforms = {}; this.uniformsGroups = []; this.vertexShader = default_vertex; this.fragmentShader = default_fragment; this.linewidth = 1; this.wireframe = false; this.wireframeLinewidth = 1; this.fog = false; // set to use scene fog this.lights = false; // set to use scene lights this.clipping = false; // set to use user-defined clipping planes this.forceSinglePass = true; this.extensions = { clipCullDistance: false, // set to use vertex shader clipping multiDraw: false // set to use vertex shader multi_draw / enable gl_DrawID }; // When rendered geometry doesn't include these attributes but the material does, // use these default values in WebGL. This avoids errors when buffer data is missing. this.defaultAttributeValues = { 'color': [ 1, 1, 1 ], 'uv': [ 0, 0 ], 'uv1': [ 0, 0 ] }; this.index0AttributeName = undefined; this.uniformsNeedUpdate = false; 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 ) { 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} 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 ); } } class MeshBasicMaterial extends Material { constructor( parameters ) { super(); this.isMeshBasicMaterial = true; this.type = 'MeshBasicMaterial'; this.color = new Color( 0xffffff ); // emissive this.map = null; this.lightMap = null; this.lightMapIntensity = 1.0; this.aoMap = null; this.aoMapIntensity = 1.0; this.specularMap = null; this.alphaMap = null; this.envMap = null; this.envMapRotation = new Euler(); this.combine = MultiplyOperation; this.reflectivity = 1; this.refractionRatio = 0.98; this.wireframe = false; this.wireframeLinewidth = 1; this.wireframeLinecap = 'round'; this.wireframeLinejoin = 'round'; 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] - 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} * @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|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|undefined} * @default undefined */ this.morphTargetInfluences = undefined; 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} 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 = "#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_LOGDEPTHBUF )\n\tgl_FragDepth = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;\n#endif"; var logdepthbuf_pars_fragment = "#if defined( USE_LOGDEPTHBUF )\n\tuniform float logDepthBufFC;\n\tvarying float vFragDepth;\n\tvarying float vIsPerspective;\n#endif"; var logdepthbuf_pars_vertex = "#ifdef USE_LOGDEPTHBUF\n\tvarying float vFragDepth;\n\tvarying float vIsPerspective;\n#endif"; var logdepthbuf_vertex = "#ifdef USE_LOGDEPTHBUF\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\treturn step( compare, unpackRGBAToDepth( texture2D( depths, uv ) ) );\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\tfloat hard_shadow = step( compare , distribution.x );\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 \n\t#include \n}"; const vertex$g = "varying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \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 \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 \n\t#include \n}"; const vertex$f = "varying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \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 \n\t#include \n}"; const vertex$e = "#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvHighPrecisionZW = gl_Position.zw;\n}"; const fragment$e = "#if DEPTH_PACKING == 3200\n\tuniform float opacity;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\tvec4 diffuseColor = vec4( 1.0 );\n\t#include \n\t#if DEPTH_PACKING == 3200\n\t\tdiffuseColor.a = opacity;\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tfloat fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5;\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 \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvWorldPosition = worldPosition.xyz;\n}"; const fragment$d = "#define DISTANCE\nuniform vec3 referencePosition;\nuniform float nearDistance;\nuniform float farDistance;\nvarying vec3 vWorldPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main () {\n\tvec4 diffuseColor = vec4( 1.0 );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \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 \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n}"; const fragment$c = "uniform sampler2D tEquirect;\nvarying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvec3 direction = normalize( vWorldDirection );\n\tvec2 sampleUV = equirectUv( direction );\n\tgl_FragColor = texture2D( tEquirect, sampleUV );\n\t#include \n\t#include \n}"; const vertex$b = "uniform float scale;\nattribute float lineDistance;\nvarying float vLineDistance;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvLineDistance = scale * lineDistance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}"; const fragment$b = "uniform vec3 diffuse;\nuniform float opacity;\nuniform float dashSize;\nuniform float totalSize;\nvarying float vLineDistance;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tif ( mod( vLineDistance, totalSize ) > dashSize ) {\n\t\tdiscard;\n\t}\n\tvec3 outgoingLight = vec3( 0.0 );\n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}"; const vertex$a = "#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#if defined ( USE_ENVMAP ) || defined ( USE_SKINNING )\n\t\t#include \n\t\t#include \n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}"; const fragment$a = "uniform vec3 diffuse;\nuniform float opacity;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \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 \n\treflectedLight.indirectDiffuse *= diffuseColor.rgb;\n\tvec3 outgoingLight = reflectedLight.indirectDiffuse;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}"; const vertex$9 = "#define LAMBERT\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n\t#include \n}"; const fragment$9 = "#define LAMBERT\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}"; const vertex$8 = "#define MATCAP\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n}"; const fragment$8 = "#define MATCAP\nuniform vec3 diffuse;\nuniform float opacity;\nuniform sampler2D matcap;\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \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 \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \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 \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \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 \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( 0.0, 0.0, 0.0, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \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 \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n\t#include \n}"; const fragment$6 = "#define PHONG\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform vec3 specular;\nuniform float shininess;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}"; const vertex$5 = "#define STANDARD\nvarying vec3 vViewPosition;\n#ifdef USE_TRANSMISSION\n\tvarying vec3 vWorldPosition;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \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 \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse;\n\tvec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular;\n\t#include \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 \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}"; const vertex$4 = "#define TOON\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n}"; const fragment$4 = "#define TOON\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}"; const vertex$3 = "uniform float size;\nuniform float scale;\n#include \n#include \n#include \n#include \n#include \n#include \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 \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \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 \n\t#include \n\t#include \n\t#include \n}"; const fragment$3 = "uniform vec3 diffuse;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tvec3 outgoingLight = vec3( 0.0 );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}"; const vertex$2 = "#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}"; const fragment$2 = "uniform vec3 color;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tgl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) );\n\t#include \n\t#include \n\t#include \n}"; const vertex$1 = "uniform float rotation;\nuniform vec2 center;\n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \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 \n\t#include \n\t#include \n}"; const fragment$1 = "uniform vec3 diffuse;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tvec3 outgoingLight = vec3( 0.0 );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \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, 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 } ) ); 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 } ) ); 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 reverseDepthBuffer = parameters.reverseDepthBuffer === 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, reverseDepthBuffer: reverseDepthBuffer, 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; } 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 not 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.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; } } class CubeTexture extends Texture { constructor( images, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, colorSpace ) { images = images !== undefined ? images : []; mapping = mapping !== undefined ? mapping : CubeReflectionMapping; super( images, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, colorSpace ); this.isCubeTexture = true; this.flipY = false; } get images() { return this.image; } set images( value ) { this.image = value; } } class WebGLCubeRenderTarget extends WebGLRenderTarget { constructor( size = 1, options = {} ) { super( size, size, options ); this.isWebGLCubeRenderTarget = true; const image = { width: size, height: size, depth: 1 }; const images = [ image, image, image, image, image, image ]; this.texture = new CubeTexture( images, options.mapping, options.wrapS, options.wrapT, options.magFilter, options.minFilter, options.format, options.type, options.anisotropy, options.colorSpace ); // 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; this.texture.generateMipmaps = options.generateMipmaps !== undefined ? options.generateMipmaps : false; this.texture.minFilter = options.minFilter !== undefined ? options.minFilter : LinearFilter; } 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 #include } `, fragmentShader: /* glsl */` uniform sampler2D tEquirect; varying vec3 vWorldDirection; #include 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; } clear( renderer, color, depth, stencil ) { 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.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 * https://drive.google.com/file/d/15y8r_UpKlU9SvV4ILb0C3qCPecS8pvLz/view */ class PMREMGenerator { 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 * @param {number} sigma * @param {number} near * @param {number} far * @param {Object} [options={}] * @return {WebGLRenderTarget} */ 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. * The smallest supported equirectangular image size is 64 x 32. * * @param {Texture} equirectangular * @param {?WebGLRenderTarget} [renderTarget=null] - Optional render target. * @return {WebGLRenderTarget} */ 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. * The smallest supported cube size is 16 x 16. * * @param {Texture} cubemap * @param {null} [renderTarget=null] - Optional render target. * @return {WebGLRenderTarget} */ 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; 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. * * @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 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 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 }; } class DataArrayTexture extends Texture { constructor( data = null, width = 1, height = 1, depth = 1 ) { super( null ); this.isDataArrayTexture = true; this.image = { data, width, height, depth }; this.magFilter = NearestFilter; this.minFilter = NearestFilter; this.wrapR = ClampToEdgeWrapping; this.generateMipmaps = false; this.flipY = false; this.unpackAlignment = 1; this.layerUpdates = new Set(); } addLayerUpdate( layerIndex ) { this.layerUpdates.add( layerIndex ); } 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 }; } class Data3DTexture extends 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.isData3DTexture = true; this.image = { data, width, height, depth }; this.magFilter = NearestFilter; this.minFilter = NearestFilter; this.wrapR = ClampToEdgeWrapping; this.generateMipmaps = false; this.flipY = false; this.unpackAlignment = 1; } } class DepthTexture extends Texture { constructor( width, height, type, mapping, wrapS, wrapT, magFilter, minFilter, anisotropy, format = DepthFormat ) { if ( format !== DepthFormat && format !== DepthStencilFormat ) { throw new Error( 'DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat' ); } if ( type === undefined && format === DepthFormat ) type = UnsignedIntType; if ( type === undefined && format === DepthStencilFormat ) type = UnsignedInt248Type; super( null, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ); this.isDepthTexture = true; this.image = { width: width, height: height }; this.magFilter = magFilter !== undefined ? magFilter : NearestFilter; this.minFilter = minFilter !== undefined ? minFilter : NearestFilter; this.flipY = false; this.generateMipmaps = false; 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 errors = gl.getShaderInfoLog( shader ).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_LOGDEPTHBUF' : '', parameters.reverseDepthBuffer ? '#define USE_REVERSEDEPTHBUF' : '', '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_LOGDEPTHBUF' : '', parameters.reverseDepthBuffer ? '#define USE_REVERSEDEPTHBUF' : '', '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 programLog = gl.getProgramInfoLog( program ).trim(); const vertexLog = gl.getShaderInfoLog( glVertexShader ).trim(); const fragmentLog = gl.getShaderInfoLog( glFragmentShader ).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 reverseDepthBuffer = 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, sizeAttenuation: material.sizeAttenuation === true, logarithmicDepthBuffer: logarithmicDepthBuffer, reverseDepthBuffer: reverseDepthBuffer, 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 ); 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.reverseDepthBuffer ) _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 }; } class MeshDepthMaterial extends Material { constructor( parameters ) { super(); this.isMeshDepthMaterial = true; this.type = 'MeshDepthMaterial'; this.depthPacking = BasicDepthPacking; this.map = null; this.alphaMap = null; this.displacementMap = null; this.displacementScale = 1; this.displacementBias = 0; this.wireframe = false; 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; } } class MeshDistanceMaterial extends Material { constructor( parameters ) { super(); this.isMeshDistanceMaterial = true; this.type = 'MeshDistanceMaterial'; this.map = null; this.alphaMap = null; this.displacementMap = null; this.displacementScale = 1; 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 \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 ); _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 ) ) { // 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.alphaTest; 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 reversed = false; let currentDepthMask = null; let currentDepthFunc = null; let currentDepthClear = null; return { setReversed: function ( value ) { if ( reversed !== value ) { 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 ); } const oldDepth = currentDepthClear; currentDepthClear = null; this.setClear( oldDepth ); } reversed = value; }, getReversed: function () { return reversed; }, 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 ( reversed ) 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 ( reversed ) { depth = 1 - depth; } gl.clearDepth( depth ); currentDepthClear = depth; } }, reset: function () { locked = false; currentDepthMask = null; currentDepthFunc = null; currentDepthClear = null; reversed = 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.ZERO, gl.SRC_COLOR, gl.ZERO, gl.SRC_ALPHA ); 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.blendFunc( gl.SRC_ALPHA, gl.ONE ); break; case SubtractiveBlending: gl.blendFuncSeparate( gl.ZERO, gl.ONE_MINUS_SRC_COLOR, gl.ZERO, gl.ONE ); break; case MultiplyBlending: gl.blendFunc( gl.ZERO, gl.SRC_COLOR ); 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 LuminanceFormat: return width * height; case LuminanceAlphaFormat: return width * height * 2; 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: 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 ( 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.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; } } state.bindTexture( _gl.TEXTURE_2D, textureProperties.__webglTexture, _gl.TEXTURE0 + slot ); } function setTexture2DArray( texture, slot ) { const textureProperties = properties.get( texture ); if ( 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.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 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 ) { state.texSubImage2D( _gl.TEXTURE_2D, 0, 0, 0, image.width, image.height, glFormat, glType, image.data ); } } 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' ); 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 { 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 ); state.bindTexture( _gl.TEXTURE_2D, attachmentProperties.__webglTexture ); setTextureParameters( _gl.TEXTURE_2D, attachment ); setupFrameBufferTexture( renderTargetProperties.__webglFramebuffer, renderTarget, attachment, _gl.COLOR_ATTACHMENT0 + i, _gl.TEXTURE_2D, 0 ); if ( textureNeedsGenerateMipmaps( attachment ) ) { generateMipmap( _gl.TEXTURE_2D ); } } 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 ); 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 === 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 === LuminanceFormat ) return gl.LUMINANCE; if ( p === LuminanceAlphaFormat ) return gl.LUMINANCE_ALPHA; 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 === RGBA_BPTC_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} [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; /** * An array of perspective sub cameras. * * @type {Array} */ this.cameras = array; this.index = 0; } } /** * 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 WebXRController { constructor() { this._targetRay = null; this._grip = null; this._hand = null; } 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; } 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; } 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; } 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; } 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; } 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; } 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; } // private method _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 ]; } } 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; } }`; class WebXRDepthSensing { constructor() { this.texture = null; this.mesh = null; this.depthNear = 0; this.depthFar = 0; } init( renderer, depthData, renderState ) { if ( this.texture === null ) { const texture = new Texture(); const texProps = renderer.properties.get( texture ); texProps.__webglTexture = depthData.texture; if ( ( depthData.depthNear !== renderState.depthNear ) || ( depthData.depthFar !== renderState.depthFar ) ) { this.depthNear = depthData.depthNear; this.depthFar = depthData.depthFar; } this.texture = texture; } } 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; } reset() { this.texture = null; this.mesh = null; } getDepthTexture() { return this.texture; } } class WebXRManager extends EventDispatcher { 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 depthSensing = new WebXRDepthSensing(); 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; // this.cameraAutoUpdate = true; this.enabled = false; this.isPresenting = false; this.getController = function ( index ) { let controller = controllers[ index ]; if ( controller === undefined ) { controller = new WebXRController(); controllers[ index ] = controller; } return controller.getTargetRaySpace(); }; this.getControllerGrip = function ( index ) { let controller = controllers[ index ]; if ( controller === undefined ) { controller = new WebXRController(); controllers[ index ] = controller; } return controller.getGripSpace(); }; 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(); // 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' } ); } this.setFramebufferScaleFactor = function ( value ) { framebufferScaleFactor = value; if ( scope.isPresenting === true ) { console.warn( 'THREE.WebXRManager: Cannot change framebuffer scale while presenting.' ); } }; this.setReferenceSpaceType = function ( value ) { referenceSpaceType = value; if ( scope.isPresenting === true ) { console.warn( 'THREE.WebXRManager: Cannot change reference space type while presenting.' ); } }; this.getReferenceSpace = function () { return customReferenceSpace || referenceSpace; }; this.setReferenceSpace = function ( space ) { customReferenceSpace = space; }; this.getBaseLayer = function () { return glProjLayer !== null ? glProjLayer : glBaseLayer; }; this.getBinding = function () { return glBinding; }; this.getFrame = function () { return xrFrame; }; this.getSession = function () { return session; }; 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 useLayers = typeof XRWebGLBinding !== 'undefined' && 'createProjectionLayer' in XRWebGLBinding.prototype; if ( ! useLayers ) { 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 = new XRWebGLBinding( session, gl ); 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' } ); } }; this.getEnvironmentBlendMode = function () { if ( session !== null ) { return session.environmentBlendMode; } }; 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(); } 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; } cameraL.layers.mask = camera.layers.mask | 0b010; cameraR.layers.mask = camera.layers.mask | 0b100; cameraXR.layers.mask = cameraL.layers.mask | cameraR.layers.mask; 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; } } this.getCamera = function () { return cameraXR; }; this.getFoveation = function () { if ( glProjLayer === null && glBaseLayer === null ) { return undefined; } return foveation; }; 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; } }; this.hasDepthSensing = function () { return depthSensing.texture !== null; }; this.getDepthSensingMesh = function () { return depthSensing.getMesh( cameraXR ); }; // 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, glProjLayer.ignoreDepthValues ? undefined : 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 && glBinding ) { const depthData = glBinding.getDepthInformation( views[ 0 ] ); if ( depthData && depthData.isValid && depthData.texture ) { depthSensing.init( renderer, depthData, session.renderState ); } } } // 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 }; } class WebGLRenderer { constructor( parameters = {} ) { const { canvas = createCanvasElement(), context = null, depth = true, stencil = false, alpha = false, antialias = false, premultipliedAlpha = true, preserveDrawingBuffer = false, powerPreference = 'default', failIfMajorPerformanceCaveat = false, reverseDepthBuffer = false, } = parameters; 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 this.domElement = canvas; // Debug configuration container 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 this.autoClear = true; this.autoClearColor = true; this.autoClearDepth = true; this.autoClearStencil = true; // scene graph this.sortObjects = true; // user-defined clipping this.clippingPlanes = []; this.localClippingEnabled = false; // physically based shading this._outputColorSpace = SRGBColorSpace; // tone mapping this.toneMapping = NoToneMapping; this.toneMappingExposure = 1.0; // internal properties const _this = this; let _isContextLost = false; // internal state cache 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; // transmission render target scale this.transmissionResolutionScale = 1.0; // camera matrices cache const _currentProjectionMatrix = new Matrix4(); 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.reverseDepthBuffer && reverseDepthBuffer ) { 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; _this.capabilities = capabilities; _this.extensions = extensions; _this.properties = properties; _this.renderLists = renderLists; _this.shadowMap = shadowMap; _this.state = state; _this.info = info; } initGLContext(); // xr const xr = new WebXRManager( _this, _gl ); this.xr = xr; // API this.getContext = function () { return _gl; }; this.getContextAttributes = function () { return _gl.getContextAttributes(); }; this.forceContextLoss = function () { const extension = extensions.get( 'WEBGL_lose_context' ); if ( extension ) extension.loseContext(); }; this.forceContextRestore = function () { const extension = extensions.get( 'WEBGL_lose_context' ); if ( extension ) extension.restoreContext(); }; this.getPixelRatio = function () { return _pixelRatio; }; this.setPixelRatio = function ( value ) { if ( value === undefined ) return; _pixelRatio = value; this.setSize( _width, _height, false ); }; this.getSize = function ( target ) { return target.set( _width, _height ); }; 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 ); }; this.getDrawingBufferSize = function ( target ) { return target.set( _width * _pixelRatio, _height * _pixelRatio ).floor(); }; 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 ); }; this.getCurrentViewport = function ( target ) { return target.copy( _currentViewport ); }; this.getViewport = function ( target ) { return target.copy( _viewport ); }; 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() ); }; this.getScissor = function ( target ) { return target.copy( _scissor ); }; 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() ); }; this.getScissorTest = function () { return _scissorTest; }; this.setScissorTest = function ( boolean ) { state.setScissorTest( _scissorTest = boolean ); }; this.setOpaqueSort = function ( method ) { _opaqueSort = method; }; this.setTransparentSort = function ( method ) { _transparentSort = method; }; // Clearing this.getClearColor = function ( target ) { return target.copy( background.getClearColor() ); }; this.setClearColor = function () { background.setClearColor( ...arguments ); }; this.getClearAlpha = function () { return background.getClearAlpha(); }; this.setClearAlpha = function () { background.setClearAlpha( ...arguments ); }; 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 ); }; this.clearColor = function () { this.clear( true, false, false ); }; this.clearDepth = function () { this.clear( false, true, false ); }; this.clearStencil = function () { this.clear( false, false, true ); }; // 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 ); } } 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 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 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 ); _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(); _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 ); _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 material = overrideMaterial === null ? renderItem.material : overrideMaterial; const group = renderItem.group; 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 reverseDepthBuffer = state.buffers.depth.getReversed(); if ( reverseDepthBuffer ) { _currentProjectionMatrix.copy( camera.projectionMatrix ); toNormalizedProjectionMatrix( _currentProjectionMatrix ); toReversedProjectionMatrix( _currentProjectionMatrix ); p_uniforms.setValue( _gl, 'projectionMatrix', _currentProjectionMatrix ); } else { 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 ); } this.getActiveCubeFace = function () { return _currentActiveCubeFace; }; this.getActiveMipmapLevel = function () { return _currentActiveMipmapLevel; }; this.getRenderTarget = function () { return _currentRenderTarget; }; this.setRenderTargetTextures = function ( renderTarget, colorTexture, depthTexture ) { properties.get( renderTarget.texture ).__webglTexture = colorTexture; properties.get( renderTarget.depthTexture ).__webglTexture = depthTexture; const renderTargetProperties = properties.get( renderTarget ); renderTargetProperties.__hasExternalTextures = true; renderTargetProperties.__autoAllocateDepthBuffer = depthTexture === undefined; if ( ! renderTargetProperties.__autoAllocateDepthBuffer ) { // 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. if ( extensions.has( 'WEBGL_multisampled_render_to_texture' ) === true ) { console.warn( 'THREE.WebGLRenderer: Render-to-texture extension was disabled because an external texture was provided' ); renderTargetProperties.__useRenderToTexture = false; } } }; this.setRenderTargetFramebuffer = function ( renderTarget, defaultFramebuffer ) { const renderTargetProperties = properties.get( renderTarget ); renderTargetProperties.__webglFramebuffer = defaultFramebuffer; renderTargetProperties.__useDefaultFramebuffer = defaultFramebuffer === undefined; }; const _scratchFrameBuffer = _gl.createFramebuffer(); 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 textureProperties = properties.get( renderTarget.texture ); const layer = activeCubeFace; _gl.framebufferTextureLayer( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, 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 }; this.readRenderTargetPixels = function ( renderTarget, x, y, width, height, buffer, activeCubeFaceIndex ) { 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.texture; 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 ) ) ) { _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 ); } } }; this.readRenderTargetPixelsAsync = async function ( renderTarget, x, y, width, height, buffer, activeCubeFaceIndex ) { 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 ) { const texture = renderTarget.texture; 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.' ); } // 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 glBuffer = _gl.createBuffer(); _gl.bindBuffer( _gl.PIXEL_PACK_BUFFER, glBuffer ); _gl.bufferData( _gl.PIXEL_PACK_BUFFER, buffer.byteLength, _gl.STREAM_READ ); _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.' ); } } }; this.copyFramebufferToTexture = function ( texture, position = null, level = 0 ) { // support previous signature with position first if ( texture.isTexture !== true ) { // @deprecated, r165 warnOnce( 'WebGLRenderer: copyFramebufferToTexture function signature has changed.' ); position = arguments[ 0 ] || null; texture = arguments[ 1 ]; } 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(); this.copyTextureToTexture = function ( srcTexture, dstTexture, srcRegion = null, dstPosition = null, srcLevel = 0, dstLevel = null ) { // support previous signature with dstPosition first if ( srcTexture.isTexture !== true ) { // @deprecated, r165 warnOnce( 'WebGLRenderer: copyTextureToTexture function signature has changed.' ); dstPosition = arguments[ 0 ] || null; srcTexture = arguments[ 1 ]; dstTexture = arguments[ 2 ]; dstLevel = arguments[ 3 ] || 0; srcRegion = 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(); }; this.copyTextureToTexture3D = function ( srcTexture, dstTexture, srcRegion = null, dstPosition = null, level = 0 ) { // support previous signature with source box first if ( srcTexture.isTexture !== true ) { // @deprecated, r165 warnOnce( 'WebGLRenderer: copyTextureToTexture3D function signature has changed.' ); srcRegion = arguments[ 0 ] || null; dstPosition = arguments[ 1 ] || null; srcTexture = arguments[ 2 ]; dstTexture = arguments[ 3 ]; level = arguments[ 4 ] || 0; } // @deprecated, r170 warnOnce( 'WebGLRenderer: copyTextureToTexture3D function has been deprecated. Use "copyTextureToTexture" instead.' ); return this.copyTextureToTexture( srcTexture, dstTexture, srcRegion, dstPosition, level ); }; this.initRenderTarget = function ( target ) { if ( properties.get( target ).__webglFramebuffer === undefined ) { textures.setupRenderTarget( target ); } }; 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(); }; 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 } ) ); } } get coordinateSystem() { return WebGLCoordinateSystem; } 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. * * @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; } } class InstancedBufferAttribute extends BufferAttribute { constructor( array, itemSize, normalized, meshPerAttribute = 1 ) { super( array, itemSize, normalized ); this.isInstancedBufferAttribute = true; 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; } } class DataTexture extends Texture { 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.isDataTexture = true; this.image = { data: data, width: width, height: height }; this.generateMipmaps = false; this.flipY = false; 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] - 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 {?InstancedBufferAttribute} * @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 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; } } } class LineBasicMaterial extends Material { constructor( parameters ) { super(); this.isLineBasicMaterial = true; this.type = 'LineBasicMaterial'; this.color = new Color( 0xffffff ); this.map = null; this.linewidth = 1; this.linecap = 'round'; this.linejoin = 'round'; 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] - 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} * @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|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|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} 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] - 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; } } class PointsMaterial extends Material { constructor( parameters ) { super(); this.isPointsMaterial = true; this.type = 'PointsMaterial'; this.color = new Color( 0xffffff ); this.map = null; this.alphaMap = null; this.size = 1; this.sizeAttenuation = 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] - 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} * @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|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|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} 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 } ); } } class CanvasTexture extends Texture { constructor( canvas, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) { super( canvas, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ); this.isCanvasTexture = true; this.needsUpdate = true; } } /** * 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} * @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} 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, normals: Array, binormals: Array}} 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.6, 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} [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} */ 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} [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} */ 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} */ 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 } 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} [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} 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} 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 - The x coordinate of the center of the arc offsetted from the previous curve. * @param {number} aY - The y coordinate of the center of the arc offsetted from the previous curve. * @param {number} aRadius - The radius of the arc. * @param {number} aStartAngle - The start angle in radians. * @param {number} aEndAngle - 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 - The x coordinate of the center of the arc. * @param {number} aY - The y coordinate of the center of the arc. * @param {number} aRadius - The radius of the arc. * @param {number} aStartAngle - The start angle in radians. * @param {number} aEndAngle - 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 - The x coordinate of the center of the ellipse offsetted from the previous curve. * @param {number} aY - The y coordinate of the center of the ellipse offsetted from the previous curve. * @param {number} xRadius - The radius of the ellipse in the x axis. * @param {number} yRadius - The radius of the ellipse in the y axis. * @param {number} aStartAngle - The start angle in radians. * @param {number} aEndAngle - The end angle in radians. * @param {boolean} [aClockwise=false] - Whether to sweep the ellipse clockwise or not. * @param {boolean} [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 - The x coordinate of the absolute center of the ellipse. * @param {number} aY - The y coordinate of the absolute center of the ellipse. * @param {number} xRadius - The radius of the ellipse in the x axis. * @param {number} yRadius - The radius of the ellipse in the y axis. * @param {number} aStartAngle - The start angle in radians. * @param {number} aEndAngle - 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; } }; /** * 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 ); } } /** * 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} [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} * @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>} 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,holes:Array>}} 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; } } /** * An implementation of the earcut polygon triangulation algorithm. The code * is a port of [mapbox/earcut]{@link https://github.com/mapbox/earcut mapbox/earcut} (v2.2.4). * * @hideconstructor */ class Earcut { /** * Triangulates the given shape definition by returning an array of triangles. * * @param {Array} data - An array with 2D points. * @param {Array} holeIndices - An array with indices defining holes. * @param {number} [dim=2] - The number of coordinates per vertex in the input array. * @return {Array} An array representing the triangulated faces. Each face is defined by three consecutive numbers * representing vertex indices. */ static triangulate( 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, maxX, maxY, x, y, 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 = maxX = data[ 0 ]; minY = maxY = data[ 1 ]; for ( let i = dim; i < outerLen; i += dim ) { x = data[ i ]; 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 i, last; if ( clockwise === ( signedArea( data, start, end, dim ) > 0 ) ) { for ( i = start; i < end; i += dim ) last = insertNode( i, data[ i ], data[ i + 1 ], last ); } else { for ( i = end - dim; i >= start; i -= dim ) last = insertNode( i, 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, prev, next; // iterate through ears, slicing them one by one while ( ear.prev !== ear.next ) { prev = ear.prev; next = ear.next; if ( invSize ? isEarHashed( ear, minX, minY, invSize ) : isEar( ear ) ) { // cut off the triangle triangles.push( prev.i / dim | 0 ); triangles.push( ear.i / dim | 0 ); triangles.push( next.i / dim | 0 ); 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, dim ); 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; min & max are calculated like this for speed const x0 = ax < bx ? ( ax < cx ? ax : cx ) : ( bx < cx ? bx : cx ), y0 = ay < by ? ( ay < cy ? ay : cy ) : ( by < cy ? by : cy ), x1 = ax > bx ? ( ax > cx ? ax : cx ) : ( bx > cx ? bx : cx ), y1 = ay > by ? ( ay > cy ? ay : cy ) : ( by > cy ? by : cy ); let p = c.next; while ( p !== a ) { if ( p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && pointInTriangle( 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; min & max are calculated like this for speed const x0 = ax < bx ? ( ax < cx ? ax : cx ) : ( bx < cx ? bx : cx ), y0 = ay < by ? ( ay < cy ? ay : cy ) : ( by < cy ? by : cy ), x1 = ax > bx ? ( ax > cx ? ax : cx ) : ( bx > cx ? bx : cx ), y1 = ay > by ? ( ay > cy ? ay : cy ) : ( by > cy ? 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 && pointInTriangle( 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 && pointInTriangle( 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 && pointInTriangle( 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 && pointInTriangle( 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, dim ) { 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 / dim | 0 ); triangles.push( p.i / dim | 0 ); triangles.push( b.i / dim | 0 ); // 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 = []; let i, len, start, end, list; for ( i = 0, len = holeIndices.length; i < len; i ++ ) { start = holeIndices[ i ] * dim; end = i < len - 1 ? holeIndices[ i + 1 ] * dim : data.length; list = linkedList( data, start, end, dim, false ); if ( list === list.next ) list.steiner = true; queue.push( getLeftmost( list ) ); } queue.sort( compareX ); // process holes from left to right for ( i = 0; i < queue.length; i ++ ) { outerNode = eliminateHole( queue[ i ], outerNode ); } return outerNode; } function compareX( a, b ) { return a.x - b.x; } // find a bridge between vertices that connects hole with an outer ring 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, qx = - Infinity, m; const hx = hole.x, hy = hole.y; // 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 do { 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, mx = m.x, my = m.y; let tanMin = Infinity, tan; 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 ) ) { 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 i, p, q, e, tail, numMerges, pSize, qSize, inSize = 1; do { p = list; list = null; tail = null; numMerges = 0; while ( p ) { numMerges ++; q = p; pSize = 0; for ( i = 0; i < inSize; i ++ ) { pSize ++; q = q.nextZ; if ( ! q ) break; } 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 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, inside = false; const px = ( a.x + b.x ) / 2, 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 = new Node$1( a.i, a.x, a.y ), b2 = new Node$1( 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 = new Node$1( 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 Node$1( i, x, y ) { // vertex index in coordinates array this.i = i; // vertex coordinates this.x = x; this.y = y; // previous and next vertex nodes in a polygon ring this.prev = null; this.next = null; // z-order curve value this.z = 0; // previous and next nodes in z-order this.prevZ = null; this.nextZ = null; // indicates whether this is a steiner point this.steiner = false; } 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; } /** * A class containing utility functions for shapes. * * @hideconstructor */ class ShapeUtils { /** * Calculate area of a ( 2D ) contour polygon. * * @param {Array} 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} 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} contour - An array of 2D points defining the contour. * @param {Array>} holes - An array that holds arrays of 2D points defining the holes. * @return {Array>} 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. * * parameters = { * * curveSegments: , // number of points on the curves * steps: , // number of points for z-side extrusions / used for subdividing segments of extrude spline too * depth: , // Depth to extrude the shape * * bevelEnabled: , // turn on bevel * bevelThickness: , // how deep into the original shape bevel goes * bevelSize: , // how far from shape outline (including bevelOffset) is bevel * bevelOffset: , // how far from shape outline does bevel start * bevelSegments: , // number of bevel layers * * extrudePath: // curve to extrude shape along * * UVGenerator: // object that provides UV generator functions * * } */ /** * 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} [shapes] - A shape or an array of shapes. * @param {Object} [options={}] - The extrude settings. * @param {number} [options.curveSegments=12] - Number of points on the curves. * @param {number} [options.steps=1] - Number of points used for subdividing segments along the depth of the extruded spline. * @param {number} [options.depth=1] - Depth to extrude the shape. * @param {boolean} [options.bevelEnabled=true] - Whether to beveling to the shape or not. * @param {number} [options.bevelThickness=0.2] - How deep into the original shape the bevel goes. * @param {number} [options.bevelSize=bevelThickness-0.1] - Distance from the shape outline that the bevel extends. * @param {number} [options.bevelOffset=0] - Distance from the shape outline that the bevel starts. * @param {number} [options.bevelSegments=3] - Number of bevel layers. * @param {Curve} [options.extrudePath=3] - A 3D spline path along which the shape should be extruded. Bevels not supported for path extrusion. * @param {Object} [options.UVGenerator] - An object that provides UV generator functions for custom UV generation. */ 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(); } } } const faces = ShapeUtils.triangulateShape( vertices, holes ); /* Vertices */ const contour = vertices; // vertices has all points but contour has only points of circumference for ( let h = 0, hl = holes.length; h < hl; 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, flen = faces.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 = holes.length; 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 ); } // 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 ); } // 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 ); v( vert.x, vert.y, - z ); } } } 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} 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 ); } } class MeshStandardMaterial extends Material { constructor( parameters ) { super(); this.isMeshStandardMaterial = true; this.type = 'MeshStandardMaterial'; this.defines = { 'STANDARD': '' }; this.color = new Color( 0xffffff ); // diffuse this.roughness = 1.0; this.metalness = 0.0; this.map = null; this.lightMap = null; this.lightMapIntensity = 1.0; this.aoMap = null; this.aoMapIntensity = 1.0; this.emissive = new Color( 0x000000 ); this.emissiveIntensity = 1.0; this.emissiveMap = null; this.bumpMap = null; this.bumpScale = 1; this.normalMap = null; this.normalMapType = TangentSpaceNormalMap; this.normalScale = new Vector2( 1, 1 ); this.displacementMap = null; this.displacementScale = 1; this.displacementBias = 0; this.roughnessMap = null; this.metalnessMap = null; this.alphaMap = null; this.envMap = null; this.envMapRotation = new Euler(); this.envMapIntensity = 1.0; this.wireframe = false; this.wireframeLinewidth = 1; this.wireframeLinecap = 'round'; this.wireframeLinejoin = 'round'; this.flatShading = false; 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; } } class MeshPhysicalMaterial extends MeshStandardMaterial { constructor( parameters ) { super(); this.isMeshPhysicalMaterial = true; this.defines = { 'STANDARD': '', 'PHYSICAL': '' }; this.type = 'MeshPhysicalMaterial'; this.anisotropyRotation = 0; this.anisotropyMap = null; this.clearcoatMap = null; this.clearcoatRoughness = 0.0; this.clearcoatRoughnessMap = null; this.clearcoatNormalScale = new Vector2( 1, 1 ); this.clearcoatNormalMap = null; this.ior = 1.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 ); } } ); this.iridescenceMap = null; this.iridescenceIOR = 1.3; this.iridescenceThicknessRange = [ 100, 400 ]; this.iridescenceThicknessMap = null; this.sheenColor = new Color( 0x000000 ); this.sheenColorMap = null; this.sheenRoughness = 1.0; this.sheenRoughnessMap = null; this.transmissionMap = null; this.thickness = 0; this.thicknessMap = null; this.attenuationDistance = Infinity; this.attenuationColor = new Color( 1, 1, 1 ); this.specularIntensity = 1.0; this.specularIntensityMap = null; this.specularColor = new Color( 1, 1, 1 ); 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 ); } get anisotropy() { return this._anisotropy; } set anisotropy( value ) { if ( this._anisotropy > 0 !== value > 0 ) { this.version ++; } this._anisotropy = value; } get clearcoat() { return this._clearcoat; } set clearcoat( value ) { if ( this._clearcoat > 0 !== value > 0 ) { this.version ++; } this._clearcoat = value; } get iridescence() { return this._iridescence; } set iridescence( value ) { if ( this._iridescence > 0 !== value > 0 ) { this.version ++; } this._iridescence = value; } get dispersion() { return this._dispersion; } set dispersion( value ) { if ( this._dispersion > 0 !== value > 0 ) { this.version ++; } this._dispersion = value; } get sheen() { return this._sheen; } set sheen( value ) { if ( this._sheen > 0 !== value > 0 ) { this.version ++; } this._sheen = value; } 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; } } class MeshPhongMaterial extends Material { constructor( parameters ) { super(); this.isMeshPhongMaterial = true; this.type = 'MeshPhongMaterial'; this.color = new Color( 0xffffff ); // diffuse this.specular = new Color( 0x111111 ); this.shininess = 30; this.map = null; this.lightMap = null; this.lightMapIntensity = 1.0; this.aoMap = null; this.aoMapIntensity = 1.0; this.emissive = new Color( 0x000000 ); this.emissiveIntensity = 1.0; this.emissiveMap = null; this.bumpMap = null; this.bumpScale = 1; this.normalMap = null; this.normalMapType = TangentSpaceNormalMap; this.normalScale = new Vector2( 1, 1 ); this.displacementMap = null; this.displacementScale = 1; this.displacementBias = 0; this.specularMap = null; this.alphaMap = null; this.envMap = null; this.envMapRotation = new Euler(); this.combine = MultiplyOperation; this.reflectivity = 1; this.refractionRatio = 0.98; this.wireframe = false; this.wireframeLinewidth = 1; this.wireframeLinecap = 'round'; this.wireframeLinejoin = 'round'; this.flatShading = false; 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; } } class MeshToonMaterial extends Material { constructor( parameters ) { super(); this.isMeshToonMaterial = true; this.defines = { 'TOON': '' }; this.type = 'MeshToonMaterial'; this.color = new Color( 0xffffff ); this.map = null; this.gradientMap = null; this.lightMap = null; this.lightMapIntensity = 1.0; this.aoMap = null; this.aoMapIntensity = 1.0; this.emissive = new Color( 0x000000 ); this.emissiveIntensity = 1.0; this.emissiveMap = null; this.bumpMap = null; this.bumpScale = 1; this.normalMap = null; this.normalMapType = TangentSpaceNormalMap; this.normalScale = new Vector2( 1, 1 ); this.displacementMap = null; this.displacementScale = 1; this.displacementBias = 0; this.alphaMap = null; this.wireframe = false; this.wireframeLinewidth = 1; this.wireframeLinecap = 'round'; this.wireframeLinejoin = 'round'; 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; } } class MeshNormalMaterial extends Material { constructor( parameters ) { super(); this.isMeshNormalMaterial = true; this.type = 'MeshNormalMaterial'; this.bumpMap = null; this.bumpScale = 1; this.normalMap = null; this.normalMapType = TangentSpaceNormalMap; this.normalScale = new Vector2( 1, 1 ); this.displacementMap = null; this.displacementScale = 1; this.displacementBias = 0; this.wireframe = false; this.wireframeLinewidth = 1; 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; } } class MeshLambertMaterial extends Material { constructor( parameters ) { super(); this.isMeshLambertMaterial = true; this.type = 'MeshLambertMaterial'; this.color = new Color( 0xffffff ); // diffuse this.map = null; this.lightMap = null; this.lightMapIntensity = 1.0; this.aoMap = null; this.aoMapIntensity = 1.0; this.emissive = new Color( 0x000000 ); this.emissiveIntensity = 1.0; this.emissiveMap = null; this.bumpMap = null; this.bumpScale = 1; this.normalMap = null; this.normalMapType = TangentSpaceNormalMap; this.normalScale = new Vector2( 1, 1 ); this.displacementMap = null; this.displacementScale = 1; this.displacementBias = 0; this.specularMap = null; this.alphaMap = null; this.envMap = null; this.envMapRotation = new Euler(); this.combine = MultiplyOperation; this.reflectivity = 1; this.refractionRatio = 0.98; this.wireframe = false; this.wireframeLinewidth = 1; this.wireframeLinecap = 'round'; this.wireframeLinejoin = 'round'; this.flatShading = false; 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; } } class MeshMatcapMaterial extends Material { constructor( parameters ) { super(); this.isMeshMatcapMaterial = true; this.defines = { 'MATCAP': '' }; this.type = 'MeshMatcapMaterial'; this.color = new Color( 0xffffff ); // diffuse this.matcap = null; this.map = null; this.bumpMap = null; this.bumpScale = 1; this.normalMap = null; this.normalMapType = TangentSpaceNormalMap; this.normalScale = new Vector2( 1, 1 ); this.displacementMap = null; this.displacementScale = 1; this.displacementBias = 0; this.alphaMap = null; this.flatShading = false; 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; } } class LineDashedMaterial extends LineBasicMaterial { constructor( parameters ) { super(); this.isLineDashedMaterial = true; this.type = 'LineDashedMaterial'; this.scale = 1; this.dashSize = 3; 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; } } const Cache = { enabled: false, files: {}, add: function ( key, file ) { if ( this.enabled === false ) return; // console.log( 'THREE.Cache', 'Adding key:', key ); this.files[ key ] = file; }, get: function ( key ) { if ( this.enabled === false ) return; // console.log( 'THREE.Cache', 'Checking key:', key ); return this.files[ key ]; }, remove: function ( key ) { delete this.files[ key ]; }, clear: function () { this.files = {}; } }; class LoadingManager { 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 this.onStart = undefined; this.onLoad = onLoad; this.onProgress = onProgress; this.onError = onError; this.itemStart = function ( url ) { itemsTotal ++; if ( isLoading === false ) { if ( scope.onStart !== undefined ) { scope.onStart( url, itemsLoaded, itemsTotal ); } } isLoading = true; }; 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.itemError = function ( url ) { if ( scope.onError !== undefined ) { scope.onError( url ); } }; this.resolveURL = function ( url ) { if ( urlModifier ) { return urlModifier( url ); } return url; }; this.setURLModifier = function ( transform ) { urlModifier = transform; return this; }; this.addHandler = function ( regex, loader ) { handlers.push( regex, loader ); return this; }; this.removeHandler = function ( regex ) { const index = handlers.indexOf( regex ); if ( index !== -1 ) { handlers.splice( index, 2 ); } return this; }; 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; }; } } 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} */ this.requestHeader = {}; } /** * This method needs to be implemented by all concrete loaders. It holds the * logic for loading assets from the backend. * * @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. * * @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; } } /** * 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'; class ImageLoader extends Loader { constructor( manager ) { super( manager ); } 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( url ); if ( cached !== undefined ) { scope.manager.itemStart( url ); setTimeout( function () { if ( onLoad ) onLoad( cached ); scope.manager.itemEnd( url ); }, 0 ); return cached; } const image = createElementNS( 'img' ); function onImageLoad() { removeEventListeners(); Cache.add( url, this ); if ( onLoad ) onLoad( this ); scope.manager.itemEnd( url ); } function onImageError( event ) { removeEventListeners(); if ( onError ) onError( event ); 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; } scope.manager.itemStart( url ); image.src = url; return image; } } class TextureLoader extends Loader { constructor( manager ) { super( manager ); } 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 ligth 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 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 ); 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.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(); class Raycaster { constructor( origin, direction, near = 0, far = Infinity ) { this.ray = new Ray( origin, direction ); // direction is assumed to be normalized (for accurate distance calculations) this.near = near; this.far = far; this.camera = null; this.layers = new Layers(); this.params = { Mesh: {}, Line: { threshold: 1 }, LOD: {}, Points: { threshold: 1 }, Sprite: {} }; } set( origin, direction ) { // direction is assumed to be normalized (for accurate distance calculations) this.ray.set( origin, direction ); } 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 ); } } setFromXRController( controller ) { _matrix.identity().extractRotation( controller.matrixWorld ); this.ray.origin.setFromMatrixPosition( controller.matrixWorld ); this.ray.direction.set( 0, 0, -1 ).applyMatrix4( _matrix ); return this; } intersectObject( object, recursive = true, intersects = [] ) { intersect( object, this, intersects, recursive ); intersects.sort( ascSort ); return intersects; } 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 Clock { constructor( autoStart = true ) { this.autoStart = autoStart; this.startTime = 0; this.oldTime = 0; this.elapsedTime = 0; this.running = false; } start() { this.startTime = now(); this.oldTime = this.startTime; this.elapsedTime = 0; this.running = true; } stop() { this.getElapsedTime(); this.running = false; this.autoStart = false; } getElapsedTime() { this.getDelta(); return this.elapsedTime; } getDelta() { let diff = 0; if ( this.autoStart && ! this.running ) { this.start(); return 0; } if ( this.running ) { const newTime = now(); diff = ( newTime - this.oldTime ) / 1000; this.oldTime = newTime; this.elapsedTime += diff; } return diff; } } function now() { return performance.now(); } /** * 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 x value. * @param {number} z - The x 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} 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(); /** * 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 closets 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 ); } /** * 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} * @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} 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} 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. */ connect() {} /** * 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 */ /** * Text = 3D Text * * parameters = { * font: , // font * * size: , // size of the text * depth: , // thickness to extrude text * curveSegments: , // number of points on the curves * * bevelEnabled: , // turn on bevel * bevelThickness: , // how deep into text bevel goes * bevelSize: , // how far from text outline (including bevelOffset) is bevel * bevelOffset: // how far from text outline does bevel start * } */ class TextGeometry extends ExtrudeGeometry { 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 Font { constructor( data ) { this.isFont = true; this.type = 'Font'; this.data = data; } 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 }; } // OrbitControls performs orbiting, dollying (zooming), and panning. // Unlike 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 const _changeEvent = { type: 'change' }; const _startEvent = { type: 'start' }; 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; class OrbitControls extends Controls { constructor( object, domElement = null ) { super( object, domElement ); this.state = _STATE.NONE; // Set to false to disable this control this.enabled = true; // "target" sets the location of focus, where the object orbits around this.target = new Vector3(); // Sets the 3D cursor (similar to Blender), from which the maxTargetRadius takes effect this.cursor = new Vector3(); // How far you can dolly in and out ( PerspectiveCamera only ) this.minDistance = 0; this.maxDistance = Infinity; // How far you can zoom in and out ( OrthographicCamera only ) this.minZoom = 0; this.maxZoom = Infinity; // Limit camera target within a spherical area around the cursor this.minTargetRadius = 0; this.maxTargetRadius = Infinity; // How far you can orbit vertically, upper and lower limits. // Range is 0 to Math.PI radians. this.minPolarAngle = 0; // radians this.maxPolarAngle = Math.PI; // radians // How far you can orbit horizontally, upper and lower limits. // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) this.minAzimuthAngle = - Infinity; // radians this.maxAzimuthAngle = Infinity; // radians // Set to true to enable damping (inertia) // If damping is enabled, you must call controls.update() in your animation loop this.enableDamping = false; this.dampingFactor = 0.05; // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. // Set to false to disable zooming this.enableZoom = true; this.zoomSpeed = 1.0; // Set to false to disable rotating this.enableRotate = true; this.rotateSpeed = 1.0; this.keyRotateSpeed = 1.0; // Set to false to disable panning this.enablePan = true; this.panSpeed = 1.0; this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up this.keyPanSpeed = 7.0; // pixels moved per arrow key push this.zoomToCursor = false; // Set to true to automatically rotate around the target // If auto-rotate is enabled, you must call controls.update() in your animation loop this.autoRotate = false; this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 // The four arrow keys this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; // Mouse buttons this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; // Touch fingers this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; // for reset this.target0 = this.target.clone(); this.position0 = this.object.position.clone(); 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.update(); } connect() { 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(); } getPolarAngle() { return this._spherical.phi; } getAzimuthalAngle() { return this._spherical.theta; } getDistance() { return this.object.position.distanceTo( this.target ); } listenToKeyEvents( domElement ) { domElement.addEventListener( 'keydown', this._onKeyDown ); this._domElementKeyEvents = domElement; } stopListenToKeyEvents() { if ( this._domElementKeyEvents !== null ) { this._domElementKeyEvents.removeEventListener( 'keydown', this._onKeyDown ); this._domElementKeyEvents = null; } } saveState() { this.target0.copy( this.target ); this.position0.copy( this.object.position ); this.zoom0 = this.object.zoom; } 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 } ); } } /** * Full-screen textured quad 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; }` }; class Pass { constructor() { this.isPass = true; // if set to true, the pass is processed by the composer this.enabled = true; // if set to true, the pass indicates to swap read and write buffer after rendering this.needsSwap = true; // if set to true, the pass clears its buffer before rendering this.clear = false; // if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer. this.renderToScreen = false; } setSize( /* width, height */ ) {} render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) { console.error( 'THREE.Pass: .render() must be implemented in derived pass.' ); } 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(); class FullScreenQuad { constructor( material ) { this._mesh = new Mesh( _geometry, material ); } dispose() { this._mesh.geometry.dispose(); } render( renderer ) { renderer.render( this._mesh, _camera ); } get material() { return this._mesh.material; } set material( value ) { this._mesh.material = value; } } class ShaderPass extends Pass { constructor( shader, textureID ) { super(); this.textureID = ( textureID !== undefined ) ? textureID : 'tDiffuse'; 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 } ); } this.fsQuad = new FullScreenQuad( this.material ); } 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 ); } } dispose() { this.material.dispose(); this.fsQuad.dispose(); } } class MaskPass extends Pass { constructor( scene, camera ) { super(); this.scene = scene; this.camera = camera; this.clear = true; this.needsSwap = false; this.inverse = false; } 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 ); } } class ClearMaskPass extends Pass { constructor() { super(); this.needsSwap = false; } render( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) { renderer.state.buffers.stencil.setLocked( false ); renderer.state.buffers.stencil.setTest( false ); } } class EffectComposer { constructor( renderer, renderTarget ) { 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'; this.writeBuffer = this.renderTarget1; this.readBuffer = this.renderTarget2; this.renderToScreen = true; this.passes = []; this.copyPass = new ShaderPass( CopyShader ); this.copyPass.material.blending = NoBlending; this.clock = new Clock(); } swapBuffers() { const tmp = this.readBuffer; this.readBuffer = this.writeBuffer; this.writeBuffer = tmp; } addPass( pass ) { this.passes.push( pass ); pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio ); } insertPass( pass, index ) { this.passes.splice( index, 0, pass ); pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio ); } removePass( pass ) { const index = this.passes.indexOf( pass ); if ( index !== -1 ) { this.passes.splice( index, 1 ); } } isLastEnabledPass( passIndex ) { for ( let i = passIndex + 1; i < this.passes.length; i ++ ) { if ( this.passes[ i ].enabled ) { return false; } } return true; } 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 ); } 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; } 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 ); } } setPixelRatio( pixelRatio ) { this._pixelRatio = pixelRatio; this.setSize( this._width, this._height ); } dispose() { this.renderTarget1.dispose(); this.renderTarget2.dispose(); this.copyPass.dispose(); } } class RenderPass extends Pass { constructor( scene, camera, overrideMaterial = null, clearColor = null, clearAlpha = null ) { super(); this.scene = scene; this.camera = camera; this.overrideMaterial = overrideMaterial; this.clearColor = clearColor; this.clearAlpha = clearAlpha; this.clear = true; this.clearDepth = false; this.needsSwap = false; this._oldClearColor = new Color(); } 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; } } /** * Luminosity * http://en.wikipedia.org/wiki/Luminosity */ 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 ); }` }; /** * UnrealBloomPass 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. * * Reference: * - https://docs.unrealengine.com/latest/INT/Engine/Rendering/PostProcessEffects/Bloom/ */ class UnrealBloomPass extends Pass { constructor( resolution, strength, radius, threshold ) { super(); this.strength = ( strength !== undefined ) ? strength : 1; this.radius = radius; this.threshold = threshold; this.resolution = ( resolution !== undefined ) ? new Vector2( resolution.x, resolution.y ) : new Vector2( 256, 256 ); // create color only once here, reuse it later inside the render function this.clearColor = new Color( 0, 0, 0 ); // 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 const copyShader = CopyShader; 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.enabled = true; this.needsSwap = false; this._oldClearColor = new Color(); this.oldClearAlpha = 1; this.basic = new MeshBasicMaterial(); this.fsQuad = new FullScreenQuad( null ); } 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(); } 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 ); } } 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; } 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 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; } } // class 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 ); } 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; } } } } } class SVGRenderer { 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' ); this.domElement = _svg; this.autoClear = true; this.sortObjects = true; this.sortElements = true; this.overdraw = 0.5; this.outputColorSpace = SRGBColorSpace; this.info = { render: { vertices: 0, faces: 0 } }; this.setQuality = function ( quality ) { switch ( quality ) { case 'high': _quality = 1; break; case 'low': _quality = 0; break; } }; this.setClearColor = function ( color ) { _clearColor.set( color ); }; this.setPixelRatio = function () {}; 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 ); }; this.getSize = function () { return { width: _svgWidth, height: _svgHeight }; }; 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; } this.clear = function () { removeChildNodes(); _svg.style.backgroundColor = _clearColor.getStyle( _this.outputColorSpace ); }; 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}; _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', 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 += ``; 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 `${wrap.accPath}`; }; 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] - preconfigured 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.getPadSvg(); if (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 availble 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 pad = this.getPadSvg(), clname = 'draw3d_' + (this.getPadName() || 'canvas'); if (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 = 0; rect.y = 0; rect.clname = clname; rect.can3d = -1; return rect; } const fp = this.getFramePainter(), pp = this.getPadPainter(); 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 = this.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(this.getPadSvg(), 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; if (chld && !chld.$jsroot) chld = chld.nextSibling; if (chld?.$jsroot) { 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 { if (this.getPadSvg().empty()) return; this.apply3dSize(size).remove(); this.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 !== null) { main.appendChild(canv); canv.painter = this; canv.$jsroot = true; // 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 { if (this.getPadSvg().empty()) return; // first hide normal frame this.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) { elem = this.getLayerSvg(size.clname); if (onlyget) return elem; const svg = this.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.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.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 */) { if (!v) return this.hide(); if (isObject(v) && (v.lines || v.line)) { if (v.only_status) return this.hide(); if (v.line) v = v.line; else { let res = v.lines[0]; for (let n = 1; n < v.lines.length; ++n) res += '
' + v.lines[n]; v = res; } } if (this.tt === null) { const doc = getDocument(); this.tt = doc.createElement('div'); this.tt.setAttribute('style', 'opacity: 1; filter: alpha(opacity=1); position: absolute; display: block; overflow: hidden; z-index: 101;'); this.cont = doc.createElement('div'); this.cont.setAttribute('style', 'display: block; padding: 2px 12px 3px 7px; margin-left: 5px; font-size: 11px; background: #777; color: #fff;'); this.tt.appendChild(this.cont); this.parent.appendChild(this.tt); } if (this.lastlbl !== v) { this.cont.innerHTML = v; this.lastlbl = v; this.tt.style.width = 'auto'; // let it be automatically resizing... } } /** @summary Hide tooltip */ hide() { if (this.tt !== null) this.parent.removeChild(this.tt); this.tt = null; this.lastlbl = ''; } } // 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(); // only left-button is considered // if ((evnt.button!==undefined) && (evnt.button !== 0)) return; // if ((evnt.buttons!==undefined) && (evnt.buttons !== 1)) return; 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, _painter = null; for (let i = 0; i < intersects.length; ++i) { if (intersects[i].object.tooltip) { tip = intersects[i].object.tooltip(intersects[i]); _painter = intersects[i].object.painter; break; } } if (tip && _painter) { return { obj: _painter.getObject(), name: _painter.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 fp = this.painter?.getFramePainter(); if (isFunc(fp?._dblclick_handler)) { const info = this.getInfoAtMousePosition(this.getMousePos(evnt, {})); if (info) { fp._dblclick_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 fp = this.painter?.getFramePainter(); if (isFunc(fp?._click_handler)) { const info = this.getInfoAtMousePosition(mouse_pos); if (info) { fp._click_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 (isFunc(this.painter?.getFramePainter()?._click_handler)) 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 } /** @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 = `` + ``+ '', 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); } /** * 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 < -709.782712893384) 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 < -709.782712893384 ) 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 < -709.782712893384 ) 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 < -709.782712893384 ) 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 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 === 0) || !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') return; window.close(); window.open('', '_self').close(); } /** @summary Tries to open ui5 * @private */ function tryOpenOpenUI(sources, args) { if (!sources || (sources.length === 0)) { 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, '://'); 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 || 'sap_belize'); 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.128.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, myDelay = delay || 300; if (myDelay < 20) myDelay = 20; 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 !== null) clearTimeout(myInterval); myInterval = setTimeout(ResizeTimer, myDelay); }); } /** @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.draw_g) return; if (painter.getPadPainter()?.isEditable() === false) enabled = false; if (!enabled) { if (painter.draw_g.property('assigned_move')) { const drag_move = drag().subject(Object); drag_move.on('start', null).on('drag', null).on('end', null); painter.draw_g .style('cursor', null) .property('assigned_move', null) .call(drag_move); } return; } if (painter.draw_g.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) return; if (detectRightButton(evnt.sourceEvent)) return; evnt.sourceEvent.preventDefault(); evnt.sourceEvent.stopPropagation(); const pos = pointer(evnt, this.draw_g.node()); not_changed = true; if (this.moveStart) this.moveStart(pos[0], pos[1]); }.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); }.bind(painter)).on('end', function(evnt) { if (move_disabled) return; evnt.sourceEvent.preventDefault(); evnt.sourceEvent.stopPropagation(); if (this.moveEnd) this.moveEnd(not_changed); let arg = null; if (not_changed) { // if not changed - provide click position const pos = pointer(evnt, this.draw_g.node()); arg = { x: pos[0], y: pos[1], dbl: false }; } this.getPadPainter()?.selectObjectPainter(this, arg); }.bind(painter)); painter.draw_g .style('cursor', hover_handler ? 'pointer' : 'move') .property('assigned_move', true) .call(drag_move); if (hover_handler) { painter.draw_g.on('mouseenter', () => painter.draw_g.style('text-decoration', 'underline')) .on('mouseleave', () => painter.draw_g.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].innerHTML = code; return true; } if (styles[n].innerHTML === code) return true; } const element = document.createElement('style'); if (tag) element.setAttribute('tag', tag); element.innerHTML = 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:') !== 0) 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), blob = new Blob([bindata], { 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:', 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 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>', '', 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 > 0) { 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('', () => { 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.add('column:'); for (let nrow = 0; nrow < 10; nrow++) { let n = ncolumn*10 + nrow; if (!useid) --n; // use -1 as none color let col = (n < 0) ? 'none' : getColor(n); if ((n === 0) && (fill_kind === 1)) col = 'none'; const 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)) ? `` : '', svg = `${rect}${lbl}`; this.add(svg, (useid ? n : col), res => set_func(useid ? parseInt(res) : res), 'Select color ' + col); } this.add('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.add('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.add('endcolumn:'); if (!this.native()) return this.endsub(); this.add('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.add('endcolumn:'); this.add('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.add('endcolumn:'); this.add('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.add('endcolumn:'); this.add('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.add('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) { // if (value === undefined) return; 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 = `${coltxt}`; 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 = `${n}`; 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.add('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.add('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.add('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) ? '' : '', svg = `${txt.node().outerHTML}${rect}`; this.add(svg, id, arg => set_func(parseInt(arg)), `${id}: ${fullname}`); if (n === 10) { this.add('endcolumn:'); this.add('column:'); } } this.add('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); if (pp) 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); if (pp) 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); if (pp) 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); if (pp) 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); if (pp) 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 = `${supported[n].toString()}`; 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?.snapid) { 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 (alone) this.header('Settings'); else this.sub('Settings'); this.sub('Files'); if (with_hierarchy) { this.addchk(settings.OnlyLastCycle, 'Last cycle', flag => { settings.OnlyLastCycle = flag; if (handle_func) handle_func('refresh'); }); this.addchk(!settings.SkipStreamerInfos, 'Streamer infos', flag => { settings.SkipStreamerInfos = !flag; if (handle_func) 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; if (handle_func) 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; if (handle_func) handle_func('width'); })); this.endsub(); } this.add('Dark mode: ' + (settings.DarkMode ? 'On' : 'Off'), () => { settings.DarkMode = !settings.DarkMode; if (handle_func) 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, `

${message}

`, { 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 = '
'+ ``+ '
'; 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 = '
'; 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 += ` `; } main_content += '
'; 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 = '
'; for (let n = 0; n < args.length; ++n) { main_content += ``+ ``; } main_content += '
'; 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 === 0) curr.at(-1).sub = undefined; return; } if (name === 'endcolumn:') return this.stack.pop(); if (isFunc(arg)) { title = func; func = arg; arg = name; } const elem = {}; curr.push(elem); if (name === 'column:') { 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.innerHTML = d.text; else { item.style.display = 'flex'; item.style['justify-content'] = 'space-between'; const txt = doc.createElement('span'); txt.innerHTML = 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('= 0) { if (need_check_area) { text.style.display = 'flex'; const chk = doc.createElement('span'); chk.innerHTML = 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.innerHTML = d.checked ? '\u2713' : ''; chk.style.display = 'inline-block'; chk.style.width = '1em'; text.appendChild(chk); } const sub = doc.createElement('span'); if (d.text.indexOf('') === 0) sub.textContent = d.text.slice(6, d.text.length - 7); else 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') .html( '
'+ `
${title}
`+ `
${main_content}
`+ '
'+ ``+ (args.btns ? '' : '') + '
'); 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?.draw_g) painter.draw_g.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; }, 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 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 => { 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 > 0) 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 !== 0) { 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 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) { const d = range[0]; range[0] = range[1]; range[1] = d; } 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 => (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) { if (!axis.fNdivisions) ndiv = 0; else ndiv = Math.max(axis.fNdivisions, 4); } this.nticks = ndiv % 100; this.nticks2 = (ndiv % 10000 - this.nticks) / 100; this.nticks3 = Math.floor(ndiv/10000); if (axis && !this.is_gaxis && (this.nticks > 20)) 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) { if (this.nticks > 8) 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 !== 0) || (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() { if (this.nmajor >= this.major.length) return null; return 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 > 0)) { 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 lbl = this.format(handle.major[indx], true); if (lbls.indexOf(lbl) < 0) { 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++; continue; } if (++this.ndig > 15) break; // not too many digits, anyway it will be exponential lbls = []; indx = 0; totallen = 0; } // 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?.snapid; // 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?.snapid; 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.kind === kAxisLabels) || */ (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, side, 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; } } let pr = Promise.resolve(); for (let lcnt = 0; lcnt < label_g.length; ++lcnt) { if (lcnt > 0) side = -side; pr = pr.then(() => 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])); 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; arg.align = rotate_lbls ? (this.optionLeft || this.reverseAlign ? 23 : 21) : (this.optionLeft || this.reverseAlign ? 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 > 1) && applied_scale) this.scaleTextDrawing(applied_scale, label_g[lcnt]); return this.finishTextDrawing(label_g[lcnt], true); }); } return pr.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 = axis.fLineColor !== 0; 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 = 1; 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.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?._fast_drawing || 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?.draw_g; 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); let dx = evnt.dx, dy = evnt.dy; if (arg.no_change_x) dx = 0; if (arg.no_change_y) dy = 0; 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); } const TooltipHandler = { /** @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.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.draw_g?.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?.snapid : ''); 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 || (hint.lines.length === 0)) { 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 !== 0) && (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 === 0) || (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.iscan) { frame_shift = getAbsPosInCanvas(this.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', this.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); }, /** @summary Assigns tooltip methods */ assign(painter) { Object.assign(painter, this, { tooltip_enabled: true }); } }, // TooltipHandler /** @summary Set of frame interactivity methods * @private */ FrameInteractive = { /** @summary Adding basic interactivity */ addBasicInteractivity() { TooltipHandler.assign(this); if (!this._frame_rotate && !this._frame_fixpos) { addDragHandler(this, { obj: this, x: this._frame_x, y: this._frame_y, 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.draw_g.selectChild('path'), main_svg = this.draw_g.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 pp = this.getPadPainter(), handlers_set = pp?._fast_drawing ? 0 : 1; if (main_svg.property('handlers_set') !== handlers_set) { const close_handler = handlers_set ? this.processFrameTooltipEvent.bind(this, null) : null, mouse_handler = handlers_set ? this.processFrameTooltipEvent.bind(this, { handler: true, touch: false }) : 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 ? this.processFrameTooltipEvent.bind(this, { handler: true, touch: true }) : 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.getPadName())) setTimeout(this.processFrameTooltipEvent.bind(this, hintsg.property('last_point'), null), 10); }, /** @summary Add interactive handlers */ async addFrameInteractivity(for_second_axes) { const pp = this.getPadPainter(), svg = this.getFrameSvg(); if (pp?._fast_drawing || 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.addFrameKeysHandler(); 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 Add keys handler */ addFrameKeysHandler() { if (this.keys_handler || (typeof window === 'undefined')) return; this.keys_handler = evnt => this.processKeyPress(evnt); window.addEventListener('keydown', this.keys_handler, false); }, /** @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.enabledKeys === 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') !== 0)) 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._dblclick_handler : this._click_handler; if (isFunc(handler)) res = handler(exact.user_info, pnt); } if (!dblckick) { pp.selectObjectPainter(exact ? exact.painter : this, { x: pnt.x + (this._frame_x || 0), y: pnt.y + (this._frame_y || 0) }); } 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.draw_g.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.draw_g.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}`); delete this._shifting_dx; delete this._shifting_dy; 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 !== 0) 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: this.zoom_curr[0] = m[0]; changed[1] = false; break; // only X case 3: this.zoom_curr[1] = m[1]; changed[0] = false; break; // only Y } 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._dblclick_handler) 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 !== 0) || 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; const pad_painter = this.getPadPainter(); if (pad_painter?.painters) { for (let k = 0; k < pad_painter.painters.length; ++k) { const subpainter = pad_painter.painters[k]; if (subpainter?.wheel_zoomy !== undefined) return subpainter.wheel_zoomy; } } return false; }, /** @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 (('zoom_kind' in this) && (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.draw_g.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 frame interactive methods */ assign(painter) { Object.assign(painter, this); } }; // FrameInteractive /** * @summary Painter class for TFrame, main handler for interactivity * @private */ class TFramePainter extends ObjectPainter { /** @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.keys_handler = null; this._borderMode = gStyle.fFrameBorderMode; this._borderSize = 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.enabledKeys = on && settings.HandleKeys; // used only in 3D mode where control is used if (this.control) this.control.enableKeys = this.enabledKeys; } /** @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 */ setLastEventPos(pnt) { this.fLastEventPnt = pnt; } /** @summary Return position of last event * @private */ getLastEventPos() { return this.fLastEventPnt; } /** @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; if (this.zoom_zmin === undefined) 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, 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, 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, swap_xy: this.swap_xy, fp: this, 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); }, 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; let mx = 1 - pad.fLeftMargin - pad.fRightMargin, my = 1 - pad.fBottomMargin - pad.fTopMargin; if (mx <= 0) mx = 0.01; // to prevent overflow if (my <= 0) my = 0.01; 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._fast_drawing) 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) return; 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) */ 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._fast_drawing) 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._borderMode = tframe.fBorderMode; this._borderSize = tframe.fBorderSize; } else if (this.fillatt === undefined) { 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?.iscan) 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.draw_g?.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.draw_g?.selectChild('.main_layer').selectAll('*').remove(); this.draw_g?.selectChild('.upper_layer').selectAll('*').remove(); this.xaxis = null; this.yaxis = null; this.zaxis = null; if (this.draw_g) { this.draw_g.selectAll('*').remove(); this.draw_g.on('mousedown', null) .on('dblclick', null) .on('wheel', null) .on('contextmenu', null) .property('interactive_set', null); this.draw_g.remove(); } delete this.draw_g; // frame element managed by the pad if (this.keys_handler) { window.removeEventListener('keydown', this.keys_handler, false); this.keys_handler = null; } } /** @summary Cleanup frame */ cleanup() { this.cleanFrameDrawings(); delete this._click_handler; delete this._dblclick_handler; delete this.enabledKeys; const pp = this.getPadPainter(); if (pp?.frame_painter_ref === this) delete pp.frame_painter_ref; super.cleanup(); } /** @summary Redraw TFrame */ redraw(/* reason */) { const pp = this.getPadPainter(); if (pp) pp.frame_painter_ref = this; // keep direct reference to the frame painter // 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)); let w = Math.round(rect.width * (this.fX2NDC - this.fX1NDC)), h = Math.round(rect.height * (this.fY2NDC - this.fY1NDC)), rotate = false, fixpos = false, trans; if (pp?.options) { if (pp.options.RotateFrame) rotate = true; if (pp.options.FixFrame) fixpos = true; } if (rotate) { trans = `rotate(-90,${lm},${tm}) translate(${lm-h},${tm})`; [w, h] = [h, w]; } else trans = makeTranslate(lm, tm); this._frame_x = lm; this._frame_y = tm; this._frame_width = w; this._frame_height = h; this._frame_rotate = rotate; this._frame_fixpos = fixpos; this._frame_trans = trans; 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 this.draw_g = this.getFrameSvg(); let top_rect, main_svg; if (this.draw_g.empty()) { this.draw_g = this.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()) this.draw_g.append('svg:title').text(''); top_rect = this.draw_g.append('svg:path'); main_svg = this.draw_g.append('svg:svg') .attr('class', 'main_layer') .attr('x', 0) .attr('y', 0) .attr('overflow', 'hidden'); this.draw_g.append('svg:g').attr('class', 'axis_layer'); this.draw_g.append('svg:g').attr('class', 'upper_layer'); } else { top_rect = this.draw_g.selectChild('path'); main_svg = this.draw_g.selectChild('.main_layer'); } this.axes_drawn = this.axes2_drawn = false; this.draw_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}`); this.draw_g.selectAll('.frame_deco').remove(); if (this._borderMode && this.fillatt.hasColor()) { const paths = getBoxDecorations(0, 0, this._frame_width, this._frame_height, this._borderMode, this._borderSize || 2, this._borderSize || 2); this.draw_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()); this.draw_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._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?.snapid && (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?.snapid && (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._borderMode + 1, v => { this._borderMode = v - 1; this.interactiveRedraw(true, `exec:SetBorderMode(${v-1})`); }, 'Frame border mode'); menu.addSizeMenu('Size', 0, 20, 2, this._borderSize, v => { this._borderSize = 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._borderMode; gStyle.fFrameBorderSize = this._borderSize; }, '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.draw_g?.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 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 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.getPadPainter()?.painters?.forEach(painter => { if (isFunc(painter?.unzoomUserRange)) { if (painter.unzoomUserRange(unzoom_x, unzoom_y, unzoom_z)) changed = true; } }); } } return changed ? this.interactiveRedraw('pad', 'zoom').then(() => true) : false; } /** @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`]; 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 (!isFunc(obj.canZoomInside)) return; if (zoom_v && obj.canZoomInside(name[0], vmin, vmax)) { this[`zoom_${name}min`] = vmin; this[`zoom_${name}max`] = vmax; changed = true; zoom_v = false; } }); } // and process unzoom, if any if (unzoom_v) { if (this[`zoom_${name}min`] !== this[`zoom_${name}max`]) { changed = true; if (name === 'y') unzoomHistogramYRange(this.getMainPainter()); } this[`zoom_${name}min`] = this[`zoom_${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]; 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 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()) return; FrameInteractive.assign(this); this.addFrameKeysHandler(); } /** @summary Add interactive functionality to the frame * @private */ addInteractivity(for_second_axes) { if (this.isBatchMode() || (!settings.Zooming && !settings.ContextMenu)) return false; FrameInteractive.assign(this); 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)`); }, 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) return; // if no significant change, do nothing // do not change if size too small if (Math.min(handle.groups[id-1].size - diff, group.size+diff) < 3) return; 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(); if (!found) 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;') .html('✕') .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) return; if (frame.getAttribute('class') !== 'jsroot_flex_draw') return; if (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.html('▞').attr('title', 'restore'); else btn.html(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('
' + `

${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: '✕', t: 'close' }, { n: '▔', t: 'maximize' }, { n: '▁', 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) .html(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 === 0) 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') .html(`${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 */ makeSVG(id, keep_frame) { const frame = this.frames[id]; if (!frame) return; const main = select(frame), mainsvg = main.select('svg'); if (mainsvg.empty()) return; 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); 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 { /** @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(`

title

${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', () => { delete this._hsepar_move; 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.innerHTML = 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(); }), 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;`), 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', () => { delete this._vsepar_move; 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); function isPadPainter(p) { return p?.pad && isFunc(p.forEachPainterInPad); } 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', this.this_pad_name), btn = group.select('[name=\'Toggle\']'); if (btn.empty()) return; let state = btn.property('buttons_state'); if (btn.property('timout_handler')) { if (action !== 'timeout') clearTimeout(btn.property('timout_handler')); btn.property('timout_handler', null); } let is_visible = false; switch (action) { case 'enable': is_visible = true; 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', this.this_pad_name); 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', this.this_pad_name); if (!group.empty()) { group.selectAll('*').remove(); group.property('nextx', null); } }, showPadButtons() { const group = this.getLayerSvg('btns_layer', this.this_pad_name); if (group.empty()) return; // clean all previous buttons group.selectAll('*').remove(); if (!this._buttons) return; const iscan = this.iscan || !this.has_canvas, y = 0; let ctrl, x = group.property('leftside') ? this.getButtonSize(1.25) : 0; if (this._fast_drawing) { 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 + (iscan ? '' : (` on pad ${this.this_pad_name}`)) + (item.keyname ? ` (keyshortcut ${item.keyname})` : ''), false); if (group.property('vertical')) svg.attr('x', y).attr('y', x); else svg.attr('x', x).attr('y', y); svg.attr('name', item.funcname) .style('display', ctrl.property('buttons_state') ? '' : 'none') .attr('key', item.keyname || null) .on('click', evnt => this.clickPadButton(item.funcname, evnt)); svg.node()._mouseenter = () => 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 // identifier used in TWebCanvas painter webSnapIds = { kObject: 1, kSVG: 2, kSubPad: 3, kColors: 4, kStyle: 5, kFont: 6 }; /** @summary Fill TWebObjectOptions for painter * @private */ function createWebObjectOptions(painter) { if (!painter?.snapid) return null; const obj = { _typename: 'TWebObjectOptions', snapid: painter.snapid.toString(), opt: painter.getDrawOpt(true), fcust: '', fopt: [] }; if (isFunc(painter.fillWebObjectOptions)) painter.fillWebObjectOptions(obj); return obj; } /** * @summary Painter for TPad object * @private */ class TPadPainter extends ObjectPainter { #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 #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 /** @summary constructor * @param {object|string} dom - DOM element for drawing or element id * @param {object} pad - TPad object to draw * @param {boolean} [iscan] - if TCanvas object */ constructor(dom, pad, iscan) { super(dom, pad); this.pad = pad; this.iscan = iscan; // indicate if working with canvas this.this_pad_name = ''; if (!this.iscan && pad?.fName) { this.this_pad_name = pad.fName.replace(' ', '_'); // avoid empty symbol in pad name const regexp = /^[A-Za-z][A-Za-z0-9_]*$/; if (!regexp.test(this.this_pad_name) || ((this.this_pad_name === 'button') && (pad._typename === clTButton))) this.this_pad_name = 'jsroot_pad_' + internals.id_counter++; } this.painters = []; // complete list of all painters in the pad this.has_canvas = true; this.forEachPainter = this.forEachPainterInPad; const d = this.selectDom(); if (!d.empty() && d.property('_batch_mode')) this.batch_mode = true; } /** @summary Indicates that drawing runs in batch mode * @private */ isBatchMode() { if (this.batch_mode !== undefined) return this.batch_mode; if (isBatchMode()) return true; if (!this.iscan && this.has_canvas) return this.getCanvPainter()?.isBatchMode(); return false; } /** @summary Indicates that is is Root6 pad painter * @private */ isRoot6() { return true; } /** @summary Returns true if pad is editable */ isEditable() { return this.pad?.fEditable ?? true; } /** @summary Returns true if button */ isButton() { return this.matchObjectType(clTButton); } /** @summary Returns SVG element for the pad itself * @private */ svg_this_pad() { return this.getPadSvg(this.this_pad_name); } /** @summary Returns main painter on the pad * @desc Typically main painter is TH1/TH2 object which is drawing axes * @private */ getMainPainter() { return this.main_painter_ref || null; } /** @summary Assign main painter on the pad * @desc Typically main painter is TH1/TH2 object which is drawing axes * @private */ setMainPainter(painter, force) { if (!this.main_painter_ref || force) this.main_painter_ref = painter; } /** @summary cleanup pad and all primitives inside */ cleanup() { if (this.#doing_draw) console.error('pad drawing is not completed when cleanup is called'); this.painters.forEach(p => p.cleanup()); const svg_p = this.svg_this_pad(); if (!svg_p.empty()) { svg_p.property('pad_painter', null); if (!this.iscan) svg_p.remove(); } delete this.main_painter_ref; delete this.frame_painter_ref; const cp = this.iscan || !this.has_canvas ? this : this.getCanvPainter(); if (cp) delete cp.pads_cache; this.#pad_x = this.#pad_y = this.#pad_width = this.#pad_height = undefined; this.#doing_draw = undefined; delete this._interactively_changed; delete this._snap_primitives; this.#last_grayscale = undefined; this.#custom_palette = this.#custom_colors = this.#custom_palette_indexes = this.#custom_palette_colors = undefined; this.painters = []; this.pad = null; this.this_pad_name = undefined; this.has_canvas = false; selectActivePad({ pp: this, active: false }); super.cleanup(); } /** @summary Returns frame painter inside the pad * @private */ getFramePainter() { return this.frame_painter_ref; } /** @summary get pad width */ getPadWidth() { return this.#pad_width || 0; } /** @summary get pad height */ getPadHeight() { return this.#pad_height || 0; } /** @summary get pad 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.main_painter_ref === painter) { delete this.main_painter_ref; resindx = -111; } }); // when main painter disappears because of special cleanup - also reset zooming if (clean_only_secondary && get_main && !this.getMainPainter()) this.getFramePainter()?.resetZoom(); return resindx; } /** @summary returns custom palette associated with pad or top canvas * @private */ getCustomPalette(no_recursion) { return this.#custom_palette || (no_recursion ? null : this.getCanvPainter()?.getCustomPalette(true)); } /** @summary Returns number of painters * @private */ getNumPainters() { return this.painters.length; } _getCustomPaletteIndexes() { return this.#custom_palette_indexes; } /** @summary Provides automatic color * @desc Uses ROOT colors palette if possible * @private */ getAutoColor(numprimitives) { if (!numprimitives) numprimitives = (this._num_primitives || 5) - (this._num_specials || 0); if (numprimitives < 2) numprimitives = 2; let indx = this._auto_color ?? 0; this._auto_color = (indx + 1) % numprimitives; if (indx >= numprimitives) indx = numprimitives - 1; 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 Call function for each painter in pad * @param {function} userfunc - function to call * @param {string} kind - 'all' for all objects (default), 'pads' only pads and sub-pads, 'objects' only for object in current pad * @private */ forEachPainterInPad(userfunc, kind) { if (!kind) kind = 'all'; if (kind !== 'objects') userfunc(this); for (let k = 0; k < this.painters.length; ++k) { const sub = this.painters[k]; if (isFunc(sub.forEachPainterInPad)) { if (kind !== 'objects') sub.forEachPainterInPad(userfunc, kind); } else if (kind !== 'pads') userfunc(sub); } } /** @summary register for pad events receiver * @desc in pad painter, while pad may be drawn without canvas */ registerForPadEvents(receiver) { this.pad_events_receiver = receiver; } /** @summary Generate pad events, normally handled by GED * @desc in pad painter, while pad may be drawn without canvas * @private */ producePadEvent(what, padpainter, painter, position) { if ((what === 'select') && isFunc(this.selectActivePad)) this.selectActivePad(padpainter, painter, position); if (isFunc(this.pad_events_receiver)) this.pad_events_receiver({ what, padpainter, painter, position }); } /** @summary method redirect call to pad events receiver */ selectObjectPainter(painter, pos) { const istoppad = this.iscan || !this.has_canvas, canp = istoppad ? this : this.getCanvPainter(); if (painter === undefined) painter = this; if (pos && !istoppad) pos = getAbsPosInCanvas(this.svg_this_pad(), pos); selectActivePad({ pp: this, active: true }); canp?.producePadEvent('select', this, painter, pos); } /** @summary Draw pad active border * @private */ drawActiveBorder(svg_rect, is_active) { if (is_active !== undefined) { if (this.is_active_pad === is_active) return; this.is_active_pad = is_active; } if (this.is_active_pad === undefined) return; if (!svg_rect) svg_rect = this.iscan ? this.getCanvSvg().selectChild('.canvas_fillrect') : this.svg_this_pad().selectChild('.root_pad_border'); const cp = this.getCanvPainter(); let lineatt = this.is_active_pad && cp?.highlight_gpad ? new TAttLineHandler({ style: 1, width: 1, color: 'red' }) : this.lineatt; if (!lineatt) lineatt = new TAttLineHandler({ color: 'none' }); svg_rect.call(lineatt.func); } /** @summary Set fast drawing property depending on the size * @private */ setFastDrawing(w, h) { const was_fast = this._fast_drawing; this._fast_drawing = (this.snapid === undefined) && settings.SmallPad && ((w < settings.SmallPad.width) || (h < settings.SmallPad.height)); if (was_fast !== this._fast_drawing) this.showPadButtons(); } /** @summary Returns true if canvas configured with grayscale * @private */ isGrayscale() { if (!this.iscan) return false; return this.pad?.TestBit(kIsGrayscale) ?? false; } /** @summary 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.iscan) return; let changed = false; if (flag === undefined) { flag = this.pad?.TestBit(kIsGrayscale) ?? false; changed = (this.#last_grayscale !== undefined) && (this.#last_grayscale !== flag); } else if (flag !== this.pad?.TestBit(kIsGrayscale)) { this.pad?.InvertBit(kIsGrayscale); changed = true; } if (changed) this.forEachPainter(p => { delete p._color_palette; }); this._root_colors = flag ? getGrayColors(this.#custom_colors) : this.#custom_colors; this.#last_grayscale = flag; this.#custom_palette = this.#custom_palette_colors ? new ColorPalette(this.#custom_palette_colors, flag) : null; } /** @summary Create SVG element for canvas */ createCanvasSvg(check_resize, new_size) { const is_batch = this.isBatchMode(), lmt = 5; let factor, 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', this.this_pad_name); info = this.getLayerSvg('info_layer', this.this_pad_name); frect = svg.selectChild('.canvas_fillrect'); } else { const render_to = this.selectDom(); if (render_to.style('position') === 'static') render_to.style('position', 'relative'); svg = render_to.append('svg') .attr('class', 'jsroot root_canvas') .property('pad_painter', this) // this is custom property .property('redraw_by_resize', false); // could be enabled to force redraw by each resize this.setTopPainter(); // assign canvas as top painter of that element if (is_batch) svg.attr('xmlns', nsSVG); else if (!this.online_canvas) svg.append('svg:title').text('ROOT canvas'); if (!is_batch) 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.snapid === undefined) { 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', this.this_pad_name); let df = info.selectChild('.canvas_item'); const fitem = getHPainter().findRootFileForItem(item_name), fname = (gStyle.fOptFile === 3) ? item_name : ((gStyle.fOptFile === 2) ? fitem?._fullurl : fitem?._name); if (!gStyle.fOptFile || !fname) df.remove(); else { if (df.empty()) df = info.append('text').attr('class', 'canvas_item'); const rect = this.getPadRect(); makeTranslate(df, Math.round(rect.width * (1 - gStyle.fDateX)), Math.round(rect.height * (1 - gStyle.fDateY))) .style('text-anchor', 'end') .text(fname); } if (((gStyle.fOptDate === 2) || (gStyle.fOptDate === 3)) && fitem?._file) { info.selectChild('.canvas_date') .text(convertDate(getTDatime(gStyle.fOptDate === 2 ? fitem._file.fDatimeC : fitem._file.fDatimeM))); } } /** @summary Return true if this pad enlarged */ isPadEnlarged() { if (this.iscan || !this.has_canvas) return this.enlargeMain('state') === 'on'; return this.getCanvSvg().property('pad_enlarged') === this.pad; } /** @summary Enlarge pad draw element when possible */ enlargePad(evnt, is_dblclick, is_escape) { evnt?.preventDefault(); evnt?.stopPropagation(); // ignore double click on canvas itself for enlarge if (is_dblclick && this._websocket && (this.enlargeMain('state') === 'off')) return; const svg_can = this.getCanvSvg(), pad_enlarged = svg_can.property('pad_enlarged'); if (this.iscan || !this.has_canvas || (!pad_enlarged && !this.hasObjectsToDraw() && !this.painters)) { if (this._fixed_size) return; // canvas cannot be enlarged in such mode if (!this.enlargeMain(is_escape ? false : 'toggle')) return; if (this.enlargeMain('state') === 'off') svg_can.property('pad_enlarged', null); else selectActivePad({ pp: this, active: true }); } else if (!pad_enlarged && !is_escape) { this.enlargeMain(true, true); svg_can.property('pad_enlarged', this.pad); selectActivePad({ pp: this, active: true }); } else if (pad_enlarged === this.pad) { this.enlargeMain(false); svg_can.property('pad_enlarged', null); } else if (!is_escape && is_dblclick) console.error('missmatch with pad double click events'); return this.checkResize(true); } /** @summary Create main SVG element for pad * @return true when pad is displayed and all its items should be redrawn */ createPadSvg(only_resize) { if (!this.has_canvas) { this.createCanvasSvg(only_resize ? 2 : 0); return true; } const svg_can = this.getCanvSvg(), width = svg_can.property('draw_width'), height = svg_can.property('draw_height'), pad_enlarged = svg_can.property('pad_enlarged'), pad_visible = !this.pad_draw_disabled && (!pad_enlarged || (pad_enlarged === this.pad)), is_batch = this.isBatchMode(); let w = Math.round(this.pad.fAbsWNDC * width), h = Math.round(this.pad.fAbsHNDC * height), x = Math.round(this.pad.fAbsXlowNDC * width), y = Math.round(height * (1 - this.pad.fAbsYlowNDC)) - h, svg_pad, svg_border, btns; if (pad_enlarged === this.pad) { w = width; h = height; x = y = 0; } if (only_resize) { svg_pad = this.svg_this_pad(); svg_border = svg_pad.selectChild('.root_pad_border'); if (!is_batch) btns = this.getLayerSvg('btns_layer', this.this_pad_name); this.addPadInteractive(true); } else { svg_pad = svg_can.selectChild('.primitives_layer') .append('svg:svg') // svg used to blend all drawings outside .classed('__root_pad_' + this.this_pad_name, true) .attr('pad', this.this_pad_name) // set extra attribute to mark pad name .property('pad_painter', this); // this is custom property if (!is_batch) svg_pad.append('svg:title').text('subpad ' + this.this_pad_name); // need to check attributes directly while attributes objects will be created later if (!is_batch || (this.pad.fFillStyle > 0) || ((this.pad.fLineStyle > 0) && (this.pad.fLineColor > 0))) svg_border = svg_pad.append('svg:path').attr('class', 'root_pad_border'); if (!is_batch) { svg_border.style('pointer-events', 'visibleFill') // get events also for not visible rect .on('dblclick', evnt => this.enlargePad(evnt, true)) .on('click', () => this.selectObjectPainter()) .on('mouseenter', () => this.showObjectStatus()) .on('contextmenu', settings.ContextMenu ? evnt => this.padContextMenu(evnt) : null); } svg_pad.append('svg:g').attr('class', 'primitives_layer'); if (!is_batch) { btns = svg_pad.append('svg:g') .attr('class', 'btns_layer') .property('leftside', settings.ToolBarSide !== 'left') .property('vertical', settings.ToolBarVert); } } this.createAttFill({ attr: this.pad }); this.createAttLine({ attr: this.pad, color0: !this.pad.fBorderMode ? 'none' : '' }); svg_pad.style('display', pad_visible ? null : 'none') .attr('viewBox', `0 0 ${w} ${h}`) // due to svg .attr('preserveAspectRatio', 'none') // due to svg, we do not preserve relative ratio .attr('x', x) // due to svg .attr('y', y) // due to svg .attr('width', w) // due to svg .attr('height', h) // due to svg .property('draw_x', x) // this is to make similar with canvas .property('draw_y', y) .property('draw_width', w) .property('draw_height', h); this.#pad_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.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.iscan || !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, // !cleanup && (this._disable_dragging || this.getFramePainter()?.mode3d), is_disabled: kind => svg_can.property('pad_enlarged') || this.btns_active_flag || (kind === 'move' && (this._disable_dragging || this.getFramePainter()?.mode3d)), getDrawG: () => this.svg_this_pad(), pad_rect: { width, height }, minwidth: 20, minheight: 20, move_resize: (_x, _y, _w, _h) => { const x0 = this.pad.fAbsXlowNDC, y0 = this.pad.fAbsYlowNDC, scale_w = _w / width / this.pad.fAbsWNDC, scale_h = _h / height / this.pad.fAbsHNDC, shift_x = _x / width - x0, shift_y = 1 - (_y + _h) / height - y0; this.forEachPainterInPad(p => { p.pad.fAbsXlowNDC += (p.pad.fAbsXlowNDC - x0) * (scale_w - 1) + shift_x; p.pad.fAbsYlowNDC += (p.pad.fAbsYlowNDC - y0) * (scale_h - 1) + shift_y; p.pad.fAbsWNDC *= scale_w; p.pad.fAbsHNDC *= scale_h; }, 'pads'); }, redraw: () => this.interactiveRedraw('pad', 'padpos') }); } /** @summary Disable pad drawing * @desc Complete SVG element will be hidden */ disablePadDrawing() { if (!this.pad_draw_disabled && this.has_canvas && !this.iscan) { this.pad_draw_disabled = true; this.createPadSvg(true); } } /** @summary Check if it is special object, which should be handled separately * @desc It can be TStyle or list of colors or palette object * @return {boolean} true if any */ checkSpecial(obj) { if (!obj) return false; if (obj._typename === clTStyle) { Object.assign(gStyle, obj); return true; } if ((obj._typename === clTObjArray) && (obj.name === 'ListOfColors')) { if (this.options?.CreatePalette) { let arr = []; for (let n = obj.arr.length - this.options.CreatePalette; n < obj.arr.length; ++n) { const col = getRGBfromTColor(obj.arr[n]); if (!col) { console.log('Fail to create color for palette'); arr = null; break; } arr.push(col); } if (arr.length) this.#custom_palette = new ColorPalette(arr); } if (!this.options || this.options.GlobalColors) // set global list of colors adoptRootColors(obj); // copy existing colors and extend with new values this.#custom_colors = this.options?.LocalColors ? extendRootColors(null, obj) : null; return true; } if ((obj._typename === clTObjArray) && (obj.name === 'CurrentColorPalette')) { const arr = [], indx = []; let missing = false; for (let n = 0; n < obj.arr.length; ++n) { const col = obj.arr[n]; if (col?._typename === clTColor) { indx[n] = col.fNumber; arr[n] = getRGBfromTColor(col); } else { console.log(`Missing color with index ${n}`); missing = true; } } const apply = (!this.options || (!missing && !this.options.IgnorePalette)); this.#custom_palette_indexes = apply ? indx : null; this.#custom_palette_colors = apply ? arr : null; return true; } return false; } /** @summary Check if special objects appears in primitives * @desc it could be list of colors or palette */ checkSpecialsInPrimitives(can, count_specials) { const lst = can?.fPrimitives; if (count_specials) this._num_specials = 0; if (!lst) return; for (let i = 0; i < lst.arr?.length; ++i) { if (this.checkSpecial(lst.arr[i])) { lst.arr[i].$special = true; // mark object as special one, do not use in drawing if (count_specials) this._num_specials++; } } } /** @summary try to find object by name in list of pad primitives * @desc used to find title drawing * @private */ findInPrimitives(objname, objtype) { const match = obj => obj && (obj?.fName === objname) && (objtype ? (obj?._typename === objtype) : true), snap = this._snap_primitives?.find(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 === 0) 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.iscan) this._start_tm = new Date().getTime(); // set number of primitives this._num_primitives = this.pad?.fPrimitives?.arr?.length || 0; // sync to prevent immediate pad redraw during normal drawing sequence return this.syncDraw(true).then(() => this.drawPrimitives(0)); } if (!this.pad || (indx >= this._num_primitives)) { if (this._start_tm) { const spenttm = new Date().getTime() - this._start_tm; if (spenttm > 1000) console.log(`Canvas ${this.pad?.fName || '---'} drawing took ${(spenttm*1e-3).toFixed(2)}s`); delete this._start_tm; } this.confirmDraw(); return; } const obj = this.pad.fPrimitives.arr[indx]; if (!obj || obj.$special || ((indx > 0) && (obj._typename === clTFrame) && this.getFramePainter())) return this.drawPrimitives(indx+1); // use of Promise should avoid large call-stack depth when many primitives are drawn return this.drawObject(this, obj, this.pad.fPrimitives.opt[indx]).then(op => { if (isObject(op)) op._primitive = true; // mark painter as belonging to primitives return this.drawPrimitives(indx+1); }); } /** @summary Divide pad on sub-pads * @return {Promise} when finished * @private */ async divide(nx, ny, use_existing) { if (nx && !ny && use_existing) { for (let k = 0; k < nx; ++k) { if (!this.getSubPadPainter(k+1)) { use_existing = false; break; } } if (use_existing) return this; } this.cleanPrimitives(isPadPainter); if (!this.pad.fPrimitives) this.pad.fPrimitives = create$1(clTList); this.pad.fPrimitives.Clear(); if ((!nx && !ny) || !this.pad.Divide(nx, ny)) return this; const drawNext = indx => { if (indx >= this.pad.fPrimitives.arr.length) return this; return this.drawObject(this, this.pad.fPrimitives.arr[indx]).then(() => drawNext(indx + 1)); }; return drawNext(0); } /** @summary Return sub-pads painter, only direct childs are checked * @private */ getSubPadPainter(n) { for (let k = 0; k < this.painters.length; ++k) { const sub = this.painters[k]; if (isPadPainter(sub) && (sub.pad.fNumber === n)) return sub; } return null; } /** @summary Process tooltip event in the pad * @private */ processPadTooltipEvent(pnt) { const painters = [], hints = []; // first count - how many processors are there this.painters?.forEach(obj => { if (isFunc(obj.processTooltipEvent)) painters.push(obj); }); if (pnt) pnt.nproc = painters.length; painters.forEach(obj => { const hint = obj.processTooltipEvent(pnt) || { user_info: null }; hints.push(hint); if (pnt?.painters) hint.painter = obj; }); return hints; } /** @summary Changes canvas dark mode * @private */ changeDarkMode(mode) { this.getCanvSvg().style('filter', (mode ?? settings.DarkMode) ? 'invert(100%)' : null); } /** @summary Fill pad context menu * @private */ fillContextMenu(menu) { if (!this.pad) return false; menu.header(`${this.pad._typename}::${this.pad.fName}`, `${urlClassPrefix}${this.pad._typename}.html`); menu.addchk(this.isTooltipAllowed(), 'Show tooltips', () => this.setTooltipAllowed('toggle')); menu.addchk(this.pad.fGridx, 'Grid x', flag => { this.pad.fGridx = flag ? 1 : 0; this.interactiveRedraw('pad', `exec:SetGridx(${flag ? 1 : 0})`); }); menu.addchk(this.pad.fGridy, 'Grid y', flag => { this.pad.fGridy = flag ? 1 : 0; this.interactiveRedraw('pad', `exec:SetGridy(${flag ? 1 : 0})`); }); menu.sub('Ticks x'); menu.addchk(this.pad.fTickx === 0, 'normal', () => { this.pad.fTickx = 0; this.interactiveRedraw('pad', 'exec:SetTickx(0)'); }); menu.addchk(this.pad.fTickx === 1, 'ticks on both sides', () => { this.pad.fTickx = 1; this.interactiveRedraw('pad', 'exec:SetTickx(1)'); }); menu.addchk(this.pad.fTickx === 2, 'labels on both sides', () => { this.pad.fTickx = 2; this.interactiveRedraw('pad', 'exec:SetTickx(2)'); }); menu.endsub(); menu.sub('Ticks y'); menu.addchk(this.pad.fTicky === 0, 'normal', () => { this.pad.fTicky = 0; this.interactiveRedraw('pad', 'exec:SetTicky(0)'); }); menu.addchk(this.pad.fTicky === 1, 'ticks on both sides', () => { this.pad.fTicky = 1; this.interactiveRedraw('pad', 'exec:SetTicky(1)'); }); menu.addchk(this.pad.fTicky === 2, 'labels on both sides', () => { this.pad.fTicky = 2; this.interactiveRedraw('pad', 'exec:SetTicky(2)'); }); menu.endsub(); menu.addchk(this.pad.fEditable, 'Editable', flag => { this.pad.fEditable = flag; this.interactiveRedraw('pad', `exec:SetEditable(${flag})`); }); if (this.iscan) { menu.addchk(this.pad.TestBit(kIsGrayscale), 'Gray scale', flag => { this.setGrayscale(flag); this.interactiveRedraw('pad', `exec:SetGrayscale(${flag})`); }); } menu.sub('Border'); menu.addSelectMenu('Mode', ['Down', 'Off', 'Up'], this.pad.fBorderMode + 1, v => { this.pad.fBorderMode = v - 1; this.interactiveRedraw(true, `exec:SetBorderMode(${v-1})`); }, 'Pad border mode'); menu.addSizeMenu('Size', 0, 20, 2, this.pad.fBorderSize, v => { this.pad.fBorderSize = v; this.interactiveRedraw(true, `exec:SetBorderSize(${v})`); }, 'Pad border size'); menu.endsub(); menu.addAttributesMenu(this); if (!this._websocket) { const do_divide = arg => { if (!arg || !isStr(arg)) return; // workaround - prevent full deletion of canvas if (this.normal_canvas === false) this.normal_canvas = 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', () => { if (!this.pad) return; this.fillatt?.saveToStyle(this.iscan ? 'fCanvasColor' : 'fPadColor'); gStyle.fPadGridX = this.pad.fGridx; gStyle.fPadGridY = this.pad.fGridy; gStyle.fPadTickX = this.pad.fTickx; gStyle.fPadTickY = this.pad.fTicky; gStyle.fOptLogx = this.pad.fLogx; gStyle.fOptLogy = this.pad.fLogy; gStyle.fOptLogz = this.pad.fLogz; }, 'Store pad fill attributes, grid, tick and log scale settings to gStyle'); if (this.iscan) { menu.addSettingsMenu(false, false, arg => { if (arg === 'dark') this.changeDarkMode(); }); } } menu.separator(); if (isFunc(this.hasMenuBar) && isFunc(this.actiavteMenuBar)) menu.addchk(this.hasMenuBar(), 'Menu bar', flag => this.actiavteMenuBar(flag)); if (isFunc(this.hasEventStatus) && isFunc(this.activateStatusBar) && isFunc(this.canStatusBar)) { if (this.canStatusBar()) menu.addchk(this.hasEventStatus(), 'Event status', () => this.activateStatusBar('toggle')); } if (this.enlargeMain() || (this.has_canvas && this.hasObjectsToDraw())) menu.addchk(this.isPadEnlarged(), 'Enlarge ' + (this.iscan ? 'canvas' : 'pad'), () => this.enlargePad()); const fname = this.this_pad_name || (this.iscan ? 'canvas' : 'pad'); menu.sub('Save as'); const fmts = ['svg', 'png', 'jpeg', 'webp']; if (internals.makePDF) fmts.push('pdf'); fmts.forEach(fmt => menu.add(`${fname}.${fmt}`, () => this.saveAs(fmt, this.iscan, `${fname}.${fmt}`))); if (this.iscan) { menu.separator(); menu.add(`${fname}.json`, () => this.saveAs('json', true, `${fname}.json`), 'Produce JSON with line spacing'); menu.add(`${fname}0.json`, () => this.saveAs('json', false, `${fname}0.json`), 'Produce JSON without line spacing'); } menu.endsub(); return true; } /** @summary Show pad context menu * @private */ async padContextMenu(evnt) { if (evnt.stopPropagation) { // this is normal event processing and not emulated jsroot event evnt.stopPropagation(); // disable main context menu evnt.preventDefault(); // disable browser context menu this.getFramePainter()?.setLastEventPos(); } return createMenu(evnt, this).then(menu => { this.fillContextMenu(menu); return this.fillObjectExecMenu(menu, ''); }).then(menu => menu.show()); } /** @summary Redraw TLegend object * @desc Used when object attributes are changed to ensure that legend is up to date * @private */ async redrawLegend() { return this.findPainterFor(null, '', clTLegend)?.redraw(); } /** @summary Redraw pad means redraw ourself * @return {Promise} when redrawing ready */ async redrawPad(reason) { const sync_promise = this.syncDraw(reason); if (sync_promise === false) { console.log(`Prevent redrawing of ${this.pad.fName}`); return false; } let showsubitems = true; const redrawNext = indx => { while (indx < this.painters.length) { const sub = this.painters[indx++]; let res = 0; if (showsubitems || sub.this_pad_name) res = sub.redraw(reason); if (isPromise(res)) return res.then(() => redrawNext(indx)); } return true; }; return sync_promise.then(() => { if (this.iscan) this.createCanvasSvg(2); else showsubitems = this.createPadSvg(true); return redrawNext(0); }).then(() => { this.addPadInteractive(); this.confirmDraw(); if (getActivePad() === this) this.getCanvPainter()?.producePadEvent('padredraw', this); return true; }); } /** @summary redraw pad */ redraw(reason) { // intentionally do not return Promise to let re-draw sub-pads in parallel this.redrawPad(reason); } /** @summary Checks if pad should be redrawn by resize * @private */ needRedrawByResize() { const elem = this.svg_this_pad(); if (!elem.empty() && elem.property('can3d') === constants$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) return false; if (!this.iscan && this.has_canvas) return false; const sync_promise = this.syncDraw('canvas_resize'); if (sync_promise === false) return false; if ((size === true) || (size === false)) { force = size; size = null; } if (isObject(size) && size.force) force = true; if (!force) force = this.needRedrawByResize(); let changed = false; const redrawNext = indx => { if (!changed || (indx >= this.painters.length)) { this.confirmDraw(); return changed; } return getPromise(this.painters[indx].redraw(force ? 'redraw' : 'resize')).then(() => redrawNext(indx+1)); }; // return sync_promise.then(() => this.ensureBrowserSize(this.pad?.fCw, this.pad?.fCh)).then(() => { return sync_promise.then(() => { changed = this.createCanvasSvg(force ? 2 : 1, size); if (changed && this.iscan && this.pad && this.online_canvas && !this.embed_canvas && !this.isBatchMode()) { if (this._resize_tmout) clearTimeout(this._resize_tmout); this._resize_tmout = setTimeout(() => { delete this._resize_tmout; if (isFunc(this.sendResized)) this.sendResized(); }, 1000); // long enough delay to prevent multiple occurrence } // if canvas changed, redraw all its subitems. // If redrawing was forced for canvas, same applied for sub-elements return redrawNext(0); }); } /** @summary Update TPad object */ updateObject(obj) { if (!obj) return false; this.pad.fBits = obj.fBits; this.pad.fTitle = obj.fTitle; this.pad.fGridx = obj.fGridx; this.pad.fGridy = obj.fGridy; this.pad.fTickx = obj.fTickx; this.pad.fTicky = obj.fTicky; this.pad.fLogx = obj.fLogx; this.pad.fLogy = obj.fLogy; this.pad.fLogz = obj.fLogz; this.pad.fUxmin = obj.fUxmin; this.pad.fUxmax = obj.fUxmax; this.pad.fUymin = obj.fUymin; this.pad.fUymax = obj.fUymax; this.pad.fX1 = obj.fX1; this.pad.fX2 = obj.fX2; this.pad.fY1 = obj.fY1; this.pad.fY2 = obj.fY2; // this is main coordinates for sub-pad relative to canvas this.pad.fAbsWNDC = obj.fAbsWNDC; this.pad.fAbsHNDC = obj.fAbsHNDC; this.pad.fAbsXlowNDC = obj.fAbsXlowNDC; this.pad.fAbsYlowNDC = obj.fAbsYlowNDC; this.pad.fLeftMargin = obj.fLeftMargin; this.pad.fRightMargin = obj.fRightMargin; this.pad.fBottomMargin = obj.fBottomMargin; this.pad.fTopMargin = obj.fTopMargin; this.pad.fFillColor = obj.fFillColor; this.pad.fFillStyle = obj.fFillStyle; this.pad.fLineColor = obj.fLineColor; this.pad.fLineStyle = obj.fLineStyle; this.pad.fLineWidth = obj.fLineWidth; this.pad.fPhi = obj.fPhi; this.pad.fTheta = obj.fTheta; this.pad.fEditable = obj.fEditable; if (this.iscan) this.checkSpecialsInPrimitives(obj); const fp = this.getFramePainter(); 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.snapid === undefined)) { // 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._is_primary) return; for (let k = 0; k < this.painters.length; ++k) { const sub = this.painters[k]; if (sub.isSecondary(p) && sub.getSecondaryId()) { sub.assignSnapId(p.snapid + '#' + 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); // set global list of colors if (!this.options || this.options.GlobalColors) adoptRootColors(ListOfColors); const greyscale = this.pad?.TestBit(kIsGrayscale) ?? false, colors = extendRootColors(null, ListOfColors, greyscale); // copy existing colors and extend with new values this.#custom_colors = this.options?.LocalColors ? colors : null; // set palette if (snap.fSnapshot.fBuf && (!this.options || !this.options.IgnorePalette)) { const indexes = [], palette = []; for (let n = 0; n < snap.fSnapshot.fBuf.length; ++n) { indexes[n] = Math.round(snap.fSnapshot.fBuf[n]); palette[n] = colors[indexes[n]]; } this.#custom_palette_indexes = indexes; this.#custom_palette_colors = palette; this.#custom_palette = new ColorPalette(palette, greyscale); } else 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.snapid === snap.fObjectID) { objpainter = subp; break; } else if (subp.snapid && !subp.isSecondary() && !is_subpad) { console.warn(`Mismatch in snapid between painter ${subp?.snapid} 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, false); padpainter.decodeOptions(snap.fOption); padpainter.addToPadPrimitives(); 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 if (subpad.$disable_drawing) padpainter.pad_draw_disabled = true; padpainter.processSpecialSnaps(snap.fPrimitives); // need to process style and colors before creating graph elements padpainter.createPadSvg(); if (padpainter.matchObjectType(clTPad) && (snap.fPrimitives.length > 0)) padpainter.addPadButtons(true); 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.snapid === snapid) return this; if (!this.painters) return null; for (let k = 0; k < this.painters.length; ++k) { let sub = this.painters[k]; if (isFunc(sub.findSnap)) sub = sub.findSnap(snapid); else if (sub.snapid !== snapid) sub = null; if (sub) return sub; } return null; } /** @summary Redraw pad snap * @desc Online version of drawing pad primitives * for the canvas snapshot contains list of objects * as first entry, graphical properties of canvas itself is provided * in ROOT6 it also includes primitives, but we ignore them * @return {Promise} with pad painter when drawing completed * @private */ async redrawPadSnap(snap) { if (!snap?.fPrimitives) return this; this.is_active_pad = 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 = first.fExecs?.arr?.length > 0; if (this.snapid === undefined) { // first time getting snap, create all gui elements first this.assignSnapId(snap.fObjectID); this.assignObject(first); this.pad = first; // first object is pad // this._fixed_size = true; // if canvas size not specified in batch mode, temporary use 900x700 size if (this.isBatchMode() && (!first.fCw || !first.fCh)) { first.fCw = 900; first.fCh = 700; } // case of ROOT7 with always dummy TPad as first entry if (!first.fCw || !first.fCh) this._fixed_size = false; const mainid = this.selectDom().attr('id'); if (!this.isBatchMode() && this.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.iscan) 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 (!isStr(sub.snapid) || 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.snapid) { 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) { delete this.pads_cache; // invalidate pads cache const old_painters = this.painters; this.painters = []; old_painters.forEach(objp => objp.cleanup()); delete this.main_painter_ref; 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._websocket || !cp._websocket.canSend(2) || cp._readonly) return; const msg = JSON.stringify([this.snapid, kind, x.toString(), y.toString(), snapid ? snapid.toString() : '']); cp.sendWebsocket(`EVENT:${msg}`); } /** @summary Create image for the pad * @desc Used with web-based canvas to create images for server side * @return {Promise} with image data, coded with btoa() function * @private */ async createImage(format) { if ((format === 'png') || (format === 'jpeg') || (format === 'svg') || (format === 'webp') || (format === 'pdf')) { return this.produceImage(true, format).then(res => { if (!res || (format === 'svg')) return res; const separ = res.indexOf('base64,'); return (separ > 0) ? res.slice(separ+7) : ''; }); } return ''; } /** @summary Collects pad information for TWebCanvas * @desc need to update different states * @private */ getWebPadOptions(arg, cp) { let is_top = (arg === undefined), elem = null, scan_subpads = true; // no any options need to be collected in readonly mode if (is_top && this._readonly) return ''; if (arg === 'only_this') { is_top = true; scan_subpads = false; } else if (arg === 'with_subpads') { is_top = true; scan_subpads = true; } if (is_top) arg = []; if (!cp) cp = this.iscan ? this : this.getCanvPainter(); if (this.snapid) { elem = { _typename: 'TWebPadOptions', snapid: this.snapid.toString(), active: 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.iscan) { elem.bits = this.getStatusBits(); elem.cw = this.getPadWidth(); elem.ch = this.getPadHeight(); elem.w = [window.screenLeft, window.screenTop, window.outerWidth, window.outerHeight]; } else if (cp) { const cw = cp.getPadWidth(), ch = cp.getPadHeight(), rect = this.getPadRect(); elem.cw = cw; elem.ch = ch; elem.xlow = rect.x / cw; elem.ylow = 1 - (rect.y + rect.height) / ch; elem.xup = elem.xlow + rect.width / cw; elem.yup = elem.ylow + rect.height / ch; } if ((this.pad.fTheta !== 30) || (this.pad.fPhi !== 30)) { elem.phi = this.pad.fPhi; elem.theta = this.pad.fTheta; } if (this.getPadRanges(elem)) arg.push(elem); else console.log(`fail to get ranges for pad ${this.pad.fName}`); } this.painters.forEach(sub => { if (isFunc(sub.getWebPadOptions)) { if (scan_subpads) sub.getWebPadOptions(arg, cp); } else { const opt = createWebObjectOptions(sub); if (opt) elem.primitives.push(opt); } }); if (is_top) return toJSON(arg); } /** @summary returns actual ranges in the pad, which can be applied to the server * @private */ getPadRanges(r) { if (!r) return false; const main = this.getFramePainter(), p = this.svg_this_pad(); r.ranges = main?.ranges_set ?? false; // indicate that ranges are assigned r.ux1 = r.px1 = r.ranges ? main.scale_xmin : 0; // need to initialize for JSON reader r.uy1 = r.py1 = r.ranges ? main.scale_ymin : 0; r.ux2 = r.px2 = r.ranges ? main.scale_xmax : 0; r.uy2 = r.py2 = r.ranges ? main.scale_ymax : 0; r.uz1 = r.ranges ? (main.scale_zmin ?? 0) : 0; r.uz2 = r.ranges ? (main.scale_zmax ?? 0) : 0; if (main) { if (main.zoom_xmin !== main.zoom_xmax) { r.zx1 = main.zoom_xmin; r.zx2 = main.zoom_xmax; } if (main.zoom_ymin !== main.zoom_ymax) { r.zy1 = main.zoom_ymin; r.zy2 = main.zoom_ymax; } if (main.zoom_zmin !== main.zoom_zmax) { r.zz1 = main.zoom_zmin; r.zz2 = main.zoom_zmax; } } if (!r.ranges || p.empty()) return true; // calculate user range for full pad const func = (log, value, err) => { if (!log) return value; if (value <= 0) return err; value = Math.log10(value); if (log > 1) value /= Math.log10(log); return value; }, frect = main.getFrameRect(); r.ux1 = func(main.logx, r.ux1, 0); r.ux2 = func(main.logx, r.ux2, 1); let k = (r.ux2 - r.ux1)/(frect.width || 10); r.px1 = r.ux1 - k*frect.x; r.px2 = r.px1 + k*this.getPadWidth(); r.uy1 = func(main.logy, r.uy1, 0); r.uy2 = func(main.logy, r.uy2, 1); k = (r.uy2 - r.uy1)/(frect.height || 10); r.py1 = r.uy1 - k*frect.y; r.py2 = r.py1 + k*this.getPadHeight(); return true; } /** @summary Show context menu for specified item * @private */ itemContextMenu(name) { const rrr = this.svg_this_pad().node().getBoundingClientRect(), evnt = { clientX: rrr.left + 10, clientY: rrr.top + 10 }; // use timeout to avoid conflict with mouse click and automatic menu close if (name === 'pad') return postponePromise(() => this.padContextMenu(evnt), 50); let selp = null, selkind; switch (name) { case 'xaxis': case 'yaxis': case 'zaxis': selp = this.getFramePainter(); selkind = name[0]; break; case 'frame': selp = this.getFramePainter(); break; default: { const indx = parseInt(name); if (Number.isInteger(indx)) selp = this.painters[indx]; } } if (!isFunc(selp?.fillContextMenu)) return; return createMenu(evnt, selp).then(menu => { const offline_menu = selp.fillContextMenu(menu, selkind); if (offline_menu || selp.snapid) return selp.fillObjectExecMenu(menu, selkind).then(() => postponePromise(() => menu.show(), 50)); }); } /** @summary Save pad as image * @param {string} kind - format of saved image like 'png', 'svg' or 'jpeg' * @param {boolean} full_canvas - does complete canvas (true) or only frame area (false) should be saved * @param {string} [filename] - name of the file which should be stored * @desc Normally used from context menu * @example * import { getElementCanvPainter } from 'https://root.cern/js/latest/modules/base/ObjectPainter.mjs'; * let canvas_painter = getElementCanvPainter('drawing_div_id'); * canvas_painter.saveAs('png', true, 'canvas.png'); */ saveAs(kind, full_canvas, filename) { if (!filename) filename = (this.this_pad_name || (this.iscan ? 'canvas' : 'pad')) + '.' + kind; this.produceImage(full_canvas, kind).then(imgdata => { if (!imgdata) return console.error(`Fail to produce image ${filename}`); if ((browser.qt6 || browser.cef3) && this.snapid) { console.warn(`sending file ${filename} to server`); let res = imgdata; if (kind !== 'svg') { const separ = res.indexOf('base64,'); res = (separ > 0) ? res.slice(separ+7) : ''; } if (res) this.getCanvPainter()?.sendWebsocket(`SAVE:${filename}:${res}`); } else { const prefix = (kind === 'svg') ? prSVG : (kind === 'json' ? prJSON : ''); saveFile(filename, prefix ? prefix + encodeURIComponent(imgdata) : imgdata); } }); } /** @summary Search active pad * @return {Object} pad painter for active pad */ findActivePad() { let active_pp; this.forEachPainterInPad(pp => { if (pp.is_active_pad && !active_pp) active_pp = pp; }, 'pads'); return active_pp; } /** @summary Produce image for the pad * @return {Promise} with created image */ async produceImage(full_canvas, file_format, args) { if (file_format === 'json') return isFunc(this.produceJSON) ? this.produceJSON(full_canvas ? 2 : 0) : ''; const use_frame = (full_canvas === 'frame'), elem = use_frame ? this.getFrameSvg(this.this_pad_name) : (full_canvas ? this.getCanvSvg() : this.svg_this_pad()), painter = (full_canvas && !use_frame) ? this.getCanvPainter() : this, items = []; // keep list of replaced elements, which should be moved back at the end if (elem.empty()) return ''; if (use_frame || !full_canvas) { const defs = this.getCanvSvg().selectChild('.canvas_defs'); if (!defs.empty()) { items.push({ prnt: this.getCanvSvg(), defs }); elem.node().insertBefore(defs.node(), elem.node().firstChild); } } let active_pp = null; painter.forEachPainterInPad(pp => { if (pp.is_active_pad && !active_pp) { active_pp = pp; active_pp.drawActiveBorder(null, false); } if (use_frame) return; // do not make transformations for the frame const item = { prnt: pp.svg_this_pad() }; items.push(item); // remove buttons from each sub-pad const btns = pp.getLayerSvg('btns_layer', pp.this_pad_name); item.btns_node = btns.node(); if (item.btns_node) { item.btns_prnt = item.btns_node.parentNode; item.btns_next = item.btns_node.nextSibling; btns.remove(); } const fp = pp.getFramePainter(); if (!isFunc(fp?.access3dKind)) return; const can3d = fp.access3dKind(); if ((can3d !== constants$1.Embed3D.Overlay) && (can3d !== constants$1.Embed3D.Embed)) return; let main, canvas; if (isFunc(fp.render3D)) { main = fp; canvas = fp.renderer?.domElement; } else { main = fp.getMainPainter(); canvas = main?._renderer?.domElement; } if (!isFunc(main?.render3D) || !isObject(canvas)) return; const sz2 = fp.getSizeFor3d(constants$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 = main.getFrameSvg(); item.frame_node = svg_frame.node(); if (item.frame_node) { item.frame_next = item.frame_node.nextSibling; svg_frame.remove(); } // add svg image item.img = item.prnt.insert('image', '.primitives_layer') // create image object .attr('x', sz2.x) .attr('y', sz2.y) .attr('width', canvas.width) .attr('height', canvas.height) .attr('href', dataUrl); }, 'pads'); let width = elem.property('draw_width'), height = elem.property('draw_height'), 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); } const arg = (file_format === 'pdf') ? { node: elem.node(), width, height, scale, reset_tranform: use_frame } : compressSVG(`${elem.node().innerHTML}`); return svgToImage(arg, file_format, args).then(res => { // reactivate border active_pp?.drawActiveBorder(null, true); for (let k = 0; k < items.length; ++k) { const item = items[k]; item.img?.remove(); // delete embed image const prim = item.prnt.selectChild('.primitives_layer'); if (item.foreign) // reinsert foreign object item.prnt.node().insertBefore(item.foreign.node(), prim.node()); if (item.frame_node) // reinsert frame as first in list of primitives prim.node().insertBefore(item.frame_node, item.frame_next); if (item.btns_node) // reinsert buttons item.btns_prnt.insertBefore(item.btns_node, item.btns_next); if (item.defs) // reinsert defs item.prnt.node().insertBefore(item.defs.node(), item.prnt.node().firstChild); } return res; }); } /** @summary Process pad button click */ clickPadButton(funcname, evnt) { if (funcname === 'CanvasSnapShot') return this.saveAs('png', true); if (funcname === 'enlargePad') return this.enlargePad(); if (funcname === 'PadSnapShot') return this.saveAs('png', false); if (funcname === 'PadContextMenus') { evnt?.preventDefault(); evnt?.stopPropagation(); if (closeMenu()) return; return createMenu(evnt, this).then(menu => { menu.header('Menus'); if (this.iscan) menu.add('Canvas', 'pad', this.itemContextMenu); else menu.add('Pad', 'pad', this.itemContextMenu); if (this.getFramePainter()) menu.add('Frame', 'frame', this.itemContextMenu); const main = this.getMainPainter(); // here pad painter method if (main) { menu.add('X axis', 'xaxis', this.itemContextMenu); menu.add('Y axis', 'yaxis', this.itemContextMenu); if (isFunc(main.getDimension) && (main.getDimension() > 1)) menu.add('Z axis', 'zaxis', this.itemContextMenu); } if (this.painters?.length) { menu.separator(); const shown = []; this.painters.forEach((pp, indx) => { const obj = pp?.getObject(); if (!obj || (shown.indexOf(obj) >= 0)) return; let name; 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 }); const iscan = this.iscan || !this.has_canvas; if (!iscan && (funcname.indexOf('Pad') !== 0) && (funcname !== 'enlargePad')) { const cp = this.getCanvPainter(); if (cp && (cp !== this)) cp.addPadButton(btn, tooltip, funcname); } } /** @summary Show pad buttons * @private */ showPadButtons() { if (!this._buttons) return; PadButtonsHandler.assign(this); this.showPadButtons(); } /** @summary Add buttons for pad or canvas * @private */ addPadButtons(is_online) { this.addPadButton('camera', 'Create PNG', this.iscan ? 'CanvasSnapShot' : 'PadSnapShot', 'Ctrl PrintScreen'); if (settings.ContextMenu) this.addPadButton('question', 'Access context menus', 'PadContextMenus'); const add_enlarge = !this.iscan && this.has_canvas && this.hasObjectsToDraw(); if (add_enlarge || this.enlargeMain('verify')) this.addPadButton('circle', 'Enlarge canvas', 'enlargePad'); if (is_online && this.brlayout) { this.addPadButton('diamand', 'Toggle Ged', 'ToggleGed'); this.addPadButton('three_circles', 'Toggle Status', 'ToggleStatus'); } } /** @summary Decode pad draw options * @private */ decodeOptions(opt) { const pad = this.getObject(); if (!pad) return; const d = new DrawOptions(opt); if (!this.options) this.options = {}; Object.assign(this.options, { GlobalColors: true, LocalColors: false, CreatePalette: 0, IgnorePalette: false, RotateFrame: false, FixFrame: false }); if (d.check('NOCOLORS') || d.check('NOCOL')) this.options.GlobalColors = this.options.LocalColors = false; if (d.check('LCOLORS') || d.check('LCOL')) { this.options.GlobalColors = false; this.options.LocalColors = true; } if (d.check('NOPALETTE') || d.check('NOPAL')) this.options.IgnorePalette = true; if (d.check('ROTATE')) this.options.RotateFrame = true; if (d.check('FIXFRAME')) this.options.FixFrame = true; if (d.check('FIXSIZE') && this.iscan) this._fixed_size = true; if (d.check('CP', true)) this.options.CreatePalette = d.partAsInt(0, 0); if (d.check('NOZOOMX')) this.options.NoZoomX = true; if (d.check('NOZOOMY')) this.options.NoZoomY = true; if (d.check('GRAYSCALE')) pad.SetBit(kIsGrayscale, true); function forEach(func, p) { if (!p) p = pad; func(p); const arr = p.fPrimitives?.arr || []; for (let i = 0; i < arr.length; ++i) { if (arr[i]._typename === clTPad) forEach(func, arr[i]); } } if (d.check('NOMARGINS')) forEach(p => { p.fLeftMargin = p.fRightMargin = p.fBottomMargin = p.fTopMargin = 0; }); if (d.check('WHITE')) forEach(p => { p.fFillColor = 0; }); if (d.check('LOG2X')) forEach(p => { p.fLogx = 2; p.fUxmin = 0; p.fUxmax = 1; p.fX1 = 0; p.fX2 = 1; }); if (d.check('LOGX')) forEach(p => { p.fLogx = 1; p.fUxmin = 0; p.fUxmax = 1; p.fX1 = 0; p.fX2 = 1; }); if (d.check('LOG2Y')) forEach(p => { p.fLogy = 2; p.fUymin = 0; p.fUymax = 1; p.fY1 = 0; p.fY2 = 1; }); if (d.check('LOGY')) forEach(p => { p.fLogy = 1; p.fUymin = 0; p.fUymax = 1; p.fY1 = 0; p.fY2 = 1; }); if (d.check('LOG2Z')) forEach(p => { p.fLogz = 2; }); if (d.check('LOGZ')) forEach(p => { p.fLogz = 1; }); if (d.check('LOGV')) forEach(p => { p.fLogv = 1; }); if (d.check('LOG2')) forEach(p => { p.fLogx = p.fLogy = p.fLogz = 2; }); if (d.check('LOG')) forEach(p => { p.fLogx = p.fLogy = p.fLogz = 1; }); if (d.check('LNX')) forEach(p => { p.fLogx = 3; p.fUxmin = 0; p.fUxmax = 1; p.fX1 = 0; p.fX2 = 1; }); if (d.check('LNY')) forEach(p => { p.fLogy = 3; p.fUymin = 0; p.fUymax = 1; p.fY1 = 0; p.fY2 = 1; }); if (d.check('LN')) forEach(p => { p.fLogx = p.fLogy = p.fLogz = 3; }); if (d.check('GRIDX')) forEach(p => { p.fGridx = 1; }); if (d.check('GRIDY')) forEach(p => { p.fGridy = 1; }); if (d.check('GRID')) forEach(p => { p.fGridx = p.fGridy = 1; }); if (d.check('TICKX')) forEach(p => { p.fTickx = 1; }); if (d.check('TICKY')) forEach(p => { p.fTicky = 1; }); if (d.check('TICKZ')) forEach(p => { p.fTickz = 1; }); if (d.check('TICK')) forEach(p => { p.fTickx = p.fTicky = 1; }); ['OTX', 'OTY', 'CTX', 'CTY', 'NOEX', 'NOEY', 'RX', 'RY'].forEach(name => { if (d.check(name)) forEach(p => { p['$' + name] = true; }); }); this.storeDrawOpt(opt); } /** @summary draw TPad object */ static async draw(dom, pad, opt) { const painter = new TPadPainter(dom, pad, false); painter.decodeOptions(opt); if (painter.getCanvSvg().empty()) { // one can draw pad without canvas painter.has_canvas = false; painter.this_pad_name = ''; painter.setTopPainter(); } else { // pad painter will be registered in the parent pad painter.addToPadPrimitives(); } if (pad?.$disable_drawing) painter.pad_draw_disabled = true; painter.createPadSvg(); if (painter.matchObjectType(clTPad) && (!painter.has_canvas || painter.hasObjectsToDraw())) painter.addPadButtons(); // set active pad selectActivePad({ pp: painter, active: true }); // flag used to prevent immediate pad redraw during first draw return painter.drawPrimitives().then(() => { painter.showPadButtons(); painter.addPadInteractive(); return painter; }); } } // class TPadPainter 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 { /** @summary Constructor */ constructor(dom, canvas) { super(dom, canvas, true); this._websocket = null; this.tooltip_allowed = settings.Tooltip; } /** @summary Cleanup canvas painter */ cleanup() { if (this._changed_layout) this.setLayoutKind('simple'); delete this._changed_layout; 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.pad, main = this.getFramePainter(); let drawopt; if (kind === 'X') { canv.fLeftMargin = pad.fLeftMargin; canv.fRightMargin = pad.fRightMargin; canv.fLogx = main.logx; canv.fUxmin = main.logx ? Math.log10(main.scale_xmin) : main.scale_xmin; canv.fUxmax = main.logx ? Math.log10(main.scale_xmax) : main.scale_xmax; drawopt = 'fixframe'; } else if (kind === 'Y') { canv.fBottomMargin = pad.fBottomMargin; canv.fTopMargin = pad.fTopMargin; canv.fLogx = main.logy; canv.fUxmin = main.logy ? Math.log10(main.scale_ymin) : main.scale_ymin; canv.fUxmax = main.logy ? Math.log10(main.scale_ymax) : main.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._readonly || !painter) return; if (!snapid) snapid = painter.snapid; if (snapid && isStr(snapid) && exec) return this.sendWebsocket(`OBJEXEC:${snapid}:${exec}`); } /** @summary Return true if message can be send via web socket * @private */ canSendWebSocket() { return this._websocket?.canSend(); } /** @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(); delete this._websocket; } } /** @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.snapid) this.resizeBrowser(snap.fSnapshot.fWindowWidth, snap.fSnapshot.fWindowHeight); if (!this.snapid && 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); delete this._getmenu_callback; } } 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) { if (!this.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) && ((this.pad.fCw !== cw) || (this.pad.fCh !== ch)); if (force) { this.pad.fCw = cw; this.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('
Loading GED ...
'); 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._websocket) return false; const new_conn = standalone ? null : this._websocket.createChannel(); this.sendWebsocket('FITPANEL:' + (standalone ? 'standalone' : new_conn.getChannelId())); return new_conn; } /** @summary Complete handling of online canvas drawing * @private */ completeCanvasSnapDrawing() { if (!this.pad) return; this.addPadInteractive(); if ((typeof document !== 'undefined') && !this.embed_canvas && this._websocket) document.title = this.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', this.pad.TestBit(kMenuBar)); this.showSection('StatusBar', this.pad.TestBit(kShowEventStatus)); this.showSection('ToolBar', this.pad.TestBit(kShowToolBar)); this.showSection('Editor', this.pad.TestBit(kShowEditor)); this.showSection('ToolTips', this.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 || hints.length === 0 || !this._highlight_connect || !this._websocket || this.doingDraw() || !this._websocket.canSend(2)) return; const hint = hints[0] || hints[1]; if (!hint || !hint.painter || !hint.painter.snapid || !hint.user_info) return; const pp = hint.painter.getPadPainter() || this; if (!pp.snapid) return; const arr = [pp.snapid, hint.painter.snapid, '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._websocket || this._readonly || !this._websocket.canSend(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.snapid) msg = 'DRAWOPT:' + JSON.stringify([painter.snapid.toString(), 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(); if (pp?.snapid && pp?.pad) { const name = 'SetLog' + kind[3], value = pp.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._websocket.send(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.snapid || !pad_painter) return; // only interactive canvas let arg = null, ischanged = false; const is_button = pad_painter.matchObjectType(clTButton); if (pad_painter.snapid && this._websocket) arg = { _typename: 'TWebPadClick', padid: pad_painter.snapid.toString(), 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?.snapid !== undefined) && arg) { ischanged = true; arg.objid = obj_painter.snapid.toString(); } 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(), fill0 = (canv.fFillStyle === 0), axes = [], hists = []; if (fill0) canv.fFillStyle = 1001; // write selected range into TAxis properties this.forEachPainterInPad(pp => { const main = pp.getMainPainter(), fp = pp.getFramePainter(); if (!isFunc(main?.getHisto) || !isFunc(main?.getDimension)) return; 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'); if (!this.normal_canvas) { // fill list of primitives from painters this.forEachPainterInPad(p => { // ignore all secondary painters if (p.isSecondary()) return; const subobj = p.getObject(); if (subobj?._typename) canv.fPrimitives.Add(subobj, p.getDrawOpt()); }, 'objects'); } // const fp = this.getFramePainter(); // fp?.setRootPadRange(this.getRootPad()); 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; }); if (!this.normal_canvas) canv.fPrimitives.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._websocket?.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); 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._fixed_size = true; } } painter.decodeOptions(opt); painter.normal_canvas = !nocanvas; 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 * 0.1; canv.fX2 = ranges.maxx + dx * 0.1; canv.fY1 = ranges.miny - dy * 0.1; canv.fY2 = ranges.maxy + dy * 0.1; return canv; }, promise = painter.getCanvSvg().empty() ? TCanvasPainter.draw(painter.getDom(), createCanv(), noframe) : Promise.resolve(true); return promise.then(() => { if ((frame_kind !== false) && painter.getFrameSvg().selectChild('.main_layer').empty() && !painter.getFramePainter()) directDrawTFrame(painter.getPadPainter(), null, frame_kind); painter.addToPadPrimitives(); 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); painter.normal_canvas = false; 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 { /** @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.getFrameSvg().selectChild('.main_layer'); let svg_code = main_svg.node().outerHTML; svg_code = compressSVG(svg_code); svg_code = ` { 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._fast_drawing) { 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); // container used to recalculate coordinates this.createG(); 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(this.draw_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(this.draw_g, width, height, 0, dpath); interactive_element = this.draw_g.append('svg:path') .attr('d', 'M0,'+h2 +dpath) .call(this.fillatt.func) .call(this.lineatt.func); const text_g = this.draw_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) { this.draw_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(this.draw_g, width, height, arc_radius); if (!this.isBatchMode() || !this.fillatt.empty() || (!this.lineatt.empty() && !noborder)) { if (arc_radius) { interactive_element = this.draw_g.append('svg:rect') .attr('width', width) .attr('height', height) .attr('rx', arc_radius); } else { interactive_element = this.draw_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); } return isFunc(this.paveDrawFunc) ? this.paveDrawFunc(width, height, arg) : true; }).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, redraw: () => { this.moved_interactive = true; this.interactiveRedraw(false, 'pave_moved'); this.drawPave(); }, ctxmenu: browser.touches && settings.ContextMenu && this.UseContextMenu }); if (this.UseContextMenu && settings.ContextMenu) this.draw_g.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${-brd+1}v${-height+2}h${width-2}v${brd-1}z`; else if ((dx < 0) && (dy > 0)) spath = `M0,${height}v${brd+1-height}h${-brd+1}v${height-2}h${width-2}v${-brd+1}z`; else if ((dx > 0) && (dy < 0)) spath = `M${brd+1},0v${-brd+1}h${width-2}v${height-2}h${-brd+1}v${brd+1-height}z`; else spath = `M${width},${brd+1}h${brd-1}v${height-2}h${-width+2}v${-brd+1}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.draw_g); } }; 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.draw_g.append('svg:path').attr('d', lpath).call(this.lineatt.func); // this.draw_g.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 > 0), 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.draw_g; const fast = (nlines === 1) && pp._fast_drawing; 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.draw_g.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, 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 (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 (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, this.draw_g, 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 i = ii; 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.draw_g.append('svg:path') .attr('d', `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.draw_g.append('svg:path') .attr('d', `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.draw_g.append('svg:path') .attr('d', 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.draw_g .append('svg:path') .attr('d', 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.draw_g.append('svg:path') .attr('d', `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; if (isStr(lopt) && (lopt.toLowerCase() !== 'h')) any_opt = true; else if (!any_opt) pos_x = x0; if (entry.fLabel) { const textatt = this.createAttText({ attr: entry, std: false, attr_alt: legend }), arg = { draw_g: this.draw_g, align: textatt.align, x: pos_x, width: Math.round(column_pos[icol + 1] - pos_x), 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.draw_g.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 draw color palette with axis */ drawPaletteAxis(s_width, s_height, arg) { const palette = this.getObject(), axis = palette.fAxis, 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(), framep = this.getFramePainter(), contour = main.fContour, 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._color_palette, 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 && framep && !is_th3) { if ((framep.zmin !== undefined) && (framep.zmax !== undefined) && (framep.zmin !== framep.zmax)) { gzmin = framep.zmin; gzmax = framep.zmax; zmin = framep.zoom_zmin; zmax = framep.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; } this.draw_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.draw_g.append('svg:path') .attr('d', `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])), lvl = (levels[i] + levels[i+1])*0.5, d; if (this._palette_vertical) { if ((z1 >= s_height) || (z0 < 0)) continue; z0 += 1; // ensure correct gap filling between colors if (z0 > s_height) { z0 = s_height; lvl = levels[i]*0.001 + levels[i+1]*0.999; if (z1 < 0) z1 = 0; } else if (z1 < 0) { z1 = 0; lvl = levels[i]*0.999 + levels[i+1]*0.001; } 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) { z1 = s_width; lvl = levels[i]*0.999 + levels[i+1]*0.001; if (z0 < 0) z0 = 0; } else if (z0 < 0) { z0 = 0; lvl = levels[i]*0.001 + levels[i+1]*0.999; } d = `M${z0},0V${s_height}H${z1}V0Z`; } const col = contour.getPaletteColor(draw_palette, lvl); if (!col) continue; const r = this.draw_g.append('svg:path') .attr('d', 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(this.draw_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 this.draw_g.node()) rect = this.draw_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(this.draw_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(this.draw_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.draw_g.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.draw_g.node()); zoom_rect = this.draw_g.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.draw_g.selectAll('.axis_zoom') .on('mousedown', startRectSel) .on('dblclick', () => this.getFramePainter().zoomSingle('z', 0, 0, true)); } if (settings.ZoomWheel) { this.draw_g.on('wheel', evnt => { const pos = pointer(evnt, this.draw_g.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) return; pave.fStatFormat = fmt; this.interactiveRedraw(true, `exec:SetStatFormat("${fmt}")`); }); }); menu.add('SetFitFormat', () => { menu.input('Enter FitFormat', pave.fFitFormat).then(fmt => { if (!fmt) return; 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 = false; if (!dofit || !f1) return false; this._has_fit = true; 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 = ''; parerr = null; } else { parvalue = this.format(Number(parvalue), 'fit'); if (print_ferrors && (parerr !== undefined)) { parerr = floatToString(parerr, this.getBestFormat(parvalue, parerr)); if ((Number(parerr) === 0) && (f1.GetParError(n) !== 0)) 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) { if (!p) return true; return !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 prev_painter = painter.getPadPainter().findPainterFor(null, kTitle, clTPaveText); if (prev_painter && (prev_painter !== painter)) { prev_painter.removeFromPadPrimitives(); prev_painter.cleanup(); } 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; } painter.NoFillStats = pave.fName !== 'stats'; switch (pave._typename) { case clTPaveLabel: case clTPaveClass: painter.paveDrawFunc = painter.drawPaveLabel; break; case clTPaveStats: painter.paveDrawFunc = painter.drawPaveStats; break; case clTPaveText: case clTPavesText: case clTDiamond: painter.paveDrawFunc = painter.drawPaveText; break; case clTLegend: painter.AutoPlace = is_auto; painter.paveDrawFunc = painter.drawLegend; break; case clTPaletteAxis: painter.paveDrawFunc = painter.drawPaletteAxis; break; } 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; /** * @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, 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)) { 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; if (d.check('HMIN:', true)) { this.ohmin = true; this.hmin = parseFloat(d.part); } else { this.ohmin = false; delete this.hmin; } if (d.check('HMAX:', true)) { this.ohmax = true; this.hmax = parseFloat(d.part); } else { this.ohmax = false; delete this.hmax; } 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.part.toLowerCase()); if (d.check('XTITLE:', true)) histo.fXaxis.fTitle = decodeURIComponent(d.part.toLowerCase()); if (d.check('YTITLE:', true)) histo.fYaxis.fTitle = decodeURIComponent(d.part.toLowerCase()); if (d.check('ZTITLE:', true)) histo.fZaxis.fTitle = decodeURIComponent(d.part.toLowerCase()); 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 (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('LINE_', 'color')) this.histoLineColor = 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 = true; this.IgnoreMainScale = true; } if (d.check('SAMES')) { this.Same = true; this.ForceStat = true; } if (d.check('SAME')) { this.Same = true; 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; } } // 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 !== 0)) { 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 { constructor(painter, pp, funcs, statpainter) { this.painter = painter; this.pp = 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); } // remove all function which are not found in new list of functions if (painters.length > 0) pp?.cleanPrimitives(p => painters.indexOf(p) >= 0); if (update_painters.length > 0) this._extraPainters = update_painters; } /** @summary Draw/update functions selected before */ drawNext(indx) { if (this._extraPainters) { const p = this._extraPainters.shift(); if (this._extraPainters.length === 0) delete this._extraPainters; return getPromise(p.redraw()).then(() => this.drawNext(0)); } if (!this.newfuncs || (indx >= this.newfuncs.length)) { delete this.newfuncs; delete this.newopts; return Promise.resolve(this.painter); // simplify drawing } const func = this.newfuncs[indx], fopt = this.newopts[indx]; if (!func || this.pp?.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.pp, func, fopt) : this.pp.drawObject(this.pp, 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 { /** @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(); delete this._color_palette; delete this.fContour; delete this.options; 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); if (!this.options) this.options = new THistDrawOptions(); else this.options.reset(); // when changing draw option, reset attributes usage this.lineatt?.setUsed(false); this.fillatt?.setUsed(false); this.markeratt?.setUsed(false); this.options.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.options, o0 = src.options; 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.options.Same ? 'pad_range' : true; } /** @summary Create necessary histogram draw attributes */ createHistDrawAttributes(only_check_auto) { const histo = this.getHisto(), o = this.options; 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 = ''; // can be reused when sending option back to server if (o._pfc > 1) { o._pfc = 1; histo.fFillColor = icolor; this._auto_exec += `SetFillColor(${icolor});;`; delete this.fillatt; } if (o._plc > 1) { o._plc = 1; histo.fLineColor = icolor; this._auto_exec += `SetLineColor(${icolor});;`; delete this.lineatt; } if (o._pmc > 1) { o._pmc = 1; histo.fMarkerColor = icolor; this._auto_exec += `SetMarkerColor(${icolor});;`; delete this.markeratt; } } } if (only_check_auto) this.deleteAttr(); else { this.createAttFill({ attr: histo, color: this.options.histoFillColor, pattern: this.options.histoFillPattern, kind: 1 }); this.createAttLine({ attr: histo, color0: this.options.histoLineColor, width: this.options.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.snapid && !fp?.zoomChangedInteractive('x')); copyTAxisMembers(tgt_histo.fYaxis, src_histo.fYaxis, this.snapid && !fp?.zoomChangedInteractive('y')); copyTAxisMembers(tgt_histo.fZaxis, src_histo.fZaxis, this.snapid && !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.options; 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) && !this.options.NoStat; // && (!this.options.Same || this.options.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.snapid !== undefined) { 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._funcHandler = new FunctionsHandler(this, pp, obj.fFunctions, statpainter); 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'; if (v === undefined) return this.options[name]; this.options[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(); 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; this._exact_y_range = (ndim === 1) && this.options.ohmin && this.options.ohmax; if (this._exact_y_range) { this.ymin = this.options.hmin; this.ymax = this.options.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) && this.options.ohmin && this.options.ohmax) { this.zmin = this.options.hmin; this.zmax = this.options.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(); // artificially add y range to display axes if (this.ymin === this.ymax) this.ymax += 1; if (!this.isMainPainter()) { const opts = { second_x: (this.options.AxisPos >= 10), second_y: (this.options.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: this.options.swap_xy(), reverse_x: this.options.RevX, reverse_y: this.options.RevY, symlog_x: this.options.SymlogX, symlog_y: this.options.SymlogY, Proj: this.options.Proj, extra_y_space: this.options.Text && (this.options.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 (this.options.Same) return false; const disable_axis_draw = (this.options.Axis < 0) || (this.options.Axis === 2); return fp.drawAxes(false, disable_axis_draw, disable_axis_draw, this.options.AxisPos, this.options.Zscale && this.options.Zvert, this.options.Zscale && !this.options.Zvert, this.options.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; delete this._auto_exec; } } /** @summary Toggle histogram title drawing */ toggleTitle(arg) { const histo = this.getHisto(); if (!this.isMainPainter() || !histo) return false; if (arg === 'only-check') 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() { // case when histogram drawn over other histogram (same option) if (!this.isMainPainter() || this.options.Same || (this.options.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() { // case when histogram drawn over other histogram (same option) if (!this.isMainPainter() || this.options.Same || (this.options.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.options.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.snapid) return; const stat = this.findStat(), statpainter = this.getPadPainter()?.findPainterFor(stat); if (statpainter && !statpainter.snapid) 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 === 'only-check') 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.options.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.snapid); // || (this.options.Axis > 0); } /** @summary Create stat box for histogram if required */ createStat(force) { const histo = this.getHisto(); if (!histo) return null; if (!force && !this.options.ForceStat) { if (this.options.NoStat || histo.TestBit(kNoStats) || !settings.AutoStat) return null; if (!this.isMainPainter()) return null; } const st = gStyle; let stats = this.findStat(), optstat = this.options.optstat, optfit = this.options.optfit; if (optstat !== undefined) { if (stats) stats.fOptStat = optstat; delete this.options.optstat; } else optstat = histo.$custom_stat || st.fOptStat; if (optfit !== undefined) { if (stats) stats.fOptFit = optfit; delete this.options.optfit; } 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) { if (func._typename === clTPaveStats) return (func.fName !== 'stats') || (!histo.TestBit(kNoStats) && !this.options.NoStat); // && (!this.options.Same || this.options.ForceStat)) if ((func._typename === clTF1) || (func._typename === clTF2)) return this.options.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._funcHandler?.drawNext(0) ?? this; delete this._funcHandler; 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; if (this.options.second_x && axis === 'x') axis = 'x2'; if (this.options.second_y && axis === 'y') axis = 'y2'; const main = this.getFramePainter(), min = main ? main[`zoom_${axis}min`] : 0, max = main ? main[`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(); 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 ((this.options.minimum === kNoZoom) && (this.options.maximum === kNoZoom)) return false; if (!this.draw_content) return false; // if not drawing content, not change min/max this.options.minimum = this.options.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.options.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) { let curr; if ((this.options.minimum !== kNoZoom) && (this.options.maximum !== kNoZoom)) curr = `[${this.options.minimum},${this.options.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])) this.options.minimum = this.options.maximum = kNoZoom; else { this.options.minimum = res[0]; this.options.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(); if (!histo) return; if ((this.options.Axis <= 0) && !this.isTF1()) menu.addchk(this.toggleStat('only-check'), 'Show statbox', () => this.toggleStat()); if (this.isMainPainter()) { menu.sub('Title'); menu.addchk(this.toggleTitle('only-check'), '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 (this.options.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(!this.options.Zero, 'Suppress zeros', () => { this.options.Zero = !this.options.Zero; this.interactiveRedraw('pad'); }); if ((this.options.Lego === 12) || (this.options.Lego === 14)) { menu.addchk(this.options.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.snapid) return ''; let res = this.snapid.toString(); if (subelem) { res += '#'; if (this.isTF1() && (subelem === 'x' || subelem === 'y' || subelem === 'z')) res += 'hist#'; res += subelem; } return res; } /** @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(); 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(this.options.Zero && !is_th2poly ? -1 : 0, (cntr.colzmin !== 0) || !this.options.Zero || is_th2poly ? 0 : -1); if (fp && (ndim < 3) && !fp.mode3d) { fp.zmin = cntr.colzmin; fp.zmax = cntr.colzmax; } this.fContour = cntr; return cntr; } /** @summary Return contour object */ getContour(force_recreate) { if (this.fContour && !force_recreate) return this.fContour; const main = this.getMainPainter(), fp = this.getFramePainter(); if (main?.fContour && (main !== this) && !this.options.IgnoreMainScale) { this.fContour = main.fContour; return this.fContour; } // 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(), src = (this !== main) && (main?.minbin !== undefined) && !this.options.IgnoreMainScale && !main?.tt_handle?.ScatterPlot ? main : this; let nlevels = 0, apply_min, zmin = src.minbin, zmax = src.maxbin, zminpos = src.minposbin, custom_levels; if (zmin === zmax) { if (this.options.ohmin && this.options.ohmax && this.options.Zscale) { zmin = this.options.hmin; zmax = this.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 (this.options.minimum !== kNoZoom) { zmin = this.options.minimum; gzmin = Math.min(gzmin, zmin); apply_min = true; } if (this.options.maximum !== kNoZoom) { zmax = this.options.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; } if (histo.fContour?.length > 1) { if (histo.TestBit(kUserContour)) custom_levels = histo.fContour; else nlevels = histo.fContour.length; } const cntr = this.createContour(nlevels, zmin, zmax, zminpos, custom_levels); if ((this.getDimension() < 3) && fp) { fp.zmin = gzmin; fp.zmax = gzmax; if ((gzmin !== cntr.colzmin) || (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) { if (force) this._color_palette = null; const pp = this.getPadPainter(); if (!this._color_palette && !this.options.Palette) { if (isFunc(pp?.getCustomPalette)) this._color_palette = pp.getCustomPalette(); } if (!this._color_palette) this._color_palette = getColorPalette(this.options.Palette, pp?.isGrayscale()); return this._color_palette; } /** @summary Fill menu entries for palette */ fillPaletteMenu(menu, only_palette) { menu.addPaletteMenu(this.options.Palette || settings.Palette, arg => { this.options.Palette = parseInt(arg); this.getHistPalette(true); this.redraw(); // redraw histogram }); if (!only_palette) { menu.add('Default position', () => { this.drawColorPalette(this.options.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 => { this.options.Zvert = flag; this.drawColorPalette(this.options.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) { // in special cases like scatter palette drawing is ignored if (this.options.IgnorePalette) return null; // only when create new palette, one could change frame size const mp = this.getMainPainter(), pp = this.getPadPainter(); if (mp !== this) { if (mp && (mp.draw_content !== false) && mp.options.Zscale) return null; } let pal = this.findFunction(clTPaletteAxis), pal_painter = pp?.findPainterFor(pal); const found_in_func = Boolean(pal); if (this._can_move_colz) { delete this._can_move_colz; if (!can_move) can_move = true; } if (!pal_painter && !pal && !this.options.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 && !this.options.Same) { this.options.Zvert = pal_painter._palette_vertical; 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 = !this.options.Same; pal.fInit = 1; pal.$can_move = can_move; pal.$generated = true; if (this.options.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?._palette_vertical !== undefined) this.options.Zvert = pal_painter._palette_vertical; const fp = this.getFramePainter(); // keep palette width if (can_move && fp && pal.$can_move) { if (this.options.Zvert) { if (can_move === '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 (can_move === '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.do_redraw_palette) arg += ';can_move'; if (this.options.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 this.options.Zvert = pal_painter._palette_vertical; // 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.do_redraw_palette) { const pad = pp?.getRootPad(true); if (this.options.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.do_redraw_palette = true; fp.redraw(); const pr2 = !postpone_draw ? this.redraw() : Promise.resolve(true); return pr2.then(() => { delete this.do_redraw_palette; return pal_painter; }); }); } /** @summary Toggle color z palette drawing */ toggleColz() { if (this.options.canHavePalette()) { this.options.Zscale = !this.options.Zscale; return this.drawColorPalette(this.options.Zscale, false, true) .then(() => this.processOnlineChange('drawopt')); } } /** @summary Toggle 3D drawing mode */ toggleMode3D() { this.options.Mode3D = !this.options.Mode3D; if (this.options.Mode3D) { if (!this.options.Surf && !this.options.Lego && !this.options.Error) { if ((this.nbinsx >= 50) || (this.nbinsy >= 50)) this.options.Lego = this.options.Scat ? 13 : 14; else this.options.Lego = this.options.Scat ? 1 : 12; this.options.Zero = false; // do not show zeros by default } } this.copyOptionsToOthers(); return this.interactiveRedraw('pad', 'drawopt'); } /** @summary Get graphics conversion functions for this histogram */ getHistGrFuncs(fp, rounding = true) { if (!this._ignore_frame) return fp?.getGrFuncs(this.options.second_x, this.options.second_y); const funcs = this.getAxisToSvgFunc(false, rounding, false); if (funcs) { funcs.painter = this; funcs.grx = funcs.x; funcs.gry = funcs.y; funcs.logx = funcs.pad?.fLogx; funcs.logy = funcs.pad?.fLogy; funcs.getFrameWidth = function() { return this.painter.getPadPainter().getPadWidth(); }; funcs.getFrameHeight = function() { return this.painter.getPadPainter().getPadHeight(); }; 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; const histo = this.getHisto(), xaxis = histo.fXaxis, yaxis = histo.fYaxis, pmain = this._ignore_frame ? null : this.getFramePainter(), 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: pmain?.getFrameWidth() ?? 600, height: pmain?.getFrameHeight() ?? 400 }; 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); } } } res.i1 = i1; res.i2 = i2; res.j1 = j1; res.j2 = 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); } if (args.pixel_density) args.rounding = true; const funcs = this.getHistGrFuncs(pmain, args.rounding); if (!funcs) { console.warn('cannot draw histogram without frame or pad'); return res; } // 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] < -pmain.size_x3d) { res.grx[i] = -pmain.size_x3d; if (this.options.RevX) res.i2 = i; else res.i1 = i; } if (res.grx[i] > pmain.size_x3d) { res.grx[i] = pmain.size_x3d; if (this.options.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] < -pmain.size_y3d) { res.gry[j] = -pmain.size_y3d; if (this.options.RevY) res.j2 = j; else res.j1 = j; } if (res.gry[j] > pmain.size_y3d) { res.gry[j] = pmain.size_y3d; if (this.options.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.fContour = null; return res; } /** @summary Get tip text for axis bin */ getAxisBinTip(name, axis, bin) { const pmain = this.getFramePainter(), funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y), 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.options.Poisson || histo.fBinStatErrOpt; if (!kind || (histo.fSumw2.fN && histo.fTsumw !== histo.fTsumw2)) return res; const alpha = (kind === kPoisson2) ? 0.05 : 1 - 0.682689492, n = Math.round(content); if (content < 0) return res; res.poisson = true; // indicate poisson error if (n === 0) res.low = 0; else res.low = content - gamma_quantile(alpha/2, n, 1); res.up = gamma_quantile_c(alpha/2, n + 1, 1) - content; return res; } /** @summary generic draw function for histograms * @private */ static async _drawHist(painter, opt) { return ensureTCanvas(painter).then(() => { painter.decodeOptions(opt); if (!painter.options.Same) painter.setAsMainPainter(); else painter._ignore_frame = !painter.getPadPainter()?.getMainPainter(); 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, 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; if (zc[0] <= zc[1]) n = 0; else n = 1; if (zc[2] <= zc[3]) m = 2; else m = 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; } if (zc[0] <= zc[1]) n = 0; else n = 1; if (zc[2] <= zc[3]) m = 2; else m = 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; k++; } for (ipoly = first; ipoly < levels.length; ipoly++) { polysort[k] = ipoly; k++; } 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 !== 0)) return; if (!this.loop) return ++nsegments; if (side1 !== 0) { 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 !== 0) { 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 !== 0) && (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); } }; } } /** @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 { /** @summary constructor * @param {object} histo - histogram object */ constructor(dom, histo) { super(dom, histo); this.wheel_zoomy = true; } /** @summary cleanup painter */ cleanup() { delete this.tt_handle; 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.options?.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 Toggle projection */ toggleProjection(kind, width) { if ((kind === 'Projections') || (kind === 'Off')) kind = ''; 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 = parseInt(kind.slice(1, ps)) || 1; widthY = parseInt(kind.slice(ps+2)) || 1; kind = 'XY'; } else if ((ps > 0) && (kind[0] === 'Y') && (kind[ps+1] === 'X')) { widthY = parseInt(kind.slice(1, ps)) || 1; widthX = parseInt(kind.slice(ps+2)) || 1; kind = 'XY'; } else { widthX = widthY = parseInt(kind.slice(1)) || 1; kind = kind[0]; } } if (!widthX && !widthY) widthX = widthY = 1; if (kind && (this.is_projection === 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.is_projection === kind) ? '' : kind; this.projection_widthX = widthX; this.projection_widthY = widthY; this.is_projection = ''; // avoid projection handling until area is created return this.provideSpecialDrawArea(new_proj).then(() => { this.is_projection = new_proj; return this.redrawProjection(); }); } /** @summary Redraw projection */ async redrawProjection(ii1, ii2, jj1, jj2) { if (!this.is_projection) 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._readonly && (this.snapid !== undefined)) { // this is when projection should be created on the server side if (((this.is_projection === 'X') || (this.is_projection === 'XY')) && !canp.websocketTimeout('projX')) { if (canp.sendWebsocket(`EXECANDSEND:DXPROJ:${this.snapid}:ProjectionX("_projx",${jj1+1},${jj2},"")`)) canp.websocketTimeout('projX', 1000); } if (((this.is_projection === 'Y') || (this.is_projection === 'XY')) && !canp.websocketTimeout('projY')) { if (canp.sendWebsocket(`EXECANDSEND:DYPROJ:${this.snapid}: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.is_projection) { 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.is_projection === '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.is_projection, 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()?.iscan) { let kind = this.is_projection || ''; if (kind) kind += this.projection_widthX; if ((this.projection_widthX !== this.projection_widthY) && (this.is_projection === 'XY')) kind = `X${this.projection_widthX}_Y${this.projection_widthY}`; const kinds = ['X1', 'X2', 'X3', 'X5', 'X10', 'Y1', 'Y2', 'Y3', 'Y5', 'Y10', 'XY1', 'XY2', 'XY3', 'XY5', 'XY10']; if (kind) kinds.unshift('Off'); menu.sub('Projections', () => menu.input('Input projection kind X1 or XY2 or X3_Y4', kind, 'string').then(val => this.toggleProjection(val))); for (let k = 0; k < kinds.length; ++k) menu.addchk(kind === kinds[k], kinds[k], kinds[k], arg => this.toggleProjection(arg)); menu.endsub(); } if (!this.isTH2Poly()) 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); const oldProject = this.options.Project; this.decodeOptions(arg); if ((oldProject === this.options.Project) || this.mode3d) this.interactiveRedraw('pad', 'drawopt'); else this.toggleProjection(this.options.Project); }); if (this.options.Color || this.options.Contour || this.options.Hist || this.options.Surf || this.options.Lego === 12 || this.options.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(); if (!pp) return; if (!this.isTH2Poly() && !this.options.Axis) pp.addPadButton('th2color', 'Toggle color', 'ToggleColor'); if (!this.options.Axis) pp.addPadButton('th2colorz', 'Toggle color palette', 'ToggleColorZ'); pp.addPadButton('th2draw3d', 'Toggle 3D mode', 'Toggle3D'); pp.showPadButtons(); } /** @summary Toggle color drawing mode */ toggleColor() { if (this.options.Mode3D) { this.options.Mode3D = false; this.options.Color = true; } else { this.options.Color = !this.options.Color; this.options.Scat = !this.options.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.getObject(); 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(); 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 !== 0) || (this.gminbin !== 0); // 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 (this.options.Axis > 0) { // Paint histogram axis only this.draw_content = false; } else if (this.isTH2Poly()) { this.draw_content = is_content || this.options.Line || this.options.Fill || this.options.Mark; if (!this.draw_content && this.options.Zero) { this.draw_content = true; this.options.Line = 1; } } else this.draw_content = is_content || this.options.ShowEmpty; } /** @summary Count TH2 histogram statistic * @desc Optionally one could provide condition function to select special range */ countStat(cond, count_skew) { if (!isFunc(cond)) cond = this.options.cutg ? (x, y) => this.options.cutg.IsInside(x, y) : null; const histo = this.getHisto(), xaxis = histo.fXaxis, yaxis = histo.fYaxis, fp = this.getFramePainter(), funcs = this.getHistGrFuncs(fp), 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 = !fp.isAxisZoomed('x') && !fp.isAxisZoomed('y') && (Math.abs(histo.fTsumw) > 1e-300) && !cond; 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 (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 !== 0) res.skewx = sumx3 / (np * stddev3x); if (np * stddev3y !== 0) res.skewy = sumy3 / (np * stddev3y); res.skewd = res.eff_entries > 0 ? Math.sqrt(6/res.eff_entries) : 0; if (np * stddev4x !== 0) res.kurtx = sumx4 / (np * stddev4x) - 3; if (np * stddev4y !== 0) 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(), handle = this.prepareDraw(), cntr = this.getContour(), palette = this.getHistPalette(), entries = [], has_sumw2 = histo.fSumw2?.length, show_empty = this.options.ShowEmpty, can_merge_x = (this.options.Color !== 7) || ((handle.xbar1 === 0) && (handle.xbar2 === 1)), can_merge_y = (this.options.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 = !this.options.Zero, skip_bin; const test_cutg = this.options.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.draw_g.append('svg:path') .attr('fill', palette.getColor(ecolindx)) .attr('d', entry.path); } }); return handle; } /** @summary Draw TH2 bins as colors in polar coordinates */ drawBinsPolar() { const histo = this.getHisto(), handle = this.prepareDraw(), cntr = this.getContour(), palette = this.getHistPalette(), entries = [], has_sumw2 = histo.fSumw2?.length, show_empty = this.options.ShowEmpty, colindx0 = cntr.getPaletteIndex(palette, 0); let binz, is_zero, colindx, skip_zero = !this.options.Zero, skip_bin; const test_cutg = this.options.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.draw_g.append('svg:path') .attr('fill', palette.getColor(ecolindx)) .attr('d', entry.path); } }); return handle; } /** @summary Draw histogram bins with projection function */ drawBinsProjected() { const handle = this.prepareDraw({ rounding: false, nozoom: true, extra: 100, original: true }), main = this.getFramePainter(), funcs = this.getHistGrFuncs(main), ilevels = this.getContourLevels(), palette = this.getHistPalette(), func = main.getProjectionFunc(); 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.draw_g .append('svg:path') .attr('d', 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 }), main = this.getFramePainter(), frame_w = main.getFrameWidth(), frame_h = main.getFrameHeight(), levels = this.getContourLevels(), palette = this.getHistPalette(), 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)) }; }, 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: frame_w, y: 0 }, { x: frame_w, y: frame_h }, { x: 0, y: frame_h }], 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 (this.options.Contour === 14) { this.draw_g .append('svg:path') .attr('d', `M0,0h${frame_w}v${frame_h}h${-frame_w}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 (this.options.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) return; this.draw_g.append('svg:path') .attr('d', 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.getObject(), fp = this.getFramePainter(), funcs = this.getHistGrFuncs(fp), draw_colors = this.options.Color || (!this.options.Line && !this.options.Fill && !this.options.Text && !this.options.Mark), draw_lines = this.options.Line || (this.options.Text && !draw_colors), draw_fill = this.options.Fill && !draw_colors, draw_mark = this.options.Mark, h = fp.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: this.options.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 || this.options.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 = this.options.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.draw_g.append('svg:path').attr('d', cmd); if (draw_colors && (colindx !== null)) item.style('fill', this._color_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.draw_g.append('svg:path').attr('d', 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.draw_g.append('svg:path') .attr('d', 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: this.options.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.draw_g.append('svg:path') .attr('d', cmdm) .call(markeratt.func); } // loop over graphs } // loop over bins } let pr = Promise.resolve(); if (textbins.length > 0) { const color = this.getColor(histo.fMarkerColor), rotate = -1*this.options.TextAngle, text_g = this.draw_g.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 (!this.options.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) { const histo = this.getObject(), test_cutg = this.options.cutg, color = this.getColor(histo.fMarkerColor), rotate = -1*this.options.TextAngle, draw_g = this.draw_g.append('svg:g').attr('class', 'th2_text'), show_err = (this.options.TextKind === 'E'), latex = (show_err && !this.options.TextLine) ? 1 : 0, text_offset = histo.fBarOffset*1e-3, text_size = ((histo.fMarkerSize === 1) || !rotate) ? 20 : Math.round(0.02*histo.fMarkerSize*this.getFramePainter().getFrameHeight()); if (!handle) handle = this.prepareDraw({ rounding: false }); 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) && !this.options.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 (this.options.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 (this.options.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.getObject(), test_cutg = this.options.cutg, handle = this.prepareDraw({ rounding: false }), cntr = this.options.Color ? this.getContour() : null, palette = this.options.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) => 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.draw_g .append('svg:path') .attr('d', 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.draw_g.append('svg:path') .attr('fill', 'none') .attr('d', entry.path) .call(this.lineatt.func); this.lineatt.color = col0; } }); return handle; } /** @summary Draw TH2 bins as boxes */ drawBinsBox() { const histo = this.getObject(), handle = this.prepareDraw({ rounding: false }), main = this.getMainPainter(); if (main === this) { if (main.maxbin === main.minbin) { main.maxbin = main.gmaxbin; main.minbin = main.gminbin; main.minposbin = main.gminposbin; } if (main.maxbin === main.minbin) main.minbin = Math.min(0, main.maxbin-1); } const absmax = Math.max(Math.abs(main.maxbin), Math.abs(main.minbin)), absmin = Math.max(0, main.minbin), pad = this.getPadPainter().getRootPad(true), test_cutg = this.options.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 ((main.minposbin >= 1) && (main.minposbin < 100)) logmin = Math.log(0.7); else logmin = (main.minposbin > 0) ? Math.log(0.7*main.minposbin) : 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) && (this.options.BoxStyle === 10)) cross += `M${xx},${yy}l${ww},${hh}m0,${-hh}l${-ww},${hh}`; if ((this.options.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.draw_g.append('svg:path') .attr('d', res) .call(this.fillatt.func); if ((this.options.BoxStyle !== 11) && this.fillatt.empty()) elem.call(this.lineatt.func); } if (btn1 && this.fillatt.hasColor()) { this.draw_g.append('svg:path') .attr('d', btn1) .call(this.fillatt.func) .style('fill', rgb(this.fillatt.color).brighter(0.5).formatRgb()); } if (btn2) { this.draw_g.append('svg:path') .attr('d', btn2) .call(this.fillatt.func) .style('fill', !this.fillatt.hasColor() ? 'red' : rgb(this.fillatt.color).darker(0.5).formatRgb()); } if (cross) { const elem = this.draw_g.append('svg:path') .attr('d', 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 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 (this.options.Candle) parseOption(this.options.Candle, true); else if (this.options.Violin) parseOption(this.options.Violin, false); const histo = this.getHisto(), handle = this.prepareDraw(), fp = this.getFramePainter(), // used for axis values conversions cp = this.getCanvPainter(), funcs = this.getHistGrFuncs(fp), swapXY = isOption(kHorizontal); let scaledViolin = gStyle.fViolinScaled, scaledCandle = gStyle.fCandleScaled, maxContent = 0, markers = '', cmarkers = '', attrcmarkers = null; if (this.options.Scaled !== null) scaledViolin = scaledCandle = this.options.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], 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) if ((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 === 0) 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.draw_g.append('svg:path') .attr('d', hlines) .style('stroke', this.getColor(histo.fFillColor)); } const hline_color = (isOption(kHistoZeroIndicator) && (histo.fFillStyle !== 0)) ? this.fillatt.color : this.lineatt.color; if (hists && (!this.fillatt.empty() || (hline_color !== 'none'))) { this.draw_g.append('svg:path') .attr('d', hists) .style('stroke', (hline_color !== 'none') ? hline_color : null) .style('pointer-events', this.isBatchMode() ? null : 'visibleFill') .call(this.fillatt.func); } if (bars) { this.draw_g.append('svg:path') .attr('d', bars) .call(this.lineatt.func) .call(this.fillatt.func); } if (lines) { this.draw_g.append('svg:path') .attr('d', lines) .call(this.lineatt.func) .style('fill', 'none'); } if (dashed_lines) { const dashed = this.createAttLine({ attr: histo, style: 2, std: false, color: kBlack }); this.draw_g.append('svg:path') .attr('d', dashed_lines) .call(dashed.func) .style('fill', 'none'); } if (cmarkers) { this.draw_g.append('svg:path') .attr('d', cmarkers) .call(attrcmarkers.func); } if (markers) { this.draw_g.append('svg:path') .attr('d', markers) .call(this.markeratt.func); } return handle; } /** @summary Draw TH2 bins as scatter plot */ drawBinsScatter() { const histo = this.getObject(), handle = this.prepareDraw({ rounding: true, pixel_density: true }), test_cutg = this.options.cutg, colPaths = [], currx = [], curry = [], cell_w = [], cell_h = [], scale = this.options.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.draw_g .append('svg:path') .attr('d', 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 layer = this.getFrameSvg().selectChild('.main_layer'); let defs = layer.selectChild('defs'); if (defs.empty() && (colPaths.length > 0)) 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 = (this.pad_name || '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]); if (npix < 1) npix = 1; 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.draw_g .append('svg:path') .attr('scatter-index', colindx) .style('fill', `url(#${pattern_id})`) .attr('d', colPaths[colindx]); } } return handle; } /** @summary Draw TH2 bins in 2D mode */ draw2DBins() { if (this._hide_frame && this.isMainPainter()) { this.getFrameSvg().style('display', null); delete this._hide_frame; } else if (this.options.Same && this._ignore_frame) this.getFrameSvg().style('display', 'none'); if (!this.draw_content) { if (this.options.Zscale && this.options.ohmin && this.options.ohmax) { this.getContour(true); this.getHistPalette(); } return this.removeG(); } this.createHistDrawAttributes(); this.createG(!this._ignore_frame); let handle, pr; if (this.isTH2Poly()) pr = this.drawPolyBins(); else { if (this.options.Scat) handle = this.drawBinsScatter(); if (this.options.System === kPOLAR) handle = this.drawBinsPolar(); else if (this.options.Arrow) handle = this.drawBinsArrow(); else if (this.options.Color) handle = this.drawBinsColor(); else if (this.options.Box) handle = this.drawBinsBox(); else if (this.options.Proj) handle = this.drawBinsProjected(); else if (this.options.Contour) handle = this.drawBinsContour(); else if (this.options.Candle || this.options.Violin) handle = this.drawBinsCandle(); if (this.options.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.getFrameSvg().style('display', 'none'); this._hide_frame = true; const rect = this.getPadPainter().getFrameRect(), hist = this.getHisto(), palette = this.options.Circular > 10 ? this.getHistPalette() : null, text_size = 20, circle_size = 16, axis = hist.fXaxis, 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(); }; this.createG(); makeTranslate(this.draw_g, 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, this.draw_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; this.draw_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 (this.options.Circular > 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 ((this.options.Circular > 11) && (max_value > min_value)) { const width = Math.round((cont - min_value) / (max_value - min_value) * (max_width - 1) + 1); this.draw_g.append('path').attr('d', path).style('stroke', pi.color).style('stroke-width', width).style('fill', 'none'); path = ''; } } if (path) this.draw_g.append('path').attr('d', path).style('stroke', pi.color).style('fill', 'none'); } return this.finishTextDrawing(); }); } /** @summary Draw histogram bins as chord diagram */ async drawBinsChord() { this.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 => 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); } this.createG(); makeTranslate(this.draw_g, midx + (this._shiftx ?? 0), midy + (this._shifty ?? 0), this._zoom); const chord$1 = chord() .padAngle(10 / innerRadius) .sortSubgroups(d3_descending) .sortChords(d3_descending), chords = chord$1(data), group = this.draw_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 => d.angle > Math.PI ? 'rotate(180) translate(-16)' : null) .attr('text-anchor', d => 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]} ↓`; }); this.draw_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()) { this.draw_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'); if (settings.Zooming && settings.ZoomWheel) { this.draw_g.on('wheel', evnt => { if (!this._zoom) { this._zoom = 1; this._shiftx = 0; this._shifty = 0; } const pos = pointer(evnt, this.draw_g.node()), delta = evnt.wheelDelta ? -evnt.wheelDelta : (evnt.deltaY || evnt.detail), prev_zoom = this._zoom; this._zoom *= (delta > 0) ? 0.8 : 1.2; this._shiftx += pos[0] * (prev_zoom - this._zoom); this._shifty += pos[1] * (prev_zoom - this._zoom); makeTranslate(this.draw_g, midx + this._shiftx, midy + this._shifty, this._zoom); }).on('dblclick', () => { delete this._zoom; delete this._shiftx; delete this._shifty; makeTranslate(this.draw_g, midx, midy); }); } assignContextMenu(this); } 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.options.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 fp = this.getFramePainter(), funcs = this.getHistGrFuncs(fp), 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], fp = this.getFramePainter(), funcs = this.getHistGrFuncs(fp), lines = []; let binname = bin.fPoly.fName, numpoints = 0; if (binname === 'Graph') binname = ''; if (binname.length === 0) 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(), h = this.tt_handle; let ttrect = this.draw_g?.selectChild('.tooltip_bin'); if (!pnt || !this.draw_content || !this.draw_g || !h || this.options.Proj) { ttrect?.remove(); return null; } if (h.poly) { // process tooltips from TH2Poly const fp = this.getFramePainter(), funcs = this.getHistGrFuncs(fp), 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 && !this.options.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.draw_g.append('svg:path') .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.draw_g.append('svg:path') .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; ++i) if ((pnt.x <= h.grx[i]) && (pnt.x >= h.grx[i+1])) break; } else { for (i = h.i1; i < h.i2; ++i) if ((pnt.x >= h.grx[i]) && (pnt.x <= h.grx[i+1])) break; } if (fp.reverse_y) { for (j = h.j1; j < h.j2; ++j) if ((pnt.y <= h.gry[j+1]) && (pnt.y >= h.gry[j])) break; } else { for (j = h.j1; j < h.j2; ++j) if ((pnt.y >= h.gry[j+1]) && (pnt.y <= h.gry[j])) break; } } 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 (this.options.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.is_projection) colindx = 0; // just to avoid hide else if (!match) colindx = null; else if (h.hide_only_zeros) colindx = (binz === 0) && !this.options.ShowEmpty ? null : 0; else { colindx = this.getContour().getPaletteIndex(this.getHistPalette(), binz); if ((colindx === null) && (binz === 0) && (this.options.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 (this.options.Color) res.color2 = this.getHistPalette().getColor(colindx); if (pnt.disabled && !this.is_projection) { ttrect.remove(); res.changed = true; } else { if (ttrect.empty()) { ttrect = this.draw_g.append('svg:path') .attr('class', 'tooltip_bin') .style('pointer-events', 'none') .call(addHighlightStyle); } let binid = i*10000 + j, path; if (this.is_projection) { const pwx = this.projection_widthX || 1, ddx = (pwx - 1) / 2; if ((this.is_projection.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.is_projection.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.is_projection === 'X') { x1 = 0; x2 = fp.getFrameWidth(); y1 = h.gry[j2]; y2 = h.gry[j1]; binid = j1*777 + j2*333; } else if (this.is_projection === 'Y') { y1 = 0; y2 = fp.getFrameHeight(); x1 = h.grx[i1]; x2 = h.grx[i2]; binid = i1*777 + i2*333; } else if (this.is_projection === '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.is_projection && 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) { if (this.options.Proj) return true; // z-scale zooming allowed only if special ignore-palette is not provided if (axis === 'z') { if (this.mode3d) return true; if (this.options.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; pp.$main_painter = this; this.options.Zvert = pp._palette_vertical; // redraw palette till the end when contours are available return pp.drawPave(this.options.Cjust ? 'cjust' : ''); } /** @summary Performs 2D drawing of histogram * @return {Promise} when ready */ async draw2D(/* reason */) { this.clear3DScene(); const need_palette = this.options.Zscale && this.options.canHavePalette() && !this._ignore_frame; // draw new palette, resize frame if required return this.drawColorPalette(need_palette, true).then(async pp => { let pr; if (this.options.Circular && this.isMainPainter()) pr = this.drawBinsCircular(); else if (this.options.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(); if ((main !== this) && fp && (fp.mode3d !== this.options.Mode3D)) this.copyOptionsFrom(main); if (!this.options.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'); this.options.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 function createLatexGeometry(painter, 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; if (isPlainText(lbl)) return new THREE.TextGeometry(translateLaTeX(lbl), geom_args); const font_size = size * 100, geoms = []; let stroke_width = 5; class TextParseWrapper { constructor(kind, parent) { this.kind = kind ?? 'g'; this.childs = []; this.x = 0; this.y = 0; this.font_size = parent?.font_size ?? font_size; 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.log('should create', kind); } style(name, value) { // console.log(`style ${name} = ${value}`); if ((name === 'stroke-width') && value) stroke_width = Number.parseInt(value); return this; } translate() { if (this.geom) { // special workaround for path elements, while 3d font is exact height, keep some space on the top // let dy = this.kind === 'path' ? this.font_size*0.002 : 0; this.geom.translate(this.x, this.y, 0); } this.childs.forEach(chld => { chld.x += this.x; chld.y += this.y; chld.translate(); }); } 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 === 'd') && (this.kind === 'path')) { if (get() !== 'M') return console.error('Not starts with M'); const pnts = []; let x1 = getN(true), y1 = getN(), next; while ((next = get())) { let x2 = x1, y2 = y1; switch (next) { case 'L': x2 = getN(true); y2 = getN(); break; case 'l': x2 += getN(true); y2 += getN(); break; case 'H': x2 = getN(); break; case 'h': x2 += getN(); break; case 'V': y2 = getN(); break; case 'v': y2 += getN(); break; default: console.log('not supported operator', next); } const angle = Math.atan2(y2-y1, x2-x1), dx = 0.5 * stroke_width * Math.sin(angle), dy = -0.5 * stroke_width * Math.cos(angle); 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); x1 = x2; y1 = y2; } 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(); geoms.push(this.geom); } return this; } text(v) { if (this.kind === 'text') { geom_args.size = Math.round(0.01*this.font_size); this.geom = new THREE.TextGeometry(v, geom_args); geoms.push(this.geom); } } } // class TextParseWrapper const node = new TextParseWrapper(), arg = { font_size, text: lbl, fast: true, font: { size: font_size, isMonospace: () => false, aver_width: 0.9 } }; produceLatex(painter, node, arg); if (!geoms.length) { geom_args.size = size; return new THREE.TextGeometry(translateLaTeX(lbl), geom_args); } node.translate(); // apply translate attributes 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 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; if ((pos.x >= 0) && (pos.y >= 0)) qudrant = 3; 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; } 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.draw_g) this.createFrameG(); return; } 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; 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 Remove 3D meshed for specified painter * @private */ function remove3DMeshes(painter) { if (!painter || !this.toplevel) return; let i = this.toplevel.children.length; while (i > 0) { const mesh = this.toplevel.children[--i]; if (mesh._painter === painter) { 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.getPadPainter()?.painters?.forEach(objp => { if (isFunc(objp.handleRender3D)) objp.handleRender3D(); }); } } /** @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.pad_name = this.pad_name; this.x_handle.assignSnapId(this.snapid); } 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.pad_name = this.pad_name; this.y_handle.assignSnapId(this.snapid); } 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.pad_name = this.pad_name; this.z_handle.assignSnapId(this.snapid); } 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 && (zgridx.length > 0)) { 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 && (zgridy.length > 0)) { 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(fpainter) { Object.assign(fpainter, { create3DScene, add3DMesh, remove3DMeshes, render3D, resize3D, change3DCamera, highlightBin3D, set3DOptions, drawXYZ, convert3DtoPadNDC }); } function _meshLegoToolTip(intersect) { if ((intersect.faceIndex < 0) || (intersect.faceIndex >= this.face_to_bins_index.length)) return null; const p = this.painter, handle = this.handle, main = p.getFramePainter(), histo = p.getHisto(), tip = p.get3DToolTip(this.face_to_bins_index[intersect.faceIndex]), x1 = Math.min(main.size_x3d, Math.max(-main.size_x3d, handle.grx[tip.ix-1] + handle.xbar1*(handle.grx[tip.ix] - handle.grx[tip.ix-1]))), x2 = Math.min(main.size_x3d, Math.max(-main.size_x3d, handle.grx[tip.ix-1] + handle.xbar2*(handle.grx[tip.ix] - handle.grx[tip.ix-1]))), y1 = Math.min(main.size_y3d, Math.max(-main.size_y3d, handle.gry[tip.iy-1] + handle.ybar1*(handle.gry[tip.iy] - handle.gry[tip.iy-1]))), y2 = Math.min(main.size_y3d, Math.max(-main.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 = main.grz(Math.max(this.zmin, binz1)); tip.z2 = main.grz(Math.min(this.zmax, binz2)); tip.color = this.tip_color; tip.$painter = p; tip.$projection = p.is_projection && (p.getDimension() === 2); 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)], main = 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 = main.z_handle.getScaleMin(), axis_zmax = main.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 = main.getHistPalette(); painter.createContour(main, 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, main.lego_zmin, main.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 = main.grz(zmin), grzmax = main.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 : main.grz(binz1), z2 = (binz2 > zmax) ? grzmax : main.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.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; main.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.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; main.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 = main.grz(axis_zmin), grzmax = main.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 : main.grz(binz1), z2 = (binz2 > axis_zmax) ? grzmax : main.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]); } */ main.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.painter, histo = p.getHisto(), main = p.getFramePainter(), tip = p.get3DToolTip(this.intersect_index[pos]), tx1 = Math.min(main.size_x3d, Math.max(-main.size_x3d, main.grx(histo.fXaxis.GetBinLowEdge(tip.ix)))), tx2 = Math.min(main.size_x3d, Math.max(-main.size_x3d, main.grx(histo.fXaxis.GetBinLowEdge(tip.ix+1)))), ty1 = Math.min(main.size_y3d, Math.max(-main.size_y3d, main.gry(histo.fYaxis.GetBinLowEdge(tip.iy)))), ty2 = Math.min(main.size_y3d, Math.max(-main.size_y3d, main.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 = main.grz(tip.value - tip.error < this.zmin ? this.zmin : tip.value - tip.error); tip.z2 = main.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 main = painter.getFramePainter(), histo = painter.getHisto(), handle = painter.prepareDraw({ rounding: false, use3d: true, extra: 1 }), zmin = main.z_handle.getScaleMin(), zmax = main.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 = main.grz((binz - errs.low < zmin) ? zmin : binz - errs.low); z2 = main.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.painter = painter; line.intersect_index = binindx; line.zmin = zmin; line.zmax = zmax; line.tip_color = (histo.fLineColor === 3) ? 0xFF0000 : 0x00FF00; line.tooltip = _lineErrToolTip; main.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 main = 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*main.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 = main.grz(levels[ilevel]); if ((layerz < 0) || (layerz > 2*main.size_z3d)) return; } for (let i=iminus; i (value < axis_zmin) ? -0.1 : main.grz(value), main_grz_min = 0, main_grz_max = 2*main.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 = main.z_handle.createTicks(true); handle.dogrid = true; break; } if (need_palette > 0) { palette = main.getHistPalette(); if (need_palette === 2) painter.createContour(main, 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 = main.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); main.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; main.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])); } if (pnts.length < 3) return; const faces = THREE.ShapeUtils.triangulateShape(pnts, []); if (!faces || (faces.length === 0)) 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; main.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.options?.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(); if (!when_axis_changed) this.extractAxesProperties(1); const left = this.getSelectIndex('x', 'left'), right = this.getSelectIndex('x', 'right'), pad_logy = this.getPadPainter()?.getPadLog(this.options.swap_xy() ? 'x' : 'y'), f1 = this.options.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 = !(this.options.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 (this.options.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() && (this.options.Axis > 0) && !this.options.ohmin && !this.options.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 = this.options.minimum; hmax = this.options.maximum; if ((hmin === hmax) && (hmin !== kNoZoom)) { if (hmin < 0) { hmin *= 2; hmax = 0; } else { hmin = 0; hmax *= 2; if (!hmax) hmax = 1; } } let fix_min = false, fix_max = false; if (this.options.ohmin && this.options.ohmax && !this.draw_content) { // case of hstack drawing, zooming allowed only when flag is provided if (this.options.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 (!this._exact_y_range && !pad_logy) { if (!fix_min) { if ((this.options.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; } } // used in FramePainter.isAllowedDefaultYZooming this.wheel_zoomy = (this.getDimension() > 1) || !this.draw_content; } /** @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 !== 0) res.skewx = sum3 / (np * stddev3); res.skewd = res.eff_entries > 0 ? Math.sqrt(6/res.eff_entries) : 0; if (np * stddev4 !== 0) 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 > 0) ? histo.fArray[0] : 0, 'entries')); if (print_over > 0) stat.addText('Overflow = ' + stat.format((histo.fArray.length > 0) ? 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) { let gry = funcs.swap_xy ? 0 : height; if (Number.isFinite(this.options.BaseLine) && (this.options.BaseLine >= funcs.scale_ymin)) gry = Math.round(funcs.gry(this.options.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(), xaxis = histo.fXaxis, show_text = this.options.Text; let text_col, text_angle, text_size, side = (this.options.BarStyle > 10) ? this.options.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*this.options.TextAngle; text_size = 20; if ((histo.fMarkerSize !== 1) && text_angle) text_size = 0.02*height*histo.fMarkerSize; pr = this.startTextDrawingAsync(42, text_size, this.draw_g, 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.draw_g.append('svg:path') .attr('d', bars) .call(this.fillatt.func); } if (barsl) { this.draw_g.append('svg:path') .attr('d', barsl) .call(this.fillatt.func) .style('fill', rgb(this.fillatt.color).brighter(0.5).formatRgb()); } if (barsr) { this.draw_g.append('svg:path') .attr('d', 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.options.ErrorKind !== 4, path1 = buildSvgCurve(bins1, { line }), path2 = buildSvgCurve(bins2, { line, cmd: 'L' }); this.draw_g.append('svg:path') .attr('d', 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(), want_tooltip = !this.isBatchMode() && settings.Tooltip, xaxis = histo.fXaxis, exclude_zero = !this.options.Zero, show_errors = this.options.Error, show_curve = this.options.Curve, show_text = this.options.Text, text_profile = show_text && (this.options.TextKind === 'E') && this.isTProfile() && histo.fBinEntries, grpnts = []; let res = '', lastbin = false, show_markers = this.options.Mark, show_line = this.options.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_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 (this.options.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 (this.options.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: this.options.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 = this.options.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*this.options.TextAngle; text_size = 20; if ((histo.fMarkerSize !== 1) && text_angle) text_size = 0.02*height*histo.fMarkerSize; if (!text_angle && !this.options.TextKind) { const space = width / (right - left + 1); if (space < 3 * text_size) { text_angle = 270; text_size = Math.round(space*0.7); } } pr = this.startTextDrawingAsync(42, text_size, this.draw_g, 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 (this.options.errorX > 0) { edx = Math.round((mx2 - mx1) * this.options.errorX); mmx1 = midx - edx; mmx2 = midx + edx; if (this.options.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 (this.options.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 !== 0) { 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 (show_line) { if (funcs.swap_xy) path_line += (path_line ? 'L' : 'M') + `${my},${midx}`; // no optimization else if (path_line.length === 0) 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.length === 0) { 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._ignore_frame; 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.draw_g.append('svg:path') .attr('d', 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 => { const d = pnt.grx; pnt.grx = pnt.gry; pnt.gry = d; }); path_line = buildSvgCurve(grpnts); } if (path_fill) { this.draw_g.append('svg:path') .attr('d', path_fill) .call(this.fillatt.func); } else if (path_line && !this.fillatt.empty() && !draw_hist) { this.draw_g.append('svg:path') .attr('d', path_line + `L${midx},${h0}H${startmidx}Z`) .call(this.fillatt.func); } if (path_err) { this.draw_g.append('svg:path') .attr('d', path_err) .call(this.lineatt.func); } if (hints_err) { this.draw_g.append('svg:path') .attr('d', hints_err) .style('fill', 'none') .style('pointer-events', this.isBatchMode() ? null : 'visibleFill'); } if (path_line) { this.draw_g.append('svg:path') .attr('d', path_line) .style('fill', 'none') .call(this.lineatt.func); } if (path_marker) { this.draw_g.append('svg:path') .attr('d', path_marker) .call(this.markeratt.func); } if (hints_marker) { this.draw_g.append('svg:path') .attr('d', hints_marker) .style('fill', 'none') .style('pointer-events', this.isBatchMode() ? null : 'visibleFill'); } } if (res && draw_hist) add_hist(); if (show_text) return this.finishTextDrawing(); }); } /** @summary Draw TH1 bins in SVG element * @return Promise or scalar value */ draw1DBins() { if (this.options.Same && this._ignore_frame) this.getFrameSvg().style('display', 'none'); this.createHistDrawAttributes(); const pmain = this.getFramePainter(), funcs = this.getHistGrFuncs(pmain), width = pmain.getFrameWidth(), height = pmain.getFrameHeight(); if (!this.draw_content || (width <= 0) || (height <= 0)) return this.removeG(); this.createG(!this._ignore_frame); if (this.options.Bar) { return this.drawBars(funcs, height).then(() => { if (this.options.ErrorKind === 1) return this.drawNormal(funcs, width, height); }); } if ((this.options.ErrorKind === 3) || (this.options.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(), pmain = this.getFramePainter(), funcs = this.getHistGrFuncs(pmain), histo = this.getHisto(), 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 (this.options.Error || this.options.Mark || this.isTF1()) { tips.push(`x = ${xlbl}`, `y = ${funcs.axisAsText('y', cont)}`); if (this.options.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) { if (!pnt || !this.draw_content || !this.draw_g || this.options.Mode3D) { this.draw_g?.selectChild('.tooltip_bin').remove(); return null; } const pmain = this.getFramePainter(), funcs = this.getHistGrFuncs(pmain), histo = this.getHisto(), left = this.getSelectIndex('x', 'left', -1), right = this.getSelectIndex('x', 'right', 2); let width = pmain.getFrameWidth(), height = pmain.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.swap_xy !== pmain.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 (this.options.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 (this.options.Bar) { show_rect = true; gapx = 0; gry1 = this.getBarBaseline(funcs, height); if (gry1 > gry2) [gry1, gry2] = [gry2, gry1]; if (!pnt.touch && (pnt.nproc === 1)) if ((pnt_y < gry1) || (pnt_y > gry2)) findbin = null; } else if ((this.options.Error && (this.options.Hist !== true)) || this.options.Mark || this.options.Line || this.options.Curve) { show_rect = !this.isTF1(); let msize = 3; if (this.markeratt) msize = Math.max(msize, this.markeratt.getFullSize()); if (this.options.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)*this.options.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 (!this.options.Zero && (histo.getBinContent(findbin+1) === 0) && (histo.getBinError(findbin+1) === 0)) findbin = null; // exclude empty bin if empty bins suppressed } let ttrect = this.draw_g.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.draw_g.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.draw_g.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.options.need_fillcol && this.fillatt?.empty()) this.fillatt.change(5, 1001); // redraw all objects in pad, inform dependent objects this.interactiveRedraw('pad', 'drawopt'); }); if (!this.snapid && !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 > 0) ? 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(); if ((main !== this) && fp && (fp.mode3d !== this.options.Mode3D)) this.copyOptionsFrom(main); if (!this.options.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'); this.options.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 main = this.getFramePainter(), // who makes axis drawing is_main = this.isMainPainter(), // is main histogram histo = this.getHisto(), zmult = 1 + 2*gStyle.fHistTopMargin; let pr = Promise.resolve(true), full_draw = true; if (reason === 'resize') { const res = is_main ? main.resize3D() : false; if (res !== 1) { full_draw = false; if (res) main.render3D(); } } if (full_draw) { this.createHistDrawAttributes(true); this.scanContent(reason === 'zoom'); // may be required for axis drawings if (is_main) { assignFrame3DMethods(main); pr = main.create3DScene(this.options.Render3D, this.options.x3dscale, this.options.y3dscale, this.options.Ortho).then(() => { main.setAxesRanges(histo.fXaxis, this.xmin, this.xmax, histo.fYaxis, this.ymin, this.ymax, histo.fZaxis, 0, 0, this); main.set3DOptions(this.options); main.drawXYZ(main.toplevel, TAxisPainter, { ndim: 1, hist_painter: this, use_y_for_z: true, zmult, zoom: settings.Zooming, draw: (this.options.Axis !== -1), drawany: this.options.isCartesian() }); }); } if (main.mode3d) { pr = pr.then(() => { drawBinsLego(this); main.render3D(); this.updateStatWebCanvas(); main.addKeysHandler(); }); } } if (is_main) pr = pr.then(() => this.drawColorPalette(this.options.Zscale && this.options.canHavePalette())); return pr.then(() => this.updateFunctions()) .then(() => this.updateHistTitle()) .then(() => this); } /** @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(), pmain = painter.getFramePainter(), axis_zmin = pmain.z_handle.getScaleMin(), axis_zmax = pmain.z_handle.getScaleMax(), len = histo.fBins.arr.length, z0 = pmain.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 > pmain.scale_xmax) || (bin.fXmax < pmain.scale_xmin) || (bin.fYmin > pmain.scale_ymax) || (bin.fYmax < pmain.scale_ymin)) continue; z1 = pmain.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 = pmain.size_x3d*pmain.size_z3d; const dist2limit = (ntry > 0) ? 0 : dist2/1e6; pnts = []; faces = null; for (let vert = 0; vert < npnts; ++vert) { currx = pmain.grx(x[vert]); curry = pmain.gry(y[vert]); if (vert > 0) dist2 = (currx-lastx)*(currx-lastx) + (curry-lasty)*(curry-lasty); 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[n], pnt2 = pnts[n > 0 ? n - 1 : pnts.length - 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._color_palette?.getColor(colindx), { vertexColors: false, side: THREE.DoubleSide })), mesh = new THREE.Mesh(geometry, material); pmain.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, fp = p.getFramePainter(), tbin = p.getObject().fBins.arr[this.bins_index], tip = { 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) }; return tip; }; } } /** @summary Draw 2-D histogram in 3D * @private */ class TH2Painter extends TH2Painter$2 { /** @summary draw TH2 object in 3D mode */ async draw3D(reason) { this.mode3d = true; const main = this.getFramePainter(), // who makes axis drawing is_main = this.isMainPainter(), // is main histogram histo = this.getHisto(); let pr = Promise.resolve(true), full_draw = true; if (reason === 'resize') { const res = is_main ? main.resize3D() : false; if (res !== 1) { full_draw = false; if (res) main.render3D(); } } if (full_draw) { const pad = this.getPadPainter().getRootPad(true), logz = pad?.fLogv ?? pad?.fLogz; let zmult = 1; if (this.options.ohmin && this.options.ohmax) { this.zmin = this.options.hmin; this.zmax = this.options.hmax; } else if (this.options.minimum !== kNoZoom && this.options.maximum !== kNoZoom) { this.zmin = this.options.minimum; this.zmax = this.options.maximum; } else if (this.draw_content || (this.gmaxbin !== 0)) { 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); if (is_main) { assignFrame3DMethods(main); pr = main.create3DScene(this.options.Render3D, this.options.x3dscale, this.options.y3dscale, this.options.Ortho).then(() => { main.setAxesRanges(histo.fXaxis, this.xmin, this.xmax, histo.fYaxis, this.ymin, this.ymax, histo.fZaxis, this.zmin, this.zmax, this); main.set3DOptions(this.options); main.drawXYZ(main.toplevel, TAxisPainter, { ndim: 2, hist_painter: this, zmult, zoom: settings.Zooming, draw: this.options.Axis !== -1, drawany: this.options.isCartesian(), reverse_x: this.options.RevX, reverse_y: this.options.RevY }); }); } if (main.mode3d) { pr = pr.then(() => { if (this.draw_content) { if (this.isTH2Poly()) drawTH2PolyLego(this); else if (this.options.Contour) drawBinsContour3D(this, true); else if (this.options.Surf) drawBinsSurf3D(this); else if (this.options.Error) drawBinsError3D(this); else drawBinsLego(this); } else if (this.options.Axis && this.options.Zscale) { this.getContourLevels(true); this.getHistPalette(); } main.render3D(); this.updateStatWebCanvas(); main.addKeysHandler(); }); } } // (re)draw palette by resize while canvas may change dimension if (is_main) { pr = pr.then(() => this.drawColorPalette(this.options.Zscale && ((this.options.Lego === 12) || (this.options.Lego === 14) || (this.options.Surf === 11) || (this.options.Surf === 12)))); } return pr.then(() => this.updateFunctions()) .then(() => this.updateHistTitle()) .then(() => this); } /** @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 { /** @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 !== 0) || (this.gminbin !== 0); 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 !== 0) res.skewx = sumx3 / (np * stddev3x); if (np * stddev3y !== 0) res.skewy = sumy3 / (np * stddev3y); if (np * stddev3z !== 0) res.skewz = sumz3 / (np * stddev3z); res.skewd = res.eff_entries > 0 ? Math.sqrt(6/res.eff_entries) : 0; if (np * stddev4x !== 0) res.kurtx = sumx4 / (np * stddev4x) - 3; if (np * stddev4y !== 0) res.kurty = sumy4 / (np * stddev4y) - 3; if (np * stddev4z !== 0) 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(), main = 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) continue; numpixels += Math.round(bin_content*coef); } } } // too many pixels - use box drawing if (numpixels > (main.webgl ? 100000 : 30000)) return false; const pnts = new PointsCreator(numpixels, main.webgl, main.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(main.grx(binx), main.gry(biny), main.grz(binz)); } } } } return pnts.createPoints({ color: this.getColor(histo.fMarkerColor) }).then(mesh => { main.add3DMesh(mesh); mesh.bins = bins; mesh.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.painter, thisto = p.getHisto(), fp = p.getFramePainter(), 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; let box_option = this.options.BoxStyle; if (!box_option && this.options.Scat) { const promise = this.draw3DScatter(); if (promise !== false) return promise; box_option = 12; // fall back to box2 draw option } else if (!box_option && !this.options.GLBox && !this.options.GLColor && !this.options.Lego) box_option = 12; // default draw option const histo = this.getHisto(), main = this.getFramePainter(); 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 && this.options.Lego) box_option = (this.options.Lego === 1) ? 10 : this.options.Lego; if ((this.options.GLBox === 11) || (this.options.GLBox === 12)) { tipscale = 0.4; use_lambert = true; if (this.options.GLBox === 12) use_colors = true; single_bin_geom = new THREE.SphereGeometry(0.5, main.webgl ? 16 : 8, main.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 (this.options.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); }, 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 = main.grx(histo.fXaxis.GetBinLowEdge(i+1)), grx2 = main.grx(histo.fXaxis.GetBinLowEdge(i+2)); for (let j = j1; j < j2; ++j) { const gry1 = main.gry(histo.fYaxis.GetBinLowEdge(j+1)), gry2 = main.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 (!this.options.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(this._color_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 = main.grz(histo.fZaxis.GetBinLowEdge(k+1)), grz2 = main.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.painter, thisto = p.getHisto(), fp = p.getFramePainter(), 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.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; main.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.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; main.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]; } } main.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 main = this.getFramePainter(), // who makes axis and 3D drawing histo = this.getHisto(); let pr = Promise.resolve(true), full_draw = true; if (reason === 'resize') { const res = main.resize3D(); if (res !== 1) { full_draw = false; if (res) main.render3D(); } } if (full_draw) { assignFrame3DMethods(main); pr = main.create3DScene(this.options.Render3D, this.options.x3dscale, this.options.y3dscale, this.options.Ortho).then(() => { main.setAxesRanges(histo.fXaxis, this.xmin, this.xmax, histo.fYaxis, this.ymin, this.ymax, histo.fZaxis, this.zmin, this.zmax, this); main.set3DOptions(this.options); main.drawXYZ(main.toplevel, TAxisPainter, { ndim: 3, hist_painter: this, zoom: settings.Zooming, draw: this.options.Axis !== -1, drawany: this.options.isCartesian() }); return this.draw3DBins(); }).then(() => { main.render3D(); this.updateStatWebCanvas(); main.addKeysHandler(); }); } if (this.isMainPainter()) pr = pr.then(() => this.drawColorPalette(this.options.Zscale && (this._box_option === 12 || this._box_option === 13 || this.options.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 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 { constructor(dom, graph) { super(dom, graph); this.axes_draw = false; // indicate if graph histogram was drawn for axes this.bins = null; this.xmin = this.ymin = this.xmax = this.ymax = 0; this.wheel_zoomy = true; 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/); } /** @summary Return drawn graph object */ getGraph() { return this.getObject(); } /** @summary Return histogram object used for axis drawings */ getHistogram() { return this.getObject()?.fHistogram; } /** @summary Set histogram object to graph */ setHistogram(histo) { const obj = this.getObject(); if (obj) obj.fHistogram = histo; } /** @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) { delete this.$redraw_hist; 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._funcHandler?.drawNext(0) ?? this; delete this._funcHandler; return res; }); } /** @summary Cleanup graph painter */ cleanup() { delete this.interactive_bin; // break mouse handling delete this.bins; 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; let blocks_gme = []; if (!this.options) this.options = {}; // decode main draw options for the graph const 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')) if (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; }; Object.assign(this.options, { Axis: '', NoOpt: 0, PadStats: false, PadPalette: false, original: opt, second_x: false, second_y: false, individual_styles: false }); 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(); } } const res = this.options; 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('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) if (d.check('S')) res.individual_styles = true; // if (d.check('E')) res.Errors = 1; // E option only defined for TGraphPolar 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) if (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 Extract errors for TGraphMultiErrors */ extractGmeErrors(nblock) { if (!this.bins) return; const gr = this.getGraph(); this.bins.forEach(bin => { bin.eylow = gr.fEyL[nblock][bin.indx]; bin.eyhigh = gr.fEyH[nblock][bin.indx]; }); } /** @summary Create bins for TF1 drawing */ createBins() { const gr = this.getGraph(); 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) && this.options.Mark && !this.options.Line && !this.options.Curve && !this.options.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._not_adjust_hrange && !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._is_scatter ? 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)) { 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._is_scatter) { 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.options.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; let step = Math.floor(selbins.length / maxpnt); if (step < 2) step = 2; const optbins = []; 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 pmain = this.get_main(), lines = [], funcs = pmain.getGrFuncs(this.options.second_x, this.options.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 (this.options.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 ((this.options.Errors || (this.options.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_main() { let pmain = this.getFramePainter(); if (pmain?.grx && pmain?.gry) return pmain; // FIXME: check if needed, can be removed easily const pp = this.getPadPainter(), rect = pp?.getPadRect() || { width: 800, height: 600 }; pmain = { 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 pmain.pad ? pmain : null; } /** @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.draw_g.append('svg:path') .attr('d', 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); } } if (drawbins.length === 1) drawbins[0].width = w/4; // pathologic case of single bin else { for (let i = 0; i < drawbins.length; ++i) drawbins[i].width = (xmax - xmin) / drawbins.length * gStyle.fBarWidth; } const 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 = d.width > 1 ? Math.round(-d.width/2) : 0, dw = d.width > 1 ? Math.round(d.width) : 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); } 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 !== 0) || !options.skip_errors_x0) && ((d.y !== 0) || !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.draw_g.append('path') .attr('d', makeLine(xqmin, yqmin, xqmax, yqmax)) .call(latt1.func) .style('fill', 'none'); this.draw_g.append('path') .attr('d', 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.options; 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});;`; delete this.fillatt; } if (o._plc > 1) { o._plc = 1; graph.fLineColor = icolor; this._auto_exec += `SetLineColor(${icolor});;`; delete this.lineatt; } if (o._pmc > 1) { o._pmc = 1; graph.fMarkerColor = icolor; this._auto_exec += `SetMarkerColor(${icolor});;`; delete this.markeratt; } } } if (only_check_auto) this.deleteAttr(); else { this.createAttLine({ attr: graph, can_excl: true }); this.createAttFill({ attr: graph }); } } /** @summary draw TGraph */ drawGraph() { const pmain = this.get_main(), graph = this.getGraph(); if (!pmain || !this.options) return; // special mode for TMultiGraph 3d drawing if (this.options.pos3d) return this.drawBins3D(pmain, graph); const is_gme = Boolean(this.get_gme()), funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y), w = pmain.getFrameWidth(), h = pmain.getFrameHeight(); this.createG(pmain.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 ? this.draw_g.append('svg:g') : this.draw_g; this.drawBins(funcs, this.options, draw_g, w, h, this.lineatt, this.fillatt, true); if (graph._typename === 'TGraphQQ') this.appendQQ(funcs, graph); if (is_gme) { for (let k = 0; k < graph.fNYErrors; ++k) { let lineatt = this.lineatt, fillatt = this.fillatt; if (this.options.individual_styles) { lineatt = this.createAttLine({ attr: graph.fAttLine[k], std: false }); fillatt = this.createAttFill({ attr: graph.fAttFill[k], std: false }); } const sub_g = this.draw_g.append('svg:g'), options = (k < this.options.blocks.length) ? this.options.blocks[k] : this.options; this.extractGmeErrors(k); this.drawBins(funcs, options, sub_g, w, h, lineatt, fillatt); } this.extractGmeErrors(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 pmain = this.get_main(), height = pmain.getFrameHeight(), esz = this.error_size, isbar1 = (this.options.Bar === 1), funcs = isbar1 ? pmain.getGrFuncs(this.options.second_x, this.options.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.draw_g.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: -d.width/2, x2: d.width/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.draw_g?.selectChild('.tooltip_bin'); if (!hint || !this.draw_g) { ttrect?.remove(); return; } if (hint.usepath) return this.showTooltipForPath(hint); const d = select(hint.d3bin).datum(); if (ttrect.empty()) { ttrect = this.draw_g.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'), funcs = this.get_main().getGrFuncs(this.options.second_x, this.options.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; let radius = Math.max(this.lineatt.width + 3, 4); if (this.marker_size > 0) radius = Math.max(this.marker_size, radius); 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 IsInside = (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 (IsInside(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 = IsInside(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 (!obj) 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 === null) 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'), pmain = this.get_main(), funcs = pmain.getGrFuncs(this.options.second_x, this.options.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 }; res.user_info = { obj: gr, name: gr.fName, bin: 0, cont: 0, grx: res.x, gry: res.y }; res.ismark = ismark; res.islines = islines; if (best.closeline) { res.menu = res.exact = true; res.menu_dist = best.linedist; } else if (best.bin) { if (this.options.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.draw_g?.selectChild('.tooltip_bin'); if (!hint?.bin || !this.draw_g) { ttbin?.remove(); return; } if (ttbin.empty()) ttbin = this.draw_g.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 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 (this.options.Line || this.options.Curve) elem.call(this.lineatt.func); else elem.style('stroke', 'black'); if (this.options.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) { this.pos_dx = this.pos_dy = 0; this.move_funcs = this.get_main().getGrFuncs(this.options.second_x, this.options.second_y); const hint = this.extractTooltip({ x, y }); if (hint && 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 delete this.move_binindx; } /** @summary Perform moving */ moveDrag(dx, dy) { this.pos_dx += dx; this.pos_dy += dy; if (this.move_binindx === undefined) makeTranslate(this.draw_g, 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.draw_g.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); delete this.move_binindx; if (graph.$redraw_pad) this.redrawPad(); } delete this.move_funcs; 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; delete this._auto_exec; } } /** @summary Fill context menu */ fillContextMenuItems(menu) { if (!this.snapid) { 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(), pmain = this.get_main(); if ((method.fName === 'RemovePoint') || (method.fName === 'InsertPoint')) { if (!canp || canp._readonly) return true; // ignore function const pnt = isFunc(pmain?.getLastEventPos) ? pmain.getLastEventPos() : null, hint = this.extractTooltip(pnt); if (method.fName === 'InsertPoint') { if (pnt) { const funcs = pmain.getGrFuncs(this.options.second_x, this.options.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.options; if (this.snapid !== undefined) 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; if (opt && (opt !== this.options.original)) this.decodeOptions(opt); const new_funcs = this._updateMembers(this.getObject(), obj); this.createBins(); delete this.$redraw_hist; // 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, this.options.Axis); this.$redraw_hist = true; } } this._funcHandler = 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._is_scatter) arr = (axis === 'x') ? gr.fX : gr.fY; else if (axis !== (this.options.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()?.normal_canvas) 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 need_histo = !this.getHistogram(), histo = this.createHistogram(need_histo, need_histo); return TH1Painter$2.draw(this.getDrawDom(), histo, this.options.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'); if (fp.zoom_xmin !== fp.zoom_xmax) if ((this.options.pos3d < fp.zoom_xmin) || (this.options.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(this.options.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.options.Axis); } /** @summary Draw TGraph */ static async draw(dom, graph, opt) { return TGraphPainter._drawGraph(new TGraphPainter(dom, graph), opt); } } // class TGraphPainter // 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 > 0) 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 < -1e-5) 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 > 0) this.front = new Node(front); if (back.length > 0) 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 > 0) { if (!this.front) this.front = new Node(); this.front.build(front); } if (back.length > 0) { 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); if (this.back) back = this.back.clipPolygons(back); else 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 }; /** @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 /** @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 : ((att & f) !== 0); } /** @summary Set fGeoAtt bit * @private */ function setGeoBit(volume, f, value) { if (volume.fGeoAtt === undefined) return; 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) { if (flag === undefined) 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) return; _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) { if (reduce === undefined) 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) { if (reduce === undefined) 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); return; } 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); if (this.v2) this.v2.setnormal(nx2, ny2, nz2); this.v3.setnormal(nx3, ny3, nz3); if (this.v4) 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); if (this.v2) this.v2.setnormal(nx12, ny12, nz12); this.v3.setnormal(nx34, ny34, nz34); if (this.v4) 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); if (this.v2) this.v2.setnormal(nx, ny, nz); this.v3.setnormal(nx, ny, nz); if (this.v4) 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= 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 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 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 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; } } 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; if (matrix.fRotation) 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) !== 0) geoWarn(`Unsupported reflected pattern ${typ}`); if (typ.indexOf('TGeoPattern') !== 0) 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 {Object} 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; } /** @summary Returns number of faces for provided geometry * @param geom - can be BufferGeometry, CsgGeometry or interim array of polygons * @private */ function countGeometryFaces(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; } 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 = countGeometryFaces(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 = countGeometryFaces(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'); } if (countGeometryFaces(bsp1) === 0) { geoWarn('Zero faces in comp shape' + ` left: ${shape.fNode.fLeft._typename} ${countGeometryFaces(geom1)} faces` + ` right: ${shape.fNode.fRight._typename} ${countGeometryFaces(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, flippedMesh) { if (!geom.boundingBox) geom.computeBoundingBox(); const box = geom.boundingBox.clone(); box.applyMatrix4(matrix); if (!position) position = 0; 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 (or undefined) 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) { if (limit === undefined) 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]; if (place.indexOf(e.message) >= 0) place = e.stack.split('\n')[1]; else place = '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: '$$Shape$$', // 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 clTGeoBBox: break; 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 clTGeoArb8: break; case clTGeoTrap: break; case clTGeoGtra: 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 clTGeoPcon: case clTGeoPgon: break; case clTGeoXtru: 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 clTGeoCompositeShape: break; case clTGeoShapeAssembly: 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; } 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.CheckBox = function(box) { const pnt = this.test; let cnt = 0; pnt.set(box.min.x, box.min.y, box.min.z); if (this.containsPoint(pnt)) cnt++; pnt.set(box.min.x, box.min.y, box.max.z); if (this.containsPoint(pnt)) cnt++; pnt.set(box.min.x, box.max.y, box.min.z); if (this.containsPoint(pnt)) cnt++; pnt.set(box.min.x, box.max.y, box.max.z); if (this.containsPoint(pnt)) cnt++; pnt.set(box.max.x, box.max.y, box.max.z); if (this.containsPoint(pnt)) cnt++; pnt.set(box.max.x, box.min.y, box.max.z); if (this.containsPoint(pnt)) cnt++; pnt.set(box.max.x, box.max.y, box.min.z); if (this.containsPoint(pnt)) cnt++; pnt.set(box.max.x, box.max.y, box.max.z); if (this.containsPoint(pnt)) cnt++; return cnt > 5; // only if 6 edges and more are seen, we think that box is fully visible }; 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; if (clone.kind === kindGeo) { if (obj.fVolume) return obj.fVolume.fShape; } else return obj.fShape; return null; } /** @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 === '$$Shape$$') 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); if (sublevel > this.maxdepth) 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 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] !== 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 dflt = { kGray: 920, kRed: 632, kGreen: 416, kBlue: 600, kYellow: 400, kMagenta: 616, kOrange: 800}, nmax = 110, col = []; for (let i=0; i 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 === 'mesh') || (options === 'delete_mesh')) { 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 === 'mesh') || !mesh) return mesh; const res = three_prnt; while (mesh && (mesh !== toplevel)) { three_prnt = mesh.parent; three_prnt.remove(mesh); mesh = (three_prnt.children.length === 0) ? 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, 'delete_mesh'); 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 > 0) { 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 > 0) { 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 = '$$Shape$$'; // 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 = countGeometryFaces(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 alwys 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) { if (!top) return; 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); // if (intersects[k1].object === mesh) break; // trace until object itself } 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.19.2 * @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 ) { // This would be the logical way to do things, but floating point errors. // return Math.round( value / this._step ) * this._step; // Using inverse step solves a lot of them, but not all // const inverseStep = 1 / this._step; // return Math.round( value * inverseStep ) / inverseStep; // Not happy about this, but haven't seen it break. const r = Math.round( value / this._step ) * this._step; return parseFloat( r.toPrecision( 15 ) ); } _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; } } const 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); line-height: calc(var(--title-height) - 4px); font-weight: 600; padding: 0 var(--padding); -webkit-tap-highlight-color: transparent; cursor: pointer; outline: 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%; height: var(--widget-height); text-transform: none; background: var(--widget-color); border-radius: var(--widget-border-radius); border: none; } @media (hover: hover) { .lil-gui button:hover { background: var(--hover-color); } .lil-gui button:focus { box-shadow: inset 0 0 0 1px var(--focus-color); } } .lil-gui 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} */ this.children = []; /** * The list of controllers contained by this GUI. * @type {Array} */ this.controllers = []; /** * The list of folders contained by this GUI. * @type {Array} */ 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( 'div' ); this.$title.classList.add( 'title' ); this.$title.setAttribute( 'role', 'button' ); this.$title.setAttribute( 'aria-expanded', true ); this.$title.setAttribute( 'tabindex', 0 ); this.$title.addEventListener( 'click', () => this.openAnimated( this._closed ) ); this.$title.addEventListener( 'keydown', e => { if ( e.code === 'Enter' || e.code === 'Space' ) { e.preventDefault(); this.$title.click(); } } ); // 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 _splitColors 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 _splitColors 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 3d painter * @private */ function findItemWithPainter(hitem, funcname) { while (hitem) { if (hitem._painter?._camera) { if (funcname && isFunc(hitem._painter[funcname])) hitem._painter[funcname](); return hitem; } hitem = hitem._parent; } return null; } /** @summary provide css style for geo object * @private */ function provideVisStyle(obj) { if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) return obj.fRnrSelf ? ' geovis_this' : ''; const vis = !testGeoBit(obj, geoBITS.kVisNone) && testGeoBit(obj, geoBITS.kVisThis); let chld = testGeoBit(obj, geoBITS.kVisDaughters); if (chld && !obj.fNodes?.arr?.length) chld = false; if (vis && chld) return ' geovis_all'; if (vis) return ' geovis_this'; if (chld) return ' geovis_daughters'; return ''; } /** @summary update icons * @private */ function updateBrowserIcons(obj, hpainter) { if (!obj || !hpainter) return; hpainter.forEachItem(m => { // update all items with that volume if ((obj === m._volume) || (obj === m._geoobj)) { m._icon = m._icon.split(' ')[0] + provideVisStyle(obj); hpainter.updateTreeNode(m); } }); } /** @summary Return stack for the item from list of intersection * @private */ function getIntersectStack(item) { const obj = item?.object; if (!obj) return null; if (obj.stack) return obj.stack; if (obj.stacks && item.instanceId !== undefined && item.instanceId < obj.stacks.length) return obj.stacks[item.instanceId]; } /** * @summary Toolbar for geometry painter * * @private */ class Toolbar { /** @summary constructor */ constructor(container, bright, buttons) { this.bright = bright; this.buttons = buttons; this.element = container.append('div').attr('style', 'float: left; box-sizing: border-box; position: relative; bottom: 23px; vertical-align: middle; padding-left: 5px'); } /** @summary add buttons */ createButtons() { const buttonsNames = []; this.buttons.forEach(buttonConfig => { const buttonName = buttonConfig.name; if (!buttonName) throw new Error('must provide button name in button config'); if (buttonsNames.indexOf(buttonName) !== -1) throw new Error(`button name ${buttonName} is taken`); buttonsNames.push(buttonName); const title = buttonConfig.title || buttonConfig.name; if (!isFunc(buttonConfig.click)) throw new Error('must provide button click() function in button config'); ToolbarIcons.createSVG(this.element, ToolbarIcons[buttonConfig.icon], 16, title, this.bright) .on('click', buttonConfig.click) .style('position', 'relative') .style('padding', '3px 1px'); }); } /** @summary change brightness */ changeBrightness(bright) { if (this.bright === bright) return; this.element.selectAll('*').remove(); this.bright = bright; this.createButtons(); } /** @summary cleanup toolbar */ cleanup() { this.element?.remove(); delete this.element; } } // class ToolBar /** * @summary geometry drawing control * * @private */ class GeoDrawingControl extends InteractiveControl { constructor(mesh, bloom) { super(); this.mesh = mesh?.material ? mesh : null; this.bloom = bloom; } /** @summary set highlight */ setHighlight(col, indx) { return this.drawSpecial(col, indx); } /** @summary draw special */ drawSpecial(col, indx) { const c = this.mesh; if (!c?.material) return; if (c.isInstancedMesh) { if (c._highlight_mesh) { c.remove(c._highlight_mesh); delete c._highlight_mesh; } if (col && indx !== undefined) { const h = new 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 { /** @summary Constructor * @param {object|string} dom - DOM element for drawing or element id * @param {object} obj - supported TGeo object */ constructor(dom, obj) { let gm; if (obj?._typename === clTGeoManager) { gm = obj; obj = obj.fMasterVolume; } if (obj?._typename && (obj._typename.indexOf(clTGeoVolume) === 0)) obj = { _typename: clTGeoNode, fVolume: obj, fName: obj.fName, $geoh: obj.$geoh, _proxy: true }; super(dom, obj); if (getHistPainter3DCfg(this.getMainPainter())) this.superimpose = true; if (gm) this.geo_manager = gm; this.no_default_title = true; // do not set title to main DIV this.mode3d = true; // indication of 3D mode this.drawing_stage = stageInit; // this.drawing_log = 'Init'; this.ctrl = { clipIntersect: true, clipVisualize: false, clip: [{ name: 'x', enabled: false, value: 0, min: -100, max: 100, step: 1 }, { name: 'y', enabled: false, value: 0, min: -100, max: 100, step: 1 }, { name: 'z', enabled: false, value: 0, min: -100, max: 100, step: 1 }], _highlight: 0, highlight: 0, highlight_bloom: 0, highlight_scene: 0, highlight_color: '#00ff00', bloom_strength: 1.5, more: 1, maxfaces: 0, vislevel: undefined, maxnodes: undefined, dflt_colors: false, info: { num_meshes: 0, num_faces: 0, num_shapes: 0 }, depthTest: true, depthMethod: 'dflt', select_in_view: false, update_browser: true, use_fog: false, light: { kind: 'points', top: false, bottom: false, left: false, right: false, front: false, specular: true, power: 1 }, lightKindItems: [ { name: 'AmbientLight', value: 'ambient' }, { name: 'DirectionalLight', value: 'points' }, { name: 'HemisphereLight', value: 'hemisphere' }, { name: 'Ambient + Point', value: 'mix' } ], trans_radial: 0, trans_z: 0, scale: new 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.cleanup(true); } /** @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; } /** @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._previousCameraPosition = this._camera.position.clone(); this._previousCameraRotation = this._camera.rotation.clone(); this._vrDisplay.requestPresent([{ source: this._renderer.domElement }]).then(() => { this._previousCameraNear = this._camera.near; this._dolly.position.set(this._camera.position.x/4, -this._camera.position.y/8, -this._camera.position.z/4); this._camera.position.set(0, 0, 0); this._dolly.add(this._camera); this._camera.near = 0.1; this._camera.updateProjectionMatrix(); this._renderer.vr.enabled = true; this._renderer.setAnimationLoop(() => { this.updateVRControllers(); this.render3D(0); }); }); this._renderer.vr.enabled = true; window.addEventListener('keydown', evnt => { // Esc Key turns VR mode off if (evnt.code === 'Escape') this.exitVRMode(); }); } /** @summary Exit VR mode * @private */ exitVRMode() { if (!this._vrDisplay.isPresenting) return; this._renderer.vr.enabled = false; this._dolly.remove(this._camera); this._scene.add(this._camera); // Restore Camera pose this._camera.position.copy(this._previousCameraPosition); this._previousCameraPosition = undefined; this._camera.rotation.copy(this._previousCameraRotation); this._previousCameraRotation = undefined; this._camera.near = this._previousCameraNear; this._camera.updateProjectionMatrix(); this._vrDisplay.exitPresent(); } /** @summary Returns main geometry object */ getGeometry() { return this.getObject(); } /** @summary Modify visibility of provided node by name */ modifyVisisbility(name, sign) { if (getNodeKind(this.getGeometry()) !== 0) return; if (!name) return setGeoBit(this.getGeometry().fVolume, geoBITS.kVisThis, (sign === '+')); let regexp, exact = false; // arg.node.fVolume if (name.indexOf('*') < 0) { regexp = new RegExp('^'+name+'$'); exact = true; } else { regexp = new RegExp('^' + name.split('*').join('.*') + '$'); exact = false; } this.findNodeWithVolume(regexp, arg => { setInvisibleAll(arg.node.fVolume, (sign !== '+')); return exact ? arg : null; // continue search if not exact expression provided }); } /** @summary Decode drawing options */ decodeOptions(opt) { if (!isStr(opt)) opt = ''; if (this.superimpose && (opt.indexOf('same') === 0)) opt = opt.slice(4); const res = this.ctrl, macro = opt.indexOf('macro:'); if (macro >= 0) { let separ = opt.indexOf(';', macro+6); if (separ < 0) separ = opt.length; res.script_name = opt.slice(macro+6, separ); opt = opt.slice(0, macro) + opt.slice(separ+1); console.log(`script ${res.script_name} rest ${opt}`); } while (true) { const pp = opt.indexOf('+'), pm = opt.indexOf('-'); if ((pp < 0) && (pm < 0)) break; let p1 = pp, sign = '+'; if ((p1 < 0) || ((pm >= 0) && (pm < pp))) { p1 = pm; sign = '-'; } let p2 = p1+1; const regexp = /[,; .]/; while ((p2 < opt.length) && !regexp.test(opt[p2]) && (opt[p2] !== '+') && (opt[p2] !== '-')) p2++; const name = opt.substring(p1+1, p2); opt = opt.slice(0, p1) + opt.slice(p2); this.modifyVisisbility(name, sign); } const d = new DrawOptions(opt); if (d.check('MAIN')) res.is_main = true; if (d.check('TRACKS')) res.tracks = true; // only for TGeoManager if (d.check('SHOWTOP')) res.showtop = true; // only for TGeoManager if (d.check('NO_SCREEN')) res.no_screen = true; // ignore kVisOnScreen bits for visibility if (d.check('NOINSTANCING')) res.instancing = -1; // disable usage of InstancedMesh if (d.check('INSTANCING')) res.instancing = 1; // force usage of InstancedMesh if (d.check('ORTHO_CAMERA')) { res.camera_kind = 'orthoXOY'; res.can_rotate = 0; } if (d.check('ORTHO', true)) { res.camera_kind = 'ortho' + d.part; res.can_rotate = 0; } if (d.check('OVERLAY', true)) res.camera_overlay = d.part.toLowerCase(); if (d.check('CAN_ROTATE')) res.can_rotate = true; if (d.check('PERSPECTIVE')) { res.camera_kind = 'perspective'; res.can_rotate = true; } if (d.check('PERSP', true)) { res.camera_kind = 'persp' + d.part; res.can_rotate = true; } if (d.check('MOUSE_CLICK')) res.mouse_click = true; if (d.check('DEPTHRAY') || d.check('DRAY')) res.depthMethod = 'ray'; if (d.check('DEPTHBOX') || d.check('DBOX')) res.depthMethod = 'box'; if (d.check('DEPTHPNT') || d.check('DPNT')) res.depthMethod = 'pnt'; if (d.check('DEPTHSIZE') || d.check('DSIZE')) res.depthMethod = 'size'; if (d.check('DEPTHDFLT') || d.check('DDFLT')) res.depthMethod = 'dflt'; if (d.check('ZOOM', true)) res.zoom = d.partAsFloat(0, 100) / 100; if (d.check('ROTY', true)) res.rotatey = d.partAsFloat(); if (d.check('ROTZ', true)) res.rotatez = d.partAsFloat(); if (d.check('PHONG')) res.material_kind = 'phong'; if (d.check('LAMBERT')) res.material_kind = 'lambert'; if (d.check('MATCAP')) res.material_kind = 'matcap'; if (d.check('TOON')) res.material_kind = 'toon'; if (d.check('AMBIENT')) res.light.kind = 'ambient'; const getCamPart = () => { let neg = 1; if (d.part[0] === 'N') { neg = -1; d.part = d.part.slice(1); } return neg * d.partAsFloat(); }; if (d.check('CAMX', true)) res.camx = getCamPart(); if (d.check('CAMY', true)) res.camy = getCamPart(); if (d.check('CAMZ', true)) res.camz = getCamPart(); if (d.check('CAMLX', true)) res.camlx = getCamPart(); if (d.check('CAMLY', true)) res.camly = getCamPart(); if (d.check('CAMLZ', true)) res.camlz = getCamPart(); if (d.check('BLACK')) res.background = '#000000'; if (d.check('WHITE')) res.background = '#FFFFFF'; if (d.check('BKGR_', true)) { let bckgr = null; if (d.partAsInt(1) > 0) bckgr = getColor(d.partAsInt()); else { for (let col = 0; col < 8; ++col) { if (getColor(col).toUpperCase() === d.part) bckgr = getColor(col); } } if (bckgr) res.background = '#' + new 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.options = res; } /** @summary Activate specified items in the browser */ activateInBrowser(names, force) { if (isStr(names)) names = [names]; if (this._hpainter) { // show browser if it not visible this._hpainter.activateItems(names, force); // if highlight in the browser disabled, suppress in few seconds if (!this.ctrl.update_browser) setTimeout(() => this._hpainter.activateItems([]), 2000); } } /** @summary method used to check matrix calculations performance with current three.js model */ testMatrixes() { let errcnt = 0, totalcnt = 0, totalmax = 0; const arg = { domatrix: true, func: (/* node */) => { let m2 = this.getmatrix(); const entry = this.copyStack(), mesh = this._clones.createObject3D(entry.stack, this._toplevel, 'mesh'); if (!mesh) return true; totalcnt++; const m1 = mesh.matrixWorld; if (m1.equals(m2)) return true; if ((m1.determinant() > 0) && (m2.determinant() < -0.9)) { const flip = new 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(); /* let cnt = */ this._clones.scanVisible(arg); const tm2 = new Date().getTime(); console.log(`Compare matrixes total ${totalcnt} errors ${errcnt} takes ${tm2-tm1} maxdiff ${totalmax}`); } /** @summary Fill context menu */ fillContextMenu(menu) { menu.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(); delete this._gui; } return; } if (!on || !this._renderer) return; const main = this.selectDom(); if (main.style('position') === 'static') main.style('position', 'relative'); this._gui = new GUI({ container: main.node(), closeFolders: true, width: Math.min(300, this._scene_width / 2), title: 'Settings' }); const dom = this._gui.domElement; dom.style.position = 'absolute'; dom.style.top = 0; dom.style.right = 0; this._gui.painter = this; const makeLil = items => { const lil = {}; items.forEach(i => { lil[i.name] = i.value; }); return lil; }; if (!this.ctrl.project) { const selection = this._gui.addFolder('Selection'); if (!this.ctrl.maxnodes) this.ctrl.maxnodes = this._clones?.getMaxVisNodes() ?? 10000; if (!this.ctrl.vislevel) this.ctrl.vislevel = this._clones?.getVisLevel() ?? 3; if (!this.ctrl.maxfaces) this.ctrl.maxfaces = 200000 * this.ctrl.more; this.ctrl.more = 1; selection.add(this.ctrl, 'vislevel', 1, 99, 1) .name('Visibility level') .listen().onChange(() => this.startRedraw(500)); selection.add(this.ctrl, 'maxnodes', 0, 500000, 1000) .name('Visible nodes') .listen().onChange(() => this.startRedraw(500)); selection.add(this.ctrl, 'maxfaces', 0, 5000000, 100000) .name('Max faces') .listen().onChange(() => this.startRedraw(500)); } if (this.ctrl.project) { const bound = this.getGeomBoundingBox(this.getProjectionSource(), 0.01), axis = this.ctrl.project; if (this.ctrl.projectPos === undefined) this.ctrl.projectPos = (bound.min[axis] + bound.max[axis])/2; this._gui.add(this.ctrl, 'projectPos', bound.min[axis], bound.max[axis]) .name(axis.toUpperCase() + ' projection') .onChange(() => this.startDrawGeometry()); } else { // Clipping Options const clipFolder = this._gui.addFolder('Clipping'); for (let naxis = 0; naxis < 3; ++naxis) { const cc = this.ctrl.clip[naxis], axisC = cc.name.toUpperCase(); clipFolder.add(cc, 'enabled') .name('Enable ' + axisC) .listen().onChange(() => this.changedClipping(-1)); clipFolder.add(cc, 'value', cc.min, cc.max, cc.step) .name(axisC + ' position') .listen().onChange(() => this.changedClipping(naxis)); } clipFolder.add(this.ctrl, 'clipIntersect').name('Clip intersection') .onChange(() => this.changedClipping(-1)); clipFolder.add(this.ctrl, 'clipVisualize').name('Visualize') .onChange(() => this.changedClipping(-1)); } // Scene Options const scene = this._gui.addFolder('Scene'); // 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 = arg !== 0; if (this.ctrl.highlight) this.ctrl.highlight_bloom = (arg === 2); } this.ensureBloom(); if (!this.ctrl.highlight) this.highlightMesh(null); this._slave_painters?.forEach(p => { p.ctrl.highlight = this.ctrl.highlight; p.ctrl.highlight_bloom = this.ctrl.highlight_bloom; p.ctrl.bloom_strength = this.ctrl.bloom_strength; p.changedHighlight(); }); } /** @summary Handle change of can rotate */ changeCanRotate(on) { if (on !== undefined) this.ctrl.can_rotate = on; if (this._controls) this._controls.enableRotate = this.ctrl.can_rotate; } /** @summary Change use fog property */ changedUseFog() { this._scene.fog = this.ctrl.use_fog ? this._fog : null; this.render3D(); } /** @summary Handle change of camera kind */ changeCamera() { // force control recreation if (this._controls) { this._controls.cleanup(); delete this._controls; } this.ensureBloom(false); // recreate camera this.createCamera(); this.createSpecialEffects(); // set proper position this.adjustCameraPosition(true); // this.drawOverlay(); this.addOrbitControls(); this.render3D(); // delete this._scene_size; // ensure reassign of camera position // this._first_drawing = true; // this.startDrawGeometry(true); } /** @summary create bloom effect */ ensureBloom(on) { if (on === undefined) { if (this.ctrl.highlight_bloom === 0) this.ctrl.highlight_bloom = this._webgl && !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(); delete this._bloomComposer; if (this._renderer) this._renderer.autoClear = true; this._camera?.layers.disable(_BLOOM_SCENE); this._camera?.layers.set(_ENTIRE_SCENE); } if (this._bloomComposer?.passes) this._bloomComposer.passes[1].strength = this.ctrl.bloom_strength; } /** @summary Show context menu for orbit control * @private */ orbitContext(evnt, intersects) { createMenu(evnt, this).then(menu => { let numitems = 0, numnodes = 0, cnt = 0; if (intersects) { for (let n = 0; n < intersects.length; ++n) { if (getIntersectStack(intersects[n])) numnodes++; if (intersects[n].geo_name) numitems++; } } if (numnodes + numitems === 0) this.fillContextMenu(menu); else { const many = (numnodes + numitems) > 1; if (many) menu.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('') === 0) itemname = (this.getItemName() || 'top') + itemname.slice(6); name = itemname.slice(itemname.lastIndexOf('/')+1); if (!name) name = itemname; hdr = name; } else if (stack) { name = this._clones.getStackName(stack); itemname = this.getStackFullName(stack); hdr = this.getItemName(); if (name.indexOf('Nodes/') === 0) hdr = name.slice(6); else if (name) hdr = name; else if (!hdr) hdr = 'header'; } else continue; cnt++; menu.add((many ? 'sub:' : 'header:') + hdr, itemname, arg => this.activateInBrowser([arg], true)); menu.add('Browse', itemname, arg => this.activateInBrowser([arg], true)); if (this._hpainter) menu.add('Inspect', itemname, arg => this._hpainter.display(arg, kInspect)); if (isFunc(this.hidePhysicalNode)) { menu.add('Hide', itemname, arg => this.hidePhysicalNode([arg])); if (cnt > 1) { menu.add('Hide all before', n, indx => { const items = []; for (let i = 0; i < indx; ++i) { const 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._hpainter); } else if (resolve.obj && (resolve.node.kind === kindEve)) { resolve.obj.fRnrSelf = false; updateBrowserIcons(resolve.obj, this._hpainter); } this.testGeomChanges();// while many volumes may disappear, recheck all of them }, 'Hide all logical nodes of that kind'); menu.add('Hide only this', n, indx => { this._clones.setPhysNodeVisibility(getIntersectStack(intersects[indx]), false); this.testGeomChanges(); }, 'Hide only this physical node'); if (n > 1) { menu.add('Hide all before', n, indx => { for (let k = 0; k < indx; ++k) this._clones.setPhysNodeVisibility(getIntersectStack(intersects[k]), false); this.testGeomChanges(); }, 'Hide all physical nodes before that'); } } if (many) menu.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 && (obj.material.opacity !== undefined)) unique = (obj.material.opacity >= 0.1); if (obj.jsroot_special) unique = false; for (let k = 0; (k < n) && unique; ++k) { if (intersects[k].object === obj) unique = false; } if (!unique) intersects.splice(n, 1); } const clip = this.ctrl.clip; if (clip[0].enabled || clip[1].enabled || clip[2].enabled) { const clippedIntersects = []; for (let i = 0; i < intersects.length; ++i) { const point = intersects[i].point, special = (intersects[i].object.type === 'Points'); let clipped = true; if (clip[0].enabled && ((this._clipPlanes[0].normal.dot(point) > this._clipPlanes[0].constant) ^ special)) clipped = false; if (clip[1].enabled && ((this._clipPlanes[1].normal.dot(point) > this._clipPlanes[1].constant) ^ special)) clipped = false; if (clip[2].enabled && (this._clipPlanes[2].normal.dot(point) > this._clipPlanes[2].constant)) clipped = false; if (!clipped) clippedIntersects.push(intersects[i]); } intersects = clippedIntersects; } return intersects; } /** @summary test camera position * @desc function analyzes camera position and start redraw of geometry * if objects in view may be changed */ testCameraPositionChange() { if (!this.ctrl.select_in_view || this._draw_all_nodes) return; const matrix = createProjectionMatrix(this._camera), frustum = createFrustum(matrix); // check if overall bounding box seen if (!frustum.CheckBox(this.getGeomBoundingBox())) this.startDrawGeometry(); } /** @summary Resolve stack */ resolveStack(stack) { return this._clones && stack ? this._clones.resolveStack(stack) : null; } /** @summary Returns stack full name * @desc Includes item name of top geo object */ getStackFullName(stack) { const mainitemname = this.getItemName(), sub = this.resolveStack(stack); if (!sub || !sub.name) return mainitemname; return mainitemname ? mainitemname + '/' + sub.name : sub.name; } /** @summary Add handler which will be called when element is highlighted in geometry drawing * @desc Handler should have highlightMesh function with same arguments as TGeoPainter */ addHighlightHandler(handler) { if (!isFunc(handler?.highlightMesh)) return; if (!this._highlight_handlers) this._highlight_handlers = []; this._highlight_handlers.push(handler); } /** @summary perform mesh highlight */ highlightMesh(active_mesh, color, geo_object, geo_index, geo_stack, no_recursive) { if (geo_object) { active_mesh = active_mesh ? [active_mesh] : []; const extras = this.getExtrasContainer(); if (extras) { extras.traverse(obj3d => { if ((obj3d.geo_object === geo_object) && (active_mesh.indexOf(obj3d) < 0)) active_mesh.push(obj3d); }); } } else if (geo_stack && this._toplevel) { active_mesh = []; this._toplevel.traverse(mesh => { if ((mesh instanceof 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._master_painter ? this._slave_painters : this._master_painter._slave_painters.concat([this._master_painter])); for (let k = 0; k < lst?.length; ++k) { if (lst[k] !== this) lst[k].highlightMesh(null, color, geo_object, geo_index, geo_stack, true); } } const curr_mesh = this._selected_mesh; if (!curr_mesh && !active_mesh) return false; const get_ctrl = mesh => mesh.get_ctrl ? mesh.get_ctrl() : new GeoDrawingControl(mesh, this.ctrl.highlight_bloom && this._bloomComposer); let same = false; // check if selections are the same if (curr_mesh && active_mesh && (curr_mesh.length === active_mesh.length)) { same = true; for (let k = 0; (k < curr_mesh.length) && same; ++k) if ((curr_mesh[k] !== active_mesh[k]) || get_ctrl(curr_mesh[k]).checkHighlightIndex(geo_index)) same = false; } if (same) return Boolean(curr_mesh); if (curr_mesh) { for (let k = 0; k < curr_mesh.length; ++k) get_ctrl(curr_mesh[k]).setHighlight(); } this._selected_mesh = active_mesh; if (active_mesh) { for (let k = 0; k < active_mesh.length; ++k) get_ctrl(active_mesh[k]).setHighlight(color || new 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('') === 0) info = this.getItemName() + info.slice(6); names.push(info); if (!active_mesh) { active_mesh = obj; tooltip = info; geo_object = obj.geo_object; if (obj.get_ctrl) { geo_index = obj.get_ctrl().extractIndex(intersects[k]); if ((geo_index !== undefined) && isStr(tooltip)) tooltip += ' indx:' + JSON.stringify(geo_index); } geo_stack = stack; if (geo_stack) { resolve = this.resolveStack(geo_stack); if (obj.stacks) geo_index = intersects[k].instanceId; } } } this.highlightMesh(active_mesh, undefined, geo_object, geo_index); if (this.ctrl.update_browser) { if (this.ctrl.highlight && tooltip) names = [tooltip]; this.activateInBrowser(names); } if (!resolve?.obj) return tooltip; const lines = provideObjectInfo(resolve.obj); lines.unshift(tooltip); return { name: resolve.obj.fName, title: resolve.obj.fTitle || resolve.obj._typename, lines }; }; this._controls.processMouseLeave = function() { this.processMouseMove([]); // to disable highlight and reset browser }; this._controls.processDblClick = () => { // painter already cleaned up, ignore any incoming events if (!this.ctrl || !this._controls) return; if (this._last_manifest) { this._last_manifest.wireframe = !this._last_manifest.wireframe; if (this._last_hidden) this._last_hidden.forEach(obj => { obj.visible = true; }); delete this._last_hidden; delete this._last_manifest; } else this.adjustCameraPosition(true); this.render3D(); }; } /** @summary Main function in geometry creation loop * @desc Returns: * - false when nothing todo * - true if one could perform next action immediately * - 1 when call after short timeout required * - 2 when call must be done from processWorkerReply */ nextDrawAction() { if (!this._clones || this.isStage(stageInit)) return false; if (this.isStage(stageCollect)) { if (this._geom_viewer) { this._draw_all_nodes = false; this.changeStage(stageAnalyze); return true; } // wait until worker is really started if (this.ctrl.use_worker > 0) { if (!this._worker) { this.startWorker(); return 1; } if (!this._worker_ready) return 1; } // first copy visibility flags and check how many unique visible nodes exists let numvis = this._first_drawing ? this._clones.countVisibles() : 0, matrix = null, frustum = null; if (!numvis) numvis = this._clones.markVisibles(false, false, 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); delete this._new_append_nodes; } else if (this._draw_nodes) { let del; if (this._geom_viewer) del = this._draw_nodes; else del = this._clones.mergeVisibles(this._new_draw_nodes, this._draw_nodes); // remove should be fast, do it here for (let n = 0; n < del.length; ++n) this._clones.createObject3D(del[n].stack, this._toplevel, 'delete_mesh'); if (del.length > 0) this.drawing_log = `Delete ${del.length} nodes`; } this._draw_nodes = this._new_draw_nodes; delete this._new_draw_nodes; this.changeStage(stageCollShapes); return true; } if (this.isStage(stageCollShapes)) { // collect shapes const shapes = this._clones.collectShapes(this._draw_nodes); // merge old and new list with produced shapes this._build_shapes = this._clones.mergeShapesLists(this._build_shapes, shapes); this.changeStage(stageStartBuild); return true; } if (this.isStage(stageStartBuild)) { // this is building of geometries, // one can ask worker to build them or do it ourself if (this.canSubmitToWorker()) { const job = { limit: this._current_face_limit, shapes: [] }; let cnt = 0; for (let n = 0; n < this._build_shapes.length; ++n) { 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: countGeometryFaces(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; // if (res.notusedshapes < 30) return true; } } // final stage, create all meshes const tm0 = new Date().getTime(), toplevel = this.ctrl.project ? this._full_geom : this._toplevel; let build_instanced = false, ready = true; if (!this.ctrl.project) build_instanced = this._clones.createInstancedMeshes(this.ctrl, toplevel, this._draw_nodes, this._build_shapes, getRootColors()); if (!build_instanced) { for (let n = 0; n < this._draw_nodes.length; ++n) { const entry = this._draw_nodes[n]; if (entry.done) continue; // shape can be provided with entry itself const shape = entry.server_shape || this._build_shapes[entry.shapeid]; this.createEntryMesh(entry, shape, toplevel); const tm1 = new Date().getTime(); if (tm1 - tm0 > 500) { ready = false; break; } } } if (ready) { if (this.ctrl.project) { this.changeStage(stageBuildProj); return true; } this.changeStage(stageInit); return false; } if (!this.isStage(stageBuild)) this.drawing_log = `Building meshes ${this.ctrl.info.num_meshes} / ${this.ctrl.info.num_faces}`; return true; } if (this.isStage(stageWaitMain)) { // wait for main painter to be ready if (!this._master_painter) { this.changeStage(stageInit, 'Lost main painter'); return false; } if (!this._master_painter._drawing_ready) return 1; this.changeStage(stageBuildProj); // just do projection } if (this.isStage(stageBuildProj)) { this.doProjection(); this.changeStage(stageInit); return false; } console.error(`never come here, stage ${this.drawing_stage}`); return false; } /** @summary Insert appropriate mesh for given entry */ createEntryMesh(entry, shape, toplevel) { // workaround for the TGeoOverlap, where two branches should get predefined color if (this._splitColors && entry.stack) { if (entry.stack[0] === 0) entry.custom_color = 'green'; else if (entry.stack[0] === 1) entry.custom_color = 'blue'; } this._clones.createEntryMesh(this.ctrl, toplevel, entry, shape, getRootColors()); return true; } /** @summary used by geometry viewer to show more nodes * @desc These nodes excluded from selection logic and always inserted into the model * Shape already should be created and assigned to the node */ appendMoreNodes(nodes, from_drawing) { if (!this.isStage(stageInit) && !from_drawing) { this._provided_more_nodes = nodes; return; } // delete old nodes if (this._more_nodes) { for (let n = 0; n < this._more_nodes.length; ++n) { const entry = this._more_nodes[n], obj3d = this._clones.createObject3D(entry.stack, this._toplevel, 'delete_mesh'); disposeThreejsObject(obj3d); cleanupShape(entry.server_shape); delete entry.server_shape; } } delete this._more_nodes; if (!nodes) return; const real_nodes = []; for (let k = 0; k < nodes.length; ++k) { const entry = nodes[k], shape = entry.server_shape; if (!shape?.ready) continue; if (this.createEntryMesh(entry, shape, this._toplevel)) real_nodes.push(entry); } // remember additional nodes only if they include shape - otherwise one can ignore them if (real_nodes.length > 0) this._more_nodes = real_nodes; if (!from_drawing) this.render3D(); } /** @summary Returns hierarchy of 3D objects used to produce projection. * @desc Typically external master painter is used, but also internal data can be used */ getProjectionSource() { if (this._clones_owner) return this._full_geom; if (!this._master_painter) { console.warn('MAIN PAINTER DISAPPER'); return null; } if (!this._master_painter._drawing_ready) { console.warn('MAIN PAINTER NOT READY WHEN DO PROJECTION'); return null; } return this._master_painter._toplevel; } /** @summary Extend custom geometry bounding box */ extendCustomBoundingBox(box) { if (!box) return; if (!this._customBoundingBox) this._customBoundingBox = new THREE.Box3().makeEmpty(); const origin = this._customBoundingBox.clone(); this._customBoundingBox.union(box); if (!this._customBoundingBox.equals(origin)) this._adjust_camera_with_render = true; } /** @summary Calculate geometry bounding box */ getGeomBoundingBox(topitem, scalar) { const box3 = new THREE.Box3(), check_any = !this._clones; if (topitem === undefined) topitem = this._toplevel; box3.makeEmpty(); if (this._customBoundingBox && (topitem === this._toplevel)) { box3.union(this._customBoundingBox); return box3; } if (!topitem) { box3.min.x = box3.min.y = box3.min.z = -1; box3.max.x = box3.max.y = box3.max.z = 1; return box3; } topitem.traverse(mesh => { if (check_any || (mesh.stack && (mesh instanceof 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) return; 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); delete this._camera; } 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_width, this._scene_height); } this.ensureBloom(); } /** @summary Initial scene creation */ async createScene(w, h, render3d) { if (this.superimpose) { const cfg = getHistPainter3DCfg(this.getMainPainter()); if (cfg?.renderer) { this._scene = cfg.scene; this._scene_width = cfg.scene_width; this._scene_height = cfg.scene_height; this._renderer = cfg.renderer; this._webgl = (this._renderer.jsroot_render3d === constants$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) this._clones.setDefaultColors(this.ctrl.dflt_colors); this._startm = new Date().getTime(); this._last_render_tm = this._startm; this._last_render_meshes = 0; this.changeStage(stageCollect); this._drawing_ready = false; this.ctrl.info.num_meshes = 0; this.ctrl.info.num_faces = 0; this.ctrl.info.num_shapes = 0; this._selected_mesh = null; if (this.ctrl.project) { if (this._clones_owner) { if (this._full_geom) this.changeStage(stageBuildProj); else this._full_geom = new THREE.Object3D(); } else this.changeStage(stageWaitMain); } delete this._last_manifest; delete this._last_hidden; // clear list of hidden objects delete this._draw_nodes_again; // forget about such flag this.continueDraw(); } /** @summary reset all kind of advanced features like depth test */ resetAdvanced() { this.ctrl.depthTest = true; this.ctrl.clipIntersect = true; this.ctrl.depthMethod = 'ray'; this.changedDepthMethod('norender'); this.changedDepthTest(); } /** @summary returns maximal dimension */ getOverallSize(force) { if (!this._overall_size || force || this._customBoundingBox) { const box = this.getGeomBoundingBox(); // if detect of coordinates fails - ignore if (!Number.isFinite(box.min.x)) return 1000; this._overall_size = 2 * Math.max(box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z); } return this._overall_size; } /** @summary Create png image with drawing snapshot. */ createSnapshot(filename) { if (!this._renderer) return; this.render3D(0); const dataUrl = this._renderer.domElement.toDataURL('image/png'); if (filename === 'asis') return dataUrl; dataUrl.replace('image/png', 'image/octet-stream'); const doc = getDocument(), link = doc.createElement('a'); if (isStr(link.download)) { doc.body.appendChild(link); // Firefox requires the link to be in the body link.download = filename || 'geometry.png'; link.href = dataUrl; link.click(); doc.body.removeChild(link); // remove the link when done } } /** @summary Returns url parameters defining camera position. * @desc Either absolute position are provided (arg === true) or zoom, roty, rotz parameters */ produceCameraUrl(arg) { if (!this._camera) return ''; if (this._camera.isOrthographicCamera) { const zoom = Math.round(this._camera.zoom * 100); return this.ctrl.camera_kind + (zoom === 100 ? '' : `,zoom=${zoom}`); } let kind = ''; if (this.ctrl.camera_kind !== 'perspective') kind = this.ctrl.camera_kind + ','; if (arg === true) { const p = this._camera?.position, t = this._controls?.target; if (!p || !t) return ''; const conv = v => { let s = ''; if (v < 0) { s = 'n'; v = -v; } return s + v.toFixed(0); }; let res = `${kind}camx${conv(p.x)},camy${conv(p.y)},camz${conv(p.z)}`; if (t.x || t.y || t.z) res += `,camlx${conv(t.x)},camly${conv(t.y)},camlz${conv(t.z)}`; return res; } if (!this._lookat || !this._camera0pos) return ''; const pos1 = new 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 d = this._scene_size, test = (v1, v2, scale) => { if (!scale) scale = Math.abs((v1+v2)/2); return scale <= 1e-20 ? true : Math.abs(v2-v1)/scale > 0.01; }, large_change = test(sizex, d.sizex) || test(sizey, d.sizey) || test(sizez, d.sizez) || test(midx, d.midx, d.sizex) || test(midy, d.midy, d.sizey) || test(midz, d.midz, d.sizez); if (!large_change) { if (this.ctrl.select_in_view) this.startDrawGeometry(); return; } } this._scene_size = { sizex, sizey, sizez, midx, midy, midz }; this._overall_size = 2 * Math.max(sizex, sizey, sizez); this._camera.near = this._overall_size / 350; this._camera.far = this._overall_size * 100; this._fog.near = this._overall_size * 0.5; this._fog.far = this._overall_size * 5; if (first_time) { for (let naxis = 0; naxis < 3; ++naxis) { const cc = this.ctrl.clip[naxis]; cc.min = box.min[cc.name]; cc.max = box.max[cc.name]; const sz = cc.max - cc.min; cc.max += sz*0.01; cc.min -= sz*0.01; if (sz > 100) cc.step = 0.1; else if (sz > 1) cc.step = 0.001; else cc.step = undefined; if (!cc.value) cc.value = (cc.min + cc.max) / 2; else if (cc.value < cc.min) cc.value = cc.min; else if (cc.value > cc.max) cc.value = cc.max; } } let k = 2*this.ctrl.zoom; const max_all = Math.max(sizex, sizey, sizez), sign = this.ctrl.camera_kind.indexOf('N') > 0 ? -1 : 1; this._lookat = new 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; // Interpolate // const animate = () => { if (this._animating === undefined) return; if (this._animating) requestAnimationFrame(animate); else if (!this._geom_viewer) this.startDrawGeometry(); const smoothFactor = -Math.cos((2.0*Math.PI*step)/frames) + 1.0; this._camera.position.add(posIncrement.clone().multiplyScalar(smoothFactor)); oldTarget.add(targetIncrement.clone().multiplyScalar(smoothFactor)); this._lookat = oldTarget; this._camera.lookAt(this._lookat); this._camera.updateProjectionMatrix(); const tm1 = new Date().getTime(); if (autoClip) { for (let axis = 0; axis < 3; ++axis) this.ctrl.clip[axis].value += this.ctrl.clip[axis].inc * smoothFactor; this.updateClipping(); } else this.render3D(0); const tm2 = new Date().getTime(); if ((step === 0) && (tm2-tm1 > 200)) frames = 20; step++; this._animating = step < frames; }; animate(); // this._controls.update(); } /** @summary 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 === 'TTree') { // drop tree means function call which must extract tracks from provided tree let funcname = 'extract_geo_tracks'; if (opt && opt.indexOf('$') > 0) { funcname = opt.slice(0, opt.indexOf('$')); opt = opt.slice(opt.indexOf('$')+1); } const func = findFunction(funcname); if (!func) return Promise.reject(Error(`Function ${funcname} not found`)); return func(obj, opt).then(tracks => { if (!tracks) return this; // FIXME: probably tracks should be remembered? return this.drawExtras(tracks, '', false).then(() => { this.updateClipping(true); return this.render3D(100); }); }); } return this.drawExtras(obj, itemname).then(is_any => { if (!is_any) return this; if (hitem) hitem._painter = this; // set for the browser item back pointer return this.render3D(100); }); } /** @summary function called when mouse is going over the item in the browser */ mouseOverHierarchy(on, itemname, hitem) { if (!this.ctrl) return; // protection for cleaned-up painter const obj = hitem._obj; // let's highlight tracks and hits only for the time being if (!obj || (obj._typename !== clTEveTrack && obj._typename !== clTEvePointSet && obj._typename !== clTPolyMarker3D)) return; this.highlightMesh(null, 0x00ff00, on ? obj : null); } /** @summary clear extra drawn objects like tracks or hits */ clearExtras() { this.getExtrasContainer('delete'); delete this._extraObjects; // workaround, later will be normal function this.render3D(); } /** @summary Register extra objects like tracks or hits * @desc Rendered after main geometry volumes are created * Check if object already exists to prevent duplication */ addExtra(obj, itemname) { if (this._extraObjects === undefined) this._extraObjects = create$1(clTList); if (this._extraObjects.arr.indexOf(obj) >= 0) return false; this._extraObjects.Add(obj, itemname); delete obj.$hidden_via_menu; // remove previous hidden property return true; } /** @summary manipulate visibility of extra objects, used for HierarchyPainter * @private */ extraObjectVisible(hpainter, hitem, toggle) { if (!this._extraObjects) return; const itemname = hpainter.itemFullName(hitem); let indx = this._extraObjects.opt.indexOf(itemname); if ((indx < 0) && hitem._obj) { indx = this._extraObjects.arr.indexOf(hitem._obj); // workaround - if object found, replace its name if (indx >= 0) this._extraObjects.opt[indx] = itemname; } if (indx < 0) return; const obj = this._extraObjects.arr[indx]; let res = 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 || '') + `/[${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('/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._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) !== 0)) return null; itemname = this.geo_manager ? prnt.fName : ''; first_level = true; volumes = []; } else { if (itemname) itemname += '/'; itemname += prnt.fName; } if (!prnt.fVolume || prnt.fVolume._searched) return null; if (name.test(prnt.fVolume.fName)) { res = action({ node: prnt, item: itemname }); if (res) return res; } prnt.fVolume._searched = true; volumes.push(prnt.fVolume); if (prnt.fVolume.fNodes) { for (let n = 0, len = prnt.fVolume.fNodes.arr.length; n < len; ++n) { res = this.findNodeWithVolume(name, action, prnt.fVolume.fNodes.arr[n], itemname, volumes); if (res) break; } } if (first_level) { for (let n = 0, len = volumes.length; n < len; ++n) delete volumes[n]._searched; } return res; } /** @summary Process script option - load and execute some gGeoManager-related calls */ async loadMacro(script_name) { const result = { obj: this.getGeometry(), prefix: '' }; if (this.geo_manager) result.prefix = result.obj.fName; if (!script_name || (script_name.length < 3) || (getNodeKind(result.obj) !== 0)) return result; const mgr = { GetVolume: name => { const regexp = new RegExp('^'+name+'$'), currnode = this.findNodeWithVolume(regexp, arg => arg); if (!currnode) console.log(`Did not found ${name} volume`); // return proxy object with several methods, typically used in ROOT geom scripts return { found: currnode, fVolume: currnode?.node?.fVolume, InvisibleAll(flag) { setInvisibleAll(this.fVolume, flag); }, Draw() { if (!this.found || !this.fVolume) return; result.obj = this.found.node; result.prefix = this.found.item; console.log(`Select volume for drawing ${this.fVolume.fName} ${result.prefix}`); }, SetTransparency(lvl) { if (this.fVolume?.fMedium?.fMaterial) this.fVolume.fMedium.fMaterial.fFillStyle = 3000 + lvl; }, SetLineColor(col) { if (this.fVolume) this.fVolume.fLineColor = col; } }; }, DefaultColors: () => { this.ctrl.dflt_colors = true; }, SetMaxVisNodes: limit => { if (!this.ctrl.maxnodes) this.ctrl.maxnodes = parseInt(limit) || 0; }, SetVisLevel: limit => { if (!this.ctrl.vislevel) this.ctrl.vislevel = parseInt(limit) || 0; } }; showProgress(`Loading macro ${script_name}`); return httpRequest(script_name, 'text').then(script => { const lines = script.split('\n'); let indx = 0; while (indx < lines.length) { let line = lines[indx++].trim(); if (line.indexOf('//') === 0) continue; if (line.indexOf('gGeoManager') < 0) continue; line = line.replace('->GetVolume', '.GetVolume'); line = line.replace('->InvisibleAll', '.InvisibleAll'); line = line.replace('->SetMaxVisNodes', '.SetMaxVisNodes'); line = line.replace('->DefaultColors', '.DefaultColors'); line = line.replace('->Draw', '.Draw'); line = line.replace('->SetTransparency', '.SetTransparency'); line = line.replace('->SetLineColor', '.SetLineColor'); line = line.replace('->SetVisLevel', '.SetVisLevel'); if (line.indexOf('->') >= 0) continue; try { const func = new Function('gGeoManager', line); func(mgr); } catch { console.error(`Problem by processing ${line}`); } } return result; }).catch(() => { console.error(`Fail to load ${script_name}`); return result; }); } /** @summary Assign clones, created outside. * @desc Used by geometry painter, where clones are handled by the server */ assignClones(clones) { this._clones_owner = true; this._clones = clones; } /** @summary Extract shapes from draw message of geometry painter * @desc For the moment used in batch production */ extractRawShapes(draw_msg, recreate) { let nodes = null, old_gradpersegm = 0; // array for descriptors for each node // if array too large (>1M), use JS object while only ~1K nodes are expected to be used if (recreate) { // if (draw_msg.kind !== 'draw') return false; nodes = (draw_msg.numnodes > 1e6) ? { length: draw_msg.numnodes } : new Array(draw_msg.numnodes); // array for all nodes } draw_msg.nodes.forEach(node => { node = ClonedNodes.formatServerElement(node); if (nodes) nodes[node.id] = node; else this._clones.updateNode(node); }); if (recreate) { this._clones_owner = true; this._clones = new ClonedNodes(null, nodes); this._clones.name_prefix = this._clones.getNodeName(0); this._clones.setConfig(this.ctrl); // normally only need when making selection, not used in geo viewer // this.geo_clones.setMaxVisNodes(draw_msg.maxvisnodes); // this.geo_clones.setVisLevel(draw_msg.vislevel); // TODO: provide from server this._clones.maxdepth = 20; } let nsegm = 0; if (draw_msg.cfg) nsegm = draw_msg.cfg.nsegm; if (nsegm) { old_gradpersegm = geoCfg('GradPerSegm'); geoCfg('GradPerSegm', 360 / Math.max(nsegm, 6)); } for (let cnt = 0; cnt < draw_msg.visibles.length; ++cnt) { const item = draw_msg.visibles[cnt], rd = item.ri; // entry may be provided without shape - it is ok if (rd) item.server_shape = rd.server_shape = createServerGeometry(rd, nsegm); } if (old_gradpersegm) geoCfg('GradPerSegm', old_gradpersegm); return true; } /** @summary Prepare drawings * @desc Return value used as promise for painter */ async prepareObjectDraw(draw_obj, name_prefix) { // if did cleanup - ignore all kind of activity if (this.did_cleanup) return null; if (name_prefix === '__geom_viewer_append__') { this._new_append_nodes = draw_obj; this.ctrl.use_worker = 0; this._geom_viewer = true; // indicate that working with geom viewer } else if ((name_prefix === '__geom_viewer_selection__') && this._clones) { // these are selection done from geom viewer this._new_draw_nodes = draw_obj; this.ctrl.use_worker = 0; this._geom_viewer = true; // indicate that working with geom viewer } else if (this._master_painter) { this._clones_owner = false; this._clones = this._master_painter._clones; console.log(`Reuse clones ${this._clones.nodes.length} from main painter`); } else if (!draw_obj) { this._clones_owner = false; this._clones = null; } else { this._start_drawing_time = new Date().getTime(); this._clones_owner = true; this._clones = new ClonedNodes(draw_obj); let lvl = this.ctrl.vislevel, maxnodes = this.ctrl.maxnodes; if (this.geo_manager) { if (!lvl && this.geo_manager.fVisLevel) lvl = this.geo_manager.fVisLevel; if (!maxnodes) maxnodes = this.geo_manager.fMaxVisNodes; } this._clones.setVisLevel(lvl); this._clones.setMaxVisNodes(maxnodes, this.ctrl.more); this._clones.setConfig(this.ctrl); this._clones.name_prefix = name_prefix; const hide_top_volume = 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) 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.options.Render3D, this.batch_mode); // activate worker if ((this.ctrl.use_worker > 0) && !this.batch_mode) this.startWorker(); assign3DHandler(this); const size = this.getSizeFor3d(undefined, render3d); this._fit_main_area = (size.can3d === -1); promise = this.createScene(size.width, size.height, render3d) .then(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._resolveFunc = resolveFunc; this.showDrawInfo('Drawing geometry'); this.startDrawGeometry(true); }); }); } /** @summary methods show info when first geometry drawing is performed */ showDrawInfo(msg) { if (this.isBatchMode() || !this._first_drawing || !this._start_drawing_time) return; const main = this._renderer.domElement.parentNode; if (!main) return; let info = main.querySelector('.geo_info'); if (!msg) info?.remove(); else { const spent = (new Date().getTime() - this._start_drawing_time)*1e-3; if (!info) { info = getDocument().createElement('p'); info.setAttribute('class', 'geo_info'); info.setAttribute('style', 'position: absolute; text-align: center; vertical-align: middle; top: 45%; left: 40%; color: red; font-size: 150%;'); main.append(info); } info.innerHTML = `${msg}, ${spent.toFixed(1)}s`; } } /** @summary Reentrant method to perform geometry drawing step by step */ continueDraw() { // nothing to do - exit if (this.isStage(stageInit)) return; const tm0 = new Date().getTime(), interval = this._first_drawing ? 1000 : 200; let now = tm0; while (true) { const res = this.nextDrawAction(); if (!res) break; now = new Date().getTime(); // stop creation after 100 sec, render as is if (now - this._startm > 1e5) { this.changeStage(stageInit, 'Abort build after 100s'); break; } // if we are that fast, do next action if ((res === true) && (now - tm0 < interval)) continue; if ((now - tm0 > interval) || (res === 1) || (res === 2)) { showProgress(this.drawing_log); this.showDrawInfo(this.drawing_log); if (this._first_drawing && this._webgl && (this._num_meshes - this._last_render_meshes > 100) && (now - this._last_render_tm > 2.5*interval)) { this.adjustCameraPosition(); this.render3D(-1); this._last_render_meshes = this.ctrl.info.num_meshes; } if (res !== 2) setTimeout(() => this.continueDraw(), (res === 1) ? 100 : 1); return; } } const take_time = now - this._startm; if (this._first_drawing || this._full_redrawing) console.log(`Create tm = ${take_time} meshes ${this.ctrl.info.num_meshes} faces ${this.ctrl.info.num_faces}`); if (take_time > 300) { showProgress('Rendering geometry'); this.showDrawInfo('Rendering'); return setTimeout(() => this.completeDraw(true), 10); } this.completeDraw(true); } /** @summary Checks camera position and recalculate rendering order if needed * @param force - if specified, forces calculations of render order */ testCameraPosition(force) { this._camera.updateMatrixWorld(); this.drawOverlay(); const origin = this._camera.position.clone(); if (!force && this._last_camera_position) { // if camera position does not changed a lot, ignore such change const dist = this._last_camera_position.distanceTo(origin); if (dist < (this._overall_size || 1000)*1e-4) return; } this._last_camera_position = origin; // remember current camera position if (this.ctrl._axis) { const vect = (this._controls?.target || this._lookat).clone().sub(this._camera.position).normalize(); this.getExtrasContainer('get', 'axis')?.traverse(obj3d => { if (isFunc(obj3d._axis_flip)) obj3d._axis_flip(vect); }); } if (!this.ctrl.project) produceRenderOrder(this._toplevel, origin, this.ctrl.depthMethod, this._clones); } /** @summary Call 3D rendering of the geometry * @param tmout - specifies delay, after which actual rendering will be invoked * @param [measure] - when true, for the first time printout rendering time * @return {Promise} when tmout bigger than 0 is specified * @desc Timeout used to avoid multiple rendering of the picture when several 3D drawings * superimposed with each other. If 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('renderer object not exists - check code'); else console.warn('try to render after cleanup'); return this; } const ret_promise = (tmout !== undefined) && (tmout > 0) && (measure !== 'nopromise'); if (tmout === undefined) tmout = 5; // by default, rendering happens with timeout if ((tmout > 0) && this._webgl) { if (this.isBatchMode()) tmout = 1; // use minimal timeout in batch mode if (ret_promise) { return new Promise(resolveFunc => { if (!this._render_resolveFuncs) this._render_resolveFuncs = []; this._render_resolveFuncs.push(resolveFunc); if (!this.render_tmout) this.render_tmout = setTimeout(() => this.render3D(0), tmout); }); } if (!this.render_tmout) this.render_tmout = setTimeout(() => this.render3D(0), tmout); return this; } if (this.render_tmout) { clearTimeout(this.render_tmout); delete this.render_tmout; } beforeRender3D(this._renderer); const tm1 = new Date(); if (this._adjust_camera_with_render) { this.adjustCameraPosition('only_set'); delete this._adjust_camera_with_render; } this.testCameraPosition(tmout === -1); // its needed for outlinePass - do rendering, most consuming time if (this._webgl && this._effectComposer && (this._effectComposer.passes.length > 0)) this._effectComposer.render(); else if (this._webgl && this._bloomComposer && (this._bloomComposer.passes.length > 0)) { this._renderer.clear(); this._camera.layers.set(_BLOOM_SCENE); this._bloomComposer.render(); this._renderer.clearDepth(); this._camera.layers.set(_ENTIRE_SCENE); this._renderer.render(this._scene, this._camera); } else this._renderer.render(this._scene, this._camera); const tm2 = new Date(); this.last_render_tm = tm2.getTime(); if ((this.first_render_tm === 0) && (measure === true)) { this.first_render_tm = tm2.getTime() - tm1.getTime(); if (this.first_render_tm > 500) console.log(`three.js r${THREE.REVISION}, first render tm = ${this.first_render_tm}`); } afterRender3D(this._renderer); if (this._render_resolveFuncs) { const arr = this._render_resolveFuncs; delete this._render_resolveFuncs; arr.forEach(func => func(this)); } } /** @summary Start geo worker */ startWorker() { if (this._worker) return; this._worker_ready = false; this._worker_jobs = 0; // counter how many requests send to worker // TODO: modules not yet working, see https://www.codedread.com/blog/archives/2017/10/19/web-workers-can-be-es6-modules-too/ this._worker = new Worker(exports.source_dir + 'scripts/geoworker.js' /* , { type: 'module' } */); this._worker.onmessage = e => { if (!isObject(e.data)) return; if ('log' in e.data) return console.log(`geo: ${e.data.log}`); if ('progress' in e.data) return showProgress(e.data.progress); e.data.tm3 = new Date().getTime(); if ('init' in e.data) { this._worker_ready = true; console.log(`Worker ready: ${e.data.tm3 - e.data.tm0}`); } else this.processWorkerReply(e.data); }; // send initialization message with clones this._worker.postMessage({ init: true, // indicate init command for worker browser, tm0: new Date().getTime(), vislevel: this._clones.getVisLevel(), maxvisnodes: this._clones.getMaxVisNodes(), clones: this._clones.nodes, sortmap: this._clones.sortmap }); } /** @summary check if one can submit request to worker * @private */ canSubmitToWorker(force) { if (!this._worker) return false; return this._worker_ready && ((this._worker_jobs === 0) || force); } /** @summary submit request to worker * @private */ submitToWorker(job) { if (!this._worker) return false; this._worker_jobs++; job.tm0 = new Date().getTime(); this._worker.postMessage(job); } /** @summary process reply from worker * @private */ processWorkerReply(job) { this._worker_jobs--; if ('collect' in job) { this._new_draw_nodes = job.new_nodes; this._draw_all_nodes = job.complete; this.changeStage(stageAnalyze); // invoke methods immediately return this.continueDraw(); } if ('shapes' in job) { for (let n=0; n { 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); this.redrawObject('same'); } /** @summary Should be called when configuration of particular axis is changed */ changedClipping(naxis = -1) { if ((naxis < 0) || this.ctrl.clip[naxis]?.enabled) this.updateClipping(false, true); } /** @summary Should be called when depth test flag is changed */ changedDepthTest() { if (!this._toplevel) return; const flag = this.ctrl.depthTest; this._toplevel.traverse(node => { if (node instanceof 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 delete this._last_camera_position; if (arg !== 'norender') return this.render3D(); } /** @summary Assign clipping attributes to the meshes - supported only for webgl */ updateClipping(without_render, force_traverse) { // do not try clipping with SVG renderer if (this._renderer?.jsroot_render3d === constants$1.Render3D.SVG) return; if (!this._clipPlanes) { this._clipPlanes = [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._clipPlanes[k].constant !== clip_constants[k]) { if (clip[k].enabled) changed = true; this._clipPlanes[k].constant = clip_constants[k]; } if (clip[k].enabled) panels.push(this._clipPlanes[k]); if (container && clip[k].enabled) { const helper = new THREE.PlaneHelper(this._clipPlanes[k], (clip[k].max - clip[k].min)); helper._no_clip = true; container.add(helper); } } if (panels.length === 0) panels = null; if (this._clipCfg !== clip_cfg) changed = true; this._clipCfg = clip_cfg; const any_clipping = 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 = Promise.resolve(true); if (!this._clones) { check_extras = false; // if extra object where append, redraw them at the end this.getExtrasContainer('delete'); // delete old container const extras = (this._master_painter ? this._master_painter._extraObjects : null) || this._extraObjects; promise = this.drawExtras(extras, '', false); } else if (this._first_drawing || this._full_redrawing) { if (this.ctrl.tracks && this.geo_manager) promise = this.drawExtras(this.geo_manager.fTracks, '/Tracks'); } return promise.then(() => { if (this._full_redrawing) { this.adjustCameraPosition('first'); this._full_redrawing = false; full_redraw = true; this.changedDepthMethod('norender'); } if (this._first_drawing) { this.adjustCameraPosition('first'); this.showDrawInfo(); this._first_drawing = false; first_time = true; full_redraw = true; } if (first_time) this.completeScene(); if (full_redraw && (this.ctrl.trans_radial || this.ctrl.trans_z)) this.changedTransformation('norender'); if (full_redraw) return this.drawAxesAndOverlay(true); }).then(() => { this._scene.overrideMaterial = null; if (this._provided_more_nodes !== undefined) { this.appendMoreNodes(this._provided_more_nodes, true); delete this._provided_more_nodes; } if (check_extras) { // if extra object where append, redraw them at the end this.getExtrasContainer('delete'); // delete old container const extras = this._master_painter?._extraObjects || this._extraObjects; return this.drawExtras(extras, '', false); } }).then(() => { this.updateClipping(true); // do not render this.render3D(0, true); if (close_progress) showProgress(); this.addOrbitControls(); if (first_time && !this.isBatchMode()) { // after first draw check if highlight can be enabled if (this.ctrl.highlight === 0) this.ctrl.highlight = (this.first_render_tm < 1000); // also highlight of scene object can be assigned at the first draw if (this.ctrl.highlight_scene === 0) this.ctrl.highlight_scene = this.ctrl.highlight; // if rotation was enabled, do it if (this._webgl && this.ctrl.rotate && !this.ctrl.project) this.autorotate(2.5); if (this._webgl && this.ctrl.show_controls) this.showControlGui(true); } this.setAsMainPainter(); if (isFunc(this._resolveFunc)) { this._resolveFunc(this); delete this._resolveFunc; } if (isFunc(this._complete_handler)) this._complete_handler(this); if (this._draw_nodes_again) this.startDrawGeometry(); // relaunch drawing else this._drawing_ready = true; // indicate that drawing is completed return this; }); } /** @summary Returns true if geometry drawing is completed */ isDrawingReady() { return this._drawing_ready || false; } /** @summary Remove already drawn node. Used by geom viewer */ removeDrawnNode(nodeid) { if (!this._draw_nodes) return; const new_nodes = []; for (let n = 0; n < this._draw_nodes.length; ++n) { const entry = this._draw_nodes[n]; if ((entry.nodeid === nodeid) || this._clones.isIdInStack(nodeid, entry.stack)) this._clones.createObject3D(entry.stack, this._toplevel, 'delete_mesh'); else new_nodes.push(entry); } if (new_nodes.length < this._draw_nodes.length) { this._draw_nodes = new_nodes; this.render3D(); } } /** @summary Cleanup geometry painter */ cleanup(first_time) { if (!first_time) { let can3d = 0; if (!this.superimpose) { this.clearTopPainter(); // remove as pointer if (this._on_pad) { const fp = this.getFramePainter(); if (fp?.mode3d) { fp.clear3dCanvas(); fp.mode3d = false; } } else can3d = this.clear3dCanvas(); // remove 3d canvas from main HTML element disposeThreejsObject(this._scene); } this._toolbar?.cleanup(); // remove toolbar disposeThreejsObject(this._full_geom); this._controls?.cleanup(); if (this._context_menu) this._renderer.domElement.removeEventListener('contextmenu', this._context_menu, false); this._gui?.destroy(); this._worker?.terminate(); delete this._animating; const obj = this.getGeometry(); if (obj && this.ctrl.is_main) { if (obj.$geo_painter === this) delete obj.$geo_painter; else if (obj.fVolume?.$geo_painter === this) delete obj.fVolume.$geo_painter; } if (this._master_painter?._slave_painters) { const pos = this._master_painter._slave_painters.indexOf(this); if (pos >= 0) this._master_painter._slave_painters.splice(pos, 1); } for (let k = 0; k < this._slave_painters?.length; ++k) { const slave = this._slave_painters[k]; if (slave?._master_painter === this) slave._master_painter = null; } delete this.geo_manager; delete this._highlight_handlers; super.cleanup(); delete this.ctrl; delete this.options; this.did_cleanup = true; if (can3d < 0) this.selectDom().html(''); } if (this._slave_painters) { for (const k in this._slave_painters) { const slave = this._slave_painters[k]; slave._master_painter = null; if (slave._clones === this._clones) slave._clones = null; } } this._master_painter = null; this._slave_painters = []; if (this._render_resolveFuncs) { this._render_resolveFuncs.forEach(func => func(this)); delete this._render_resolveFuncs; } if (!this.superimpose) cleanupRender3D(this._renderer); this.ensureBloom(false); delete this._effectComposer; delete this._scene; delete this._scene_size; this._scene_width = 0; this._scene_height = 0; this._renderer = null; this._toplevel = null; delete this._full_geom; delete this._fog; delete this._camera; delete this._camera0pos; delete this._lookat; delete this._selected_mesh; if (this._clones && this._clones_owner) this._clones.cleanup(this._draw_nodes, this._build_shapes); delete this._clones; delete this._clones_owner; delete this._draw_nodes; delete this._drawing_ready; delete this._build_shapes; delete this._new_draw_nodes; delete this._new_append_nodes; delete this._last_camera_position; this.first_render_tm = 0; // time needed for first rendering this.last_render_tm = 0; this.changeStage(stageInit, 'cleanup'); delete this.drawing_log; delete this._gui; delete this._controls; delete this._context_menu; delete this._toolbar; delete this._worker; } /** @summary perform resize */ performResize(width, height) { if ((this._scene_width === width) && (this._scene_height === height)) return false; if ((width < 10) || (height < 10)) return false; this._scene_width = width; this._scene_height = height; if (this._camera && this._renderer) { if (this._camera.isPerspectiveCamera) this._camera.aspect = this._scene_width / this._scene_height; else if (this._camera.isOrthographicCamera) this.adjustCameraPosition(true, true); this._camera.updateProjectionMatrix(); this._renderer.setSize(this._scene_width, this._scene_height, !this._fit_main_area); this._effectComposer?.setSize(this._scene_width, this._scene_height); this._bloomComposer?.setSize(this._scene_width, this._scene_height); if (this.isStage(stageInit)) this.render3D(); } return true; } /** @summary Check if HTML element was resized and drawing need to be adjusted */ checkResize(arg) { const cp = this.getCanvPainter(); // firefox is the only browser which correctly supports resize of embedded canvas, // for others we should force canvas redrawing at every step if (cp && !cp.checkCanvasResize(arg)) return false; const sz = this.getSizeFor3d(); return this.performResize(sz.width, sz.height); } /** @summary Toggle enlarge state */ toggleEnlarge() { if (this.enlargeMain('toggle')) this.checkResize(); } /** @summary either change mesh wireframe or return current value * @return undefined when wireframe cannot be accessed * @private */ accessObjectWireFrame(obj, on) { if (!obj?.material) return; if ((on !== undefined) && obj.stack) obj.material.wireframe = on; return obj.material.wireframe; } /** @summary handle wireframe flag change in GUI * @private */ changedWireFrame() { this._scene?.traverse(obj => this.accessObjectWireFrame(obj, this.ctrl.wireframe)); this.render3D(); } /** @summary Update object in geo painter */ updateObject(obj) { if ((obj === 'same') || !obj?._typename) return false; if (obj === this.getObject()) return true; let gm; if (obj._typename === clTGeoManager) { gm = obj; obj = obj.fMasterVolume; } if (obj._typename.indexOf(clTGeoVolume) === 0) obj = { _typename: clTGeoNode, fVolume: obj, fName: obj.fName, $geoh: obj.$geoh, _proxy: true }; if (this.geo_manager && gm) { this.geo_manager = gm; this.assignObject(obj); this._did_update = true; return true; } if (!this.matchObjectType(obj._typename)) return false; this.assignObject(obj); this._did_update = true; return true; } /** @summary Cleanup TGeo drawings */ clearDrawings() { if (this._clones && this._clones_owner) this._clones.cleanup(this._draw_nodes, this._build_shapes); delete this._clones; delete this._clones_owner; delete this._draw_nodes; delete this._drawing_ready; delete this._build_shapes; delete this._extraObjects; delete this._clipCfg; // only remove all childs from top level object disposeThreejsObject(this._toplevel, true); this._full_redrawing = true; } /** @summary Redraw TGeo object inside TPad */ redraw() { if (this.superimpose) { const cfg = getHistPainter3DCfg(this.getMainPainter()); if (cfg) { this._toplevel.scale.set(cfg.scale_x ?? 1, cfg.scale_y ?? 1, cfg.scale_z ?? 1); this._toplevel.position.set(cfg.offset_x ?? 0, cfg.offset_y ?? 0, cfg.offset_z ?? 0); this._toplevel.updateMatrix(); this._toplevel.updateMatrixWorld(); } } if (this._did_update) return this.startRedraw(); const main = this._on_pad ? this.getFramePainter() : null; if (!main) return Promise.resolve(false); const sz = main.getSizeFor3d(main.access3dKind()); main.apply3dSize(sz); return this.performResize(sz.width, sz.height); } /** @summary Redraw TGeo object */ redrawObject(obj, opt) { if (!this.updateObject(obj, opt)) return false; return this.startRedraw(); } /** @summary Start geometry redraw */ startRedraw(tmout) { if (tmout) { if (this._redraw_timer) clearTimeout(this._redraw_timer); this._redraw_timer = setTimeout(() => this.startRedraw(), tmout); return; } delete this._redraw_timer; delete this._did_update; this.clearDrawings(); const draw_obj = this.getGeometry(), name_prefix = this.geo_manager ? draw_obj.fName : ''; return this.prepareObjectDraw(draw_obj, name_prefix); } /** @summary draw TGeo object */ static async draw(dom, obj, opt) { if (!obj) return null; let shape = null, extras = null, extras_path = '', is_eve = false; if (('fShapeBits' in obj) && ('fShapeId' in obj)) { shape = obj; obj = null; } else if ((obj._typename === clTGeoVolumeAssembly) || (obj._typename === clTGeoVolume)) shape = obj.fShape; else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) { shape = obj.fShape; is_eve = true; } else if (obj._typename === clTGeoManager) shape = obj.fMasterVolume.fShape; else if (obj._typename === clTGeoOverlap) { extras = obj.fMarker; extras_path = '/Marker'; obj = buildOverlapVolume(obj); if (!opt) opt = 'wire'; } else if ('fVolume' in obj) { if (obj.fVolume) shape = obj.fVolume.fShape; } else obj = null; if (isStr(opt) && opt.indexOf('comp') === 0 && shape && (shape._typename === clTGeoCompositeShape) && shape.fNode) { let maxlvl = 1; opt = opt.slice(4); if (opt[0] === 'x') { maxlvl = 999; opt = opt.slice(1) + '_vislvl999'; } obj = buildCompositeVolume(shape, maxlvl); } if (!obj && shape) obj = Object.assign(create$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._master_painter = obj.$geo_painter; painter._master_painter._slave_painters.push(painter); } if (is_eve && (!painter.ctrl.vislevel || (painter.ctrl.vislevel < 9))) painter.ctrl.vislevel = 9; if (extras) { painter._splitColors = true; painter.addExtra(extras, extras_path); } return painter.loadMacro(painter.ctrl.script_name).then(arg => painter.prepareObjectDraw(arg.obj, arg.prefix)); } } // class TGeoPainter let add_settings = false; /** @summary Get icon for the browser * @private */ function getBrowserIcon(hitem, hpainter) { let icon = ''; switch (hitem._kind) { case prROOT + clTEveTrack: icon = 'img_evetrack'; break; case prROOT + clTEvePointSet: icon = 'img_evepoints'; break; case prROOT + clTPolyMarker3D: icon = 'img_evepoints'; break; } if (icon) { const drawitem = findItemWithPainter(hitem); if (drawitem?._painter?.extraObjectVisible(hpainter, hitem)) icon += ' geovis_this'; } return icon; } /** @summary handle click on browser icon * @private */ function browserIconClick(hitem, hpainter) { if (hitem._volume) { if (hitem._more && hitem._volume.fNodes?.arr?.length) toggleGeoBit(hitem._volume, geoBITS.kVisDaughters); else toggleGeoBit(hitem._volume, geoBITS.kVisThis); updateBrowserIcons(hitem._volume, hpainter); findItemWithPainter(hitem, 'testGeomChanges'); return false; // no need to update icon - we did it ourself } if (hitem._geoobj && ((hitem._geoobj._typename === clTEveGeoShapeExtract) || (hitem._geoobj._typename === clREveGeoShapeExtract))) { hitem._geoobj.fRnrSelf = !hitem._geoobj.fRnrSelf; updateBrowserIcons(hitem._geoobj, hpainter); findItemWithPainter(hitem, 'testGeomChanges'); return false; // no need to update icon - we did it ourself } // first check that geo painter assigned with the item const drawitem = findItemWithPainter(hitem), newstate = drawitem?._painter?.extraObjectVisible(hpainter, hitem, true); // return true means browser should update icon for the item return newstate !== undefined; } /** @summary 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); } findItemWithPainter(item, 'testGeomChanges'); }, toggleMenuBit = arg => { toggleGeoBit(vol, arg); const newname = item._icon.split(' ')[0] + provideVisStyle(vol); hpainter.forEachItem(m => { // update all items with that volume if (item._volume === m._volume) { m._icon = newname; hpainter.updateTreeNode(m); } }); hpainter.updateTreeNode(item); findItemWithPainter(item, 'testGeomChanges'); }, drawitem = findItemWithPainter(item), fullname = drawitem ? hpainter.itemFullName(item, drawitem) : ''; if ((item._geoobj._typename.indexOf(clTGeoNode) === 0) && drawitem) { menu.add('Focus', () => { if (drawitem && isFunc(drawitem._painter?.focusOnItem)) drawitem._painter.focusOnItem(fullname); }); } if (iseve) { menu.addchk(obj.fRnrSelf, 'Visible', 'self', toggleEveVisibility); const res = scanEveVisible(obj, undefined, true); if (res.hidden + res.visible > 0) menu.addchk((res.hidden === 0), 'Daughters', res.hidden !== 0 ? 'true' : 'false', toggleEveVisibility); } else { const stack = drawitem?._painter?._clones?.findStackByName(fullname), phys_vis = stack ? drawitem._painter._clones.getPhysNodeVisibility(stack) : null, is_visible = testGeoBit(vol, geoBITS.kVisThis); menu.addchk(testGeoBit(vol, geoBITS.kVisNone), 'Invisible', geoBITS.kVisNone, toggleMenuBit); if (stack) { const changePhysVis = arg => { drawitem._painter._clones.setPhysNodeVisibility(stack, (arg === 'off') ? false : arg); findItemWithPainter(item, 'testGeomChanges'); }; menu.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: prROOT + 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: prROOT + obj._typename, _name: name || getObjectName(obj), _title: obj.fTitle, _parent: node, _geoobj: obj, _get(item /* ,itemname */) { // mark object as belong to the hierarchy, require to if (item._geoobj) item._geoobj.$geoh = true; return Promise.resolve(item._geoobj); } }; let volume, shape, subnodes, iseve = false; if (obj._typename === 'TGeoMaterial') sub._icon = 'img_geomaterial'; else if (obj._typename === 'TGeoMedium') sub._icon = 'img_geomedium'; else if (obj._typename === 'TGeoMixture') sub._icon = 'img_geomixture'; else if ((obj._typename.indexOf(clTGeoNode) === 0) && obj.fVolume) { sub._title = 'node:' + obj._typename; if (obj.fTitle) sub._title += ' ' + obj.fTitle; volume = obj.fVolume; } else if (obj._typename.indexOf(clTGeoVolume) === 0) volume = obj; else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) { iseve = true; shape = obj.fShape; subnodes = obj.fElements ? obj.fElements.arr : null; } else if ((obj.fShapeBits !== undefined) && (obj.fShapeId !== undefined)) shape = obj; if (volume) { shape = volume.fShape; subnodes = volume.fNodes ? volume.fNodes.arr : null; } if (volume || shape || subnodes) { if (volume) sub._volume = volume; if (subnodes) { sub._more = true; sub._expand = expandGeoObject; } else if (shape && (shape._typename === clTGeoCompositeShape) && shape.fNode) { sub._more = true; sub._shape = shape; sub._expand = function(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(), opt = (pp?.pad?.fFillColor && (pp?.pad?.fFillStyle > 1000)) ? 'bkgr_' + pp.pad.fFillColor : ''; return TGeoPainter.draw(pp, obj, opt) .then(geop => { geop._dummy = true; return geop; }); } /** @summary Direct draw function for TAxis3D * @private */ function drawAxis3D() { const main = this.getMainPainter(); if (isFunc(main?.setAxesDraw)) return main.setAxesDraw(true); console.error('no geometry painter found to toggle TAxis3D drawing'); } /** @summary Build three.js model for given geometry object * @param {Object} obj - TGeo-related object * @param {Object} [opt] - options * @param {Number} [opt.vislevel] - visibility level like TGeoManager, when not specified - show all * @param {Number} [opt.numnodes=1000] - maximal number of visible nodes * @param {Number} [opt.numfaces=100000] - approx maximal number of created triangles * @param {Number} [opt.instancing=-1] - <0 disable use of InstancedMesh, =0 only for large geometries, >0 enforce usage of InstancedMesh * @param {boolean} [opt.doubleside=false] - use double-side material * @param {boolean} [opt.wireframe=false] - show wireframe for created shapes * @param {boolean} [opt.transparency=0] - make nodes transparent * @param {boolean} [opt.dflt_colors=false] - use default ROOT colors * @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) { if (!obj) return null; if (!opt) opt = {}; if (!opt.numfaces) opt.numfaces = 100000; if (!opt.numnodes) opt.numnodes = 1000; if (!opt.frustum) opt.frustum = null; opt.res_mesh = opt.res_faces = 0; if (opt.instancing === undefined) opt.instancing = -1; opt.info = { num_meshes: 0, num_faces: 0 }; let clones, 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; } 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 */ 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.ntou1() !== 0); 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.ntou1() !== 0); 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 = 'TTree'; obj.$file = buf.fFile; } }, RooRealVar(buf, obj) { const v = buf.last_read_version; buf.classStreamer(obj, 'RooAbsRealLValue'); if (v === 1) { buf.ntod(); buf.ntod(); buf.ntoi4(); } // skip fitMin, fitMax, fitBins 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.ntou1() !== 0); } }, { 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.ntou1() !== 0) { 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': (buf, obj) => { buf.classStreamer(obj, 'TMatrixTBase'); 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': (buf, obj) => { buf.classStreamer(obj, 'TMatrixTBase'); 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') !== 0)) 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) { 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.ntou1() !== 0; }; break; case kOffsetL + kBool: case kOffsetL + kInt: case kOffsetL + kCounter: case kOffsetL + kDouble: 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; member.func = function(buf, obj) { obj[this.name] = buf.readFastArray(this.arrlength, this.type - kOffsetL); }; } else { member.arrlength = element.fMaxIndex[element.fArrayDim - 1]; member.minus1 = true; member.func = function(buf, obj) { obj[this.name] = buf.readNdimArray(this, (buf2, handle) => buf2.readFastArray(handle.arrlength, handle.type - kOffsetL)); }; } break; case kOffsetL + kChar: if (element.fArrayDim < 2) { member.arrlength = element.fArrayLength; member.func = function(buf, obj) { obj[this.name] = buf.readFastString(this.arrlength); }; } else { member.minus1 = true; // one dimension used for char* member.arrlength = element.fMaxIndex[element.fArrayDim - 1]; 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 + 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; member.func = function(buf, obj) { obj[this.name] = (buf.ntou1() === 1) ? buf.readFastArray(obj[this.cntname], this.type - kOffsetP) : []; }; break; case kOffsetP + kChar: member.cntname = element.fCountName; 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 !== 0) { 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 = ((ver.val & kStreamedMemberWise) !== 0); 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); let res = buf.readNdimArray(this, (buf2, member2) => member2.readelem(buf2)); if (!buf.checkByteCount(ver, this.typename)) res = null; obj[this.name] = res; }; 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 * 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. zip_cplens = [ // Copy lengths for literal codes 257..285 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. */ zip_cplext = [ // Extra bits for literal codes 257..285 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 zip_cpdist = [ // Copy offsets for distance codes 0..29 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], zip_cpdext = [ // Extra bits for distance codes 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], zip_border = [ // Order of the bit length code lengths 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 res = { status: 0, // 0: success, 1: incomplete table, 2: bad input root: null, // (zip_HuftList) starting table m: 0 // maximum lookup bits, returns actual }, 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 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; ++j) if (c[j] !== 0) break; k = j; // minimum code length if (mm < j) mm = j; for (i = BMAX; i !== 0; --i) if (c[i] !== 0) break; 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++]) !== 0) 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) !== 0; 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 !== 0 && 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 !== 0) 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 !== 0) 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 !== 0) 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 !== 0)) // 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 !== 0 /* 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) !== 0) 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} 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 === 0) 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()) !== 0) 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 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 || !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 & 2147483647) + 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; } } // 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(); 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] &= 16777215; } } 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 !== 0)) 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.length === 0)) 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)) 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) { // these are special cases, which are handled separately if (clname === clTQObject || clname === clTBasket) return null; let streamer, fullname = clname; if (ver) { fullname += (ver.checksum ? `$chksum${ver.checksum}` : `$ver${ver.val}`); streamer = this.fStreamers[fullname]; if (streamer !== undefined) return streamer; } const custom = 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)) { streamer = [{ typename: clname, func: custom }]; return addClassMethods(clname, streamer); } 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) { 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 if (s_i.fElements) { for (let j = 0; j < s_i.fElements.arr.length; ++j) streamer.push(createMemberStreamer(s_i.fElements.arr[j], 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 (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://') === 0) file = new TNodejsFile(arg.slice(7)); else if (arg.indexOf('http') !== 0) 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(); } // 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 > 0) || (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 || (lst.arr.length === 0)) 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) !== 0) && (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) !== 0) 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); if (!res) res = findBranchComplex(tree, search.slice(pnt + 1), lst.arr[n].fBranches); if (!res) res = { 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 = findBranchComplex(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 === 0)) { 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 if (arriter.length === 0) { // TODO: if selected member is simple data type - no need to make other checks - just break here if ((br.fType === kClonesNode) || (br.fType === kSTLNode)) arriter.push(undefined); else { const objclass = getBranchObjectClass(br, tree, false, true); if (objclass && isRootCollection(null, objclass)) 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 === 0) 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 === 0) && !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 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 = tree.fEntries; else if (parvalue === 'half') args.numentries = Math.round(tree.fEntries / 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 !== undefined) ? 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.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}' 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 && (args.branch.fLeaves.arr.length > 1)) || (args.branch.fBranches && (args.branch.fBranches.arr.length > 0))) && !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 = tree.fEntries; 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 === '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(); 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 = ''; res.fLabels.Add(s); } } 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; } 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 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 === 0) && (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 ''; let clname = elem.fTypeName; if (clname.at(-1) === '*') clname = clname.slice(0, clname.length - 1); return 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 'TLeafC': datakind = kTString; break; default: return null; } const elem = createStreamerElement(name || leaf.fName, datakind); if (leaf.fLen > 1) { 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 == '' 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; } // console.log(`Add branch ${branch.fName}`); 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 && 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 ? (target_object.length + 1) : 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) }; if (!scanBranches(branch.fBranches, newtgt, 0)) return null; return item; // this kind of branch does not have baskets and not need to be read } 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) if (!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)) { const max = handle.process_min + args.numentries; if (max < handle.process_max) handle.process_max = max; } 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)) 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); } } } 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 === 0) 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 > 0 ? { 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 !== 0) || (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 > 0) ? 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.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 = ''; const selector = new TDrawSelector(); 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 treeProcess(tree, selector, args).then(sel => { if (!args.staged) return sel; delete args.dump_entries; const selector2 = new TDrawSelector(), 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 treeProcess(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: prROOT + 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: prROOT + '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 > 0)) { 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: prROOT + 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: prROOT + 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 for (let i = 0; i < obj.fBranches.arr?.length; ++i) createBranchItem(tree_node, obj.fBranches.arr[i], 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 2.5.2 * * Copyright (c) 2010-2021 James Hall , https://github.com/MrRio/jsPDF * 2015-2021 yWorks GmbH, http://www.yworks.com * 2015-2021 Lukas Holländer , https://github.com/HackbrettXXX * 2016-2018 Aras Abbasi * 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 * {@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").
* @param {string} [options.unit=mm] Measurement unit (base unit) to be used when coordinates are specified.
* 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:
  • a0 - a10
  • b0 - b10
  • c0 - c10
  • dl
  • letter
  • government-letter
  • legal
  • junior-legal
  • ledger
  • tabloid
  • credit-card

* 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:
* | a b 0 |
* | c d 0 |
* | e f 1 |
* 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} 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} 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.} 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("<>"); 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("<>"); 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.
* Possible values are:
* 'arraybuffer' -> (ArrayBuffer)
* 'blob' -> (Blob)
* 'bloburi'/'bloburl' -> (string)
* 'datauristring'/'dataurlstring' -> (string)
* 'datauri'/'dataurl' -> (undefined) -> change location to generated datauristring/dataurlstring
* @param {Object|string} options An object providing some additional signalling to PDF generator.
* Possible options are 'filename'.
* 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:
  • a0 - a10
  • b0 - b10
  • c0 - c10
  • dl
  • letter
  • government-letter
  • legal
  • junior-legal
  • ledger
  • tabloid
  • credit-card

* 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; if (l < len - 1) { wordSpacingPerLine.push( hpf( scale( (maxWidth - lineWidths[l]) / (da[l].split(" ").length - 1) ) ) ); } 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.|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} 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} 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 = getPageWidth(currentPage); this.height = getPageHeight(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; setPageWidth(currentPage, this.width); setPageHeight(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]; } } } var getPageWidth = (API.getPageWidth = function(pageNumber) { pageNumber = pageNumber || currentPage; return ( (pagesContext[pageNumber].mediaBox.topRightX - pagesContext[pageNumber].mediaBox.bottomLeftX) / scaleFactor ); }); var setPageWidth = (API.setPageWidth = function(pageNumber, value) { pagesContext[pageNumber].mediaBox.topRightX = value * scaleFactor + pagesContext[pageNumber].mediaBox.bottomLeftX; }); var getPageHeight = (API.getPageHeight = function(pageNumber) { pageNumber = pageNumber || currentPage; return ( (pagesContext[pageNumber].mediaBox.topRightY - pagesContext[pageNumber].mediaBox.bottomLeftY) / scaleFactor ); }); var setPageHeight = (API.setPageHeight = function(pageNumber, value) { pagesContext[pageNumber].mediaBox.topRightY = value * scaleFactor + pagesContext[pageNumber].mediaBox.bottomLeftY; }); /** * 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 = "2.5.2"; /* 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: "<>" }); 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)) { 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") { var decodeParameters = "/Predictor " + image.predictor + " /Colors 1 /BitsPerComponent " + image.bitsPerComponent + " /Columns " + image.width; var sMask = { width: image.width, height: image.height, colorSpace: "DeviceGray", bitsPerComponent: image.bitsPerComponent, decodeParameters: decodeParameters, data: image.sMask }; if ("filter" in image) { 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:[][;base64],' * @returns {Array}an Array containing the following * [0] the complete data URI * [1] * [2] format - the second part of the mime-type i.e 'png' in 'image/png' * [4] */ var extractImageFromDataUrl = (jsPDFAPI.__addimage__.extractImageFromDataUrl = function( dataUrl ) { dataUrl = dataUrl || ""; var dataUrlParts = dataUrl.split("base64,"); var result = null; if (dataUrlParts.length === 2) { var extractedInfo = /^data:(\w*\/\w*);*(charset=(?!charset=)[\w=-]*)*;*$/.exec( dataUrlParts[0] ); if (Array.isArray(extractedInfo)) { result = { mimeType: extractedInfo[1], charset: extractedInfo[2], data: dataUrlParts[1] }; } } return result; }); /** * Check to see if ArrayBuffer is supported * * @name supportsArrayBuffer * @function * @returns {boolean} */ var supportsArrayBuffer = (jsPDFAPI.__addimage__.supportsArrayBuffer = function() { return ( typeof ArrayBuffer !== "undefined" && typeof Uint8Array !== "undefined" ); }); /** * Tests supplied object to determine if ArrayBuffer * * @name isArrayBuffer * @function * @param {Object} object an Object * * @returns {boolean} */ jsPDFAPI.__addimage__.isArrayBuffer = function(object) { return supportsArrayBuffer() && 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 ( supportsArrayBuffer() && typeof Uint32Array !== "undefined" && (object instanceof Int8Array || object instanceof Uint8Array || (typeof Uint8ClampedArray !== "undefined" && 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) { if (supportsArrayBuffer()) { // 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 base64Info; var imageData = ""; var rawData; if (typeof stringData === "string") { base64Info = extractImageFromDataUrl(stringData); rawData = base64Info !== null ? base64Info.data : 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 (supportsArrayBuffer() && !(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 = "<>"; 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 = "<>"; } else if (anno.options.pageNumber) { // first page is 0 var info = this.internal.getPageInfo(anno.options.pageNumber); line = "< pageNumber or url [required] *

If pageNumber is specified, top and zoom may also be specified

* @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 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 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.} 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: *
    *
  • * false: Auto paging is disabled. *
  • *
  • * true or 'slice': Will cut shapes or text chunks across page breaks. Will possibly * slice text in half, making it difficult to read. *
  • *
  • * 'text': Trys not to cut text in half across page breaks. Works best for documents consisting * mostly of a single column of text. *
  • *
* @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
* A gradient object (linear or radial) used to fill the drawing (not supported by context2d)
* 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
* round A rounded end cap is added to each end of the line
* square A square end cap is added to each end of the line
*/ 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.

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.

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.
* 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.
* 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.

The transform() method replaces the current transformation matrix. It multiplies the current transformation matrix with the matrix described by:



a c e

b d f

0 0 1

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.

The setTransform() method resets the current transform to the identity matrix, and then runs transform() with the same arguments.

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: // https://tools.ietf.org/html/rfc1951 // You may also wish to take a look at the guide I made about this program: // https://gist.github.com/101arrowz/253f31eb5abc3d9275ab943003ffecad // Much of the following code is similar to that of UZIP.js: // https://github.com/photopea/UZIP.js // Many optimizations have been made, so the bundle size is ultimately smaller but performance is similar. // Sometimes 0 will appear where -1 would be more appropriate. This is because using a uint // is better for memory in most engines (I *think*). // Mediocre shim // aliases for shorter compressed code (most minifers don't do this) var u8 = Uint8Array, u16 = Uint16Array, u32 = Uint32Array; // 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 // see fleb note 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 u32(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, r]; }; var _a = freb(fleb, 2), fl = _a[0], revfl = _a[1]; // 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), fd = _b[0], revfd = _b[1]; // 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) ++l[cd[i] - 1]; // u16 "map": index -> minimum code for bit length = index var le = new u16(mb); for (i = 0; i < mb; ++i) { le[i] = (le[i - 1] + l[i - 1]) << 1; } var co; if (r) { // u16 "map": index -> number of actual bits, symbol for code co = new u16(1 << mb); // bits to remove for reverser var rvb = 15 - mb; for (i = 0; i < s; ++i) { // ignore 0 lengths if (cd[i]) { // num encoding both symbol and bits read var sv = (i << 4) | cd[i]; // free bits var r_1 = mb - cd[i]; // start value var v = le[cd[i] - 1]++ << r_1; // m is end value for (var m = v | ((1 << r_1) - 1); v <= m; ++v) { // every 16 bit value starting with the code yields the same result co[rev[v] >>> rvb] = sv; } } } } else { co = new u16(s); for (i = 0; i < s; ++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, 0), flrm = /*#__PURE__*/ hMap(flt, 9, 1); // fixed distance map var fdm = /*#__PURE__*/ hMap(fdt, 5, 0), fdrm = /*#__PURE__*/ hMap(fdt, 5, 1); // find max of array var max = function (a) { var m = a[0]; for (var i = 1; i < a.length; ++i) { if (a[i] > m) m = a[i]; } return m; }; // read d, starting at bit p and mask with m var bits = function (d, p, m) { var o = (p / 8) >> 0; return ((d[o] | (d[o + 1] << 8)) >>> (p & 7)) & m; }; // read d, starting at bit p continuing for at least 16 bits var bits16 = function (d, p) { var o = (p / 8) >> 0; return ((d[o] | (d[o + 1] << 8) | (d[o + 2] << 16)) >>> (p & 7)); }; // get end of byte var shft = function (p) { return ((p / 8) >> 0) + (p & 7 && 1); }; // 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 var n = new (v instanceof u16 ? u16 : v instanceof u32 ? u32 : u8)(e - s); n.set(v.subarray(s, e)); return n; }; // expands raw DEFLATE data var inflt = function (dat, buf, st) { // source length var sl = dat.length; // have to estimate size var noBuf = !buf || st; // no state var noSt = !st || st.i; if (!st) st = {}; // Assumes roughly 33% compression ratio average if (!buf) buf = new u8(sl * 3); // ensure buffer can fit at least l elements var cbuf = function (l) { var bl = buf.length; // need to increase size to fit if (l > bl) { // Double or set to necessary, whichever is greater var nbuf = new u8(Math.max(bl * 2, l)); nbuf.set(buf); buf = nbuf; } }; // last chunk bitpos bytes var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n; // total bits var tbts = sl * 8; do { if (!lm) { // BFINAL - this is only 1 when last chunk is next st.f = final = bits(dat, pos, 1); // type: 0 = no compression, 1 = fixed huffman, 2 = dynamic huffman var type = bits(dat, pos + 1, 3); pos += 3; if (!type) { // go to end of byte boundary var s = shft(pos) + 4, l = dat[s - 4] | (dat[s - 3] << 8), t = s + l; if (t > sl) { if (noSt) throw 'unexpected EOF'; break; } // ensure size if (noBuf) cbuf(bt + l); // Copy over uncompressed data buf.set(dat.subarray(s, t), bt); // Get new bitpos, update byte count st.b = bt += l, st.p = pos = t * 8; continue; } else if (type == 1) lm = flrm, dm = fdrm, lbt = 9, dbt = 5; else if (type == 2) { // literal lengths var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4; var tl = hLit + bits(dat, pos + 5, 31) + 1; pos += 14; // length+distance tree var ldt = new u8(tl); // code length tree var clt = new u8(19); for (var i = 0; i < hcLen; ++i) { // use index map to get real code clt[clim[i]] = bits(dat, pos + i * 3, 7); } pos += hcLen * 3; // code lengths bits var clb = max(clt), clbmsk = (1 << clb) - 1; if (!noSt && pos + tl * (clb + 7) > tbts) break; // code lengths map var clm = hMap(clt, clb, 1); for (var i = 0; i < tl;) { var r = clm[bits(dat, pos, clbmsk)]; // bits read pos += r & 15; // symbol var s = r >>> 4; // code length to copy if (s < 16) { ldt[i++] = s; } else { // copy count var c = 0, n = 0; if (s == 16) n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1]; else if (s == 17) n = 3 + bits(dat, pos, 7), pos += 3; else if (s == 18) n = 11 + bits(dat, pos, 127), pos += 7; while (n--) ldt[i++] = c; } } // length tree distance tree var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit); // max length bits lbt = max(lt); // max dist bits dbt = max(dt); lm = hMap(lt, lbt, 1); dm = hMap(dt, dbt, 1); } else throw 'invalid block type'; if (pos > tbts) throw 'unexpected EOF'; } // Make sure the buffer can hold this + the largest possible addition // Maximum chunk size (practically, theoretically infinite) is 2^17; if (noBuf) cbuf(bt + 131072); var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1; var mxa = lbt + dbt + 18; while (noSt || pos + mxa < tbts) { // bits read, code var c = lm[bits16(dat, pos) & lms], sym = c >>> 4; pos += c & 15; if (pos > tbts) throw 'unexpected EOF'; if (!c) throw 'invalid length/literal'; if (sym < 256) buf[bt++] = sym; else if (sym == 256) { lm = null; break; } else { var add = sym - 254; // no extra bits needed if less if (sym > 264) { // index var i = sym - 257, b = fleb[i]; add = bits(dat, pos, (1 << b) - 1) + fl[i]; pos += b; } // dist var d = dm[bits16(dat, pos) & dms], dsym = d >>> 4; if (!d) throw 'invalid distance'; pos += d & 15; var dt = fd[dsym]; if (dsym > 3) { var b = fdeb[dsym]; dt += bits16(dat, pos) & ((1 << b) - 1), pos += b; } if (pos > tbts) throw 'unexpected EOF'; if (noBuf) cbuf(bt + 131072); var end = bt + add; for (; bt < end; bt += 4) { buf[bt] = buf[bt - dt]; buf[bt + 1] = buf[bt + 1 - dt]; buf[bt + 2] = buf[bt + 2 - dt]; buf[bt + 3] = buf[bt + 3 - dt]; } bt = end; } } st.l = lm, st.p = pos, st.b = bt; if (lm) final = 1, st.m = lbt, st.d = dm, st.n = dbt; } while (!final); return bt == buf.length ? buf : slc(buf, 0, bt); }; // 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 [new u8(0), 0]; if (s == 1) { var v = new u8(t[0].s + 1); v[t[0].s] = 1; return [v, 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 [new u8(tr), 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 [cl.subarray(0, cli), 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[0], mlb = _a[1]; var _b = hTree(df, 15), ddt = _b[0], mdb = _b[1]; var _c = lc(dlt), lclt = _c[0], nlc = _c[1]; var _d = lc(ddt), lcdt = _d[0], ndc = _d[1]; 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[0], mlcb = _e[1]; 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 (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, 0), ll = dlt, dm = hMap(ddt, mdb, 0), dl = ddt; var llm = hMap(lct, mlcb, 0); 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) { if (syms[i] > 255) { var len = (syms[i] >>> 18) & 31; wbits16(out, p, lm[len + 257]), p += ll[len + 257]; if (len > 7) wbits(out, p, (syms[i] >>> 23) & 31), p += fleb[len]; var dst = syms[i] & 31; wbits16(out, p, dm[dst]), p += dl[dst]; if (dst > 3) wbits16(out, p, (syms[i] >>> 5) & 8191), p += fdeb[dst]; } else { wbits16(out, p, lm[syms[i]]), p += ll[syms[i]]; } } wbits16(out, p, lm[256]); return p + ll[256]; }; // deflate options (nice << 13) | chain var deo = /*#__PURE__*/ new u32([65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632]); // compresses data into a raw DEFLATE buffer var dflt = function (dat, lvl, plvl, pre, post, lst) { var s = dat.length; var o = new u8(pre + s + 5 * (1 + Math.floor(s / 7000)) + post); // writing to this writes to the output buffer var w = o.subarray(pre, o.length - post); var pos = 0; if (!lvl || s < 8) { for (var i = 0; i <= s; i += 65535) { // end var e = i + 65535; if (e < s) { // write full block pos = wfblk(w, pos, dat.subarray(i, e)); } else { // write final block w[i] = lst; pos = wfblk(w, pos, dat.subarray(i, s)); } } } else { 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 = new u16(32768), head = 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 u32(25000); // length/literal freq distance freq var lf = new u16(288), df = new u16(32); // l/lcnt exbits index l/lind waitdx bitpos var lc_1 = 0, eb = 0, i = 0, li = 0, wi = 0, bs = 0; for (; i < s; ++i) { // hash value var hv = hsh(i); // index mod 32768 var imod = i & 32767; // previous index with this value var 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) { 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 + 32768) & 32767; var pti = prev[ti]; var cd = (ti - pti + 32768) & 32767; if (cd > md) md = cd, pimod = ti; } } } // check the previous match imod = pimod, pimod = prev[imod]; dif += (imod - pimod + 32768) & 32767; } } // d will be nonzero only when a match was found if (d) { // store both dist and len data in one Uint32 // 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]]; } } } pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i - bs, pos); } return slc(o, 0, pre + shft(pos) + post); }; // Alder32 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; for (var i = 0; i != l;) { var e = Math.min(i + 5552, l); for (; i < e; ++i) n += d[i], m += n; n %= 65521, m %= 65521; } a = n, b = m; }, d: function () { return ((a >>> 8) << 16 | (b & 255) << 8 | (b >>> 8)) + ((a & 255) << 23) * 2; } }; }; // deflate with opts var dopt = function (dat, opt, pre, post, st) { 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, true); }; // 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) | (fl ? (32 - 2 * fl) : 1); }; // zlib valid var zlv = function (d) { if ((d[0] & 15) != 8 || (d[0] >>> 4) > 7 || ((d[0] << 8 | d[1]) % 31)) throw 'invalid zlib data'; if (d[1] & 32) throw 'invalid zlib data: preset dictionaries not supported'; }; /** * 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 === void 0) { opts = {}; } var a = adler(); a.p(data); var d = dopt(data, opts, 2, 4); return zlh(d, opts), wbytes(d, d.length - 4, a.d()), d; } /** * Expands Zlib data * @param data The data to decompress * @param out Where to write the data. Saves memory if you know the decompressed size and provide an output buffer of that length. * @returns The decompressed version of the data */ function unzlibSync(data, out) { return inflt((zlv(data), data.subarray(2, -4)), out); } /** * @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 <>"); } }); 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("<> 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('<> 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); /** * @license * * Licensed under the MIT License. * http://opensource.org/licenses/mit-license */ /** * jsPDF jpeg Support PlugIn * * @name jpeg_support * @module */ (function(jsPDFAPI) { /** * 0xc0 (SOF) Huffman - Baseline DCT * 0xc1 (SOF) Huffman - Extended sequential DCT * 0xc2 Progressive DCT (SOF2) * 0xc3 Spatial (sequential) lossless (SOF3) * 0xc4 Differential sequential DCT (SOF5) * 0xc5 Differential progressive DCT (SOF6) * 0xc6 Differential spatial (SOF7) * 0xc7 */ var markers = [0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7]; //takes a string imgData containing the raw bytes of //a jpeg image and returns [width, height] //Algorithm from: http://www.64lines.com/jpeg-width-height var getJpegInfo = function(imgData) { var width, height, numcomponents; var blockLength = imgData.charCodeAt(4) * 256 + imgData.charCodeAt(5); var len = imgData.length; var result = { width: 0, height: 0, numcomponents: 1 }; for (var i = 4; i < len; i += 2) { i += blockLength; if (markers.indexOf(imgData.charCodeAt(i + 1)) !== -1) { height = imgData.charCodeAt(i + 5) * 256 + imgData.charCodeAt(i + 6); width = imgData.charCodeAt(i + 7) * 256 + imgData.charCodeAt(i + 8); numcomponents = imgData.charCodeAt(i + 9); result = { width: width, height: height, numcomponents: numcomponents }; break; } else { blockLength = imgData.charCodeAt(i + 2) * 256 + imgData.charCodeAt(i + 3); } } return result; }; /** * @ignore */ jsPDFAPI.processJPEG = function( data, index, alias, compression, dataAsBinaryString, colorSpace ) { var filter = this.decode.DCT_DECODE, bpc = 8, dims, result = null; if ( typeof data === "string" || this.__addimage__.isArrayBuffer(data) || this.__addimage__.isArrayBufferView(data) ) { // if we already have a stored binary string rep use that data = dataAsBinaryString || data; data = this.__addimage__.isArrayBuffer(data) ? new Uint8Array(data) : data; data = this.__addimage__.isArrayBufferView(data) ? this.__addimage__.arrayBufferToBinaryString(data) : data; dims = getJpegInfo(data); switch (dims.numcomponents) { case 1: colorSpace = this.color_spaces.DEVICE_GRAY; break; case 4: colorSpace = this.color_spaces.DEVICE_CMYK; break; case 3: colorSpace = this.color_spaces.DEVICE_RGB; break; } result = { data: data, width: dims.width, height: dims.height, colorSpace: colorSpace, bitsPerComponent: bpc, filter: filter, index: index, alias: alias }; } return result; }; })(jsPDF.API); // Generated by CoffeeScript 1.4.0 var PNG = (function() { var APNG_BLEND_OP_SOURCE, APNG_DISPOSE_OP_BACKGROUND, APNG_DISPOSE_OP_PREVIOUS, makeImage, scratchCanvas, scratchCtx; APNG_DISPOSE_OP_BACKGROUND = 1; APNG_DISPOSE_OP_PREVIOUS = 2; APNG_BLEND_OP_SOURCE = 0; function PNG(data) { var chunkSize, colors, palLen, delayDen, delayNum, frame, index, key, section, palShort, text, _i, _j, _ref; this.data = data; this.pos = 8; this.palette = []; this.imgData = []; this.transparency = {}; this.animation = null; this.text = {}; frame = null; while (true) { chunkSize = this.readUInt32(); section = function() { var _i, _results; _results = []; for (_i = 0; _i < 4; ++_i) { _results.push(String.fromCharCode(this.data[this.pos++])); } return _results; } .call(this) .join(""); switch (section) { case "IHDR": this.width = this.readUInt32(); this.height = this.readUInt32(); this.bits = this.data[this.pos++]; this.colorType = this.data[this.pos++]; this.compressionMethod = this.data[this.pos++]; this.filterMethod = this.data[this.pos++]; this.interlaceMethod = this.data[this.pos++]; break; case "acTL": this.animation = { numFrames: this.readUInt32(), numPlays: this.readUInt32() || Infinity, frames: [] }; break; case "PLTE": this.palette = this.read(chunkSize); break; case "fcTL": if (frame) { this.animation.frames.push(frame); } this.pos += 4; frame = { width: this.readUInt32(), height: this.readUInt32(), xOffset: this.readUInt32(), yOffset: this.readUInt32() }; delayNum = this.readUInt16(); delayDen = this.readUInt16() || 100; frame.delay = (1000 * delayNum) / delayDen; frame.disposeOp = this.data[this.pos++]; frame.blendOp = this.data[this.pos++]; frame.data = []; break; case "IDAT": case "fdAT": if (section === "fdAT") { this.pos += 4; chunkSize -= 4; } data = (frame != null ? frame.data : void 0) || this.imgData; for ( _i = 0; 0 <= chunkSize ? _i < chunkSize : _i > chunkSize; 0 <= chunkSize ? ++_i : --_i ) { data.push(this.data[this.pos++]); } break; case "tRNS": this.transparency = {}; switch (this.colorType) { case 3: palLen = this.palette.length / 3; this.transparency.indexed = this.read(chunkSize); if (this.transparency.indexed.length > palLen) throw new Error("More transparent colors than palette size"); /* * According to the PNG spec trns should be increased to the same size as palette if shorter */ //palShort = 255 - this.transparency.indexed.length; palShort = palLen - this.transparency.indexed.length; if (palShort > 0) { for ( _j = 0; 0 <= palShort ? _j < palShort : _j > palShort; 0 <= palShort ? ++_j : --_j ) { this.transparency.indexed.push(255); } } break; case 0: this.transparency.grayscale = this.read(chunkSize)[0]; break; case 2: this.transparency.rgb = this.read(chunkSize); } break; case "tEXt": text = this.read(chunkSize); index = text.indexOf(0); key = String.fromCharCode.apply(String, text.slice(0, index)); this.text[key] = String.fromCharCode.apply( String, text.slice(index + 1) ); break; case "IEND": if (frame) { this.animation.frames.push(frame); } this.colors = function() { switch (this.colorType) { case 0: case 3: case 4: return 1; case 2: case 6: return 3; } }.call(this); this.hasAlphaChannel = (_ref = this.colorType) === 4 || _ref === 6; colors = this.colors + (this.hasAlphaChannel ? 1 : 0); this.pixelBitlength = this.bits * colors; this.colorSpace = function() { switch (this.colors) { case 1: return "DeviceGray"; case 3: return "DeviceRGB"; } }.call(this); this.imgData = new Uint8Array(this.imgData); return; default: this.pos += chunkSize; } this.pos += 4; if (this.pos > this.data.length) { throw new Error("Incomplete or corrupt PNG file"); } } } PNG.prototype.read = function(bytes) { var _i, _results; _results = []; for ( _i = 0; 0 <= bytes ? _i < bytes : _i > bytes; 0 <= bytes ? ++_i : --_i ) { _results.push(this.data[this.pos++]); } return _results; }; PNG.prototype.readUInt32 = function() { var b1, b2, b3, b4; b1 = this.data[this.pos++] << 24; b2 = this.data[this.pos++] << 16; b3 = this.data[this.pos++] << 8; b4 = this.data[this.pos++]; return b1 | b2 | b3 | b4; }; PNG.prototype.readUInt16 = function() { var b1, b2; b1 = this.data[this.pos++] << 8; b2 = this.data[this.pos++]; return b1 | b2; }; PNG.prototype.decodePixels = function(data) { var pixelBytes = this.pixelBitlength / 8; var fullPixels = new Uint8Array(this.width * this.height * pixelBytes); var pos = 0; var _this = this; if (data == null) { data = this.imgData; } if (data.length === 0) { return new Uint8Array(0); } data = unzlibSync(data); function pass(x0, y0, dx, dy) { var abyte, c, col, i, left, length, p, pa, paeth, pb, pc, pixels, row, scanlineLength, upper, upperLeft, _i, _j, _k, _l, _m; var w = Math.ceil((_this.width - x0) / dx), h = Math.ceil((_this.height - y0) / dy); var isFull = _this.width == w && _this.height == h; scanlineLength = pixelBytes * w; pixels = isFull ? fullPixels : new Uint8Array(scanlineLength * h); length = data.length; row = 0; c = 0; while (row < h && pos < length) { switch (data[pos++]) { case 0: for (i = _i = 0; _i < scanlineLength; i = _i += 1) { pixels[c++] = data[pos++]; } break; case 1: for (i = _j = 0; _j < scanlineLength; i = _j += 1) { abyte = data[pos++]; left = i < pixelBytes ? 0 : pixels[c - pixelBytes]; pixels[c++] = (abyte + left) % 256; } break; case 2: for (i = _k = 0; _k < scanlineLength; i = _k += 1) { abyte = data[pos++]; col = (i - (i % pixelBytes)) / pixelBytes; upper = row && pixels[ (row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes) ]; pixels[c++] = (upper + abyte) % 256; } break; case 3: for (i = _l = 0; _l < scanlineLength; i = _l += 1) { abyte = data[pos++]; col = (i - (i % pixelBytes)) / pixelBytes; left = i < pixelBytes ? 0 : pixels[c - pixelBytes]; upper = row && pixels[ (row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes) ]; pixels[c++] = (abyte + Math.floor((left + upper) / 2)) % 256; } break; case 4: for (i = _m = 0; _m < scanlineLength; i = _m += 1) { abyte = data[pos++]; col = (i - (i % pixelBytes)) / pixelBytes; left = i < pixelBytes ? 0 : pixels[c - pixelBytes]; if (row === 0) { upper = upperLeft = 0; } else { upper = pixels[ (row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes) ]; upperLeft = col && pixels[ (row - 1) * scanlineLength + (col - 1) * pixelBytes + (i % pixelBytes) ]; } p = left + upper - upperLeft; pa = Math.abs(p - left); pb = Math.abs(p - upper); pc = Math.abs(p - upperLeft); if (pa <= pb && pa <= pc) { paeth = left; } else if (pb <= pc) { paeth = upper; } else { paeth = upperLeft; } pixels[c++] = (abyte + paeth) % 256; } break; default: throw new Error("Invalid filter algorithm: " + data[pos - 1]); } if (!isFull) { var fullPos = ((y0 + row * dy) * _this.width + x0) * pixelBytes; var partPos = row * scanlineLength; for (i = 0; i < w; i += 1) { for (var j = 0; j < pixelBytes; j += 1) fullPixels[fullPos++] = pixels[partPos++]; fullPos += (dx - 1) * pixelBytes; } } row++; } } if (_this.interlaceMethod == 1) { /* 1 6 4 6 2 6 4 6 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 3 6 4 6 3 6 4 6 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 */ pass(0, 0, 8, 8); // 1 /* NOTE these seem to follow the pattern: * pass(x, 0, 2*x, 2*x); * pass(0, x, x, 2*x); * with x being 4, 2, 1. */ pass(4, 0, 8, 8); // 2 pass(0, 4, 4, 8); // 3 pass(2, 0, 4, 4); // 4 pass(0, 2, 2, 4); // 5 pass(1, 0, 2, 2); // 6 pass(0, 1, 1, 2); // 7 } else { pass(0, 0, 1, 1); } return fullPixels; }; PNG.prototype.decodePalette = function() { var c, i, length, palette, pos, ret, transparency, _i, _ref, _ref1; palette = this.palette; transparency = this.transparency.indexed || []; ret = new Uint8Array((transparency.length || 0) + palette.length); pos = 0; length = palette.length; c = 0; for (i = _i = 0, _ref = length; _i < _ref; i = _i += 3) { ret[pos++] = palette[i]; ret[pos++] = palette[i + 1]; ret[pos++] = palette[i + 2]; ret[pos++] = (_ref1 = transparency[c++]) != null ? _ref1 : 255; } return ret; }; PNG.prototype.copyToImageData = function(imageData, pixels) { var alpha, colors, data, i, input, j, k, length, palette, v, _ref; colors = this.colors; palette = null; alpha = this.hasAlphaChannel; if (this.palette.length) { palette = (_ref = this._decodedPalette) != null ? _ref : (this._decodedPalette = this.decodePalette()); colors = 4; alpha = true; } data = imageData.data || imageData; length = data.length; input = palette || pixels; i = j = 0; if (colors === 1) { while (i < length) { k = palette ? pixels[i / 4] * 4 : j; v = input[k++]; data[i++] = v; data[i++] = v; data[i++] = v; data[i++] = alpha ? input[k++] : 255; j = k; } } else { while (i < length) { k = palette ? pixels[i / 4] * 4 : j; data[i++] = input[k++]; data[i++] = input[k++]; data[i++] = input[k++]; data[i++] = alpha ? input[k++] : 255; j = k; } } }; PNG.prototype.decode = function() { var ret; ret = new Uint8Array(this.width * this.height * 4); this.copyToImageData(ret, this.decodePixels()); return ret; }; var hasBrowserCanvas = function() { if (Object.prototype.toString.call(globalObject) === "[object Window]") { try { scratchCanvas = globalObject.document.createElement("canvas"); scratchCtx = scratchCanvas.getContext("2d"); } catch (e) { return false; } return true; } return false; }; hasBrowserCanvas(); makeImage = function(imageData) { if (hasBrowserCanvas() === true) { var img; scratchCtx.width = imageData.width; scratchCtx.height = imageData.height; scratchCtx.clearRect(0, 0, imageData.width, imageData.height); scratchCtx.putImageData(imageData, 0, 0); img = new Image(); img.src = scratchCanvas.toDataURL(); return img; } throw new Error("This method requires a Browser with Canvas-capability."); }; PNG.prototype.decodeFrames = function(ctx) { var frame, i, imageData, pixels, _i, _len, _ref, _results; if (!this.animation) { return; } _ref = this.animation.frames; _results = []; for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { frame = _ref[i]; imageData = ctx.createImageData(frame.width, frame.height); pixels = this.decodePixels(new Uint8Array(frame.data)); this.copyToImageData(imageData, pixels); frame.imageData = imageData; _results.push((frame.image = makeImage(imageData))); } return _results; }; PNG.prototype.renderFrame = function(ctx, number) { var frame, frames, prev; frames = this.animation.frames; frame = frames[number]; prev = frames[number - 1]; if (number === 0) { ctx.clearRect(0, 0, this.width, this.height); } if ( (prev != null ? prev.disposeOp : void 0) === APNG_DISPOSE_OP_BACKGROUND ) { ctx.clearRect(prev.xOffset, prev.yOffset, prev.width, prev.height); } else if ( (prev != null ? prev.disposeOp : void 0) === APNG_DISPOSE_OP_PREVIOUS ) { ctx.putImageData(prev.imageData, prev.xOffset, prev.yOffset); } if (frame.blendOp === APNG_BLEND_OP_SOURCE) { ctx.clearRect(frame.xOffset, frame.yOffset, frame.width, frame.height); } return ctx.drawImage(frame.image, frame.xOffset, frame.yOffset); }; PNG.prototype.animate = function(ctx) { var doFrame, frameNumber, frames, numFrames, numPlays, _ref, _this = this; frameNumber = 0; (_ref = this.animation), (numFrames = _ref.numFrames), (frames = _ref.frames), (numPlays = _ref.numPlays); return (doFrame = function() { var f, frame; f = frameNumber++ % numFrames; frame = frames[f]; _this.renderFrame(ctx, f); if (numFrames > 1 && frameNumber / numFrames < numPlays) { return (_this.animation._timeout = setTimeout(doFrame, frame.delay)); } })(); }; PNG.prototype.stopAnimation = function() { var _ref; return clearTimeout( (_ref = this.animation) != null ? _ref._timeout : void 0 ); }; PNG.prototype.render = function(canvas) { var ctx, data; if (canvas._png) { canvas._png.stopAnimation(); } canvas._png = this; canvas.width = this.width; canvas.height = this.height; ctx = canvas.getContext("2d"); if (this.animation) { this.decodeFrames(ctx); return this.animate(ctx); } else { data = ctx.createImageData(this.width, this.height); this.copyToImageData(data, this.decodePixels()); return ctx.putImageData(data, 0, 0); } }; return PNG; })(); /** * @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. * ==================================================================== */ /** * jsPDF PNG PlugIn * @name png_support * @module */ (function(jsPDFAPI) { /* * @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. */ /* * 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 */ var canCompress = function(value) { return value !== jsPDFAPI.image_compression.NONE && hasCompressionJS(); }; var hasCompressionJS = function() { return typeof zlibSync === "function"; }; var compressBytes = function(bytes, lineLength, colorsPerPixel, compression) { var level = 4; var filter_method = filterUp; switch (compression) { case jsPDFAPI.image_compression.FAST: level = 1; filter_method = filterSub; break; case jsPDFAPI.image_compression.MEDIUM: level = 6; filter_method = filterAverage; break; case jsPDFAPI.image_compression.SLOW: level = 9; filter_method = filterPaeth; break; } bytes = applyPngFilterMethod( bytes, lineLength, colorsPerPixel, filter_method ); var dat = zlibSync(bytes, { level: level }); return jsPDFAPI.__addimage__.arrayBufferToBinaryString(dat); }; var applyPngFilterMethod = function( bytes, lineLength, colorsPerPixel, filter_method ) { var lines = bytes.length / lineLength, result = new Uint8Array(bytes.length + lines), filter_methods = getFilterMethods(), line, prevLine, offset; for (var i = 0; i < lines; i += 1) { offset = i * lineLength; line = bytes.subarray(offset, offset + lineLength); if (filter_method) { result.set(filter_method(line, colorsPerPixel, prevLine), offset + i); } else { var len = filter_methods.length, results = []; for (var j; j < len; j += 1) { results[j] = filter_methods[j](line, colorsPerPixel, prevLine); } var ind = getIndexOfSmallestSum(results.concat()); result.set(results[ind], offset + i); } prevLine = line; } return result; }; var filterNone = function(line) { /*var result = new Uint8Array(line.length + 1); result[0] = 0; result.set(line, 1);*/ var result = Array.apply([], line); result.unshift(0); return result; }; var filterSub = function(line, colorsPerPixel) { var result = [], len = line.length, left; result[0] = 1; for (var i = 0; i < len; i += 1) { left = line[i - colorsPerPixel] || 0; result[i + 1] = (line[i] - left + 0x0100) & 0xff; } return result; }; var filterUp = function(line, colorsPerPixel, prevLine) { var result = [], len = line.length, up; result[0] = 2; for (var i = 0; i < len; i += 1) { up = (prevLine && prevLine[i]) || 0; result[i + 1] = (line[i] - up + 0x0100) & 0xff; } return result; }; var filterAverage = function(line, colorsPerPixel, prevLine) { var result = [], len = line.length, left, up; result[0] = 3; for (var i = 0; i < len; i += 1) { left = line[i - colorsPerPixel] || 0; up = (prevLine && prevLine[i]) || 0; result[i + 1] = (line[i] + 0x0100 - ((left + up) >>> 1)) & 0xff; } return result; }; var filterPaeth = function(line, colorsPerPixel, prevLine) { var result = [], len = line.length, left, up, upLeft, paeth; result[0] = 4; for (var i = 0; i < len; i += 1) { left = line[i - colorsPerPixel] || 0; up = (prevLine && prevLine[i]) || 0; upLeft = (prevLine && prevLine[i - colorsPerPixel]) || 0; paeth = paethPredictor(left, up, upLeft); result[i + 1] = (line[i] - paeth + 0x0100) & 0xff; } return result; }; var paethPredictor = function(left, up, upLeft) { if (left === up && up === upLeft) { return left; } var 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; }; var getFilterMethods = function() { return [filterNone, filterSub, filterUp, filterAverage, filterPaeth]; }; var getIndexOfSmallestSum = function(arrays) { var 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)); }; var getPredictorFromCompression = function(compression) { var predictor; switch (compression) { case jsPDFAPI.image_compression.FAST: predictor = 11; break; case jsPDFAPI.image_compression.MEDIUM: predictor = 13; break; case jsPDFAPI.image_compression.SLOW: predictor = 14; break; default: predictor = 12; break; } return predictor; }; /** * @name processPNG * @function * @ignore */ jsPDFAPI.processPNG = function(imageData, index, alias, compression) { var colorSpace, filter = this.decode.FLATE_DECODE, bitsPerComponent, image, decodeParameters = "", trns, colors, pal, smask, pixels, len, alphaData, imgData, hasColors, pixel, i, n; if (this.__addimage__.isArrayBuffer(imageData)) imageData = new Uint8Array(imageData); if (this.__addimage__.isArrayBufferView(imageData)) { image = new PNG(imageData); imageData = image.imgData; bitsPerComponent = image.bits; colorSpace = image.colorSpace; colors = image.colors; /* * colorType 6 - Each pixel is an R,G,B triple, followed by an alpha sample. * * colorType 4 - Each pixel is a grayscale sample, followed by an alpha sample. * * Extract alpha to create two separate images, using the alpha as a sMask */ if ([4, 6].indexOf(image.colorType) !== -1) { /* * processes 8 bit RGBA and grayscale + alpha images */ if (image.bits === 8) { pixels = image.pixelBitlength == 32 ? new Uint32Array(image.decodePixels().buffer) : image.pixelBitlength == 16 ? new Uint16Array(image.decodePixels().buffer) : new Uint8Array(image.decodePixels().buffer); len = pixels.length; imgData = new Uint8Array(len * image.colors); alphaData = new Uint8Array(len); var pDiff = image.pixelBitlength - image.bits; i = 0; n = 0; var pbl; for (; i < len; i++) { pixel = pixels[i]; pbl = 0; while (pbl < pDiff) { imgData[n++] = (pixel >>> pbl) & 0xff; pbl = pbl + image.bits; } alphaData[i] = (pixel >>> pbl) & 0xff; } } /* * processes 16 bit RGBA and grayscale + alpha images */ if (image.bits === 16) { pixels = new Uint32Array(image.decodePixels().buffer); len = pixels.length; imgData = new Uint8Array( len * (32 / image.pixelBitlength) * image.colors ); alphaData = new Uint8Array(len * (32 / image.pixelBitlength)); hasColors = image.colors > 1; i = 0; n = 0; var a = 0; while (i < len) { pixel = pixels[i++]; imgData[n++] = (pixel >>> 0) & 0xff; if (hasColors) { imgData[n++] = (pixel >>> 16) & 0xff; pixel = pixels[i++]; imgData[n++] = (pixel >>> 0) & 0xff; } alphaData[a++] = (pixel >>> 16) & 0xff; } bitsPerComponent = 8; } if (canCompress(compression)) { imageData = compressBytes( imgData, image.width * image.colors, image.colors, compression ); smask = compressBytes(alphaData, image.width, 1, compression); } else { imageData = imgData; smask = alphaData; filter = undefined; } } /* * Indexed png. Each pixel is a palette index. */ if (image.colorType === 3) { colorSpace = this.color_spaces.INDEXED; pal = image.palette; if (image.transparency.indexed) { var trans = image.transparency.indexed; var total = 0; i = 0; len = trans.length; for (; i < len; ++i) { total += trans[i]; } total = total / 255; /* * a single color is specified as 100% transparent (0), * so we set trns to use a /Mask with that index */ if (total === len - 1 && trans.indexOf(0) !== -1) { trns = [trans.indexOf(0)]; /* * there's more than one colour within the palette that specifies * a transparency value less than 255, so we unroll the pixels to create an image sMask */ } else if (total !== len) { pixels = image.decodePixels(); alphaData = new Uint8Array(pixels.length); i = 0; len = pixels.length; for (; i < len; i++) { alphaData[i] = trans[pixels[i]]; } smask = compressBytes(alphaData, image.width, 1); } } } var predictor = getPredictorFromCompression(compression); if (filter === this.decode.FLATE_DECODE) { decodeParameters = "/Predictor " + predictor + " "; } decodeParameters += "/Colors " + colors + " /BitsPerComponent " + bitsPerComponent + " /Columns " + image.width; 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: alias, data: imageData, index: index, filter: filter, decodeParameters: decodeParameters, transparency: trns, palette: pal, sMask: smask, predictor: predictor, width: image.width, height: image.height, bitsPerComponent: bitsPerComponent, colorSpace: colorSpace }; } }; })(jsPDF.API); /** * @license * (c) Dean McNamee , 2013. * * https://github.com/deanm/omggif * * 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. * * omggif is a JavaScript implementation of a GIF 89a encoder and decoder, * including animation and compression. It does not rely on any specific * underlying system, so should run in the browser, Node, or Plask. */ function GifReader(buf) { var p = 0; // - Header (GIF87a or GIF89a). if ( buf[p++] !== 0x47 || buf[p++] !== 0x49 || buf[p++] !== 0x46 || buf[p++] !== 0x38 || ((buf[p++] + 1) & 0xfd) !== 0x38 || buf[p++] !== 0x61 ) { throw new Error("Invalid GIF 87a/89a header."); } // - Logical Screen Descriptor. var width = buf[p++] | (buf[p++] << 8); var height = buf[p++] | (buf[p++] << 8); var pf0 = buf[p++]; // . var global_palette_flag = pf0 >> 7; var num_global_colors_pow2 = pf0 & 0x7; var num_global_colors = 1 << (num_global_colors_pow2 + 1); buf[p++]; buf[p++]; // Pixel aspect ratio (unused?). var global_palette_offset = null; var global_palette_size = null; if (global_palette_flag) { global_palette_offset = p; global_palette_size = num_global_colors; p += num_global_colors * 3; // Seek past palette. } var no_eof = true; var frames = []; var delay = 0; var transparent_index = null; var disposal = 0; // 0 - No disposal specified. var loop_count = null; this.width = width; this.height = height; while (no_eof && p < buf.length) { switch (buf[p++]) { case 0x21: // Graphics Control Extension Block switch (buf[p++]) { case 0xff: // Application specific block // Try if it's a Netscape block (with animation loop counter). if ( buf[p] !== 0x0b || // 21 FF already read, check block size. // NETSCAPE2.0 (buf[p + 1] == 0x4e && buf[p + 2] == 0x45 && buf[p + 3] == 0x54 && buf[p + 4] == 0x53 && buf[p + 5] == 0x43 && buf[p + 6] == 0x41 && buf[p + 7] == 0x50 && buf[p + 8] == 0x45 && buf[p + 9] == 0x32 && buf[p + 10] == 0x2e && buf[p + 11] == 0x30 && // Sub-block buf[p + 12] == 0x03 && buf[p + 13] == 0x01 && buf[p + 16] == 0) ) { p += 14; loop_count = buf[p++] | (buf[p++] << 8); p++; // Skip terminator. } else { // We don't know what it is, just try to get past it. p += 12; while (true) { // Seek through subblocks. var block_size = buf[p++]; // Bad block size (ex: undefined from an out of bounds read). if (!(block_size >= 0)) throw Error("Invalid block size"); if (block_size === 0) break; // 0 size is terminator p += block_size; } } break; case 0xf9: // Graphics Control Extension if (buf[p++] !== 0x4 || buf[p + 4] !== 0) throw new Error("Invalid graphics extension block."); var pf1 = buf[p++]; delay = buf[p++] | (buf[p++] << 8); transparent_index = buf[p++]; if ((pf1 & 1) === 0) transparent_index = null; disposal = (pf1 >> 2) & 0x7; p++; // Skip terminator. break; case 0xfe: // Comment Extension. while (true) { // Seek through subblocks. var block_size = buf[p++]; // Bad block size (ex: undefined from an out of bounds read). if (!(block_size >= 0)) throw Error("Invalid block size"); if (block_size === 0) break; // 0 size is terminator // console.log(buf.slice(p, p+block_size).toString('ascii')); p += block_size; } break; default: throw new Error( "Unknown graphic control label: 0x" + buf[p - 1].toString(16) ); } break; case 0x2c: // Image Descriptor. var x = buf[p++] | (buf[p++] << 8); var y = buf[p++] | (buf[p++] << 8); var w = buf[p++] | (buf[p++] << 8); var h = buf[p++] | (buf[p++] << 8); var pf2 = buf[p++]; var local_palette_flag = pf2 >> 7; var interlace_flag = (pf2 >> 6) & 1; var num_local_colors_pow2 = pf2 & 0x7; var num_local_colors = 1 << (num_local_colors_pow2 + 1); var palette_offset = global_palette_offset; var palette_size = global_palette_size; var has_local_palette = false; if (local_palette_flag) { var has_local_palette = true; palette_offset = p; // Override with local palette. palette_size = num_local_colors; p += num_local_colors * 3; // Seek past palette. } var data_offset = p; p++; // codesize while (true) { var block_size = buf[p++]; // Bad block size (ex: undefined from an out of bounds read). if (!(block_size >= 0)) throw Error("Invalid block size"); if (block_size === 0) break; // 0 size is terminator p += block_size; } frames.push({ x: x, y: y, width: w, height: h, has_local_palette: has_local_palette, palette_offset: palette_offset, palette_size: palette_size, data_offset: data_offset, data_length: p - data_offset, transparent_index: transparent_index, interlaced: !!interlace_flag, delay: delay, disposal: disposal }); break; case 0x3b: // Trailer Marker (end of file). no_eof = false; break; default: throw new Error("Unknown gif block: 0x" + buf[p - 1].toString(16)); } } this.numFrames = function() { return frames.length; }; this.loopCount = function() { return loop_count; }; this.frameInfo = function(frame_num) { if (frame_num < 0 || frame_num >= frames.length) throw new Error("Frame index out of range."); return frames[frame_num]; }; this.decodeAndBlitFrameBGRA = function(frame_num, pixels) { var frame = this.frameInfo(frame_num); var num_pixels = frame.width * frame.height; var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices. GifReaderLZWOutputIndexStream( buf, frame.data_offset, index_stream, num_pixels ); var palette_offset = frame.palette_offset; // NOTE(deanm): It seems to be much faster to compare index to 256 than // to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in // the profile, not sure if it's related to using a Uint8Array. var trans = frame.transparent_index; if (trans === null) trans = 256; // We are possibly just blitting to a portion of the entire frame. // That is a subrect within the framerect, so the additional pixels // must be skipped over after we finished a scanline. var framewidth = frame.width; var framestride = width - framewidth; var xleft = framewidth; // Number of subrect pixels left in scanline. // Output indices of the top left and bottom right corners of the subrect. var opbeg = (frame.y * width + frame.x) * 4; var opend = ((frame.y + frame.height) * width + frame.x) * 4; var op = opbeg; var scanstride = framestride * 4; // Use scanstride to skip past the rows when interlacing. This is skipping // 7 rows for the first two passes, then 3 then 1. if (frame.interlaced === true) { scanstride += width * 4 * 7; // Pass 1. } var interlaceskip = 8; // Tracking the row interval in the current pass. for (var i = 0, il = index_stream.length; i < il; ++i) { var index = index_stream[i]; if (xleft === 0) { // Beginning of new scan line op += scanstride; xleft = framewidth; if (op >= opend) { // Catch the wrap to switch passes when interlacing. scanstride = framestride * 4 + width * 4 * (interlaceskip - 1); // interlaceskip / 2 * 4 is interlaceskip << 1. op = opbeg + (framewidth + framestride) * (interlaceskip << 1); interlaceskip >>= 1; } } if (index === trans) { op += 4; } else { var r = buf[palette_offset + index * 3]; var g = buf[palette_offset + index * 3 + 1]; var b = buf[palette_offset + index * 3 + 2]; pixels[op++] = b; pixels[op++] = g; pixels[op++] = r; pixels[op++] = 255; } --xleft; } }; // I will go to copy and paste hell one day... this.decodeAndBlitFrameRGBA = function(frame_num, pixels) { var frame = this.frameInfo(frame_num); var num_pixels = frame.width * frame.height; var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices. GifReaderLZWOutputIndexStream( buf, frame.data_offset, index_stream, num_pixels ); var palette_offset = frame.palette_offset; // NOTE(deanm): It seems to be much faster to compare index to 256 than // to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in // the profile, not sure if it's related to using a Uint8Array. var trans = frame.transparent_index; if (trans === null) trans = 256; // We are possibly just blitting to a portion of the entire frame. // That is a subrect within the framerect, so the additional pixels // must be skipped over after we finished a scanline. var framewidth = frame.width; var framestride = width - framewidth; var xleft = framewidth; // Number of subrect pixels left in scanline. // Output indices of the top left and bottom right corners of the subrect. var opbeg = (frame.y * width + frame.x) * 4; var opend = ((frame.y + frame.height) * width + frame.x) * 4; var op = opbeg; var scanstride = framestride * 4; // Use scanstride to skip past the rows when interlacing. This is skipping // 7 rows for the first two passes, then 3 then 1. if (frame.interlaced === true) { scanstride += width * 4 * 7; // Pass 1. } var interlaceskip = 8; // Tracking the row interval in the current pass. for (var i = 0, il = index_stream.length; i < il; ++i) { var index = index_stream[i]; if (xleft === 0) { // Beginning of new scan line op += scanstride; xleft = framewidth; if (op >= opend) { // Catch the wrap to switch passes when interlacing. scanstride = framestride * 4 + width * 4 * (interlaceskip - 1); // interlaceskip / 2 * 4 is interlaceskip << 1. op = opbeg + (framewidth + framestride) * (interlaceskip << 1); interlaceskip >>= 1; } } if (index === trans) { op += 4; } else { var r = buf[palette_offset + index * 3]; var g = buf[palette_offset + index * 3 + 1]; var b = buf[palette_offset + index * 3 + 2]; pixels[op++] = r; pixels[op++] = g; pixels[op++] = b; pixels[op++] = 255; } --xleft; } }; } function GifReaderLZWOutputIndexStream(code_stream, p, output, output_length) { var min_code_size = code_stream[p++]; var clear_code = 1 << min_code_size; var eoi_code = clear_code + 1; var next_code = eoi_code + 1; var cur_code_size = min_code_size + 1; // Number of bits per code. // NOTE: This shares the same name as the encoder, but has a different // meaning here. Here this masks each code coming from the code stream. var code_mask = (1 << cur_code_size) - 1; var cur_shift = 0; var cur = 0; var op = 0; // Output pointer. var subblock_size = code_stream[p++]; // TODO(deanm): Would using a TypedArray be any faster? At least it would // solve the fast mode / backing store uncertainty. // var code_table = Array(4096); var code_table = new Int32Array(4096); // Can be signed, we only use 20 bits. var prev_code = null; // Track code-1. while (true) { // Read up to two bytes, making sure we always 12-bits for max sized code. while (cur_shift < 16) { if (subblock_size === 0) break; // No more data to be read. cur |= code_stream[p++] << cur_shift; cur_shift += 8; if (subblock_size === 1) { // Never let it get to 0 to hold logic above. subblock_size = code_stream[p++]; // Next subblock. } else { --subblock_size; } } // TODO(deanm): We should never really get here, we should have received // and EOI. if (cur_shift < cur_code_size) break; var code = cur & code_mask; cur >>= cur_code_size; cur_shift -= cur_code_size; // TODO(deanm): Maybe should check that the first code was a clear code, // at least this is what you're supposed to do. But actually our encoder // now doesn't emit a clear code first anyway. if (code === clear_code) { // We don't actually have to clear the table. This could be a good idea // for greater error checking, but we don't really do any anyway. We // will just track it with next_code and overwrite old entries. next_code = eoi_code + 1; cur_code_size = min_code_size + 1; code_mask = (1 << cur_code_size) - 1; // Don't update prev_code ? prev_code = null; continue; } else if (code === eoi_code) { break; } // We have a similar situation as the decoder, where we want to store // variable length entries (code table entries), but we want to do in a // faster manner than an array of arrays. The code below stores sort of a // linked list within the code table, and then "chases" through it to // construct the dictionary entries. When a new entry is created, just the // last byte is stored, and the rest (prefix) of the entry is only // referenced by its table entry. Then the code chases through the // prefixes until it reaches a single byte code. We have to chase twice, // first to compute the length, and then to actually copy the data to the // output (backwards, since we know the length). The alternative would be // storing something in an intermediate stack, but that doesn't make any // more sense. I implemented an approach where it also stored the length // in the code table, although it's a bit tricky because you run out of // bits (12 + 12 + 8), but I didn't measure much improvements (the table // entries are generally not the long). Even when I created benchmarks for // very long table entries the complexity did not seem worth it. // The code table stores the prefix entry in 12 bits and then the suffix // byte in 8 bits, so each entry is 20 bits. var chase_code = code < next_code ? code : prev_code; // Chase what we will output, either {CODE} or {CODE-1}. var chase_length = 0; var chase = chase_code; while (chase > clear_code) { chase = code_table[chase] >> 8; ++chase_length; } var k = chase; var op_end = op + chase_length + (chase_code !== code ? 1 : 0); if (op_end > output_length) { console$1.log("Warning, gif stream longer than expected."); return; } // Already have the first byte from the chase, might as well write it fast. output[op++] = k; op += chase_length; var b = op; // Track pointer, writing backwards. if (chase_code !== code) // The case of emitting {CODE-1} + k. output[op++] = k; chase = chase_code; while (chase_length--) { chase = code_table[chase]; output[--b] = chase & 0xff; // Write backwards. chase >>= 8; // Pull down to the prefix code. } if (prev_code !== null && next_code < 4096) { code_table[next_code++] = (prev_code << 8) | k; // TODO(deanm): Figure out this clearing vs code growth logic better. I // have an feeling that it should just happen somewhere else, for now it // is awkward between when we grow past the max and then hit a clear code. // For now just check if we hit the max 12-bits (then a clear code should // follow, also of course encoded in 12-bits). if (next_code >= code_mask + 1 && cur_code_size < 12) { ++cur_code_size; code_mask = (code_mask << 1) | 1; } } prev_code = code; } if (op !== output_length) { console$1.log("Warning, gif stream shorter than expected."); } return output; } /** * @license Copyright (c) 2008, Adobe Systems Incorporated All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Adobe Systems Incorporated nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* JPEG encoder ported to JavaScript and optimized by Andreas Ritter, www.bytestrom.eu, 11/2009 Basic GUI blocking jpeg encoder */ function JPEGEncoder(quality) { var ffloor = Math.floor; var YTable = new Array(64); var UVTable = new Array(64); var fdtbl_Y = new Array(64); var fdtbl_UV = new Array(64); var YDC_HT; var UVDC_HT; var YAC_HT; var UVAC_HT; var bitcode = new Array(65535); var category = new Array(65535); var outputfDCTQuant = new Array(64); var DU = new Array(64); var byteout = []; var bytenew = 0; var bytepos = 7; var YDU = new Array(64); var UDU = new Array(64); var VDU = new Array(64); var clt = new Array(256); var RGB_YUV_TABLE = new Array(2048); var currentQuality; var ZigZag = [ 0, 1, 5, 6, 14, 15, 27, 28, 2, 4, 7, 13, 16, 26, 29, 42, 3, 8, 12, 17, 25, 30, 41, 43, 9, 11, 18, 24, 31, 40, 44, 53, 10, 19, 23, 32, 39, 45, 52, 54, 20, 22, 33, 38, 46, 51, 55, 60, 21, 34, 37, 47, 50, 56, 59, 61, 35, 36, 48, 49, 57, 58, 62, 63 ]; var std_dc_luminance_nrcodes = [ 0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 ]; var std_dc_luminance_values = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; var std_ac_luminance_nrcodes = [ 0, 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d ]; var std_ac_luminance_values = [ 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa ]; var std_dc_chrominance_nrcodes = [ 0, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 ]; var std_dc_chrominance_values = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; var std_ac_chrominance_nrcodes = [ 0, 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77 ]; var std_ac_chrominance_values = [ 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa ]; function initQuantTables(sf) { var YQT = [ 16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, 14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, 80, 62, 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113, 92, 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99 ]; for (var i = 0; i < 64; i++) { var t = ffloor((YQT[i] * sf + 50) / 100); t = Math.min(Math.max(t, 1), 255); YTable[ZigZag[i]] = t; } var UVQT = [ 17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, 24, 26, 56, 99, 99, 99, 99, 99, 47, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99 ]; for (var j = 0; j < 64; j++) { var u = ffloor((UVQT[j] * sf + 50) / 100); u = Math.min(Math.max(u, 1), 255); UVTable[ZigZag[j]] = u; } var aasf = [ 1.0, 1.387039845, 1.306562965, 1.175875602, 1.0, 0.785694958, 0.5411961, 0.275899379 ]; var k = 0; for (var row = 0; row < 8; row++) { for (var col = 0; col < 8; col++) { fdtbl_Y[k] = 1.0 / (YTable[ZigZag[k]] * aasf[row] * aasf[col] * 8.0); fdtbl_UV[k] = 1.0 / (UVTable[ZigZag[k]] * aasf[row] * aasf[col] * 8.0); k++; } } } function computeHuffmanTbl(nrcodes, std_table) { var codevalue = 0; var pos_in_table = 0; var HT = new Array(); for (var k = 1; k <= 16; k++) { for (var j = 1; j <= nrcodes[k]; j++) { HT[std_table[pos_in_table]] = []; HT[std_table[pos_in_table]][0] = codevalue; HT[std_table[pos_in_table]][1] = k; pos_in_table++; codevalue++; } codevalue *= 2; } return HT; } function initHuffmanTbl() { YDC_HT = computeHuffmanTbl( std_dc_luminance_nrcodes, std_dc_luminance_values ); UVDC_HT = computeHuffmanTbl( std_dc_chrominance_nrcodes, std_dc_chrominance_values ); YAC_HT = computeHuffmanTbl( std_ac_luminance_nrcodes, std_ac_luminance_values ); UVAC_HT = computeHuffmanTbl( std_ac_chrominance_nrcodes, std_ac_chrominance_values ); } function initCategoryNumber() { var nrlower = 1; var nrupper = 2; for (var cat = 1; cat <= 15; cat++) { //Positive numbers for (var nr = nrlower; nr < nrupper; nr++) { category[32767 + nr] = cat; bitcode[32767 + nr] = []; bitcode[32767 + nr][1] = cat; bitcode[32767 + nr][0] = nr; } //Negative numbers for (var nrneg = -(nrupper - 1); nrneg <= -nrlower; nrneg++) { category[32767 + nrneg] = cat; bitcode[32767 + nrneg] = []; bitcode[32767 + nrneg][1] = cat; bitcode[32767 + nrneg][0] = nrupper - 1 + nrneg; } nrlower <<= 1; nrupper <<= 1; } } function initRGBYUVTable() { for (var i = 0; i < 256; i++) { RGB_YUV_TABLE[i] = 19595 * i; RGB_YUV_TABLE[(i + 256) >> 0] = 38470 * i; RGB_YUV_TABLE[(i + 512) >> 0] = 7471 * i + 0x8000; RGB_YUV_TABLE[(i + 768) >> 0] = -11059 * i; RGB_YUV_TABLE[(i + 1024) >> 0] = -21709 * i; RGB_YUV_TABLE[(i + 1280) >> 0] = 32768 * i + 0x807fff; RGB_YUV_TABLE[(i + 1536) >> 0] = -27439 * i; RGB_YUV_TABLE[(i + 1792) >> 0] = -5329 * i; } } // IO functions function writeBits(bs) { var value = bs[0]; var posval = bs[1] - 1; while (posval >= 0) { if (value & (1 << posval)) { bytenew |= 1 << bytepos; } posval--; bytepos--; if (bytepos < 0) { if (bytenew == 0xff) { writeByte(0xff); writeByte(0); } else { writeByte(bytenew); } bytepos = 7; bytenew = 0; } } } function writeByte(value) { //byteout.push(clt[value]); // write char directly instead of converting later byteout.push(value); } function writeWord(value) { writeByte((value >> 8) & 0xff); writeByte(value & 0xff); } // DCT & quantization core function fDCTQuant(data, fdtbl) { var d0, d1, d2, d3, d4, d5, d6, d7; /* Pass 1: process rows. */ var dataOff = 0; var i; var I8 = 8; var I64 = 64; for (i = 0; i < I8; ++i) { d0 = data[dataOff]; d1 = data[dataOff + 1]; d2 = data[dataOff + 2]; d3 = data[dataOff + 3]; d4 = data[dataOff + 4]; d5 = data[dataOff + 5]; d6 = data[dataOff + 6]; d7 = data[dataOff + 7]; var tmp0 = d0 + d7; var tmp7 = d0 - d7; var tmp1 = d1 + d6; var tmp6 = d1 - d6; var tmp2 = d2 + d5; var tmp5 = d2 - d5; var tmp3 = d3 + d4; var tmp4 = d3 - d4; /* Even part */ var tmp10 = tmp0 + tmp3; /* phase 2 */ var tmp13 = tmp0 - tmp3; var tmp11 = tmp1 + tmp2; var tmp12 = tmp1 - tmp2; data[dataOff] = tmp10 + tmp11; /* phase 3 */ data[dataOff + 4] = tmp10 - tmp11; var z1 = (tmp12 + tmp13) * 0.707106781; /* c4 */ data[dataOff + 2] = tmp13 + z1; /* phase 5 */ data[dataOff + 6] = tmp13 - z1; /* Odd part */ tmp10 = tmp4 + tmp5; /* phase 2 */ tmp11 = tmp5 + tmp6; tmp12 = tmp6 + tmp7; /* The rotator is modified from fig 4-8 to avoid extra negations. */ var z5 = (tmp10 - tmp12) * 0.382683433; /* c6 */ var z2 = 0.5411961 * tmp10 + z5; /* c2-c6 */ var z4 = 1.306562965 * tmp12 + z5; /* c2+c6 */ var z3 = tmp11 * 0.707106781; /* c4 */ var z11 = tmp7 + z3; /* phase 5 */ var z13 = tmp7 - z3; data[dataOff + 5] = z13 + z2; /* phase 6 */ data[dataOff + 3] = z13 - z2; data[dataOff + 1] = z11 + z4; data[dataOff + 7] = z11 - z4; dataOff += 8; /* advance pointer to next row */ } /* Pass 2: process columns. */ dataOff = 0; for (i = 0; i < I8; ++i) { d0 = data[dataOff]; d1 = data[dataOff + 8]; d2 = data[dataOff + 16]; d3 = data[dataOff + 24]; d4 = data[dataOff + 32]; d5 = data[dataOff + 40]; d6 = data[dataOff + 48]; d7 = data[dataOff + 56]; var tmp0p2 = d0 + d7; var tmp7p2 = d0 - d7; var tmp1p2 = d1 + d6; var tmp6p2 = d1 - d6; var tmp2p2 = d2 + d5; var tmp5p2 = d2 - d5; var tmp3p2 = d3 + d4; var tmp4p2 = d3 - d4; /* Even part */ var tmp10p2 = tmp0p2 + tmp3p2; /* phase 2 */ var tmp13p2 = tmp0p2 - tmp3p2; var tmp11p2 = tmp1p2 + tmp2p2; var tmp12p2 = tmp1p2 - tmp2p2; data[dataOff] = tmp10p2 + tmp11p2; /* phase 3 */ data[dataOff + 32] = tmp10p2 - tmp11p2; var z1p2 = (tmp12p2 + tmp13p2) * 0.707106781; /* c4 */ data[dataOff + 16] = tmp13p2 + z1p2; /* phase 5 */ data[dataOff + 48] = tmp13p2 - z1p2; /* Odd part */ tmp10p2 = tmp4p2 + tmp5p2; /* phase 2 */ tmp11p2 = tmp5p2 + tmp6p2; tmp12p2 = tmp6p2 + tmp7p2; /* The rotator is modified from fig 4-8 to avoid extra negations. */ var z5p2 = (tmp10p2 - tmp12p2) * 0.382683433; /* c6 */ var z2p2 = 0.5411961 * tmp10p2 + z5p2; /* c2-c6 */ var z4p2 = 1.306562965 * tmp12p2 + z5p2; /* c2+c6 */ var z3p2 = tmp11p2 * 0.707106781; /* c4 */ var z11p2 = tmp7p2 + z3p2; /* phase 5 */ var z13p2 = tmp7p2 - z3p2; data[dataOff + 40] = z13p2 + z2p2; /* phase 6 */ data[dataOff + 24] = z13p2 - z2p2; data[dataOff + 8] = z11p2 + z4p2; data[dataOff + 56] = z11p2 - z4p2; dataOff++; /* advance pointer to next column */ } // Quantize/descale the coefficients var fDCTQuant; for (i = 0; i < I64; ++i) { // Apply the quantization and scaling factor & Round to nearest integer fDCTQuant = data[i] * fdtbl[i]; outputfDCTQuant[i] = fDCTQuant > 0.0 ? (fDCTQuant + 0.5) | 0 : (fDCTQuant - 0.5) | 0; //outputfDCTQuant[i] = fround(fDCTQuant); } return outputfDCTQuant; } function writeAPP0() { writeWord(0xffe0); // marker writeWord(16); // length writeByte(0x4a); // J writeByte(0x46); // F writeByte(0x49); // I writeByte(0x46); // F writeByte(0); // = "JFIF",'\0' writeByte(1); // versionhi writeByte(1); // versionlo writeByte(0); // xyunits writeWord(1); // xdensity writeWord(1); // ydensity writeByte(0); // thumbnwidth writeByte(0); // thumbnheight } function writeSOF0(width, height) { writeWord(0xffc0); // marker writeWord(17); // length, truecolor YUV JPG writeByte(8); // precision writeWord(height); writeWord(width); writeByte(3); // nrofcomponents writeByte(1); // IdY writeByte(0x11); // HVY writeByte(0); // QTY writeByte(2); // IdU writeByte(0x11); // HVU writeByte(1); // QTU writeByte(3); // IdV writeByte(0x11); // HVV writeByte(1); // QTV } function writeDQT() { writeWord(0xffdb); // marker writeWord(132); // length writeByte(0); for (var i = 0; i < 64; i++) { writeByte(YTable[i]); } writeByte(1); for (var j = 0; j < 64; j++) { writeByte(UVTable[j]); } } function writeDHT() { writeWord(0xffc4); // marker writeWord(0x01a2); // length writeByte(0); // HTYDCinfo for (var i = 0; i < 16; i++) { writeByte(std_dc_luminance_nrcodes[i + 1]); } for (var j = 0; j <= 11; j++) { writeByte(std_dc_luminance_values[j]); } writeByte(0x10); // HTYACinfo for (var k = 0; k < 16; k++) { writeByte(std_ac_luminance_nrcodes[k + 1]); } for (var l = 0; l <= 161; l++) { writeByte(std_ac_luminance_values[l]); } writeByte(1); // HTUDCinfo for (var m = 0; m < 16; m++) { writeByte(std_dc_chrominance_nrcodes[m + 1]); } for (var n = 0; n <= 11; n++) { writeByte(std_dc_chrominance_values[n]); } writeByte(0x11); // HTUACinfo for (var o = 0; o < 16; o++) { writeByte(std_ac_chrominance_nrcodes[o + 1]); } for (var p = 0; p <= 161; p++) { writeByte(std_ac_chrominance_values[p]); } } function writeSOS() { writeWord(0xffda); // marker writeWord(12); // length writeByte(3); // nrofcomponents writeByte(1); // IdY writeByte(0); // HTY writeByte(2); // IdU writeByte(0x11); // HTU writeByte(3); // IdV writeByte(0x11); // HTV writeByte(0); // Ss writeByte(0x3f); // Se writeByte(0); // Bf } function processDU(CDU, fdtbl, DC, HTDC, HTAC) { var EOB = HTAC[0x00]; var M16zeroes = HTAC[0xf0]; var pos; var I16 = 16; var I63 = 63; var I64 = 64; var DU_DCT = fDCTQuant(CDU, fdtbl); //ZigZag reorder for (var j = 0; j < I64; ++j) { DU[ZigZag[j]] = DU_DCT[j]; } var Diff = DU[0] - DC; DC = DU[0]; //Encode DC if (Diff == 0) { writeBits(HTDC[0]); // Diff might be 0 } else { pos = 32767 + Diff; writeBits(HTDC[category[pos]]); writeBits(bitcode[pos]); } //Encode ACs var end0pos = 63; // was const... which is crazy while (end0pos > 0 && DU[end0pos] == 0) { end0pos--; } //end0pos = first element in reverse order !=0 if (end0pos == 0) { writeBits(EOB); return DC; } var i = 1; var lng; while (i <= end0pos) { var startpos = i; while (DU[i] == 0 && i <= end0pos) { ++i; } var nrzeroes = i - startpos; if (nrzeroes >= I16) { lng = nrzeroes >> 4; for (var nrmarker = 1; nrmarker <= lng; ++nrmarker) writeBits(M16zeroes); nrzeroes = nrzeroes & 0xf; } pos = 32767 + DU[i]; writeBits(HTAC[(nrzeroes << 4) + category[pos]]); writeBits(bitcode[pos]); i++; } if (end0pos != I63) { writeBits(EOB); } return DC; } function initCharLookupTable() { var sfcc = String.fromCharCode; for (var i = 0; i < 256; i++) { ///// ACHTUNG // 255 clt[i] = sfcc(i); } } this.encode = function( image, quality // image data object ) { if (quality) setQuality(quality); // Initialize bit writer byteout = new Array(); bytenew = 0; bytepos = 7; // Add JPEG headers writeWord(0xffd8); // SOI writeAPP0(); writeDQT(); writeSOF0(image.width, image.height); writeDHT(); writeSOS(); // Encode 8x8 macroblocks var DCY = 0; var DCU = 0; var DCV = 0; bytenew = 0; bytepos = 7; this.encode.displayName = "_encode_"; var imageData = image.data; var width = image.width; var height = image.height; var quadWidth = width * 4; var x, y = 0; var r, g, b; var start, p, col, row, pos; while (y < height) { x = 0; while (x < quadWidth) { start = quadWidth * y + x; col = -1; row = 0; for (pos = 0; pos < 64; pos++) { row = pos >> 3; // /8 col = (pos & 7) * 4; // %8 p = start + row * quadWidth + col; if (y + row >= height) { // padding bottom p -= quadWidth * (y + 1 + row - height); } if (x + col >= quadWidth) { // padding right p -= x + col - quadWidth + 4; } r = imageData[p++]; g = imageData[p++]; b = imageData[p++]; /* // calculate YUV values dynamically YDU[pos]=((( 0.29900)*r+( 0.58700)*g+( 0.11400)*b))-128; //-0x80 UDU[pos]=(((-0.16874)*r+(-0.33126)*g+( 0.50000)*b)); VDU[pos]=((( 0.50000)*r+(-0.41869)*g+(-0.08131)*b)); */ // use lookup table (slightly faster) YDU[pos] = ((RGB_YUV_TABLE[r] + RGB_YUV_TABLE[(g + 256) >> 0] + RGB_YUV_TABLE[(b + 512) >> 0]) >> 16) - 128; UDU[pos] = ((RGB_YUV_TABLE[(r + 768) >> 0] + RGB_YUV_TABLE[(g + 1024) >> 0] + RGB_YUV_TABLE[(b + 1280) >> 0]) >> 16) - 128; VDU[pos] = ((RGB_YUV_TABLE[(r + 1280) >> 0] + RGB_YUV_TABLE[(g + 1536) >> 0] + RGB_YUV_TABLE[(b + 1792) >> 0]) >> 16) - 128; } DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT); DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); x += 32; } y += 8; } //////////////////////////////////////////////////////////////// // Do the bit alignment of the EOI marker if (bytepos >= 0) { var fillbits = []; fillbits[1] = bytepos + 1; fillbits[0] = (1 << (bytepos + 1)) - 1; writeBits(fillbits); } writeWord(0xffd9); //EOI return new Uint8Array(byteout); }; function setQuality(quality) { quality = Math.min(Math.max(quality, 1), 100); if (currentQuality == quality) return; // don't recalc if unchanged var sf = quality < 50 ? Math.floor(5000 / quality) : Math.floor(200 - quality * 2); initQuantTables(sf); currentQuality = quality; //console.log('Quality set to: '+quality +'%'); } function init() { quality = quality || 50; // Create tables initCharLookupTable(); initHuffmanTbl(); initCategoryNumber(); initRGBYUVTable(); setQuality(quality); } init(); } /** * @license * Copyright (c) 2017 Aras Abbasi * * Licensed under the MIT License. * http://opensource.org/licenses/mit-license */ /** * jsPDF Gif Support PlugIn * * @name gif_support * @module */ (function(jsPDFAPI) { jsPDFAPI.processGIF89A = function(imageData, index, alias, compression) { var reader = new GifReader(imageData); var width = reader.width, height = reader.height; var qu = 100; var pixels = []; reader.decodeAndBlitFrameRGBA(0, pixels); var rawImageData = { data: pixels, width: width, height: height }; var encoder = new JPEGEncoder(qu); var data = encoder.encode(rawImageData, qu); return jsPDFAPI.processJPEG.call(this, data, index, alias, compression); }; jsPDFAPI.processGIF87A = jsPDFAPI.processGIF89A; })(jsPDF.API); /** * @author shaozilee * * Bmp format decoder,support 1bit 4bit 8bit 24bit bmp * */ function BmpDecoder(buffer, is_with_alpha) { this.pos = 0; this.buffer = buffer; this.datav = new DataView(buffer.buffer); this.is_with_alpha = !!is_with_alpha; this.bottom_up = true; this.flag = String.fromCharCode(this.buffer[0]) + String.fromCharCode(this.buffer[1]); this.pos += 2; if (["BM", "BA", "CI", "CP", "IC", "PT"].indexOf(this.flag) === -1) throw new Error("Invalid BMP File"); this.parseHeader(); this.parseBGR(); } BmpDecoder.prototype.parseHeader = function() { this.fileSize = this.datav.getUint32(this.pos, true); this.pos += 4; this.reserved = this.datav.getUint32(this.pos, true); this.pos += 4; this.offset = this.datav.getUint32(this.pos, true); this.pos += 4; this.headerSize = this.datav.getUint32(this.pos, true); this.pos += 4; this.width = this.datav.getUint32(this.pos, true); this.pos += 4; this.height = this.datav.getInt32(this.pos, true); this.pos += 4; this.planes = this.datav.getUint16(this.pos, true); this.pos += 2; this.bitPP = this.datav.getUint16(this.pos, true); this.pos += 2; this.compress = this.datav.getUint32(this.pos, true); this.pos += 4; this.rawSize = this.datav.getUint32(this.pos, true); this.pos += 4; this.hr = this.datav.getUint32(this.pos, true); this.pos += 4; this.vr = this.datav.getUint32(this.pos, true); this.pos += 4; this.colors = this.datav.getUint32(this.pos, true); this.pos += 4; this.importantColors = this.datav.getUint32(this.pos, true); this.pos += 4; if (this.bitPP === 16 && this.is_with_alpha) { this.bitPP = 15; } if (this.bitPP < 15) { var len = this.colors === 0 ? 1 << this.bitPP : this.colors; this.palette = new Array(len); for (var i = 0; i < len; i++) { var blue = this.datav.getUint8(this.pos++, true); var green = this.datav.getUint8(this.pos++, true); var red = this.datav.getUint8(this.pos++, true); var quad = this.datav.getUint8(this.pos++, true); this.palette[i] = { red: red, green: green, blue: blue, quad: quad }; } } if (this.height < 0) { this.height *= -1; this.bottom_up = false; } }; BmpDecoder.prototype.parseBGR = function() { this.pos = this.offset; try { var bitn = "bit" + this.bitPP; var len = this.width * this.height * 4; this.data = new Uint8Array(len); this[bitn](); } catch (e) { console$1.log("bit decode error:" + e); } }; BmpDecoder.prototype.bit1 = function() { var xlen = Math.ceil(this.width / 8); var mode = xlen % 4; var y; for (y = this.height - 1; y >= 0; y--) { var line = this.bottom_up ? y : this.height - 1 - y; for (var x = 0; x < xlen; x++) { var b = this.datav.getUint8(this.pos++, true); var location = line * this.width * 4 + x * 8 * 4; for (var i = 0; i < 8; i++) { if (x * 8 + i < this.width) { var rgb = this.palette[(b >> (7 - i)) & 0x1]; this.data[location + i * 4] = rgb.blue; this.data[location + i * 4 + 1] = rgb.green; this.data[location + i * 4 + 2] = rgb.red; this.data[location + i * 4 + 3] = 0xff; } else { break; } } } if (mode !== 0) { this.pos += 4 - mode; } } }; BmpDecoder.prototype.bit4 = function() { var xlen = Math.ceil(this.width / 2); var mode = xlen % 4; for (var y = this.height - 1; y >= 0; y--) { var line = this.bottom_up ? y : this.height - 1 - y; for (var x = 0; x < xlen; x++) { var b = this.datav.getUint8(this.pos++, true); var location = line * this.width * 4 + x * 2 * 4; var before = b >> 4; var after = b & 0x0f; var rgb = this.palette[before]; this.data[location] = rgb.blue; this.data[location + 1] = rgb.green; this.data[location + 2] = rgb.red; this.data[location + 3] = 0xff; if (x * 2 + 1 >= this.width) break; rgb = this.palette[after]; this.data[location + 4] = rgb.blue; this.data[location + 4 + 1] = rgb.green; this.data[location + 4 + 2] = rgb.red; this.data[location + 4 + 3] = 0xff; } if (mode !== 0) { this.pos += 4 - mode; } } }; BmpDecoder.prototype.bit8 = function() { var mode = this.width % 4; for (var y = this.height - 1; y >= 0; y--) { var line = this.bottom_up ? y : this.height - 1 - y; for (var x = 0; x < this.width; x++) { var b = this.datav.getUint8(this.pos++, true); var location = line * this.width * 4 + x * 4; if (b < this.palette.length) { var rgb = this.palette[b]; this.data[location] = rgb.red; this.data[location + 1] = rgb.green; this.data[location + 2] = rgb.blue; this.data[location + 3] = 0xff; } else { this.data[location] = 0xff; this.data[location + 1] = 0xff; this.data[location + 2] = 0xff; this.data[location + 3] = 0xff; } } if (mode !== 0) { this.pos += 4 - mode; } } }; BmpDecoder.prototype.bit15 = function() { var dif_w = this.width % 3; var _11111 = parseInt("11111", 2), _1_5 = _11111; for (var y = this.height - 1; y >= 0; y--) { var line = this.bottom_up ? y : this.height - 1 - y; for (var x = 0; x < this.width; x++) { var B = this.datav.getUint16(this.pos, true); this.pos += 2; var blue = (((B & _1_5) / _1_5) * 255) | 0; var green = ((((B >> 5) & _1_5) / _1_5) * 255) | 0; var red = ((((B >> 10) & _1_5) / _1_5) * 255) | 0; var alpha = B >> 15 ? 0xff : 0x00; var location = line * this.width * 4 + x * 4; this.data[location] = red; this.data[location + 1] = green; this.data[location + 2] = blue; this.data[location + 3] = alpha; } //skip extra bytes this.pos += dif_w; } }; BmpDecoder.prototype.bit16 = function() { var dif_w = this.width % 3; var _11111 = parseInt("11111", 2), _1_5 = _11111; var _111111 = parseInt("111111", 2), _1_6 = _111111; for (var y = this.height - 1; y >= 0; y--) { var line = this.bottom_up ? y : this.height - 1 - y; for (var x = 0; x < this.width; x++) { var B = this.datav.getUint16(this.pos, true); this.pos += 2; var alpha = 0xff; var blue = (((B & _1_5) / _1_5) * 255) | 0; var green = ((((B >> 5) & _1_6) / _1_6) * 255) | 0; var red = (((B >> 11) / _1_5) * 255) | 0; var location = line * this.width * 4 + x * 4; this.data[location] = red; this.data[location + 1] = green; this.data[location + 2] = blue; this.data[location + 3] = alpha; } //skip extra bytes this.pos += dif_w; } }; BmpDecoder.prototype.bit24 = function() { //when height > 0 for (var y = this.height - 1; y >= 0; y--) { var line = this.bottom_up ? y : this.height - 1 - y; for (var x = 0; x < this.width; x++) { var blue = this.datav.getUint8(this.pos++, true); var green = this.datav.getUint8(this.pos++, true); var red = this.datav.getUint8(this.pos++, true); var location = line * this.width * 4 + x * 4; this.data[location] = red; this.data[location + 1] = green; this.data[location + 2] = blue; this.data[location + 3] = 0xff; } //skip extra bytes this.pos += this.width % 4; } }; /** * add 32bit decode func * @author soubok */ BmpDecoder.prototype.bit32 = function() { //when height > 0 for (var y = this.height - 1; y >= 0; y--) { var line = this.bottom_up ? y : this.height - 1 - y; for (var x = 0; x < this.width; x++) { var blue = this.datav.getUint8(this.pos++, true); var green = this.datav.getUint8(this.pos++, true); var red = this.datav.getUint8(this.pos++, true); var alpha = this.datav.getUint8(this.pos++, true); var location = line * this.width * 4 + x * 4; this.data[location] = red; this.data[location + 1] = green; this.data[location + 2] = blue; this.data[location + 3] = alpha; } //skip extra bytes //this.pos += (this.width % 4); } }; BmpDecoder.prototype.getData = function() { return this.data; }; /** * @license * Copyright (c) 2018 Aras Abbasi * * Licensed under the MIT License. * http://opensource.org/licenses/mit-license */ /** * jsPDF bmp Support PlugIn * @name bmp_support * @module */ (function(jsPDFAPI) { jsPDFAPI.processBMP = function(imageData, index, alias, compression) { var reader = new BmpDecoder(imageData, false); var width = reader.width, height = reader.height; var qu = 100; var pixels = reader.getData(); var rawImageData = { data: pixels, width: width, height: height }; var encoder = new JPEGEncoder(qu); var data = encoder.encode(rawImageData, qu); return jsPDFAPI.processJPEG.call(this, data, index, alias, compression); }; })(jsPDF.API); function WebPDecoder(imageData) { function x(F) { if (!F) throw Error("assert :P"); } function fa(F, L, J) { for (var H = 0; 4 > H; H++) if (F[L + H] != J.charCodeAt(H)) return true; return false; } function I(F, L, J, H, Z) { for (var O = 0; O < Z; O++) F[L + O] = J[H + O]; } function M(F, L, J, H) { for (var Z = 0; Z < H; Z++) F[L + Z] = J; } function V(F) { return new Int32Array(F); } function wa(F, L) { for (var J = [], H = 0; H < F; H++) J.push(new L()); return J; } function wb() { function F(J, H, Z) { for (var O = Z[H], L = 0; L < O; L++) { J.push(Z.length > H + 1 ? [] : 0); if (Z.length < H + 1) break; F(J[L], H + 1, Z); } } var L = []; F(L, 0, [3, 11]); return L; } function Ed(F, L) { function J(H, O, F) { for (var Z = F[O], ma = 0; ma < Z; ma++) { H.push(F.length > O + 1 ? [] : new L()); if (F.length < O + 1) break; J(H[ma], O + 1, F); } } var H = []; J(H, 0, F); return H; } var _WebPDecoder = function() { var self = this; function L(a, b) { for (var c = (1 << (b - 1)) >>> 0; a & c; ) c >>>= 1; return c ? (a & (c - 1)) + c : a; } function J(a, b, c, d, e) { x(!(d % c)); do (d -= c), (a[b + d] = e); while (0 < d); } function H(a, b, c, d, e, f) { var g = b, h = 1 << c, k, l, m = V(16), n = V(16); x(0 != e); x(null != d); x(null != a); x(0 < c); for (l = 0; l < e; ++l) { if (15 < d[l]) return 0; ++m[d[l]]; } if (m[0] == e) return 0; n[1] = 0; for (k = 1; 15 > k; ++k) { if (m[k] > 1 << k) return 0; n[k + 1] = n[k] + m[k]; } for (l = 0; l < e; ++l) (k = d[l]), 0 < d[l] && (f[n[k]++] = l); if (1 == n[15]) return (d = new O()), (d.g = 0), (d.value = f[0]), J(a, g, 1, h, d), h; var r = -1, q = h - 1, t = 0, v = 1, p = 1, u, w = 1 << c; l = 0; k = 1; for (e = 2; k <= c; ++k, e <<= 1) { p <<= 1; v += p; p -= m[k]; if (0 > p) return 0; for (; 0 < m[k]; --m[k]) (d = new O()), (d.g = k), (d.value = f[l++]), J(a, g + t, e, w, d), (t = L(t, k)); } k = c + 1; for (e = 2; 15 >= k; ++k, e <<= 1) { p <<= 1; v += p; p -= m[k]; if (0 > p) return 0; for (; 0 < m[k]; --m[k]) { d = new O(); if ((t & q) != r) { g += w; r = k; for (u = 1 << (r - c); 15 > r; ) { u -= m[r]; if (0 >= u) break; ++r; u <<= 1; } u = r - c; w = 1 << u; h += w; r = t & q; a[b + r].g = u + c; a[b + r].value = g - b - r; } d.g = k - c; d.value = f[l++]; J(a, g + (t >> c), e, w, d); t = L(t, k); } } return v != 2 * n[15] - 1 ? 0 : h; } function Z(a, b, c, d, e) { x(2328 >= e); if (512 >= e) var f = V(512); else if (((f = V(e)), null == f)) return 0; return H(a, b, c, d, e, f); } function O() { this.value = this.g = 0; } function Fd() { this.value = this.g = 0; } function Ub() { this.G = wa(5, O); this.H = V(5); this.jc = this.Qb = this.qb = this.nd = 0; this.pd = wa(xb, Fd); } function ma(a, b, c, d) { x(null != a); x(null != b); x(2147483648 > d); a.Ca = 254; a.I = 0; a.b = -8; a.Ka = 0; a.oa = b; a.pa = c; a.Jd = b; a.Yc = c + d; a.Zc = 4 <= d ? c + d - 4 + 1 : c; Qa(a); } function na(a, b) { for (var c = 0; 0 < b--; ) c |= K(a, 128) << b; return c; } function ca(a, b) { var c = na(a, b); return G(a) ? -c : c; } function cb(a, b, c, d) { var e, f = 0; x(null != a); x(null != b); x(4294967288 > d); a.Sb = d; a.Ra = 0; a.u = 0; a.h = 0; 4 < d && (d = 4); for (e = 0; e < d; ++e) f += b[c + e] << (8 * e); a.Ra = f; a.bb = d; a.oa = b; a.pa = c; } function Vb(a) { for (; 8 <= a.u && a.bb < a.Sb; ) (a.Ra >>>= 8), (a.Ra += (a.oa[a.pa + a.bb] << (ob - 8)) >>> 0), ++a.bb, (a.u -= 8); db(a) && ((a.h = 1), (a.u = 0)); } function D(a, b) { x(0 <= b); if (!a.h && b <= Gd) { var c = pb(a) & Hd[b]; a.u += b; Vb(a); return c; } a.h = 1; return (a.u = 0); } function Wb() { this.b = this.Ca = this.I = 0; this.oa = []; this.pa = 0; this.Jd = []; this.Yc = 0; this.Zc = []; this.Ka = 0; } function Ra() { this.Ra = 0; this.oa = []; this.h = this.u = this.bb = this.Sb = this.pa = 0; } function pb(a) { return (a.Ra >>> (a.u & (ob - 1))) >>> 0; } function db(a) { x(a.bb <= a.Sb); return a.h || (a.bb == a.Sb && a.u > ob); } function qb(a, b) { a.u = b; a.h = db(a); } function Sa(a) { a.u >= Xb && (x(a.u >= Xb), Vb(a)); } function Qa(a) { x(null != a && null != a.oa); a.pa < a.Zc ? ((a.I = (a.oa[a.pa++] | (a.I << 8)) >>> 0), (a.b += 8)) : (x(null != a && null != a.oa), a.pa < a.Yc ? ((a.b += 8), (a.I = a.oa[a.pa++] | (a.I << 8))) : a.Ka ? (a.b = 0) : ((a.I <<= 8), (a.b += 8), (a.Ka = 1))); } function G(a) { return na(a, 1); } function K(a, b) { var c = a.Ca; 0 > a.b && Qa(a); var d = a.b, e = (c * b) >>> 8, f = (a.I >>> d > e) + 0; f ? ((c -= e), (a.I -= ((e + 1) << d) >>> 0)) : (c = e + 1); d = c; for (e = 0; 256 <= d; ) (e += 8), (d >>= 8); d = 7 ^ (e + Id[d]); a.b -= d; a.Ca = (c << d) - 1; return f; } function ra(a, b, c) { a[b + 0] = (c >> 24) & 255; a[b + 1] = (c >> 16) & 255; a[b + 2] = (c >> 8) & 255; a[b + 3] = (c >> 0) & 255; } function Ta(a, b) { return (a[b + 0] << 0) | (a[b + 1] << 8); } function Yb(a, b) { return Ta(a, b) | (a[b + 2] << 16); } function Ha(a, b) { return Ta(a, b) | (Ta(a, b + 2) << 16); } function Zb(a, b) { var c = 1 << b; x(null != a); x(0 < b); a.X = V(c); if (null == a.X) return 0; a.Mb = 32 - b; a.Xa = b; return 1; } function $b(a, b) { x(null != a); x(null != b); x(a.Xa == b.Xa); I(b.X, 0, a.X, 0, 1 << b.Xa); } function ac() { this.X = []; this.Xa = this.Mb = 0; } function bc(a, b, c, d) { x(null != c); x(null != d); var e = c[0], f = d[0]; 0 == e && (e = (a * f + b / 2) / b); 0 == f && (f = (b * e + a / 2) / a); if (0 >= e || 0 >= f) return 0; c[0] = e; d[0] = f; return 1; } function xa(a, b) { return (a + (1 << b) - 1) >>> b; } function yb(a, b) { return ( (((((a & 4278255360) + (b & 4278255360)) >>> 0) & 4278255360) + ((((a & 16711935) + (b & 16711935)) >>> 0) & 16711935)) >>> 0 ); } function X(a, b) { self[b] = function(b, d, e, f, g, h, k) { var c; for (c = 0; c < g; ++c) { var m = self[a](h[k + c - 1], e, f + c); h[k + c] = yb(b[d + c], m); } }; } function Jd() { this.ud = this.hd = this.jd = 0; } function aa(a, b) { return ((((a ^ b) & 4278124286) >>> 1) + (a & b)) >>> 0; } function sa(a) { if (0 <= a && 256 > a) return a; if (0 > a) return 0; if (255 < a) return 255; } function eb(a, b) { return sa(a + ((a - b + 0.5) >> 1)); } function Ia(a, b, c) { return Math.abs(b - c) - Math.abs(a - c); } function cc(a, b, c, d, e, f, g) { d = f[g - 1]; for (c = 0; c < e; ++c) f[g + c] = d = yb(a[b + c], d); } function Kd(a, b, c, d, e) { var f; for (f = 0; f < c; ++f) { var g = a[b + f], h = (g >> 8) & 255, k = g & 16711935, k = k + ((h << 16) + h), k = k & 16711935; d[e + f] = ((g & 4278255360) + k) >>> 0; } } function dc(a, b) { b.jd = (a >> 0) & 255; b.hd = (a >> 8) & 255; b.ud = (a >> 16) & 255; } function Ld(a, b, c, d, e, f) { var g; for (g = 0; g < d; ++g) { var h = b[c + g], k = h >>> 8, l = h >>> 16, m = h, l = l + ((((a.jd << 24) >> 24) * ((k << 24) >> 24)) >>> 5), l = l & 255, m = m + ((((a.hd << 24) >> 24) * ((k << 24) >> 24)) >>> 5), m = m + ((((a.ud << 24) >> 24) * ((l << 24) >> 24)) >>> 5), m = m & 255; e[f + g] = (h & 4278255360) + (l << 16) + m; } } function ec(a, b, c, d, e) { self[b] = function(a, b, c, k, l, m, n, r, q) { for (k = n; k < r; ++k) for (n = 0; n < q; ++n) l[m++] = e(c[d(a[b++])]); }; self[a] = function(a, b, h, k, l, m, n) { var f = 8 >> a.b, g = a.Ea, t = a.K[0], v = a.w; if (8 > f) for (a = (1 << a.b) - 1, v = (1 << f) - 1; b < h; ++b) { var p = 0, u; for (u = 0; u < g; ++u) u & a || (p = d(k[l++])), (m[n++] = e(t[p & v])), (p >>= f); } else self["VP8LMapColor" + c](k, l, t, v, m, n, b, h, g); }; } function Md(a, b, c, d, e) { for (c = b + c; b < c; ) { var f = a[b++]; d[e++] = (f >> 16) & 255; d[e++] = (f >> 8) & 255; d[e++] = (f >> 0) & 255; } } function Nd(a, b, c, d, e) { for (c = b + c; b < c; ) { var f = a[b++]; d[e++] = (f >> 16) & 255; d[e++] = (f >> 8) & 255; d[e++] = (f >> 0) & 255; d[e++] = (f >> 24) & 255; } } function Od(a, b, c, d, e) { for (c = b + c; b < c; ) { var f = a[b++], g = ((f >> 16) & 240) | ((f >> 12) & 15), f = ((f >> 0) & 240) | ((f >> 28) & 15); d[e++] = g; d[e++] = f; } } function Pd(a, b, c, d, e) { for (c = b + c; b < c; ) { var f = a[b++], g = ((f >> 16) & 248) | ((f >> 13) & 7), f = ((f >> 5) & 224) | ((f >> 3) & 31); d[e++] = g; d[e++] = f; } } function Qd(a, b, c, d, e) { for (c = b + c; b < c; ) { var f = a[b++]; d[e++] = (f >> 0) & 255; d[e++] = (f >> 8) & 255; d[e++] = (f >> 16) & 255; } } function fb(a, b, c, d, e, f) { if (0 == f) for (c = b + c; b < c; ) (f = a[b++]), ra( d, ((f[0] >> 24) | ((f[1] >> 8) & 65280) | ((f[2] << 8) & 16711680) | (f[3] << 24)) >>> 0 ), (e += 32); else I(d, e, a, b, c); } function gb(a, b) { self[b][0] = self[a + "0"]; self[b][1] = self[a + "1"]; self[b][2] = self[a + "2"]; self[b][3] = self[a + "3"]; self[b][4] = self[a + "4"]; self[b][5] = self[a + "5"]; self[b][6] = self[a + "6"]; self[b][7] = self[a + "7"]; self[b][8] = self[a + "8"]; self[b][9] = self[a + "9"]; self[b][10] = self[a + "10"]; self[b][11] = self[a + "11"]; self[b][12] = self[a + "12"]; self[b][13] = self[a + "13"]; self[b][14] = self[a + "0"]; self[b][15] = self[a + "0"]; } function hb(a) { return a == zb || a == Ab || a == Ja || a == Bb; } function Rd() { this.eb = []; this.size = this.A = this.fb = 0; } function Sd() { this.y = []; this.f = []; this.ea = []; this.F = []; this.Tc = this.Ed = this.Cd = this.Fd = this.lb = this.Db = this.Ab = this.fa = this.J = this.W = this.N = this.O = 0; } function Cb() { this.Rd = this.height = this.width = this.S = 0; this.f = {}; this.f.RGBA = new Rd(); this.f.kb = new Sd(); this.sd = null; } function Td() { this.width = [0]; this.height = [0]; this.Pd = [0]; this.Qd = [0]; this.format = [0]; } function Ud() { this.Id = this.fd = this.Md = this.hb = this.ib = this.da = this.bd = this.cd = this.j = this.v = this.Da = this.Sd = this.ob = 0; } function Vd(a) { alert("todo:WebPSamplerProcessPlane"); return a.T; } function Wd(a, b) { var c = a.T, d = b.ba.f.RGBA, e = d.eb, f = d.fb + a.ka * d.A, g = P[b.ba.S], h = a.y, k = a.O, l = a.f, m = a.N, n = a.ea, r = a.W, q = b.cc, t = b.dc, v = b.Mc, p = b.Nc, u = a.ka, w = a.ka + a.T, y = a.U, A = (y + 1) >> 1; 0 == u ? g(h, k, null, null, l, m, n, r, l, m, n, r, e, f, null, null, y) : (g(b.ec, b.fc, h, k, q, t, v, p, l, m, n, r, e, f - d.A, e, f, y), ++c); for (; u + 2 < w; u += 2) (q = l), (t = m), (v = n), (p = r), (m += a.Rc), (r += a.Rc), (f += 2 * d.A), (k += 2 * a.fa), g(h, k - a.fa, h, k, q, t, v, p, l, m, n, r, e, f - d.A, e, f, y); k += a.fa; a.j + w < a.o ? (I(b.ec, b.fc, h, k, y), I(b.cc, b.dc, l, m, A), I(b.Mc, b.Nc, n, r, A), c--) : w & 1 || g( h, k, null, null, l, m, n, r, l, m, n, r, e, f + d.A, null, null, y ); return c; } function Xd(a, b, c) { var d = a.F, e = [a.J]; if (null != d) { var f = a.U, g = b.ba.S, h = g == ya || g == Ja; b = b.ba.f.RGBA; var k = [0], l = a.ka; k[0] = a.T; a.Kb && (0 == l ? --k[0] : (--l, (e[0] -= a.width)), a.j + a.ka + a.T == a.o && (k[0] = a.o - a.j - l)); var m = b.eb, l = b.fb + l * b.A; a = fc(d, e[0], a.width, f, k, m, l + (h ? 0 : 3), b.A); x(c == k); a && hb(g) && za(m, l, h, f, k, b.A); } return 0; } function gc(a) { var b = a.ma, c = b.ba.S, d = 11 > c, e = c == Ua || c == Va || c == ya || c == Db || 12 == c || hb(c); b.memory = null; b.Ib = null; b.Jb = null; b.Nd = null; if (!hc(b.Oa, a, e ? 11 : 12)) return 0; e && hb(c) && ic(); if (a.da) alert("todo:use_scaling"); else { if (d) { if (((b.Ib = Vd), a.Kb)) { c = (a.U + 1) >> 1; b.memory = V(a.U + 2 * c); if (null == b.memory) return 0; b.ec = b.memory; b.fc = 0; b.cc = b.ec; b.dc = b.fc + a.U; b.Mc = b.cc; b.Nc = b.dc + c; b.Ib = Wd; ic(); } } else alert("todo:EmitYUV"); e && ((b.Jb = Xd), d && Aa()); } if (d && !jc) { for (a = 0; 256 > a; ++a) (Yd[a] = (89858 * (a - 128) + Ba) >> Wa), (Zd[a] = -22014 * (a - 128) + Ba), ($d[a] = -45773 * (a - 128)), (ae[a] = (113618 * (a - 128) + Ba) >> Wa); for (a = ta; a < Eb; ++a) (b = (76283 * (a - 16) + Ba) >> Wa), (be[a - ta] = ga(b, 255)), (ce[a - ta] = ga((b + 8) >> 4, 15)); jc = 1; } return 1; } function kc(a) { var b = a.ma, c = a.U, d = a.T; x(!(a.ka & 1)); if (0 >= c || 0 >= d) return 0; c = b.Ib(a, b); null != b.Jb && b.Jb(a, b, c); b.Dc += c; return 1; } function lc(a) { a.ma.memory = null; } function mc(a, b, c, d) { if (47 != D(a, 8)) return 0; b[0] = D(a, 14) + 1; c[0] = D(a, 14) + 1; d[0] = D(a, 1); return 0 != D(a, 3) ? 0 : !a.h; } function ib(a, b) { if (4 > a) return a + 1; var c = (a - 2) >> 1; return ((2 + (a & 1)) << c) + D(b, c) + 1; } function nc(a, b) { if (120 < b) return b - 120; var c = de[b - 1], c = (c >> 4) * a + (8 - (c & 15)); return 1 <= c ? c : 1; } function ua(a, b, c) { var d = pb(c); b += d & 255; var e = a[b].g - 8; 0 < e && (qb(c, c.u + 8), (d = pb(c)), (b += a[b].value), (b += d & ((1 << e) - 1))); qb(c, c.u + a[b].g); return a[b].value; } function ub(a, b, c) { c.g += a.g; c.value += (a.value << b) >>> 0; x(8 >= c.g); return a.g; } function ha(a, b, c) { var d = a.xc; b = 0 == d ? 0 : a.vc[a.md * (c >> d) + (b >> d)]; x(b < a.Wb); return a.Ya[b]; } function oc(a, b, c, d) { var e = a.ab, f = a.c * b, g = a.C; b = g + b; var h = c, k = d; d = a.Ta; for (c = a.Ua; 0 < e--; ) { var l = a.gc[e], m = g, n = b, r = h, q = k, k = d, h = c, t = l.Ea; x(m < n); x(n <= l.nc); switch (l.hc) { case 2: pc(r, q, (n - m) * t, k, h); break; case 0: var v = l, p = m, u = n, w = k, y = h, A = v.Ea; 0 == p && (ee(r, q, null, null, 1, w, y), cc(r, q + 1, 0, 0, A - 1, w, y + 1), (q += A), (y += A), ++p); for ( var E = 1 << v.b, B = E - 1, C = xa(A, v.b), N = v.K, v = v.w + (p >> v.b) * C; p < u; ) { var z = N, Q = v, S = 1; for (fe(r, q, w, y - A, 1, w, y); S < A; ) { var K = qc[(z[Q++] >> 8) & 15], D = (S & ~B) + E; D > A && (D = A); K(r, q + +S, w, y + S - A, D - S, w, y + S); S = D; } q += A; y += A; ++p; p & B || (v += C); } n != l.nc && I(k, h - t, k, h + (n - m - 1) * t, t); break; case 1: t = r; u = q; r = l.Ea; q = 1 << l.b; w = q - 1; y = r & ~w; A = r - y; p = xa(r, l.b); E = l.K; for (l = l.w + (m >> l.b) * p; m < n; ) { B = E; C = l; N = new Jd(); v = u + y; for (z = u + r; u < v; ) dc(B[C++], N), Fb(N, t, u, q, k, h), (u += q), (h += q); u < z && (dc(B[C++], N), Fb(N, t, u, A, k, h), (u += A), (h += A)); ++m; m & w || (l += p); } break; case 3: if (r == k && q == h && 0 < l.b) { y = (n - m) * xa(l.Ea, l.b); t = h + (n - m) * t - y; u = k; r = t; q = k; w = h; A = y; p = []; for (y = A - 1; 0 <= y; --y) p[y] = q[w + y]; for (y = A - 1; 0 <= y; --y) u[r + y] = p[y]; rc(l, m, n, k, t, k, h); } else rc(l, m, n, r, q, k, h); } h = d; k = c; } k != c && I(d, c, h, k, f); } function ge(a, b) { var c = a.V, d = a.Ba + a.c * a.C, e = b - a.C; x(b <= a.l.o); x(16 >= e); if (0 < e) { var f = a.l, g = a.Ta, h = a.Ua, k = f.width; oc(a, e, c, d); h = [h]; c = a.C; d = b; e = h; x(c < d); x(f.v < f.va); d > f.o && (d = f.o); if (c < f.j) { var l = f.j - c, c = f.j; e[0] += l * k; } c >= d ? (c = 0) : ((e[0] += 4 * f.v), (f.ka = c - f.j), (f.U = f.va - f.v), (f.T = d - c), (c = 1)); if (c) { h = h[0]; c = a.ca; if (11 > c.S) { for ( var m = c.f.RGBA, d = c.S, e = f.U, f = f.T, l = m.eb, n = m.A, r = f, m = m.fb + a.Ma * m.A; 0 < r--; ) { var q = g, t = h, v = e, p = l, u = m; switch (d) { case Ca: sc(q, t, v, p, u); break; case Ua: Gb(q, t, v, p, u); break; case zb: Gb(q, t, v, p, u); za(p, u, 0, v, 1, 0); break; case tc: uc(q, t, v, p, u); break; case Va: fb(q, t, v, p, u, 1); break; case Ab: fb(q, t, v, p, u, 1); za(p, u, 0, v, 1, 0); break; case ya: fb(q, t, v, p, u, 0); break; case Ja: fb(q, t, v, p, u, 0); za(p, u, 1, v, 1, 0); break; case Db: Hb(q, t, v, p, u); break; case Bb: Hb(q, t, v, p, u); vc(p, u, v, 1, 0); break; case wc: xc(q, t, v, p, u); break; default: x(0); } h += k; m += n; } a.Ma += f; } else alert("todo:EmitRescaledRowsYUVA"); x(a.Ma <= c.height); } } a.C = b; x(a.C <= a.i); } function yc(a) { var b; if (0 < a.ua) return 0; for (b = 0; b < a.Wb; ++b) { var c = a.Ya[b].G, d = a.Ya[b].H; if ( 0 < c[1][d[1] + 0].g || 0 < c[2][d[2] + 0].g || 0 < c[3][d[3] + 0].g ) return 0; } return 1; } function zc(a, b, c, d, e, f) { if (0 != a.Z) { var g = a.qd, h = a.rd; for (x(null != ia[a.Z]); b < c; ++b) ia[a.Z](g, h, d, e, d, e, f), (g = d), (h = e), (e += f); a.qd = g; a.rd = h; } } function Ib(a, b) { var c = a.l.ma, d = 0 == c.Z || 1 == c.Z ? a.l.j : a.C, d = a.C < d ? d : a.C; x(b <= a.l.o); if (b > d) { var e = a.l.width, f = c.ca, g = c.tb + e * d, h = a.V, k = a.Ba + a.c * d, l = a.gc; x(1 == a.ab); x(3 == l[0].hc); he(l[0], d, b, h, k, f, g); zc(c, d, b, f, g, e); } a.C = a.Ma = b; } function Jb(a, b, c, d, e, f, g) { var h = a.$ / d, k = a.$ % d, l = a.m, m = a.s, n = c + a.$, r = n; e = c + d * e; var q = c + d * f, t = 280 + m.ua, v = a.Pb ? h : 16777216, p = 0 < m.ua ? m.Wa : null, u = m.wc, w = n < q ? ha(m, k, h) : null; x(a.C < f); x(q <= e); var y = false; a: for (;;) { for (; y || n < q; ) { var A = 0; if (h >= v) { var v = a, E = n - c; x(v.Pb); v.wd = v.m; v.xd = E; 0 < v.s.ua && $b(v.s.Wa, v.s.vb); v = h + ie; } k & u || (w = ha(m, k, h)); x(null != w); w.Qb && ((b[n] = w.qb), (y = true)); if (!y) if ((Sa(l), w.jc)) { var A = l, E = b, B = n, C = w.pd[pb(A) & (xb - 1)]; x(w.jc); 256 > C.g ? (qb(A, A.u + C.g), (E[B] = C.value), (A = 0)) : (qb(A, A.u + C.g - 256), x(256 <= C.value), (A = C.value)); 0 == A && (y = true); } else A = ua(w.G[0], w.H[0], l); if (l.h) break; if (y || 256 > A) { if (!y) if (w.nd) b[n] = (w.qb | (A << 8)) >>> 0; else { Sa(l); y = ua(w.G[1], w.H[1], l); Sa(l); E = ua(w.G[2], w.H[2], l); B = ua(w.G[3], w.H[3], l); if (l.h) break; b[n] = ((B << 24) | (y << 16) | (A << 8) | E) >>> 0; } y = false; ++n; ++k; if ( k >= d && ((k = 0), ++h, null != g && h <= f && !(h % 16) && g(a, h), null != p) ) for (; r < n; ) (A = b[r++]), (p.X[((506832829 * A) & 4294967295) >>> p.Mb] = A); } else if (280 > A) { A = ib(A - 256, l); E = ua(w.G[4], w.H[4], l); Sa(l); E = ib(E, l); E = nc(d, E); if (l.h) break; if (n - c < E || e - n < A) break a; else for (B = 0; B < A; ++B) b[n + B] = b[n + B - E]; n += A; for (k += A; k >= d; ) (k -= d), ++h, null != g && h <= f && !(h % 16) && g(a, h); x(n <= e); k & u && (w = ha(m, k, h)); if (null != p) for (; r < n; ) (A = b[r++]), (p.X[((506832829 * A) & 4294967295) >>> p.Mb] = A); } else if (A < t) { y = A - 280; for (x(null != p); r < n; ) (A = b[r++]), (p.X[((506832829 * A) & 4294967295) >>> p.Mb] = A); A = n; E = p; x(!(y >>> E.Xa)); b[A] = E.X[y]; y = true; } else break a; y || x(l.h == db(l)); } if (a.Pb && l.h && n < e) x(a.m.h), (a.a = 5), (a.m = a.wd), (a.$ = a.xd), 0 < a.s.ua && $b(a.s.vb, a.s.Wa); else if (l.h) break a; else null != g && g(a, h > f ? f : h), (a.a = 0), (a.$ = n - c); return 1; } a.a = 3; return 0; } function Ac(a) { x(null != a); a.vc = null; a.yc = null; a.Ya = null; var b = a.Wa; null != b && (b.X = null); a.vb = null; x(null != a); } function Bc() { var a = new je(); if (null == a) return null; a.a = 0; a.xb = Cc; gb("Predictor", "VP8LPredictors"); gb("Predictor", "VP8LPredictors_C"); gb("PredictorAdd", "VP8LPredictorsAdd"); gb("PredictorAdd", "VP8LPredictorsAdd_C"); pc = Kd; Fb = Ld; sc = Md; Gb = Nd; Hb = Od; xc = Pd; uc = Qd; self.VP8LMapColor32b = ke; self.VP8LMapColor8b = le; return a; } function rb(a, b, c, d, e) { var f = 1, g = [a], h = [b], k = d.m, l = d.s, m = null, n = 0; a: for (;;) { if (c) for (; f && D(k, 1); ) { var r = g, q = h, t = d, v = 1, p = t.m, u = t.gc[t.ab], w = D(p, 2); if (t.Oc & (1 << w)) f = 0; else { t.Oc |= 1 << w; u.hc = w; u.Ea = r[0]; u.nc = q[0]; u.K = [null]; ++t.ab; x(4 >= t.ab); switch (w) { case 0: case 1: u.b = D(p, 3) + 2; v = rb(xa(u.Ea, u.b), xa(u.nc, u.b), 0, t, u.K); u.K = u.K[0]; break; case 3: var y = D(p, 8) + 1, A = 16 < y ? 0 : 4 < y ? 1 : 2 < y ? 2 : 3; r[0] = xa(u.Ea, A); u.b = A; var v = rb(y, 1, 0, t, u.K), E; if ((E = v)) { var B, C = y, N = u, z = 1 << (8 >> N.b), Q = V(z); if (null == Q) E = 0; else { var S = N.K[0], K = N.w; Q[0] = N.K[0][0]; for (B = 1; B < 1 * C; ++B) Q[B] = yb(S[K + B], Q[B - 1]); for (; B < 4 * z; ++B) Q[B] = 0; N.K[0] = null; N.K[0] = Q; E = 1; } } v = E; break; case 2: break; default: x(0); } f = v; } } g = g[0]; h = h[0]; if (f && D(k, 1) && ((n = D(k, 4)), (f = 1 <= n && 11 >= n), !f)) { d.a = 3; break a; } var H; if ((H = f)) b: { var F = d, G = g, L = h, J = n, T = c, Da, ba, X = F.m, R = F.s, P = [null], U, W = 1, aa = 0, na = me[J]; c: for (;;) { if (T && D(X, 1)) { var ca = D(X, 3) + 2, ga = xa(G, ca), ka = xa(L, ca), qa = ga * ka; if (!rb(ga, ka, 0, F, P)) break c; P = P[0]; R.xc = ca; for (Da = 0; Da < qa; ++Da) { var ia = (P[Da] >> 8) & 65535; P[Da] = ia; ia >= W && (W = ia + 1); } } if (X.h) break c; for (ba = 0; 5 > ba; ++ba) { var Y = Dc[ba]; !ba && 0 < J && (Y += 1 << J); aa < Y && (aa = Y); } var ma = wa(W * na, O); var ua = W, va = wa(ua, Ub); if (null == va) var la = null; else x(65536 >= ua), (la = va); var ha = V(aa); if (null == la || null == ha || null == ma) { F.a = 1; break c; } var pa = ma; for (Da = U = 0; Da < W; ++Da) { var ja = la[Da], da = ja.G, ea = ja.H, Fa = 0, ra = 1, Ha = 0; for (ba = 0; 5 > ba; ++ba) { Y = Dc[ba]; da[ba] = pa; ea[ba] = U; !ba && 0 < J && (Y += 1 << J); d: { var sa, za = Y, ta = F, oa = ha, db = pa, eb = U, Ia = 0, Ka = ta.m, fb = D(Ka, 1); M(oa, 0, 0, za); if (fb) { var gb = D(Ka, 1) + 1, hb = D(Ka, 1), Ja = D(Ka, 0 == hb ? 1 : 8); oa[Ja] = 1; 2 == gb && ((Ja = D(Ka, 8)), (oa[Ja] = 1)); var ya = 1; } else { var Ua = V(19), Va = D(Ka, 4) + 4; if (19 < Va) { ta.a = 3; var Aa = 0; break d; } for (sa = 0; sa < Va; ++sa) Ua[ne[sa]] = D(Ka, 3); var Ba = void 0, sb = void 0, Wa = ta, ib = Ua, Ca = za, Xa = oa, Oa = 0, La = Wa.m, Ya = 8, Za = wa(128, O); e: for (;;) { if (!Z(Za, 0, 7, ib, 19)) break e; if (D(La, 1)) { var kb = 2 + 2 * D(La, 3), Ba = 2 + D(La, kb); if (Ba > Ca) break e; } else Ba = Ca; for (sb = 0; sb < Ca && Ba--; ) { Sa(La); var $a = Za[0 + (pb(La) & 127)]; qb(La, La.u + $a.g); var jb = $a.value; if (16 > jb) (Xa[sb++] = jb), 0 != jb && (Ya = jb); else { var lb = 16 == jb, ab = jb - 16, mb = oe[ab], bb = D(La, pe[ab]) + mb; if (sb + bb > Ca) break e; else for (var nb = lb ? Ya : 0; 0 < bb--; ) Xa[sb++] = nb; } } Oa = 1; break e; } Oa || (Wa.a = 3); ya = Oa; } (ya = ya && !Ka.h) && (Ia = Z(db, eb, 8, oa, za)); ya && 0 != Ia ? (Aa = Ia) : ((ta.a = 3), (Aa = 0)); } if (0 == Aa) break c; ra && 1 == qe[ba] && (ra = 0 == pa[U].g); Fa += pa[U].g; U += Aa; if (3 >= ba) { var Pa = ha[0], tb; for (tb = 1; tb < Y; ++tb) ha[tb] > Pa && (Pa = ha[tb]); Ha += Pa; } } ja.nd = ra; ja.Qb = 0; ra && ((ja.qb = ((da[3][ea[3] + 0].value << 24) | (da[1][ea[1] + 0].value << 16) | da[2][ea[2] + 0].value) >>> 0), 0 == Fa && 256 > da[0][ea[0] + 0].value && ((ja.Qb = 1), (ja.qb += da[0][ea[0] + 0].value << 8))); ja.jc = !ja.Qb && 6 > Ha; if (ja.jc) { var Ga, Ea = ja; for (Ga = 0; Ga < xb; ++Ga) { var Ma = Ga, Na = Ea.pd[Ma], vb = Ea.G[0][Ea.H[0] + Ma]; 256 <= vb.value ? ((Na.g = vb.g + 256), (Na.value = vb.value)) : ((Na.g = 0), (Na.value = 0), (Ma >>= ub(vb, 8, Na)), (Ma >>= ub(Ea.G[1][Ea.H[1] + Ma], 16, Na)), (Ma >>= ub(Ea.G[2][Ea.H[2] + Ma], 0, Na)), ub(Ea.G[3][Ea.H[3] + Ma], 24, Na)); } } } R.vc = P; R.Wb = W; R.Ya = la; R.yc = ma; H = 1; break b; } H = 0; } f = H; if (!f) { d.a = 3; break a; } if (0 < n) { if (((l.ua = 1 << n), !Zb(l.Wa, n))) { d.a = 1; f = 0; break a; } } else l.ua = 0; var Qa = d, cb = g, ob = h, Ra = Qa.s, Ta = Ra.xc; Qa.c = cb; Qa.i = ob; Ra.md = xa(cb, Ta); Ra.wc = 0 == Ta ? -1 : (1 << Ta) - 1; if (c) { d.xb = re; break a; } m = V(g * h); if (null == m) { d.a = 1; f = 0; break a; } f = (f = Jb(d, m, 0, g, h, h, null)) && !k.h; break a; } f ? (null != e ? (e[0] = m) : (x(null == m), x(c)), (d.$ = 0), c || Ac(l)) : Ac(l); return f; } function Ec(a, b) { var c = a.c * a.i, d = c + b + 16 * b; x(a.c <= b); a.V = V(d); if (null == a.V) return (a.Ta = null), (a.Ua = 0), (a.a = 1), 0; a.Ta = a.V; a.Ua = a.Ba + c + b; return 1; } function se(a, b) { var c = a.C, d = b - c, e = a.V, f = a.Ba + a.c * c; for (x(b <= a.l.o); 0 < d; ) { var g = 16 < d ? 16 : d, h = a.l.ma, k = a.l.width, l = k * g, m = h.ca, n = h.tb + k * c, r = a.Ta, q = a.Ua; oc(a, g, e, f); Fc(r, q, m, n, l); zc(h, c, c + g, m, n, k); d -= g; e += g * a.c; c += g; } x(c == b); a.C = a.Ma = b; } function te(a, b) { var c = [0], d = [0], e = [0]; a: for (;;) { if (null == a) return 0; if (null == b) return (a.a = 2), 0; a.l = b; a.a = 0; cb(a.m, b.data, b.w, b.ha); if (!mc(a.m, c, d, e)) { a.a = 3; break a; } a.xb = Cc; b.width = c[0]; b.height = d[0]; if (!rb(c[0], d[0], 1, a, null)) break a; return 1; } x(0 != a.a); return 0; } function ue() { this.ub = this.yd = this.td = this.Rb = 0; } function ve() { this.Kd = this.Ld = this.Ud = this.Td = this.i = this.c = 0; } function we() { this.Fb = this.Bb = this.Cb = 0; this.Zb = V(4); this.Lb = V(4); } function Gc() { this.Yb = wb(); } function xe() { this.jb = V(3); this.Wc = Ed([4, 8], Gc); this.Xc = Ed([4, 17], Gc); } function ye() { this.Pc = this.wb = this.Tb = this.zd = 0; this.vd = new V(4); this.od = new V(4); } function Xa() { this.ld = this.La = this.dd = this.tc = 0; } function Hc() { this.Na = this.la = 0; } function ze() { this.Sc = [0, 0]; this.Eb = [0, 0]; this.Qc = [0, 0]; this.ia = this.lc = 0; } function Kb() { this.ad = V(384); this.Za = 0; this.Ob = V(16); this.$b = this.Ad = this.ia = this.Gc = this.Hc = this.Dd = 0; } function Ae() { this.uc = this.M = this.Nb = 0; this.wa = Array(new Xa()); this.Y = 0; this.ya = Array(new Kb()); this.aa = 0; this.l = new Oa(); } function Ic() { this.y = V(16); this.f = V(8); this.ea = V(8); } function Be() { this.cb = this.a = 0; this.sc = ""; this.m = new Wb(); this.Od = new ue(); this.Kc = new ve(); this.ed = new ye(); this.Qa = new we(); this.Ic = this.$c = this.Aa = 0; this.D = new Ae(); this.Xb = this.Va = this.Hb = this.zb = this.yb = this.Ub = this.za = 0; this.Jc = wa(8, Wb); this.ia = 0; this.pb = wa(4, ze); this.Pa = new xe(); this.Bd = this.kc = 0; this.Ac = []; this.Bc = 0; this.zc = [0, 0, 0, 0]; this.Gd = Array(new Ic()); this.Hd = 0; this.rb = Array(new Hc()); this.sb = 0; this.wa = Array(new Xa()); this.Y = 0; this.oc = []; this.pc = 0; this.sa = []; this.ta = 0; this.qa = []; this.ra = 0; this.Ha = []; this.B = this.R = this.Ia = 0; this.Ec = []; this.M = this.ja = this.Vb = this.Fc = 0; this.ya = Array(new Kb()); this.L = this.aa = 0; this.gd = Ed([4, 2], Xa); this.ga = null; this.Fa = []; this.Cc = this.qc = this.P = 0; this.Gb = []; this.Uc = 0; this.mb = []; this.nb = 0; this.rc = []; this.Ga = this.Vc = 0; } function ga(a, b) { return 0 > a ? 0 : a > b ? b : a; } function Oa() { this.T = this.U = this.ka = this.height = this.width = 0; this.y = []; this.f = []; this.ea = []; this.Rc = this.fa = this.W = this.N = this.O = 0; this.ma = "void"; this.put = "VP8IoPutHook"; this.ac = "VP8IoSetupHook"; this.bc = "VP8IoTeardownHook"; this.ha = this.Kb = 0; this.data = []; this.hb = this.ib = this.da = this.o = this.j = this.va = this.v = this.Da = this.ob = this.w = 0; this.F = []; this.J = 0; } function Ce() { var a = new Be(); null != a && ((a.a = 0), (a.sc = "OK"), (a.cb = 0), (a.Xb = 0), oa || (oa = De)); return a; } function T(a, b, c) { 0 == a.a && ((a.a = b), (a.sc = c), (a.cb = 0)); return 0; } function Jc(a, b, c) { return 3 <= c && 157 == a[b + 0] && 1 == a[b + 1] && 42 == a[b + 2]; } function Kc(a, b) { if (null == a) return 0; a.a = 0; a.sc = "OK"; if (null == b) return T(a, 2, "null VP8Io passed to VP8GetHeaders()"); var c = b.data; var d = b.w; var e = b.ha; if (4 > e) return T(a, 7, "Truncated header."); var f = c[d + 0] | (c[d + 1] << 8) | (c[d + 2] << 16); var g = a.Od; g.Rb = !(f & 1); g.td = (f >> 1) & 7; g.yd = (f >> 4) & 1; g.ub = f >> 5; if (3 < g.td) return T(a, 3, "Incorrect keyframe parameters."); if (!g.yd) return T(a, 4, "Frame not displayable."); d += 3; e -= 3; var h = a.Kc; if (g.Rb) { if (7 > e) return T(a, 7, "cannot parse picture header"); if (!Jc(c, d, e)) return T(a, 3, "Bad code word"); h.c = ((c[d + 4] << 8) | c[d + 3]) & 16383; h.Td = c[d + 4] >> 6; h.i = ((c[d + 6] << 8) | c[d + 5]) & 16383; h.Ud = c[d + 6] >> 6; d += 7; e -= 7; a.za = (h.c + 15) >> 4; a.Ub = (h.i + 15) >> 4; b.width = h.c; b.height = h.i; b.Da = 0; b.j = 0; b.v = 0; b.va = b.width; b.o = b.height; b.da = 0; b.ib = b.width; b.hb = b.height; b.U = b.width; b.T = b.height; f = a.Pa; M(f.jb, 0, 255, f.jb.length); f = a.Qa; x(null != f); f.Cb = 0; f.Bb = 0; f.Fb = 1; M(f.Zb, 0, 0, f.Zb.length); M(f.Lb, 0, 0, f.Lb); } if (g.ub > e) return T(a, 7, "bad partition length"); f = a.m; ma(f, c, d, g.ub); d += g.ub; e -= g.ub; g.Rb && ((h.Ld = G(f)), (h.Kd = G(f))); h = a.Qa; var k = a.Pa, l; x(null != f); x(null != h); h.Cb = G(f); if (h.Cb) { h.Bb = G(f); if (G(f)) { h.Fb = G(f); for (l = 0; 4 > l; ++l) h.Zb[l] = G(f) ? ca(f, 7) : 0; for (l = 0; 4 > l; ++l) h.Lb[l] = G(f) ? ca(f, 6) : 0; } if (h.Bb) for (l = 0; 3 > l; ++l) k.jb[l] = G(f) ? na(f, 8) : 255; } else h.Bb = 0; if (f.Ka) return T(a, 3, "cannot parse segment header"); h = a.ed; h.zd = G(f); h.Tb = na(f, 6); h.wb = na(f, 3); h.Pc = G(f); if (h.Pc && G(f)) { for (k = 0; 4 > k; ++k) G(f) && (h.vd[k] = ca(f, 6)); for (k = 0; 4 > k; ++k) G(f) && (h.od[k] = ca(f, 6)); } a.L = 0 == h.Tb ? 0 : h.zd ? 1 : 2; if (f.Ka) return T(a, 3, "cannot parse filter header"); l = d; var m = e; e = l; d = l + m; h = m; a.Xb = (1 << na(a.m, 2)) - 1; k = a.Xb; if (m < 3 * k) c = 7; else { l += 3 * k; h -= 3 * k; for (m = 0; m < k; ++m) { var n = c[e + 0] | (c[e + 1] << 8) | (c[e + 2] << 16); n > h && (n = h); ma(a.Jc[+m], c, l, n); l += n; h -= n; e += 3; } ma(a.Jc[+k], c, l, h); c = l < d ? 0 : 5; } if (0 != c) return T(a, c, "cannot parse partitions"); l = a.m; c = na(l, 7); e = G(l) ? ca(l, 4) : 0; d = G(l) ? ca(l, 4) : 0; h = G(l) ? ca(l, 4) : 0; k = G(l) ? ca(l, 4) : 0; l = G(l) ? ca(l, 4) : 0; m = a.Qa; for (n = 0; 4 > n; ++n) { if (m.Cb) { var r = m.Zb[n]; m.Fb || (r += c); } else if (0 < n) { a.pb[n] = a.pb[0]; continue; } else r = c; var q = a.pb[n]; q.Sc[0] = Lb[ga(r + e, 127)]; q.Sc[1] = Mb[ga(r + 0, 127)]; q.Eb[0] = 2 * Lb[ga(r + d, 127)]; q.Eb[1] = (101581 * Mb[ga(r + h, 127)]) >> 16; 8 > q.Eb[1] && (q.Eb[1] = 8); q.Qc[0] = Lb[ga(r + k, 117)]; q.Qc[1] = Mb[ga(r + l, 127)]; q.lc = r + l; } if (!g.Rb) return T(a, 4, "Not a key frame."); G(f); g = a.Pa; for (c = 0; 4 > c; ++c) { for (e = 0; 8 > e; ++e) for (d = 0; 3 > d; ++d) for (h = 0; 11 > h; ++h) (k = K(f, Ee[c][e][d][h]) ? na(f, 8) : Fe[c][e][d][h]), (g.Wc[c][e].Yb[d][h] = k); for (e = 0; 17 > e; ++e) g.Xc[c][e] = g.Wc[c][Ge[e]]; } a.kc = G(f); a.kc && (a.Bd = na(f, 8)); return (a.cb = 1); } function De(a, b, c, d, e, f, g) { var h = b[e].Yb[c]; for (c = 0; 16 > e; ++e) { if (!K(a, h[c + 0])) return e; for (; !K(a, h[c + 1]); ) if (((h = b[++e].Yb[0]), (c = 0), 16 == e)) return 16; var k = b[e + 1].Yb; if (K(a, h[c + 2])) { var l = a, m = h, n = c; var r = 0; if (K(l, m[n + 3])) if (K(l, m[n + 6])) { h = 0; r = K(l, m[n + 8]); m = K(l, m[n + 9 + r]); n = 2 * r + m; r = 0; for (m = He[n]; m[h]; ++h) r += r + K(l, m[h]); r += 3 + (8 << n); } else K(l, m[n + 7]) ? ((r = 7 + 2 * K(l, 165)), (r += K(l, 145))) : (r = 5 + K(l, 159)); else K(l, m[n + 4]) ? (r = 3 + K(l, m[n + 5])) : (r = 2); h = k[2]; } else (r = 1), (h = k[1]); k = g + Ie[e]; l = a; 0 > l.b && Qa(l); var m = l.b, n = l.Ca >> 1, q = (n - (l.I >> m)) >> 31; --l.b; l.Ca += q; l.Ca |= 1; l.I -= ((n + 1) & q) << m; f[k] = ((r ^ q) - q) * d[(0 < e) + 0]; } return 16; } function Lc(a) { var b = a.rb[a.sb - 1]; b.la = 0; b.Na = 0; M(a.zc, 0, 0, a.zc.length); a.ja = 0; } function Je(a, b) { for (a.M = 0; a.M < a.Va; ++a.M) { var c = a.Jc[a.M & a.Xb], d = a.m, e = a, f; for (f = 0; f < e.za; ++f) { var g = d; var h = e; var k = h.Ac, l = h.Bc + 4 * f, m = h.zc, n = h.ya[h.aa + f]; h.Qa.Bb ? (n.$b = K(g, h.Pa.jb[0]) ? 2 + K(g, h.Pa.jb[2]) : K(g, h.Pa.jb[1])) : (n.$b = 0); h.kc && (n.Ad = K(g, h.Bd)); n.Za = !K(g, 145) + 0; if (n.Za) { var r = n.Ob, q = 0; for (h = 0; 4 > h; ++h) { var t = m[0 + h]; var v; for (v = 0; 4 > v; ++v) { t = Ke[k[l + v]][t]; for (var p = Mc[K(g, t[0])]; 0 < p; ) p = Mc[2 * p + K(g, t[p])]; t = -p; k[l + v] = t; } I(r, q, k, l, 4); q += 4; m[0 + h] = t; } } else (t = K(g, 156) ? (K(g, 128) ? 1 : 3) : K(g, 163) ? 2 : 0), (n.Ob[0] = t), M(k, l, t, 4), M(m, 0, t, 4); n.Dd = K(g, 142) ? (K(g, 114) ? (K(g, 183) ? 1 : 3) : 2) : 0; } if (e.m.Ka) return T(a, 7, "Premature end-of-partition0 encountered."); for (; a.ja < a.za; ++a.ja) { d = a; e = c; g = d.rb[d.sb - 1]; k = d.rb[d.sb + d.ja]; f = d.ya[d.aa + d.ja]; if ((l = d.kc ? f.Ad : 0)) (g.la = k.la = 0), f.Za || (g.Na = k.Na = 0), (f.Hc = 0), (f.Gc = 0), (f.ia = 0); else { var u, w, g = k, k = e, l = d.Pa.Xc, m = d.ya[d.aa + d.ja], n = d.pb[m.$b]; h = m.ad; r = 0; q = d.rb[d.sb - 1]; t = v = 0; M(h, r, 0, 384); if (m.Za) { var y = 0; var A = l[3]; } else { p = V(16); var E = g.Na + q.Na; E = oa(k, l[1], E, n.Eb, 0, p, 0); g.Na = q.Na = (0 < E) + 0; if (1 < E) Nc(p, 0, h, r); else { var B = (p[0] + 3) >> 3; for (p = 0; 256 > p; p += 16) h[r + p] = B; } y = 1; A = l[0]; } var C = g.la & 15; var N = q.la & 15; for (p = 0; 4 > p; ++p) { var z = N & 1; for (B = w = 0; 4 > B; ++B) (E = z + (C & 1)), (E = oa(k, A, E, n.Sc, y, h, r)), (z = E > y), (C = (C >> 1) | (z << 7)), (w = (w << 2) | (3 < E ? 3 : 1 < E ? 2 : 0 != h[r + 0])), (r += 16); C >>= 4; N = (N >> 1) | (z << 7); v = ((v << 8) | w) >>> 0; } A = C; y = N >> 4; for (u = 0; 4 > u; u += 2) { w = 0; C = g.la >> (4 + u); N = q.la >> (4 + u); for (p = 0; 2 > p; ++p) { z = N & 1; for (B = 0; 2 > B; ++B) (E = z + (C & 1)), (E = oa(k, l[2], E, n.Qc, 0, h, r)), (z = 0 < E), (C = (C >> 1) | (z << 3)), (w = (w << 2) | (3 < E ? 3 : 1 < E ? 2 : 0 != h[r + 0])), (r += 16); C >>= 2; N = (N >> 1) | (z << 5); } t |= w << (4 * u); A |= (C << 4) << u; y |= (N & 240) << u; } g.la = A; q.la = y; m.Hc = v; m.Gc = t; m.ia = t & 43690 ? 0 : n.ia; l = !(v | t); } 0 < d.L && ((d.wa[d.Y + d.ja] = d.gd[f.$b][f.Za]), (d.wa[d.Y + d.ja].La |= !l)); if (e.Ka) return T(a, 7, "Premature end-of-file encountered."); } Lc(a); c = a; d = b; e = 1; f = c.D; g = 0 < c.L && c.M >= c.zb && c.M <= c.Va; if (0 == c.Aa) a: { (f.M = c.M), (f.uc = g), Oc(c, f), (e = 1); w = c.D; f = w.Nb; t = Ya[c.L]; g = t * c.R; k = (t / 2) * c.B; p = 16 * f * c.R; B = 8 * f * c.B; l = c.sa; m = c.ta - g + p; n = c.qa; h = c.ra - k + B; r = c.Ha; q = c.Ia - k + B; C = w.M; N = 0 == C; v = C >= c.Va - 1; 2 == c.Aa && Oc(c, w); if (w.uc) for (E = c, z = E.D.M, x(E.D.uc), w = E.yb; w < E.Hb; ++w) { var Q = E; y = w; A = z; var S = Q.D, D = S.Nb; u = Q.R; var S = S.wa[S.Y + y], F = Q.sa, H = Q.ta + 16 * D * u + 16 * y, J = S.dd, G = S.tc; if (0 != G) if ((x(3 <= G), 1 == Q.L)) 0 < y && Pc(F, H, u, G + 4), S.La && Qc(F, H, u, G), 0 < A && Rc(F, H, u, G + 4), S.La && Sc(F, H, u, G); else { var L = Q.B, O = Q.qa, P = Q.ra + 8 * D * L + 8 * y, R = Q.Ha, Q = Q.Ia + 8 * D * L + 8 * y, D = S.ld; 0 < y && (Tc(F, H, u, G + 4, J, D), Uc(O, P, R, Q, L, G + 4, J, D)); S.La && (Vc(F, H, u, G, J, D), Wc(O, P, R, Q, L, G, J, D)); 0 < A && (Xc(F, H, u, G + 4, J, D), Yc(O, P, R, Q, L, G + 4, J, D)); S.La && (Zc(F, H, u, G, J, D), $c(O, P, R, Q, L, G, J, D)); } } c.ia && alert("todo:DitherRow"); if (null != d.put) { w = 16 * C; C = 16 * (C + 1); N ? ((d.y = c.sa), (d.O = c.ta + p), (d.f = c.qa), (d.N = c.ra + B), (d.ea = c.Ha), (d.W = c.Ia + B)) : ((w -= t), (d.y = l), (d.O = m), (d.f = n), (d.N = h), (d.ea = r), (d.W = q)); v || (C -= t); C > d.o && (C = d.o); d.F = null; d.J = null; if ( null != c.Fa && 0 < c.Fa.length && w < C && ((d.J = Le(c, d, w, C - w)), (d.F = c.mb), null == d.F && 0 == d.F.length) ) { e = T(c, 3, "Could not decode alpha data."); break a; } w < d.j && ((t = d.j - w), (w = d.j), x(!(t & 1)), (d.O += c.R * t), (d.N += c.B * (t >> 1)), (d.W += c.B * (t >> 1)), null != d.F && (d.J += d.width * t)); w < C && ((d.O += d.v), (d.N += d.v >> 1), (d.W += d.v >> 1), null != d.F && (d.J += d.v), (d.ka = w - d.j), (d.U = d.va - d.v), (d.T = C - w), (e = d.put(d))); } f + 1 != c.Ic || v || (I(c.sa, c.ta - g, l, m + 16 * c.R, g), I(c.qa, c.ra - k, n, h + 8 * c.B, k), I(c.Ha, c.Ia - k, r, q + 8 * c.B, k)); } if (!e) return T(a, 6, "Output aborted."); } return 1; } function Me(a, b) { if (null == a) return 0; if (null == b) return T(a, 2, "NULL VP8Io parameter in VP8Decode()."); if (!a.cb && !Kc(a, b)) return 0; x(a.cb); if (null == b.ac || b.ac(b)) { b.ob && (a.L = 0); var c = Ya[a.L]; 2 == a.L ? ((a.yb = 0), (a.zb = 0)) : ((a.yb = (b.v - c) >> 4), (a.zb = (b.j - c) >> 4), 0 > a.yb && (a.yb = 0), 0 > a.zb && (a.zb = 0)); a.Va = (b.o + 15 + c) >> 4; a.Hb = (b.va + 15 + c) >> 4; a.Hb > a.za && (a.Hb = a.za); a.Va > a.Ub && (a.Va = a.Ub); if (0 < a.L) { var d = a.ed; for (c = 0; 4 > c; ++c) { var e; if (a.Qa.Cb) { var f = a.Qa.Lb[c]; a.Qa.Fb || (f += d.Tb); } else f = d.Tb; for (e = 0; 1 >= e; ++e) { var g = a.gd[c][e], h = f; d.Pc && ((h += d.vd[0]), e && (h += d.od[0])); h = 0 > h ? 0 : 63 < h ? 63 : h; if (0 < h) { var k = h; 0 < d.wb && ((k = 4 < d.wb ? k >> 2 : k >> 1), k > 9 - d.wb && (k = 9 - d.wb)); 1 > k && (k = 1); g.dd = k; g.tc = 2 * h + k; g.ld = 40 <= h ? 2 : 15 <= h ? 1 : 0; } else g.tc = 0; g.La = e; } } } c = 0; } else T(a, 6, "Frame setup failed"), (c = a.a); if ((c = 0 == c)) { if (c) { a.$c = 0; 0 < a.Aa || (a.Ic = Ne); b: { c = a.Ic; var k = a.za, d = 4 * k, l = 32 * k, m = k + 1, n = 0 < a.L ? k * (0 < a.Aa ? 2 : 1) : 0, r = (2 == a.Aa ? 2 : 1) * k; e = ((3 * (16 * c + Ya[a.L])) / 2) * l; f = null != a.Fa && 0 < a.Fa.length ? a.Kc.c * a.Kc.i : 0; g = d + 832 + e + f; if (g != g) c = 0; else { if (g > a.Vb) { a.Vb = 0; a.Ec = V(g); a.Fc = 0; if (null == a.Ec) { c = T(a, 1, "no memory during frame initialization."); break b; } a.Vb = g; } g = a.Ec; h = a.Fc; a.Ac = g; a.Bc = h; h += d; a.Gd = wa(l, Ic); a.Hd = 0; a.rb = wa(m + 1, Hc); a.sb = 1; a.wa = n ? wa(n, Xa) : null; a.Y = 0; a.D.Nb = 0; a.D.wa = a.wa; a.D.Y = a.Y; 0 < a.Aa && (a.D.Y += k); x(true); a.oc = g; a.pc = h; h += 832; a.ya = wa(r, Kb); a.aa = 0; a.D.ya = a.ya; a.D.aa = a.aa; 2 == a.Aa && (a.D.aa += k); a.R = 16 * k; a.B = 8 * k; l = Ya[a.L]; k = l * a.R; l = (l / 2) * a.B; a.sa = g; a.ta = h + k; a.qa = a.sa; a.ra = a.ta + 16 * c * a.R + l; a.Ha = a.qa; a.Ia = a.ra + 8 * c * a.B + l; a.$c = 0; h += e; a.mb = f ? g : null; a.nb = f ? h : null; x(h + f <= a.Fc + a.Vb); Lc(a); M(a.Ac, a.Bc, 0, d); c = 1; } } if (c) { b.ka = 0; b.y = a.sa; b.O = a.ta; b.f = a.qa; b.N = a.ra; b.ea = a.Ha; b.Vd = a.Ia; b.fa = a.R; b.Rc = a.B; b.F = null; b.J = 0; if (!ad) { for (c = -255; 255 >= c; ++c) bd[255 + c] = 0 > c ? -c : c; for (c = -1020; 1020 >= c; ++c) cd[1020 + c] = -128 > c ? -128 : 127 < c ? 127 : c; for (c = -112; 112 >= c; ++c) dd[112 + c] = -16 > c ? -16 : 15 < c ? 15 : c; for (c = -255; 510 >= c; ++c) ed[255 + c] = 0 > c ? 0 : 255 < c ? 255 : c; ad = 1; } Nc = Oe; Za = Pe; Nb = Qe; pa = Re; Ob = Se; fd = Te; Xc = Ue; Tc = Ve; Yc = We; Uc = Xe; Zc = Ye; Vc = Ze; $c = $e; Wc = af; Rc = gd; Pc = hd; Sc = bf; Qc = cf; W[0] = df; W[1] = ef; W[2] = ff; W[3] = gf; W[4] = hf; W[5] = jf; W[6] = kf; W[7] = lf; W[8] = mf; W[9] = nf; Y[0] = of; Y[1] = pf; Y[2] = qf; Y[3] = rf; Y[4] = sf; Y[5] = tf; Y[6] = uf; ka[0] = vf; ka[1] = wf; ka[2] = xf; ka[3] = yf; ka[4] = zf; ka[5] = Af; ka[6] = Bf; c = 1; } else c = 0; } c && (c = Je(a, b)); null != b.bc && b.bc(b); c &= 1; } if (!c) return 0; a.cb = 0; return c; } function qa(a, b, c, d, e) { e = a[b + c + 32 * d] + (e >> 3); a[b + c + 32 * d] = e & -256 ? (0 > e ? 0 : 255) : e; } function kb(a, b, c, d, e, f) { qa(a, b, 0, c, d + e); qa(a, b, 1, c, d + f); qa(a, b, 2, c, d - f); qa(a, b, 3, c, d - e); } function da(a) { return ((20091 * a) >> 16) + a; } function id(a, b, c, d) { var e = 0, f; var g = V(16); for (f = 0; 4 > f; ++f) { var h = a[b + 0] + a[b + 8]; var k = a[b + 0] - a[b + 8]; var l = ((35468 * a[b + 4]) >> 16) - da(a[b + 12]); var m = da(a[b + 4]) + ((35468 * a[b + 12]) >> 16); g[e + 0] = h + m; g[e + 1] = k + l; g[e + 2] = k - l; g[e + 3] = h - m; e += 4; b++; } for (f = e = 0; 4 > f; ++f) (a = g[e + 0] + 4), (h = a + g[e + 8]), (k = a - g[e + 8]), (l = ((35468 * g[e + 4]) >> 16) - da(g[e + 12])), (m = da(g[e + 4]) + ((35468 * g[e + 12]) >> 16)), qa(c, d, 0, 0, h + m), qa(c, d, 1, 0, k + l), qa(c, d, 2, 0, k - l), qa(c, d, 3, 0, h - m), e++, (d += 32); } function Te(a, b, c, d) { var e = a[b + 0] + 4, f = (35468 * a[b + 4]) >> 16, g = da(a[b + 4]), h = (35468 * a[b + 1]) >> 16; a = da(a[b + 1]); kb(c, d, 0, e + g, a, h); kb(c, d, 1, e + f, a, h); kb(c, d, 2, e - f, a, h); kb(c, d, 3, e - g, a, h); } function Pe(a, b, c, d, e) { id(a, b, c, d); e && id(a, b + 16, c, d + 4); } function Qe(a, b, c, d) { Za(a, b + 0, c, d, 1); Za(a, b + 32, c, d + 128, 1); } function Re(a, b, c, d) { a = a[b + 0] + 4; var e; for (e = 0; 4 > e; ++e) for (b = 0; 4 > b; ++b) qa(c, d, b, e, a); } function Se(a, b, c, d) { a[b + 0] && pa(a, b + 0, c, d); a[b + 16] && pa(a, b + 16, c, d + 4); a[b + 32] && pa(a, b + 32, c, d + 128); a[b + 48] && pa(a, b + 48, c, d + 128 + 4); } function Oe(a, b, c, d) { var e = V(16), f; for (f = 0; 4 > f; ++f) { var g = a[b + 0 + f] + a[b + 12 + f]; var h = a[b + 4 + f] + a[b + 8 + f]; var k = a[b + 4 + f] - a[b + 8 + f]; var l = a[b + 0 + f] - a[b + 12 + f]; e[0 + f] = g + h; e[8 + f] = g - h; e[4 + f] = l + k; e[12 + f] = l - k; } for (f = 0; 4 > f; ++f) (a = e[0 + 4 * f] + 3), (g = a + e[3 + 4 * f]), (h = e[1 + 4 * f] + e[2 + 4 * f]), (k = e[1 + 4 * f] - e[2 + 4 * f]), (l = a - e[3 + 4 * f]), (c[d + 0] = (g + h) >> 3), (c[d + 16] = (l + k) >> 3), (c[d + 32] = (g - h) >> 3), (c[d + 48] = (l - k) >> 3), (d += 64); } function Pb(a, b, c) { var d = b - 32, e = R, f = 255 - a[d - 1], g; for (g = 0; g < c; ++g) { var h = e, k = f + a[b - 1], l; for (l = 0; l < c; ++l) a[b + l] = h[k + a[d + l]]; b += 32; } } function ef(a, b) { Pb(a, b, 4); } function wf(a, b) { Pb(a, b, 8); } function pf(a, b) { Pb(a, b, 16); } function qf(a, b) { var c; for (c = 0; 16 > c; ++c) I(a, b + 32 * c, a, b - 32, 16); } function rf(a, b) { var c; for (c = 16; 0 < c; --c) M(a, b, a[b - 1], 16), (b += 32); } function $a(a, b, c) { var d; for (d = 0; 16 > d; ++d) M(b, c + 32 * d, a, 16); } function of(a, b) { var c = 16, d; for (d = 0; 16 > d; ++d) c += a[b - 1 + 32 * d] + a[b + d - 32]; $a(c >> 5, a, b); } function sf(a, b) { var c = 8, d; for (d = 0; 16 > d; ++d) c += a[b - 1 + 32 * d]; $a(c >> 4, a, b); } function tf(a, b) { var c = 8, d; for (d = 0; 16 > d; ++d) c += a[b + d - 32]; $a(c >> 4, a, b); } function uf(a, b) { $a(128, a, b); } function z(a, b, c) { return (a + 2 * b + c + 2) >> 2; } function ff(a, b) { var c = b - 32, c = new Uint8Array([ z(a[c - 1], a[c + 0], a[c + 1]), z(a[c + 0], a[c + 1], a[c + 2]), z(a[c + 1], a[c + 2], a[c + 3]), z(a[c + 2], a[c + 3], a[c + 4]) ]), d; for (d = 0; 4 > d; ++d) I(a, b + 32 * d, c, 0, c.length); } function gf(a, b) { var c = a[b - 1], d = a[b - 1 + 32], e = a[b - 1 + 64], f = a[b - 1 + 96]; ra(a, b + 0, 16843009 * z(a[b - 1 - 32], c, d)); ra(a, b + 32, 16843009 * z(c, d, e)); ra(a, b + 64, 16843009 * z(d, e, f)); ra(a, b + 96, 16843009 * z(e, f, f)); } function df(a, b) { var c = 4, d; for (d = 0; 4 > d; ++d) c += a[b + d - 32] + a[b - 1 + 32 * d]; c >>= 3; for (d = 0; 4 > d; ++d) M(a, b + 32 * d, c, 4); } function hf(a, b) { var c = a[b - 1 + 0], d = a[b - 1 + 32], e = a[b - 1 + 64], f = a[b - 1 - 32], g = a[b + 0 - 32], h = a[b + 1 - 32], k = a[b + 2 - 32], l = a[b + 3 - 32]; a[b + 0 + 96] = z(d, e, a[b - 1 + 96]); a[b + 1 + 96] = a[b + 0 + 64] = z(c, d, e); a[b + 2 + 96] = a[b + 1 + 64] = a[b + 0 + 32] = z(f, c, d); a[b + 3 + 96] = a[b + 2 + 64] = a[b + 1 + 32] = a[b + 0 + 0] = z(g, f, c); a[b + 3 + 64] = a[b + 2 + 32] = a[b + 1 + 0] = z(h, g, f); a[b + 3 + 32] = a[b + 2 + 0] = z(k, h, g); a[b + 3 + 0] = z(l, k, h); } function kf(a, b) { var c = a[b + 1 - 32], d = a[b + 2 - 32], e = a[b + 3 - 32], f = a[b + 4 - 32], g = a[b + 5 - 32], h = a[b + 6 - 32], k = a[b + 7 - 32]; a[b + 0 + 0] = z(a[b + 0 - 32], c, d); a[b + 1 + 0] = a[b + 0 + 32] = z(c, d, e); a[b + 2 + 0] = a[b + 1 + 32] = a[b + 0 + 64] = z(d, e, f); a[b + 3 + 0] = a[b + 2 + 32] = a[b + 1 + 64] = a[b + 0 + 96] = z(e, f, g); a[b + 3 + 32] = a[b + 2 + 64] = a[b + 1 + 96] = z(f, g, h); a[b + 3 + 64] = a[b + 2 + 96] = z(g, h, k); a[b + 3 + 96] = z(h, k, k); } function jf(a, b) { var c = a[b - 1 + 0], d = a[b - 1 + 32], e = a[b - 1 + 64], f = a[b - 1 - 32], g = a[b + 0 - 32], h = a[b + 1 - 32], k = a[b + 2 - 32], l = a[b + 3 - 32]; a[b + 0 + 0] = a[b + 1 + 64] = (f + g + 1) >> 1; a[b + 1 + 0] = a[b + 2 + 64] = (g + h + 1) >> 1; a[b + 2 + 0] = a[b + 3 + 64] = (h + k + 1) >> 1; a[b + 3 + 0] = (k + l + 1) >> 1; a[b + 0 + 96] = z(e, d, c); a[b + 0 + 64] = z(d, c, f); a[b + 0 + 32] = a[b + 1 + 96] = z(c, f, g); a[b + 1 + 32] = a[b + 2 + 96] = z(f, g, h); a[b + 2 + 32] = a[b + 3 + 96] = z(g, h, k); a[b + 3 + 32] = z(h, k, l); } function lf(a, b) { var c = a[b + 0 - 32], d = a[b + 1 - 32], e = a[b + 2 - 32], f = a[b + 3 - 32], g = a[b + 4 - 32], h = a[b + 5 - 32], k = a[b + 6 - 32], l = a[b + 7 - 32]; a[b + 0 + 0] = (c + d + 1) >> 1; a[b + 1 + 0] = a[b + 0 + 64] = (d + e + 1) >> 1; a[b + 2 + 0] = a[b + 1 + 64] = (e + f + 1) >> 1; a[b + 3 + 0] = a[b + 2 + 64] = (f + g + 1) >> 1; a[b + 0 + 32] = z(c, d, e); a[b + 1 + 32] = a[b + 0 + 96] = z(d, e, f); a[b + 2 + 32] = a[b + 1 + 96] = z(e, f, g); a[b + 3 + 32] = a[b + 2 + 96] = z(f, g, h); a[b + 3 + 64] = z(g, h, k); a[b + 3 + 96] = z(h, k, l); } function nf(a, b) { var c = a[b - 1 + 0], d = a[b - 1 + 32], e = a[b - 1 + 64], f = a[b - 1 + 96]; a[b + 0 + 0] = (c + d + 1) >> 1; a[b + 2 + 0] = a[b + 0 + 32] = (d + e + 1) >> 1; a[b + 2 + 32] = a[b + 0 + 64] = (e + f + 1) >> 1; a[b + 1 + 0] = z(c, d, e); a[b + 3 + 0] = a[b + 1 + 32] = z(d, e, f); a[b + 3 + 32] = a[b + 1 + 64] = z(e, f, f); a[b + 3 + 64] = a[b + 2 + 64] = a[b + 0 + 96] = a[b + 1 + 96] = a[ b + 2 + 96 ] = a[b + 3 + 96] = f; } function mf(a, b) { var c = a[b - 1 + 0], d = a[b - 1 + 32], e = a[b - 1 + 64], f = a[b - 1 + 96], g = a[b - 1 - 32], h = a[b + 0 - 32], k = a[b + 1 - 32], l = a[b + 2 - 32]; a[b + 0 + 0] = a[b + 2 + 32] = (c + g + 1) >> 1; a[b + 0 + 32] = a[b + 2 + 64] = (d + c + 1) >> 1; a[b + 0 + 64] = a[b + 2 + 96] = (e + d + 1) >> 1; a[b + 0 + 96] = (f + e + 1) >> 1; a[b + 3 + 0] = z(h, k, l); a[b + 2 + 0] = z(g, h, k); a[b + 1 + 0] = a[b + 3 + 32] = z(c, g, h); a[b + 1 + 32] = a[b + 3 + 64] = z(d, c, g); a[b + 1 + 64] = a[b + 3 + 96] = z(e, d, c); a[b + 1 + 96] = z(f, e, d); } function xf(a, b) { var c; for (c = 0; 8 > c; ++c) I(a, b + 32 * c, a, b - 32, 8); } function yf(a, b) { var c; for (c = 0; 8 > c; ++c) M(a, b, a[b - 1], 8), (b += 32); } function lb(a, b, c) { var d; for (d = 0; 8 > d; ++d) M(b, c + 32 * d, a, 8); } function vf(a, b) { var c = 8, d; for (d = 0; 8 > d; ++d) c += a[b + d - 32] + a[b - 1 + 32 * d]; lb(c >> 4, a, b); } function Af(a, b) { var c = 4, d; for (d = 0; 8 > d; ++d) c += a[b + d - 32]; lb(c >> 3, a, b); } function zf(a, b) { var c = 4, d; for (d = 0; 8 > d; ++d) c += a[b - 1 + 32 * d]; lb(c >> 3, a, b); } function Bf(a, b) { lb(128, a, b); } function ab(a, b, c) { var d = a[b - c], e = a[b + 0], f = 3 * (e - d) + Qb[1020 + a[b - 2 * c] - a[b + c]], g = mb[112 + ((f + 4) >> 3)]; a[b - c] = R[255 + d + mb[112 + ((f + 3) >> 3)]]; a[b + 0] = R[255 + e - g]; } function jd(a, b, c, d) { var e = a[b + 0], f = a[b + c]; return U[255 + a[b - 2 * c] - a[b - c]] > d || U[255 + f - e] > d; } function kd(a, b, c, d) { return ( 4 * U[255 + a[b - c] - a[b + 0]] + U[255 + a[b - 2 * c] - a[b + c]] <= d ); } function ld(a, b, c, d, e) { var f = a[b - 3 * c], g = a[b - 2 * c], h = a[b - c], k = a[b + 0], l = a[b + c], m = a[b + 2 * c], n = a[b + 3 * c]; return 4 * U[255 + h - k] + U[255 + g - l] > d ? 0 : U[255 + a[b - 4 * c] - f] <= e && U[255 + f - g] <= e && U[255 + g - h] <= e && U[255 + n - m] <= e && U[255 + m - l] <= e && U[255 + l - k] <= e; } function gd(a, b, c, d) { var e = 2 * d + 1; for (d = 0; 16 > d; ++d) kd(a, b + d, c, e) && ab(a, b + d, c); } function hd(a, b, c, d) { var e = 2 * d + 1; for (d = 0; 16 > d; ++d) kd(a, b + d * c, 1, e) && ab(a, b + d * c, 1); } function bf(a, b, c, d) { var e; for (e = 3; 0 < e; --e) (b += 4 * c), gd(a, b, c, d); } function cf(a, b, c, d) { var e; for (e = 3; 0 < e; --e) (b += 4), hd(a, b, c, d); } function ea(a, b, c, d, e, f, g, h) { for (f = 2 * f + 1; 0 < e--; ) { if (ld(a, b, c, f, g)) if (jd(a, b, c, h)) ab(a, b, c); else { var k = a, l = b, m = c, n = k[l - 2 * m], r = k[l - m], q = k[l + 0], t = k[l + m], v = k[l + 2 * m], p = Qb[1020 + 3 * (q - r) + Qb[1020 + n - t]], u = (27 * p + 63) >> 7, w = (18 * p + 63) >> 7, p = (9 * p + 63) >> 7; k[l - 3 * m] = R[255 + k[l - 3 * m] + p]; k[l - 2 * m] = R[255 + n + w]; k[l - m] = R[255 + r + u]; k[l + 0] = R[255 + q - u]; k[l + m] = R[255 + t - w]; k[l + 2 * m] = R[255 + v - p]; } b += d; } } function Fa(a, b, c, d, e, f, g, h) { for (f = 2 * f + 1; 0 < e--; ) { if (ld(a, b, c, f, g)) if (jd(a, b, c, h)) ab(a, b, c); else { var k = a, l = b, m = c, n = k[l - m], r = k[l + 0], q = k[l + m], t = 3 * (r - n), v = mb[112 + ((t + 4) >> 3)], t = mb[112 + ((t + 3) >> 3)], p = (v + 1) >> 1; k[l - 2 * m] = R[255 + k[l - 2 * m] + p]; k[l - m] = R[255 + n + t]; k[l + 0] = R[255 + r - v]; k[l + m] = R[255 + q - p]; } b += d; } } function Ue(a, b, c, d, e, f) { ea(a, b, c, 1, 16, d, e, f); } function Ve(a, b, c, d, e, f) { ea(a, b, 1, c, 16, d, e, f); } function Ye(a, b, c, d, e, f) { var g; for (g = 3; 0 < g; --g) (b += 4 * c), Fa(a, b, c, 1, 16, d, e, f); } function Ze(a, b, c, d, e, f) { var g; for (g = 3; 0 < g; --g) (b += 4), Fa(a, b, 1, c, 16, d, e, f); } function We(a, b, c, d, e, f, g, h) { ea(a, b, e, 1, 8, f, g, h); ea(c, d, e, 1, 8, f, g, h); } function Xe(a, b, c, d, e, f, g, h) { ea(a, b, 1, e, 8, f, g, h); ea(c, d, 1, e, 8, f, g, h); } function $e(a, b, c, d, e, f, g, h) { Fa(a, b + 4 * e, e, 1, 8, f, g, h); Fa(c, d + 4 * e, e, 1, 8, f, g, h); } function af(a, b, c, d, e, f, g, h) { Fa(a, b + 4, 1, e, 8, f, g, h); Fa(c, d + 4, 1, e, 8, f, g, h); } function Cf() { this.ba = new Cb(); this.ec = []; this.cc = []; this.Mc = []; this.Dc = this.Nc = this.dc = this.fc = 0; this.Oa = new Ud(); this.memory = 0; this.Ib = "OutputFunc"; this.Jb = "OutputAlphaFunc"; this.Nd = "OutputRowFunc"; } function md() { this.data = []; this.offset = this.kd = this.ha = this.w = 0; this.na = []; this.xa = this.gb = this.Ja = this.Sa = this.P = 0; } function Df() { this.nc = this.Ea = this.b = this.hc = 0; this.K = []; this.w = 0; } function Ef() { this.ua = 0; this.Wa = new ac(); this.vb = new ac(); this.md = this.xc = this.wc = 0; this.vc = []; this.Wb = 0; this.Ya = new Ub(); this.yc = new O(); } function je() { this.xb = this.a = 0; this.l = new Oa(); this.ca = new Cb(); this.V = []; this.Ba = 0; this.Ta = []; this.Ua = 0; this.m = new Ra(); this.Pb = 0; this.wd = new Ra(); this.Ma = this.$ = this.C = this.i = this.c = this.xd = 0; this.s = new Ef(); this.ab = 0; this.gc = wa(4, Df); this.Oc = 0; } function Ff() { this.Lc = this.Z = this.$a = this.i = this.c = 0; this.l = new Oa(); this.ic = 0; this.ca = []; this.tb = 0; this.qd = null; this.rd = 0; } function Rb(a, b, c, d, e, f, g) { a = null == a ? 0 : a[b + 0]; for (b = 0; b < g; ++b) (e[f + b] = (a + c[d + b]) & 255), (a = e[f + b]); } function Gf(a, b, c, d, e, f, g) { if (null == a) Rb(null, null, c, d, e, f, g); else { var h; for (h = 0; h < g; ++h) e[f + h] = (a[b + h] + c[d + h]) & 255; } } function Hf(a, b, c, d, e, f, g) { if (null == a) Rb(null, null, c, d, e, f, g); else { var h = a[b + 0], k = h, l = h, m; for (m = 0; m < g; ++m) (h = a[b + m]), (k = l + h - k), (l = (c[d + m] + (k & -256 ? (0 > k ? 0 : 255) : k)) & 255), (k = h), (e[f + m] = l); } } function Le(a, b, c, d) { var e = b.width, f = b.o; x(null != a && null != b); if (0 > c || 0 >= d || c + d > f) return null; if (!a.Cc) { if (null == a.ga) { a.ga = new Ff(); var g; (g = null == a.ga) || ((g = b.width * b.o), x(0 == a.Gb.length), (a.Gb = V(g)), (a.Uc = 0), null == a.Gb ? (g = 0) : ((a.mb = a.Gb), (a.nb = a.Uc), (a.rc = null), (g = 1)), (g = !g)); if (!g) { g = a.ga; var h = a.Fa, k = a.P, l = a.qc, m = a.mb, n = a.nb, r = k + 1, q = l - 1, t = g.l; x(null != h && null != m && null != b); ia[0] = null; ia[1] = Rb; ia[2] = Gf; ia[3] = Hf; g.ca = m; g.tb = n; g.c = b.width; g.i = b.height; x(0 < g.c && 0 < g.i); if (1 >= l) b = 0; else if ( ((g.$a = (h[k + 0] >> 0) & 3), (g.Z = (h[k + 0] >> 2) & 3), (g.Lc = (h[k + 0] >> 4) & 3), (k = (h[k + 0] >> 6) & 3), 0 > g.$a || 1 < g.$a || 4 <= g.Z || 1 < g.Lc || k) ) b = 0; else if ( ((t.put = kc), (t.ac = gc), (t.bc = lc), (t.ma = g), (t.width = b.width), (t.height = b.height), (t.Da = b.Da), (t.v = b.v), (t.va = b.va), (t.j = b.j), (t.o = b.o), g.$a) ) b: { x(1 == g.$a), (b = Bc()); c: for (;;) { if (null == b) { b = 0; break b; } x(null != g); g.mc = b; b.c = g.c; b.i = g.i; b.l = g.l; b.l.ma = g; b.l.width = g.c; b.l.height = g.i; b.a = 0; cb(b.m, h, r, q); if (!rb(g.c, g.i, 1, b, null)) break c; 1 == b.ab && 3 == b.gc[0].hc && yc(b.s) ? ((g.ic = 1), (h = b.c * b.i), (b.Ta = null), (b.Ua = 0), (b.V = V(h)), (b.Ba = 0), null == b.V ? ((b.a = 1), (b = 0)) : (b = 1)) : ((g.ic = 0), (b = Ec(b, g.c))); if (!b) break c; b = 1; break b; } g.mc = null; b = 0; } else b = q >= g.c * g.i; g = !b; } if (g) return null; 1 != a.ga.Lc ? (a.Ga = 0) : (d = f - c); } x(null != a.ga); x(c + d <= f); a: { h = a.ga; b = h.c; f = h.l.o; if (0 == h.$a) { r = a.rc; q = a.Vc; t = a.Fa; k = a.P + 1 + c * b; l = a.mb; m = a.nb + c * b; x(k <= a.P + a.qc); if (0 != h.Z) for (x(null != ia[h.Z]), g = 0; g < d; ++g) ia[h.Z](r, q, t, k, l, m, b), (r = l), (q = m), (m += b), (k += b); else for (g = 0; g < d; ++g) I(l, m, t, k, b), (r = l), (q = m), (m += b), (k += b); a.rc = r; a.Vc = q; } else { x(null != h.mc); b = c + d; g = h.mc; x(null != g); x(b <= g.i); if (g.C >= b) b = 1; else if ((h.ic || Aa(), h.ic)) { var h = g.V, r = g.Ba, q = g.c, v = g.i, t = 1, k = g.$ / q, l = g.$ % q, m = g.m, n = g.s, p = g.$, u = q * v, w = q * b, y = n.wc, A = p < w ? ha(n, l, k) : null; x(p <= u); x(b <= v); x(yc(n)); c: for (;;) { for (; !m.h && p < w; ) { l & y || (A = ha(n, l, k)); x(null != A); Sa(m); v = ua(A.G[0], A.H[0], m); if (256 > v) (h[r + p] = v), ++p, ++l, l >= q && ((l = 0), ++k, k <= b && !(k % 16) && Ib(g, k)); else if (280 > v) { var v = ib(v - 256, m); var E = ua(A.G[4], A.H[4], m); Sa(m); E = ib(E, m); E = nc(q, E); if (p >= E && u - p >= v) { var B; for (B = 0; B < v; ++B) h[r + p + B] = h[r + p + B - E]; } else { t = 0; break c; } p += v; for (l += v; l >= q; ) (l -= q), ++k, k <= b && !(k % 16) && Ib(g, k); p < w && l & y && (A = ha(n, l, k)); } else { t = 0; break c; } x(m.h == db(m)); } Ib(g, k > b ? b : k); break c; } !t || (m.h && p < u) ? ((t = 0), (g.a = m.h ? 5 : 3)) : (g.$ = p); b = t; } else b = Jb(g, g.V, g.Ba, g.c, g.i, b, se); if (!b) { d = 0; break a; } } c + d >= f && (a.Cc = 1); d = 1; } if (!d) return null; if ( a.Cc && ((d = a.ga), null != d && (d.mc = null), (a.ga = null), 0 < a.Ga) ) return alert("todo:WebPDequantizeLevels"), null; } return a.nb + c * e; } function If(a, b, c, d, e, f) { for (; 0 < e--; ) { var g = a, h = b + (c ? 1 : 0), k = a, l = b + (c ? 0 : 3), m; for (m = 0; m < d; ++m) { var n = k[l + 4 * m]; 255 != n && ((n *= 32897), (g[h + 4 * m + 0] = (g[h + 4 * m + 0] * n) >> 23), (g[h + 4 * m + 1] = (g[h + 4 * m + 1] * n) >> 23), (g[h + 4 * m + 2] = (g[h + 4 * m + 2] * n) >> 23)); } b += f; } } function Jf(a, b, c, d, e) { for (; 0 < d--; ) { var f; for (f = 0; f < c; ++f) { var g = a[b + 2 * f + 0], h = a[b + 2 * f + 1], k = h & 15, l = 4369 * k, h = (((h & 240) | (h >> 4)) * l) >> 16; a[b + 2 * f + 0] = (((((g & 240) | (g >> 4)) * l) >> 16) & 240) | ((((((g & 15) | (g << 4)) * l) >> 16) >> 4) & 15); a[b + 2 * f + 1] = (h & 240) | k; } b += e; } } function Kf(a, b, c, d, e, f, g, h) { var k = 255, l, m; for (m = 0; m < e; ++m) { for (l = 0; l < d; ++l) { var n = a[b + l]; f[g + 4 * l] = n; k &= n; } b += c; g += h; } return 255 != k; } function Lf(a, b, c, d, e) { var f; for (f = 0; f < e; ++f) c[d + f] = a[b + f] >> 8; } function Aa() { za = If; vc = Jf; fc = Kf; Fc = Lf; } function va(a, b, c) { self[a] = function(a, e, f, g, h, k, l, m, n, r, q, t, v, p, u, w, y) { var d, E = (y - 1) >> 1; var B = h[k + 0] | (l[m + 0] << 16); var C = n[r + 0] | (q[t + 0] << 16); x(null != a); var z = (3 * B + C + 131074) >> 2; b(a[e + 0], z & 255, z >> 16, v, p); null != f && ((z = (3 * C + B + 131074) >> 2), b(f[g + 0], z & 255, z >> 16, u, w)); for (d = 1; d <= E; ++d) { var D = h[k + d] | (l[m + d] << 16); var G = n[r + d] | (q[t + d] << 16); var F = B + D + C + G + 524296; var H = (F + 2 * (D + C)) >> 3; F = (F + 2 * (B + G)) >> 3; z = (H + B) >> 1; B = (F + D) >> 1; b(a[e + 2 * d - 1], z & 255, z >> 16, v, p + (2 * d - 1) * c); b(a[e + 2 * d - 0], B & 255, B >> 16, v, p + (2 * d - 0) * c); null != f && ((z = (F + C) >> 1), (B = (H + G) >> 1), b(f[g + 2 * d - 1], z & 255, z >> 16, u, w + (2 * d - 1) * c), b(f[g + 2 * d + 0], B & 255, B >> 16, u, w + (2 * d + 0) * c)); B = D; C = G; } y & 1 || ((z = (3 * B + C + 131074) >> 2), b(a[e + y - 1], z & 255, z >> 16, v, p + (y - 1) * c), null != f && ((z = (3 * C + B + 131074) >> 2), b(f[g + y - 1], z & 255, z >> 16, u, w + (y - 1) * c))); }; } function ic() { P[Ca] = Mf; P[Ua] = nd; P[tc] = Nf; P[Va] = od; P[ya] = pd; P[Db] = qd; P[wc] = Of; P[zb] = nd; P[Ab] = od; P[Ja] = pd; P[Bb] = qd; } function Sb(a) { return a & -16384 ? (0 > a ? 0 : 255) : a >> rd; } function bb(a, b) { return Sb(((19077 * a) >> 8) + ((26149 * b) >> 8) - 14234); } function nb(a, b, c) { return Sb( ((19077 * a) >> 8) - ((6419 * b) >> 8) - ((13320 * c) >> 8) + 8708 ); } function Pa(a, b) { return Sb(((19077 * a) >> 8) + ((33050 * b) >> 8) - 17685); } function Ga(a, b, c, d, e) { d[e + 0] = bb(a, c); d[e + 1] = nb(a, b, c); d[e + 2] = Pa(a, b); } function Tb(a, b, c, d, e) { d[e + 0] = Pa(a, b); d[e + 1] = nb(a, b, c); d[e + 2] = bb(a, c); } function sd(a, b, c, d, e) { var f = nb(a, b, c); b = ((f << 3) & 224) | (Pa(a, b) >> 3); d[e + 0] = (bb(a, c) & 248) | (f >> 5); d[e + 1] = b; } function td(a, b, c, d, e) { var f = (Pa(a, b) & 240) | 15; d[e + 0] = (bb(a, c) & 240) | (nb(a, b, c) >> 4); d[e + 1] = f; } function ud(a, b, c, d, e) { d[e + 0] = 255; Ga(a, b, c, d, e + 1); } function vd(a, b, c, d, e) { Tb(a, b, c, d, e); d[e + 3] = 255; } function wd(a, b, c, d, e) { Ga(a, b, c, d, e); d[e + 3] = 255; } function ga(a, b) { return 0 > a ? 0 : a > b ? b : a; } function la(a, b, c) { self[a] = function(a, e, f, g, h, k, l, m, n) { for (var d = m + (n & -2) * c; m != d; ) b(a[e + 0], f[g + 0], h[k + 0], l, m), b(a[e + 1], f[g + 0], h[k + 0], l, m + c), (e += 2), ++g, ++k, (m += 2 * c); n & 1 && b(a[e + 0], f[g + 0], h[k + 0], l, m); }; } function xd(a, b, c) { return 0 == c ? (0 == a ? (0 == b ? 6 : 5) : 0 == b ? 4 : 0) : c; } function yd(a, b, c, d, e) { switch (a >>> 30) { case 3: Za(b, c, d, e, 0); break; case 2: fd(b, c, d, e); break; case 1: pa(b, c, d, e); } } function Oc(a, b) { var c, d, e = b.M, f = b.Nb, g = a.oc, h = a.pc + 40, k = a.oc, l = a.pc + 584, m = a.oc, n = a.pc + 600; for (c = 0; 16 > c; ++c) g[h + 32 * c - 1] = 129; for (c = 0; 8 > c; ++c) (k[l + 32 * c - 1] = 129), (m[n + 32 * c - 1] = 129); 0 < e ? (g[h - 1 - 32] = k[l - 1 - 32] = m[n - 1 - 32] = 129) : (M(g, h - 32 - 1, 127, 21), M(k, l - 32 - 1, 127, 9), M(m, n - 32 - 1, 127, 9)); for (d = 0; d < a.za; ++d) { var r = b.ya[b.aa + d]; if (0 < d) { for (c = -1; 16 > c; ++c) I(g, h + 32 * c - 4, g, h + 32 * c + 12, 4); for (c = -1; 8 > c; ++c) I(k, l + 32 * c - 4, k, l + 32 * c + 4, 4), I(m, n + 32 * c - 4, m, n + 32 * c + 4, 4); } var q = a.Gd, t = a.Hd + d, v = r.ad, p = r.Hc; 0 < e && (I(g, h - 32, q[t].y, 0, 16), I(k, l - 32, q[t].f, 0, 8), I(m, n - 32, q[t].ea, 0, 8)); if (r.Za) { var u = g; var w = h - 32 + 16; 0 < e && (d >= a.za - 1 ? M(u, w, q[t].y[15], 4) : I(u, w, q[t + 1].y, 0, 4)); for (c = 0; 4 > c; c++) u[w + 128 + c] = u[w + 256 + c] = u[w + 384 + c] = u[w + 0 + c]; for (c = 0; 16 > c; ++c, p <<= 2) (u = g), (w = h + zd[c]), W[r.Ob[c]](u, w), yd(p, v, 16 * +c, u, w); } else if (((u = xd(d, e, r.Ob[0])), Y[u](g, h), 0 != p)) for (c = 0; 16 > c; ++c, p <<= 2) yd(p, v, 16 * +c, g, h + zd[c]); c = r.Gc; u = xd(d, e, r.Dd); ka[u](k, l); ka[u](m, n); r = c >> 0; p = v; u = k; w = l; r & 255 && (r & 170 ? Nb(p, 256, u, w) : Ob(p, 256, u, w)); c >>= 8; r = m; p = n; c & 255 && (c & 170 ? Nb(v, 320, r, p) : Ob(v, 320, r, p)); e < a.Ub - 1 && (I(q[t].y, 0, g, h + 480, 16), I(q[t].f, 0, k, l + 224, 8), I(q[t].ea, 0, m, n + 224, 8)); c = 8 * f * a.B; q = a.sa; t = a.ta + 16 * d + 16 * f * a.R; v = a.qa; r = a.ra + 8 * d + c; p = a.Ha; u = a.Ia + 8 * d + c; for (c = 0; 16 > c; ++c) I(q, t + c * a.R, g, h + 32 * c, 16); for (c = 0; 8 > c; ++c) I(v, r + c * a.B, k, l + 32 * c, 8), I(p, u + c * a.B, m, n + 32 * c, 8); } } function Ad(a, b, c, d, e, f, g, h, k) { var l = [0], m = [0], n = 0, r = null != k ? k.kd : 0, q = null != k ? k : new md(); if (null == a || 12 > c) return 7; q.data = a; q.w = b; q.ha = c; b = [b]; c = [c]; q.gb = [q.gb]; a: { var t = b; var v = c; var p = q.gb; x(null != a); x(null != v); x(null != p); p[0] = 0; if (12 <= v[0] && !fa(a, t[0], "RIFF")) { if (fa(a, t[0] + 8, "WEBP")) { p = 3; break a; } var u = Ha(a, t[0] + 4); if (12 > u || 4294967286 < u) { p = 3; break a; } if (r && u > v[0] - 8) { p = 7; break a; } p[0] = u; t[0] += 12; v[0] -= 12; } p = 0; } if (0 != p) return p; u = 0 < q.gb[0]; for (c = c[0]; ; ) { t = [0]; n = [n]; a: { var w = a; v = b; p = c; var y = n, A = l, z = m, B = t; y[0] = 0; if (8 > p[0]) p = 7; else { if (!fa(w, v[0], "VP8X")) { if (10 != Ha(w, v[0] + 4)) { p = 3; break a; } if (18 > p[0]) { p = 7; break a; } var C = Ha(w, v[0] + 8); var D = 1 + Yb(w, v[0] + 12); w = 1 + Yb(w, v[0] + 15); if (2147483648 <= D * w) { p = 3; break a; } null != B && (B[0] = C); null != A && (A[0] = D); null != z && (z[0] = w); v[0] += 18; p[0] -= 18; y[0] = 1; } p = 0; } } n = n[0]; t = t[0]; if (0 != p) return p; v = !!(t & 2); if (!u && n) return 3; null != f && (f[0] = !!(t & 16)); null != g && (g[0] = v); null != h && (h[0] = 0); g = l[0]; t = m[0]; if (n && v && null == k) { p = 0; break; } if (4 > c) { p = 7; break; } if ((u && n) || (!u && !n && !fa(a, b[0], "ALPH"))) { c = [c]; q.na = [q.na]; q.P = [q.P]; q.Sa = [q.Sa]; a: { C = a; p = b; u = c; var y = q.gb, A = q.na, z = q.P, B = q.Sa; D = 22; x(null != C); x(null != u); w = p[0]; var F = u[0]; x(null != A); x(null != B); A[0] = null; z[0] = null; for (B[0] = 0; ; ) { p[0] = w; u[0] = F; if (8 > F) { p = 7; break a; } var G = Ha(C, w + 4); if (4294967286 < G) { p = 3; break a; } var H = (8 + G + 1) & -2; D += H; if (0 < y && D > y) { p = 3; break a; } if (!fa(C, w, "VP8 ") || !fa(C, w, "VP8L")) { p = 0; break a; } if (F[0] < H) { p = 7; break a; } fa(C, w, "ALPH") || ((A[0] = C), (z[0] = w + 8), (B[0] = G)); w += H; F -= H; } } c = c[0]; q.na = q.na[0]; q.P = q.P[0]; q.Sa = q.Sa[0]; if (0 != p) break; } c = [c]; q.Ja = [q.Ja]; q.xa = [q.xa]; a: if ( ((y = a), (p = b), (u = c), (A = q.gb[0]), (z = q.Ja), (B = q.xa), (C = p[0]), (w = !fa(y, C, "VP8 ")), (D = !fa(y, C, "VP8L")), x(null != y), x(null != u), x(null != z), x(null != B), 8 > u[0]) ) p = 7; else { if (w || D) { y = Ha(y, C + 4); if (12 <= A && y > A - 12) { p = 3; break a; } if (r && y > u[0] - 8) { p = 7; break a; } z[0] = y; p[0] += 8; u[0] -= 8; B[0] = D; } else (B[0] = 5 <= u[0] && 47 == y[C + 0] && !(y[C + 4] >> 5)), (z[0] = u[0]); p = 0; } c = c[0]; q.Ja = q.Ja[0]; q.xa = q.xa[0]; b = b[0]; if (0 != p) break; if (4294967286 < q.Ja) return 3; null == h || v || (h[0] = q.xa ? 2 : 1); g = [g]; t = [t]; if (q.xa) { if (5 > c) { p = 7; break; } h = g; r = t; v = f; null == a || 5 > c ? (a = 0) : 5 <= c && 47 == a[b + 0] && !(a[b + 4] >> 5) ? ((u = [0]), (y = [0]), (A = [0]), (z = new Ra()), cb(z, a, b, c), mc(z, u, y, A) ? (null != h && (h[0] = u[0]), null != r && (r[0] = y[0]), null != v && (v[0] = A[0]), (a = 1)) : (a = 0)) : (a = 0); } else { if (10 > c) { p = 7; break; } h = t; null == a || 10 > c || !Jc(a, b + 3, c - 3) ? (a = 0) : ((r = a[b + 0] | (a[b + 1] << 8) | (a[b + 2] << 16)), (v = ((a[b + 7] << 8) | a[b + 6]) & 16383), (a = ((a[b + 9] << 8) | a[b + 8]) & 16383), r & 1 || 3 < ((r >> 1) & 7) || !((r >> 4) & 1) || r >> 5 >= q.Ja || !v || !a ? (a = 0) : (g && (g[0] = v), h && (h[0] = a), (a = 1))); } if (!a) return 3; g = g[0]; t = t[0]; if (n && (l[0] != g || m[0] != t)) return 3; null != k && ((k[0] = q), (k.offset = b - k.w), x(4294967286 > b - k.w), x(k.offset == k.ha - c)); break; } return 0 == p || (7 == p && n && null == k) ? (null != f && (f[0] |= null != q.na && 0 < q.na.length), null != d && (d[0] = g), null != e && (e[0] = t), 0) : p; } function hc(a, b, c) { var d = b.width, e = b.height, f = 0, g = 0, h = d, k = e; b.Da = null != a && 0 < a.Da; if ( b.Da && ((h = a.cd), (k = a.bd), (f = a.v), (g = a.j), 11 > c || ((f &= -2), (g &= -2)), 0 > f || 0 > g || 0 >= h || 0 >= k || f + h > d || g + k > e) ) return 0; b.v = f; b.j = g; b.va = f + h; b.o = g + k; b.U = h; b.T = k; b.da = null != a && 0 < a.da; if (b.da) { c = [a.ib]; f = [a.hb]; if (!bc(h, k, c, f)) return 0; b.ib = c[0]; b.hb = f[0]; } b.ob = null != a && a.ob; b.Kb = null == a || !a.Sd; b.da && ((b.ob = b.ib < (3 * d) / 4 && b.hb < (3 * e) / 4), (b.Kb = 0)); return 1; } function Bd(a) { if (null == a) return 2; if (11 > a.S) { var b = a.f.RGBA; b.fb += (a.height - 1) * b.A; b.A = -b.A; } else (b = a.f.kb), (a = a.height), (b.O += (a - 1) * b.fa), (b.fa = -b.fa), (b.N += ((a - 1) >> 1) * b.Ab), (b.Ab = -b.Ab), (b.W += ((a - 1) >> 1) * b.Db), (b.Db = -b.Db), null != b.F && ((b.J += (a - 1) * b.lb), (b.lb = -b.lb)); return 0; } function Cd(a, b, c, d) { if (null == d || 0 >= a || 0 >= b) return 2; if (null != c) { if (c.Da) { var e = c.cd, f = c.bd, g = c.v & -2, h = c.j & -2; if (0 > g || 0 > h || 0 >= e || 0 >= f || g + e > a || h + f > b) return 2; a = e; b = f; } if (c.da) { e = [c.ib]; f = [c.hb]; if (!bc(a, b, e, f)) return 2; a = e[0]; b = f[0]; } } d.width = a; d.height = b; a: { var k = d.width; var l = d.height; a = d.S; if (0 >= k || 0 >= l || !(a >= Ca && 13 > a)) a = 2; else { if (0 >= d.Rd && null == d.sd) { var g = (f = e = b = 0), h = k * Dd[a], m = h * l; 11 > a || ((b = (k + 1) / 2), (f = ((l + 1) / 2) * b), 12 == a && ((e = k), (g = e * l))); l = V(m + 2 * f + g); if (null == l) { a = 1; break a; } d.sd = l; 11 > a ? ((k = d.f.RGBA), (k.eb = l), (k.fb = 0), (k.A = h), (k.size = m)) : ((k = d.f.kb), (k.y = l), (k.O = 0), (k.fa = h), (k.Fd = m), (k.f = l), (k.N = 0 + m), (k.Ab = b), (k.Cd = f), (k.ea = l), (k.W = 0 + m + f), (k.Db = b), (k.Ed = f), 12 == a && ((k.F = l), (k.J = 0 + m + 2 * f)), (k.Tc = g), (k.lb = e)); } b = 1; e = d.S; f = d.width; g = d.height; if (e >= Ca && 13 > e) if (11 > e) (a = d.f.RGBA), (h = Math.abs(a.A)), (b &= h * (g - 1) + f <= a.size), (b &= h >= f * Dd[e]), (b &= null != a.eb); else { a = d.f.kb; h = (f + 1) / 2; m = (g + 1) / 2; k = Math.abs(a.fa); var l = Math.abs(a.Ab), n = Math.abs(a.Db), r = Math.abs(a.lb), q = r * (g - 1) + f; b &= k * (g - 1) + f <= a.Fd; b &= l * (m - 1) + h <= a.Cd; b &= n * (m - 1) + h <= a.Ed; b = b & (k >= f) & (l >= h) & (n >= h); b &= null != a.y; b &= null != a.f; b &= null != a.ea; 12 == e && ((b &= r >= f), (b &= q <= a.Tc), (b &= null != a.F)); } else b = 0; a = b ? 0 : 2; } } if (0 != a) return a; null != c && c.fd && (a = Bd(d)); return a; } var xb = 64, Hd = [ 0, 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383, 32767, 65535, 131071, 262143, 524287, 1048575, 2097151, 4194303, 8388607, 16777215 ], Gd = 24, ob = 32, Xb = 8, Id = [ 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 ]; X("Predictor0", "PredictorAdd0"); self.Predictor0 = function() { return 4278190080; }; self.Predictor1 = function(a) { return a; }; self.Predictor2 = function(a, b, c) { return b[c + 0]; }; self.Predictor3 = function(a, b, c) { return b[c + 1]; }; self.Predictor4 = function(a, b, c) { return b[c - 1]; }; self.Predictor5 = function(a, b, c) { return aa(aa(a, b[c + 1]), b[c + 0]); }; self.Predictor6 = function(a, b, c) { return aa(a, b[c - 1]); }; self.Predictor7 = function(a, b, c) { return aa(a, b[c + 0]); }; self.Predictor8 = function(a, b, c) { return aa(b[c - 1], b[c + 0]); }; self.Predictor9 = function(a, b, c) { return aa(b[c + 0], b[c + 1]); }; self.Predictor10 = function(a, b, c) { return aa(aa(a, b[c - 1]), aa(b[c + 0], b[c + 1])); }; self.Predictor11 = function(a, b, c) { var d = b[c + 0]; b = b[c - 1]; return 0 >= Ia((d >> 24) & 255, (a >> 24) & 255, (b >> 24) & 255) + Ia((d >> 16) & 255, (a >> 16) & 255, (b >> 16) & 255) + Ia((d >> 8) & 255, (a >> 8) & 255, (b >> 8) & 255) + Ia(d & 255, a & 255, b & 255) ? d : a; }; self.Predictor12 = function(a, b, c) { var d = b[c + 0]; b = b[c - 1]; return ( ((sa(((a >> 24) & 255) + ((d >> 24) & 255) - ((b >> 24) & 255)) << 24) | (sa(((a >> 16) & 255) + ((d >> 16) & 255) - ((b >> 16) & 255)) << 16) | (sa(((a >> 8) & 255) + ((d >> 8) & 255) - ((b >> 8) & 255)) << 8) | sa((a & 255) + (d & 255) - (b & 255))) >>> 0 ); }; self.Predictor13 = function(a, b, c) { var d = b[c - 1]; a = aa(a, b[c + 0]); return ( ((eb((a >> 24) & 255, (d >> 24) & 255) << 24) | (eb((a >> 16) & 255, (d >> 16) & 255) << 16) | (eb((a >> 8) & 255, (d >> 8) & 255) << 8) | eb((a >> 0) & 255, (d >> 0) & 255)) >>> 0 ); }; var ee = self.PredictorAdd0; self.PredictorAdd1 = cc; X("Predictor2", "PredictorAdd2"); X("Predictor3", "PredictorAdd3"); X("Predictor4", "PredictorAdd4"); X("Predictor5", "PredictorAdd5"); X("Predictor6", "PredictorAdd6"); X("Predictor7", "PredictorAdd7"); X("Predictor8", "PredictorAdd8"); X("Predictor9", "PredictorAdd9"); X("Predictor10", "PredictorAdd10"); X("Predictor11", "PredictorAdd11"); X("Predictor12", "PredictorAdd12"); X("Predictor13", "PredictorAdd13"); var fe = self.PredictorAdd2; ec( "ColorIndexInverseTransform", "MapARGB", "32b", function(a) { return (a >> 8) & 255; }, function(a) { return a; } ); ec( "VP8LColorIndexInverseTransformAlpha", "MapAlpha", "8b", function(a) { return a; }, function(a) { return (a >> 8) & 255; } ); var rc = self.ColorIndexInverseTransform, ke = self.MapARGB, he = self.VP8LColorIndexInverseTransformAlpha, le = self.MapAlpha, pc, qc = (self.VP8LPredictorsAdd = []); qc.length = 16; (self.VP8LPredictors = []).length = 16; (self.VP8LPredictorsAdd_C = []).length = 16; (self.VP8LPredictors_C = []).length = 16; var Fb, sc, Gb, Hb, xc, uc, bd = V(511), cd = V(2041), dd = V(225), ed = V(767), ad = 0, Qb = cd, mb = dd, R = ed, U = bd, Ca = 0, Ua = 1, tc = 2, Va = 3, ya = 4, Db = 5, wc = 6, zb = 7, Ab = 8, Ja = 9, Bb = 10, pe = [2, 3, 7], oe = [3, 3, 11], Dc = [280, 256, 256, 256, 40], qe = [0, 1, 1, 1, 0], ne = [17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], de = [ 24, 7, 23, 25, 40, 6, 39, 41, 22, 26, 38, 42, 56, 5, 55, 57, 21, 27, 54, 58, 37, 43, 72, 4, 71, 73, 20, 28, 53, 59, 70, 74, 36, 44, 88, 69, 75, 52, 60, 3, 87, 89, 19, 29, 86, 90, 35, 45, 68, 76, 85, 91, 51, 61, 104, 2, 103, 105, 18, 30, 102, 106, 34, 46, 84, 92, 67, 77, 101, 107, 50, 62, 120, 1, 119, 121, 83, 93, 17, 31, 100, 108, 66, 78, 118, 122, 33, 47, 117, 123, 49, 63, 99, 109, 82, 94, 0, 116, 124, 65, 79, 16, 32, 98, 110, 48, 115, 125, 81, 95, 64, 114, 126, 97, 111, 80, 113, 127, 96, 112 ], me = [ 2954, 2956, 2958, 2962, 2970, 2986, 3018, 3082, 3212, 3468, 3980, 5004 ], ie = 8, Lb = [ 4, 5, 6, 7, 8, 9, 10, 10, 11, 12, 13, 14, 15, 16, 17, 17, 18, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 25, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 91, 93, 95, 96, 98, 100, 101, 102, 104, 106, 108, 110, 112, 114, 116, 118, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 143, 145, 148, 151, 154, 157 ], Mb = [ 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 119, 122, 125, 128, 131, 134, 137, 140, 143, 146, 149, 152, 155, 158, 161, 164, 167, 170, 173, 177, 181, 185, 189, 193, 197, 201, 205, 209, 213, 217, 221, 225, 229, 234, 239, 245, 249, 254, 259, 264, 269, 274, 279, 284 ], oa = null, He = [ [173, 148, 140, 0], [176, 155, 140, 135, 0], [180, 157, 141, 134, 130, 0], [254, 254, 243, 230, 196, 177, 153, 140, 133, 130, 129, 0] ], Ie = [0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15], Mc = [-0, 1, -1, 2, -2, 3, 4, 6, -3, 5, -4, -5, -6, 7, -7, 8, -8, -9], Fe = [ [ [ [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128] ], [ [253, 136, 254, 255, 228, 219, 128, 128, 128, 128, 128], [189, 129, 242, 255, 227, 213, 255, 219, 128, 128, 128], [106, 126, 227, 252, 214, 209, 255, 255, 128, 128, 128] ], [ [1, 98, 248, 255, 236, 226, 255, 255, 128, 128, 128], [181, 133, 238, 254, 221, 234, 255, 154, 128, 128, 128], [78, 134, 202, 247, 198, 180, 255, 219, 128, 128, 128] ], [ [1, 185, 249, 255, 243, 255, 128, 128, 128, 128, 128], [184, 150, 247, 255, 236, 224, 128, 128, 128, 128, 128], [77, 110, 216, 255, 236, 230, 128, 128, 128, 128, 128] ], [ [1, 101, 251, 255, 241, 255, 128, 128, 128, 128, 128], [170, 139, 241, 252, 236, 209, 255, 255, 128, 128, 128], [37, 116, 196, 243, 228, 255, 255, 255, 128, 128, 128] ], [ [1, 204, 254, 255, 245, 255, 128, 128, 128, 128, 128], [207, 160, 250, 255, 238, 128, 128, 128, 128, 128, 128], [102, 103, 231, 255, 211, 171, 128, 128, 128, 128, 128] ], [ [1, 152, 252, 255, 240, 255, 128, 128, 128, 128, 128], [177, 135, 243, 255, 234, 225, 128, 128, 128, 128, 128], [80, 129, 211, 255, 194, 224, 128, 128, 128, 128, 128] ], [ [1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], [246, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], [255, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128] ] ], [ [ [198, 35, 237, 223, 193, 187, 162, 160, 145, 155, 62], [131, 45, 198, 221, 172, 176, 220, 157, 252, 221, 1], [68, 47, 146, 208, 149, 167, 221, 162, 255, 223, 128] ], [ [1, 149, 241, 255, 221, 224, 255, 255, 128, 128, 128], [184, 141, 234, 253, 222, 220, 255, 199, 128, 128, 128], [81, 99, 181, 242, 176, 190, 249, 202, 255, 255, 128] ], [ [1, 129, 232, 253, 214, 197, 242, 196, 255, 255, 128], [99, 121, 210, 250, 201, 198, 255, 202, 128, 128, 128], [23, 91, 163, 242, 170, 187, 247, 210, 255, 255, 128] ], [ [1, 200, 246, 255, 234, 255, 128, 128, 128, 128, 128], [109, 178, 241, 255, 231, 245, 255, 255, 128, 128, 128], [44, 130, 201, 253, 205, 192, 255, 255, 128, 128, 128] ], [ [1, 132, 239, 251, 219, 209, 255, 165, 128, 128, 128], [94, 136, 225, 251, 218, 190, 255, 255, 128, 128, 128], [22, 100, 174, 245, 186, 161, 255, 199, 128, 128, 128] ], [ [1, 182, 249, 255, 232, 235, 128, 128, 128, 128, 128], [124, 143, 241, 255, 227, 234, 128, 128, 128, 128, 128], [35, 77, 181, 251, 193, 211, 255, 205, 128, 128, 128] ], [ [1, 157, 247, 255, 236, 231, 255, 255, 128, 128, 128], [121, 141, 235, 255, 225, 227, 255, 255, 128, 128, 128], [45, 99, 188, 251, 195, 217, 255, 224, 128, 128, 128] ], [ [1, 1, 251, 255, 213, 255, 128, 128, 128, 128, 128], [203, 1, 248, 255, 255, 128, 128, 128, 128, 128, 128], [137, 1, 177, 255, 224, 255, 128, 128, 128, 128, 128] ] ], [ [ [253, 9, 248, 251, 207, 208, 255, 192, 128, 128, 128], [175, 13, 224, 243, 193, 185, 249, 198, 255, 255, 128], [73, 17, 171, 221, 161, 179, 236, 167, 255, 234, 128] ], [ [1, 95, 247, 253, 212, 183, 255, 255, 128, 128, 128], [239, 90, 244, 250, 211, 209, 255, 255, 128, 128, 128], [155, 77, 195, 248, 188, 195, 255, 255, 128, 128, 128] ], [ [1, 24, 239, 251, 218, 219, 255, 205, 128, 128, 128], [201, 51, 219, 255, 196, 186, 128, 128, 128, 128, 128], [69, 46, 190, 239, 201, 218, 255, 228, 128, 128, 128] ], [ [1, 191, 251, 255, 255, 128, 128, 128, 128, 128, 128], [223, 165, 249, 255, 213, 255, 128, 128, 128, 128, 128], [141, 124, 248, 255, 255, 128, 128, 128, 128, 128, 128] ], [ [1, 16, 248, 255, 255, 128, 128, 128, 128, 128, 128], [190, 36, 230, 255, 236, 255, 128, 128, 128, 128, 128], [149, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128] ], [ [1, 226, 255, 128, 128, 128, 128, 128, 128, 128, 128], [247, 192, 255, 128, 128, 128, 128, 128, 128, 128, 128], [240, 128, 255, 128, 128, 128, 128, 128, 128, 128, 128] ], [ [1, 134, 252, 255, 255, 128, 128, 128, 128, 128, 128], [213, 62, 250, 255, 255, 128, 128, 128, 128, 128, 128], [55, 93, 255, 128, 128, 128, 128, 128, 128, 128, 128] ], [ [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128] ] ], [ [ [202, 24, 213, 235, 186, 191, 220, 160, 240, 175, 255], [126, 38, 182, 232, 169, 184, 228, 174, 255, 187, 128], [61, 46, 138, 219, 151, 178, 240, 170, 255, 216, 128] ], [ [1, 112, 230, 250, 199, 191, 247, 159, 255, 255, 128], [166, 109, 228, 252, 211, 215, 255, 174, 128, 128, 128], [39, 77, 162, 232, 172, 180, 245, 178, 255, 255, 128] ], [ [1, 52, 220, 246, 198, 199, 249, 220, 255, 255, 128], [124, 74, 191, 243, 183, 193, 250, 221, 255, 255, 128], [24, 71, 130, 219, 154, 170, 243, 182, 255, 255, 128] ], [ [1, 182, 225, 249, 219, 240, 255, 224, 128, 128, 128], [149, 150, 226, 252, 216, 205, 255, 171, 128, 128, 128], [28, 108, 170, 242, 183, 194, 254, 223, 255, 255, 128] ], [ [1, 81, 230, 252, 204, 203, 255, 192, 128, 128, 128], [123, 102, 209, 247, 188, 196, 255, 233, 128, 128, 128], [20, 95, 153, 243, 164, 173, 255, 203, 128, 128, 128] ], [ [1, 222, 248, 255, 216, 213, 128, 128, 128, 128, 128], [168, 175, 246, 252, 235, 205, 255, 255, 128, 128, 128], [47, 116, 215, 255, 211, 212, 255, 255, 128, 128, 128] ], [ [1, 121, 236, 253, 212, 214, 255, 255, 128, 128, 128], [141, 84, 213, 252, 201, 202, 255, 219, 128, 128, 128], [42, 80, 160, 240, 162, 185, 255, 205, 128, 128, 128] ], [ [1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], [244, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], [238, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128] ] ] ], Ke = [ [ [231, 120, 48, 89, 115, 113, 120, 152, 112], [152, 179, 64, 126, 170, 118, 46, 70, 95], [175, 69, 143, 80, 85, 82, 72, 155, 103], [56, 58, 10, 171, 218, 189, 17, 13, 152], [114, 26, 17, 163, 44, 195, 21, 10, 173], [121, 24, 80, 195, 26, 62, 44, 64, 85], [144, 71, 10, 38, 171, 213, 144, 34, 26], [170, 46, 55, 19, 136, 160, 33, 206, 71], [63, 20, 8, 114, 114, 208, 12, 9, 226], [81, 40, 11, 96, 182, 84, 29, 16, 36] ], [ [134, 183, 89, 137, 98, 101, 106, 165, 148], [72, 187, 100, 130, 157, 111, 32, 75, 80], [66, 102, 167, 99, 74, 62, 40, 234, 128], [41, 53, 9, 178, 241, 141, 26, 8, 107], [74, 43, 26, 146, 73, 166, 49, 23, 157], [65, 38, 105, 160, 51, 52, 31, 115, 128], [104, 79, 12, 27, 217, 255, 87, 17, 7], [87, 68, 71, 44, 114, 51, 15, 186, 23], [47, 41, 14, 110, 182, 183, 21, 17, 194], [66, 45, 25, 102, 197, 189, 23, 18, 22] ], [ [88, 88, 147, 150, 42, 46, 45, 196, 205], [43, 97, 183, 117, 85, 38, 35, 179, 61], [39, 53, 200, 87, 26, 21, 43, 232, 171], [56, 34, 51, 104, 114, 102, 29, 93, 77], [39, 28, 85, 171, 58, 165, 90, 98, 64], [34, 22, 116, 206, 23, 34, 43, 166, 73], [107, 54, 32, 26, 51, 1, 81, 43, 31], [68, 25, 106, 22, 64, 171, 36, 225, 114], [34, 19, 21, 102, 132, 188, 16, 76, 124], [62, 18, 78, 95, 85, 57, 50, 48, 51] ], [ [193, 101, 35, 159, 215, 111, 89, 46, 111], [60, 148, 31, 172, 219, 228, 21, 18, 111], [112, 113, 77, 85, 179, 255, 38, 120, 114], [40, 42, 1, 196, 245, 209, 10, 25, 109], [88, 43, 29, 140, 166, 213, 37, 43, 154], [61, 63, 30, 155, 67, 45, 68, 1, 209], [100, 80, 8, 43, 154, 1, 51, 26, 71], [142, 78, 78, 16, 255, 128, 34, 197, 171], [41, 40, 5, 102, 211, 183, 4, 1, 221], [51, 50, 17, 168, 209, 192, 23, 25, 82] ], [ [138, 31, 36, 171, 27, 166, 38, 44, 229], [67, 87, 58, 169, 82, 115, 26, 59, 179], [63, 59, 90, 180, 59, 166, 93, 73, 154], [40, 40, 21, 116, 143, 209, 34, 39, 175], [47, 15, 16, 183, 34, 223, 49, 45, 183], [46, 17, 33, 183, 6, 98, 15, 32, 183], [57, 46, 22, 24, 128, 1, 54, 17, 37], [65, 32, 73, 115, 28, 128, 23, 128, 205], [40, 3, 9, 115, 51, 192, 18, 6, 223], [87, 37, 9, 115, 59, 77, 64, 21, 47] ], [ [104, 55, 44, 218, 9, 54, 53, 130, 226], [64, 90, 70, 205, 40, 41, 23, 26, 57], [54, 57, 112, 184, 5, 41, 38, 166, 213], [30, 34, 26, 133, 152, 116, 10, 32, 134], [39, 19, 53, 221, 26, 114, 32, 73, 255], [31, 9, 65, 234, 2, 15, 1, 118, 73], [75, 32, 12, 51, 192, 255, 160, 43, 51], [88, 31, 35, 67, 102, 85, 55, 186, 85], [56, 21, 23, 111, 59, 205, 45, 37, 192], [55, 38, 70, 124, 73, 102, 1, 34, 98] ], [ [125, 98, 42, 88, 104, 85, 117, 175, 82], [95, 84, 53, 89, 128, 100, 113, 101, 45], [75, 79, 123, 47, 51, 128, 81, 171, 1], [57, 17, 5, 71, 102, 57, 53, 41, 49], [38, 33, 13, 121, 57, 73, 26, 1, 85], [41, 10, 67, 138, 77, 110, 90, 47, 114], [115, 21, 2, 10, 102, 255, 166, 23, 6], [101, 29, 16, 10, 85, 128, 101, 196, 26], [57, 18, 10, 102, 102, 213, 34, 20, 43], [117, 20, 15, 36, 163, 128, 68, 1, 26] ], [ [102, 61, 71, 37, 34, 53, 31, 243, 192], [69, 60, 71, 38, 73, 119, 28, 222, 37], [68, 45, 128, 34, 1, 47, 11, 245, 171], [62, 17, 19, 70, 146, 85, 55, 62, 70], [37, 43, 37, 154, 100, 163, 85, 160, 1], [63, 9, 92, 136, 28, 64, 32, 201, 85], [75, 15, 9, 9, 64, 255, 184, 119, 16], [86, 6, 28, 5, 64, 255, 25, 248, 1], [56, 8, 17, 132, 137, 255, 55, 116, 128], [58, 15, 20, 82, 135, 57, 26, 121, 40] ], [ [164, 50, 31, 137, 154, 133, 25, 35, 218], [51, 103, 44, 131, 131, 123, 31, 6, 158], [86, 40, 64, 135, 148, 224, 45, 183, 128], [22, 26, 17, 131, 240, 154, 14, 1, 209], [45, 16, 21, 91, 64, 222, 7, 1, 197], [56, 21, 39, 155, 60, 138, 23, 102, 213], [83, 12, 13, 54, 192, 255, 68, 47, 28], [85, 26, 85, 85, 128, 128, 32, 146, 171], [18, 11, 7, 63, 144, 171, 4, 4, 246], [35, 27, 10, 146, 174, 171, 12, 26, 128] ], [ [190, 80, 35, 99, 180, 80, 126, 54, 45], [85, 126, 47, 87, 176, 51, 41, 20, 32], [101, 75, 128, 139, 118, 146, 116, 128, 85], [56, 41, 15, 176, 236, 85, 37, 9, 62], [71, 30, 17, 119, 118, 255, 17, 18, 138], [101, 38, 60, 138, 55, 70, 43, 26, 142], [146, 36, 19, 30, 171, 255, 97, 27, 20], [138, 45, 61, 62, 219, 1, 81, 188, 64], [32, 41, 20, 117, 151, 142, 20, 21, 163], [112, 19, 12, 61, 195, 128, 48, 4, 24] ] ], Ee = [ [ [ [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [176, 246, 255, 255, 255, 255, 255, 255, 255, 255, 255], [223, 241, 252, 255, 255, 255, 255, 255, 255, 255, 255], [249, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 244, 252, 255, 255, 255, 255, 255, 255, 255, 255], [234, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], [253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 246, 254, 255, 255, 255, 255, 255, 255, 255, 255], [239, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], [254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255], [251, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], [251, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], [254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 254, 253, 255, 254, 255, 255, 255, 255, 255, 255], [250, 255, 254, 255, 254, 255, 255, 255, 255, 255, 255], [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ] ], [ [ [217, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [225, 252, 241, 253, 255, 255, 254, 255, 255, 255, 255], [234, 250, 241, 250, 253, 255, 253, 254, 255, 255, 255] ], [ [255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], [223, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], [238, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255], [249, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 253, 255, 255, 255, 255, 255, 255, 255, 255, 255], [247, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], [252, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], [253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255], [250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ] ], [ [ [186, 251, 250, 255, 255, 255, 255, 255, 255, 255, 255], [234, 251, 244, 254, 255, 255, 255, 255, 255, 255, 255], [251, 251, 243, 253, 254, 255, 254, 255, 255, 255, 255] ], [ [255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], [236, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], [251, 253, 253, 254, 254, 255, 255, 255, 255, 255, 255] ], [ [255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], [254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], [254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ] ], [ [ [248, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [250, 254, 252, 254, 255, 255, 255, 255, 255, 255, 255], [248, 254, 249, 253, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255], [246, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255], [252, 254, 251, 254, 254, 255, 255, 255, 255, 255, 255] ], [ [255, 254, 252, 255, 255, 255, 255, 255, 255, 255, 255], [248, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255], [253, 255, 254, 254, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255], [245, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255], [253, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 251, 253, 255, 255, 255, 255, 255, 255, 255, 255], [252, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], [255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 252, 255, 255, 255, 255, 255, 255, 255, 255, 255], [249, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 255, 253, 255, 255, 255, 255, 255, 255, 255, 255], [250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], [ [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ] ] ], Ge = [0, 1, 2, 3, 6, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 0], Nc, Y = [], W = [], ka = [], Za, fd, Nb, pa, Ob, Xc, Tc, Yc, Uc, Zc, Vc, $c, Wc, Rc, Pc, Sc, Qc, re = 1, Cc = 2, ia = [], za, vc, fc, Fc, P = []; va("UpsampleRgbLinePair", Ga, 3); va("UpsampleBgrLinePair", Tb, 3); va("UpsampleRgbaLinePair", wd, 4); va("UpsampleBgraLinePair", vd, 4); va("UpsampleArgbLinePair", ud, 4); va("UpsampleRgba4444LinePair", td, 2); va("UpsampleRgb565LinePair", sd, 2); var Mf = self.UpsampleRgbLinePair, Nf = self.UpsampleBgrLinePair, nd = self.UpsampleRgbaLinePair, od = self.UpsampleBgraLinePair, pd = self.UpsampleArgbLinePair, qd = self.UpsampleRgba4444LinePair, Of = self.UpsampleRgb565LinePair, Wa = 16, Ba = 1 << (Wa - 1), ta = -227, Eb = 482, rd = 6, jc = 0, Yd = V(256), ae = V(256), $d = V(256), Zd = V(256), be = V(Eb - ta), ce = V(Eb - ta); la("YuvToRgbRow", Ga, 3); la("YuvToBgrRow", Tb, 3); la("YuvToRgbaRow", wd, 4); la("YuvToBgraRow", vd, 4); la("YuvToArgbRow", ud, 4); la("YuvToRgba4444Row", td, 2); la("YuvToRgb565Row", sd, 2); var zd = [ 0, 4, 8, 12, 128, 132, 136, 140, 256, 260, 264, 268, 384, 388, 392, 396 ], Ya = [0, 2, 8], Qf = [8, 7, 6, 4, 4, 2, 2, 2, 1, 1, 1, 1], Ne = 1; this.WebPDecodeRGBA = function(a, b, c, d, e) { var f = Ua; var g = new Cf(), h = new Cb(); g.ba = h; h.S = f; h.width = [h.width]; h.height = [h.height]; var k = h.width; var l = h.height, m = new Td(); if (null == m || null == a) var n = 2; else x(null != m), (n = Ad(a, b, c, m.width, m.height, m.Pd, m.Qd, m.format, null)); 0 != n ? (k = 0) : (null != k && (k[0] = m.width[0]), null != l && (l[0] = m.height[0]), (k = 1)); if (k) { h.width = h.width[0]; h.height = h.height[0]; null != d && (d[0] = h.width); null != e && (e[0] = h.height); b: { d = new Oa(); e = new md(); e.data = a; e.w = b; e.ha = c; e.kd = 1; b = [0]; x(null != e); a = Ad(e.data, e.w, e.ha, null, null, null, b, null, e); (0 == a || 7 == a) && b[0] && (a = 4); b = a; if (0 == b) { x(null != g); d.data = e.data; d.w = e.w + e.offset; d.ha = e.ha - e.offset; d.put = kc; d.ac = gc; d.bc = lc; d.ma = g; if (e.xa) { a = Bc(); if (null == a) { g = 1; break b; } if (te(a, d)) { b = Cd(d.width, d.height, g.Oa, g.ba); if ((d = 0 == b)) { c: { d = a; d: for (;;) { if (null == d) { d = 0; break c; } x(null != d.s.yc); x(null != d.s.Ya); x(0 < d.s.Wb); c = d.l; x(null != c); e = c.ma; x(null != e); if (0 != d.xb) { d.ca = e.ba; d.tb = e.tb; x(null != d.ca); if (!hc(e.Oa, c, Va)) { d.a = 2; break d; } if (!Ec(d, c.width)) break d; if (c.da) break d; (c.da || hb(d.ca.S)) && Aa(); 11 > d.ca.S || (alert("todo:WebPInitConvertARGBToYUV"), null != d.ca.f.kb.F && Aa()); if ( d.Pb && 0 < d.s.ua && null == d.s.vb.X && !Zb(d.s.vb, d.s.Wa.Xa) ) { d.a = 1; break d; } d.xb = 0; } if (!Jb(d, d.V, d.Ba, d.c, d.i, c.o, ge)) break d; e.Dc = d.Ma; d = 1; break c; } x(0 != d.a); d = 0; } d = !d; } d && (b = a.a); } else b = a.a; } else { a = new Ce(); if (null == a) { g = 1; break b; } a.Fa = e.na; a.P = e.P; a.qc = e.Sa; if (Kc(a, d)) { if (((b = Cd(d.width, d.height, g.Oa, g.ba)), 0 == b)) { a.Aa = 0; c = g.Oa; e = a; x(null != e); if (null != c) { k = c.Md; k = 0 > k ? 0 : 100 < k ? 255 : (255 * k) / 100; if (0 < k) { for (l = m = 0; 4 > l; ++l) (n = e.pb[l]), 12 > n.lc && (n.ia = (k * Qf[0 > n.lc ? 0 : n.lc]) >> 3), (m |= n.ia); m && (alert("todo:VP8InitRandom"), (e.ia = 1)); } e.Ga = c.Id; 100 < e.Ga ? (e.Ga = 100) : 0 > e.Ga && (e.Ga = 0); } Me(a, d) || (b = a.a); } } else b = a.a; } 0 == b && null != g.Oa && g.Oa.fd && (b = Bd(g.ba)); } g = b; } f = 0 != g ? null : 11 > f ? h.f.RGBA.eb : h.f.kb.y; } else f = null; return f; }; var Dd = [3, 4, 3, 4, 4, 2, 2, 4, 4, 4, 2, 1, 1]; }; new _WebPDecoder(); /** @license * Copyright (c) 2017 Dominik Homberger 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. https://webpjs.appspot.com WebPRiffParser dominikhlbg@gmail.com */ function memcmp(data, data_off, str, size) { for (var i = 0; i < size; i++) if (data[data_off + i] != str.charCodeAt(i)) return true; return false; } function GetTag(data, data_off) { var str = ""; for (var i = 0; i < 4; i++) str += String.fromCharCode(data[data_off++]); return str; } function GetLE16(data, data_off) { return (data[data_off + 0] << 0) | (data[data_off + 1] << 8); } function GetLE24(data, data_off) { return ( ((data[data_off + 0] << 0) | (data[data_off + 1] << 8) | (data[data_off + 2] << 16)) >>> 0 ); } function GetLE32(data, data_off) { return ( ((data[data_off + 0] << 0) | (data[data_off + 1] << 8) | (data[data_off + 2] << 16) | (data[data_off + 3] << 24)) >>> 0 ); } function WebPRiffParser(src, src_off) { var imagearray = {}; var i = 0; var alpha_chunk = false; var alpha_size = 0; var alpha_offset = 0; imagearray["frames"] = []; if (memcmp(src, src_off, "RIFF", 4)) return; src_off += 4; GetLE32(src, src_off) + 8; src_off += 8; while (src_off < src.length) { var fourcc = GetTag(src, src_off); src_off += 4; var payload_size = GetLE32(src, src_off); src_off += 4; var payload_size_padded = payload_size + (payload_size & 1); switch (fourcc) { case "VP8 ": case "VP8L": if (typeof imagearray["frames"][i] === "undefined") imagearray["frames"][i] = {}; var obj = imagearray["frames"][i]; obj["src_off"] = alpha_chunk ? alpha_offset : src_off - 8; obj["src_size"] = alpha_size + payload_size + 8; //var rgba = webpdecoder.WebPDecodeRGBA(src,(alpha_chunk?alpha_offset:src_off-8),alpha_size+payload_size+8,width,height); //imagearray[i]={'rgba':rgba,'width':width[0],'height':height[0]}; i++; if (alpha_chunk) { alpha_chunk = false; alpha_size = 0; alpha_offset = 0; } break; case "VP8X": var obj = (imagearray["header"] = {}); (obj["feature_flags"] = src[src_off]); var src_off_ = src_off + 4; (obj["canvas_width"] = 1 + GetLE24(src, src_off_)); src_off_ += 3; (obj["canvas_height"] = 1 + GetLE24(src, src_off_)); src_off_ += 3; break; case "ALPH": alpha_chunk = true; alpha_size = payload_size_padded + 8; alpha_offset = src_off - 8; break; case "ANIM": var obj = imagearray["header"]; (obj["bgcolor"] = GetLE32(src, src_off)); src_off_ = src_off + 4; (obj["loop_count"] = GetLE16(src, src_off_)); src_off_ += 2; break; case "ANMF": var temp = 0; var obj = (imagearray["frames"][i] = {}); obj["offset_x"] = 2 * GetLE24(src, src_off); src_off += 3; obj["offset_y"] = 2 * GetLE24(src, src_off); src_off += 3; obj["width"] = 1 + GetLE24(src, src_off); src_off += 3; obj["height"] = 1 + GetLE24(src, src_off); src_off += 3; obj["duration"] = GetLE24(src, src_off); src_off += 3; temp = src[src_off++]; obj["dispose"] = temp & 1; obj["blend"] = (temp >> 1) & 1; break; } if (fourcc != "ANMF") src_off += payload_size_padded; } return imagearray; } var height = [0]; var width = [0]; var pixels = []; var webpdecoder = new _WebPDecoder(); var response = imageData; var imagearray = WebPRiffParser(response, 0); imagearray["response"] = response; imagearray["rgbaoutput"] = true; imagearray["dataurl"] = false; var header = imagearray["header"] ? imagearray["header"] : null; var frames = imagearray["frames"] ? imagearray["frames"] : null; if (header) { header["loop_counter"] = header["loop_count"]; height = [header["canvas_height"]]; width = [header["canvas_width"]]; for (var f = 0; f < frames.length; f++) if (frames[f]["blend"] == 0) { break; } } var frame = frames[0]; var rgba = webpdecoder.WebPDecodeRGBA( response, frame["src_off"], frame["src_size"], width, height ); frame["rgba"] = rgba; frame["imgwidth"] = width[0]; frame["imgheight"] = height[0]; for (var i = 0; i < width[0] * height[0] * 4; i++) { pixels[i] = rgba[i]; } this.width = width; this.height = height; this.data = pixels; return this; } WebPDecoder.prototype.getData = function() { return this.data; }; /** * @license * Copyright (c) 2019 Aras Abbasi * * Licensed under the MIT License. * http://opensource.org/licenses/mit-license */ /** * jsPDF webp Support PlugIn * * @name webp_support * @module */ (function(jsPDFAPI) { jsPDFAPI.processWEBP = function(imageData, index, alias, compression) { var reader = new WebPDecoder(imageData); var width = reader.width, height = reader.height; var qu = 100; var pixels = reader.getData(); var rawImageData = { data: pixels, width: width, height: height }; var encoder = new JPEGEncoder(qu); var data = encoder.encode(rawImageData, qu); return jsPDFAPI.processJPEG.call(this, data, index, alias, compression); }; })(jsPDF.API); /** * @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.6) 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
* Example: doc.viewerPreferences({"FitWindow":true});
*
* You can set following preferences:
*
* HideToolbar (boolean)
* Default value: false
*
* HideMenubar (boolean)
* Default value: false.
*
* HideWindowUI (boolean)
* Default value: false.
*
* FitWindow (boolean)
* Default value: false.
*
* CenterWindow (boolean)
* Default value: false
*
* DisplayDocTitle (boolean)
* Default value: false.
*
* NonFullScreenPageMode (string)
* Possible values: UseNone, UseOutlines, UseThumbs, UseOC
* Default value: UseNone
*
* Direction (string)
* Possible values: L2R, R2L
* Default value: L2R.
*
* ViewArea (string)
* Possible values: MediaBox, CropBox, TrimBox, BleedBox, ArtBox
* Default value: CropBox.
*
* ViewClip (string)
* Possible values: MediaBox, CropBox, TrimBox, BleedBox, ArtBox
* Default value: CropBox
*
* PrintArea (string)
* Possible values: MediaBox, CropBox, TrimBox, BleedBox, ArtBox
* Default value: CropBox
*
* PrintClip (string)
* Possible values: MediaBox, CropBox, TrimBox, BleedBox, ArtBox
* Default value: CropBox.
*
* PrintScaling (string)
* Possible values: AppDefault, None
* Default value: AppDefault.
*
* Duplex (string)
* Possible values: Simplex, DuplexFlipLongEdge, DuplexFlipShortEdge * Default value: none
*
* PickTrayByPDFSize (boolean)
* Default value: false
*
* PrintPageRange (Array)
* Example: [[1,5], [7,9]]
* Default value: as defined by PDF viewer application
*
* NumCopies (Number)
* Possible values: 1, 2, 3, 4, 5
* Default value: 1
*
* 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 = ''; var rdf_beginning = ''; var rdf_ending = ""; var xmpmeta_ending = ""; 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>\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( "<>" ); 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.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.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.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 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) { // 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 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); if (attributeState.xmlSpace === 'default') { 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--) { if (this.contexts[r].attributeState.xmlSpace === 'default') { 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 xmlSpace = context.attributeState.xmlSpace; 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 (xmlSpace === 'default') { 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, 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); if (context.attributeState.xmlSpace === 'default' && 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 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), ph - scale * (box[1] * context.transform.sy + context.transform.ty), scale * box[2], scale * 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; 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: context.pdf.clip().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); }; 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', clTTree = 'TTree', clTCanvasWebSnapshot = 'TCanvasWebSnapshot', fPrimitives = 'fPrimitives', fFunctions = 'fFunctions', /** @summary list of registered draw functions * @private */ 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: clTLatex, icon: 'img_text', draw: () => import_more().then(h => h.drawText), direct: true }, { name: clTMathText, sameas: clTLatex }, { name: clTText, sameas: clTLatex }, { name: clTLink, sameas: clTText }, { name: clTAnnotation, sameas: clTLatex }, { 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: 'TMarker', 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', ctrl: 'dflt', noappend: true, exapnd_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 }, { 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: 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+'RHist1Drawable', icon: 'img_histo1d', class: () => import_v7('rh1').then(h => h.RH1Painter), opt: '' }, { name: nsREX+'RHist2Drawable', icon: 'img_histo2d', class: () => import_v7('rh2').then(h => h.RH2Painter), opt: '' }, { name: nsREX+'RHist3Drawable', icon: 'img_histo3d', class: () => import_v7('rh3').then(h => h.RH3Painter), opt: '' }, { name: nsREX+'RHistDisplayItem', icon: 'img_histo1d', draw: () => import_v7('rh3').then(h => h.drawHistDisplayItem), opt: '' }, { 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+'RDisplayHistStat', icon: 'img_pavetext', class: () => import_v7('pave').then(h => h.RHistStatsPainter), 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: '' } ], 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 = (kind.indexOf(prROOT) === 0) ? kind.slice(5) : `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(prROOT + 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; } /** @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 && (kind.indexOf(prROOT) === 0) && !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(prROOT + 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(prROOT + 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(prROOT + 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 { for (let i = 0; i < can_painter.painters.length; ++i) { const painter = can_painter.painters[i]; if (painter.matchObjectType(obj._typename)) { redraw_res = painter.redrawObject(obj, opt); if (redraw_res) { res_painter = painter; break; } } } } } 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 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(prROOT + 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(prROOT + 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'); 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); 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(prROOT + '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; }); case 'rh1': return Promise.resolve().then(function () { return RH1Painter$1; }); case 'rh2': return Promise.resolve().then(function () { return RH2Painter$1; }); case 'rh3': return Promise.resolve().then(function () { return RH3Painter$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 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 || !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 === 0) { 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: prROOT + 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 === undefined) || (lst.arr.length === 0)) { 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: prROOT + 'NULL', _title: 'NULL', _value: 'null', _obj: null }; } else { item = { _name: obj.fName || obj.name, _kind: prROOT + 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: prROOT + 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 = prROOT + 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 = prROOT + 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= 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 === 0) { 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 = prROOT + (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 > 0) item._more = true; } else { item._more = true; item._value = '{ }'; } } } } } else if ((typeof fld === 'number') || (typeof fld === 'boolean')) { 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.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>') + '"'; 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 === 0) && (item._childs.length === 0)) { 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: prROOT + 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 === 0) 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 (obj && node && ('_childs' in obj)) { 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 = prROOT + 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 { /** @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)}, 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); if ('_childs' in item) { for (let n = 0; n < item._childs.length; ++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 === 0) || !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 = ('_parent' in curr) ? curr._parent : null; } 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 && isStr(hitem._kind) && (hitem._kind.indexOf(prROOT) === 0)) 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) { 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 (('_icon_click' in hitem) || (handle && ('icon_click' in handle))) 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.html(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 exapndToLevel(level) { if (!level || !Number.isFinite(level) || (level < 0)) return this; const promises = []; this.toggleOpenState(true, this.h, promises); return Promise.all(promises).then(() => this.exapndToLevel(level - 1)); } /** @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 === prROOT + 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) && !evnt.ctrlKey && !evnt.shiftKey) place = kPM; // special case - one should expand item if (((place === kPM) && !('_childs' in hitem) && hitem._more) || ((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?.exapnd_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 (isStr(hitem._kind) && (hitem._kind.indexOf(prROOT) === 0) && 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 => { let p = getElementCanvPainter(frame); if (!p) p = 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('&').split('<').join('<').split('"').join('"'), el = document.createElement('div'); el.innerHTML = `x`; return el.firstChild.href; } createMenu(evnt, this).then(menu => { if ((!itemname || !hitem._parent) && !('_jsonfile' in hitem)) { let addr = '', cnt = 0; const files = [], separ = () => (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 !== 0) arg0 += `&optfit=${gStyle.fOptFit}`; if (gStyle.fOptDate !== 0) arg0 += `&optdate=${gStyle.fOptDate}`; if (gStyle.fOptFile !== 0) 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), 'Exapnd 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 === prROOT + 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' in item)) 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(); let use_dflt_opt = false; // deprecated - drawing divid was possible to code in draw options if (isStr(drawopt) && (drawopt.indexOf('divid:') >= 0)) { const pos = drawopt.indexOf('divid:'); if (!dom) dom = drawopt.slice(pos+6); drawopt = drawopt.slice(0, pos); } if (drawopt === kDfltDrawOpt) { use_dflt_opt = true; 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(prROOT + 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(prROOT + 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; 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) && p.redrawObject(obj, drawopt)) painter = p; }); if (painter) return 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 (isStr(ditem?._kind) && (ditem._kind.indexOf(prROOT) === 0)) 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.svg_this_pad().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(prROOT + 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); 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 || ('_not_monitor' in item) || ('_player' in item)) 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 || (items.length === 0)) 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('

Start I/O test

'); const tm0 = new Date(); return this.getObject(items[0]).then(() => { const tm1 = new Date(); select('#' + this.disp_frameid).append('h2').html('Item ' + items[0] + ' reading time = ' + (tm1.getTime() - tm0.getTime()) + 'ms'); 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] && dropitems[i].length > 0) { // 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 === 0) 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:') !== 0) { mdi.createFrame(frame_names[i]); doms[i] = 'frame:' + frame_names[i]; } } function dropNextItem(indx, painter) { if (painter && dropitems[indx] && (dropitems[indx].length > 0)) return h.dropItem(dropitems[indx].shift(), painter.getDom(), 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 mark_active = () => { for (let n = update.length - 1; n >= 0; --n) this.updateTreeNode(update[n]); for (let n = 0; n < active.length; ++n) this.updateBackground(active[n], force); }, find_next = (itemname, prev_found) => { if (itemname === undefined) { // extract next element if (items.length === 0) return mark_active(); 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; async function doExpandItem(_item, _obj) { 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(prROOT + _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)) { if (_item._expand(_item, _obj)) { _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; } } if (_obj && objectHierarchy(_item, _obj)) { _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; } 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; if (hitem._childs && hitem._isopen) { hitem._isopen = false; 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 ('_online' in this.h) return this.h; if (this.h._childs && ('_online' in this.h._childs[0])) 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 ('_jsonfile' in this.h) return func(this.h); if (this.h._childs) { for (let n = 0; n < this.h._childs.length; ++n) { const item = this.h._childs[n]; if ('_jsonfile' in item) 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: prROOT + 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 = ('_jsonfile' in h0) ? '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); if (this.h._childs) { for (let n = 0; n < this.h._childs.length; ++n) { const item = this.h._childs[n]; if ((item._kind === kindTFile) && ('_fullurl' in item)) 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._exapnd; 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 = prROOT + res2._typename; item._jsontmp = res2; this.updateTreeNode(item); } return res2; }); } }); } } if (h._childs.length > 0) 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; let subname = item._name; while (item._parent) { item = item._parent; if ('_file' in item) return { kind: 'file', fileurl: item._file.fURL, itemname: subname, localfile: Boolean(item._file.fLocalFile) }; if ('_jsonfile' in item) return { kind: 'json', fileurl: item._jsonfile, itemname: subname }; subname = item._name + '/' + subname; } 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 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 ('_kind' in item) 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 ('req' in dreq) req = dreq.req; if ('kind' in dreq) req_kind = dreq.kind; } } } if (!req && (item._kind.indexOf(prROOT) !== 0)) req = 'item.json.gz?compact=3'; } if (!itemname && item && ('_cached_draw_object' in this) && !req) { // special handling for online draw when cashed const obj = this._cached_draw_object; delete this._cached_draw_object; 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; let typename = 'kind:' + item._kind; if (item._kind.indexOf(prROOT) === 0) typename = item._kind.slice(5); const 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; let subname = item._name; while (item._parent) { item = item._parent; if ('_online' in item) { return { server: item._online, itemname: subname }; } subname = item._name + '/' + subname; } 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 = isStr(node._kind) ? node._kind.indexOf(prROOT) === 0 : false; 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 ('_player' in node) 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); delete this._monitoring_handle; } if (this._monitoring_frame) { cancelAnimationFrame(this._monitoring_frame); delete this._monitoring_frame; } return; } if (arg === 'frame') { // process of timeout, request animation frame delete this._monitoring_handle; this._monitoring_frame = requestAnimationFrame(this._runMonitoring.bind(this, 'draw')); return; } if (arg === 'draw') { delete this._monitoring_frame; this.updateItems(); } this._monitoring_handle = setTimeout(this._runMonitoring.bind(this, '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 > 0) { 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 ('disp' in this) { 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(prROOT + 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 > 0 || !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'); if (getOption('files_monitoring') !== null) this.files_monitoring = true; if (title && (typeof document !== 'undefined')) document.title = title; if (expanditems.length === 0 && (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 === 0) && ((getOption('item') === '') || ((jsonarr.length === 1) && (expanditems.length === 0)))) 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 > 0) promise = this.openJsonFile(jsonarr.shift()); else if (filesarr.length > 0) promise = this.openRootFile(filesarr.shift()); else if (dir) { promise = this.listServerDir(dir); dir = ''; } else if (expanditems.length > 0) promise = this.expandItem(expanditems.shift()); else if (style.length > 0) promise = this.applyStyle(style.shift()); else { return this.refreshHtml() .then(() => this.displayItems(itemsarr, optionsarr)) .then(() => 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 && ('_browser' in this.h)) { 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 (('_loadfile' in this.h) && (filesarr.length === 0)) filesarr = parseAsArray(this.h._loadfile); if (('_drawitem' in this.h) && (itemsarr.length === 0)) { itemsarr = parseAsArray(this.h._drawitem); optionsarr = parseAsArray(this.h._drawopt); } if (('_layout' in this.h) && !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 = `

JSROOT version ${version}

`; if (this.is_online) { guiCode += '

Hierarchy in json and xml format

' + '
' + ''; } 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 += '' + '
' + '' + '' + '
' + '
' + '

Read docu' + ' how to open files from other servers.

' + '
' + '' + ''; } else if (this.no_select === 'file') guiCode += '
'; if (this.is_online || !this.no_select || this.no_select === 'file') { guiCode += '' + '
'; } guiCode += `
`; this.brlayout.setBrowserContent(guiCode); const title_elem = this.brlayout.setBrowserTitle(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).html(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.innerHTML = 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.length > 0) 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.exapndToLevel(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] !== 0; } 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_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 `
` 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._cached_draw_object = 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 TText * @private */ async function drawText$1() { const text = this.getObject(), pp = this.getPadPainter(), fp = this.getFramePainter(), is_url = text.fName.startsWith('http://') || text.fName.startsWith('https://'); let pos_x = text.fX, pos_y = text.fY, use_frame = false, fact = 1, annot = this.matchObjectType(clTAnnotation); this.createAttText({ attr: text }); if (annot && fp?.mode3d && isFunc(fp?.convert3DtoPadNDC)) { const pos = fp.convert3DtoPadNDC(text.fX, text.fY, text.fZ); pos_x = pos.x; pos_y = pos.y; this.isndc = true; annot = '3d'; } else if (text.TestBit(BIT(14))) { // NDC coordinates this.isndc = true; } else if (pp.getRootPad(true)) { // force pad coordinates const d = new DrawOptions(this.getDrawOpt()); use_frame = d.check('FRAME'); } else { // place in the middle this.isndc = true; pos_x = pos_y = 0.5; text.fTextAlign = 22; } this.createG(use_frame ? 'frame2d' : undefined, is_url); this.draw_g.attr('transform', null); // remove transform from interactive changes this.pos_x = this.axisToSvg('x', pos_x, this.isndc); this.pos_y = this.axisToSvg('y', pos_y, this.isndc); this.swap_xy = use_frame && fp?.swap_xy; if (this.swap_xy) [this.pos_x, this.pos_y] = [this.pos_y, this.pos_x]; const arg = this.textatt.createArg({ x: this.pos_x, y: this.pos_y, text: text.fTitle, latex: 0 }); if ((text._typename === clTLatex) || annot) arg.latex = 1; else if (text._typename === clTMathText) { arg.latex = 2; fact = 0.8; } if (is_url) { this.draw_g.attr('href', text.fName); if (!this.isBatchMode()) this.draw_g.append('svg:title').text(`link on ${text.fName}`); } return this.startTextDrawingAsync(this.textatt.font, this.textatt.getSize(pp, fact /* , 0.05 */)) .then(() => this.drawText(arg)) .then(() => this.finishTextDrawing()) .then(() => { if (this.isBatchMode()) return this; if (pp.isButton() && !pp.isEditable()) { this.draw_g.on('click', () => this.getCanvPainter().selectActivePad(pp)); return this; } this.pos_dx = this.pos_dy = 0; if (!this.moveDrag) { this.moveDrag = function(dx, dy) { this.pos_dx += dx; this.pos_dy += dy; makeTranslate(this.draw_g, this.pos_dx, this.pos_dy); }; } if (!this.moveEnd) { this.moveEnd = function(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});;`); }; } if (annot !== '3d') addMoveHandler(this, true, is_url); else { fp.processRender3D = true; this.handleRender3D = () => { 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.draw_g, new_x - this.pos_x, new_y - this.pos_y); }; } assignContextMenu(this); this.fillContextMenuItems = function(menu) { menu.add('Change text', () => menu.input('Enter new text', text.fTitle).then(t => { text.fTitle = t; this.interactiveRedraw('pad', `exec:SetTitle("${t}")`); })); }; if (this.matchObjectType(clTLink)) { this.draw_g.style('cursor', 'pointer') .on('click', () => this.submitCanvExec('ExecuteEvent(kButton1Up, 0, 0);;')); } return this; }); } /** @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 }); this.createG(); const 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(this.draw_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.draw_g.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() { this.createG(); const 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(this.draw_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)); this.draw_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 use_frame = this.isndc ? false : new DrawOptions(this.getDrawOpt()).check('FRAME'), swap_xy = use_frame && this.getFramePainter()?.swap_xy; this.createAttMarker({ attr: marker }); 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) { this.draw_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.draw_g) makeTranslate(this.draw_g.select('path'), this.dx, this.dy); }; this.moveEnd = function(not_changed) { if (not_changed || !this.draw_g) 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 }); 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) { this.draw_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; makeTranslate(this.draw_g.select('path'), this.dx, this.dy); }; this.moveEnd = function(not_changed) { if (not_changed) 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, drawText: drawText$1 }); /** @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.floor(numselect/sizelimit); if (step <= 2) step = 2; } 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 !== 0) 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 = 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: '+ 'Opt: '+ `Num: `+ `First: `+ ''; 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('
'+ '
' + '' + 'Expr:'+ '' + '' + '
' + '

' + `
` + '
'); // 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; const val = elem.property('value'); if (val !== undefined) return val; return 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 (leaf && leaf._kind && (leaf._kind.indexOf(prROOT + '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 }); const kIsZoomed = BIT(16); // bit set when zooming on Y axis /** * @summary Painter class for THStack * * @private */ let THStackPainter$2 = class THStackPainter extends ObjectPainter { /** @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); }); delete this.firstpainter; delete this.painters; super.cleanup(); } /** @summary Build sum of all histograms * @desc Build a separate list fStack containing the running sum of all histograms */ buildStack(stack, pp) { this.fStack = 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.fStack = 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.fStack = arr; return true; } /** @summary Returns stack min/max values */ getMinMax(iserr) { const stack = this.getObject(), pad = this.getPadPainter()?.getRootPad(true), logscale = pad?.fLogv ?? (this.options.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 (this.options.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.fStack.arr.at(0), iserr).min; themax = getHistMinMax(this.fStack.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 (!this.options.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 (!this.options.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) { let hopt = opt || hist.fOption || this.options.hopt; if (hopt.toUpperCase().indexOf(this.options.hopt) < 0) hopt += ' ' + this.options.hopt; if (this.options.draw_errors && !hopt) hopt = 'E'; if (this.options.zscale) { const p = hopt.toUpperCase().indexOf('COLZ'); if (p >= 0) hopt = hopt.slice(0, p + 3) + hopt.slice(p + 4); } if (!this.options.pads) hopt += ' same nostat' + this.options.auto; return hopt; } /** @summary Draw next stack histogram */ async drawNextHisto(indx, pad_painter) { const stack = this.getObject(), hlst = this.options.nostack ? stack.fHists : this.fStack, nhists = hlst?.arr?.length || 0; if (indx >= nhists) return this; const rindx = this.options.horder ? indx : nhists-indx-1, subid = this.options.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) && !this.options.nostack) hist.$baseh = hlst.arr[rindx - 1]; // this number used for auto colors creation if (this.options.auto) hist.$num_histos = nhists; return this.drawHist(this.getPadPainter(), 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) { if (!this.options) this.options = {}; Object.assign(this.options, { ndim: 1, nostack: false, same: false, horder: true, has_errors: false, draw_errors: false, hopt: '', auto: '' }); const stack = this.getObject(), hist = stack.fHistogram || (stack.fHists ? stack.fHists.arr[0] : null) || (this.fStack ? this.fStack.arr[0] : null), hasErrors = hist2 => { const len = hist2.fSumw2?.length ?? 0; for (let n = 0; n < len; ++n) { if (hist2.fSumw2[n] > 0) return true; } return false; }; if (hist?._typename.indexOf(clTH2) === 0) this.options.ndim = 2; if ((this.options.ndim === 2) && !opt) opt = 'lego1'; if (stack.fHists && !this.options.nostack) { for (let k = 0; k < stack.fHists.arr.length; ++k) this.options.has_errors = this.options.has_errors || hasErrors(stack.fHists.arr[k]); } this.options.nhist = stack.fHists?.arr?.length ?? 1; const d = new DrawOptions(opt); this.options.nostack = d.check('NOSTACK'); if (d.check('STACK')) this.options.nostack = false; this.options.same = d.check('SAME'); d.check('NOCLEAR'); // ignore option ['PFC', 'PLC', 'PMC'].forEach(f => { if (d.check(f)) this.options.auto += ' ' + f; }); this.options.pads = d.check('PADS'); if (this.options.pads) this.options.nostack = true; this.options.hopt = d.remain().trim(); // use remaining draw options for histogram draw const dolego = d.check('LEGO'); this.options.errors = d.check('E'); this.options.zscale = d.check('COLZ'); // if any histogram appears with pre-calculated errors, use E for all histograms if (!this.options.nostack && this.options.has_errors && !dolego && !d.check('HIST') && (this.options.hopt.indexOf('E') < 0)) this.options.draw_errors = true; this.options.horder = this.options.nostack || dolego; } /** @summary Create main histogram for THStack axis drawing */ createHistogram(stack) { const histos = stack.fHists, numhistos = histos ? 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((this.options.ndim === 1) ? clTH1F : clTH2F, h0.fXaxis.fNbins, h0.fYaxis.fNbins); histo.fName = 'axis_hist'; histo.fBits |= kNoStats; Object.assign(histo.fXaxis, h0.fXaxis); if (this.options.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 ((this.options.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(); stack.fHists = obj.fHists; stack.fTitle = obj.fTitle; stack.fMinimum = obj.fMinimum; stack.fMaximum = obj.fMaximum; if (!this.options.nostack) this.options.nostack = !this.buildStack(stack, pp); if (this.firstpainter) { let src = obj.fHistogram; if (!src) src = stack.fHistogram = this.createHistogram(stack); const mm = this.getMinMax(this.options.errors || this.options.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 (this.options.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 = this.options.nostack ? stack.fHists : this.fStack, 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 = this.options.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; delete this.did_update; let pr = Promise.resolve(this); if (this.firstpainter) { const mm = this.getMinMax(this.options.errors || this.options.draw_errors); this.firstpainter.decodeOptions(this.options.hopt + mm.hopt); pr = this.firstpainter.redraw(reason); } return pr.then(() => { if (full_redraw) return this.drawNextHisto(0, this.options.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) { menu.addRedrawMenu(this); if (!this.options.pads) { menu.addchk(this.options.draw_errors, 'Draw errors', flag => { this.options.draw_errors = flag; const stack = this.getObject(), hlst = this.options.nostack ? stack.fHists : this.fStack, nhists = hlst?.arr?.length ?? 0; for (let indx = 0; indx < nhists; ++indx) { const rindx = this.options.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.options.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(); if (!skip_cleanup && pp) { this.firstpainter = null; this.painters = []; if (this.options.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 (this.options.pads) { pr = ensureTCanvas(this, false).then(() => { pad_painter = this.getPadPainter(); return pad_painter.divide(this.options.nhist, 0, true); }); } else { if (!this.options.nostack) this.options.nostack = !this.buildStack(stack, pp); if (!this.options.same && stack.fHists?.arr.length) { if (!stack.fHistogram) stack.fHistogram = this.createHistogram(stack); const mm = this.getMinMax(this.options.errors || this.options.draw_errors); pr = this.drawHist(this.getDrawDom(), stack.fHistogram, this.options.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 (!this.options.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.options.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._disable_dragging = true; if (geop._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 { /** @summary Redraw object */ redraw() { if (this.step === undefined) this.startDrawing(); } /** @summary Decode drawing options */ decodeOptions(opt) { const d = new DrawOptions(opt || 'REPEAT'); if (!this.options) this.options = {}; Object.assign(this.options, { once: d.check('ONCE'), repeat: d.check('REPEAT'), first: d.check('FIRST') }); this.storeDrawOpt(opt); } /** @summary Draw primitives */ async drawPrimitives(indx) { if (!indx) { indx = 0; this._doing_primitives = true; } const lst = this.getObject()?.fSteps.arr[this.step]; if (!lst || (indx >= lst.arr.length)) { delete this._doing_primitives; return; } return draw(this.getPadPainter(), lst.arr[indx], lst.opt[indx]).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() { if (!this.options) return; const gr = this.getObject(); if (this.options.first) { // draw only single frame, cancel all others delete this.step; return; } if (this.wait_animation_frame) { delete this.wait_animation_frame; // clear pad const pp = this.getPadPainter(); if (!pp) { // most probably, pad is cleared delete this.step; return; } // draw primitives again this.drawPrimitives().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); delete this.running_timeout; 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 (this.options.repeat) { this.step = 0; // start again sleeptime = Math.max(5000, 5*sleeptime); // increase sleep time } else { delete this.step; // clear indicator that animation running return; } } this.running_timeout = setTimeout(() => this.continueDrawing(), sleeptime); } } /** @summary Start drawing of graph time */ startDrawing() { this.step = 0; return this.drawPrimitives().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); if (painter.getMainPainter()) { console.error('Cannot draw graph time on top of other histograms'); return null; } painter.decodeOptions(opt); 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]; } painter.selfid = 'grtime_' + internals.id_counter++; // use to identify primitives which should be clean 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)) { if (x[i]+(yp-y[i])/(y[j]-y[i])*(x[j]-x[i]) ps) { tmp = ps; ps = ns; ns = tmp; swap = true; } if (ms > ns) { tmp = ns; ns = ms; ms = tmp; swap = true; } } while (swap); // store a new Delaunay triangle this.fNdt++; this.fPTried.push(ps); this.fNTried.push(ns); this.fMTried.push(ms); } // Attempt to find all the Delaunay triangles of the point set. It is not // guaranteed that it will fully succeed, and no check is made that it has // fully succeeded (such a check would be possible by referencing the points // that make up the convex hull). The method is to check if each triangle // shares all three of its sides with other triangles. If not, a point is // generated just outside the triangle on the side(s) not shared, and a new // triangle is found for that point. If this method is not working properly // (many triangles are not being found) it's probably because the new points // are too far beyond or too close to the non-shared sides. Fiddling with // the size of the `alittlebit' parameter may help. FindAllTriangles() { if (this.fAllTri) return; this.fAllTri = true; let xcntr, ycntr, xm, ym, xx, yy, sx, sy, nx, ny, mx, my, mdotn, nn, a, t1, t2, pa, na, ma, pb, nb, mb, p1=0, p2=0, m, n, p3=0; const s = [false, false, false], alittlebit = 0.0001; this.Initialize(); // start with a point that is guaranteed to be inside the hull (the // centre of the hull). The starting point is shifted "a little bit" // otherwise, in case of triangles aligned on a regular grid, we may // found none of them. xcntr = 0; ycntr = 0; for (n = 1; n <= this.fNhull; n++) { xcntr += this.fXN[this.fHullPoints[n-1]]; ycntr += this.fYN[this.fHullPoints[n-1]]; } xcntr = xcntr/this.fNhull+alittlebit; ycntr = ycntr/this.fNhull+alittlebit; // and calculate it's triangle this.Interpolate(xcntr, ycntr); // loop over all Delaunay triangles (including those constantly being // produced within the loop) and check to see if their 3 sides also // correspond to the sides of other Delaunay triangles, i.e. that they // have all their 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 !== 0) { u = (dx2*dy3-dx3*dy2)/dd1; v = (dx1*dy3-dx3*dy1)/dd2; if ((u < 0) || (v < 0)) { // No, it cannot - point m does not lie in-between n1 and n2 as // viewed from e. Replace either n1 or n2 to increase the // n1-e-n2 angle. The one to replace is the one which makes the // smallest angle with e->m vNv1 = (dx1*dx3+dy1*dy3)/Math.sqrt(dx1*dx1+dy1*dy1); vNv2 = (dx2*dx3+dy2*dy3)/Math.sqrt(dx2*dx2+dy2*dy2); if (vNv1 > vNv2) { n1 = m; phi1 = Math.atan2(dy3, dx3); phi2 = Math.atan2(dy2, dx2); } else { n2 = m; phi1 = Math.atan2(dy1, dx1); phi2 = Math.atan2(dy3, dx3); } dphi = (phi1-phi2)-(Math.floor((phi1-phi2)/(Math.PI*2))*Math.PI*2); if (dphi < 0) dphi += Math.PI*2; if ((dphi - Math.PI)*(lastdphi - Math.PI) < 0) { // The addition of point m means the angle n1-e-n2 has risen // above 180 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 tmp, swap, t1 = TI1, t2 = TI2, t3 = TI3; // order the vertices do { swap = false; if (t2 > t1) { tmp = t1; t1 = t2; t2 = tmp; swap = true; } if (t3 > t2) { tmp = t2; t2 = t3; t3 = tmp; swap = true; } } while (swap); const x1 = this.fXN[t1], x2 = this.fXN[t2], x3 = this.fXN[t3], y1 = this.fYN[t1], y2 = this.fYN[t2], y3 = this.fYN[t3], f1 = this.fZ[t1-1], f2 = this.fZ[t2-1], f3 = this.fZ[t3-1], u = (f1*(y2-y3)+f2*(y3-y1)+f3*(y1-y2))/(x1*(y2-y3)+x2*(y3-y1)+x3*(y1-y2)), v = (f1*(x2-x3)+f2*(x3-x1)+f3*(x1-x2))/(y1*(x2-x3)+y2*(x3-x1)+y3*(x1-x2)), w = f1-u*x1-v*y1; return u*this.fXN[e] + v*this.fYN[e] + w; } // Finds the Delaunay triangle that the point (xi,yi) sits in (if any) and // calculate a z-value for it by linearly interpolating the z-values that // make up that triangle. Interpolate(xx, yy) { let thevalue, it, ntris_tried, p, n, m, i, j, k, l, z, f, d, o1, o2, a, b, t1, t2, t3, /* 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.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= 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= 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(), asymm = this.matchObjectType(clTGraph2DAsymmErrors); let xmin = gr.fX[0], xmax = xmin, ymin = gr.fY[0], ymax = ymin, zmin = gr.fZ[0], zmax = zmin; for (let p = 0; p < gr.fNpoints; ++p) { const x = gr.fX[p], y = gr.fY[p], z = gr.fZ[p]; if (this.options.Error) { xmin = Math.min(xmin, x - (asymm ? gr.fEXlow[p] : gr.fEX[p])); xmax = Math.max(xmax, x + (asymm ? gr.fEXhigh[p] : gr.fEX[p])); ymin = Math.min(ymin, y - (asymm ? gr.fEYlow[p] : gr.fEY[p])); ymax = Math.max(ymax, y + (asymm ? gr.fEYhigh[p] : gr.fEY[p])); zmin = Math.min(zmin, z - (asymm ? gr.fEZlow[p] : gr.fEZ[p])); zmax = Math.max(zmax, z + (asymm ? gr.fEZhigh[p] : gr.fEZ[p])); } else { xmin = Math.min(xmin, x); xmax = Math.max(xmax, x); ymin = Math.min(ymin, y); ymax = Math.max(ymax, y); zmin = Math.min(zmin, z); zmax = Math.max(zmax, z); } } function calc_delta(min, max, margin) { if (min < max) return margin * (max - min); return Math.abs(min) < 1e5 ? 0.02 : 0.02 * Math.abs(min); } const dx = calc_delta(xmin, xmax, gr.fMargin), dy = calc_delta(ymin, ymax, gr.fMargin), dz = calc_delta(zmin, zmax, 0); let uxmin = xmin - dx, uxmax = xmax + dx, uymin = ymin - dy, uymax = ymax + dy, uzmin = zmin - dz, uzmax = zmax + dz; if ((uxmin < 0) && (xmin >= 0)) uxmin = xmin*0.98; if ((uxmax > 0) && (xmax <= 0)) uxmax = 0; if ((uymin < 0) && (ymin >= 0)) uymin = ymin*0.98; if ((uymax > 0) && (ymax <= 0)) uymax = 0; if ((uzmin < 0) && (zmin >= 0)) uzmin = zmin*0.98; if ((uzmax > 0) && (zmax <= 0)) uzmax = 0; const graph = this.getObject(); if (graph.fMinimum !== kNoZoom) uzmin = graph.fMinimum; if (graph.fMaximum !== kNoZoom) uzmax = graph.fMaximum; this._own_histogram = true; // when histogram created on client side const histo = createHistogram(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 (!this.options.isAny()) { const dulaunay = this.buildDelaunay(graph); if (dulaunay) { for (let i = 0; i < graph.fNpx; ++i) { const xx = uxmin + (i + 0.5) / graph.fNpx * (uxmax - uxmin); for (let j = 0; j < graph.fNpy; ++j) { const yy = uymin + (j + 0.5) / graph.fNpy * (uymax - uymin), zz = dulaunay.ComputeZ(xx, yy); histo.fArray[histo.getBin(i+1, j+1)] = zz; } } } } return histo; } buildDelaunay(graph) { if (!this._delaunay) { this._delaunay = new TGraphDelaunay(graph); this._delaunay.FindAllTriangles(); if (!this._delaunay.fNdt) delete this._delaunay; } return this._delaunay; } drawTriangles(fp, graph, levels, palette) { const dulaunay = this.buildDelaunay(graph); if (!dulaunay) return; const main_grz = !fp.logz ? fp.grz : value => (value < fp.scale_zmin) ? -0.1 : fp.grz(value), plain_mode = this.options.Triangles === 2, do_faces = (this.options.Triangles >= 10) || plain_mode, do_lines = (this.options.Triangles % 10 === 1) || (plain_mode && (graph.fLineColor !== graph.fFillColor)), triangles = new Triangles3DHandler(levels, main_grz, 0, 2*fp.size_z3d, do_lines); for (triangles.loop = 0; triangles.loop < 2; ++triangles.loop) { triangles.createBuffers(); for (let t = 0; t < dulaunay.fNdt; ++t) { const points = [dulaunay.fPTried[t], dulaunay.fNTried[t], dulaunay.fMTried[t]], coord = []; let use_triangle = true; for (let i = 0; i < 3; ++i) { const pnt = points[i] - 1; coord.push(fp.grx(graph.fX[pnt]), fp.gry(graph.fY[pnt]), main_grz(graph.fZ[pnt])); if ((graph.fX[pnt] < fp.scale_xmin) || (graph.fX[pnt] > fp.scale_xmax) || (graph.fY[pnt] < fp.scale_ymin) || (graph.fY[pnt] > fp.scale_ymax)) use_triangle = false; } if (do_faces && use_triangle) triangles.addMainTriangle(...coord); if (do_lines && use_triangle) { triangles.addLineSegment(coord[0], coord[1], coord[2], coord[3], coord[4], coord[5]); triangles.addLineSegment(coord[3], coord[4], coord[5], coord[6], coord[7], coord[8]); triangles.addLineSegment(coord[6], coord[7], coord[8], coord[0], coord[1], coord[2]); } } } triangles.callFuncs((lvl, pos) => { const geometry = createLegoGeom(this.getMainPainter(), pos, null, 100, 100), color = plain_mode ? this.getColor(graph.fFillColor) : palette.calcColor(lvl, levels.length), material = new 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; if (opt && (opt !== this.options.original)) this.decodeOptions(opt, obj); Object.assign(this.getObject(), obj); delete this._delaunay; // rebuild triangles delete this.$redraw_hist; // if our own histogram was used as axis drawing, we need update histogram as well if (this.axes_draw) { const hist_painter = this.getMainPainter(); hist_painter?.updateObject(this.createHistogram(), this.options.Axis); this.$redraw_hist = hist_painter; } return true; } /** @summary Redraw TGraph2D object * @desc Update histogram drawing if necessary * @return {Promise} for drawing ready */ async redraw() { let promise = Promise.resolve(true); if (this.$redraw_hist) { promise = this.$redraw_hist.redraw(); delete this.$redraw_hist; } return promise.then(() => this.drawGraph2D()); } 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(); 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; this.draw_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(); if (!graph || !main || !fp) return this; if (this.options.Contour) return this.drawContour(fp, main, graph); if (!fp.mode3d) return this; fp.remove3DMeshes(this); if (!this.options.isAny()) { // no need to draw smoothing if histogram content was drawn if (main.draw_content) return this; if ((graph.fMarkerSize === 1) && (graph.fMarkerStyle === 1)) this.options.Circles = true; else this.options.Markers = true; } const countSelected = (zmin, zmax) => { let cnt = 0; for (let i = 0; i < graph.fNpoints; ++i) { if ((graph.fX[i] >= fp.scale_xmin) && (graph.fX[i] <= fp.scale_xmax) && (graph.fY[i] >= fp.scale_ymin) && (graph.fY[i] <= fp.scale_ymax) && (graph.fZ[i] >= zmin) && (graph.fZ[i] < zmax)) ++cnt; } return cnt; }; // try to define scale-down factor let step = 1; if ((settings.OptimizeDraw > 0) && !fp.webgl) { const numselected = countSelected(fp.scale_zmin, fp.scale_zmax), sizelimit = 50000; if (numselected > sizelimit) { step = Math.floor(numselected / sizelimit); if (step <= 2) step = 2; } } const markeratt = this.createAttMarker({ attr: graph, std: false }), promises = []; let palette = null, levels = [fp.scale_zmin, fp.scale_zmax], scale = fp.size_x3d / 100 * markeratt.getFullSize(); if (this.options.Circles) scale = 0.06 * fp.size_x3d; if (fp.usesvg) scale *= 0.3; scale *= 7 * Math.max(fp.size_x3d / fp.getFrameWidth(), fp.size_z3d / fp.getFrameHeight()); if (this.options.Color || (this.options.Triangles >= 10)) { levels = main.getContourLevels(true); palette = main.getHistPalette(); } if (this.options.Triangles) this.drawTriangles(fp, graph, levels, palette); for (let lvl = 0; lvl < levels.length - 1; ++lvl) { const lvl_zmin = Math.max(levels[lvl], fp.scale_zmin), lvl_zmax = Math.min(levels[lvl+1], fp.scale_zmax); if (lvl_zmin >= lvl_zmax) continue; const size = Math.floor(countSelected(lvl_zmin, lvl_zmax) / step), index = new Int32Array(size), pnts = this.options.Markers || this.options.Circles ? new PointsCreator(size, fp.webgl, scale/3) : null, err = this.options.Error ? new Float32Array(size*6*3) : null, asymm = err && this.matchObjectType(clTGraph2DAsymmErrors), line = this.options.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 !== 0) continue; } index[icnt++] = i; // remember point index for tooltip const x = fp.grx(graph.fX[i]), y = fp.gry(graph.fY[i]), z = fp.grz(graph.fZ[i]); 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 (!this.options.Circles || this.options.Color) color = palette?.calcColor(lvl, levels.length) ?? this.getColor(graph.fMarkerColor); const pr = pnts.createPoints({ color, fill: this.options.Circles ? 'white' : undefined, style: this.options.Circles ? 4 : graph.fMarkerStyle }).then(mesh => { mesh.graph = graph; mesh.fp = fp; mesh.tip_color = (graph.fMarkerColor === 3) ? 0xFF0000 : 0x00FF00; mesh.scale0 = 0.3*scale; mesh.index = index; mesh.tip_name = this.getObjectHint(); mesh.tooltip = graph2DTooltip; fp.add3DMesh(mesh, this); }); promises.push(pr); } } return Promise.all(promises).then(() => { 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 = this.options.Zscale; if (this.options.Zscale) return pal_painter.drawPave(); pal_painter.removeG(); // completely remove drawing without need to redraw complete pad }).then(() => { fp.render3D(100); return this; }); } /** @summary draw TGraph2D object */ static async draw(dom, gr, opt) { const painter = new TGraph2DPainter(dom, gr); painter.decodeOptions(opt, gr); let promise = Promise.resolve(null); if (!painter.getMainPainter()) { // histogram is not preserved in TGraph2D promise = TH2Painter.draw(dom, painter.createHistogram(), painter.options.Axis); painter.axes_draw = true; } return promise.then(() => { painter.addToPadPrimitives(); return painter.drawGraph2D(); }); } } // class TGraph2DPainter var TGraph2DPainter$1 = /*#__PURE__*/Object.freeze({ __proto__: null, TGraph2DPainter: TGraph2DPainter }); const kNoTitle = BIT(17); /** * @summary Painter for TGraphPolargram objects. * * @private */ class TGraphPolargramPainter extends ObjectPainter { /** @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); } /** @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); if (!this.options) this.options = {}; Object.assign(this.options, { 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) { // const layer = this.getLayerSvg('primitives_layer'), // interactive = layer.select('.interactive_ellipse'); // if (interactive.empty()) return; let pnt = null; if (kind !== 'leave') { const pos = pointer(evnt, this.draw_g.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); 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.draw_g.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 (this.options.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(), rect = this.getPadPainter().getFrameRect(); this.createG(); makeTranslate(this.draw_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= 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[ticks.length - 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 = - (this.options.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; this.draw_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 (this.options.rdot) { this.draw_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; this.draw_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) { this.draw_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 this.options.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; this.draw_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; TooltipHandler.assign(this); assignContextMenu(this, kNoReorder); this.assignZoomHandler(this.draw_g); }); } /** @summary Fill TGraphPolargram context menu */ fillContextMenuItems(menu) { const pp = this.getObject(); 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, this.options.rangle || pp.fAxisAngle, v => { this.options.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'); if (!this.options) this.options = {}; const rdot = d.check('RDOT'), rangle = d.check('RANGLE', true) ? d.partAsInt() : 0; Object.assign(this.options, { 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' : '' }); if (d.check('O')) this.options.Axis += 'O'; if (rdot) this.options.Axis += '_rdot'; if (rangle) this.options.Axis += `_rangle${rangle}`; this.storeDrawOpt(opt); } /** @summary Update TGraphPolar with polargram */ updateObject(obj, opt) { if (!this.matchObjectType(obj)) return false; if (opt && (opt !== this.options.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(), main = this.getMainPainter(); if (!graph || !main?.$polargram) return; if (this.options.mark) this.createAttMarker({ attr: graph }); if (this.options.err || this.options.line || this.options.curve) this.createAttLine({ attr: graph }); if (this.options.fill) this.createAttFill({ attr: graph }); 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); } this.draw_g.attr('transform', main.draw_g.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 (this.options.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 (this.options.mark) mpath += this.markeratt.create(pos.grx, pos.gry); if (this.options.curve || this.options.line || this.options.fill) bins.push(pos); } if ((this.options.fill || this.options.line) && bins.length) { const lpath = buildSvgCurve(bins, { line: true }); if (this.options.fill) { this.draw_g.append('svg:path') .attr('d', lpath + 'Z') .style('pointer-events', pointer_events) .call(this.fillatt.func); } if (this.options.line) { this.draw_g.append('svg:path') .attr('d', lpath) .style('fill', 'none') .style('pointer-events', pointer_events) .call(this.lineatt.func); } } if (this.options.curve && bins.length) { this.draw_g.append('svg:path') .attr('d', buildSvgCurve(bins)) .style('fill', 'none') .style('pointer-events', pointer_events) .call(this.lineatt.func); } if (epath) { this.draw_g.append('svg:path') .attr('d', epath) .style('fill', 'none') .style('pointer-events', pointer_events) .call(this.lineatt.func); } if (mpath) { this.draw_g.append('svg:path') .attr('d', mpath) .style('pointer-events', pointer_events) .call(this.markeratt.func); } if (!this.isBatchMode()) { assignContextMenu(this, kNoReorder); main.assignZoomHandler(this.draw_g); } } /** @summary Create polargram object */ createPolargram(gr) { if (!gr.fPolargram) { gr.fPolargram = create$1('TGraphPolargram'); if (this.options.radian) gr.fPolargram.fRadian = true; else if (this.options.degree) gr.fPolargram.fDegree = true; else if (this.options.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.draw_g?.selectChild('.tooltip_bin'); if (!hint || !this.draw_g) { ttcircle?.remove(); return; } if (ttcircle.empty()) { ttcircle = this.draw_g.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 /** @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.$func = 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 = []; }; delete this._fail_eval; // 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) { delete this.$tmp_tooltip; const lines = [this.getObjectHint()], funcs = this.getFramePainter()?.getGrFuncs(this.options.second_x, this.options.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 ? '' : 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.draw_g?.selectChild('.tooltip_bin'); if (!this.draw_g || !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.draw_g.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) { const dummy = new ObjectPainter(dom); hist = dummy.getPadPainter()?.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.$func = 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) => (obj.fBeta_bin_params.length > bin) ? obj.fBeta_bin_params[bin].first : obj.fBeta_alpha, getBetaBeta = (obj, bin) => (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) => (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 }); class TScatterPainter extends TGraphPainter$1 { constructor(dom, obj) { super(dom, obj); this._is_scatter = true; this._not_adjust_hrange = true; } /** @summary Return drawn graph object */ getGraph() { return this.getObject()?.fGraph; } /** @summary Return margins for histogram ranges */ getHistRangeMargin() { return this.getObject()?.fMargin ?? 0.1; } /** @summary Draw axis histogram * @private */ async drawAxisHisto() { const need_histo = !this.getHistogram(), histo = this.createHistogram(need_histo, need_histo); return TH2Painter$2.draw(this.getDrawDom(), histo, this.options.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_main(); 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.fContour?.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 Actual drawing of TScatter */ async drawGraph() { const fpainter = this.get_main(), hpainter = this.getMainPainter(), scatter = this.getObject(), hist = this.getHistogram(); let scale = 1, offset = 0; if (!fpainter || !hpainter || !scatter) return; if (scatter.fColor) { const pal = this.getPalette(); if (pal) pal.$main_painter = this; const pp = this.getPadPainter(); if (!this._color_palette && isFunc(pp?.getCustomPalette)) this._color_palette = pp.getCustomPalette(); if (!this._color_palette) this._color_palette = getColorPalette(this.options.Palette, pp?.isGrayscale()); 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.fContour = new HistContour(minc, maxc); this.fContour.createNormal(30); this.fContour.configIndicies(0, 0); fpainter.zmin = minc; fpainter.zmax = maxc; if (!fpainter.zoomChangedInteractive('z') && hist && hist.fMinimum !== kNoZoom && hist.fMaximum !== kNoZoom) { fpainter.zoom_zmin = hist.fMinimum; fpainter.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; } this.createG(!fpainter.pad_layer); const funcs = fpainter.getGrFuncs(), is_zoom = (fpainter.zoom_zmin !== fpainter.zoom_zmax) && scatter.fColor; for (let i = 0; i < this.bins.length; ++i) { if (is_zoom && ((scatter.fColor[i] < fpainter.zoom_zmin) || (scatter.fColor[i] > fpainter.zoom_zmax))) continue; const pnt = this.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.fContour.getPaletteColor(this._color_palette, scatter.fColor[i]) : this.getColor(scatter.fMarkerColor), handle = new TAttMarkerHandler({ color, size, style: scatter.fMarkerStyle }); this.draw_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); class TLinePainter extends ObjectPainter { /** @summary Start interactive moving */ moveStart(x, y) { const fullsize = 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.draw_g.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.draw_g.append('svg:path') .attr('d', 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 }); /** * @summary Painter class for TRatioPlot * * @private */ const k_upper_pad = 'upper_pad', k_lower_pad = 'lower_pad', k_top_pad = 'top_pad'; class TRatioPlotPainter extends ObjectPainter { /** @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 { if (xmin < up_fp.xmin) xmin = up_fp.xmin; if (xmax > up_fp.xmax) 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 > 0) && (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 { /** @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 = []; super.cleanup(); } /** @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._funcHandler = 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._funcHandler?.drawNext(0) ?? this; delete this._funcHandler; 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._3d && 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._3d) { 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._3d ? 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._3d = d.check('3D'); this._auto = ''; // extra options for auto colors 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._3d) 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._3d ? 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._3d) 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 Draw direct TVirtualX commands into SVG * @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 pos = pointer(evnt, this.draw_g.node()), pp = this.getPadPainter(), rect = pp?.getPadRect(); if (pp && rect && this.snapid) pp.selectObjectPainter(this, { x: pos[0] + rect.x, y: pos[1] + rect.y }); // pp.deliverWebCanvasEvent('click', pos[0] + rect.x, pos[1] + rect.y, this.snapid); } /** @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; const arr = obj.fOper.split(';'), 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 = this.draw_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; } }, 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; }, 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 = this.draw_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); }; this.createG(); return process(-1).then(() => { check_attributes(); assignContextMenu(this); if (!this.isBatchMode()) this.draw_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 /** @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.$func = 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 = []; }; delete this._fail_eval; 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()], funcs = this.getFramePainter()?.getGrFuncs(this.options.second_x, this.options.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 ? '' : 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.draw_g?.selectChild('.tooltip_bin'); if (!this.draw_g || !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.draw_g.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) { const dummy = new ObjectPainter(dom); hist = dummy.getPadPainter()?.findInPrimitives('Func', clTH2F); } if (!hist) { hist = createHistogram(clTH2F, 20, 20); hist.fBits |= kNoStats; } const painter = new TF2Painter(dom, hist); painter.$func = 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 /** @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.$func = 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; }; delete this._fail_eval; 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) { const dummy = new ObjectPainter(dom); hist = dummy.getPadPainter()?.findInPrimitives('Func', clTH2F); } if (!hist) { hist = createHistogram(clTH2F, 20, 20); hist.fBits |= kNoStats; } const painter = new TF3Painter(dom, hist); painter.$func = tf3; 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 objects. * * @private */ class TSplinePainter extends ObjectPainter { /** @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 0; 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) if (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(), funcs = this.getFramePainter()?.getGrFuncs(this.options.second_x, this.options.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.draw_g?.selectChild('.tooltip_bin'); const radius = this.lineatt.width + 3; if (cleanup || !this.draw_g) { gbin?.remove(); return null; } if (gbin.empty()) { gbin = this.draw_g.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(), pmain = this.getFramePainter(), funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y), w = pmain.getFrameWidth(), h = pmain.getFrameHeight(); this.createG(true); this.knot_size = 5; // used in tooltip handling this.createAttLine({ attr: spline }); if (this.options.Line || this.options.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 (pmain.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 (pmain.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) }); } this.draw_g.append('svg:path') .attr('class', 'line') .attr('d', buildSvgCurve(bins)) .style('fill', 'none') .call(this.lineatt.func); } if (this.options.Mark) { // for tooltips use markers only if nodes where not created let path = ''; this.createAttMarker({ attr: spline }); this.markeratt.resetPos(); this.knot_size = this.markeratt.getFullSize(); 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) { this.draw_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 */) { if (axis !== 'x') return false; // spline can always be calculated and therefore one can zoom inside return Boolean(this.getObject()); } /** @summary Decode options for TSpline drawing */ decodeOptions(opt) { const d = new DrawOptions(opt); if (!this.options) this.options = {}; const has_main = Boolean(this.getMainPainter()); Object.assign(this.options, { Same: d.check('SAME'), Line: d.check('L'), Curve: d.check('C'), Mark: d.check('P'), Hopt: '', second_x: false, second_y: false }); if (!this.options.Line && !this.options.Curve && !this.options.Mark) this.options.Curve = true; if (d.check('X+')) { this.options.Hopt += 'X+'; this.options.second_x = has_main; } if (d.check('Y+')) { this.options.Hopt += 'Y+'; this.options.second_y = has_main; } 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 Drawing TArrow * @private */ class TArrowPainter extends TLinePainter { /** @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); class TPolyLinePainter extends ObjectPainter { /** @summary Dragging object * @private */ moveDrag(dx, dy) { this.dx += dx; this.dy += dy; makeTranslate(this.draw_g.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() { this.createG(); const 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])}`; this.draw_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.createG(); this.gaxis_x = x1; this.gaxis_y = y2; return this.drawAxis(this.getG(), 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 }); class TBoxPainter extends ObjectPainter { /** @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) { if (this.c_x1) this.x1 += dx; if (this.c_x2) this.x2 += dx; if (this.c_y1) this.y1 += dy; if (this.c_y2) this.y2 += dy; const nodes = this.draw_g.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.borderMode) return [path]; return [path].concat(getBoxDecorations(xx, yy, ww, hh, this.borderMode, this.borderSize, this.borderSize)); } /** @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'; 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.borderMode = (box.fBorderMode && this.fillatt.hasColor()) ? box.fBorderMode : 0; this.borderSize = box.fBorderSize || 2; const paths = this.getPathes(); this.draw_g .append('svg:path') .attr('d', paths[0]) .call(this.lineatt.func) .call(this.fillatt.func); if (this.borderMode) { this.draw_g.append('svg:path') .attr('d', paths[1]) .call(this.fillatt.func) .style('fill', rgb(this.fillatt.color).brighter(0.5).formatRgb()); this.draw_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 { /** @summary Decode options string */ decodeOptions(opt) { const d = new DrawOptions(opt); this.options = { Zscale: false }; const obj = this.getObject(); if (d.check('CONST')) { this.options.constRatio = true; if (obj) obj.fConstRatio = true; } if (d.check('Z')) this.options.Zscale = true; } /** @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 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.fContour = { 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.fContour.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 Draw image */ async drawImage() { const obj = this.getObject(), fp = this.getFramePainter(), rect = fp?.getFrameRect() ?? this.getPadPainter().getPadRect(); this.wheel_zoomy = true; 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.options.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(); 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(this.options.Zscale, 'Color palette', flag => { this.options.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 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; this._color_palette = true; // to emulate behavior of hist painter } 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) { this.options.Zscale = !this.options.Zscale; return this.drawColorPalette(this.options.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 kNormal = 1, /* kLessTraffic = 2, */ kOffline = 3; class RObjectPainter extends ObjectPainter { #pending_request; 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() { const pp = super.addToPadPrimitives(); 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?._auto_color_cnt !== undefined) { const pal = pp.getHistPalette(), cnt = pp._auto_color_cnt++; let num = pp._num_primitives - 1; if (num < 2) num = 2; val = pal ? pal.getColorOrdinal((cnt % num) / num) : 'blue'; 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.snapid) 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.snapid); 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.snapid) { 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) { this.snapid = id; if (this.snapid && 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._websocket) 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 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 => (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); if (this.nticks > 20) this.nticks = 20; const gr_range = Math.abs(this.gr_range) || 100; if (this.kind === kAxisTime) { if (this.nticks > 8) 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() { if (this.nmajor >= this.major.length) return null; return 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 > 0)) { 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 11) break; // not too many digits, anyway it will be exponential lbls = []; indx = 0; totallen = 0; } // 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 = this.labelsFont.angle !== 0, 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) { let maxwidth = this.gap_before*0.45 + this.gap_after*0.45; if (!this.gap_before) maxwidth = 0.9*this.gap_after; else if (!this.gap_after) maxwidth = 0.9*this.gap_before; 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), // draw labels labelsPromise = this.optionUnlab ? Promise.resolve(tgaps) : this.drawLabels(axis_g, side, tgaps); 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 }); this.createG(); this.standalone = true; // no need to clean axis container const promise = this.drawAxis(this.draw_g, makeTranslate(pos.x, pos.y)); if (this.isBatchMode()) return promise; return promise.then(() => { if (settings.ContextMenu) { this.draw_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) }); this.draw_g.on('dblclick', () => this.zoomStandalone()); if (settings.ZoomWheel) { this.draw_g.on('wheel', evnt => { evnt.stopPropagation(); evnt.preventDefault(); const pos2 = pointer(evnt, this.draw_g.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 { /** @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.keys_handler = null; 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.enabledKeys = on && settings.HandleKeys; // used only in 3D mode if (this.control) this.control.enableKeys = this.enabledKeys; } setLastEventPos(pnt) { // set position of last context menu event, can be this.fLastEventPnt = pnt; } getLastEventPos() { // return position of last event return this.fLastEventPnt; } /** @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); } } /** @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); let sidex = 1, sidey = 1; if (this.v7EvalAttr('swapX', false)) sidex = -1; if (this.v7EvalAttr('swapY', false)) sidey = -1; const 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.snapid); 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.snapid); 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.snapid); 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()?._fast_drawing) 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.snapid); 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.snapid); 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, swap_xy: this.swap_xy, fp: this, 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.draw_g?.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.draw_g?.selectChild('.main_layer').selectAll('*').remove(); this.draw_g?.selectChild('.upper_layer').selectAll('*').remove(); } /** @summary Fully cleanup frame * @private */ cleanup() { this.cleanFrameDrawings(); if (this.draw_g) { this.draw_g.selectAll('*').remove(); this.draw_g.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 = null; } delete this.enabledKeys; delete this.self_drawaxes; delete this.xaxis; delete this.yaxis; delete this.zaxis; delete this.x2axis; delete this.y2axis; delete this.draw_g; // frame element managed by the pad delete this._click_handler; delete this._dblclick_handler; const pp = this.getPadPainter(); if (pp?.frame_painter_ref === this) delete pp.frame_painter_ref; super.cleanup(); } /** @summary Redraw frame * @private */ redraw() { const pp = this.getPadPainter(); if (pp) pp.frame_painter_ref = this; // 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)); let w = Math.round(rect.width * (this.fX2NDC - this.fX1NDC)), h = Math.round(rect.height * (this.fY2NDC - this.fY1NDC)), rotate = false, fixpos = false, trans; if (pp?.options) { if (pp.options.RotateFrame) rotate = true; if (pp.options.FixFrame) fixpos = true; } if (rotate) { trans = `rotate(-90,${lm},${tm}) translate(${lm-h},${tm})`; [w, h] = [h, w]; } else trans = makeTranslate(lm, tm); // update values here to let access even when frame is not really updated this._frame_x = lm; this._frame_y = tm; this._frame_width = w; this._frame_height = h; this._frame_rotate = rotate; this._frame_fixpos = fixpos; this._frame_trans = trans; 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 this.draw_g = this.getFrameSvg(); let top_rect, main_svg; if (this.draw_g.empty()) { this.draw_g = this.getLayerSvg('primitives_layer').append('svg:g').attr('class', 'root_frame'); if (!this.isBatchMode()) this.draw_g.append('svg:title').text(''); top_rect = this.draw_g.append('svg:rect'); main_svg = this.draw_g.append('svg:svg') .attr('class', 'main_layer') .attr('x', 0) .attr('y', 0) .attr('overflow', 'hidden'); this.draw_g.append('svg:g').attr('class', 'axis_layer'); this.draw_g.append('svg:g').attr('class', 'upper_layer'); } else { top_rect = this.draw_g.selectChild('rect'); main_svg = this.draw_g.selectChild('.main_layer'); } this.axes_drawn = false; this.draw_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.draw_g?.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 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 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] }, 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()) return; FrameInteractive.assign(this); this.addFrameKeysHandler(); } /** @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 { #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 /** @summary constructor */ constructor(dom, pad, iscan) { super(dom, pad, '', 'pad'); this.pad = pad; this.iscan = iscan; // indicate if working with canvas this.this_pad_name = ''; if (!this.iscan && (pad !== null)) { if (pad.fObjectID) this.this_pad_name = 'pad' + pad.fObjectID; // use objectid as pad name else this.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; } /** @summary Indicates that drawing runs in batch mode * @private */ isBatchMode() { if (this.batch_mode !== undefined) return this.batch_mode; if (isBatchMode()) return true; if (!this.iscan && this.has_canvas) return this.getCanvPainter()?.isBatchMode(); return false; } /** @summary Indicates that is 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 SVG element for the pad itself * @private */ svg_this_pad() { return this.getPadSvg(this.this_pad_name); } /** @summary Returns main painter on the pad * @desc Typically main painter is TH1/TH2 object which is drawing axes * @private */ getMainPainter() { return this.main_painter_ref || null; } /** @summary Assign main painter on the pad * @private */ setMainPainter(painter, force) { if (!this.main_painter_ref || force) this.main_painter_ref = painter; } /** @summary cleanup pad and all primitives inside */ cleanup() { if (this.#doing_draw) console.error('pad drawing is not completed when cleanup is called'); this.painters.forEach(p => p.cleanup()); const svg_p = this.svg_this_pad(); if (!svg_p.empty()) { svg_p.property('pad_painter', null); if (!this.iscan) svg_p.remove(); } const cp = this.iscan || !this.has_canvas ? this : this.getCanvPainter(); if (cp) delete cp.pads_cache; delete this.main_painter_ref; delete this.frame_painter_ref; this.#pad_x = this.#pad_y = this.#pad_width = this.#pad_height = undefined; this.#doing_draw = undefined; delete this._dfltRFont; this.painters = []; this.pad = null; this.assignObject(null); this.pad_frame = null; this.this_pad_name = undefined; this.has_canvas = false; selectActivePad({ pp: this, active: false }); super.cleanup(); } /** @summary Returns frame painter inside the pad * @private */ getFramePainter() { return this.frame_painter_ref; } /** @summary get pad width */ getPadWidth() { return this.#pad_width || 0; } /** @summary get pad height */ getPadHeight() { return this.#pad_height || 0; } /** @summary get pad 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 */) { 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.main_painter_ref === painter) { delete this.main_painter_ref; 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 */) { 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 * @private */ getNumPainters() { return this.painters.length; } /** @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 istoppad = (this.iscan || !this.has_canvas), canp = istoppad ? this : this.getCanvPainter(); if (painter === undefined) painter = this; if (pos && !istoppad) pos = getAbsPosInCanvas(this.svg_this_pad(), pos); selectActivePad({ pp: this, active: true }); canp.producePadEvent('select', this, painter, pos); } /** @summary Set fast drawing property depending on the size * @private */ setFastDrawing(w, h) { const was_fast = this._fast_drawing; this._fast_drawing = (this.snapid === undefined) && settings.SmallPad && ((w < settings.SmallPad.width) || (h < settings.SmallPad.height)); if (was_fast !== this._fast_drawing) this.showPadButtons(); } /** @summary Returns true if canvas configured with grayscale * @private */ isGrayscale() { 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', this.this_pad_name); frect = svg.selectChild('.canvas_fillrect'); } else { const render_to = this.selectDom(); if (render_to.style('position') === 'static') render_to.style('position', 'relative'); svg = render_to.append('svg') .attr('class', 'jsroot root_canvas') .property('pad_painter', this) // this is custom property .property('redraw_by_resize', false); // could be enabled to force redraw by each resize this.setTopPainter(); // assign canvas as top painter of that element if (!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 && 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.snapid === undefined) { 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 (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 canvas itself for enlarge if (is_dblclick && this._websocket && (this.enlargeMain('state') === 'off')) return; const svg_can = this.getCanvSvg(), pad_enlarged = svg_can.property('pad_enlarged'); if (this.iscan || !this.has_canvas || (!pad_enlarged && !this.hasObjectsToDraw() && !this.painters)) { if (this._fixed_size) return; // canvas cannot be enlarged in such mode if (!this.enlargeMain(is_escape ? false : 'toggle')) return; if (this.enlargeMain('state') === 'off') svg_can.property('pad_enlarged', null); else selectActivePad({ pp: this, active: true }); } else if (!pad_enlarged && !is_escape) { this.enlargeMain(true, true); svg_can.property('pad_enlarged', this.pad); selectActivePad({ pp: this, active: true }); } else if (pad_enlarged === this.pad) { this.enlargeMain(false); svg_can.property('pad_enlarged', null); } else if (!is_escape && is_dblclick) console.error('missmatch with pad double click events'); return this.checkResize(true); } /** @summary Create SVG element for the pad * @return true when pad is displayed and all its items should be redrawn */ createPadSvg(only_resize) { if (!this.has_canvas) { this.createCanvasSvg(only_resize ? 2 : 0); return true; } const svg_parent = this.getPadSvg(this.pad_name), // this.pad_name MUST be here to select parent pad 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.svg_this_pad(); svg_rect = svg_pad.selectChild('.root_pad_border'); if (!this.isBatchMode()) btns = this.getLayerSvg('btns_layer', this.this_pad_name); 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.this_pad_name, true) .attr('pad', this.this_pad_name) // set extra attribute to mark pad name .property('pad_painter', this); // this is custom property if (!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.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 === 0) 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.iscan) this._start_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_tm) { const spenttm = new Date().getTime() - this._start_tm; if (spenttm > 3000) console.log(`Canvas drawing took ${(spenttm*1e-3).toFixed(2)}s`); delete this._start_tm; } 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 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.iscan ? 'RCanvas' : 'RPad'; menu.header(clname, `${urlClassPrefix}ROOT_1_1Experimental_1_1${clname}.html`); menu.addchk(this.isTooltipAllowed(), 'Show tooltips', () => this.setTooltipAllowed('toggle')); if (!this._websocket) { menu.addAttributesMenu(this); if (this.iscan) { menu.addSettingsMenu(false, false, arg => { if (arg === 'dark') this.changeDarkMode(); }); } } menu.separator(); if (isFunc(this.hasMenuBar) && isFunc(this.actiavteMenuBar)) menu.addchk(this.hasMenuBar(), 'Menu bar', flag => this.actiavteMenuBar(flag)); if (isFunc(this.hasEventStatus) && isFunc(this.activateStatusBar) && isFunc(this.canStatusBar)) { if (this.canStatusBar()) menu.addchk(this.hasEventStatus(), 'Event status', () => this.activateStatusBar('toggle')); } if (this.enlargeMain() || (this.has_canvas && this.hasObjectsToDraw())) menu.addchk((this.enlargeMain('state') === 'on'), 'Enlarge ' + (this.iscan ? 'canvas' : 'pad'), () => this.enlargePad()); const fname = this.this_pad_name || (this.iscan ? 'canvas' : 'pad'); menu.sub('Save as'); ['svg', 'png', 'jpeg', 'pdf', 'webp'].forEach(fmt => menu.add(`${fname}.${fmt}`, () => this.saveAs(fmt, this.iscan, `${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 || sub.this_pad_name) res = sub.redraw(reason); if (isPromise(res)) return res.then(() => redrawNext(indx)); } return true; }; return sync_promise.then(() => { if (this.iscan) this.createCanvasSvg(2); else showsubitems = this.createPadSvg(true); return redrawNext(0); }).then(() => { this.addPadInteractive(); 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.svg_this_pad(); 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) return false; if (!this.iscan && this.has_canvas) return false; const sync_promise = this.syncDraw('canvas_resize'); if (sync_promise === false) return false; if ((size === true) || (size === false)) { force = size; size = null; } if (isObject(size) && size.force) force = true; if (!force) force = this.needRedrawByResize(); let changed = false; const redrawNext = indx => { if (!changed || (indx >= this.painters.length)) { this.confirmDraw(); return changed; } return getPromise(this.painters[indx].redraw(force ? 'redraw' : 'resize')).then(() => redrawNext(indx+1)); }; return sync_promise.then(() => { changed = this.createCanvasSvg(force ? 2 : 1, size); if (changed && this.iscan && this.pad && this.online_canvas && !this.embed_canvas && !this.isBatchMode()) { if (this._resize_tmout) clearTimeout(this._resize_tmout); this._resize_tmout = setTimeout(() => { delete this._resize_tmout; if (!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.iscan) { 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.snapid === undefined)) { // 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._root_colors || 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._root_colors); }; // 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 ? lst.length : 0; this._auto_color_cnt = 0; } delete this.next_rstyle; ++indx; // change to the next snap if (!lst || indx >= lst.length) { delete this._auto_color_cnt; 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._root_colors = 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.snapid === snap.fObjectID) { objpainter = subp; break; } else if (subp.snapid && !subp.isSecondary() && !is_subpad) { console.warn(`Mismatch in snapid between painter ${subp?.snapid} 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 subpad = snap, // not sub-pad, but just attributes padpainter = new RPadPainter(this, subpad, false); padpainter.decodeOptions(''); padpainter.addToPadPrimitives(); 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.snapid)) return this; if (!this.painters) return null; for (let k=0; k { 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.iscan) 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 (!isStr(sub.snapid) || 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.snapid) { 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) { delete this.pads_cache; const old_painters = this.painters; this.painters = []; old_painters.forEach(objp => objp.cleanup()); delete this.main_painter_ref; 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.svg_this_pad().node().getBoundingClientRect(), evnt = { clientX: rrr.left+10, clientY: rrr.top + 10 }; // use timeout to avoid conflict with mouse click and automatic menu close if (name === 'pad') return postponePromise(() => this.padContextMenu(evnt), 50); let selp = null, selkind; switch (name) { case 'xaxis': case 'yaxis': case 'zaxis': selp = this.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.snapid) 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.this_pad_name || (this.iscan ? 'canvas' : 'pad')) + '.' + kind; this.produceImage(full_canvas, kind).then(imgdata => { if (!imgdata) return console.error(`Fail to produce image ${filename}`); if ((browser.qt6 || browser.cef3) && this.snapid) { console.warn(`sending file ${filename} to server`); let res = imgdata; if (kind !== 'svg') { const separ = res.indexOf('base64,'); res = (separ > 0) ? res.slice(separ+7) : ''; } if (res) this.getCanvPainter()?.sendWebsocket(`SAVE:${filename}:${res}`); } else 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(this.this_pad_name) : (full_canvas ? this.getCanvSvg() : this.svg_this_pad()), painter = (full_canvas && !use_frame) ? this.getCanvPainter() : this, items = []; // keep list of replaced elements, which should be moved back at the end if (elem.empty()) return ''; if (use_frame || !full_canvas) { const defs = this.getCanvSvg().selectChild('.canvas_defs'); if (!defs.empty()) { items.push({ prnt: this.getCanvSvg(), defs }); elem.node().insertBefore(defs.node(), elem.node().firstChild); } } if (!use_frame) { // do not make transformations for the frame painter.forEachPainterInPad(pp => { const item = { prnt: pp.svg_this_pad() }; items.push(item); // remove buttons from each sub-pad const btns = pp.getLayerSvg('btns_layer', this.this_pad_name); item.btns_node = btns.node(); if (item.btns_node) { item.btns_prnt = item.btns_node.parentNode; item.btns_next = item.btns_node.nextSibling; btns.remove(); } const fp = pp.getFramePainter(); if (!isFunc(fp?.access3dKind)) return; const can3d = fp.access3dKind(); if ((can3d !== constants$1.Embed3D.Overlay) && (can3d !== constants$1.Embed3D.Embed)) return; let main, canvas; if (isFunc(fp.render3D)) { main = fp; canvas = fp.renderer?.domElement; } else { main = fp.getMainPainter(); canvas = main?._renderer?.domElement; } if (!isFunc(main?.render3D) || !isObject(canvas)) return; const sz2 = fp.getSizeFor3d(constants$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 = main.getFrameSvg(); item.frame_node = svg_frame.node(); if (item.frame_node) { item.frame_next = item.frame_node.nextSibling; svg_frame.remove(); } // add svg image item.img = item.prnt.insert('image', '.primitives_layer') // create image object .attr('x', sz2.x) .attr('y', sz2.y) .attr('width', canvas.width) .attr('height', canvas.height) .attr('href', dataUrl); }, 'pads'); } let width = elem.property('draw_width'), height = elem.property('draw_height'); if (use_frame) { const fp = this.getFramePainter(); width = fp.getFrameWidth(); height = fp.getFrameHeight(); } const arg = (file_format === 'pdf') ? { node: elem.node(), width, height, reset_tranform: use_frame } : compressSVG(`${elem.node().innerHTML}`); 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'); if (this.iscan) menu.add('Canvas', 'pad', this.itemContextMenu); else menu.add('Pad', 'pad', this.itemContextMenu); if (this.getFramePainter()) menu.add('Frame', 'frame', this.itemContextMenu); const main = this.getMainPainter(); // here 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 }); const iscan = this.iscan || !this.has_canvas; if (!iscan && (funcname.indexOf('Pad') !== 0) && (funcname !== 'enlargePad')) { const cp = this.getCanvPainter(); if (cp && (cp !== this)) cp.addPadButton(btn, tooltip, funcname); } } /** @summary Add buttons for pad or canvas * @private */ addPadButtons(is_online) { this.addPadButton('camera', 'Create PNG', this.iscan ? 'CanvasSnapShot' : 'PadSnapShot', 'Ctrl PrintScreen'); if (settings.ContextMenu) this.addPadButton('question', 'Access context menus', 'PadContextMenus'); const add_enlarge = !this.iscan && this.has_canvas && this.hasObjectsToDraw(); if (add_enlarge || this.enlargeMain('verify')) this.addPadButton('circle', 'Enlarge canvas', 'enlargePad'); if (is_online && this.brlayout) { this.addPadButton('diamand', 'Toggle Ged', 'ToggleGed'); this.addPadButton('three_circles', 'Toggle Status', 'ToggleStatus'); } } /** @summary 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) => (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); if (!this.options) this.options = {}; Object.assign(this.options, { GlobalColors: true, LocalColors: false, IgnorePalette: false, RotateFrame: false, FixFrame: false }); if (d.check('NOCOLORS') || d.check('NOCOL')) this.options.GlobalColors = this.options.LocalColors = false; if (d.check('LCOLORS') || d.check('LCOL')) { this.options.GlobalColors = false; this.options.LocalColors = true; } if (d.check('NOPALETTE') || d.check('NOPAL')) this.options.IgnorePalette = true; if (d.check('ROTATE')) this.options.RotateFrame = true; if (d.check('FIXFRAME')) this.options.FixFrame = true; if (d.check('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, false); painter.decodeOptions(opt); if (painter.getCanvSvg().empty()) { painter.has_canvas = false; painter.this_pad_name = ''; painter.setTopPainter(); } else painter.addToPadPrimitives(); // must be here due to pad painter painter.createPadSvg(); if (painter.matchObjectType(clTPad) && (!painter.has_canvas || 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 { /** @summary constructor */ constructor(dom, canvas) { super(dom, canvas, true); this._websocket = null; this.tooltip_allowed = settings.Tooltip; this.v7canvas = true; } /** @summary Cleanup canvas painter */ cleanup() { delete this._websocket; delete this._submreq; if (this._changed_layout) this.setLayoutKind('simple'); delete this._changed_layout; super.cleanup(); } /** @summary Returns canvas name */ getCanvasName() { const title = this.pad?.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 true if message can be send via web socket * @private */ canSendWebSocket() { return this._websocket?.canSend(); } /** @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(); delete this._websocket; } } /** @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.snapid && 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._websocket || !req || !req._typename || !painter.snapid || !isStr(painter.snapid)) 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.snapid; 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(); if (!this._submreq) this._submreq = {}; 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) { // snapid is intentionally ignored - only painter.snapid has to be used if (!this._websocket) return; 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 || !reply.reqid || !this._submreq) return false; const req = this._submreq[reply.reqid]; if (!req) return false; // remove reference first delete this._submreq[reply.reqid]; // 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._websocket || !this._websocket.canSend(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?.snapid) 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('
Loading GED ...
'); 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._websocket?.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); painter.normal_canvas = !nocanvas; 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); painter.normal_canvas = false; 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 pr = painter.getCanvSvg().empty() ? RCanvasPainter.draw(painter.getDom(), null /* noframe */) : Promise.resolve(true); return pr.then(() => { if ((frame_kind !== false) && painter.getFrameSvg().selectChild('.main_layer').empty()) return RFramePainter.draw(painter.getDom(), null, isStr(frame_kind) ? frame_kind : ''); }).then(() => { painter.addToPadPrimitives(); 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 } this.createG(); makeTranslate(this.draw_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() { delete this.fContour; }, 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 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, false) .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, false).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); this.createG(clipping ? 'main_layer' : (onframe ? 'upper_layer' : false)); this.createv7AttLine(); this.draw_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); this.createG(clipping ? 'main_layer' : (onframe ? 'upper_layer' : false)); this.createv7AttLine('border_'); this.createv7AttFill(); this.draw_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); this.createG(clipping ? 'main_layer' : (onframe ? 'upper_layer' : false)); this.createv7AttMarker(); const path = this.markeratt.create(p.x, p.y); if (path) { this.draw_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(), framep = this.getFramePainter(); if (!contour) return console.log('no contour - no palette'); // frame painter must be there if (!framep) return console.log('no frame painter - no palette'); const zmin = contour.at(0), zmax = contour.at(-1), rect = framep.getFrameRect(), pad_width = this.getPadPainter().getPadWidth(), pad_height = this.getPadPainter().getPadHeight(), visible = this.v7EvalAttr('visible', true), vertical = this.v7EvalAttr('vertical', true); 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(this.draw_g, palette_x, palette_y); } let g_btns = this.draw_g.selectChild('.colbtns'); if (g_btns.empty()) g_btns = this.draw_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) framep.z_handle.configureAxis('zaxis', gmin, gmax, zmin, zmax, true, [palette_height, 0], -palette_height, { reverse: false }); else framep.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(framep.z_handle.gr(contour[i])), z1 = Math.round(framep.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', () => framep.unzoom('z')); } framep.z_handle.maxTickSize = Math.round(palette_width*0.3); const promise = framep.z_handle.drawAxis(this.draw_g, makeTranslate(vertical ? palette_width : 0, palette_height), vertical ? -1 : 1); if (this.isBatchMode() || drag) return promise; return promise.then(() => { if (settings.ContextMenu) { this.draw_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(); }); framep.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.draw_g.node()); if (moving_labels) return framep.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) framep.z_handle.processLabelsMove('stop', last_pos); else { const z = framep.z_handle.func, z1 = z.invert(sel1), z2 = z.invert(sel2); this.getFramePainter().zoom('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.draw_g.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 = framep.z_handle.processLabelsMove('start', last_pos); }, 500); }, assignHandlers = () => { this.draw_g.selectAll('.axis_zoom, .axis_labels') .on('mousedown', startRectSel) .on('dblclick', () => framep.unzoom('z')); if (settings.ZoomWheel) { this.draw_g.on('wheel', evnt => { evnt.stopPropagation(); evnt.preventDefault(); const pos = pointer(evnt, this.draw_g.node()), coord = vertical ? (1 - pos[1] / palette_height) : pos[0] / palette_width, item = framep.z_handle.analyzeWheelEvent(evnt, coord); if (item.changed) framep.zoom('z', item.min, item.max); }); } }; framep.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, false).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); this.createG(); this.draw_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(this.draw_g, pave_x, pave_y); this.draw_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) this.draw_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.draw_g.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, false).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.draw_g .append('svg:path') .attr('d', `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.draw_g .append('svg:path') .attr('d', `M${Math.round(margin_x)},${Math.round(posy + stepy/2)}h${w4}`) .call(objp.lineatt.func); } if (entry.fError && objp?.lineatt) { this.draw_g .append('svg:path') .attr('d', `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.draw_g.append('svg:path') .attr('d', 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, false).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(), textFont = this.v7EvalFont('text', { size: 12, color: 'black', align: 22 }), width = this.pave_width, height = this.pave_height, nlines = pavetext.fText.length; if (!nlines) return; const 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, false).then(() => painter.drawPave()); } } // class RPaveTextPainter /** * @summary Painter for RHistStats class * * @private */ class RHistStatsPainter extends RPavePainter { /** @summary clear entries from stat box */ clearStat() { this.stats_lines = []; } /** @summary add text entry to stat box */ addText(line) { this.stats_lines.push(line); } /** @summary update statistic from the server */ updateStatistic(reply) { this.stats_lines = reply.lines; this.drawStatistic(this.stats_lines); } /** @summary fill statistic */ fillStatistic() { const pp = this.getPadPainter(); if (pp?._fast_drawing) return false; const obj = this.getObject(); if (obj.fLines !== undefined) { this.stats_lines = obj.fLines; delete obj.fLines; return true; } if (this.v7OfflineMode()) { const main = this.getMainPainter(); if (!isFunc(main?.fillStatistic)) return false; // we take statistic from main painter return main.fillStatistic(this, gStyle.fOptStat, gStyle.fOptFit); } // show lines which are exists, maybe server request will be received later return (this.stats_lines !== undefined); } /** @summary Draw content */ async drawContent() { if (this.fillStatistic()) return this.drawStatistic(this.stats_lines); return this; } /** @summary Change mask */ changeMask(nbit) { const obj = this.getObject(), mask = 1 << nbit; if (obj.fShowMask & mask) obj.fShowMask &= ~mask; else obj.fShowMask |= mask; if (this.fillStatistic()) this.drawStatistic(this.stats_lines); } /** @summary Context menu */ statsContextMenu(evnt) { evnt.preventDefault(); evnt.stopPropagation(); // disable main context menu createMenu(evnt, this).then(menu => { const obj = this.getObject(), action = this.changeMask.bind(this); menu.header('Stat Box'); for (let n = 0; n < obj.fEntries.length; ++n) menu.addchk((obj.fShowMask & (1< menu.show()); } /** @summary Draw statistic */ async drawStatistic(lines) { if (!lines) return this; const textFont = this.v7EvalFont('stats_text', { size: 12, color: 'black', align: 22 }), width = this.pave_width, height = this.pave_height, nlines = lines.length; let first_stat = 0, num_cols = 0, maxlen = 0; // 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 = 0.02 * width; let has_head = false, text_g = this.draw_g.selectChild('.statlines'); if (text_g.empty()) text_g = this.draw_g.append('svg:g').attr('class', 'statlines'); else text_g.selectAll('*').remove(); textFont.setSize(height/(nlines * 1.2)); return this.startTextDrawingAsync(textFont, 'font', text_g).then(() => { if (nlines === 1) this.drawText({ width, height, text: lines[0], latex: 1, draw_g: text_g }); else { for (let j = 0; j < nlines; ++j) { const posy = j*stepy; 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: posy, latex: 0, width: width/num_cols, height: stepy, text: parts[n], draw_g: text_g }); } } 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: posy, width: width - 2*margin_x, height: stepy, text: lines[j], draw_g: text_g }); } 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: posy, width: width-2*margin_x, height: stepy, text: parts[n], draw_g: text_g, _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, this.draw_g); } }; args.push(arg); } for (let n = 0; n < 2; ++n) this.drawText(args[n]); } } } let lpath = ''; if (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.draw_g.append('svg:path').attr('d', lpath); return this.finishTextDrawing(text_g); }); } /** @summary Redraw stats box */ async redraw(reason) { if (reason && isStr(reason) && (reason.indexOf('zoom') === 0) && this.v7NormalMode()) { const req = { _typename: `${nsREX}RHistStatBoxBase::RRequest`, mask: this.getObject().fShowMask // lines to show in stat box }; this.v7SubmitRequest('stat', req, reply => this.updateStatistic(reply)); } return this.drawPave(); } /** @summary draw RHistStats object */ static async draw(dom, stats, opt) { const painter = new RHistStatsPainter(dom, stats, opt, stats); return ensureRCanvas(painter, false).then(() => painter.drawPave()); } } // class RHistStatsPainter var RPavePainter$1 = /*#__PURE__*/Object.freeze({ __proto__: null, RHistStatsPainter: RHistStatsPainter, RLegendPainter: RLegendPainter, RPavePainter: RPavePainter, RPaveTextPainter: RPaveTextPainter }); /** @summary assign methods for the RAxis objects * @private */ function assignRAxisMethods(axis) { if ((axis._typename === `${nsREX}RAxisEquidistant`) || (axis._typename === `${nsREX}RAxisLabels`)) { if (axis.fInvBinWidth === 0) { axis.$dummy = true; axis.fInvBinWidth = 1; axis.fNBinsNoOver = 0; axis.fLow = 0; } axis.min = axis.fLow; axis.max = axis.fLow + axis.fNBinsNoOver/axis.fInvBinWidth; axis.GetNumBins = function() { return this.fNBinsNoOver; }; axis.GetBinCoord = function(bin) { return this.fLow + bin/this.fInvBinWidth; }; axis.FindBin = function(x, add) { return Math.floor((x - this.fLow)*this.fInvBinWidth + add); }; } else if (axis._typename === `${nsREX}RAxisIrregular`) { axis.min = axis.fBinBorders.at(0); axis.max = axis.fBinBorders.at(-1); axis.GetNumBins = function() { return this.fBinBorders.length; }; axis.GetBinCoord = function(bin) { const indx = Math.round(bin); if (indx <= 0) return this.fBinBorders.at(0); if (indx >= this.fBinBorders.length) return this.fBinBorders.at(-1); if (indx === bin) return this.fBinBorders[indx]; const indx2 = (bin < indx) ? indx - 1 : indx + 1; return this.fBinBorders[indx] * Math.abs(bin-indx2) + this.fBinBorders[indx2] * Math.abs(bin-indx); }; axis.FindBin = function(x, add) { for (let k = 1; k < this.fBinBorders.length; ++k) if (x < this.fBinBorders[k]) return Math.floor(k-1+add); return this.fBinBorders.length - 1; }; } // to support some code from ROOT6 drawing axis.GetBinCenter = function(bin) { return this.GetBinCoord(bin-0.5); }; axis.GetBinLowEdge = function(bin) { return this.GetBinCoord(bin-1); }; } /** @summary Returns real histogram impl * @private */ function getHImpl(obj) { return obj?.fHistImpl?.fIO || null; } /** @summary Base painter class for RHist objects * * @private */ class RHistPainter extends RObjectPainter { /** @summary Constructor * @param {object|string} dom - DOM element for drawing or element id * @param {object} histo - RHist object */ constructor(dom, histo) { super(dom, histo); this.csstype = 'hist'; this.draw_content = true; this.nbinsx = 0; this.nbinsy = 0; this.mode3d = false; // initialize histogram methods this.getHisto(true); } /** @summary Returns true if RHistDisplayItem is used */ isDisplayItem() { return this.getObject()?.fAxes; } /** @summary get histogram */ getHisto(force) { const obj = this.getObject(); let histo = getHImpl(obj); if (histo && (!histo.getBinContent || force)) { if (histo.fAxes._2) { assignRAxisMethods(histo.fAxes._0); assignRAxisMethods(histo.fAxes._1); assignRAxisMethods(histo.fAxes._2); histo.getBin = function(x, y, z) { return (x-1) + this.fAxes._0.GetNumBins()*(y-1) + this.fAxes._0.GetNumBins()*this.fAxes._1.GetNumBins()*(z-1); }; // all normal ROOT methods uses indx+1 logic, but RHist has no underflow/overflow bins now histo.getBinContent = function(x, y, z) { return this.fStatistics.fBinContent[this.getBin(x, y, z)]; }; histo.getBinError = function(x, y, z) { const bin = this.getBin(x, y, z); if (this.fStatistics.fSumWeightsSquared) return Math.sqrt(this.fStatistics.fSumWeightsSquared[bin]); return Math.sqrt(Math.abs(this.fStatistics.fBinContent[bin])); }; } else if (histo.fAxes._1) { assignRAxisMethods(histo.fAxes._0); assignRAxisMethods(histo.fAxes._1); histo.getBin = function(x, y) { return (x-1) + this.fAxes._0.GetNumBins()*(y-1); }; // all normal ROOT methods uses indx+1 logic, but RHist has no underflow/overflow bins now histo.getBinContent = function(x, y) { return this.fStatistics.fBinContent[this.getBin(x, y)]; }; histo.getBinError = function(x, y) { const bin = this.getBin(x, y); if (this.fStatistics.fSumWeightsSquared) return Math.sqrt(this.fStatistics.fSumWeightsSquared[bin]); return Math.sqrt(Math.abs(this.fStatistics.fBinContent[bin])); }; } else { assignRAxisMethods(histo.fAxes._0); histo.getBin = function(x) { return x-1; }; // all normal ROOT methods uses indx+1 logic, but RHist has no underflow/overflow bins now histo.getBinContent = function(x) { return this.fStatistics.fBinContent[x-1]; }; histo.getBinError = function(x) { if (this.fStatistics.fSumWeightsSquared) return Math.sqrt(this.fStatistics.fSumWeightsSquared[x-1]); return Math.sqrt(Math.abs(this.fStatistics.fBinContent[x-1])); }; } } else if (!histo && obj?.fAxes) { // case of RHistDisplayItem histo = obj; if (!histo.getBinContent || force) { if (histo.fAxes.length === 3) { assignRAxisMethods(histo.fAxes[0]); assignRAxisMethods(histo.fAxes[1]); assignRAxisMethods(histo.fAxes[2]); histo.nx = histo.fIndicies[1] - histo.fIndicies[0]; histo.dx = histo.fIndicies[0] + 1; histo.stepx = histo.fIndicies[2]; histo.ny = histo.fIndicies[4] - histo.fIndicies[3]; histo.dy = histo.fIndicies[3] + 1; histo.stepy = histo.fIndicies[5]; histo.nz = histo.fIndicies[7] - histo.fIndicies[6]; histo.dz = histo.fIndicies[6] + 1; histo.stepz = histo.fIndicies[8]; // this is index in original histogram histo.getBin = function(x, y, z) { return (x-1) + this.fAxes[0].GetNumBins()*(y-1) + this.fAxes[0].GetNumBins()*this.fAxes[1].GetNumBins()*(z-1); }; // this is index in current available data if ((histo.stepx > 1) || (histo.stepy > 1) || (histo.stepz > 1)) histo.getBin0 = function(x, y, z) { return Math.floor((x-this.dx)/this.stepx) + this.nx/this.stepx*Math.floor((y-this.dy)/this.stepy) + this.nx/this.stepx*this.ny/this.stepy*Math.floor((z-this.dz)/this.stepz); }; else histo.getBin0 = function(x, y, z) { return (x-this.dx) + this.nx*(y-this.dy) + this.nx*this.ny*(z-this.dz); }; histo.getBinContent = function(x, y, z) { return this.fBinContent[this.getBin0(x, y, z)]; }; histo.getBinError = function(x, y, z) { return Math.sqrt(Math.abs(this.getBinContent(x, y, z))); }; } else if (histo.fAxes.length === 2) { assignRAxisMethods(histo.fAxes[0]); assignRAxisMethods(histo.fAxes[1]); histo.nx = histo.fIndicies[1] - histo.fIndicies[0]; histo.dx = histo.fIndicies[0] + 1; histo.stepx = histo.fIndicies[2]; histo.ny = histo.fIndicies[4] - histo.fIndicies[3]; histo.dy = histo.fIndicies[3] + 1; histo.stepy = histo.fIndicies[5]; // this is index in original histogram histo.getBin = function(x, y) { return (x-1) + this.fAxes[0].GetNumBins()*(y-1); }; // this is index in current available data if ((histo.stepx > 1) || (histo.stepy > 1)) histo.getBin0 = function(x, y) { return Math.floor((x-this.dx)/this.stepx) + this.nx/this.stepx*Math.floor((y-this.dy)/this.stepy); }; else histo.getBin0 = function(x, y) { return (x-this.dx) + this.nx*(y-this.dy); }; histo.getBinContent = function(x, y) { return this.fBinContent[this.getBin0(x, y)]; }; histo.getBinError = function(x, y) { return Math.sqrt(Math.abs(this.getBinContent(x, y))); }; } else { assignRAxisMethods(histo.fAxes[0]); histo.nx = histo.fIndicies[1] - histo.fIndicies[0]; histo.dx = histo.fIndicies[0] + 1; histo.stepx = histo.fIndicies[2]; histo.getBin = function(x) { return x-1; }; if (histo.stepx > 1) histo.getBin0 = function(x) { return Math.floor((x-this.dx)/this.stepx); }; else histo.getBin0 = function(x) { return x-this.dx; }; histo.getBinContent = function(x) { return this.fBinContent[this.getBin0(x)]; }; histo.getBinError = function(x) { return Math.sqrt(Math.abs(this.getBinContent(x))); }; } } } return histo; } /** @summary Decode options */ decodeOptions(/* opt */) { if (!this.options) this.options = { Hist: 1, System: 1 }; } /** @summary Copy draw options from other painter */ copyOptionsFrom(src) { if (src === this) return; const o = this.options, o0 = src.options; o.Mode3D = o0.Mode3D; } /** @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 Clear 3d drawings - if any */ clear3DScene() { const fp = this.getFramePainter(); if (isFunc(fp?.create3DScene)) fp.create3DScene(-1); this.mode3d = false; } /** @summary Cleanup hist painter */ cleanup() { this.clear3DScene(); delete this.options; super.cleanup(); } /** @summary Returns histogram dimension */ getDimension() { return 1; } /** @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 Draw axes */ async drawFrameAxes() { // return true when axes was drawn const main = this.getFramePainter(); if (!main) return false; if (!this.draw_content) return true; if (!this.isMainPainter()) { if (!this.options.second_x && !this.options.second_y) return true; main.setAxes2Ranges(this.options.second_x, this.getAxis('x'), this.xmin, this.xmax, this.options.second_y, this.getAxis('y'), this.ymin, this.ymax); return main.drawAxes2(this.options.second_x, this.options.second_y); } main.cleanupAxes(); main.xmin = main.xmax = 0; main.ymin = main.ymax = 0; main.zmin = main.zmax = 0; main.setAxesRanges(this.getAxis('x'), this.xmin, this.xmax, this.getAxis('y'), this.ymin, this.ymax, this.getAxis('z'), this.zmin, this.zmax); return main.drawAxes(); } /** @summary create attributes */ createHistDrawAttributes() { this.createv7AttFill(); this.createv7AttLine(); } /** @summary update display item */ updateDisplayItem(obj, src) { if (!obj || !src) return false; obj.fAxes = src.fAxes; obj.fIndicies = src.fIndicies; obj.fBinContent = src.fBinContent; obj.fContMin = src.fContMin; obj.fContMinPos = src.fContMinPos; obj.fContMax = src.fContMax; // update histogram attributes this.getHisto(true); return true; } /** @summary update histogram object */ updateObject(obj /* , opt */) { const origin = this.getObject(); if (obj !== origin) { if (!this.matchObjectType(obj)) return false; if (this.isDisplayItem()) this.updateDisplayItem(origin, obj); else { const horigin = getHImpl(origin), hobj = getHImpl(obj); if (!horigin || !hobj) return false; // make it easy - copy statistics without axes horigin.fStatistics = hobj.fStatistics; origin.fTitle = obj.fTitle; } } this.scanContent(); this.histogram_updated = true; // indicate that object updated return true; } /** @summary Get axis object */ getAxis(name) { const histo = this.getHisto(), obj = this.getObject(); let axis; if (obj?.fAxes) { switch (name) { case 'x': axis = obj.fAxes[0]; break; case 'y': axis = obj.fAxes[1]; break; case 'z': axis = obj.fAxes[2]; break; default: axis = obj.fAxes[0]; break; } } else if (histo?.fAxes) { switch (name) { case 'x': axis = histo.fAxes._0; break; case 'y': axis = histo.fAxes._1; break; case 'z': axis = histo.fAxes._2; break; default: axis = histo.fAxes._0; break; } } if (axis && !axis.GetBinCoord) assignRAxisMethods(axis); return axis; } /** @summary Get tip text for axis bin */ getAxisBinTip(name, bin, step) { const pmain = this.getFramePainter(), handle = pmain[`${name}_handle`], axis = this.getAxis(name), x1 = axis.GetBinCoord(bin); if (handle.kind === kAxisLabels) return pmain.axisAsText(name, x1); const x2 = axis.GetBinCoord(bin+(step || 1)); if (handle.kind === kAxisTime) return pmain.axisAsText(name, (x1+x2)/2); return `[${pmain.axisAsText(name, x1)}, ${pmain.axisAsText(name, x2)})`; } /** @summary Extract axes ranges and bins numbers * @desc Also here ensured that all axes objects got their necessary methods */ extractAxesProperties(ndim) { const histo = this.getHisto(); if (!histo) return; this.nbinsx = this.nbinsy = this.nbinsz = 0; let axis = this.getAxis('x'); this.nbinsx = axis.GetNumBins(); this.xmin = axis.min; this.xmax = axis.max; if (ndim < 2) return; axis = this.getAxis('y'); this.nbinsy = axis.GetNumBins(); this.ymin = axis.min; this.ymax = axis.max; if (ndim < 3) return; axis = this.getAxis('z'); this.nbinsz = axis.GetNumBins(); this.zmin = axis.min; this.zmax = axis.max; } /** @summary Add interactive features, only main painter does it */ addInteractivity() { // only first painter in list allowed to add interactive functionality to the frame const ismain = this.isMainPainter(), second_axis = this.options.second_x || this.options.second_y, fp = ismain || second_axis ? this.getFramePainter() : null; return fp?.addInteractivity(!ismain && second_axis) ?? true; } /** @summary Process item reply */ processItemReply(reply, req) { if (!this.isDisplayItem()) return console.error('Get item when display normal histogram'); if (req.reqid === this.current_item_reqid) { if (reply !== null) this.updateDisplayItem(this.getObject(), reply.item); req.resolveFunc(true); } } /** @summary Special method to request bins from server if existing data insufficient * @return {Promise} when ready */ async drawingBins(reason) { let is_axes_zoomed = false; if (reason && isStr(reason) && (reason.indexOf('zoom') === 0)) { if (reason.indexOf('0') > 0) is_axes_zoomed = true; if ((this.getDimension() > 1) && (reason.indexOf('1') > 0)) is_axes_zoomed = true; if ((this.getDimension() > 2) && (reason.indexOf('2') > 0)) is_axes_zoomed = true; } if (this.isDisplayItem() && is_axes_zoomed && this.v7NormalMode()) { const handle = this.prepareDraw({ only_indexes: true }); // submit request if histogram data not enough for display if (handle.incomplete) { return new Promise(resolveFunc => { // use empty kind to always submit request const req = this.v7SubmitRequest('', { _typename: `${nsREX}RHistDrawableBase::RRequest` }, this.processItemReply.bind(this)); if (req) { this.current_item_reqid = req.reqid; // ignore all previous requests, only this one will be processed req.resolveFunc = resolveFunc; setTimeout(this.processItemReply.bind(this, null, req), 1000); // after 1 s draw something that we can } else resolveFunc(true); }); } } return true; } /** @summary Toggle statistic box drawing * @desc Not yet implemented */ toggleStat(/* arg */) {} /** @summary get selected index for axis */ getSelectIndex(axis, size, add) { // be aware - here indexes starts from 0 const taxis = this.getAxis(axis), nbins = this['nbins'+axis] || 0; if (this.options.second_x && axis === 'x') axis = 'x2'; if (this.options.second_y && axis === 'y') axis = 'y2'; const main = this.getFramePainter(), min = main ? main[`zoom_${axis}min`] : 0, max = main ? main[`zoom_${axis}max`] : 0; let indx; if ((min !== max) && taxis) { if (size === 'left') indx = taxis.FindBin(min, add || 0); else indx = taxis.FindBin(max, (add || 0) + 0.5); if (indx < 0) indx = 0; else if (indx > nbins) indx = nbins; } else indx = (size === 'left') ? 0 : nbins; return indx; } /** @summary Auto zoom into histogram non-empty range * @abstract */ autoZoom() {} /** @summary Process click on histogram-defined buttons */ clickButton(funcname) { const fp = this.getFramePainter(); if (!fp) return false; switch (funcname) { case 'ToggleZoom': if ((this.zoom_xmin !== this.zoom_xmax) || (this.zoom_ymin !== this.zoom_ymax) || (this.zoom_zmin !== this.zoom_zmax)) { const res = this.unzoom(); fp.zoomChangedInteractive('reset'); return res; } 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()); } return false; } /** @summary Fill pad toolbar with hist-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'); if (this.draw_content) pp.addPadButton('statbox', 'Toggle stat box', 'ToggleStatBox'); if (!not_shown) pp.showPadButtons(); } /** @summary Return histo bin errors * @private */ getBinErrors(histo, bin /* , binz */) { const err = histo.getBinError(bin); return { low: err, up: err }; } /** @summary get tool tips used in 3d mode */ get3DToolTip(indx) { const histo = this.getHisto(), tip = { bin: indx, name: histo.fName || 'histo', title: histo.fTitle }; switch (this.getDimension()) { case 1: tip.ix = indx + 1; tip.iy = 1; tip.value = histo.getBinContent(tip.ix); tip.error = histo.getBinError(tip.ix); tip.lines = this.getBinTooltips(indx-1); break; case 2: tip.ix = (indx % this.nbinsx) + 1; tip.iy = (indx - (tip.ix - 1)) / this.nbinsx + 1; tip.value = histo.getBinContent(tip.ix, tip.iy); tip.error = histo.getBinError(tip.ix, tip.iy); tip.lines = this.getBinTooltips(tip.ix-1, tip.iy-1); break; case 3: tip.ix = indx % this.nbinsx + 1; tip.iy = ((indx - (tip.ix - 1)) / this.nbinsx) % this.nbinsy + 1; tip.iz = (indx - (tip.ix - 1) - (tip.iy - 1) * this.nbinsx) / this.nbinsx / this.nbinsy + 1; tip.value = histo.getBinContent(tip.ix, tip.iy, tip.iz); tip.error = histo.getBinError(tip.ix, tip.iy, tip.iz); tip.lines = this.getBinTooltips(tip.ix-1, tip.iy-1, tip.iz-1); break; } return tip; } /** @summary Create contour levels for currently selected Z range */ createContour(main, palette, args) { if (!main || !palette) return; if (!args) args = {}; let nlevels = gStyle.fNumberContours, zmin = this.minbin, zmax = this.maxbin, zminpos = this.minposbin; if (args.scatter_plot) { if (nlevels > 50) nlevels = 50; zmin = this.minposbin; } if (zmin === zmax) { zmin = this.gminbin; zmax = this.gmaxbin; zminpos = this.gminposbin; } if (this.getDimension() < 3) { if (main.zoom_zmin !== main.zoom_zmax) { zmin = main.zoom_zmin; zmax = main.zoom_zmax; } else if (args.full_z_range) { zmin = main.zmin; zmax = main.zmax; } } palette.setFullRange(main.zmin, main.zmax); palette.createContour(main.logz, nlevels, zmin, zmax, zminpos); if (this.getDimension() < 3) { main.scale_zmin = palette.colzmin; main.scale_zmax = palette.colzmax; } } /** @summary Start dialog to modify range of axis where histogram values are displayed */ changeValuesRange(menu, arg) { const pmain = this.getFramePainter(); if (!pmain) return; const prefix = pmain.isAxisZoomed(arg) ? 'zoom_' + arg : arg, curr = '[' + pmain[`${prefix}min`] + ',' + pmain[`${prefix}max`] + ']'; menu.input('Enter values range for axis ' + arg + ' like [0,100] or empty string to unzoom', curr).then(res => { res = res ? JSON.parse(res) : []; if (!isObject(res) || (res.length !== 2) || !Number.isFinite(res[0]) || !Number.isFinite(res[1])) pmain.unzoom(arg); else pmain.zoom(arg, res[0], res[1]); }); } /** @summary Fill histogram context menu */ fillContextMenuItems(menu) { if (this.draw_content) { menu.addchk(this.toggleStat('only-check'), 'Show statbox', () => this.toggleStat()); if (this.getDimension() === 2) menu.add('Values range', () => this.changeValuesRange(menu, 'z')); if (isFunc(this.fillHistContextMenu)) this.fillHistContextMenu(menu); } const fp = this.getFramePainter(); if (this.options.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 && main.mode3d && isFunc(main.highlightBin3D)) main.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(); }); } if (this.draw_content) { menu.addchk(!this.options.Zero, 'Suppress zeros', () => { this.options.Zero = !this.options.Zero; this.redrawPad(); }); if ((this.options.Lego === 12) || (this.options.Lego === 14)) this.fillPaletteMenu(menu); } 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 Update palette drawing */ updatePaletteDraw() { if (this.isMainPainter()) this.getPadPainter().findPainterFor(undefined, undefined, `${nsREX}RPaletteDrawable`)?.drawPalette(); } /** @summary Fill menu entries for palette */ fillPaletteMenu(menu) { menu.addPaletteMenu(this.options.Palette || settings.Palette, arg => { // TODO: rewrite for RPalette functionality this.options.Palette = parseInt(arg); this.redraw(); // redraw histogram }); } /** @summary Toggle 3D drawing mode */ toggleMode3D() { this.options.Mode3D = !this.options.Mode3D; if (this.options.Mode3D) { if (!this.options.Surf && !this.options.Lego && !this.options.Error) { if ((this.nbinsx >= 50) || (this.nbinsy >= 50)) this.options.Lego = this.options.Color ? 14 : 13; else this.options.Lego = this.options.Color ? 12 : 1; this.options.Zero = false; // do not show zeros by default } } this.copyOptionsToOthers(); return this.interactiveRedraw('pad', 'drawopt'); } /** @summary Calculate histogram indices and axes values for each visible bin */ prepareDraw(args) { if (!args) args = { rounding: true, extra: 0, middle: 0 }; if (args.extra === undefined) args.extra = 0; if (args.right_extra === undefined) args.right_extra = args.extra; if (args.middle === undefined) args.middle = 0; const histo = this.getHisto(), xaxis = this.getAxis('x'), yaxis = this.getAxis('y'), pmain = this.getFramePainter(), hdim = this.getDimension(), res = { i1: this.getSelectIndex('x', 'left', 0 - args.extra), i2: this.getSelectIndex('x', 'right', 1 + args.right_extra), j1: (hdim < 2) ? 0 : this.getSelectIndex('y', 'left', 0 - args.extra), j2: (hdim < 2) ? 1 : this.getSelectIndex('y', 'right', 1 + args.right_extra), k1: (hdim < 3) ? 0 : this.getSelectIndex('z', 'left', 0 - args.extra), k2: (hdim < 3) ? 1 : this.getSelectIndex('z', 'right', 1 + args.right_extra), stepi: 1, stepj: 1, stepk: 1, min: 0, max: 0, sumz: 0, xbar1: 0, xbar2: 1, ybar1: 0, ybar2: 1 }; let i, j, x, y, binz, binarea; if (this.isDisplayItem() && histo.fIndicies) { if (res.i1 < histo.fIndicies[0]) { res.i1 = histo.fIndicies[0]; res.incomplete = true; } if (res.i2 > histo.fIndicies[1]) { res.i2 = histo.fIndicies[1]; res.incomplete = true; } res.stepi = histo.fIndicies[2]; if (res.stepi > 1) res.incomplete = true; if ((hdim > 1) && (histo.fIndicies.length > 5)) { if (res.j1 < histo.fIndicies[3]) { res.j1 = histo.fIndicies[3]; res.incomplete = true; } if (res.j2 > histo.fIndicies[4]) { res.j2 = histo.fIndicies[4]; res.incomplete = true; } res.stepj = histo.fIndicies[5]; if (res.stepj > 1) res.incomplete = true; } if ((hdim > 2) && (histo.fIndicies.length > 8)) { if (res.k1 < histo.fIndicies[6]) { res.k1 = histo.fIndicies[6]; res.incomplete = true; } if (res.k2 > histo.fIndicies[7]) { res.k2 = histo.fIndicies[7]; res.incomplete = true; } res.stepk = histo.fIndicies[8]; if (res.stepk > 1) res.incomplete = true; } } if (args.only_indexes) return res; // no need for Float32Array, plain Array is 10% faster // reserve more places to avoid complex boundary checks res.grx = new Array(res.i2+res.stepi+1); res.gry = new Array(res.j2+res.stepj+1); if (args.original) { res.original = true; res.origx = new Array(res.i2+1); res.origy = new Array(res.j2+1); } if (args.pixel_density) args.rounding = true; const funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y); // 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] < -pmain.size_x3d) { res.i1 = i; res.grx[i] = -pmain.size_x3d; } if (res.grx[i] > pmain.size_x3d) { res.i2 = i; res.grx[i] = pmain.size_x3d; } } } if (args.use3d) { if ((res.i1 < res.i2-2) && (res.grx[res.i1] === res.grx[res.i1+1])) res.i1++; if ((res.i1 < res.i2-2) && (res.grx[res.i2-1] === res.grx[res.i2])) res.i2--; } // copy last valid value to higher indices while (i < res.i2 + res.stepi + 1) res.grx[i++] = res.grx[res.i2]; 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] < -pmain.size_y3d) { res.j1 = j; res.gry[j] = -pmain.size_y3d; } if (res.gry[j] > pmain.size_y3d) { res.j2 = j; res.gry[j] = pmain.size_y3d; } } } } if (args.use3d && (hdim > 1)) { if ((res.j1 < res.j2-2) && (res.gry[res.j1] === res.gry[res.j1+1])) res.j1++; if ((res.j1 < res.j2-2) && (res.gry[res.j2-1] === res.gry[res.j2])) res.j2--; } // copy last valid value to higher indices if (hdim > 1) { while (j < res.j2 + res.stepj + 1) res.gry[j++] = res.gry[res.j2]; } // find min/max values in selected range let is_first = true; this.minposbin = 0; for (i = res.i1; i < res.i2; i += res.stepi) { for (j = res.j1; j < res.j2; j += res.stepj) { binz = histo.getBinContent(i + 1, j + 1); if (!Number.isFinite(binz)) continue; res.sumz += binz; if (args.pixel_density) { binarea = (res.grx[i+res.stepi] - res.grx[i]) * (res.gry[j] - res.gry[j+res.stepj]); 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; res.palette = pmain.getHistPalette(); if (res.palette) this.createContour(pmain, res.palette, args); return res; } } // class RHistPainter /** * @summary Painter for RH1 classes * * @private */ let RH1Painter$2 = class RH1Painter extends RHistPainter { /** @summary Constructor * @param {object|string} dom - DOM element or id * @param {object} histo - histogram object */ constructor(dom, histo) { super(dom, histo); this.wheel_zoomy = false; } /** @summary Scan content */ scanContent(when_axis_changed) { // if when_axis_changed === true specified, content will be scanned after axis zoom changed const histo = this.getHisto(); if (!histo) return; if (!this.nbinsx && when_axis_changed) when_axis_changed = false; if (!when_axis_changed) this.extractAxesProperties(1); let hmin = 0, hmin_nz = 0, hmax = 0, hsum = 0; if (this.isDisplayItem()) { // take min/max values from the display item hmin = histo.fContMin; hmin_nz = histo.fContMinPos; hmax = histo.fContMax; hsum = hmax; } else { const left = this.getSelectIndex('x', 'left'), right = this.getSelectIndex('x', 'right'); if (when_axis_changed) if ((left === this.scan_xleft) && (right === this.scan_xright)) return; this.scan_xleft = left; this.scan_xright = right; let first = true, value, err; for (let i = 0; i < this.nbinsx; ++i) { value = histo.getBinContent(i+1); hsum += value; if ((i=right)) continue; if (value > 0) if ((hmin_nz === 0) || (value= 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 { const dy = (hmax - hmin) * 0.05; this.ymin = hmin - dy; if ((this.ymin < 0) && (hmin >= 0)) this.ymin = 0; this.ymax = hmax + dy; } } } /** @summary Count statistic */ countStat(cond) { const histo = this.getHisto(), xaxis = this.getAxis('x'), left = this.getSelectIndex('x', 'left'), right = this.getSelectIndex('x', 'right'), stat_sumwy = 0, stat_sumwy2 = 0, res = { name: 'histo', meanx: 0, meany: 0, rmsx: 0, rmsy: 0, integral: 0, entries: this.stat_entries, xmax: 0, wmax: 0 }; let stat_sumw = 0, stat_sumwx = 0, stat_sumwx2 = 0, i, xmax = null, wmax = null; for (i = left; i < right; ++i) { const xx = xaxis.GetBinCoord(i+0.5); if (cond && !cond(xx)) continue; const w = histo.getBinContent(i + 1); if ((xmax === null) || (w > wmax)) { xmax = xx; wmax = w; } stat_sumw += w; stat_sumwx += w * xx; stat_sumwx2 += w * xx**2; } res.integral = 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; } return res; } /** @summary Fill statistic */ fillStatistic(stat, dostat /* , dofit */) { const histo = this.getHisto(), data = this.countStat(), 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; // make empty at the beginning stat.clearStat(); 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 = ' + 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.getBinContent(0), 'entries')); if (print_over > 0) stat.addText('Overflow = ' + stat.format(histo.getBinContent(this.nbinsx+1), 'entries')); if (print_integral > 0) stat.addText('Integral = ' + stat.format(data.integral, 'entries')); if (print_skew > 0) stat.addText('Skew = '); if (print_kurt > 0) stat.addText('Kurt = '); return true; } /** @summary Get baseline for bar drawings * @private */ getBarBaseline(funcs, height) { let gry = funcs.swap_xy ? 0 : height; if (Number.isFinite(this.options.BaseLine) && (this.options.BaseLine >= funcs.scale_ymin)) gry = Math.round(funcs.gry(this.options.BaseLine)); return gry; } /** @summary Draw histogram as bars */ async drawBars(handle, funcs, width, height) { this.createG(true); const left = handle.i1, right = handle.i2, di = handle.stepi, pmain = this.getFramePainter(), histo = this.getHisto(), xaxis = this.getAxis('x'); let i, x1, x2, grx1, grx2, y, gry1, w, bars = '', barsl = '', barsr = ''; const gry2 = this.getBarBaseline(funcs, height); for (i = left; i < right; i += di) { x1 = xaxis.GetBinCoord(i); x2 = xaxis.GetBinCoord(i+di); if (funcs.logx && (x2 <= 0)) continue; grx1 = Math.round(funcs.grx(x1)); grx2 = Math.round(funcs.grx(x2)); y = histo.getBinContent(i+1); if (funcs.logy && (y < funcs.scale_ymin)) continue; gry1 = Math.round(funcs.gry(y)); w = grx2 - grx1; grx1 += Math.round(this.options.BarOffset*w); w = Math.round(this.options.BarWidth*w); if (pmain.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 (this.options.BarStyle > 0) { grx2 = grx1 + w; w = Math.round(w / 10); if (pmain.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 (this.fillatt.empty()) this.fillatt.setSolidColor('blue'); if (bars) { this.draw_g.append('svg:path') .attr('d', bars) .call(this.fillatt.func); } if (barsl) { this.draw_g.append('svg:path') .attr('d', barsl) .call(this.fillatt.func) .style('fill', rgb(this.fillatt.color).brighter(0.5).formatRgb()); } if (barsr) { this.draw_g.append('svg:path') .attr('d', barsr) .call(this.fillatt.func) .style('fill', rgb(this.fillatt.color).darker(0.5).formatRgb()); } return true; } /** @summary Draw histogram as filled errors */ async drawFilledErrors(handle, funcs /* , width, height */) { this.createG(true); const left = handle.i1, right = handle.i2, di = handle.stepi, histo = this.getHisto(), xaxis = this.getAxis('x'), bins1 = [], bins2 = []; let i, x, grx, y, yerr, gry; for (i = left; i < right; i += di) { x = xaxis.GetBinCoord(i+0.5); if (funcs.logx && (x <= 0)) continue; grx = Math.round(funcs.grx(x)); y = histo.getBinContent(i+1); yerr = histo.getBinError(i+1); if (funcs.logy && (y-yerr < funcs.scale_ymin)) continue; gry = Math.round(funcs.gry(y + yerr)); bins1.push({ grx, gry }); gry = Math.round(funcs.gry(y - yerr)); bins2.unshift({ grx, gry }); } const path1 = buildSvgCurve(bins1, { line: this.options.ErrorKind !== 4 }), path2 = buildSvgCurve(bins2, { line: this.options.ErrorKind !== 4, cmd: 'L' }); if (this.fillatt.empty()) this.fillatt.setSolidColor('blue'); this.draw_g.append('svg:path') .attr('d', path1 + path2 + 'Z') .call(this.fillatt.func); return true; } /** @summary Draw 1D histogram as SVG */ async draw1DBins() { const pmain = this.getFramePainter(), rect = pmain.getFrameRect(); if (!this.draw_content || (rect.width <= 0) || (rect.height <= 0)) { this.removeG(); return false; } this.createHistDrawAttributes(); const handle = this.prepareDraw({ extra: 1, only_indexes: true }), funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y); if (this.options.Bar) return this.drawBars(handle, funcs, rect.width, rect.height); if ((this.options.ErrorKind === 3) || (this.options.ErrorKind === 4)) return this.drawFilledErrors(handle, funcs, rect.width, rect.height); return this.drawHistBins(handle, funcs, rect.width, rect.height); } /** @summary Draw histogram bins */ async drawHistBins(handle, funcs, width, height) { this.createG(true); const options = this.options, left = handle.i1, right = handle.i2, di = handle.stepi, histo = this.getHisto(), want_tooltip = !this.isBatchMode() && settings.Tooltip, xaxis = this.getAxis('x'), exclude_zero = !options.Zero, show_errors = options.Error, show_line = options.Line, show_text = options.Text; let show_markers = options.Mark, res = '', lastbin = false, startx, 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 = null, hints_err = null, endx = '', endy = '', dend = 0, my, yerr1, yerr2, bincont, binerr, mx1, mx2, midx, text_font, pr = Promise.resolve(); if (show_errors && !show_markers && (this.v7EvalAttr('marker_style', 1) > 1)) show_markers = true; if (options.ErrorKind === 2) { if (this.fillatt.empty()) show_markers = true; else path_fill = ''; } else if (options.Error) { path_err = ''; hints_err = want_tooltip ? '' : null; } if (show_line) path_line = ''; if (show_markers) { // draw markers also when e2 option was specified this.createv7AttMarker(); if (this.markeratt.size > 0) { // simply use relative move from point, can optimize in the future path_marker = ''; this.markeratt.resetPos(); } else show_markers = false; } if (show_text) { text_font = this.v7EvalFont('text', { size: 20, color: 'black', align: 22 }); if (!text_font.angle && !options.TextKind) { const space = width / (right - left + 1); if (space < 3 * text_font.size) { text_font.setAngle(270); text_font.setSize(Math.round(space*0.7)); } } pr = this.startTextDrawingAsync(text_font, 'font'); } return pr.then(() => { // 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 let use_minmax = ((right-left) > 3*width); if (options.ErrorKind === 1) { const lw = this.lineatt.width + gStyle.fEndErrorSize; endx = `m0,${lw}v${ -2*lw}m0,${lw}`; endy = `m${lw},0h${ -2*lw}m${lw},0`; dend = Math.floor((this.lineatt.width-1)/2); } const draw_markers = show_errors || show_markers; if (draw_markers || show_text || show_line) use_minmax = true; const draw_bin = besti => { bincont = histo.getBinContent(besti+1); if (!exclude_zero || (bincont !== 0)) { mx1 = Math.round(funcs.grx(xaxis.GetBinCoord(besti))); mx2 = Math.round(funcs.grx(xaxis.GetBinCoord(besti+di))); midx = Math.round((mx1+mx2)/2); my = Math.round(funcs.gry(bincont)); yerr1 = yerr2 = 20; if (show_errors) { binerr = histo.getBinError(besti+1); yerr1 = Math.round(my - funcs.gry(bincont + binerr)); // up yerr2 = Math.round(funcs.gry(bincont - binerr) - my); // down } if (show_text && (bincont !== 0)) { const lbl = (bincont === Math.round(bincont)) ? bincont.toString() : floatToString(bincont, gStyle.fPaintTextFormat); if (text_font.angle) this.drawText({ align: 12, x: midx, y: Math.round(my - 2 - text_font.size / 5), text: lbl, latex: 0 }); else this.drawText({ x: Math.round(mx1 + (mx2 - mx1) * 0.1), y: Math.round(my - 2 - text_font.size), width: Math.round((mx2 - mx1) * 0.8), height: text_font.size, text: lbl, latex: 0 }); } if (show_line && (path_line !== null)) path_line += ((path_line.length === 0) ? 'M' : 'L') + midx + ',' + my; 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) path_marker += this.markeratt.create(midx, my); if (path_err !== null) { let edx = 5; if (this.options.errorX > 0) { edx = Math.round((mx2-mx1)*this.options.errorX); const mmx1 = midx - edx, mmx2 = midx + edx; path_err += `M${mmx1+dend},${my}${endx}h${mmx2-mmx1-2*dend}${endx}`; } path_err += `M${midx},${my-yerr1+dend}${endy}v${yerr1+yerr2-2*dend}${endy}`; if (hints_err !== null) hints_err += `M${midx-edx},${my-yerr1}h${2*edx}v${yerr1+yerr2}h${ -2*edx}z`; } } } } }; for (i = left; i <= right; i += di) { x = xaxis.GetBinCoord(i); if (funcs.logx && (x <= 0)) continue; grx = Math.round(funcs.grx(x)); lastbin = (i > right - di); if (lastbin && (left < right)) gry = curry; else { y = histo.getBinContent(i+1); gry = Math.round(funcs.gry(y)); } if (res.length === 0) { 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_markers || show_text || show_line) { 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 as same X differs, need complete logic if (!draw_markers && ((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; } } 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 = !this.isBatchMode() && this.fillatt.empty() && options.Hist && settings.Tooltip && !draw_markers && !show_line; 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`; if (draw_markers || show_line) { if (path_fill) { this.draw_g.append('svg:path') .attr('d', path_fill) .call(this.fillatt.func); } if (path_err) { this.draw_g.append('svg:path') .attr('d', path_err) .call(this.lineatt.func); } if (hints_err) { this.draw_g.append('svg:path') .attr('d', hints_err) .style('fill', 'none') .style('pointer-events', this.isBatchMode() ? null : 'visibleFill'); } if (path_line) { if (!this.fillatt.empty() && !options.Hist) { this.draw_g.append('svg:path') .attr('d', path_line + close_path) .call(this.fillatt.func); } this.draw_g.append('svg:path') .attr('d', path_line) .style('fill', 'none') .call(this.lineatt.func); } if (path_marker) { this.draw_g.append('svg:path') .attr('d', path_marker) .call(this.markeratt.func); } } else if (res && options.Hist) { this.draw_g.append('svg:path') .attr('d', res + ((!this.fillatt.empty() || fill_for_interactive) ? close_path : '')) .style('stroke-linejoin', 'miter') .call(this.lineatt.func) .call(this.fillatt.func); } return show_text ? this.finishTextDrawing() : true; }); } /** @summary Provide text information (tooltips) for histogram bin */ getBinTooltips(bin) { const tips = [], name = this.getObjectHint(), pmain = this.getFramePainter(), histo = this.getHisto(), xaxis = this.getAxis('x'), di = this.isDisplayItem() ? histo.stepx : 1, x1 = xaxis.GetBinCoord(bin), x2 = xaxis.GetBinCoord(bin+di), xlbl = this.getAxisBinTip('x', bin, di); let cont = histo.getBinContent(bin+1); if (name) tips.push(name); if (this.options.Error || this.options.Mark) { tips.push(`x = ${xlbl}`, `y = ${pmain.axisAsText('y', cont)}`); if (this.options.Error) { if (xlbl[0] === '[') tips.push('error x = ' + ((x2 - x1) / 2).toPrecision(4)); tips.push('error y = ' + histo.getBinError(bin + 1).toPrecision(4)); } } else { tips.push(`bin = ${bin+1}`, `x = ${xlbl}`); if (histo.$baseh) cont -= histo.$baseh.getBinContent(bin+1); const lbl = 'entries = ' + (di > 1 ? '~' : ''); if (cont === Math.round(cont)) tips.push(lbl + cont); else tips.push(lbl + floatToString(cont, gStyle.fStatFormat)); } return tips; } /** @summary Process tooltip event */ processTooltipEvent(pnt) { let ttrect = this.draw_g?.selectChild('.tooltip_bin'); if (!pnt || !this.draw_content || this.options.Mode3D || !this.draw_g) { ttrect?.remove(); return null; } const pmain = this.getFramePainter(), funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y), width = pmain.getFrameWidth(), height = pmain.getFrameHeight(), histo = this.getHisto(), xaxis = this.getAxis('x'), left = this.getSelectIndex('x', 'left', -1), right = this.getSelectIndex('x', 'right', 2); let show_rect, grx1, grx2, gry1, gry2, gapx = 2, l = left, r = right; function GetBinGrX(i) { const xx = xaxis.GetBinCoord(i); return (funcs.logx && (xx <= 0)) ? null : funcs.grx(xx); } function 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)); } const pnt_x = funcs.swap_xy ? pnt.y : pnt.x, pnt_y = funcs.swap_xy ? pnt.x : pnt.y; while (l < r-1) { const m = Math.round((l+r)*0.5), xx = GetBinGrX(m); if ((xx === null) || (xx < pnt_x - 0.5)) if (funcs.swap_xy) r = m; else l = m; else if (xx > pnt_x + 0.5) if (funcs.swap_xy) l = m; else r = m; else { l++; r--; } } let findbin = r = l; grx1 = GetBinGrX(findbin); if (funcs.swap_xy) { 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 (this.options.Bar) { const w = grx2 - grx1; grx1 += Math.round(this.options.BarOffset*w); grx2 = grx1 + Math.round(this.options.BarWidth*w); } if (grx1 > grx2) [grx1, grx2] = [grx2, grx1]; if (this.isDisplayItem() && ((findbin <= histo.dx) || (findbin >= histo.dx + histo.nx))) { // special case when zoomed out of scale and bin is not available ttrect.remove(); return null; } const midx = Math.round((grx1 + grx2)/2), midy = gry1 = gry2 = GetBinGrY(findbin); if (this.options.Bar) { show_rect = true; gapx = 0; gry1 = this.getBarBaseline(funcs, height); if (gry1 > gry2) [gry1, gry2] = [gry2, gry1]; if (!pnt.touch && (pnt.nproc === 1)) if ((pnt_y < gry1) || (pnt_y > gry2)) findbin = null; } else if (this.options.Error || this.options.Mark) { show_rect = true; let msize = 3; if (this.markeratt) msize = Math.max(msize, this.markeratt.getFullSize()); if (this.options.Error) { const cont = histo.getBinContent(findbin+1), binerr = histo.getBinError(findbin+1); gry1 = Math.round(funcs.gry(cont + binerr)); // up gry2 = Math.round(funcs.gry(cont - binerr)); // down const dx = (grx2-grx1)*this.options.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 (this.options.Line) show_rect = false; 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 bars option used check that bar is not match if ((pnt_x < grx1 - gapx) || (pnt_x > grx2 + gapx)) findbin = null; else // exclude empty bin if empty bins suppressed if (!this.options.Zero && (histo.getBinContent(findbin+1) === 0)) findbin = null; } if ((findbin === null) || ((gry2 <= 0) || (gry1 >= height))) { ttrect.remove(); return null; } const res = { name: 'histo', 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.draw_g.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', pmain.swap_xy ? gry1 : grx1) .attr('width', pmain.swap_xy ? gry2-gry1 : grx2-grx1) .attr('y', pmain.swap_xy ? grx1 : gry1) .attr('height', pmain.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 // 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.draw_g.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', 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); // obsolete, should be implemented differently if (this.options.need_fillcol && this.fillatt?.empty()) this.fillatt.change(5, 1001); // redraw all objects this.interactiveRedraw('pad', 'drawopt'); }); } /** @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(), xaxis = this.getAxis('x'); if (dist === 0) 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(xaxis.GetBinCoord(left), xaxis.GetBinCoord(right)); } /** @summary Checks if it makes sense to zoom inside specified axis range */ canZoomInside(axis, min, max) { const xaxis = this.getAxis('x'); if ((axis === 'x') && (xaxis.FindBin(max, 0.5) - xaxis.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 Call appropriate draw function */ async callDrawFunc(reason) { const main = this.getFramePainter(); if (main && (main.mode3d !== this.options.Mode3D) && !this.isMainPainter()) this.options.Mode3D = main.mode3d; return this.options.Mode3D ? this.draw3D(reason) : this.draw2D(reason); } /** @summary Draw in 2d */ async draw2D(reason) { this.clear3DScene(); return this.drawFrameAxes().then(res => { return res ? this.drawingBins(reason) : false; }).then(res => { if (res) return this.draw1DBins().then(() => this.addInteractivity()); }).then(() => this); } /** @summary Draw in 3d */ async draw3D(reason) { console.log('3D drawing is disabled, load ./hist/RH1Painter.mjs'); return this.draw2D(reason); } /** @summary Redraw histogram */ async redraw(reason) { return this.callDrawFunc(reason); } static async _draw(painter, opt) { return ensureRCanvas(painter).then(() => { painter.setAsMainPainter(); painter.options = { Hist: false, Bar: false, BarStyle: 0, Error: false, ErrorKind: -1, errorX: gStyle.fErrorX, Zero: false, Mark: false, Line: false, Fill: false, Lego: 0, Surf: 0, Text: false, TextAngle: 0, TextKind: '', AutoColor: 0, BarOffset: 0, BarWidth: 1, BaseLine: false, Mode3D: false, FrontBox: false, BackBox: false }; const d = new DrawOptions(opt); if (d.check('R3D_', true)) painter.options.Render3D = constants$1.Render3D.fromString(d.part.toLowerCase()); const kind = painter.v7EvalAttr('kind', 'hist'), sub = painter.v7EvalAttr('sub', 0), has_main = Boolean(painter.getMainPainter()), o = painter.options; o.Text = painter.v7EvalAttr('drawtext', false); o.BarOffset = painter.v7EvalAttr('baroffset', 0.0); o.BarWidth = painter.v7EvalAttr('barwidth', 1.0); o.second_x = has_main && painter.v7EvalAttr('secondx', false); o.second_y = has_main && painter.v7EvalAttr('secondy', false); switch (kind) { case 'bar': o.Bar = true; o.BarStyle = sub; break; case 'err': o.Error = true; o.ErrorKind = sub; break; case 'p': o.Mark = true; break; case 'l': o.Line = true; break; case 'lego': o.Lego = sub > 0 ? 10+sub : 12; o.Mode3D = true; break; default: o.Hist = true; } painter.scanContent(); return painter.callDrawFunc(); }); } /** @summary draw RH1 object */ static async draw(dom, histo, opt) { return RH1Painter._draw(new RH1Painter(dom, histo), opt); } }; // class RH1Painter class RH1Painter extends RH1Painter$2 { /** @summary Draw 1-D histogram in 3D mode */ draw3D(reason) { this.mode3d = true; const main = this.getFramePainter(), // who makes axis drawing is_main = this.isMainPainter(), // is main histogram zmult = 1 + 2*gStyle.fHistTopMargin; let pr = Promise.resolve(this); if (reason === 'resize') { if (is_main && main.resize3D()) main.render3D(); return pr; } this.deleteAttr(); this.scanContent(true); // may be required for axis drawings if (is_main) { assignFrame3DMethods(main); pr = main.create3DScene(this.options.Render3D).then(() => { main.setAxesRanges(this.getAxis('x'), this.xmin, this.xmax, null, this.ymin, this.ymax, null, 0, 0); main.set3DOptions(this.options); main.drawXYZ(main.toplevel, RAxisPainter, { use_y_for_z: true, zmult, zoom: settings.Zooming, ndim: 1, draw: true, v7: true }); }); } if (!main.mode3d) return pr; return pr.then(() => this.drawingBins(reason)).then(() => { // called when bins received from server, must be reentrant const fp = this.getFramePainter(); drawBinsLego(this, true); this.updatePaletteDraw(); fp.render3D(); fp.addKeysHandler(); return this; }); } /** @summary draw RH1 object */ static async draw(dom, histo, opt) { return RH1Painter._draw(new RH1Painter(dom, histo), opt); } } // class RH1Painter var RH1Painter$1 = /*#__PURE__*/Object.freeze({ __proto__: null, RH1Painter: RH1Painter }); /** * @summary Painter for RH2 classes * * @private */ let RH2Painter$2 = class RH2Painter extends RHistPainter { /** @summary constructor * @param {object|string} dom - DOM element or id * @param {object} histo - histogram object */ constructor(dom, histo) { super(dom, histo); this.wheel_zoomy = true; } /** @summary Cleanup painter */ cleanup() { delete this.tt_handle; super.cleanup(); } /** @summary Returns histogram dimension */ getDimension() { return 2; } /** @summary Toggle projection */ toggleProjection(kind, width) { if ((kind === 'Projections') || (kind === 'Off')) kind = ''; 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); } else if (isStr(kind) && (kind.length > 1)) { const ps = kind.indexOf('_'); if ((ps > 0) && (kind[0] === 'X') && (kind[ps+1] === 'Y')) { widthX = parseInt(kind.slice(1, ps)) || 1; widthY = parseInt(kind.slice(ps+2)) || 1; kind = 'XY'; } else if ((ps > 0) && (kind[0] === 'Y') && (kind[ps+1] === 'X')) { widthY = parseInt(kind.slice(1, ps)) || 1; widthX = parseInt(kind.slice(ps+2)) || 1; kind = 'XY'; } else { widthX = widthY = parseInt(kind.slice(1)) || 1; kind = kind[0]; } } if (!widthX && !widthY) widthX = widthY = 1; if (kind && (this.is_projection === 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.is_projection === kind) ? '' : kind; this.projection_widthX = widthX; this.projection_widthY = widthY; this.is_projection = ''; // avoid projection handling until area is created this.provideSpecialDrawArea(new_proj).then(() => { this.is_projection = new_proj; return this.redrawProjection(); }); } /** @summary Redraw projections */ redrawProjection(/* ii1, ii2, jj1, jj2 */) { // do nothing for the moment // if (!this.is_projection) return; } /** @summary Execute menu command */ 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; } return false; } /** @summary Fill histogram context menu */ fillHistContextMenu(menu) { if (this.getPadPainter()?.iscan) { let kind = this.is_projection || ''; if (kind) kind += this.projection_widthX; if ((this.projection_widthX !== this.projection_widthY) && (this.is_projection === 'XY')) kind = `X${this.projection_widthX}_Y${this.projection_widthY}`; const kinds = ['X1', 'X2', 'X3', 'X5', 'X10', 'Y1', 'Y2', 'Y3', 'Y5', 'Y10', 'XY1', 'XY2', 'XY3', 'XY5', 'XY10']; if (kind) kinds.unshift('Off'); menu.sub('Projections', () => menu.input('Input projection kind X1 or XY2 or X3_Y4', kind, 'string').then(val => this.toggleProjection(val))); for (let k = 0; k < kinds.length; ++k) menu.addchk(kind === kinds[k], kinds[k], kinds[k], arg => this.toggleProjection(arg)); menu.endsub(); } 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); this.interactiveRedraw('pad', 'drawopt'); }); if (this.options.Color) this.fillPaletteMenu(menu); } /** @summary Process click on histogram-defined buttons */ clickButton(funcname) { const res = super.clickButton(funcname); if (res) return res; 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 RH2-related functions */ fillToolbar() { super.fillToolbar(true); const pp = this.getPadPainter(); if (!pp) return; pp.addPadButton('th2color', 'Toggle color', 'ToggleColor'); pp.addPadButton('th2colorz', 'Toggle color palette', 'ToggleColorZ'); pp.addPadButton('th2draw3d', 'Toggle 3D mode', 'Toggle3D'); pp.showPadButtons(); } /** @summary Toggle color drawing mode */ toggleColor() { if (this.options.Mode3D) { this.options.Mode3D = false; this.options.Color = true; } else this.options.Color = !this.options.Color; return this.redraw(); } /** @summary Perform automatic zoom inside non-zero region of histogram */ autoZoom() { 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(), xaxis = this.getAxis('x'), yaxis = this.getAxis('y'); 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 = xaxis.GetBinCoord(ileft); xmax = xaxis.GetBinCoord(iright); isany = true; } if ((jleft > j1 || jright < j2) && (jleft < jright - 1)) { ymin = yaxis.GetBinCoord(jleft); ymax = yaxis.GetBinCoord(jright); isany = true; } if (isany) return this.getFramePainter().zoom(xmin, xmax, ymin, ymax); } /** @summary Scan content of 2-dim histogram */ 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.getHisto(); this.extractAxesProperties(2); if (this.isDisplayItem()) { // take min/max values from the display item this.gminbin = histo.fContMin; this.gminposbin = histo.fContMinPos > 0 ? histo.fContMinPos : null; this.gmaxbin = histo.fContMax; } else { // global min/max, used at the moment in 3D drawing this.gminbin = this.gmaxbin = histo.getBinContent(1, 1); this.gminposbin = null; for (let i = 0; i < this.nbinsx; ++i) { for (let 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.zmin = this.gminbin; this.zmax = this.gmaxbin; // this value used for logz scale drawing if ((this.gminposbin === null) && (this.gmaxbin > 0)) this.gminposbin = this.gmaxbin*1e-4; if (this.options.Axis > 0) // Paint histogram axis only this.draw_content = false; else this.draw_content = (this.gmaxbin !== 0) || (this.gminbin !== 0); } /** @summary Count statistic */ countStat(cond) { const histo = this.getHisto(), res = { name: 'histo', 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 }, xleft = this.getSelectIndex('x', 'left'), xright = this.getSelectIndex('x', 'right'), yleft = this.getSelectIndex('y', 'left'), yright = this.getSelectIndex('y', 'right'), xaxis = this.getAxis('x'), yaxis = this.getAxis('y'); let stat_sum0 = 0, stat_sumx1 = 0, stat_sumy1 = 0, stat_sumx2 = 0, stat_sumy2 = 0, xside, yside, xx, yy, zz, xi, yi; // TODO: account underflow/overflow bins, now stored in different array and only by histogram itself for (xi = 1; xi <= this.nbinsx; ++xi) { xside = (xi <= xleft+1) ? 0 : (xi > xright+1 ? 2 : 1); xx = xaxis.GetBinCoord(xi - 0.5); for (yi = 1; yi <= this.nbinsy; ++yi) { yside = (yi <= yleft+1) ? 0 : (yi > yright+1 ? 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)) continue; if (cond && !cond(xx, yy)) continue; if ((res.wmax === null) || (zz > res.wmax)) { res.wmax = zz; res.xmax = xx; res.ymax = yy; } stat_sum0 += zz; stat_sumx1 += xx * zz; stat_sumy1 += yy * zz; stat_sumx2 += xx**2 * zz; stat_sumy2 += yy**2 * zz; } } 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; return res; } /** @summary Fill statistic into statistic box */ fillStatistic(stat, dostat /* , dofit */) { const data = this.countStat(), 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; stat.clearStat(); 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 > 0) { stat.addText('Skewness x = '); stat.addText('Skewness y = '); } if (print_kurt > 0) stat.addText('Kurt = '); if ((print_under > 0) || (print_over > 0)) { const m = data.matrix; stat.addText(m[6].toFixed(0) + ' | ' + m[7].toFixed(0) + ' | ' + m[7].toFixed(0)); stat.addText(m[3].toFixed(0) + ' | ' + m[4].toFixed(0) + ' | ' + m[5].toFixed(0)); stat.addText(m[0].toFixed(0) + ' | ' + m[1].toFixed(0) + ' | ' + m[2].toFixed(0)); } return true; } /** @summary Draw histogram bins as color */ drawBinsColor() { const histo = this.getHisto(), handle = this.prepareDraw(), di = handle.stepi, dj = handle.stepj, entries = []; let colindx, cmd1, cmd2, i, j, binz, dx, dy, entry, last_entry; const flush_last_entry = () => { last_entry.path += `h${dx}v${last_entry.y2-last_entry.y}h${-dx}z`; last_entry.dy = 0; last_entry = null; }; // now start build for (i = handle.i1; i < handle.i2; i += di) { dx = (handle.grx[i+di] - handle.grx[i]) || 1; for (j = handle.j1; j < handle.j2; j += dj) { binz = histo.getBinContent(i+1, j+1); colindx = handle.palette.getContourIndex(binz); if (binz === 0) { if (!this.options.Zero) colindx = null; else if ((colindx === null) && this._show_empty_bins) colindx = 0; } if (colindx === null) { if (last_entry) flush_last_entry(); continue; } cmd1 = `M${handle.grx[i]},${handle.gry[j]}`; dy = (handle.gry[j+dj] - handle.gry[j]) || -1; entry = entries[colindx]; if (entry === undefined) entry = entries[colindx] = { path: cmd1 }; else if ((entry === last_entry)) { entry.y2 = handle.gry[j] + dy; continue; } else { cmd2 = `m${handle.grx[i]-entry.x},${handle.gry[j]-entry.y}`; entry.path += (cmd2.length < cmd1.length) ? cmd2 : cmd1; } if (last_entry) flush_last_entry(); entry.x = handle.grx[i]; entry.y = handle.gry[j]; { entry.y2 = handle.gry[j] + dy; last_entry = entry; } } if (last_entry) flush_last_entry(); } entries.forEach((entry2, ecolindx) => { if (entry2) { this.draw_g .append('svg:path') .attr('d', entry2.path) .style('fill', handle.palette.getColor(ecolindx)); } }); this.updatePaletteDraw(); return handle; } /** @summary Draw histogram bins as contour */ drawBinsContour(funcs, frame_w, frame_h) { const handle = this.prepareDraw({ rounding: false, extra: 100 }), main = this.getFramePainter(), palette = main.getHistPalette(), levels = palette.getContour(), func = main.getProjectionFunc(), BuildPath = (xp, yp, iminus, iplus, do_close) => { let cmd = '', last, pnt, first, isany; for (let i = iminus; i <= iplus; ++i) { if (func) { pnt = func(xp[i], yp[i]); pnt.x = Math.round(funcs.grx(pnt.x)); pnt.y = Math.round(funcs.gry(pnt.y)); } else pnt = { x: Math.round(xp[i]), y: Math.round(yp[i]) }; if (!cmd) { cmd = `M${pnt.x},${pnt.y}`; first = pnt; } else if ((i === iplus) && first && (pnt.x === first.x) && (pnt.y === first.y)) { if (!isany) return ''; // all same points cmd += 'z'; do_close = false; } else if ((pnt.x !== last.x) && (pnt.y !== last.y)) { cmd += `l${pnt.x - last.x},${pnt.y - last.y}`; isany = true; } else if (pnt.x !== last.x) { cmd += `h${pnt.x - last.x}`; isany = true; } else if (pnt.y !== last.y) { cmd += `v${pnt.y - last.y}`; isany = true; } last = pnt; } if (do_close) cmd += 'z'; return cmd; }; if (this.options.Contour === 14) { this.draw_g .append('svg:path') .attr('d', `M0,0h${frame_w}v${frame_h}h${-frame_w}z`) .style('fill', palette.getColor(0)); } buildHist2dContour(this.getHisto(), handle, levels, palette, (colindx, xp, yp, iminus, iplus) => { const icol = palette.getColor(colindx); let fillcolor = icol, lineatt; switch (this.options.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: (colindx%5 + 1), width: 1, std: false }); break; case 13: fillcolor = 'none'; lineatt = this.lineatt; break; } const dd = BuildPath(xp, yp, iminus, iplus, fillcolor !== 'none'); if (!dd) return; const elem = this.draw_g .append('svg:path') .attr('d', dd) .style('fill', fillcolor); if (lineatt) elem.call(lineatt.func); } ); handle.hide_only_zeros = true; // text drawing suppress only zeros return handle; } /** @summary Create poly bin */ createPolyBin() { // see how TH2Painter is implemented return ''; } /** @summary Draw RH2 bins as text */ async drawBinsText(handle) { if (!handle) handle = this.prepareDraw({ rounding: false }); const histo = this.getHisto(), textFont = this.v7EvalFont('text', { size: 20, color: 'black', align: 22 }), text_offset = this.options.BarOffset || 0, text_g = this.draw_g.append('svg:g').attr('class', 'th2_text'), di = handle.stepi, dj = handle.stepj; return this.startTextDrawingAsync(textFont, 'font', text_g).then(() => { for (let i = handle.i1; i < handle.i2; i += di) { for (let j = handle.j1; j < handle.j2; j += dj) { let binz = histo.getBinContent(i+1, j+1); if ((binz === 0) && !this._show_empty_bins) continue; const binw = handle.grx[i+di] - handle.grx[i], binh = handle.gry[j] - handle.gry[j+dj]; const text = (binz === Math.round(binz)) ? binz.toString() : floatToString(binz, gStyle.fPaintTextFormat); let x, y, width, height; if (textFont.angle) { x = Math.round(handle.grx[i] + binw*0.5); y = Math.round(handle.gry[j+dj] + binh*(0.5 + text_offset)); width = height = 0; } else { x = Math.round(handle.grx[i] + binw*0.1); y = Math.round(handle.gry[j+dj] + 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, text, latex: 0, draw_g: text_g }); } } handle.hide_only_zeros = true; // text drawing suppress only zeros return this.finishTextDrawing(text_g, true); }).then(() => handle); } /** @summary Draw RH2 bins as arrows */ drawBinsArrow() { const histo = this.getHisto(), handle = this.prepareDraw({ rounding: false }), scale_x = (handle.grx[handle.i2] - handle.grx[handle.i1])/(handle.i2 - handle.i1 + 1-0.03)/2, scale_y = (handle.gry[handle.j2] - handle.gry[handle.j1])/(handle.j2 - handle.j1 + 1-0.03)/2, di = handle.stepi, dj = handle.stepj, makeLine = (dx, dy) => dx ? (dy ? `l${dx},${dy}` : `h${dx}`) : (dy ? `v${dy}` : ''); let cmd = '', i, j, dn = 1e-30, dx, dy, xc, yc, dxn, dyn, x1, x2, y1, y2, anr, si, co; for (let loop = 0; loop < 2; ++loop) { for (i = handle.i1; i < handle.i2; i += di) { for (j = handle.j1; j < handle.j2; j += dj) { if (i === handle.i1) dx = histo.getBinContent(i+1+di, j+1) - histo.getBinContent(i+1, j+1); else if (i >= handle.i2-di) dx = histo.getBinContent(i+1, j+1) - histo.getBinContent(i+1-di, j+1); else dx = 0.5*(histo.getBinContent(i+1+di, j+1) - histo.getBinContent(i+1-di, j+1)); if (j === handle.j1) dy = histo.getBinContent(i+1, j+1+dj) - histo.getBinContent(i+1, j+1); else if (j >= handle.j2-dj) dy = histo.getBinContent(i+1, j+1) - histo.getBinContent(i+1, j+1-dj); else dy = 0.5*(histo.getBinContent(i+1, j+1+dj) - histo.getBinContent(i+1, j+1-dj)); if (loop === 0) dn = Math.max(dn, Math.abs(dx), Math.abs(dy)); else { xc = (handle.grx[i] + handle.grx[i+di])/2; yc = (handle.gry[j] + handle.gry[j+dj])/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 !== 0) || (dy !== 0)) { cmd += 'M'+Math.round(x1)+','+Math.round(y1) + makeLine(dx, dy); if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { anr = Math.sqrt(2/(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); } } } } } } this.draw_g .append('svg:path') .attr('d', cmd) .style('fill', 'none') .call(this.lineatt.func); return handle; } /** @summary Draw RH2 bins as boxes */ drawBinsBox() { const histo = this.getHisto(), handle = this.prepareDraw({ rounding: false }), main = this.getFramePainter(); if (main.maxbin === main.minbin) { main.maxbin = this.gmaxbin; main.minbin = this.gminbin; main.minposbin = this.gminposbin; } if (main.maxbin === main.minbin) main.minbin = Math.min(0, main.maxbin-1); const absmax = Math.max(Math.abs(main.maxbin), Math.abs(main.minbin)), absmin = Math.max(0, main.minbin), di = handle.stepi, dj = handle.stepj; let i, j, binz, absz, res = '', cross = '', btn1 = '', btn2 = '', zdiff, dgrx, dgry, xx, yy, ww, hh, xyfactor, uselogz = false, logmin = 0; if (main.logz && (absmax > 0)) { uselogz = true; const logmax = Math.log(absmax); if (absmin > 0) logmin = Math.log(absmin); else if ((main.minposbin >= 1) && (main.minposbin < 100)) logmin = Math.log(0.7); else logmin = (main.minposbin > 0) ? Math.log(0.7*main.minposbin) : 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 += di) { for (j = handle.j1; j < handle.j2; j += dj) { binz = histo.getBinContent(i + 1, j + 1); absz = Math.abs(binz); if ((absz === 0) || (absz < absmin)) 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+di] - handle.grx[i]; hh = handle.gry[j] - handle.gry[j+dj]; dgrx = zdiff * ww; dgry = zdiff * hh; xx = Math.round(handle.grx[i] + dgrx); yy = Math.round(handle.gry[j+dj] + 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) && (this.options.BoxStyle === 10)) cross += `M${xx},${yy}l${ww},${hh}M${xx+ww},${yy}l${-ww},${hh}`; if ((this.options.BoxStyle === 11) && (ww>5) && (hh>5)) { const pww = Math.round(ww*0.1), phh = Math.round(hh*0.1), 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`; btn2 += (binz < 0) ? side1 : side2; btn1 += (binz < 0) ? side2 : side1; } } } if (res) { const elem = this.draw_g .append('svg:path') .attr('d', res) .call(this.fillatt.func); if ((this.options.BoxStyle !== 11) && this.fillatt.empty()) elem.call(this.lineatt.func); } if (btn1 && this.fillatt.hasColor()) { this.draw_g.append('svg:path') .attr('d', btn1) .call(this.fillatt.func) .style('fill', rgb(this.fillatt.color).brighter(0.5).formatRgb()); } if (btn2) { this.draw_g.append('svg:path') .attr('d', btn2) .call(this.fillatt.func) .style('fill', !this.fillatt.hasColor() ? 'red' : rgb(this.fillatt.color).darker(0.5).formatRgb()); } if (cross) { const elem = this.draw_g.append('svg:path') .attr('d', cross) .style('fill', 'none'); if (!this.lineatt.empty()) elem.call(this.lineatt.func); } return handle; } /** @summary Draw RH2 bins as scatter plot */ drawBinsScatter() { const histo = this.getHisto(), handle = this.prepareDraw({ rounding: true, pixel_density: true, scatter_plot: true }), colPaths = [], currx = [], curry = [], cell_w = [], cell_h = [], scale = this.options.ScatCoef * ((this.gmaxbin) > 2000 ? 2000 / this.gmaxbin : 1), di = handle.stepi, dj = handle.stepj, rnd = new TRandom(handle.sumz); let colindx, cmd1, cmd2, i, j, binz, cw, ch, factor = 1; if (scale*handle.sumz < 1e5) { // one can use direct drawing of scatter plot without any patterns this.createv7AttMarker(); this.markeratt.resetPos(); let path = '', k, npix; for (i = handle.i1; i < handle.i2; i += di) { cw = handle.grx[i+di] - handle.grx[i]; for (j = handle.j1; j < handle.j2; j += dj) { ch = handle.gry[j] - handle.gry[j+dj]; binz = histo.getBinContent(i + 1, j + 1); npix = Math.round(scale*binz); if (npix <= 0) continue; for (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.draw_g .append('svg:path') .attr('d', 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; // now start build for (i = handle.i1; i < handle.i2; i += di) { for (j = handle.j1; j < handle.j2; j += dj) { binz = histo.getBinContent(i + 1, j + 1); if ((binz <= 0) || (binz < this.minbin)) continue; cw = handle.grx[i+di] - handle.grx[i]; ch = handle.gry[j] - handle.gry[j+dj]; if (cw*ch <= 0) continue; colindx = handle.palette.getContourIndex(binz/cw/ch); if (colindx < 0) continue; cmd1 = `M${handle.grx[i]},${handle.gry[j+dj]}`; 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+dj]-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+dj]; colPaths[colindx] += `v${ch}h${cw}v${-ch}z`; } } const layer = this.getFrameSvg().selectChild('.main_layer'); let defs = layer.selectChild('def'); if (defs.empty() && (colPaths.length > 0)) defs = layer.insert('svg:defs', ':first-child'); this.createv7AttMarker(); const cntr = handle.palette.getContour(); for (colindx = 0; colindx < colPaths.length; ++colindx) { if ((colPaths[colindx] !== undefined) && (colindx 0) handle = this.drawBinsContour(funcs, rect.width, rect.height); if (this.options.Text) pr = this.drawBinsText(handle); if (!handle && !pr) handle = this.drawBinsColor(); if (!pr) pr = Promise.resolve(handle); return pr.then(h => { this.tt_handle = h; return this; }); } /** @summary Provide text information (tooltips) for histogram bin */ getBinTooltips(i, j) { const lines = [], histo = this.getHisto(); let binz = histo.getBinContent(i+1, j+1), di = 1, dj = 1; if (this.isDisplayItem()) { di = histo.stepx || 1; dj = histo.stepy || 1; } lines.push(this.getObjectHint() || 'histo<2>', 'x = ' + this.getAxisBinTip('x', i, di), 'y = ' + this.getAxisBinTip('y', j, dj), `bin = ${i+1}, ${j+1}`); if (histo.$baseh) binz -= histo.$baseh.getBinContent(i+1, j+1); const lbl = 'entries = ' + ((di > 1) || (dj > 1) ? '~' : ''); if (binz === Math.round(binz)) lines.push(lbl + binz); else lines.push(lbl + floatToString(binz, gStyle.fStatFormat)); return lines; } /** @summary Provide text information (tooltips) for poly bin */ getPolyBinTooltips() { // see how TH2Painter is implemented return []; } /** @summary Process tooltip event */ processTooltipEvent(pnt) { const histo = this.getHisto(), h = this.tt_handle; let ttrect = this.draw_g?.selectChild('.tooltip_bin'); if (!pnt || !this.draw_content || !this.draw_g || !h || this.options.Proj) { ttrect?.remove(); return null; } if (h.poly) { // process tooltips from TH2Poly - see TH2Painter return null; } let i, j, binz = 0, colindx = null; // search bins position for (i = h.i1; i < h.i2; ++i) if ((pnt.x>=h.grx[i]) && (pnt.x<=h.grx[i+1])) break; for (j = h.j1; j < h.j2; ++j) if ((pnt.y>=h.gry[j+1]) && (pnt.y<=h.gry[j])) break; if ((i < h.i2) && (j < h.j2)) { binz = histo.getBinContent(i+1, j+1); if (this.is_projection) colindx = 0; // just to avoid hide else if (h.hide_only_zeros) colindx = (binz === 0) && !this._show_empty_bins ? null : 0; else { colindx = h.palette.getContourIndex(binz); if ((colindx === null) && (binz === 0) && this._show_empty_bins) colindx = 0; } } if (colindx === null) { ttrect.remove(); return null; } const res = { name: 'histo', title: histo.fTitle || 'title', 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 (this.options.Color) res.color2 = h.palette.getColor(colindx); if (pnt.disabled && !this.is_projection) { ttrect.remove(); res.changed = true; } else { if (ttrect.empty()) { ttrect = this.draw_g.append('svg:path') .attr('class', 'tooltip_bin') .style('pointer-events', 'none') .call(addHighlightStyle); } const pmain = this.getFramePainter(); let 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], binid = i*10000 + j, path; if (this.is_projection) { const pwx = this.projection_widthX || 1, ddx = (pwx - 1) / 2; if ((this.is_projection.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.is_projection.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 (this.is_projection === 'X') { x1 = 0; x2 = pmain.getFrameWidth(); y1 = h.gry[j2]; y2 = h.gry[j1]; binid = j1*777 + j2*333; } else if (this.is_projection === 'Y') { y1 = 0; y2 = pmain.getFrameHeight(); x1 = h.grx[i1]; x2 = h.grx[i2]; binid = i1*777 + i2*333; } else if (this.is_projection === '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${pmain.getFrameWidth()}V${y2}H${x2}V${pmain.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.is_projection && res.changed) this.redrawProjection(i1, i2, j1, j2); } if (res.changed) { res.user_info = { obj: histo, name: 'histo', 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) { if (axis === 'z') return true; const obj = this.getAxis(axis); return obj.FindBin(max, 0.5) - obj.FindBin(min, 0) > 1; } /** @summary Performs 2D drawing of histogram * @return {Promise} when ready */ async draw2D(reason) { this.clear3DScene(); return this.drawFrameAxes().then(res => { return res ? this.drawingBins(reason) : false; }).then(res => { if (res) return this.draw2DBins().then(() => this.addInteractivity()); }).then(() => this); } /** @summary Performs 3D drawing of histogram * @return {Promise} when ready */ async draw3D(reason) { console.log('3D drawing is disabled, load ./hist/RH1Painter.mjs'); return this.draw2D(reason); } /** @summary Call drawing function depending from 3D mode */ async callDrawFunc(reason) { const main = this.getFramePainter(); if (main && (main.mode3d !== this.options.Mode3D) && !this.isMainPainter()) this.options.Mode3D = main.mode3d; return this.options.Mode3D ? this.draw3D(reason) : this.draw2D(reason); } /** @summary Redraw histogram */ async redraw(reason) { return this.callDrawFunc(reason); } /** @summary Draw histogram using painter instance * @private */ static async _draw(painter /* , opt */) { return ensureRCanvas(painter).then(() => { painter.setAsMainPainter(); painter.options = { Hist: false, Error: false, Zero: false, Mark: false, Line: false, Fill: false, Lego: 0, Surf: 0, Text: true, TextAngle: 0, TextKind: '', BaseLine: false, Mode3D: false, AutoColor: 0, Color: false, Scat: false, ScatCoef: 1, Box: false, BoxStyle: 0, Arrow: false, Contour: 0, Proj: 0, BarOffset: 0, BarWidth: 1, minimum: kNoZoom, maximum: kNoZoom, FrontBox: false, BackBox: false }; const kind = painter.v7EvalAttr('kind', ''), sub = painter.v7EvalAttr('sub', 0), o = painter.options; o.Text = painter.v7EvalAttr('drawtext', false); switch (kind) { case 'lego': o.Lego = sub > 0 ? 10+sub : 12; o.Mode3D = true; break; case 'surf': o.Surf = sub > 0 ? 10+sub : 1; o.Mode3D = true; break; case 'box': o.Box = true; o.BoxStyle = 10 + sub; break; case 'err': o.Error = true; o.Mode3D = true; break; case 'cont': o.Contour = sub > 0 ? 10+sub : 1; break; case 'arr': o.Arrow = true; break; case 'scat': o.Scat = true; break; case 'col': o.Color = true; break; default: if (!o.Text) o.Color = true; } // here we deciding how histogram will look like and how will be shown // painter.decodeOptions(opt); painter._show_empty_bins = false; painter.scanContent(); return painter.callDrawFunc(); }); } /** @summary draw RH2 object */ static async draw(dom, obj, opt) { // create painter and add it to canvas return RH2Painter._draw(new RH2Painter(dom, obj), opt); } }; // class RH2Painter class RH2Painter extends RH2Painter$2 { /** Draw histogram bins in 3D, using provided draw options */ draw3DBins() { if (!this.draw_content) return; if (this.options.Surf) return drawBinsSurf3D(this, true); if (this.options.Error) return drawBinsError3D(this, true); if (this.options.Contour) return drawBinsContour3D(this, true, true); drawBinsLego(this, true); this.updatePaletteDraw(); } draw3D(reason) { this.mode3d = true; const main = this.getFramePainter(), // who makes axis drawing is_main = this.isMainPainter(); // is main histogram let pr = Promise.resolve(this); if (reason === 'resize') { if (is_main && main.resize3D()) main.render3D(); return pr; } let zmult = 1 + 2*gStyle.fHistTopMargin; this.zmin = main.logz ? this.gminposbin * 0.3 : this.gminbin; this.zmax = this.gmaxbin; if (this.options.minimum !== kNoZoom) this.zmin = this.options.minimum; if (this.options.maximum !== kNoZoom) { this.zmax = this.options.maximum; zmult = 1; } if (main.logz && (this.zmin <= 0)) this.zmin = this.zmax * 1e-5; this.deleteAttr(); if (is_main) { assignFrame3DMethods(main); pr = main.create3DScene(this.options.Render3D).then(() => { main.setAxesRanges(this.getAxis('x'), this.xmin, this.xmax, this.getAxis('y'), this.ymin, this.ymax, null, this.zmin, this.zmax); main.set3DOptions(this.options); main.drawXYZ(main.toplevel, RAxisPainter, { zmult, zoom: settings.Zooming, ndim: 2, draw: true, v7: true }); }); } if (!main.mode3d) return pr; return pr.then(() => this.drawingBins(reason)).then(() => { // called when bins received from server, must be reentrant const fp = this.getFramePainter(); this.draw3DBins(); fp.render3D(); fp.addKeysHandler(); return this; }); } /** @summary draw RH2 object */ static async draw(dom, obj, opt) { // create painter and add it to canvas return RH2Painter._draw(new RH2Painter(dom, obj), opt); } } // class RH2Painter var RH2Painter$1 = /*#__PURE__*/Object.freeze({ __proto__: null, RH2Painter: RH2Painter }); /** * @summary Painter for RH3 classes * * @private */ class RH3Painter extends RHistPainter { /** @summary Returns histogram dimension */ getDimension() { return 3; } 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(); if (!histo) return; this.extractAxesProperties(3); // global min/max, used at the moment in 3D drawing if (this.isDisplayItem()) { // take min/max values from the display item this.gminbin = histo.fContMin; this.gminposbin = histo.fContMinPos > 0 ? histo.fContMinPos : null; this.gmaxbin = histo.fContMax; } else { this.gminbin = this.gmaxbin = histo.getBinContent(1, 1, 1); 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; } } } } this.draw_content = (this.gmaxbin !== 0) || (this.gminbin !== 0); } /** @summary Count histogram statistic */ countStat() { const histo = this.getHisto(), xaxis = this.getAxis('x'), yaxis = this.getAxis('y'), zaxis = this.getAxis('z'), 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'), res = { name: histo.fName, entries: 0, integral: 0, meanx: 0, meany: 0, meanz: 0, rmsx: 0, rmsy: 0, rmsz: 0 }; let stat_sum0 = 0, stat_sumx1 = 0, stat_sumy1 = 0, stat_sumz1 = 0, stat_sumx2 = 0, stat_sumy2 = 0, stat_sumz2 = 0, xi, yi, zi, xx, xside, yy, yside, zz, zside, cont; for (xi = 1; xi <= this.nbinsx; ++xi) { xx = xaxis.GetBinCoord(xi - 0.5); xside = (xi <= i1+1) ? 0 : (xi > i2+1 ? 2 : 1); for (yi = 1; yi <= this.nbinsy; ++yi) { yy = yaxis.GetBinCoord(yi - 0.5); yside = (yi <= j1+1) ? 0 : (yi > j2+1 ? 2 : 1); for (zi = 1; zi <= this.nbinsz; ++zi) { zz = zaxis.GetBinCoord(zi - 0.5); zside = (zi <= k1+1) ? 0 : (zi > k2+1 ? 2 : 1); cont = histo.getBinContent(xi, yi, zi); res.entries += cont; if ((xside === 1) && (yside === 1) && (zside === 1)) { stat_sum0 += 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 (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**2)); res.rmsy = Math.sqrt(Math.abs(stat_sumy2 / stat_sum0 - res.meany**2)); res.rmsz = Math.sqrt(Math.abs(stat_sumz2 / stat_sum0 - res.meanz**2)); } res.integral = stat_sum0; return res; } /** @summary Fill statistic */ fillStatistic(stat, dostat /* , dofit */) { const data = this.countStat(), 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; stat.clearStat(); 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')); return true; } /** @summary Provide text information (tooltips) for histogram bin */ getBinTooltips(ix, iy, iz) { const lines = [], histo = this.getHisto(); let dx = 1, dy = 1, dz = 1; if (this.isDisplayItem()) { dx = histo.stepx || 1; dy = histo.stepy || 1; dz = histo.stepz || 1; } lines.push(this.getObjectHint(), `x = ${this.getAxisBinTip('x', ix, dx)} xbin=${ix+1}`, `y = ${this.getAxisBinTip('y', iy, dy)} ybin=${iy+1}`, `z = ${this.getAxisBinTip('z', iz, dz)} zbin=${iz+1}`); const binz = histo.getBinContent(ix+1, iy+1, iz+1), lbl = 'entries = '+ ((dx > 1) || (dy > 1) || (dz > 1) ? '~' : ''); if (binz === Math.round(binz)) lines.push(lbl + binz); else lines.push(lbl + floatToString(binz, gStyle.fStatFormat)); return lines; } /** @summary Try to draw 3D histogram as scatter plot * @desc If there are too many points, returns promise with false */ async draw3DScatter(handle) { const histo = this.getHisto(), main = this.getFramePainter(), i1 = handle.i1, i2 = handle.i2, di = handle.stepi, j1 = handle.j1, j2 = handle.j2, dj = handle.stepj, k1 = handle.k1, k2 = handle.k2, dk = handle.stepk; if ((i2 <= i1) || (j2 <= j1) || (k2 <= k1)) return 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 i, j, k, bin_content, numpixels = 0, sumz = 0; for (i = i1; i < i2; i += di) { for (j = j1; j < j2; j += dj) { for (k = k1; k < k2; k += dk) { bin_content = histo.getBinContent(i+1, j+1, k+1); sumz += bin_content; if (bin_content <= content_lmt) continue; numpixels += Math.round(bin_content*coef); } } } // too many pixels - use box drawing if (numpixels > (main.webgl ? 100000 : 30000)) return false; const pnts = new PointsCreator(numpixels, main.webgl, main.size_x3d/200), bins = new Int32Array(numpixels), xaxis = this.getAxis('x'), yaxis = this.getAxis('y'), zaxis = this.getAxis('z'), rnd = new TRandom(sumz); let nbin = 0; for (i = i1; i < i2; i += di) { for (j = j1; j < j2; j += dj) { for (k = k1; k < k2; k += dk) { 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 { main.add3DMesh(mesh); mesh.bins = bins; mesh.painter = this; mesh.tip_color = 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.painter, fp = p.getFramePainter(), tip = p.get3DToolTip(this.bins[indx]); tip.x1 = fp.grx(p.getAxis('x').GetBinLowEdge(tip.ix)); tip.x2 = fp.grx(p.getAxis('x').GetBinLowEdge(tip.ix+di)); tip.y1 = fp.gry(p.getAxis('y').GetBinLowEdge(tip.iy)); tip.y2 = fp.gry(p.getAxis('y').GetBinLowEdge(tip.iy+dj)); tip.z1 = fp.grz(p.getAxis('z').GetBinLowEdge(tip.iz)); tip.z2 = fp.grz(p.getAxis('z').GetBinLowEdge(tip.iz+dk)); tip.color = this.tip_color; tip.opacity = 0.3; return tip; }; return true; }); } /** @summary Drawing of 3D histogram */ draw3DBins(handle) { const main = this.getFramePainter(); let fillcolor = this.v7EvalColor('fill_color', 'red'), use_lambert = false, use_helper = false, use_colors = false, use_opacity = 1, use_scale = true, tipscale = 0.5, single_bin_geom; if (this.options.Sphere) { // drawing spheres tipscale = 0.4; use_lambert = true; if (this.options.Sphere === 11) use_colors = true; single_bin_geom = new THREE.SphereGeometry(0.5, main.webgl ? 16 : 8, main.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 (this.options.Box === 11) use_colors = true; else if (this.options.Box === 12) { use_colors = true; use_helper = false; } else if (this.options.Color) { use_colors = true; use_opacity = 0.5; use_scale = false; use_helper = false; 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)); } if (use_scale) use_scale = (this.gminbin || this.gmaxbin) ? 1 / Math.max(Math.abs(this.gminbin), Math.abs(this.gmaxbin)) : 1; const histo = this.getHisto(), i1 = handle.i1, i2 = handle.i2, di = handle.stepi, j1 = handle.j1, j2 = handle.j2, dj = handle.stepj, k1 = handle.k1, k2 = handle.k2, dk = handle.stepk, bins_matrixes = [], bins_colors = [], bins_ids = []; let palette = null; if (use_colors) { palette = main.getHistPalette(); this.createContour(main, palette); } if ((i2 <= i1) || (j2 <= j1) || (k2 <= k1)) return true; const xaxis = this.getAxis('x'), yaxis = this.getAxis('y'), zaxis = this.getAxis('z'); for (let i = i1; i < i2; i += di) { const grx1 = main.grx(xaxis.GetBinLowEdge(i+1)), grx2 = main.grx(xaxis.GetBinLowEdge(i+2)); for (let j = j1; j < j2; j += dj) { const gry1 = main.gry(yaxis.GetBinLowEdge(j+1)), gry2 = main.gry(yaxis.GetBinLowEdge(j+2)); for (let k = k1; k < k2; k +=dk) { const bin_content = histo.getBinContent(i+1, j+1, k+1); if (!this.options.Color && ((bin_content === 0) || (bin_content < this.gminbin))) continue; const wei = use_scale ? Math.pow(Math.abs(bin_content * use_scale), 0.3333) : 1; if (wei < 1e-3) continue; // do not show very small bins if (use_colors) { const colindx = palette.getContourIndex(bin_content); if (colindx < 0) continue; bins_colors.push(palette.getColor(colindx)); } const grz1 = main.grz(zaxis.GetBinLowEdge(k+1)), grz2 = main.grz(zaxis.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); } } } 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.painter, fp = p.getFramePainter(), tip = p.get3DToolTip(binid), grx1 = fp.grx(xaxis.GetBinCoord(tip.ix-1)), grx2 = fp.grx(xaxis.GetBinCoord(tip.ix)), gry1 = fp.gry(yaxis.GetBinCoord(tip.iy-1)), gry2 = fp.gry(yaxis.GetBinCoord(tip.iy)), grz1 = fp.grz(zaxis.GetBinCoord(tip.iz-1)), grz2 = fp.grz(zaxis.GetBinCoord(tip.iz)), wei2 = (this.use_scale ? Math.pow(Math.abs(tip.value*this.use_scale), 0.3333) : 1) * 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 && (use_opacity !== 1)) { // create individual meshes for each bin for (let n = 0; n < bins_matrixes.length; ++n) { const opacity = 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.painter = this; bin_mesh.binid = bins_ids[n]; bin_mesh.tipscale = tipscale; bin_mesh.tip_color = 0x00FF00; bin_mesh.use_scale = use_scale; bin_mesh.tooltip = getBinTooltip; main.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.painter = this; all_bins_mesh.bins = bins_ids; all_bins_mesh.tipscale = tipscale; all_bins_mesh.tip_color = 0x00FF00; all_bins_mesh.use_scale = use_scale; all_bins_mesh.tooltip = getBinTooltip; main.add3DMesh(all_bins_mesh); } if (use_helper) { const helper_segments = Box3D.Segments, helper_positions = new Float32Array(bins_matrixes.length * Box3D.Segments.length * 3); let vvv = 0; for (let i = 0; i < bins_matrixes.length; ++i) { const m = bins_matrixes[i].elements; for (let n = 0; n < helper_segments.length; ++n, vvv += 3) { const vert = Box3D.Vertices[helper_segments[n]]; helper_positions[vvv] = m[12] + (vert.x - 0.5) * m[0]; helper_positions[vvv+1] = m[13] + (vert.y - 0.5) * m[5]; helper_positions[vvv+2] = m[14] + (vert.z - 0.5) * m[10]; } } const helper_material = new THREE.LineBasicMaterial({ color: this.v7EvalColor('line_color', 'lightblue') }), lines = createLineSegments(helper_positions, helper_material); main.add3DMesh(lines); } if (use_colors) this.updatePaletteDraw(); return true; } draw3D() { if (!this.draw_content) return false; // this.options.Scatter = false; // this.options.Box = true; const handle = this.prepareDraw({ only_indexes: true, extra: -0.5, right_extra: -1 }), pr = this.options.Scatter ? this.draw3DScatter(handle) : Promise.resolve(false); return pr.then(res => { return res || this.draw3DBins(handle); }); } /** @summary Redraw histogram */ redraw(reason) { const main = this.getFramePainter(); // who makes axis and 3D drawing if (reason === 'resize') { if (main.resize3D()) main.render3D(); return this; } assignFrame3DMethods(main); return main.create3DScene(this.options.Render3D).then(() => { main.setAxesRanges(this.getAxis('x'), this.xmin, this.xmax, this.getAxis('y'), this.ymin, this.ymax, this.getAxis('z'), this.zmin, this.zmax); main.set3DOptions(this.options); main.drawXYZ(main.toplevel, RAxisPainter, { zoom: settings.Zooming, ndim: 3, draw: true, v7: true }); return this.drawingBins(reason); }).then(() => this.draw3D()).then(() => { main.render3D(); main.addKeysHandler(); return this; }); } /** @summary Fill pad toolbar with RH3-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.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.getHisto(); 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 = this.getAxis('x').GetBinLowEdge(ileft+1); xmax = this.getAxis('x').GetBinLowEdge(iright+1); isany = true; } if ((jleft > j1 || jright < j2) && (jleft < jright - 1)) { ymin = this.getAxis('y').GetBinLowEdge(jleft+1); ymax = this.getAxis('y').GetBinLowEdge(jright+1); isany = true; } if ((kleft > k1 || kright < k2) && (kleft < kright - 1)) { zmin = this.getAxis('z').GetBinLowEdge(kleft+1); zmax = this.getAxis('z').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 draw RH3 object */ static async draw(dom, histo /* ,opt */) { const painter = new RH3Painter(dom, histo); painter.mode3d = true; return ensureRCanvas(painter, '3d').then(() => { painter.setAsMainPainter(); painter.options = { Box: 0, Scatter: false, Sphere: 0, Color: false, minimum: kNoZoom, maximum: kNoZoom, FrontBox: false, BackBox: false }; const kind = painter.v7EvalAttr('kind', ''), sub = painter.v7EvalAttr('sub', 0), o = painter.options; switch (kind) { case 'box': o.Box = 10 + sub; break; case 'sphere': o.Sphere = 10 + sub; break; case 'col': o.Color = true; break; case 'scat': o.Scatter = true; break; default: o.Box = 10; } painter.scanContent(); return painter.redraw(); }); } } // class RH3Painter /** @summary draw RHistDisplayItem object * @private */ function drawHistDisplayItem(dom, obj, opt) { if (!obj) return null; if (obj.fAxes.length === 1) return RH1Painter.draw(dom, obj, opt); if (obj.fAxes.length === 2) return RH2Painter.draw(dom, obj, opt); if (obj.fAxes.length === 3) return RH3Painter.draw(dom, obj, opt); return null; } var RH3Painter$1 = /*#__PURE__*/Object.freeze({ __proto__: null, RH3Painter: RH3Painter, drawHistDisplayItem: drawHistDisplayItem }); 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.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.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.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.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.getElementCanvPainter = getElementCanvPainter; exports.getElementMainPainter = getElementMainPainter; exports.getElementRect = getElementRect; exports.getHPainter = getHPainter; exports.getMethods = getMethods; exports.getPromise = getPromise; exports.getTDatime = getTDatime; 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.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.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.urlClassPrefix = urlClassPrefix; exports.version = version; exports.version_date = version_date; exports.version_id = version_id; }));