import { select as d3_select, pointer as d3_pointer } from '../d3.mjs';
import { settings, constants, internals, isNodeJs, isBatchMode, getPromise, BIT,
prROOT, clTObjString, clTAxis, isObject, isFunc, isStr, getDocument, urlClassPrefix } from '../core.mjs';
import { isPlainText, producePlainText, produceLatex, produceMathjax, typesetMathjax, approximateLabelWidth } from './latex.mjs';
import { getElementRect, BasePainter, makeTranslate } from './BasePainter.mjs';
import { TAttMarkerHandler } from './TAttMarkerHandler.mjs';
import { TAttFillHandler } from './TAttFillHandler.mjs';
import { TAttLineHandler } from './TAttLineHandler.mjs';
import { TAttTextHandler } from './TAttTextHandler.mjs';
import { FontHandler } from './FontHandler.mjs';
import { getRootColors } from './colors.mjs';
/**
* @summary Painter class for ROOT objects
*
*/
class ObjectPainter extends BasePainter {
/** @summary constructor
* @param {object|string} dom - dom element or identifier or pad painter
* @param {object} obj - object to draw
* @param {string} [opt] - object draw options */
constructor(dom, obj, opt) {
let pp = null;
if (isFunc(dom?.forEachPainterInPad) && (dom?.this_pad_name !== undefined)) {
pp = dom;
dom = pp.getDom();
}
super(dom);
// this.draw_g = undefined; // container for all drawn objects
// this._main_painter = undefined; // main painter in the correspondent pad
this.pad_name = pp?.this_pad_name ?? ''; // name of pad where object is drawn
this.assignObject(obj);
if (isStr(opt))
this.options = { original: opt };
}
/** @summary Assign object to the painter
* @protected */
assignObject(obj) {
if (isObject(obj))
this.draw_object = obj;
else
delete this.draw_object;
}
/** @summary Assigns pad name where element will be drawn
* @desc Should happened before first draw of element is performed, only for special use case
* @param {string} [pad_name] - on which sub-pad element should be draw, if not specified - use current
* @protected
* @deprecated to be removed in v8 */
setPadName(pad_name) {
// console.warn('setPadName is deprecated, to be removed in v8');
this.pad_name = isStr(pad_name) ? pad_name : '';
}
/** @summary Returns pad name where object is drawn */
getPadName() { return this.pad_name || ''; }
/** @summary Indicates that drawing runs in batch mode
* @private */
isBatchMode() { return isBatchMode() ? true : (this.getCanvPainter()?.isBatchMode() ?? false); }
/** @summary Assign snapid to the painter
* @desc Identifier used to communicate with server side and identifies object on the server
* @private */
assignSnapId(id) { this.snapid = id; }
/** @summary Generic method to cleanup painter.
* @desc Remove object drawing and (in case of main painter) also main HTML components
* @protected */
cleanup() {
this.removeG();
let keep_origin = true;
if (this.isMainPainter()) {
const pp = this.getPadPainter();
if (!pp || (pp.normal_canvas === false))
keep_origin = false;
}
// cleanup all existing references
delete this.pad_name;
delete this._main_painter;
this.draw_object = null;
delete this.snapid;
// remove attributes objects (if any)
delete this.fillatt;
delete this.lineatt;
delete this.markeratt;
delete this.bins;
delete this.root_colors;
delete this.options;
delete this.options_store;
// remove extra fields from v7 painters
delete this.rstyle;
delete this.csstype;
super.cleanup(keep_origin);
}
/** @summary Returns drawn object */
getObject() { return this.draw_object; }
/** @summary Returns drawn object name */
getObjectName() { return this.getObject()?.fName ?? ''; }
/** @summary Returns drawn object class name */
getClassName() { return this.getObject()?._typename ?? ''; }
/** @summary Checks if drawn object matches with provided typename
* @param {string|object} arg - typename (or object with _typename member)
* @protected */
matchObjectType(arg) {
const clname = this.getClassName();
if (!arg || !clname)
return false;
if (isStr(arg))
return arg === clname;
if (isStr(arg._typename))
return arg._typename === clname;
return !!clname.match(arg);
}
/** @summary Change item name
* @desc When available, used for svg:title property
* @private */
setItemName(name, opt, hpainter) {
super.setItemName(name, opt, hpainter);
if (this.no_default_title || !name) return;
const can = this.getCanvSvg();
if (!can.empty()) can.select('title').text(name);
else this.selectDom().attr('title', name);
const cp = this.getCanvPainter();
if (cp && ((cp === this) || (this.isMainPainter() && (cp === this.getPadPainter()))))
cp.drawItemNameOnCanvas(name);
}
/** @summary Store actual this.options together with original string
* @private */
storeDrawOpt(original) {
if (!this.options) return;
if (!original) original = '';
const pp = original.indexOf(';;');
if (pp >= 0) original = original.slice(0, pp);
this.options.original = original;
this.options_store = Object.assign({}, this.options);
}
/** @summary Return dom argument for object drawing
* @desc Can be used to draw other objects on same pad / same dom element
* @protected */
getDrawDom() {
return this.getPadPainter() || this.getDom();
}
/** @summary Return actual draw options as string
* @param ignore_pad - do not include pad settings into histogram draw options
* @desc if options are not modified - returns original string which was specified for object draw */
getDrawOpt(ignore_pad) {
if (!this.options) return '';
if (isFunc(this.options.asString)) {
let changed = false;
const pp = this.getPadPainter();
if (!this.options_store || pp?._interactively_changed)
changed = true;
else {
for (const k in this.options_store) {
if (this.options[k] !== this.options_store[k]) {
if ((k[0] !== '_') && (k[0] !== '$') && (k[0].toLowerCase() !== k[0]))
changed = true;
}
}
}
if (changed && isFunc(this.options.asString))
return this.options.asString(this.isMainPainter(), ignore_pad ? null : pp?.getRootPad());
}
return this.options.original || ''; // nothing better, return original draw option
}
/** @summary Returns array with supported draw options as configured in draw.mjs
* @desc works via pad painter and only when module was loaded */
getSupportedDrawOptions() {
const pp = this.getPadPainter(),
cl = this.getClassName();
if (!cl || !isFunc(pp?.getObjectDrawSettings))
return [];
return pp.getObjectDrawSettings(prROOT + cl, 'nosame')?.opts;
}
/** @summary Central place to update objects drawing
* @param {object} obj - new version of object, values will be updated in original object
* @param {string} [opt] - when specified, new draw options
* @return {boolean|Promise} for object redraw
* @desc Two actions typically done by redraw - update object content via {@link ObjectPainter#updateObject} and
* then redraw correspondent pad via {@link ObjectPainter#redrawPad}. If possible one should redefine
* only updateObject function and keep this function unchanged. But for some special painters this function is the
* only way to control how object can be update while requested from the server
* @protected */
redrawObject(obj, opt) {
if (!this.updateObject(obj, opt)) return false;
const doc = getDocument(),
current = doc.body.style.cursor;
document.body.style.cursor = 'wait';
const res = this.redrawPad();
doc.body.style.cursor = current;
return res;
}
/** @summary Generic method to update object content.
* @desc Default implementation just copies first-level members to current object
* @param {object} obj - object with new data
* @param {string} [opt] - option which will be used for redrawing
* @protected */
updateObject(obj /* , opt */) {
if (!this.matchObjectType(obj)) return false;
Object.assign(this.getObject(), obj);
return true;
}
/** @summary Returns string with object hint
* @desc It is either item name or object name or class name.
* Such string typically used as object tooltip.
* If result string larger than 20 symbols, it will be cutted. */
getObjectHint() {
const iname = this.getItemName();
if (iname)
return (iname.length > 20) ? '...' + iname.slice(iname.length - 17) : iname;
return this.getObjectName() || this.getClassName() || '';
}
/** @summary returns color from current list of colors
* @desc First checks canvas painter and then just access global list of colors
* @param {number} indx - color index
* @return {string} with SVG color name or rgb()
* @protected */
getColor(indx) {
if (!this.root_colors)
this.root_colors = this.getCanvPainter()?.root_colors || getRootColors();
return this.root_colors[indx];
}
/** @summary Add color to list of colors
* @desc Returned color index can be used as color number in all other draw functions
* @return {number} new color index
* @protected */
addColor(color) {
if (!this.root_colors)
this.root_colors = this.getCanvPainter()?.root_colors || getRootColors();
const indx = this.root_colors.indexOf(color);
if (indx >= 0) return indx;
this.root_colors.push(color);
return this.root_colors.length - 1;
}
/** @summary returns tooltip allowed flag
* @desc If available, checks in canvas painter
* @private */
isTooltipAllowed() {
const src = this.getCanvPainter() || this;
return src.tooltip_allowed;
}
/** @summary change tooltip allowed flag
* @param {boolean|string} [on = true] set tooltip allowed state or 'toggle'
* @private */
setTooltipAllowed(on) {
if (on === undefined) on = true;
const src = this.getCanvPainter() || this;
src.tooltip_allowed = (on === 'toggle') ? !src.tooltip_allowed : on;
}
/** @summary Checks if draw elements were resized and drawing should be updated.
* @desc Redirects to {@link TPadPainter#checkCanvasResize}
* @private */
checkResize(arg) {
return this.getCanvPainter()?.checkCanvasResize(arg);
}
/** @summary removes <g> element with object drawing
* @desc generic method to delete all graphical elements, associated with the painter
* @protected */
removeG() {
this.draw_g?.remove();
delete this.draw_g;
}
/** @summary Returns created <g> element used for object drawing
* @desc Element should be created by {@link ObjectPainter#createG}
* @protected */
getG() { return this.draw_g; }
/** @summary (re)creates svg:g element for object drawings
* @desc either one attach svg:g to pad primitives (default)
* or svg:g element created in specified frame layer ('main_layer' will be used when true specified)
* @param {boolean|string} [frame_layer] - when specified, <g> element will be created inside frame layer, otherwise in the pad
* @protected */
createG(frame_layer, use_a = false) {
let layer;
if (frame_layer === 'frame2d') {
const fp = this.getFramePainter();
frame_layer = fp && !fp.mode3d;
}
if (frame_layer) {
const frame = this.getFrameSvg();
if (frame.empty()) {
console.error('Not found frame to create g element inside');
return frame;
}
if (!isStr(frame_layer)) frame_layer = 'main_layer';
layer = frame.selectChild('.' + frame_layer);
} else
layer = this.getLayerSvg('primitives_layer');
if (this.draw_g && this.draw_g.node().parentNode !== layer.node()) {
console.log('g element changes its layer!!');
this.removeG();
}
if (this.draw_g) {
// clear all elements, keep g element on its place
this.draw_g.selectAll('*').remove();
} else {
this.draw_g = layer.append(use_a ? 'svg:a' : 'svg:g');
if (!frame_layer)
layer.selectChildren('.most_upper_primitives').raise();
}
// set attributes for debugging, both should be there for opt out them later
const clname = this.getClassName(), objname = this.getObjectName();
if (objname || clname) {
this.draw_g.attr('objname', (objname || 'name').replace(/[^\w]/g, '_'))
.attr('objtype', (clname || 'type').replace(/[^\w]/g, '_'));
}
this.draw_g.property('in_frame', !!frame_layer); // indicates coordinate system
return this.draw_g;
}
/** @summary Bring draw element to the front */
bringToFront(check_online) {
if (!this.draw_g) return;
const prnt = this.draw_g.node().parentNode;
prnt?.appendChild(this.draw_g.node());
if (!check_online || !this.snapid) return;
const pp = this.getPadPainter();
if (!pp?.snapid) return;
this.getCanvPainter()?.sendWebsocket('POPOBJ:'+JSON.stringify([pp.snapid.toString(), this.snapid.toString()]));
}
/** @summary Canvas main svg element
* @return {object} d3 selection with canvas svg
* @protected */
getCanvSvg() { return this.selectDom().select('.root_canvas'); }
/** @summary Pad svg element
* @param {string} [pad_name] - pad name to select, if not specified - pad where object is drawn
* @return {object} d3 selection with pad svg
* @protected */
getPadSvg(pad_name) {
if (pad_name === undefined)
pad_name = this.pad_name;
let c = this.getCanvSvg();
if (!pad_name || c.empty()) return c;
const cp = c.property('pad_painter');
if (cp?.pads_cache && cp.pads_cache[pad_name])
return d3_select(cp.pads_cache[pad_name]);
c = c.select('.primitives_layer .__root_pad_' + pad_name);
if (cp) {
if (!cp.pads_cache) cp.pads_cache = {};
cp.pads_cache[pad_name] = c.node();
}
return c;
}
/** @summary Assign unique identifier for the painter
* @private */
getUniqueId(only_read = false) {
if (!only_read && (this._unique_painter_id === undefined))
this._unique_painter_id = internals.id_counter++; // assign unique identifier
return this._unique_painter_id;
}
/** @summary Assign secondary id
* @private */
setSecondaryId(main, name) {
this._main_painter_id = main.getUniqueId();
this._secondary_id = name;
}
/** @summary Check if this is secondary painter
* @desc if main painter provided - check if this really main for this
* @private */
isSecondary(main) {
if (this._main_painter_id === undefined)
return false;
return !isObject(main) ? true : this._main_painter_id === main.getUniqueId(true);
}
/** @summary Return primary object
* @private */
getPrimary() {
let res = null;
if (this.isSecondary()) {
this.forEachPainter(p => {
if (this.isSecondary(p))
res = p;
});
}
return res;
}
/** @summary Provides identifier on server for requested sub-element */
getSnapId(subelem) {
if (!this.snapid)
return '';
return this.snapid.toString() + (subelem ? '#'+subelem : '');
}
/** @summary Method selects immediate layer under canvas/pad main element
* @param {string} name - layer name, exits 'primitives_layer', 'btns_layer', 'info_layer'
* @param {string} [pad_name] - pad name; current pad name used by default
* @protected */
getLayerSvg(name, pad_name) {
let svg = this.getPadSvg(pad_name);
if (svg.empty()) return svg;
if (name.indexOf('prim#') === 0) {
svg = svg.selectChild('.primitives_layer');
name = name.slice(5);
}
return svg.selectChild('.' + name);
}
/** @summary Method selects current pad name
* @param {string} [new_name] - when specified, new current pad name will be configured
* @return {string} previous selected pad or actual pad when new_name not specified
* @private
* @deprecated to be removed in v8 */
selectCurrentPad() {
console.warn('selectCurrentPad is deprecated, will be removed in v8');
return '';
}
/** @summary returns pad painter
* @param {string} [pad_name] pad name or use current pad by default
* @protected */
getPadPainter(pad_name) {
const elem = this.getPadSvg(isStr(pad_name) ? pad_name : undefined);
return elem.empty() ? null : elem.property('pad_painter');
}
/** @summary returns canvas painter
* @protected */
getCanvPainter() {
const elem = this.getCanvSvg();
return elem.empty() ? null : elem.property('pad_painter');
}
/** @summary Return functor, which can convert x and y coordinates into pixels, used for drawing in the pad
* @desc X and Y coordinates can be converted by calling func.x(x) and func.y(y)
* Only can be used for painting in the pad, means CreateG() should be called without arguments
* @param {boolean} isndc - if NDC coordinates will be used
* @param {boolean} [noround] - if set, return coordinates will not be rounded
* @param {boolean} [use_frame_coordinates] - use frame coordinates even when drawing on the pad
* @protected */
getAxisToSvgFunc(isndc, nornd, use_frame_coordinates) {
const func = { isndc, nornd },
use_frame = this.draw_g?.property('in_frame');
if (use_frame || (use_frame_coordinates && !isndc))
func.main = this.getFramePainter();
if (func.main?.grx && func.main?.gry) {
func.x0 = (use_frame_coordinates && !isndc) ? func.main.getFrameX() : 0;
func.y0 = (use_frame_coordinates && !isndc) ? func.main.getFrameY() : 0;
if (nornd) {
func.x = function(x) { return this.x0 + this.main.grx(x); };
func.y = function(y) { return this.y0 + this.main.gry(y); };
} else {
func.x = function(x) { return this.x0 + Math.round(this.main.grx(x)); };
func.y = function(y) { return this.y0 + Math.round(this.main.gry(y)); };
}
} else if (!use_frame) {
const pp = this.getPadPainter();
if (!isndc) func.pad = pp?.getRootPad(true); // need for NDC conversion
func.padw = pp?.getPadWidth() ?? 10;
func.x = function(value) {
if (this.pad) {
if (this.pad.fLogx)
value = (value > 0) ? Math.log10(value) : this.pad.fUxmin;
value = (value - this.pad.fX1) / (this.pad.fX2 - this.pad.fX1);
}
value *= this.padw;
return this.nornd ? value : Math.round(value);
};
func.padh = pp?.getPadHeight() ?? 10;
func.y = function(value) {
if (this.pad) {
if (this.pad.fLogy)
value = (value > 0) ? Math.log10(value) : this.pad.fUymin;
value = (value - this.pad.fY1) / (this.pad.fY2 - this.pad.fY1);
}
value = (1 - value) * this.padh;
return this.nornd ? value : Math.round(value);
};
} else {
console.error(`Problem to create functor for ${this.getClassName()}`);
func.x = () => 0;
func.y = () => 0;
}
return func;
}
/** @summary Converts x or y coordinate into pad SVG coordinates.
* @desc Only can be used for painting in the pad, means CreateG() should be called without arguments
* @param {string} axis - name like 'x' or 'y'
* @param {number} value - axis value to convert.
* @param {boolean} ndc - is value in NDC coordinates
* @param {boolean} [noround] - skip rounding
* @return {number} value of requested coordinates
* @protected */
axisToSvg(axis, value, ndc, noround) {
const func = this.getAxisToSvgFunc(ndc, noround);
return func[axis](value);
}
/** @summary Converts pad SVG x or y coordinates into axis values.
* @desc Reverse transformation for {@link ObjectPainter#axisToSvg}
* @param {string} axis - name like 'x' or 'y'
* @param {number} coord - graphics coordinate.
* @param {boolean} ndc - kind of return value
* @return {number} value of requested coordinates
* @protected */
svgToAxis(axis, coord, ndc) {
const use_frame = this.draw_g?.property('in_frame');
if (use_frame)
return this.getFramePainter()?.revertAxis(axis, coord) ?? 0;
const pp = this.getPadPainter(),
pad = (ndc || !pp) ? null : pp.getRootPad(true);
let value = !pp ? 0 : ((axis === 'y') ? (1 - coord / pp.getPadHeight()) : coord / pp.getPadWidth());
if (pad) {
if (axis === 'y') {
value = pad.fY1 + value * (pad.fY2 - pad.fY1);
if (pad.fLogy) value = Math.pow(10, value);
} else {
value = pad.fX1 + value * (pad.fX2 - pad.fX1);
if (pad.fLogx) value = Math.pow(10, value);
}
}
return value;
}
/** @summary Returns svg element for the frame in current pad
* @protected */
getFrameSvg(pad_name) {
const layer = this.getLayerSvg('primitives_layer', pad_name);
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 frame painter for current pad
* @desc Pad has direct reference on frame if any
* @protected */
getFramePainter() {
return this.getPadPainter()?.getFramePainter();
}
/** @summary Returns painter for main object on the pad.
* @desc Typically it is first histogram drawn on the pad and which draws frame axes
* But it also can be special use-case as TASImage or TGraphPolargram
* @param {boolean} [not_store] - if true, prevent temporary storage of main painter reference
* @protected */
getMainPainter(not_store) {
let res = this._main_painter;
if (!res) {
const pp = this.getPadPainter();
res = pp ? pp.getMainPainter() : this.getTopPainter();
if (!res) res = null;
if (!not_store)
this._main_painter = res;
}
return res;
}
/** @summary Returns true if this is main painter
* @protected */
isMainPainter() { return this === this.getMainPainter(); }
/** @summary Assign this as main painter on the pad
* @desc Main painter typically responsible for axes drawing
* Should not be used by pad/canvas painters, but rather by objects which are drawing axis
* @protected */
setAsMainPainter(force) {
const pp = this.getPadPainter();
if (!pp)
this.setTopPainter(); // fallback on BasePainter method
else
pp.setMainPainter(this, force);
}
/** @summary Add painter to pad list of painters
* @desc Normally called from {@link ensureTCanvas} function when new painter is created
* @protected */
addToPadPrimitives() {
const pp = this.getPadPainter();
if (!pp || (pp === this))
return null;
if (pp.painters.indexOf(this) < 0)
pp.painters.push(this);
return pp;
}
/** @summary Remove painter from pad list of painters
* @protected */
removeFromPadPrimitives() {
const pp = this.getPadPainter();
if (!pp || (pp === this))
return false;
const k = pp.painters.indexOf(this);
if (k >= 0)
pp.painters.splice(k, 1);
return true;
}
/** @summary Creates marker attributes object
* @desc Can be used to produce markers in painter.
* See {@link TAttMarkerHandler} for more info.
* Instance assigned as this.markeratt data member, recognized by GED editor
* @param {object} args - either TAttMarker or see arguments of {@link TAttMarkerHandler}
* @return {object} created handler
* @protected */
createAttMarker(args) {
if (args === undefined)
args = { attr: this.getObject() };
else if (!isObject(args))
args = { std: true };
else if (args.fMarkerColor !== undefined && args.fMarkerStyle !== undefined && args.fMarkerSize !== undefined)
args = { attr: args, std: false };
if (args.std === undefined)
args.std = true;
if (args.painter === undefined)
args.painter = this;
let handler = args.std ? this.markeratt : null;
if (!handler)
handler = new TAttMarkerHandler(args);
else if (!handler.changed || args.force)
handler.setArgs(args);
if (args.std)
this.markeratt = handler;
return handler;
}
/** @summary Creates line attributes object.
* @desc Can be used to produce lines in painter.
* See {@link TAttLineHandler} for more info.
* Instance assigned as this.lineatt data member, recognized by GED editor
* @param {object} args - either TAttLine or see constructor arguments of {@link TAttLineHandler}
* @protected */
createAttLine(args) {
if (args === undefined)
args = { attr: this.getObject() };
else if (!isObject(args))
args = { std: true };
else if (args.fLineColor !== undefined && args.fLineStyle !== undefined && args.fLineWidth !== undefined)
args = { attr: args, std: false };
if (args.std === undefined)
args.std = true;
if (args.painter === undefined)
args.painter = this;
let handler = args.std ? this.lineatt : null;
if (!handler)
handler = new TAttLineHandler(args);
else if (!handler.changed || args.force)
handler.setArgs(args);
if (args.std)
this.lineatt = handler;
return handler;
}
/** @summary Creates text attributes object.
* @param {object} args - either TAttText or see constructor arguments of {@link TAttTextHandler}
* @protected */
createAttText(args) {
if (args === undefined)
args = { attr: this.getObject() };
else if (!isObject(args))
args = { std: true };
else if (args.fTextFont !== undefined && args.fTextSize !== undefined && args.fTextColor !== undefined)
args = { attr: args, std: false };
if (args.std === undefined)
args.std = true;
if (args.painter === undefined)
args.painter = this;
let handler = args.std ? this.textatt : null;
if (!handler)
handler = new TAttTextHandler(args);
else if (!handler.changed || args.force)
handler.setArgs(args);
if (args.std)
this.textatt = handler;
return handler;
}
/** @summary Creates fill attributes object.
* @desc Method dedicated to create fill attributes, bound to canvas SVG
* otherwise newly created patters will not be usable in the canvas
* See {@link TAttFillHandler} for more info.
* Instance assigned as this.fillatt data member, recognized by GED editors
* @param {object} [args] - for special cases one can specify TAttFill as args or number of parameters
* @param {boolean} [args.std = true] - this is standard fill attribute for object and should be used as this.fillatt
* @param {object} [args.attr = null] - object, derived from TAttFill
* @param {number} [args.pattern = undefined] - integer index of fill pattern
* @param {number} [args.color = undefined] - integer index of fill color
* @param {string} [args.color_as_svg = undefined] - color will be specified as SVG string, not as index from color palette
* @param {number} [args.kind = undefined] - some special kind which is handled differently from normal patterns
* @return created handle
* @protected */
createAttFill(args) {
if (args === undefined)
args = { attr: this.getObject() };
else if (!isObject(args))
args = { std: true };
else if (args._typename && args.fFillColor !== undefined && args.fFillStyle !== undefined)
args = { attr: args, std: false };
if (args.std === undefined)
args.std = true;
if (args.painter === undefined)
args.painter = this;
let handler = args.std ? this.fillatt : null;
if (!args.svg)
args.svg = this.getCanvSvg();
if (!handler)
handler = new TAttFillHandler(args);
else if (!handler.changed || args.force)
handler.setArgs(args);
if (args.std)
this.fillatt = handler;
return handler;
}
/** @summary call function for each painter in the pad
* @desc Iterate over all known painters
* @private */
forEachPainter(userfunc, kind) {
// iterate over all painters from pad list
const pp = this.getPadPainter();
if (pp)
pp.forEachPainterInPad(userfunc, kind);
else {
const painter = this.getTopPainter();
if (painter && (kind !== 'pads')) userfunc(painter);
}
}
/** @summary indicate that redraw was invoked via interactive action (like context menu or zooming)
* @desc Use to catch such action by GED and by server-side
* @return {Promise} when completed
* @private */
async interactiveRedraw(arg, info, subelem) {
let reason, res;
if (isStr(info) && (info.indexOf('exec:') !== 0))
reason = info;
if (arg === 'pad')
res = this.redrawPad(reason);
else if (arg !== false)
res = this.redraw(reason);
return getPromise(res).then(() => {
if (arg === 'attribute')
return this.getPadPainter()?.redrawLegend();
}).then(() => {
// inform GED that something changes
const canp = this.getCanvPainter();
if (isFunc(canp?.producePadEvent))
canp.producePadEvent('redraw', this.getPadPainter(), this, null, subelem);
// inform server that draw options changes
if (isFunc(canp?.processChanges))
canp.processChanges(info, this, subelem);
return this;
});
}
/** @summary Redraw all objects in the current pad
* @param {string} [reason] - like 'resize' or 'zoom'
* @return {Promise} when pad redraw completed
* @protected */
async redrawPad(reason) {
return this.getPadPainter()?.redrawPad(reason) ?? false;
}
/** @summary execute selected menu command, either locally or remotely
* @private */
executeMenuCommand(method) {
if (method.fName === 'Inspect')
// primitive inspector, keep it here
return this.showInspector();
return false;
}
/** @summary Invoke method for object via WebCanvas functionality
* @desc Requires that painter marked with object identifier (this.snapid) or identifier provided as second argument
* Canvas painter should exists and in non-readonly mode
* Execution string can look like 'Print()'.
* Many methods call can be chained with 'Print();;Update();;Clear()'
* @private */
submitCanvExec(exec, snapid) {
if (!exec || !isStr(exec)) return;
const canp = this.getCanvPainter();
if (isFunc(canp?.submitExec))
canp.submitExec(this, exec, snapid);
}
/** @summary remove all created draw attributes
* @protected */
deleteAttr() {
delete this.lineatt;
delete this.fillatt;
delete this.markeratt;
}
/** @summary Show object in inspector for provided object
* @protected */
showInspector(/* opt */) {
return false;
}
/** @summary Fill context menu for the object
* @private */
fillContextMenu(menu) {
const name = this.getObjectName();
let cl = this.getClassName();
const p = cl.lastIndexOf('::');
if (p > 0) cl = cl.slice(p+2);
const hdr = (cl && name) ? `${cl}:${name}` : (cl || name || 'object'),
url = (p < 0) ? `${urlClassPrefix}${cl}.html` : '';
menu.header(hdr, url);
const size0 = menu.size();
if (isFunc(this.fillContextMenuItems))
this.fillContextMenuItems(menu);
if ((menu.size() > size0) && this.showInspector('check'))
menu.add('Inspect', this.showInspector);
menu.addAttributesMenu(this);
return menu.size() > size0;
}
/** @summary shows objects status
* @desc Either used canvas painter method or globally assigned
* When no parameters are specified, just basic object properties are shown
* @private */
showObjectStatus(name, title, info, info2) {
let cp = this.getCanvPainter();
if (cp && !isFunc(cp.showCanvasStatus)) cp = null;
if (!cp && !isFunc(internals.showStatus)) return false;
if (this.enlargeMain('state') === 'on') return false;
if ((name === undefined) && (title === undefined)) {
const obj = this.getObject();
if (!obj) return;
name = this.getItemName() || obj.fName;
title = obj.fTitle || obj._typename;
info = obj._typename;
}
if (cp)
cp.showCanvasStatus(name, title, info, info2);
else
internals.showStatus(name, title, info, info2);
}
/** @summary Redraw object
* @desc Basic method, should be reimplemented in all derived objects
* for the case when drawing should be repeated
* @abstract
* @protected */
redraw(/* reason */) {}
/** @summary Start text drawing
* @desc required before any text can be drawn
* @param {number} font_face - font id as used in ROOT font attributes
* @param {number} font_size - font size as used in ROOT font attributes
* @param {object} [draw_g] - element where text drawn, by default using main object <g> element
* @param {number} [max_font_size] - maximal font size, used when text can be scaled
* @protected */
startTextDrawing(font_face, font_size, draw_g, max_font_size, can_async) {
if (!draw_g) draw_g = this.draw_g;
if (!draw_g || draw_g.empty())
return false;
const font = (font_size === 'font') ? font_face : new FontHandler(font_face, font_size);
if (can_async && font.needLoad())
return font;
font.setPainter(this); // may be required when custom font is used
draw_g.call(font.func);
draw_g.property('draw_text_completed', false) // indicate that draw operations submitted
.property('all_args', []) // array of all submitted args, makes easier to analyze them
.property('text_font', font)
.property('text_factor', 0)
.property('max_text_width', 0) // keep maximal text width, use it later
.property('max_font_size', max_font_size)
.property('_fast_drawing', this.getPadPainter()?._fast_drawing ?? false);
if (draw_g.property('_fast_drawing'))
draw_g.property('_font_too_small', (max_font_size && (max_font_size < 5)) || (font.size < 4));
return true;
}
/** @summary Start async text drawing
* @return {Promise} for loading of font if necessary
* @private */
async startTextDrawingAsync(font_face, font_size, draw_g, max_font_size) {
const font = this.startTextDrawing(font_face, font_size, draw_g, max_font_size, true);
if ((font === true) || (font === false))
return font;
return font.load().then(res => {
if (!res)
return false;
return this.startTextDrawing(font, 'font', draw_g, max_font_size);
});
}
/** @summary Apply scaling factor to all drawn text in the <g> element
* @desc Can be applied at any time before finishTextDrawing is called - even in the postprocess callbacks of text draw
* @param {number} factor - scaling factor
* @param {object} [draw_g] - drawing element for the text
* @protected */
scaleTextDrawing(factor, draw_g) {
if (!draw_g) draw_g = this.draw_g;
if (!draw_g || draw_g.empty()) return;
if (factor && (factor > draw_g.property('text_factor')))
draw_g.property('text_factor', factor);
}
/** @summary Analyze if all text draw operations are completed
* @private */
_checkAllTextDrawing(draw_g, resolveFunc, try_optimize) {
let all_args = draw_g.property('all_args'), missing = 0;
if (!all_args) {
console.log('Text drawing is finished - why calling _checkAllTextDrawing?????');
all_args = [];
}
all_args.forEach(arg => { if (!arg.ready) missing++; });
if (missing > 0) {
if (isFunc(resolveFunc)) {
draw_g.node().textResolveFunc = resolveFunc;
draw_g.node().try_optimize = try_optimize;
}
return;
}
draw_g.property('all_args', null); // clear all_args property
// adjust font size (if there are normal text)
const f = draw_g.property('text_factor'),
font = draw_g.property('text_font'),
max_sz = draw_g.property('max_font_size');
let font_size = font.size, any_text = false, only_text = true;
if ((f > 0) && ((f < 0.95) || (f > 1.05)))
font.size = Math.max(1, Math.floor(font.size / f));
if (max_sz && (font.size > max_sz))
font.size = max_sz;
if (font.size !== font_size) {
draw_g.call(font.func);
font_size = font.size;
}
all_args.forEach(arg => {
if (arg.mj_node && arg.applyAttributesToMathJax) {
const svg = arg.mj_node.select('svg'); // MathJax svg
arg.applyAttributesToMathJax(this, arg.mj_node, svg, arg, font_size, f);
delete arg.mj_node; // remove reference
only_text = false;
} else if (arg.txt_g)
only_text = false;
});
if (!resolveFunc) {
resolveFunc = draw_g.node().textResolveFunc;
try_optimize = draw_g.node().try_optimize;
delete draw_g.node().textResolveFunc;
delete draw_g.node().try_optimize;
}
const optimize_arr = (try_optimize && only_text) ? [] : null;
// now process text and latex drawings
all_args.forEach(arg => {
let txt, is_txt, scale = 1;
if (arg.txt_node) {
txt = arg.txt_node;
delete arg.txt_node;
is_txt = true;
if (optimize_arr !== null) optimize_arr.push(txt);
} else if (arg.txt_g) {
txt = arg.txt_g;
delete arg.txt_g;
is_txt = false;
} else
return;
txt.attr('visibility', null);
any_text = true;
if (arg.width) {
// adjust x position when scale into specified rectangle
if (arg.align[0] === 'middle')
arg.x += arg.width / 2;
else if (arg.align[0] === 'end')
arg.x += arg.width;
}
if (arg.height) {
if (arg.align[1].indexOf('bottom') === 0)
arg.y += arg.height;
else if (arg.align[1] === 'middle')
arg.y += arg.height / 2;
}
let dx = 0, dy = 0;
if (is_txt) {
// handle simple text drawing
if (isNodeJs()) {
if (arg.scale && (f > 0)) { arg.box.width *= 1/f; arg.box.height *= 1/f; }
} else if (!arg.plain && !arg.fast) {
// exact box dimension only required when complex text was build
arg.box = getElementRect(txt, 'bbox');
}
if (arg.plain) {
txt.attr('text-anchor', arg.align[0]);
if (arg.align[1] === 'top')
txt.attr('dy', '.8em');
else if (arg.align[1] === 'middle') {
// if (isNodeJs()) txt.attr('dy', '.4em'); else // old workaround for node.js
txt.attr('dominant-baseline', 'middle');
}
} else {
txt.attr('text-anchor', 'start');
dx = ((arg.align[0] === 'middle') ? -0.5 : ((arg.align[0] === 'end') ? -1 : 0)) * arg.box.width;
dy = ((arg.align[1] === 'top') ? (arg.top_shift || 1) : (arg.align[1] === 'middle') ? (arg.mid_shift || 0.5) : 0) * arg.box.height;
}
} else if (arg.text_rect) {
// handle latex drawing
const box = arg.text_rect;
scale = (f > 0) && (Math.abs(1-f) > 0.01) ? 1/f : 1;
dx = ((arg.align[0] === 'middle') ? -0.5 : ((arg.align[0] === 'end') ? -1 : 0)) * box.width * scale;
if (arg.align[1] === 'top')
dy = -box.y1*scale;
else if (arg.align[1] === 'bottom')
dy = -box.y2*scale;
else if (arg.align[1] === 'middle')
dy = -0.5*(box.y1 + box.y2)*scale;
} else
console.error('text rect not calcualted - please check code');
if (!arg.rotate) { arg.x += dx; arg.y += dy; dx = dy = 0; }
// use translate and then rotate to avoid complex sign calculations
let trans = makeTranslate(Math.round(arg.x), Math.round(arg.y)) || '';
const dtrans = makeTranslate(Math.round(dx), Math.round(dy)),
append = arg => { if (trans) trans += ' '; trans += arg; };
if (arg.rotate)
append(`rotate(${Math.round(arg.rotate)})`);
if (scale !== 1)
append(`scale(${scale.toFixed(3)})`);
if (dtrans)
append(dtrans);
if (trans) txt.attr('transform', trans);
});
// when no any normal text drawn - remove font attributes
if (!any_text)
font.clearFont(draw_g);
if ((optimize_arr !== null) && (optimize_arr.length > 1)) {
['fill', 'text-anchor'].forEach(name => {
let first = optimize_arr[0].attr(name);
optimize_arr.forEach(txt_node => {
const value = txt_node.attr(name);
if (!value || (value !== first)) first = undefined;
});
if (first) {
draw_g.attr(name, first);
optimize_arr.forEach(txt_node => { txt_node.attr(name, null); });
}
});
}
// if specified, call resolve function
if (resolveFunc) resolveFunc(this); // IMPORTANT - return painter, may use in draw methods
}
/** @summary Post-process plain text drawing
* @private */
_postprocessDrawText(arg, txt_node) {
// complete rectangle with very rough size estimations
arg.box = !isNodeJs() && !settings.ApproxTextSize && !arg.fast
? getElementRect(txt_node, 'bbox')
: (arg.text_rect || { height: Math.round(1.15 * arg.font_size), width: approximateLabelWidth(arg.text, arg.font, arg.font_size) });
txt_node.attr('visibility', 'hidden'); // hide elements until text drawing is finished
if (arg.box.width > arg.draw_g.property('max_text_width'))
arg.draw_g.property('max_text_width', arg.box.width);
if (arg.scale)
this.scaleTextDrawing(Math.max(1.05 * arg.box.width / arg.width, arg.box.height / arg.height), arg.draw_g);
arg.result_width = arg.box.width;
arg.result_height = arg.box.height;
if (isFunc(arg.post_process))
arg.post_process(this);
return arg.box.width;
}
/** @summary Draw text
* @desc The only legal way to draw text, support plain, latex and math text output
* @param {object} arg - different text draw options
* @param {string} arg.text - text to draw
* @param {number} [arg.align = 12] - int value like 12 or 31
* @param {string} [arg.align = undefined] - end;bottom
* @param {number} [arg.x = 0] - x position
* @param {number} [arg.y = 0] - y position
* @param {number} [arg.width] - when specified, adjust font size in the specified box
* @param {number} [arg.height] - when specified, adjust font size in the specified box
* @param {boolean} [arg.scale = true] - scale into draw box when width and height parameters are specified
* @param {number} [arg.latex] - 0 - plain text, 1 - normal TLatex, 2 - math
* @param {string} [arg.color=black] - text color
* @param {number} [arg.rotate] - rotation angle
* @param {number} [arg.font_size] - fixed font size
* @param {object} [arg.draw_g] - element where to place text, if not specified central draw_g container is used
* @param {function} [arg.post_process] - optional function called when specified text is drawn
* @protected */
drawText(arg) {
if (!arg.text)
arg.text = '';
arg.draw_g = arg.draw_g || this.draw_g;
if (!arg.draw_g || arg.draw_g.empty()) return;
const font = arg.draw_g.property('text_font');
arg.font = font; // use in latex conversion
if (font) {
if (font.color && !arg.color) arg.color = font.color;
if (font.align && !arg.align) arg.align = font.align;
if (font.angle && !arg.rotate) arg.rotate = font.angle;
}
let align = ['start', 'middle'];
if (isStr(arg.align)) {
align = arg.align.split(';');
if (align.length === 1) align.push('middle');
} else if (typeof arg.align === 'number') {
if ((arg.align / 10) >= 3)
align[0] = 'end';
else if ((arg.align / 10) >= 2)
align[0] = 'middle';
if ((arg.align % 10) === 0)
align[1] = 'bottom';
else if ((arg.align % 10) === 1)
align[1] = 'bottom-base';
else if ((arg.align % 10) === 3)
align[1] = 'top';
} else if (isObject(arg.align) && (arg.align.length === 2))
align = arg.align;
if (arg.latex === undefined) arg.latex = 1; // latex 0-text, 1-latex, 2-math
arg.align = align;
arg.x = arg.x || 0;
arg.y = arg.y || 0;
if (arg.scale !== false)
arg.scale = arg.width && arg.height && !arg.font_size;
arg.width = arg.width || 0;
arg.height = arg.height || 0;
if (arg.draw_g.property('_fast_drawing')) {
if (arg.scale) {
// area too small - ignore such drawing
if (arg.height < 4) return 0;
} else if (arg.font_size) {
// font size too small
if (arg.font_size < 4) return 0;
} else if (arg.draw_g.property('_font_too_small')) {
// configure font is too small - ignore drawing
return 0;
}
}
// include drawing into list of all args
arg.draw_g.property('all_args').push(arg);
arg.ready = false; // indicates if drawing is ready for post-processing
let use_mathjax = (arg.latex === 2);
const cl = constants.Latex;
if (arg.latex === 1) {
use_mathjax = (settings.Latex === cl.AlwaysMathJax) ||
((settings.Latex === cl.MathJax) && arg.text.match(/[#{\\]/g)) ||
arg.text.match(/[\\]/g);
}
if (!use_mathjax || arg.nomathjax) {
arg.txt_node = arg.draw_g.append('svg:text');
if (arg.color) arg.txt_node.attr('fill', arg.color);
if (arg.font_size) arg.txt_node.attr('font-size', arg.font_size);
else arg.font_size = font.size;
arg.plain = !arg.latex || (settings.Latex === cl.Off) || (settings.Latex === cl.Symbols);
arg.simple_latex = arg.latex && (settings.Latex === cl.Symbols);
if (!arg.plain || arg.simple_latex || arg.font?.isSymbol) {
if (arg.simple_latex || isPlainText(arg.text) || arg.plain) {
arg.simple_latex = true;
producePlainText(this, arg.txt_node, arg);
} else {
arg.txt_node.remove(); // just remove text node
delete arg.txt_node;
arg.txt_g = arg.draw_g.append('svg:g');
produceLatex(this, arg.txt_g, arg);
}
arg.ready = true;
this._postprocessDrawText(arg, arg.txt_g || arg.txt_node);
if (arg.draw_g.property('draw_text_completed'))
this._checkAllTextDrawing(arg.draw_g); // check if all other elements are completed
return 0;
}
arg.plain = true;
arg.txt_node.text(arg.text);
arg.ready = true;
return this._postprocessDrawText(arg, arg.txt_node);
}
arg.mj_node = arg.draw_g.append('svg:g').attr('visibility', 'hidden'); // hide text until drawing is finished
produceMathjax(this, arg.mj_node, arg).then(() => {
arg.ready = true;
if (arg.draw_g.property('draw_text_completed'))
this._checkAllTextDrawing(arg.draw_g);
});
return 0;
}
/** @summary Finish text drawing
* @desc Should be called to complete all text drawing operations
* @param {function} [draw_g] - <g> element for text drawing, this.draw_g used when not specified
* @return {Promise} when text drawing completed
* @protected */
async finishTextDrawing(draw_g, try_optimize) {
if (!draw_g) draw_g = this.draw_g;
if (!draw_g || draw_g.empty())
return false;
draw_g.property('draw_text_completed', true); // mark that text drawing is completed
return new Promise(resolveFunc => {
this._checkAllTextDrawing(draw_g, resolveFunc, try_optimize);
});
}
/** @summary Configure user-defined context menu for the object
* @desc fillmenu_func will be called when context menu is activated
* Arguments fillmenu_func are (menu,kind)
* First is menu object, second is object sub-element like axis 'x' or 'y'
* Function should return promise with menu when items are filled
* @param {function} fillmenu_func - function to fill custom context menu for object */
configureUserContextMenu(fillmenu_func) {
if (!fillmenu_func || !isFunc(fillmenu_func))
delete this._userContextMenuFunc;
else
this._userContextMenuFunc = fillmenu_func;
}
/** @summary Fill object menu in web canvas
* @private */
async fillObjectExecMenu(menu, kind) {
if (isFunc(this._userContextMenuFunc))
return this._userContextMenuFunc(menu, kind);
const canvp = this.getCanvPainter();
if (!this.snapid || !canvp || canvp?._readonly || !canvp?._websocket)
return menu;
function DoExecMenu(arg) {
const execp = menu.exec_painter || this,
cp = execp.getCanvPainter(),
item = menu.exec_items[parseInt(arg)];
if (!item?.fName) return;
// this is special entry, produced by TWebMenuItem, which recognizes editor entries itself
if (item.fExec === 'Show:Editor') {
if (isFunc(cp?.activateGed))
cp.activateGed(execp);
return;
}
if (isFunc(cp?.executeObjectMethod))
if (cp.executeObjectMethod(execp, item, item.$execid)) return;
item.fClassName = execp.getClassName();
if ((item.$execid.indexOf('#x') > 0) || (item.$execid.indexOf('#y') > 0) || (item.$execid.indexOf('#z') > 0))
item.fClassName = clTAxis;
if (execp.executeMenuCommand(item)) return;
if (!item.$execid) return;
if (!item.fArgs) {
if (cp?.v7canvas)
return cp.submitExec(execp, item.fExec, kind);
else
return execp.submitCanvExec(item.fExec, item.$execid);
}
menu.showMethodArgsDialog(item).then(args => {
if (!args) return;
if (execp.executeMenuCommand(item, args)) return;
const exec = item.fExec.slice(0, item.fExec.length-1) + args + ')';
if (cp?.v7canvas)
cp.submitExec(execp, exec, kind);
else
cp?.sendWebsocket(`OBJEXEC:${item.$execid}:${exec}`);
});
}
const DoFillMenu = (_menu, _reqid, _resolveFunc, reply) => {
// avoid multiple call of the callback after timeout
if (menu._got_menu) return;
menu._got_menu = true;
if (reply && (_reqid !== reply.fId))
console.error(`missmatch between request ${_reqid} and reply ${reply.fId} identifiers`);
menu.exec_items = reply?.fItems;
if (menu.exec_items?.length) {
if (_menu.size() > 0)
_menu.separator();
let lastclname;
for (let n = 0; n < menu.exec_items.length; ++n) {
const item = menu.exec_items[n];
item.$execid = reply.fId;
item.$menu = menu;
if (item.fClassName && lastclname && (lastclname !== item.fClassName)) {
_menu.endsub();
lastclname = '';
}
if (lastclname !== item.fClassName) {
lastclname = item.fClassName;
const p = lastclname.lastIndexOf('::'),
shortname = (p > 0) ? lastclname.slice(p+2) : lastclname;
_menu.sub(shortname.replace(/[<>]/g, '_'));
}
if ((item.fChecked === undefined) || (item.fChecked < 0))
_menu.add(item.fName, n, DoExecMenu);
else
_menu.addchk(item.fChecked, item.fName, n, DoExecMenu);
}
if (lastclname) _menu.endsub();
}
_resolveFunc(_menu);
},
reqid = this.getSnapId(kind);
menu._got_menu = false;
// if menu painter differs from this, remember it for further usage
if (menu.painter)
menu.exec_painter = (menu.painter !== this) ? this : undefined;
return new Promise(resolveFunc => {
let did_resolve = false;
function handleResolve(res) {
if (did_resolve) return;
did_resolve = true;
resolveFunc(res);
}
// set timeout to avoid menu hanging
setTimeout(() => DoFillMenu(menu, reqid, handleResolve), 2000);
canvp.submitMenuRequest(this, kind, reqid).then(lst => DoFillMenu(menu, reqid, handleResolve, lst));
});
}
/** @summary Configure user-defined tooltip handler
* @desc Hook for the users to get tooltip information when mouse cursor moves over frame area
* Handler function will be called every time when new data is selected
* when mouse leave frame area, handler(null) will be called
* @param {function} handler - function called when tooltip is produced
* @param {number} [tmout = 100] - delay in ms before tooltip delivered */
configureUserTooltipHandler(handler, tmout) {
if (!handler || !isFunc(handler)) {
delete this._user_tooltip_handler;
delete this._user_tooltip_timeout;
} else {
this._user_tooltip_handler = handler;
this._user_tooltip_timeout = tmout || 100;
}
}
/** @summary Configure user-defined click handler
* @desc Function will be called every time when frame click was performed
* As argument, tooltip object with selected bins will be provided
* If handler function returns true, default handling of click will be disabled
* @param {function} handler - function called when mouse click is done */
configureUserClickHandler(handler) {
const fp = this.getFramePainter();
if (isFunc(fp?.configureUserClickHandler))
fp.configureUserClickHandler(handler);
}
/** @summary Configure user-defined dblclick handler
* @desc Function will be called every time when double click was called
* As argument, tooltip object with selected bins will be provided
* If handler function returns true, default handling of dblclick (unzoom) will be disabled
* @param {function} handler - function called when mouse double click is done */
configureUserDblclickHandler(handler) {
const fp = this.getFramePainter();
if (isFunc(fp?.configureUserDblclickHandler))
fp.configureUserDblclickHandler(handler);
}
/** @summary Check if user-defined tooltip function was configured
* @return {boolean} flag is user tooltip handler was configured */
hasUserTooltip() {
return isFunc(this._user_tooltip_handler);
}
/** @summary Provide tooltips data to user-defined function
* @param {object} data - tooltip data
* @private */
provideUserTooltip(data) {
if (!this.hasUserTooltip()) return;
if (this._user_tooltip_timeout <= 0)
return this._user_tooltip_handler(data);
if (this._user_tooltip_handle) {
clearTimeout(this._user_tooltip_handle);
delete this._user_tooltip_handle;
}
if (!data)
return this._user_tooltip_handler(data);
// only after timeout user function will be called
this._user_tooltip_handle = setTimeout(() => {
delete this._user_tooltip_handle;
if (this._user_tooltip_handler) this._user_tooltip_handler(data);
}, this._user_tooltip_timeout);
}
/** @summary Provide projection areas
* @param kind - 'X', 'Y', 'XY' or ''
* @private */
async provideSpecialDrawArea(kind) {
if (kind === this._special_draw_area)
return true;
return this.getCanvPainter().toggleProjection(kind).then(() => {
this._special_draw_area = kind;
return true;
});
}
/** @summary Draw in special projection areas
* @param obj - object to draw
* @param opt - draw option
* @param kind - '', 'X', 'Y'
* @private */
async drawInSpecialArea(obj, opt, kind) {
const canp = this.getCanvPainter();
if (this._special_draw_area && isFunc(canp?.drawProjection))
return canp.drawProjection(kind || this._special_draw_area, obj, opt);
return false;
}
/** @summary Get tooltip for painter and specified event position
* @param {Object} evnt - object with clientX and clientY positions
* @private */
getToolTip(evnt) {
if ((evnt?.clientX === undefined) || (evnt?.clientY === undefined)) return null;
const frame = this.getFrameSvg();
if (frame.empty()) return null;
const layer = frame.selectChild('.main_layer');
if (layer.empty()) return null;
const pos = d3_pointer(evnt, layer.node()),
pnt = { touch: false, x: pos[0], y: pos[1] };
if (isFunc(this.extractToolTip))
return this.extractToolTip(pnt);
pnt.disabled = true;
const res = isFunc(this.processTooltipEvent) ? this.processTooltipEvent(pnt) : null;
return res?.user_info || res;
}
} // class ObjectPainter
/** @summary Generic text drawing
* @private */
function drawRawText(dom, txt /* , opt */) {
const painter = new BasePainter(dom);
painter.txt = txt;
painter.redrawObject = function(obj) {
this.txt = obj;
this.drawText();
return true;
};
painter.drawText = async function() {
let txt = (this.txt._typename === clTObjString) ? this.txt.fString : this.txt.value;
if (!isStr(txt)) txt = '<undefined>';
const mathjax = this.txt.mathjax || (settings.Latex === constants.Latex.AlwaysMathJax);
if (!mathjax && !('as_is' in this.txt)) {
const arr = txt.split('\n'); txt = '';
for (let i = 0; i < arr.length; ++i)
txt += `<pre style='margin:0'>${arr[i]}</pre>`;
}
const frame = this.selectDom();
let main = frame.select('div');
if (main.empty())
main = frame.append('div').attr('style', 'max-width:100%;max-height:100%;overflow:auto');
main.html(txt);
// (re) set painter to first child element, base painter not requires canvas
this.setTopPainter();
if (mathjax)
typesetMathjax(frame.node());
return this;
};
return painter.drawText();
}
/** @summary Returns canvas painter (if any) for specified HTML element
* @param {string|object} dom - id or DOM element
* @private */
function getElementCanvPainter(dom) {
return new ObjectPainter(dom).getCanvPainter();
}
/** @summary Returns main painter (if any) for specified HTML element - typically histogram painter
* @param {string|object} dom - id or DOM element
* @private */
function getElementMainPainter(dom) {
return new ObjectPainter(dom).getMainPainter(true);
}
/** @summary Save object, drawn in specified element, as JSON.
* @desc Normally it is TCanvas object with list of primitives
* @param {string|object} dom - id of top div element or directly DOMElement
* @return {string} produced JSON string */
function drawingJSON(dom) {
return getElementCanvPainter(dom)?.produceJSON() || '';
}
let $active_pp = null;
/** @summary Set active pad painter
* @desc Normally be used to handle key press events, which are global in the web browser
* @param {object} args - functions arguments
* @param {object} args.pp - pad painter
* @param {boolean} [args.active] - is pad activated or not
* @private */
function selectActivePad(args) {
if (args.active) {
$active_pp?.getFramePainter()?.setFrameActive(false);
$active_pp = args.pp;
$active_pp?.getFramePainter()?.setFrameActive(true);
} else if ($active_pp === args.pp)
$active_pp = null;
}
/** @summary Returns current active pad
* @desc Should be used only for keyboard handling
* @private */
function getActivePad() {
return $active_pp;
}
/** @summary Check resize of drawn element
* @param {string|object} dom - id or DOM element
* @param {boolean|object} arg - options on how to resize
* @desc As first argument dom one should use same argument as for the drawing
* As second argument, one could specify 'true' value to force redrawing of
* the element even after minimal resize
* Or one just supply object with exact sizes like { width:300, height:200, force:true };
* @example
* import { resize } from 'https://root.cern/js/latest/modules/base/ObjectPainter.mjs';
* resize('drawing', { width: 500, height: 200 });
* resize(document.querySelector('#drawing'), true); */
function resize(dom, arg) {
if (arg === true)
arg = { force: true };
else if (!isObject(arg))
arg = null;
let done = false;
new ObjectPainter(dom).forEachPainter(painter => {
if (!done && isFunc(painter.checkResize))
done = painter.checkResize(arg);
});
return done;
}
/** @summary Safely remove all drawings from specified element
* @param {string|object} dom - id or DOM element
* @public
* @example
* import { cleanup } from 'https://root.cern/js/latest/modules/base/ObjectPainter.mjs';
* cleanup('drawing');
* cleanup(document.querySelector('#drawing')); */
function cleanup(dom) {
const dummy = new ObjectPainter(dom), lst = [];
dummy.forEachPainter(p => { if (lst.indexOf(p) < 0) lst.push(p); });
lst.forEach(p => p.cleanup());
dummy.selectDom().html('');
return lst;
}
const EAxisBits = {
kDecimals: BIT(7),
kTickPlus: BIT(9),
kTickMinus: BIT(10),
kAxisRange: BIT(11),
kCenterTitle: BIT(12),
kCenterLabels: BIT(14),
kRotateTitle: BIT(15),
kPalette: BIT(16),
kNoExponent: BIT(17),
kLabelsHori: BIT(18),
kLabelsVert: BIT(19),
kLabelsDown: BIT(20),
kLabelsUp: BIT(21),
kIsInteger: BIT(22),
kMoreLogLabels: BIT(23),
kOppositeTitle: BIT(32) // artificial bit, not possible to set in ROOT
}, kAxisLabels = 'labels', kAxisNormal = 'normal', kAxisFunc = 'func', kAxisTime = 'time';
Object.assign(internals.jsroot, { ObjectPainter, cleanup, resize });
export { getElementCanvPainter, getElementMainPainter, drawingJSON,
selectActivePad, getActivePad, cleanup, resize,
ObjectPainter, drawRawText,
EAxisBits, kAxisLabels, kAxisNormal, kAxisFunc, kAxisTime };