jsroot
Version:
JavaScript ROOT
1,374 lines (1,138 loc) • 97 kB
JavaScript
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