jsroot
Version:
JavaScript ROOT
1,388 lines (1,158 loc) • 101 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, 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))