UNPKG

jsrootdi

Version:
1,315 lines (1,087 loc) 93.9 kB
import { gStyle, settings, constants, browser, internals, BIT, create, toJSON, isBatchMode, loadScript, injectCode, isPromise, getPromise, postponePromise, isObject, isFunc, isStr, clTObjArray, clTPaveText, clTColor, clTPad, clTFrame, clTStyle, clTLegend, clTHStack, clTMultiGraph, clTLegendEntry, kTitle } 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 { getElementRect, getAbsPosInCanvas, DrawOptions, compressSVG, makeTranslate, convertDate, svgToImage } from '../base/BasePainter.mjs'; import { ObjectPainter, selectActivePad, getActivePad } from '../base/ObjectPainter.mjs'; import { TAttLineHandler } from '../base/TAttLineHandler.mjs'; import { addCustomFont } from '../base/FontHandler.mjs'; import { addDragHandler } from './TFramePainter.mjs'; import { createMenu, closeMenu } from '../gui/menu.mjs'; import { ToolbarIcons, registerForResize, saveFile } from '../gui/utils.mjs'; import { BrowserLayout, getHPainter } from '../gui/display.mjs'; const clTButton = 'TButton', kIsGrayscale = BIT(22); function getButtonSize(handler, fact) { return Math.round((fact || 1) * (handler.iscan || !handler.has_canvas ? 16 : 12)); } function toggleButtonsVisibility(handler, action, evnt) { evnt?.preventDefault(); evnt?.stopPropagation(); const group = handler.getLayerSvg('btns_layer', handler.this_pad_name), btn = group.select('[name=\'Toggle\']'); if (btn.empty()) return; let state = btn.property('buttons_state'); if (btn.property('timout_handler')) { if (action !== 'timeout') clearTimeout(btn.property('timout_handler')); btn.property('timout_handler', null); } let is_visible = false; switch (action) { case 'enable': is_visible = true; handler.btns_active_flag = true; break; case 'enterbtn': handler.btns_active_flag = true; return; // do nothing, just cleanup timeout case 'timeout': is_visible = false; break; case 'toggle': state = !state; btn.property('buttons_state', state); is_visible = state; break; case 'disable': case 'leavebtn': handler.btns_active_flag = false; if (!state) btn.property('timout_handler', setTimeout(() => toggleButtonsVisibility(handler, 'timeout'), 1200)); return; } group.selectAll('svg').each(function() { if (this !== btn.node()) d3_select(this).style('display', is_visible ? '' : 'none'); }); } const PadButtonsHandler = { alignButtons(btns, width, height) { const sz0 = getButtonSize(this, 1.25), nextx = (btns.property('nextx') || 0) + sz0; let btns_x, btns_y; if (btns.property('vertical')) { btns_x = btns.property('leftside') ? 2 : (width - sz0); btns_y = height - nextx; } else { btns_x = btns.property('leftside') ? 2 : (width - nextx); btns_y = height - sz0; } makeTranslate(btns, btns_x, btns_y); }, findPadButton(keyname) { const group = this.getLayerSvg('btns_layer', this.this_pad_name); let found_func = ''; if (!group.empty()) { group.selectAll('svg').each(function() { if (d3_select(this).attr('key') === keyname) found_func = d3_select(this).attr('name'); }); } return found_func; }, removePadButtons() { const group = this.getLayerSvg('btns_layer', this.this_pad_name); if (!group.empty()) { group.selectAll('*').remove(); group.property('nextx', null); } }, showPadButtons() { const group = this.getLayerSvg('btns_layer', this.this_pad_name); if (group.empty()) return; // clean all previous buttons group.selectAll('*').remove(); if (!this._buttons) return; const iscan = this.iscan || !this.has_canvas, y = 0; let ctrl, x = group.property('leftside') ? getButtonSize(this, 1.25) : 0; if (this._fast_drawing) { ctrl = ToolbarIcons.createSVG(group, ToolbarIcons.circle, getButtonSize(this), 'enlargePad', false) .attr('name', 'Enlarge').attr('x', 0).attr('y', 0) .on('click', evnt => this.clickPadButton('enlargePad', evnt)); } else { ctrl = ToolbarIcons.createSVG(group, ToolbarIcons.rect, getButtonSize(this), 'Toggle tool buttons', false) .attr('name', 'Toggle').attr('x', 0).attr('y', 0) .property('buttons_state', (settings.ToolBar !== 'popup') || browser.touches) .on('click', evnt => toggleButtonsVisibility(this, 'toggle', evnt)); ctrl.node()._mouseenter = () => toggleButtonsVisibility(this, 'enable'); ctrl.node()._mouseleave = () => toggleButtonsVisibility(this, 'disable'); for (let k = 0; k < this._buttons.length; ++k) { const item = this._buttons[k]; let btn = item.btn; if (isStr(btn)) btn = ToolbarIcons[btn]; if (!btn) btn = ToolbarIcons.circle; const svg = ToolbarIcons.createSVG(group, btn, getButtonSize(this), item.tooltip + (iscan ? '' : (` on pad ${this.this_pad_name}`)) + (item.keyname ? ` (keyshortcut ${item.keyname})` : ''), false); if (group.property('vertical')) svg.attr('x', y).attr('y', x); else svg.attr('x', x).attr('y', y); svg.attr('name', item.funcname) .style('display', ctrl.property('buttons_state') ? '' : 'none') .attr('key', item.keyname || null) .on('click', evnt => this.clickPadButton(item.funcname, evnt)); svg.node()._mouseenter = () => toggleButtonsVisibility(this, 'enterbtn'); svg.node()._mouseleave = () => toggleButtonsVisibility(this, 'leavebtn'); x += getButtonSize(this, 1.25); } } group.property('nextx', x); this.alignButtons(group, this.getPadWidth(), this.getPadHeight()); if (group.property('vertical')) ctrl.attr('y', x); else if (!group.property('leftside')) ctrl.attr('x', x); }, assign(painter) { Object.assign(painter, this); } }, // PadButtonsHandler // identifier used in TWebCanvas painter webSnapIds = { kNone: 0, kObject: 1, kSVG: 2, kSubPad: 3, kColors: 4, kStyle: 5, kFont: 6 }; /** @summary Fill TWebObjectOptions for painter * @private */ function createWebObjectOptions(painter) { if (!painter?.snapid) return null; const obj = { _typename: 'TWebObjectOptions', snapid: painter.snapid.toString(), opt: painter.getDrawOpt(true), fcust: '', fopt: [] }; if (isFunc(painter.fillWebObjectOptions)) painter.fillWebObjectOptions(obj); return obj; } /** * @summary Painter for TPad object * @private */ class TPadPainter extends ObjectPainter { /** @summary constructor * @param {object|string} dom - DOM element for drawing or element id * @param {object} pad - TPad object to draw * @param {boolean} [iscan] - if TCanvas object */ constructor(dom, pad, iscan) { super(dom, pad); this.pad = pad; this.iscan = iscan; // indicate if working with canvas this.this_pad_name = ''; if (!this.iscan && pad?.fName) { this.this_pad_name = pad.fName.replace(' ', '_'); // avoid empty symbol in pad name const regexp = /^[A-Za-z][A-Za-z0-9_]*$/; if (!regexp.test(this.this_pad_name) || ((this.this_pad_name === 'button') && (pad._typename === clTButton))) this.this_pad_name = 'jsroot_pad_' + internals.id_counter++; } this.painters = []; // complete list of all painters in the pad this.has_canvas = true; this.forEachPainter = this.forEachPainterInPad; const d = this.selectDom(); if (!d.empty() && d.property('_batch_mode')) this.batch_mode = true; } /** @summary Indicates that drawing runs in batch mode * @private */ isBatchMode() { if (this.batch_mode !== undefined) return this.batch_mode; if (isBatchMode()) return true; if (!this.iscan && this.has_canvas) return this.getCanvPainter()?.isBatchMode(); return false; } /** @summary Indicates that is is Root6 pad painter * @private */ isRoot6() { return true; } /** @summary Returns true if pad is editable */ isEditable() { return this.pad?.fEditable ?? true; } /** @summary Returns SVG element for the pad itself * @private */ svg_this_pad() { return this.getPadSvg(this.this_pad_name); } /** @summary Returns main painter on the pad * @desc Typically main painter is TH1/TH2 object which is drawing axes * @private */ getMainPainter() { return this.main_painter_ref || null; } /** @summary Assign main painter on the pad * @desc Typically main painter is TH1/TH2 object which is drawing axes * @private */ setMainPainter(painter, force) { if (!this.main_painter_ref || force) this.main_painter_ref = painter; } /** @summary cleanup pad and all primitives inside */ cleanup() { if (this._doing_draw) console.error('pad drawing is not completed when cleanup is called'); this.painters.forEach(p => p.cleanup()); const svg_p = this.svg_this_pad(); if (!svg_p.empty()) { svg_p.property('pad_painter', null); if (!this.iscan) svg_p.remove(); } delete this.main_painter_ref; delete this.frame_painter_ref; delete this.pads_cache; delete this.custom_palette; delete this._pad_x; delete this._pad_y; delete this._pad_width; delete this._pad_height; delete this._doing_draw; delete this._interactively_changed; delete this._snap_primitives; delete this._last_grayscale; delete this._custom_colors; delete this._custom_palette_indexes; delete this._custom_palette_colors; delete this.root_colors; this.painters = []; this.pad = null; this.this_pad_name = undefined; this.has_canvas = false; selectActivePad({ pp: this, active: false }); super.cleanup(); } /** @summary Returns frame painter inside the pad * @private */ getFramePainter() { return this.frame_painter_ref; } /** @summary get pad width */ getPadWidth() { return this._pad_width || 0; } /** @summary get pad height */ getPadHeight() { return this._pad_height || 0; } /** @summary get pad rect */ getPadRect() { return { x: this._pad_x || 0, y: this._pad_y || 0, width: this.getPadWidth(), height: this.getPadHeight() }; } /** @summary return pad log state x or y are allowed */ getPadLog(name) { const pad = this.getRootPad(); if (name === 'x') return pad?.fLogx; if (name === 'y') return pad?.fLogv ?? pad?.fLogy; return false; } /** @summary Returns frame coordiantes - 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 */ cleanPrimitives(selector) { if (!isFunc(selector)) return; for (let k = this.painters.length-1; k >= 0; --k) { if (selector(this.painters[k])) { this.painters[k].cleanup(); this.painters.splice(k, 1); } } } /** @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(indx) { const prim = this.painters[indx], arr = []; let resindx = indx; for (let k = this.painters.length-1; k >= 0; --k) { if ((k === indx) || this.painters[k].isSecondary(prim)) { arr.push(this.painters[k]); this.painters.splice(k, 1); if (k <= indx) resindx--; } } arr.forEach(painter => { painter.cleanup(); if (this.main_painter_ref === painter) { delete this.main_painter_ref; resindx = -111; } }); return resindx; } /** @summary returns custom palette associated with pad or top canvas * @private */ getCustomPalette() { return this.custom_palette || this.getCanvPainter()?.custom_palette; } /** @summary Returns number of painters * @private */ getNumPainters() { return this.painters.length; } /** @summary Provides automatic color * @desc Uses ROOT colors palette if possible * @private */ getAutoColor(numprimitives) { if (!numprimitives) numprimitives = this._num_primitives || 5; if (numprimitives < 2) numprimitives = 2; let indx = this._auto_color ?? 0; this._auto_color = (indx + 1) % numprimitives; if (indx >= numprimitives) indx = numprimitives - 1; const indexes = this._custom_palette_indexes || this.getCanvPainter()?._custom_palette_indexes; if (indexes?.length) { const p = Math.round(indx * (indexes.length - 3) / (numprimitives - 1)); return indexes[p]; } if (!this._auto_palette) this._auto_palette = getColorPalette(settings.Palette, this.isGrayscale()); const palindx = Math.round(indx * (this._auto_palette.getLength()-3) / (numprimitives-1)), colvalue = this._auto_palette.getColor(palindx); return this.addColor(colvalue); } /** @summary Call function for each painter in pad * @param {function} userfunc - function to call * @param {string} kind - 'all' for all objects (default), 'pads' only pads and subpads, '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, place) { 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, place }); } /** @summary method redirect call to pad events receiver */ selectObjectPainter(painter, pos, place) { 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, place); } /** @summary Draw pad active border * @private */ drawActiveBorder(svg_rect, is_active) { if (is_active !== undefined) { if (this.is_active_pad === is_active) return; this.is_active_pad = is_active; } if (this.is_active_pad === undefined) return; if (!svg_rect) svg_rect = this.iscan ? this.getCanvSvg().selectChild('.canvas_fillrect') : this.svg_this_pad().selectChild('.root_pad_border'); const cp = this.getCanvPainter(); let lineatt = this.is_active_pad && cp?.highlight_gpad ? new TAttLineHandler({ style: 1, width: 1, color: 'red' }) : this.lineatt; if (!lineatt) lineatt = new TAttLineHandler({ color: 'none' }); svg_rect.call(lineatt.func); } /** @summary Set fast drawing property depending on the size * @private */ setFastDrawing(w, h) { const was_fast = this._fast_drawing; this._fast_drawing = settings.SmallPad && ((w < settings.SmallPad.width) || (h < settings.SmallPad.height)); if (was_fast !== this._fast_drawing) this.showPadButtons(); } /** @summary Returns true if canvas configured with grayscale * @private */ isGrayscale() { if (!this.iscan) return false; return this.pad?.TestBit(kIsGrayscale) ?? false; } /** @summary Set grayscale mode for the canvas * @private */ setGrayscale(flag) { if (!this.iscan) return; let changed = false; if (flag === undefined) { flag = this.pad?.TestBit(kIsGrayscale) ?? false; changed = (this._last_grayscale !== undefined) && (this._last_grayscale !== flag); } else if (flag !== this.pad?.TestBit(kIsGrayscale)) { this.pad?.InvertBit(kIsGrayscale); changed = true; } if (changed) this.forEachPainter(p => { delete p._color_palette; }); this.root_colors = flag ? getGrayColors(this._custom_colors) : this._custom_colors; this._last_grayscale = flag; this.custom_palette = this._custom_palette_colors ? new ColorPalette(this._custom_palette_colors, flag) : null; } /** @summary Create SVG element for canvas */ createCanvasSvg(check_resize, new_size) { const is_batch = this.isBatchMode(), lmt = 5; let factor = null, svg = null, rect = null, btns, info, frect; if (check_resize > 0) { if (this._fixed_size) return check_resize > 1; // flag used to force re-drawing of all subpads 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('current_pad', '') // 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', 'http://www.w3.org/2000/svg'); else if (!this.online_canvas) svg.append('svg:title').text('ROOT canvas'); if (!is_batch || (this.pad.fFillStyle > 0)) frect = svg.append('svg:path').attr('class', 'canvas_fillrect'); if (!is_batch) { frect.style('pointer-events', 'visibleFill') .on('dblclick', evnt => this.enlargePad(evnt, true)) .on('click', () => this.selectObjectPainter()) .on('mouseenter', () => this.showObjectStatus()) .on('contextmenu', settings.ContextMenu ? evnt => this.padContextMenu(evnt) : null); } svg.append('svg:g').attr('class', 'primitives_layer'); info = svg.append('svg:g').attr('class', 'info_layer'); if (!is_batch) { btns = svg.append('svg:g') .attr('class', 'btns_layer') .property('leftside', settings.ToolBarSide === 'left') .property('vertical', settings.ToolBarVert); } factor = 0.66; if (this.pad?.fCw && this.pad?.fCh && (this.pad?.fCw > 0)) { factor = this.pad.fCh / this.pad.fCw; if ((factor < 0.1) || (factor > 10)) factor = 0.66; } if (this._fixed_size) { render_to.style('overflow', 'auto'); rect = { width: this.pad.fCw, height: this.pad.fCh }; if (!rect.width || !rect.height) rect = getElementRect(render_to); } else rect = this.testMainResize(2, new_size, factor); } this.setGrayscale(); this.createAttFill({ attr: this.pad }); if ((rect.width <= lmt) || (rect.height <= lmt)) { svg.style('display', 'none'); console.warn(`Hide canvas while geometry too small w=${rect.width} h=${rect.height}`); if (this._pad_width && this._pad_height) { // use last valid dimensions rect.width = this._pad_width; rect.height = this._pad_height; } else { // just to complete drawing. rect.width = 800; rect.height = 600; } } else svg.style('display', null); svg.attr('x', 0).attr('y', 0).style('position', 'absolute'); if (this._fixed_size) svg.attr('width', rect.width).attr('height', rect.height); else svg.style('width', '100%').style('height', '100%').style('left', 0).style('top', 0).style('bottom', 0).style('right', 0); svg.style('filter', settings.DarkMode || this.pad?.$dark ? 'invert(100%)' : null); svg.attr('viewBox', `0 0 ${rect.width} ${rect.height}`) .attr('preserveAspectRatio', 'none') // we do not preserve relative ratio .property('height_factor', factor) .property('draw_x', 0) .property('draw_y', 0) .property('draw_width', rect.width) .property('draw_height', rect.height); this._pad_x = 0; this._pad_y = 0; this._pad_width = rect.width; this._pad_height = rect.height; if (frect) { frect.attr('d', `M0,0H${rect.width}V${rect.height}H0Z`) .call(this.fillatt.func); this.drawActiveBorder(frect); } this.setFastDrawing(rect.width * (1 - this.pad.fLeftMargin - this.pad.fRightMargin), rect.height * (1 - this.pad.fBottomMargin - this.pad.fTopMargin)); if (this.alignButtons && btns) this.alignButtons(btns, rect.width, rect.height); let dt = info.selectChild('.canvas_date'); if (!gStyle.fOptDate) dt.remove(); else { if (dt.empty()) dt = info.append('text').attr('class', 'canvas_date'); const posy = Math.round(rect.height * (1 - gStyle.fDateY)), date = new Date(); let posx = Math.round(rect.width * gStyle.fDateX); if (!is_batch && (posx < 25)) posx = 25; if (gStyle.fOptDate > 3) date.setTime(gStyle.fOptDate*1000); makeTranslate(dt, posx, posy) .style('text-anchor', 'start') .text(convertDate(date)); } const iname = this.getItemName(); if (iname) this.drawItemNameOnCanvas(iname); else if (!gStyle.fOptFile) info.selectChild('.canvas_item').remove(); return true; } /** @summary Draw item name on canvas if gStyle.fOptFile is configured * @private */ drawItemNameOnCanvas(item_name) { const info = this.getLayerSvg('info_layer', this.this_pad_name); let df = info.selectChild('.canvas_item'); const fitem = getHPainter().findRootFileForItem(item_name), fname = (gStyle.fOptFile === 3) ? item_name : ((gStyle.fOptFile === 2) ? fitem?._fullurl : fitem?._name); if (!gStyle.fOptFile || !fname) df.remove(); else { if (df.empty()) df = info.append('text').attr('class', 'canvas_item'); const rect = this.getPadRect(); makeTranslate(df, Math.round(rect.width * (1 - gStyle.fDateX)), Math.round(rect.height * (1 - gStyle.fDateY))) .style('text-anchor', 'end') .text(fname); } if (((gStyle.fOptDate === 2) || (gStyle.fOptDate === 3)) && fitem?._file) { info.selectChild('.canvas_date') .text(convertDate(gStyle.fOptDate === 2 ? fitem._file.fDatimeC.getDate() : fitem._file.fDatimeM.getDate())); } } /** @summary Return true if this pad enlarged */ isPadEnlarged() { if (this.iscan || !this.has_canvas) return this.enlargeMain('state') === 'on'; return this.getCanvSvg().property('pad_enlarged') === this.pad; } /** @summary Enlarge pad draw element when possible */ enlargePad(evnt, is_dblclick, is_escape) { evnt?.preventDefault(); evnt?.stopPropagation(); // ignore double click on canvas itself for enlarge if (is_dblclick && this._websocket && (this.enlargeMain('state') === 'off')) return; const svg_can = this.getCanvSvg(), pad_enlarged = svg_can.property('pad_enlarged'); if (this.iscan || !this.has_canvas || (!pad_enlarged && !this.hasObjectsToDraw() && !this.painters)) { if (this._fixed_size) return; // canvas cannot be enlarged in such mode if (!this.enlargeMain(is_escape ? false : 'toggle')) return; if (this.enlargeMain('state') === 'off') svg_can.property('pad_enlarged', null); else selectActivePad({ pp: this, active: true }); } else if (!pad_enlarged && !is_escape) { this.enlargeMain(true, true); svg_can.property('pad_enlarged', this.pad); selectActivePad({ pp: this, active: true }); } else if (pad_enlarged === this.pad) { this.enlargeMain(false); svg_can.property('pad_enlarged', null); } else if (!is_escape && is_dblclick) console.error('missmatch with pad double click events'); return this.checkResize(true); } /** @summary Create main SVG element for pad * @return true when pad is displayed and all its items should be redrawn */ createPadSvg(only_resize) { if (!this.has_canvas) { this.createCanvasSvg(only_resize ? 2 : 0); return true; } const svg_can = this.getCanvSvg(), width = svg_can.property('draw_width'), height = svg_can.property('draw_height'), pad_enlarged = svg_can.property('pad_enlarged'), pad_visible = !this.pad_draw_disabled && (!pad_enlarged || (pad_enlarged === this.pad)), is_batch = this.isBatchMode(); let w = Math.round(this.pad.fAbsWNDC * width), h = Math.round(this.pad.fAbsHNDC * height), x = Math.round(this.pad.fAbsXlowNDC * width), y = Math.round(height * (1 - this.pad.fAbsYlowNDC)) - h, svg_pad, svg_border, btns; if (pad_enlarged === this.pad) { w = width; h = height; x = y = 0; } if (only_resize) { svg_pad = this.svg_this_pad(); svg_border = svg_pad.selectChild('.root_pad_border'); if (!is_batch) btns = this.getLayerSvg('btns_layer', this.this_pad_name); this.addPadInteractive(true); } else { svg_pad = svg_can.selectChild('.primitives_layer') .append('svg:svg') // svg used to blend all drawings outside .classed('__root_pad_' + this.this_pad_name, true) .attr('pad', this.this_pad_name) // set extra attribute to mark pad name .property('pad_painter', this); // this is custom property if (!is_batch) svg_pad.append('svg:title').text('subpad ' + this.this_pad_name); // need to check attributes directly while attributes objects will be created later if (!is_batch || (this.pad.fFillStyle > 0) || ((this.pad.fLineStyle > 0) && (this.pad.fLineColor > 0))) svg_border = svg_pad.append('svg:path').attr('class', 'root_pad_border'); if (!is_batch) { svg_border.style('pointer-events', 'visibleFill') // get events also for not visible rect .on('dblclick', evnt => this.enlargePad(evnt, true)) .on('click', () => this.selectObjectPainter()) .on('mouseenter', () => this.showObjectStatus()) .on('contextmenu', settings.ContextMenu ? evnt => this.padContextMenu(evnt) : null); } svg_pad.append('svg:g').attr('class', 'primitives_layer'); if (!is_batch) { btns = svg_pad.append('svg:g') .attr('class', 'btns_layer') .property('leftside', settings.ToolBarSide !== 'left') .property('vertical', settings.ToolBarVert); } } this.createAttFill({ attr: this.pad }); this.createAttLine({ attr: this.pad, color0: !this.pad.fBorderMode ? 'none' : '' }); svg_pad.style('display', pad_visible ? null : 'none') .attr('viewBox', `0 0 ${w} ${h}`) // due to svg .attr('preserveAspectRatio', 'none') // due to svg, we do not preserve relative ratio .attr('x', x) // due to svg .attr('y', y) // due to svg .attr('width', w) // due to svg .attr('height', h) // due to svg .property('draw_x', x) // this is to make similar with canvas .property('draw_y', y) .property('draw_width', w) .property('draw_height', h); this._pad_x = x; this._pad_y = y; this._pad_width = w; this._pad_height = h; if (svg_border) { svg_border.attr('d', `M0,0H${w}V${h}H0Z`) .call(this.fillatt.func) .call(this.lineatt.func); this.drawActiveBorder(svg_border); let svg_border1 = svg_pad.selectChild('.root_pad_border1'), svg_border2 = svg_pad.selectChild('.root_pad_border2'); if (this.pad.fBorderMode && this.pad.fBorderSize) { const pw = this.pad.fBorderSize, ph = this.pad.fBorderSize, side1 = `M0,0h${w}l${-pw},${ph}h${2*pw-w}v${h-2*ph}l${-pw},${ph}z`, side2 = `M${w},${h}v${-h}l${-pw},${ph}v${h-2*ph}h${2*pw-w}l${-pw},${ph}z`; if (svg_border2.empty()) svg_border2 = svg_pad.insert('svg:path', '.primitives_layer').attr('class', 'root_pad_border2'); if (svg_border1.empty()) svg_border1 = svg_pad.insert('svg:path', '.primitives_layer').attr('class', 'root_pad_border1'); svg_border1.attr('d', this.pad.fBorderMode > 0 ? side1 : side2) .call(this.fillatt.func) .style('fill', d3_rgb(this.fillatt.color).brighter(0.5).formatHex()); svg_border2.attr('d', this.pad.fBorderMode > 0 ? side2 : side1) .call(this.fillatt.func) .style('fill', d3_rgb(this.fillatt.color).darker(0.5).formatHex()); } else { svg_border1.remove(); svg_border2.remove(); } } this.setFastDrawing(w * (1 - this.pad.fLeftMargin-this.pad.fRightMargin), h * (1 - this.pad.fBottomMargin - this.pad.fTopMargin)); // special case of 3D canvas overlay if (svg_pad.property('can3d') === constants.Embed3D.Overlay) { this.selectDom().select('.draw3d_' + this.this_pad_name) .style('display', pad_visible ? '' : 'none'); } if (this.alignButtons && btns) this.alignButtons(btns, w, h); return pad_visible; } /** @summary Add pad interactive features like dragging and resize * @private */ addPadInteractive(cleanup = false) { if (isFunc(this.$userInteractive)) { this.$userInteractive(); delete this.$userInteractive; } if (this.isBatchMode() || this.iscan) return; const svg_can = this.getCanvSvg(), width = svg_can.property('draw_width'), height = svg_can.property('draw_height'); addDragHandler(this, { cleanup, // do cleanup to let assign new handlers later on x: this._pad_x, y: this._pad_y, width: this._pad_width, height: this._pad_height, no_transform: true, only_resize: true, // !cleanup && (this._disable_dragging || this.getFramePainter()?.mode3d), is_disabled: kind => svg_can.property('pad_enlarged') || this.btns_active_flag || (kind === 'move' && (this._disable_dragging || this.getFramePainter()?.mode3d)), getDrawG: () => this.svg_this_pad(), pad_rect: { width, height }, minwidth: 20, minheight: 20, move_resize: (_x, _y, _w, _h) => { const x0 = this.pad.fAbsXlowNDC, y0 = this.pad.fAbsYlowNDC, scale_w = _w / width / this.pad.fAbsWNDC, scale_h = _h / height / this.pad.fAbsHNDC, shift_x = _x / width - x0, shift_y = 1 - (_y + _h) / height - y0; this.forEachPainterInPad(p => { p.pad.fAbsXlowNDC += (p.pad.fAbsXlowNDC - x0) * (scale_w - 1) + shift_x; p.pad.fAbsYlowNDC += (p.pad.fAbsYlowNDC - y0) * (scale_h - 1) + shift_y; p.pad.fAbsWNDC *= scale_w; p.pad.fAbsHNDC *= scale_h; }, 'pads'); }, redraw: () => this.interactiveRedraw('pad', 'padpos') }); } /** @summary Disable pad drawing * @desc Complete SVG element will be hidden */ disablePadDrawing() { if (!this.pad_draw_disabled && this.has_canvas && !this.iscan) { this.pad_draw_disabled = true; this.createPadSvg(true); } } /** @summary Check if it is special object, which should be handled separately * @desc It can be TStyle or list of colors or palette object * @return {boolean} true if any */ checkSpecial(obj) { if (!obj) return false; if (obj._typename === clTStyle) { Object.assign(gStyle, obj); return true; } if ((obj._typename === clTObjArray) && (obj.name === 'ListOfColors')) { if (this.options?.CreatePalette) { let arr = []; for (let n = obj.arr.length - this.options.CreatePalette; n < obj.arr.length; ++n) { const col = getRGBfromTColor(obj.arr[n]); if (!col) { console.log('Fail to create color for palette'); arr = null; break; } arr.push(col); } if (arr) this.custom_palette = new ColorPalette(arr); } if (!this.options || this.options.GlobalColors) // set global list of colors adoptRootColors(obj); // copy existing colors and extend with new values this._custom_colors = this.options?.LocalColors ? extendRootColors(null, obj) : null; return true; } if ((obj._typename === clTObjArray) && (obj.name === 'CurrentColorPalette')) { const arr = [], indx = []; let missing = false; for (let n = 0; n < obj.arr.length; ++n) { const col = obj.arr[n]; if (col?._typename === clTColor) { indx[n] = col.fNumber; arr[n] = getRGBfromTColor(col); } else { console.log(`Missing color with index ${n}`); missing = true; } } const apply = (!this.options || (!missing && !this.options.IgnorePalette)); this._custom_palette_indexes = apply ? indx : null; this._custom_palette_colors = apply ? arr : null; return true; } return false; } /** @summary Check if special objects appears in primitives * @desc it could be list of colors or palette */ checkSpecialsInPrimitives(can) { const lst = can?.fPrimitives; if (!lst) return; for (let i = 0; i < lst.arr?.length; ++i) { if (this.checkSpecial(lst.arr[i])) { lst.arr.splice(i, 1); lst.opt.splice(i, 1); i--; } } } /** @summary try to find object by name in list of pad primitives * @desc used to find title drawing * @private */ findInPrimitives(objname, objtype) { const match = obj => obj && (obj?.fName === objname) && (objtype ? (obj?._typename === objtype) : true), snap = this._snap_primitives?.find(snap => match((snap.fKind === webSnapIds.kObject) ? snap.fSnapshot : null)); if (snap) return snap.fSnapshot; return this.pad?.fPrimitives?.arr.find(match); } /** @summary Try to find painter for specified object * @desc can be used to find painter for some special objects, registered as * histogram functions * @param {object} selobj - object to which painter should be search, set null to ignore parameter * @param {string} [selname] - object name, set to null to ignore * @param {string} [seltype] - object type, set to null to ignore * @return {object} - painter for specified object (if any) * @private */ findPainterFor(selobj, selname, seltype) { return this.painters.find(p => { const pobj = p.getObject(); if (!pobj) return false; if (selobj && (pobj === selobj)) return true; if (!selname && !seltype) return false; if (selname && (pobj.fName !== selname)) return false; if (seltype && (pobj._typename !== seltype)) return false; return true; }); } /** @summary Return true if any objects beside sub-pads exists in the pad */ hasObjectsToDraw() { return this.pad?.fPrimitives?.arr?.find(obj => obj._typename !== clTPad); } /** @summary sync drawing/redrawing/resize of the pad * @param {string} kind - kind of draw operation, if true - always queued * @return {Promise} when pad is ready for draw operation or false if operation already queued * @private */ syncDraw(kind) { const entry = { kind: kind || 'redraw' }; if (this._doing_draw === undefined) { this._doing_draw = [entry]; return Promise.resolve(true); } // if queued operation registered, ignore next calls, indx === 0 is running operation if ((entry.kind !== true) && (this._doing_draw.findIndex((e, i) => (i > 0) && (e.kind === entry.kind)) > 0)) return false; this._doing_draw.push(entry); return new Promise(resolveFunc => { entry.func = resolveFunc; }); } /** @summary indicates if painter performing objects draw * @private */ doingDraw() { return this._doing_draw !== undefined; } /** @summary confirms that drawing is completed, may trigger next drawing immediately * @private */ confirmDraw() { if (this._doing_draw === undefined) return console.warn('failure, should not happen'); this._doing_draw.shift(); if (this._doing_draw.length === 0) delete this._doing_draw; else { const entry = this._doing_draw[0]; if (entry.func) { entry.func(); delete entry.func; } } } /** @summary Draw single primitive */ async drawObject(/* dom, obj, opt */) { console.log('Not possible to draw object without loading of draw.mjs'); return null; } /** @summary Draw pad primitives * @return {Promise} when drawing completed * @private */ async drawPrimitives(indx) { if (indx === undefined) { if (this.iscan) this._start_tm = new Date().getTime(); // set number of primitves 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 || ((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.getDom(), 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 subpads * @return {Promise} when finished * @private */ async divide(nx, ny) { if (!this.pad.Divide(nx, ny)) return this; const drawNext = indx => { if (indx >= this.pad.fPrimitives.arr.length) return this; return this.drawObject(this.getDom(), 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 (sub.pad && isFunc(sub.forEachPainterInPad) && (sub.pad.fNumber === n)) return sub; } return null; } /** @summary Process tooltip event in the pad * @private */ processPadTooltipEvent(pnt) { const painters = [], hints = []; // first count - how many processors are there this.painters?.forEach(obj => { if (isFunc(obj.processTooltipEvent)) painters.push(obj); }); if (pnt) pnt.nproc = painters.length; painters.forEach(obj => { const hint = obj.processTooltipEvent(pnt) || { user_info: null }; hints.push(hint); if (pnt?.painters) hint.painter = obj; }); return hints; } /** @summary Changes canvas dark mode * @private */ changeDarkMode(mode) { this.getCanvSvg().style('filter', (mode ?? settings.DarkMode) ? 'invert(100%)' : null); } /** @summary Fill pad context menu * @private */ fillContextMenu(menu) { if (this.pad) menu.add(`header:${this.pad._typename}::${this.pad.fName}`); else menu.add('header:Canvas'); menu.addchk(this.isTooltipAllowed(), 'Show tooltips', () => this.setTooltipAllowed('toggle')); if (!this._websocket) { function SetPadField(arg) { this.pad[arg.slice(1)] = parseInt(arg[0]); this.interactiveRedraw('pad', arg.slice(1)); } menu.addchk(this.pad?.fGridx, 'Grid x', (this.pad?.fGridx ? '0' : '1') + 'fGridx', SetPadField); menu.addchk(this.pad?.fGridy, 'Grid y', (this.pad?.fGridy ? '0' : '1') + 'fGridy', SetPadField); menu.add('sub:Ticks x'); menu.addchk(this.pad?.fTickx === 0, 'normal', '0fTickx', SetPadField); menu.addchk(this.pad?.fTickx === 1, 'ticks on both sides', '1fTickx', SetPadField); menu.addchk(this.pad?.fTickx === 2, 'labels on both sides', '2fTickx', SetPadField); menu.add('endsub:'); menu.add('sub:Ticks y'); menu.addchk(this.pad?.fTicky === 0, 'normal', '0fTicky', SetPadField); menu.addchk(this.pad?.fTicky === 1, 'ticks on both sides', '1fTicky', SetPadField); menu.addchk(this.pad?.fTicky === 2, 'labels on both sides', '2fTicky', SetPadField); menu.add('endsub:'); menu.addchk(this.pad?.fEditable, 'Editable', flag => { this.pad.fEditable = flag; this.interactiveRedraw('pad'); }); if (this.iscan) menu.addchk(this.pad?.TestBit(kIsGrayscale), 'Gray scale', flag => { this.setGrayscale(flag); this.interactiveRedraw('pad'); }); if (isFunc(this.drawObject)) menu.add('Build legend', () => this.buildLegend()); menu.addAttributesMenu(this); menu.add('Save to gStyle', () => { if (!this.pad) return; this.fillatt?.saveToStyle(this.iscan ? 'fCanvasColor' : 'fPadColor'); gStyle.fPadGridX = this.pad.fGridx; gStyle.fPadGridY = this.pad.fGridy; gStyle.fPadTickX = this.pad.fTickx; gStyle.fPadTickY = this.pad.fTicky; gStyle.fOptLogx = this.pad.fLogx; gStyle.fOptLogy = this.pad.fLogy; gStyle.fOptLogz = this.pad.fLogz; }, 'Store pad fill attributes, grid, tick and log scale settings to gStyle'); if (this.iscan) { menu.addSettingsMenu(false, false, arg => { if (arg === 'dark') this.changeDarkMode(); }); } } menu.add('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.add('sub:Save as'); ['svg', 'png', 'jpeg', 'pdf', 'webp'].forEach(fmt => menu.add(`${fname}.${fmt}`, () => this.saveAs(fmt, this.iscan, `${fname}.${fmt}`))); menu.add('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 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 showsubi