UNPKG

jsroot

Version:
1,388 lines (1,158 loc) 101 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, isPadPainter } 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), // identifier used in TWebCanvas painter webSnapIds = { kNone: 0, kObject: 1, kSVG: 2, kSubPad: 3, kColors: 4, kStyle: 5, kFont: 6 }; // eslint-disable-next-line one-var const PadButtonsHandler = { getButtonSize(fact) { const cp = this.getCanvPainter(); return Math.round((fact || 1) * (cp?.getPadScale() || 1) * (cp === this ? 16 : 12)); }, toggleButtonsVisibility(action, evnt) { evnt?.preventDefault(); evnt?.stopPropagation(); const group = this.getLayerSvg('btns_layer'), btn = group.select('[name=\'Toggle\']'); if (btn.empty()) return; let state = btn.property('buttons_state'); if (btn.property('timout_handler')) { if (action !== 'timeout') clearTimeout(btn.property('timout_handler')); btn.property('timout_handler', null); } let is_visible = false; switch (action) { case 'enable': is_visible = true; this.btns_active_flag = true; break; case 'enterbtn': this.btns_active_flag = true; return; // do nothing, just cleanup timeout case 'timeout': break; case 'toggle': state = !state; btn.property('buttons_state', state); is_visible = state; break; case 'disable': case 'leavebtn': this.btns_active_flag = false; if (!state) btn.property('timout_handler', setTimeout(() => this.toggleButtonsVisibility('timeout'), 1200)); return; } group.selectAll('svg').each(function() { if (this !== btn.node()) 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'); 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'); if (!group.empty()) { group.selectAll('*').remove(); group.property('nextx', null); } }, showPadButtons() { const group = this.getLayerSvg('btns_layer'); if (group.empty()) return; // clean all previous buttons group.selectAll('*').remove(); if (!this._buttons) return; const istop = this.isTopPad(), y = 0; let ctrl, x = group.property('leftside') ? this.getButtonSize(1.25) : 0; if (this.isFastDrawing()) { ctrl = ToolbarIcons.createSVG(group, ToolbarIcons.circle, this.getButtonSize(), 'enlargePad', false) .attr('name', 'Enlarge').attr('x', 0).attr('y', 0) .on('click', evnt => this.clickPadButton('enlargePad', evnt)); } else { ctrl = ToolbarIcons.createSVG(group, ToolbarIcons.rect, this.getButtonSize(), 'Toggle tool buttons', false) .attr('name', 'Toggle').attr('x', 0).attr('y', 0) .property('buttons_state', (settings.ToolBar !== 'popup') || browser.touches) .on('click', evnt => this.toggleButtonsVisibility('toggle', evnt)); ctrl.node()._mouseenter = () => this.toggleButtonsVisibility('enable'); ctrl.node()._mouseleave = () => this.toggleButtonsVisibility('disable'); for (let k = 0; k < this._buttons.length; ++k) { const item = this._buttons[k]; let btn = item.btn; if (isStr(btn)) btn = ToolbarIcons[btn]; if (!btn) btn = ToolbarIcons.circle; const svg = ToolbarIcons.createSVG(group, btn, this.getButtonSize(), item.tooltip + (istop ? '' : (` on pad ${this.getPadName()}`)) + (item.keyname ? ` (keyshortcut ${item.keyname})` : ''), false); if (group.property('vertical')) svg.attr('x', y).attr('y', x); else svg.attr('x', x).attr('y', y); svg.attr('name', item.funcname) .style('display', ctrl.property('buttons_state') ? '' : 'none') .attr('key', item.keyname || null) .on('click', evnt => this.clickPadButton(item.funcname, evnt)); svg.node()._mouseenter = () => this.toggleButtonsVisibility('enterbtn'); svg.node()._mouseleave = () => this.toggleButtonsVisibility('leavebtn'); x += this.getButtonSize(1.25); } } group.property('nextx', x); this.alignButtons(group, this.getPadWidth(), this.getPadHeight()); if (group.property('vertical')) ctrl.attr('y', x); else if (!group.property('leftside')) ctrl.attr('x', x); }, assign(painter) { Object.assign(painter, this); } }; // PadButtonsHandler /** @summary Fill TWebObjectOptions for painter * @private */ function createWebObjectOptions(painter) { if (!painter?.getSnapId()) return null; const obj = { _typename: 'TWebObjectOptions', snapid: painter.getSnapId(), opt: painter.getDrawOpt(true), fcust: '', fopt: [] }; if (isFunc(painter.fillWebObjectOptions)) painter.fillWebObjectOptions(obj); return obj; } /** * @summary Painter for TPad object * @private */ class TPadPainter extends ObjectPainter { #iscan; // is canvas flag #pad_name; // name of the pad #pad; // TPad object #painters; // painters in the pad #pad_scale; // scale factor of the pad #pad_x; // pad x coordinate #pad_y; // pad y coordinate #pad_width; // pad width #pad_height; // pad height #doing_draw; // drawing handles #pad_draw_disabled; // disable drawing of the pad #last_grayscale; // grayscale change flag #custom_palette; // custom palette #custom_colors; // custom colors #custom_palette_indexes; // custom palette indexes #custom_palette_colors; // custom palette colors #frame_painter_ref; // frame painter #main_painter_ref; // main painter on the pad #snap_primitives; // stored snap primitives from web canvas #has_execs; // indicate is pad has TExec objects assigned #deliver_move_events; // deliver move events to server #readonly; // if changes on pad is not allowed #num_primitives; // number of primitives #num_specials; // number of special objects - if counted #auto_color_cnt; // counter used in assigning auto colors #auto_palette; // palette for creating of automatic colors #fixed_size; // fixed size flag #has_canvas; // indicate if top canvas painter exists #fast_drawing; // fast drawing flag #resize_tmout; // timeout handle for resize #start_draw_tm; // time when start drawing primitives /** @summary constructor * @param {object|string} dom - DOM element for drawing or element id * @param {object} pad - TPad object to draw * @param {String} [opt] - draw option * @param {boolean} [iscan] - if TCanvas object * @param [add_to_primitives] - add pad painter to canvas * */ constructor(dom, pad, opt, iscan, add_to_primitives) { super(dom, pad); this.#pad = pad; this.#iscan = iscan; // indicate if working with canvas this.#pad_name = ''; if (!iscan && pad?.fName) { this.#pad_name = pad.fName.replace(' ', '_'); // avoid empty symbol in pad name const regexp = /^[A-Za-z][A-Za-z0-9_]*$/; if (!regexp.test(this.#pad_name) || ((this.#pad_name === 'button') && (pad._typename === clTButton))) this.#pad_name = 'jsroot_pad_' + internals.id_counter++; } this.#painters = []; // complete list of all painters in the pad this.#has_canvas = true; this.forEachPainter = this.forEachPainterInPad; const d = this.selectDom(); if (!d.empty() && d.property('_batch_mode')) this.batch_mode = true; if (opt !== undefined) this.decodeOptions(opt); if (add_to_primitives) { if ((add_to_primitives !== 'webpad') && this.getCanvSvg().empty()) { // one can draw pad without canvas this.#has_canvas = false; this.#pad_name = ''; this.setTopPainter(); } else { // pad painter will be registered in the parent pad this.addToPadPrimitives(); } } if (pad?.$disable_drawing) this.#pad_draw_disabled = true; } /** @summary returns pad painter * @protected */ getPadPainter() { return this.isTopPad() ? null : super.getPadPainter(); } /** @summary returns canvas painter * @protected */ getCanvPainter(try_select) { return this.isTopPad() ? this : super.getCanvPainter(try_select); } /** @summary Returns pad name * @protected */ getPadName() { return this.#pad_name; } /** @summary Indicates that drawing runs in batch mode * @private */ isBatchMode() { if (this.batch_mode !== undefined) return this.batch_mode; if (isBatchMode()) return true; return this.isTopPad() ? false : this.getCanvPainter()?.isBatchMode(); } /** @summary Indicates that is is Root6 pad painter * @private */ isRoot6() { return true; } /** @summary Returns true if pad is editable */ isEditable() { return this.#pad?.fEditable ?? true; } /** @summary Returns true if button */ isButton() { return this.matchObjectType(clTButton); } /** @summary Returns true if read-only mode is enabled */ isReadonly() { return this.#readonly; } /** @summary Returns true if it is canvas * @param {Boolean} [is_online = false] - if specified, checked if it is canvas with configured connection to server */ isCanvas(is_online = false) { if (!this.#iscan) return false; if (is_online === true) return isFunc(this.getWebsocket) && this.getWebsocket(); return isStr(is_online) ? this.#iscan === is_online : true; } /** @summary Returns true if it is canvas or top pad without canvas */ isTopPad() { return this.isCanvas() || !this.#has_canvas; } /** @summary Canvas main svg element * @return {object} d3 selection with canvas svg * @protected */ getCanvSvg() { return this.selectDom().select('.root_canvas'); } /** @summary Pad svg element * @return {object} d3 selection with pad svg * @protected */ getPadSvg() { const c = this.getCanvSvg(); if (!this.#pad_name || c.empty()) return c; return c.select('.primitives_layer .__root_pad_' + this.#pad_name); } /** @summary Method selects immediate layer under canvas/pad main element * @param {string} name - layer name lik 'primitives_layer', 'btns_layer', 'info_layer' * @protected */ getLayerSvg(name) { return this.getPadSvg().selectChild('.' + name); } /** @summary Returns svg element for the frame in current pad * @protected */ getFrameSvg() { const layer = this.getLayerSvg('primitives_layer'); if (layer.empty()) return layer; let node = layer.node().firstChild; while (node) { const elem = d3_select(node); if (elem.classed('root_frame')) return elem; node = node.nextSibling; } return d3_select(null); } /** @summary Returns main painter on the pad * @desc Typically main painter is TH1/TH2 object which is drawing axes * @private */ getMainPainter() { return this.#main_painter_ref || null; } /** @summary Assign main painter on the pad * @desc Typically main painter is TH1/TH2 object which is drawing axes * @private */ setMainPainter(painter, force) { if (!this.#main_painter_ref || force) this.#main_painter_ref = painter; } /** @summary cleanup pad and all primitives inside */ cleanup() { if (this.#doing_draw) console.error('pad drawing is not completed when cleanup is called'); this.#painters.forEach(p => p.cleanup()); const svg_p = this.getPadSvg(); if (!svg_p.empty()) { svg_p.property('pad_painter', null); if (!this.isCanvas()) svg_p.remove(); } this.#main_painter_ref = undefined; this.#frame_painter_ref = undefined; this.#pad_x = this.#pad_y = this.#pad_width = this.#pad_height = undefined; this.#doing_draw = undefined; this.#snap_primitives = undefined; this.#last_grayscale = undefined; this.#custom_palette = this.#custom_colors = this.#custom_palette_indexes = this.#custom_palette_colors = undefined; this.#painters = []; this.#pad = undefined; this.#pad_name = undefined; this.#has_canvas = false; selectActivePad({ pp: this, active: false }); super.cleanup(); } /** @summary Returns frame painter inside the pad * @private */ getFramePainter() { return this.#frame_painter_ref; } /** @summary Assign actual frame painter * @private */ setFramePainter(fp, on) { if (on) this.#frame_painter_ref = fp; else if (this.#frame_painter_ref === fp) this.#frame_painter_ref = undefined; } /** @summary get pad width */ getPadWidth() { return this.#pad_width || 0; } /** @summary get pad height */ getPadHeight() { return this.#pad_height || 0; } /** @summary get pad height */ getPadScale() { return this.#pad_scale || 1; } /** @summary get pad rect */ getPadRect() { return { x: this.#pad_x || 0, y: this.#pad_y || 0, width: this.getPadWidth(), height: this.getPadHeight() }; } /** @summary return pad log state x or y are allowed */ getPadLog(name) { const pad = this.getRootPad(); if (name === 'x') return pad?.fLogx; if (name === 'y') return pad?.fLogv ?? pad?.fLogy; return false; } /** @summary Returns frame coordinates - also when frame is not drawn */ getFrameRect() { const fp = this.getFramePainter(); if (fp) return fp.getFrameRect(); const w = this.getPadWidth(), h = this.getPadHeight(), rect = {}; if (this.#pad) { rect.szx = Math.round(Math.max(0, 0.5 - Math.max(this.#pad.fLeftMargin, this.#pad.fRightMargin)) * w); rect.szy = Math.round(Math.max(0, 0.5 - Math.max(this.#pad.fBottomMargin, this.#pad.fTopMargin)) * h); } else { rect.szx = Math.round(0.5 * w); rect.szy = Math.round(0.5 * h); } rect.width = 2 * rect.szx; rect.height = 2 * rect.szy; rect.x = Math.round(w / 2 - rect.szx); rect.y = Math.round(h / 2 - rect.szy); rect.hint_delta_x = rect.szx; rect.hint_delta_y = rect.szy; rect.transform = makeTranslate(rect.x, rect.y) || ''; return rect; } /** @summary return RPad object */ getRootPad(is_root6) { return (is_root6 === undefined) || is_root6 ? this.#pad : null; } /** @summary Cleanup primitives from pad - selector lets define which painters to remove * @return true if any painter was removed */ cleanPrimitives(selector) { // remove all primitives if (selector === true) selector = () => true; if (!isFunc(selector)) return false; let is_any = false; for (let k = this.#painters.length - 1; k >= 0; --k) { const subp = this.#painters[k]; if (!subp || selector(subp)) { subp?.cleanup(); this.#painters.splice(k, 1); is_any = true; } } return is_any; } /** @summary Removes and cleanup specified primitive * @desc also secondary primitives will be removed * @return new index to continue loop or -111 if main painter removed * @private */ removePrimitive(arg, clean_only_secondary) { let indx, prim; if (Number.isInteger(arg)) { indx = arg; prim = this.#painters[indx]; } else { indx = this.#painters.indexOf(arg); prim = arg; } if (indx < 0) return indx; const arr = [], get_main = clean_only_secondary ? this.getMainPainter() : null; let resindx = indx - 1; // object removed itself arr.push(prim); this.#painters.splice(indx, 1); // loop to extract all dependent painters let len0 = 0; while (len0 < arr.length) { for (let k = this.#painters.length - 1; k >= 0; --k) { if (this.#painters[k].isSecondary(arr[len0])) { arr.push(this.#painters[k]); this.#painters.splice(k, 1); if (k < indx) resindx--; } } len0++; } arr.forEach(painter => { if ((painter !== prim) || !clean_only_secondary) painter.cleanup(); if (this.getMainPainter() === painter) { this.setMainPainter(undefined, true); resindx = -111; } }); // when main painter disappears because of special cleanup - also reset zooming if (clean_only_secondary && get_main && !this.getMainPainter()) this.getFramePainter()?.resetZoom(); return resindx; } /** @summary returns custom palette associated with pad or top canvas * @private */ getCustomPalette(no_recursion) { return this.#custom_palette || (no_recursion ? null : this.getCanvPainter()?.getCustomPalette(true)); } _getCustomPaletteIndexes() { return this.#custom_palette_indexes; } /** @summary Provides automatic color * @desc Uses ROOT colors palette if possible * @private */ getAutoColor(numprimitives) { numprimitives = Math.max(numprimitives || (this.#num_primitives || 5) - (this.#num_specials || 0), 2); let indx = this.#auto_color_cnt ?? 0; this.#auto_color_cnt = (indx + 1) % numprimitives; if (indx >= numprimitives) indx = numprimitives - 1; let indexes = this._getCustomPaletteIndexes(); if (!indexes) { const cp = this.getCanvPainter(); if ((cp !== this) && isFunc(cp?._getCustomPaletteIndexes)) indexes = cp._getCustomPaletteIndexes(); } if (indexes?.length) { const p = Math.round(indx * (indexes.length - 3) / (numprimitives - 1)); return indexes[p]; } if (!this.#auto_palette) this.#auto_palette = getColorPalette(settings.Palette, this.isGrayscale()); const palindx = Math.round(indx * (this.#auto_palette.getLength() - 3) / (numprimitives - 1)), colvalue = this.#auto_palette.getColor(palindx); return this.addColor(colvalue); } /** @summary Returns number of painters * @protected */ getNumPainters() { return this.#painters.length; } /** @summary Add painter to pad list of painters * @protected */ addToPrimitives(painter) { if (this.#painters.indexOf(painter) < 0) this.#painters.push(painter); return this; } /** @summary Call function for each painter in pad * @param {function} userfunc - function to call * @param {string} kind - 'all' for all objects (default), 'pads' only pads and sub-pads, 'objects' only for object in current pad * @private */ forEachPainterInPad(userfunc, kind) { if (!kind) kind = 'all'; if (kind !== 'objects') userfunc(this); for (let k = 0; k < this.#painters.length; ++k) { const sub = this.#painters[k]; if (isFunc(sub.forEachPainterInPad)) { if (kind !== 'objects') sub.forEachPainterInPad(userfunc, kind); } else if (kind !== 'pads') userfunc(sub); } } /** @summary register for pad events receiver * @desc in pad painter, while pad may be drawn without canvas */ registerForPadEvents(receiver) { this.pad_events_receiver = receiver; } /** @summary Generate pad events, normally handled by GED * @desc in pad painter, while pad may be drawn without canvas * @private */ producePadEvent(what, padpainter, painter, position) { if ((what === 'select') && isFunc(this.selectActivePad)) this.selectActivePad(padpainter, painter, position); if (isFunc(this.pad_events_receiver)) this.pad_events_receiver({ what, padpainter, painter, position }); } /** @summary method redirect call to pad events receiver */ selectObjectPainter(painter, pos) { const canp = this.isTopPad() ? this : this.getCanvPainter(); if (painter === undefined) painter = this; if (pos && !this.isTopPad()) pos = getAbsPosInCanvas(this.getPadSvg(), pos); selectActivePad({ pp: this, active: true }); canp?.producePadEvent('select', this, painter, pos); } /** @summary Draw pad active border * @private */ drawActiveBorder(svg_rect, is_active) { if (is_active !== undefined) { if (this.is_active_pad === is_active) return; this.is_active_pad = is_active; } if (this.is_active_pad === undefined) return; if (!svg_rect) svg_rect = this.isCanvas() ? this.getCanvSvg().selectChild('.canvas_fillrect') : this.getPadSvg().selectChild('.root_pad_border'); const cp = this.getCanvPainter(); let lineatt = this.is_active_pad && cp?.highlight_gpad ? new TAttLineHandler({ style: 1, width: 1, color: 'red' }) : this.lineatt; if (!lineatt) lineatt = new TAttLineHandler({ color: 'none' }); svg_rect.call(lineatt.func); } /** @summary Set fast drawing property depending on the size * @private */ setFastDrawing(w, h) { const was_fast = this.#fast_drawing; this.#fast_drawing = !this.hasSnapId() && settings.SmallPad && ((w < settings.SmallPad.width) || (h < settings.SmallPad.height)); if (was_fast !== this.#fast_drawing) this.showPadButtons(); } /** @summary Return fast drawing flag * @private */ isFastDrawing() { return this.#fast_drawing; } /** @summary Returns true if canvas configured with grayscale * @private */ isGrayscale() { if (!this.isCanvas()) return false; return this.#pad?.TestBit(kIsGrayscale) ?? false; } /** @summary Returns true if default pad range is configured * @private */ isDefaultPadRange() { if (!this.#pad) return true; return (this.#pad.fX1 === 0) && (this.#pad.fX2 === 1) && (this.#pad.fY1 === 0) && (this.#pad.fY2 === 1); } /** @summary Set grayscale mode for the canvas * @private */ setGrayscale(flag) { if (!this.isTopPad()) return; let changed = false; if (flag === undefined) { flag = this.#pad?.TestBit(kIsGrayscale) ?? false; changed = (this.#last_grayscale !== undefined) && (this.#last_grayscale !== flag); } else if (flag !== this.#pad?.TestBit(kIsGrayscale)) { this.#pad?.InvertBit(kIsGrayscale); changed = true; } if (changed) { this.forEachPainter(p => { if (isFunc(p.clearHistPalette)) p.clearHistPalette(); }); } this.setColors(flag ? getGrayColors(this.#custom_colors) : this.#custom_colors); this.#last_grayscale = flag; this.#custom_palette = this.#custom_palette_colors ? new ColorPalette(this.#custom_palette_colors, flag) : null; } /** @summary Set fixed-size canvas * @private */ _setFixedSize(on) { this.#fixed_size = on; } /** @summary Create SVG element for canvas */ createCanvasSvg(check_resize, new_size) { const is_batch = this.isBatchMode(), lmt = 5; let factor, svg, rect, btns, info, frect; if (check_resize > 0) { if (this.#fixed_size) return check_resize > 1; // flag used to force re-drawing of all sub-pads svg = this.getCanvSvg(); if (svg.empty()) return false; factor = svg.property('height_factor'); rect = this.testMainResize(check_resize, null, factor); if (!rect.changed && (check_resize === 1)) return false; if (!is_batch) btns = this.getLayerSvg('btns_layer'); info = this.getLayerSvg('info_layer'); frect = svg.selectChild('.canvas_fillrect'); } else { const render_to = this.selectDom(); if (render_to.style('position') === 'static') render_to.style('position', 'relative'); svg = render_to.append('svg') .attr('class', 'jsroot root_canvas') .property('pad_painter', this) // this is custom property .property('redraw_by_resize', false); // could be enabled to force redraw by each resize this.setTopPainter(); // assign canvas as top painter of that element if (is_batch) svg.attr('xmlns', nsSVG); else if (!this.online_canvas) svg.append('svg:title').text('ROOT canvas'); if (!is_batch) svg.style('user-select', settings.UserSelect || null); if (!is_batch || (this.#pad.fFillStyle > 0)) frect = svg.append('svg:path').attr('class', 'canvas_fillrect'); if (!is_batch) { frect.style('pointer-events', 'visibleFill') .on('dblclick', evnt => this.enlargePad(evnt, true)) .on('click', () => this.selectObjectPainter()) .on('mouseenter', () => this.showObjectStatus()) .on('contextmenu', settings.ContextMenu ? evnt => this.padContextMenu(evnt) : null); } svg.append('svg:g').attr('class', 'primitives_layer'); info = svg.append('svg:g').attr('class', 'info_layer'); if (!is_batch) { btns = svg.append('svg:g') .attr('class', 'btns_layer') .property('leftside', settings.ToolBarSide === 'left') .property('vertical', settings.ToolBarVert); } factor = 0.66; if (this.#pad?.fCw && this.#pad?.fCh && (this.#pad?.fCw > 0)) { factor = this.#pad.fCh / this.#pad.fCw; if ((factor < 0.1) || (factor > 10)) factor = 0.66; } if (this.#fixed_size) { render_to.style('overflow', 'auto'); rect = { width: this.#pad.fCw, height: this.#pad.fCh }; if (!rect.width || !rect.height) rect = getElementRect(render_to); } else rect = this.testMainResize(2, new_size, factor); } this.setGrayscale(); this.createAttFill({ attr: this.#pad }); if ((rect.width <= lmt) || (rect.height <= lmt)) { if (!this.hasSnapId()) { svg.style('display', 'none'); console.warn(`Hide canvas while geometry too small w=${rect.width} h=${rect.height}`); } if (this.#pad_width && this.#pad_height) { // use last valid dimensions rect.width = this.#pad_width; rect.height = this.#pad_height; } else { // just to complete drawing. rect.width = 800; rect.height = 600; } } else svg.style('display', null); svg.attr('x', 0).attr('y', 0).style('position', 'absolute'); if (this.#fixed_size) svg.attr('width', rect.width).attr('height', rect.height); else svg.style('width', '100%').style('height', '100%').style('left', 0).style('top', 0).style('bottom', 0).style('right', 0); svg.style('filter', settings.DarkMode || this.#pad?.$dark ? 'invert(100%)' : null); this.#pad_scale = settings.CanvasScale || 1; this.#pad_x = 0; this.#pad_y = 0; this.#pad_width = rect.width * this.#pad_scale; this.#pad_height = rect.height * this.#pad_scale; svg.attr('viewBox', `0 0 ${this.#pad_width} ${this.#pad_height}`) .attr('preserveAspectRatio', 'none') // we do not preserve relative ratio .property('height_factor', factor) .property('draw_x', this.#pad_x) .property('draw_y', this.#pad_y) .property('draw_width', this.#pad_width) .property('draw_height', this.#pad_height); this.addPadBorder(svg, frect); this.setFastDrawing(this.#pad_width * (1 - this.#pad.fLeftMargin - this.#pad.fRightMargin), this.#pad_height * (1 - this.#pad.fBottomMargin - this.#pad.fTopMargin)); if (this.alignButtons && btns) this.alignButtons(btns, this.#pad_width, this.#pad_height); let dt = info.selectChild('.canvas_date'); if (!gStyle.fOptDate) dt.remove(); else { if (dt.empty()) dt = info.append('text').attr('class', 'canvas_date'); const posy = Math.round(this.#pad_height * (1 - gStyle.fDateY)), date = new Date(); let posx = Math.round(this.#pad_width * gStyle.fDateX); if (!is_batch && (posx < 25)) posx = 25; if (gStyle.fOptDate > 3) date.setTime(gStyle.fOptDate * 1000); makeTranslate(dt, posx, posy) .style('text-anchor', 'start') .text(convertDate(date)); } const iname = this.getItemName(); if (iname) this.drawItemNameOnCanvas(iname); else if (!gStyle.fOptFile) info.selectChild('.canvas_item').remove(); return true; } /** @summary Draw item name on canvas if gStyle.fOptFile is configured * @private */ drawItemNameOnCanvas(item_name) { const info = this.getLayerSvg('info_layer'); let df = info.selectChild('.canvas_item'); const fitem = getHPainter().findRootFileForItem(item_name), fname = (gStyle.fOptFile === 3) ? item_name : ((gStyle.fOptFile === 2) ? fitem?._fullurl : fitem?._name); if (!gStyle.fOptFile || !fname) df.remove(); else { if (df.empty()) df = info.append('text').attr('class', 'canvas_item'); const rect = this.getPadRect(); makeTranslate(df, Math.round(rect.width * (1 - gStyle.fDateX)), Math.round(rect.height * (1 - gStyle.fDateY))) .style('text-anchor', 'end') .text(fname); } if (((gStyle.fOptDate === 2) || (gStyle.fOptDate === 3)) && fitem?._file) { info.selectChild('.canvas_date') .text(convertDate(getTDatime(gStyle.fOptDate === 2 ? fitem._file.fDatimeC : fitem._file.fDatimeM))); } } /** @summary Return true if this pad enlarged */ isPadEnlarged() { if (this.isTopPad()) return this.enlargeMain('state') === 'on'; return this.getCanvSvg().property('pad_enlarged') === this.#pad; } /** @summary Enlarge pad draw element when possible */ enlargePad(evnt, is_dblclick, is_escape) { evnt?.preventDefault(); evnt?.stopPropagation(); // ignore double click on online canvas itself for enlarge if (is_dblclick && this.isCanvas(true) && (this.enlargeMain('state') === 'off')) return; const svg_can = this.getCanvSvg(), pad_enlarged = svg_can.property('pad_enlarged'); if (this.isTopPad() || (!pad_enlarged && !this.hasObjectsToDraw() && !this.#painters)) { if (this.#fixed_size) return; // canvas cannot be enlarged in such mode if (!this.enlargeMain(is_escape ? false : 'toggle')) return; if (this.enlargeMain('state') === 'off') svg_can.property('pad_enlarged', null); else selectActivePad({ pp: this, active: true }); } else if (!pad_enlarged && !is_escape) { this.enlargeMain(true, true); svg_can.property('pad_enlarged', this.#pad); selectActivePad({ pp: this, active: true }); } else if (pad_enlarged === this.#pad) { this.enlargeMain(false); svg_can.property('pad_enlarged', null); } else if (!is_escape && is_dblclick) console.error('missmatch with pad double click events'); return this.checkResize(true); } /** @summary Create main SVG element for pad * @return true when pad is displayed and all its items should be redrawn */ createPadSvg(only_resize) { if (this.isTopPad()) { this.createCanvasSvg(only_resize ? 2 : 0); return true; } const svg_can = this.getCanvSvg(), width = svg_can.property('draw_width'), height = svg_can.property('draw_height'), pad_enlarged = svg_can.property('pad_enlarged'), pad_visible = !this.#pad_draw_disabled && (!pad_enlarged || (pad_enlarged === this.#pad)), is_batch = this.isBatchMode(); let w = Math.round(this.#pad.fAbsWNDC * width), h = Math.round(this.#pad.fAbsHNDC * height), x = Math.round(this.#pad.fAbsXlowNDC * width), y = Math.round(height * (1 - this.#pad.fAbsYlowNDC)) - h, svg_pad, svg_border, btns; if (pad_enlarged === this.#pad) { w = width; h = height; x = y = 0; } if (only_resize) { svg_pad = this.getPadSvg(); svg_border = svg_pad.selectChild('.root_pad_border'); if (!is_batch) btns = this.getLayerSvg('btns_layer'); this.addPadInteractive(true); } else { svg_pad = svg_can.selectChild('.primitives_layer') .append('svg:svg') // svg used to blend all drawings outside .classed('__root_pad_' + this.#pad_name, true) .attr('pad', this.#pad_name) // set extra attribute to mark pad name .property('pad_painter', this); // this is custom property if (!is_batch) svg_pad.append('svg:title').text('subpad ' + this.#pad_name); // need to check attributes directly while attributes objects will be created later if (!is_batch || (this.#pad.fFillStyle > 0) || ((this.#pad.fLineStyle > 0) && (this.#pad.fLineColor > 0))) svg_border = svg_pad.append('svg:path').attr('class', 'root_pad_border'); if (!is_batch) { svg_border.style('pointer-events', 'visibleFill') // get events also for not visible rect .on('dblclick', evnt => this.enlargePad(evnt, true)) .on('click', () => this.selectObjectPainter()) .on('mouseenter', () => this.showObjectStatus()) .on('contextmenu', settings.ContextMenu ? evnt => this.padContextMenu(evnt) : null); } svg_pad.append('svg:g').attr('class', 'primitives_layer'); if (!is_batch) { btns = svg_pad.append('svg:g') .attr('class', 'btns_layer') .property('leftside', settings.ToolBarSide !== 'left') .property('vertical', settings.ToolBarVert); } } this.createAttFill({ attr: this.#pad }); this.createAttLine({ attr: this.#pad, color0: !this.#pad.fBorderMode ? 'none' : '' }); svg_pad.style('display', pad_visible ? null : 'none') .attr('viewBox', `0 0 ${w} ${h}`) // due to svg .attr('preserveAspectRatio', 'none') // due to svg, we do not preserve relative ratio .attr('x', x) // due to svg .attr('y', y) // due to svg .attr('width', w) // due to svg .attr('height', h) // due to svg .property('draw_x', x) // this is to make similar with canvas .property('draw_y', y) .property('draw_width', w) .property('draw_height', h); this.#pad_scale = this.getCanvPainter().getPadScale(); this.#pad_x = x; this.#pad_y = y; this.#pad_width = w; this.#pad_height = h; this.addPadBorder(svg_pad, svg_border, true); this.setFastDrawing(w * (1 - this.#pad.fLeftMargin - this.#pad.fRightMargin), h * (1 - this.#pad.fBottomMargin - this.#pad.fTopMargin)); // special case of 3D canvas overlay if (svg_pad.property('can3d') === constants.Embed3D.Overlay) { this.selectDom().select('.draw3d_' + this.#pad_name) .style('display', pad_visible ? '' : 'none'); } if (this.alignButtons && btns) this.alignButtons(btns, this.#pad_width, this.#pad_height); return pad_visible; } /** @summary Add border decorations * @private */ addPadBorder(svg_pad, svg_border, draw_line) { if (!svg_border) return; svg_border.attr('d', `M0,0H${this.#pad_width}V${this.#pad_height}H0Z`) .call(this.fillatt.func); if (draw_line) svg_border.call(this.lineatt.func); this.drawActiveBorder(svg_border); let svg_border1 = svg_pad.selectChild('.root_pad_border1'), svg_border2 = svg_pad.selectChild('.root_pad_border2'); if (this.#pad.fBorderMode && this.#pad.fBorderSize) { const arr = getBoxDecorations(0, 0, this.#pad_width, this.#pad_height, this.#pad.fBorderMode, this.#pad.fBorderSize, this.#pad.fBorderSize); if (svg_border2.empty()) svg_border2 = svg_pad.insert('svg:path', '.primitives_layer').attr('class', 'root_pad_border2'); if (svg_border1.empty()) svg_border1 = svg_pad.insert('svg:path', '.primitives_layer').attr('class', 'root_pad_border1'); svg_border1.attr('d', arr[0]) .call(this.fillatt.func) .style('fill', 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.isCanvas() || !this.isEditable()) return; const svg_can = this.getCanvSvg(), width = svg_can.property('draw_width'), height = svg_can.property('draw_height'); addDragHandler(this, { cleanup, // do cleanup to let assign new handlers later on x: this.#pad_x, y: this.#pad_y, width: this.#pad_width, height: this.#pad_height, no_transform: true, only_resize: true, is_disabled: kind => svg_can.property('pad_enlarged') || this.btns_active_flag || (kind === 'move' && (this.options._disable_dragging || this.getFramePainter()?.mode3d)), getDrawG: () => this.getPadSvg(), pad_rect: { width, height }, minwidth: 20, minheight: 20, move_resize: (_x, _y, _w, _h) => { const x0 = this.#pad.fAbsXlowNDC, y0 = this.#pad.fAbsYlowNDC, scale_w = _w / width / this.#pad.fAbsWNDC, scale_h = _h / height / this.#pad.fAbsHNDC, shift_x = _x / width - x0, shift_y = 1 - (_y + _h) / height - y0; this.forEachPainterInPad(p => { const subpad = p.getRootPad(); subpad.fAbsXlowNDC += (subpad.fAbsXlowNDC - x0) * (scale_w - 1) + shift_x; subpad.fAbsYlowNDC += (subpad.fAbsYlowNDC - y0) * (scale_h - 1) + shift_y; subpad.fAbsWNDC *= scale_w; subpad.fAbsHNDC *= scale_h; }, 'pads'); }, redraw: () => this.interactiveRedraw('pad', 'padpos') }); } /** @summary Disable pad drawing * @desc Complete SVG element will be hidden */ disablePadDrawing() { if (!this.#pad_draw_disabled && !this.isTopPad()) { this.#pad_draw_disabled = true; this.createPadSvg(true); } } /** @summary Check if it is special object, which should be handled separately * @desc It can be TStyle or list of colors or palette object * @return {boolean} true if any */ checkSpecial(obj) { if (!obj) return false; if (obj._typename === clTStyle) { Object.assign(gStyle, obj); return true; } const o = this.getOptions(true); if ((obj._typename === clTObjArray) && (obj.name === 'ListOfColors')) { if (o?.CreatePalette) { let arr = []; for (let n = obj.arr.length - o.CreatePalette; n < obj.arr.length; ++n) { const col = getRGBfromTColor(obj.arr[n]); if (!col) { console.log('Fail to create color for palette'); arr = null; break; } arr.push(col); } if (arr.length) this.#custom_palette = new ColorPalette(arr); } if (!o || o.GlobalColors) // set global list of colors adoptRootColors(obj); // copy existing colors and extend with new values this.#custom_colors = o?.LocalColors ? extendRootColors(null, obj) : null; return true; } if ((obj._typename === clTObjArray) && (obj.name === 'CurrentColorPalette')) { const arr = [], indx = []; let missing = false; for (let n = 0; n < obj.arr.length; ++n) { const col = obj.arr[n]; if (col?._typename === clTColor) { indx[n] = col.fNumber; arr[n] = getRGBfromTColor(col); } else { console.log(`Missing color with index ${n}`); missing = true; } } const apply = (!o || (!missing && !o.IgnorePalette)); this.#custom_palette_indexes = apply ? indx : null; this.#custom_palette_colors = apply ? arr : null; return true; } return false; } /** @summary Check if special objects appears in primitives * @desc it could be list of colors or palette */ checkSpecialsInPrimitives(can, count_specials) { const lst = can?.fPrimitives; if (count_specials) this.#num_specials = 0; if (!lst) return; for (let i = 0; i < lst.arr?.length; ++i) { if (this.checkSpecial(lst.arr[i])) { lst.arr[i].$special = true; // mark object as special one, do not use in drawing if (count_specials) this.#num_specials++; } } } /** @summary try to find object by name in list of pad primitives * @desc used to find title drawing * @private */ findInPrimitives(objname, objtype) { const match = obj => obj && (obj?.fName === objname) && (objtype ? (obj?._typename === objtype) : true), snap = this.#snap_primitives?.find(s => match((s.fKind === webSnapIds.kObject) ? s.fSnapshot : null)); return snap ? snap.fSnapshot : this.#pad?.fPrimitives?.arr.find(match); } /** @summary Try to find painter for specified object * @desc can be used to find painter for some special objects, registered as * histogram functions * @param {object} selobj - object to which painter should be search, set null to ignore parameter * @param {string} [selname] - object name, set to null to ignore * @param {string} [seltype] - object type, set to null to ignore * @return {object} - painter for specified object (if any) * @private */ findPainterFor(selobj, selname, seltype) { return this.#painters.find(p => { const pobj = p.getObject(); if (!pobj) return false; if (selobj && (pobj === selobj)) return true; if (!selname && !seltype) return false; if (selname && (pobj.fName !== selname)) return false; if (seltype && (pobj._typename !== seltype)) return false; return true; }); } /** @summary Return true if any objects beside sub-pads exists in the pad */ hasObjectsToDraw() { return this.#pad?.fPrimitives?.arr?.find(obj => obj._typename !== clTPad); } /** @summary sync drawing/redrawing/resize of the pad * @param {string} kind - kind of draw operation, if true - always queued * @return {Promise} when pad is ready for draw operation or false if operation already queued * @private */ syncDraw(kind) { const entry = { kind: kind || 'redraw' }; if (this.#doing_draw === undefined) { this.#doing_draw = [entry]; return Promise.resolve(true); } // if queued operation registered, ignore next calls, indx === 0 is running operation if ((entry.kind !== true) && (this.#doing_draw.findIndex((e, i) => (i > 0) && (e.kind === entry.kind)) > 0)) return false; this.#doing_draw.push(entry); return new Promise(resolveFunc => { entry.func = resolveFunc; }); } /** @summary indicates if painter performing objects draw * @private */ doingDraw() { return this.#doing_draw !== undefined; } /** @summary confirms that drawing is completed, may trigger next drawing immediately * @private */ confirmDraw() { if (this.#doing_draw === undefined) return console.warn('failure, should not happen'); this.#doing_draw.shift(); if (!this.#doing_draw.length) this.#doing_draw = undefined; else { const entry = this.#doing_draw[0]; if (entry.func) { entry.func(); delete entry.func; } } } /** @summary Draw single primitive */ async drawObject(/* dom, obj, opt */) { console.log('Not possible to draw object without loading of draw.mjs'); return null; } /** @summary Draw pad primitives * @return {Promise} when drawing completed * @private */ async drawPrimitives(indx) { if (indx === undefined) { if (this.isCanvas()) this.#start_draw_tm = new Date().getTime(); // set number of primitives this.#num_primitives = this.#pad?.fPrimitives?.arr?.length || 0; // sync to prevent immediate pad redraw during normal drawing sequence return this.syncDraw(true).then(() => this.drawPrimitives(0)); } if (!this.#pad || (indx >= this.#num_primitives)) { if (this.#start_draw_tm) { const spenttm = new Date().getTime() - this.#start_draw_tm; if (spenttm > 1000) console.log(`Canvas ${this.#pad?.fName || '---'} drawing took ${(spenttm * 1e-3).toFixed(2)}s`); this.#start_draw_tm = undefined; } this.confirmDraw(); return; } const obj = this.#pad.fPrimitives.arr[indx]; if (!obj || obj.$special || ((indx > 0) && (obj._typename === clTFrame) && this.getFramePainter())) return this.drawPrimitives(indx + 1); // use of Promise should avoid large call-stack depth when many primitives are drawn return this.drawObject(this, obj, this.#pad.fPrimitives.opt[indx]).then(op => { if (isObject(op)) op._primitive = true; // mark painter as belonging to primitives return this.drawPrimitives(indx + 1); }); } /** @summary Divide pad on sub-pads * @return {Promise} when finished * @private */ async divide(nx, ny, use_existing) { let color = this.#pad.fFillColor; if (!use_existing) { if (color < 15) color = 19; else if (color < 20) color--; } if (nx && !ny && use_existing) { for (let k = 0; k < nx; ++k) { if (!this.getSubPadPainter(k + 1))