UNPKG

jsroot

Version:
1,374 lines (1,138 loc) 97 kB
import { gStyle, settings, constants, browser, internals, BIT, create, toJSON, isBatchMode, loadModules, loadScript, injectCode, isPromise, getPromise, postponePromise, isObject, isFunc, isStr, clTObjArray, clTColor, clTPad, clTFrame, clTStyle, clTLegend, clTHStack, clTMultiGraph, clTLegendEntry, nsSVG, kTitle, clTList, urlClassPrefix } from '../core.mjs'; import { select as d3_select, rgb as d3_rgb } from '../d3.mjs'; import { ColorPalette, adoptRootColors, getColorPalette, getGrayColors, extendRootColors, getRGBfromTColor, decodeWebCanvasColors } from '../base/colors.mjs'; import { prSVG, prJSON, getElementRect, getAbsPosInCanvas, DrawOptions, compressSVG, makeTranslate, getTDatime, convertDate, svgToImage, getBoxDecorations } from '../base/BasePainter.mjs'; import { ObjectPainter, selectActivePad, getActivePad } from '../base/ObjectPainter.mjs'; import { TAttLineHandler } from '../base/TAttLineHandler.mjs'; import { addCustomFont } from '../base/FontHandler.mjs'; import { addDragHandler } from './TFramePainter.mjs'; import { createMenu, closeMenu } from '../gui/menu.mjs'; import { ToolbarIcons, registerForResize, saveFile } from '../gui/utils.mjs'; import { BrowserLayout, getHPainter } from '../gui/display.mjs'; const clTButton = 'TButton', kIsGrayscale = BIT(22); function 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()) d3_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 (d3_select(this).attr('key') === keyname) found_func = d3_select(this).attr('name'); }); } return found_func; }, removePadButtons() { const group = this.getLayerSvg('btns_layer', this.this_pad_name); if (!group.empty()) { group.selectAll('*').remove(); group.property('nextx', null); } }, showPadButtons() { const group = this.getLayerSvg('btns_layer', this.this_pad_name); if (group.empty()) return; // clean all previous buttons group.selectAll('*').remove(); if (!this._buttons) return; const iscan = this.iscan || !this.has_canvas, y = 0; let ctrl, x = group.property('leftside') ? 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 = { kNone: 0, kObject: 1, kSVG: 2, kSubPad: 3, kColors: 4, kStyle: 5, kFont: 6 }; /** @summary Fill TWebObjectOptions for painter * @private */ function createWebObjectOptions(painter) { if (!painter?.snapid) return null; const obj = { _typename: 'TWebObjectOptions', snapid: painter.snapid.toString(), opt: painter.getDrawOpt(true), fcust: '', fopt: [] }; if (isFunc(painter.fillWebObjectOptions)) painter.fillWebObjectOptions(obj); return obj; } /** * @summary Painter for TPad object * @private */ class TPadPainter extends ObjectPainter { #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.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', d3_rgb(this.fillatt.color).brighter(0.5).formatRgb()); svg_border2.attr('d', arr[1]) .call(this.fillatt.func) .style('fill', d3_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(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