base/ObjectPainter.mjs

  1. import { select as d3_select, pointer as d3_pointer } from '../d3.mjs';
  2. import { settings, constants, internals, isNodeJs, isBatchMode, getPromise, BIT,
  3. prROOT, clTObjString, clTAxis, isObject, isFunc, isStr, getDocument, urlClassPrefix } from '../core.mjs';
  4. import { isPlainText, producePlainText, produceLatex, produceMathjax, typesetMathjax, approximateLabelWidth } from './latex.mjs';
  5. import { getElementRect, BasePainter, makeTranslate } from './BasePainter.mjs';
  6. import { TAttMarkerHandler } from './TAttMarkerHandler.mjs';
  7. import { TAttFillHandler } from './TAttFillHandler.mjs';
  8. import { TAttLineHandler } from './TAttLineHandler.mjs';
  9. import { TAttTextHandler } from './TAttTextHandler.mjs';
  10. import { FontHandler } from './FontHandler.mjs';
  11. import { getRootColors } from './colors.mjs';
  12. /**
  13. * @summary Painter class for ROOT objects
  14. *
  15. */
  16. class ObjectPainter extends BasePainter {
  17. #pad_name; // pad name where object is drawn
  18. #draw_object; // drawn object
  19. #main_painter; // WeakRef to main painter in the pad
  20. #primary_ref; // reference of primary painter - if any
  21. #secondary_id; // id of this painter in relation to primary painter
  22. #options_store; // stored draw options used to check changes
  23. #user_tooltip_handler; // configured user tooltip handler
  24. #user_tooltip_timeout; // timeout configured with tooltip handler
  25. #user_toottip_handle; // timeout handle processing user tooltip
  26. #user_context_menu; // function for user context menu
  27. #special_draw_area; // current special draw area like projection
  28. #root_colors; // custom colors list
  29. /** @summary constructor
  30. * @param {object|string} dom - dom element or identifier or pad painter
  31. * @param {object} obj - object to draw
  32. * @param {string} [opt] - object draw options */
  33. constructor(dom, obj, opt) {
  34. let pad_name = '';
  35. if (isFunc(dom?.forEachPainterInPad)) {
  36. pad_name = dom.this_pad_name;
  37. dom = dom.getDom();
  38. }
  39. super(dom);
  40. // this.draw_g = undefined; // container for all drawn objects
  41. this.setPadName(pad_name); // name of pad where object is drawn
  42. this.assignObject(obj);
  43. if (isStr(opt))
  44. this.options = { original: opt };
  45. }
  46. /** @summary Assign object to the painter
  47. * @protected */
  48. assignObject(obj) { this.#draw_object = isObject(obj) ? obj : null; }
  49. /** @summary Returns drawn object */
  50. getObject() { return this.#draw_object; }
  51. /** @summary Assigns pad name where element will be drawn
  52. * @desc Should happened before first draw of element is performed, only for special use case
  53. * @param {string} [pad_name] - on which sub-pad element should be draw, if not specified - use current
  54. * @protected */
  55. setPadName(pad_name) {
  56. // console.warn('setPadName is deprecated, to be removed in v8');
  57. this.#pad_name = isStr(pad_name) ? pad_name : '';
  58. }
  59. /** @summary Returns pad name where object is drawn */
  60. getPadName() { return this.#pad_name || ''; }
  61. /** @summary Indicates that drawing runs in batch mode
  62. * @private */
  63. isBatchMode() { return isBatchMode() ? true : (this.getCanvPainter()?.isBatchMode() ?? false); }
  64. /** @summary Assign snapid to the painter
  65. * @desc Identifier used to communicate with server side and identifies object on the server
  66. * @private */
  67. assignSnapId(id) { this.snapid = id; }
  68. /** @summary Generic method to cleanup painter.
  69. * @desc Remove object drawing and (in case of main painter) also main HTML components
  70. * @protected */
  71. cleanup() {
  72. this.removeG();
  73. let keep_origin = true;
  74. if (this.isMainPainter()) {
  75. const pp = this.getPadPainter();
  76. if (!pp || pp.isCanvas('auto'))
  77. keep_origin = false;
  78. }
  79. // cleanup all existing references
  80. this.#pad_name = undefined;
  81. this.#main_painter = null;
  82. this.#draw_object = null;
  83. delete this.snapid;
  84. this._is_primary = undefined;
  85. this.#primary_ref = undefined;
  86. this.#secondary_id = undefined;
  87. // remove attributes objects (if any)
  88. delete this.fillatt;
  89. delete this.lineatt;
  90. delete this.markeratt;
  91. this.#root_colors = undefined;
  92. delete this.options;
  93. this.#options_store = undefined;
  94. // remove extra fields from v7 painters
  95. delete this.rstyle;
  96. delete this.csstype;
  97. super.cleanup(keep_origin);
  98. }
  99. /** @summary Returns drawn object name */
  100. getObjectName() { return this.getObject()?.fName ?? ''; }
  101. /** @summary Returns drawn object class name */
  102. getClassName() { return this.getObject()?._typename ?? ''; }
  103. /** @summary Checks if drawn object matches with provided typename
  104. * @param {string|object} arg - typename (or object with _typename member)
  105. * @protected */
  106. matchObjectType(arg) {
  107. const clname = this.getClassName();
  108. if (!arg || !clname)
  109. return false;
  110. if (isStr(arg))
  111. return arg === clname;
  112. if (isStr(arg._typename))
  113. return arg._typename === clname;
  114. return Boolean(clname.match(arg));
  115. }
  116. /** @summary Change item name
  117. * @desc When available, used for svg:title property
  118. * @private */
  119. setItemName(name, opt, hpainter) {
  120. super.setItemName(name, opt, hpainter);
  121. if (this.no_default_title || !name) return;
  122. const can = this.getCanvSvg();
  123. if (!can.empty()) can.select('title').text(name);
  124. else this.selectDom().attr('title', name);
  125. const cp = this.getCanvPainter();
  126. if (cp && ((cp === this) || (this.isMainPainter() && (cp === this.getPadPainter()))))
  127. cp.drawItemNameOnCanvas(name);
  128. }
  129. /** @summary Store actual this.options together with original string
  130. * @private */
  131. storeDrawOpt(original) {
  132. if (!this.options) return;
  133. if (!original) original = '';
  134. const pp = original.indexOf(';;');
  135. if (pp >= 0) original = original.slice(0, pp);
  136. this.options.original = original;
  137. this.#options_store = Object.assign({}, this.options);
  138. }
  139. /** @summary Return dom argument for object drawing
  140. * @desc Can be used to draw other objects on same pad / same dom element
  141. * @protected */
  142. getDrawDom() {
  143. return this.getPadPainter() || this.getDom();
  144. }
  145. /** @summary Return actual draw options as string
  146. * @param ignore_pad - do not include pad settings into histogram draw options
  147. * @desc if options are not modified - returns original string which was specified for object draw */
  148. getDrawOpt(ignore_pad) {
  149. if (!this.options) return '';
  150. if (isFunc(this.options.asString)) {
  151. let changed = false;
  152. const pp = this.getPadPainter();
  153. if (!this.#options_store || pp?._interactively_changed)
  154. changed = true;
  155. else {
  156. for (const k in this.#options_store) {
  157. if (this.options[k] !== this.#options_store[k]) {
  158. if ((k[0] !== '_') && (k[0] !== '$') && (k[0].toLowerCase() !== k[0]))
  159. changed = true;
  160. }
  161. }
  162. }
  163. if (changed && isFunc(this.options.asString))
  164. return this.options.asString(this.isMainPainter(), ignore_pad ? null : pp?.getRootPad());
  165. }
  166. return this.options.original || ''; // nothing better, return original draw option
  167. }
  168. /** @summary Returns array with supported draw options as configured in draw.mjs
  169. * @desc works via pad painter and only when module was loaded */
  170. getSupportedDrawOptions() {
  171. const pp = this.getPadPainter(),
  172. cl = this.getClassName();
  173. if (!cl || !isFunc(pp?.getObjectDrawSettings))
  174. return [];
  175. return pp.getObjectDrawSettings(prROOT + cl, 'nosame')?.opts;
  176. }
  177. /** @summary Central place to update objects drawing
  178. * @param {object} obj - new version of object, values will be updated in original object
  179. * @param {string} [opt] - when specified, new draw options
  180. * @return {boolean|Promise} for object redraw
  181. * @desc Two actions typically done by redraw - update object content via {@link ObjectPainter#updateObject} and
  182. * then redraw correspondent pad via {@link ObjectPainter#redrawPad}. If possible one should redefine
  183. * only updateObject function and keep this function unchanged. But for some special painters this function is the
  184. * only way to control how object can be update while requested from the server
  185. * @protected */
  186. redrawObject(obj, opt) {
  187. if (!this.updateObject(obj, opt)) return false;
  188. const doc = getDocument(),
  189. current = doc.body.style.cursor;
  190. document.body.style.cursor = 'wait';
  191. const res = this.redrawPad();
  192. doc.body.style.cursor = current;
  193. return res;
  194. }
  195. /** @summary Generic method to update object content.
  196. * @desc Default implementation just copies first-level members to current object
  197. * @param {object} obj - object with new data
  198. * @param {string} [opt] - option which will be used for redrawing
  199. * @protected */
  200. updateObject(obj /* , opt */) {
  201. if (!this.matchObjectType(obj))
  202. return false;
  203. Object.assign(this.getObject(), obj);
  204. return true;
  205. }
  206. /** @summary Returns string with object hint
  207. * @desc It is either item name or object name or class name.
  208. * Such string typically used as object tooltip.
  209. * If result string larger than 20 symbols, it will be shorten. */
  210. getObjectHint() {
  211. const iname = this.getItemName();
  212. if (iname)
  213. return (iname.length > 20) ? '...' + iname.slice(iname.length - 17) : iname;
  214. return this.getObjectName() || this.getClassName() || '';
  215. }
  216. /** @summary Set colors list
  217. * @protected */
  218. setColors(lst) { this.#root_colors = lst; }
  219. /** @summary Return colors list
  220. * @protected */
  221. getColors(force) {
  222. if (!this.#root_colors && force)
  223. this.setColors(this.getCanvPainter()?.getColors() || getRootColors());
  224. return this.#root_colors;
  225. }
  226. /** @summary returns color from current list of colors
  227. * @desc First checks canvas painter and then just access global list of colors
  228. * @param {number} indx - color index
  229. * @return {string} with SVG color name or rgb()
  230. * @protected */
  231. getColor(indx) { return this.getColors(true)[indx]; }
  232. /** @summary Add color to list of colors
  233. * @desc Returned color index can be used as color number in all other draw functions
  234. * @return {number} new color index
  235. * @protected */
  236. addColor(color) {
  237. const lst = this.getColors(true),
  238. indx = lst.indexOf(color);
  239. if (indx >= 0)
  240. return indx;
  241. lst.push(color);
  242. return lst.length - 1;
  243. }
  244. /** @summary returns tooltip allowed flag
  245. * @desc If available, checks in canvas painter
  246. * @private */
  247. isTooltipAllowed() {
  248. const src = this.getCanvPainter() || this;
  249. return src.tooltip_allowed;
  250. }
  251. /** @summary change tooltip allowed flag
  252. * @param {boolean|string} [on = true] set tooltip allowed state or 'toggle'
  253. * @private */
  254. setTooltipAllowed(on = true) {
  255. const src = this.getCanvPainter() || this;
  256. src.tooltip_allowed = (on === 'toggle') ? !src.tooltip_allowed : on;
  257. }
  258. /** @summary Checks if draw elements were resized and drawing should be updated.
  259. * @desc Redirects to {@link TPadPainter#checkCanvasResize}
  260. * @private */
  261. checkResize(arg) {
  262. return this.getCanvPainter()?.checkCanvasResize(arg);
  263. }
  264. /** @summary removes <g> element with object drawing
  265. * @desc generic method to delete all graphical elements, associated with the painter
  266. * @protected */
  267. removeG() {
  268. this.draw_g?.remove();
  269. delete this.draw_g;
  270. }
  271. /** @summary Returns created <g> element used for object drawing
  272. * @desc Element should be created by {@link ObjectPainter#createG}
  273. * @protected */
  274. getG() { return this.draw_g; }
  275. /** @summary (re)creates svg:g element for object drawings
  276. * @desc either one attach svg:g to pad primitives (default)
  277. * or svg:g element created in specified frame layer ('main_layer' will be used when true specified)
  278. * @param {boolean|string} [frame_layer] - when specified, <g> element will be created inside frame layer, otherwise in the pad
  279. * @protected */
  280. createG(frame_layer, use_a = false) {
  281. let layer;
  282. if (frame_layer === 'frame2d') {
  283. const fp = this.getFramePainter();
  284. frame_layer = fp && !fp.mode3d;
  285. }
  286. if (frame_layer) {
  287. const frame = this.getFrameSvg();
  288. if (frame.empty()) {
  289. console.error('Not found frame to create g element inside');
  290. return frame;
  291. }
  292. if (!isStr(frame_layer)) frame_layer = 'main_layer';
  293. layer = frame.selectChild('.' + frame_layer);
  294. } else
  295. layer = this.getLayerSvg('primitives_layer');
  296. if (this.draw_g && this.draw_g.node().parentNode !== layer.node()) {
  297. console.log('g element changes its layer!!');
  298. this.removeG();
  299. }
  300. if (this.draw_g) {
  301. // clear all elements, keep g element on its place
  302. this.draw_g.selectAll('*').remove();
  303. } else {
  304. this.draw_g = layer.append(use_a ? 'svg:a' : 'svg:g');
  305. if (!frame_layer)
  306. layer.selectChildren('.most_upper_primitives').raise();
  307. }
  308. // set attributes for debugging, both should be there for opt out them later
  309. const clname = this.getClassName(), objname = this.getObjectName();
  310. if (objname || clname) {
  311. this.draw_g.attr('objname', (objname || 'name').replace(/[^\w]/g, '_'))
  312. .attr('objtype', (clname || 'type').replace(/[^\w]/g, '_'));
  313. }
  314. this.draw_g.property('in_frame', Boolean(frame_layer)); // indicates coordinate system
  315. return this.draw_g;
  316. }
  317. /** @summary Bring draw element to the front */
  318. bringToFront(check_online) {
  319. if (!this.draw_g) return;
  320. const prnt = this.draw_g.node().parentNode;
  321. prnt?.appendChild(this.draw_g.node());
  322. if (!check_online || !this.snapid) return;
  323. const pp = this.getPadPainter();
  324. if (!pp?.snapid) return;
  325. this.getCanvPainter()?.sendWebsocket('POPOBJ:'+JSON.stringify([pp.snapid.toString(), this.snapid.toString()]));
  326. }
  327. /** @summary Canvas main svg element
  328. * @return {object} d3 selection with canvas svg
  329. * @protected */
  330. getCanvSvg() { return this.selectDom().select('.root_canvas'); }
  331. /** @summary Pad svg element
  332. * @param {string} [pad_name] - pad name to select, if not specified - pad where object is drawn
  333. * @return {object} d3 selection with pad svg
  334. * @protected */
  335. getPadSvg(pad_name) {
  336. if (pad_name === undefined)
  337. pad_name = this.getPadName();
  338. const c = this.getCanvSvg();
  339. if (!pad_name || c.empty())
  340. return c;
  341. return c.select('.primitives_layer .__root_pad_' + pad_name);
  342. }
  343. /** @summary Assign secondary id
  344. * @private */
  345. setSecondaryId(primary, name) {
  346. primary._is_primary = true; // mark as primary, used later
  347. this.#primary_ref = new WeakRef(primary);
  348. this.#secondary_id = name;
  349. }
  350. /** @summary Returns secondary id
  351. * @private */
  352. getSecondaryId() { return this.#secondary_id; }
  353. /** @summary Check if this is secondary painter
  354. * @desc if primary painter provided - check if this really main for this
  355. * @private */
  356. isSecondary(primary) {
  357. if (!this.#primary_ref)
  358. return false;
  359. return !isObject(primary) ? true : this.#primary_ref.deref() === primary;
  360. }
  361. /** @summary Return primary object
  362. * @private */
  363. getPrimary() { return this.#primary_ref?.deref(); }
  364. /** @summary Provides identifier on server for requested sub-element */
  365. getSnapId(subelem) {
  366. if (!this.snapid)
  367. return '';
  368. return this.snapid.toString() + (subelem ? '#'+subelem : '');
  369. }
  370. /** @summary Method selects immediate layer under canvas/pad main element
  371. * @param {string} name - layer name, exits 'primitives_layer', 'btns_layer', 'info_layer'
  372. * @param {string} [pad_name] - pad name; current pad name used by default
  373. * @protected */
  374. getLayerSvg(name, pad_name) {
  375. let svg = this.getPadSvg(pad_name);
  376. if (svg.empty()) return svg;
  377. if (name.indexOf('prim#') === 0) {
  378. svg = svg.selectChild('.primitives_layer');
  379. name = name.slice(5);
  380. }
  381. return svg.selectChild('.' + name);
  382. }
  383. /** @summary Method selects current pad name
  384. * @param {string} [new_name] - when specified, new current pad name will be configured
  385. * @return {string} previous selected pad or actual pad when new_name not specified
  386. * @private
  387. * @deprecated to be removed in v8 */
  388. selectCurrentPad() {
  389. console.warn('selectCurrentPad is deprecated, will be removed in v8');
  390. return '';
  391. }
  392. /** @summary returns pad painter
  393. * @param {string} [pad_name] pad name or use current pad by default
  394. * @protected */
  395. getPadPainter(pad_name) {
  396. const elem = this.getPadSvg(isStr(pad_name) ? pad_name : undefined);
  397. return elem.empty() ? null : elem.property('pad_painter');
  398. }
  399. /** @summary returns canvas painter
  400. * @protected */
  401. getCanvPainter() {
  402. const elem = this.getCanvSvg();
  403. return elem.empty() ? null : elem.property('pad_painter');
  404. }
  405. /** @summary Return functor, which can convert x and y coordinates into pixels, used for drawing in the pad
  406. * @desc X and Y coordinates can be converted by calling func.x(x) and func.y(y)
  407. * Only can be used for painting in the pad, means CreateG() should be called without arguments
  408. * @param {boolean} isndc - if NDC coordinates will be used
  409. * @param {boolean} [noround] - if set, return coordinates will not be rounded
  410. * @param {boolean} [use_frame_coordinates] - use frame coordinates even when drawing on the pad
  411. * @protected */
  412. getAxisToSvgFunc(isndc, nornd, use_frame_coordinates) {
  413. const func = { isndc, nornd },
  414. use_frame = this.draw_g?.property('in_frame');
  415. if (use_frame || (use_frame_coordinates && !isndc))
  416. func.fp = this.getFramePainter();
  417. if (func.fp?.grx && func.fp?.gry) {
  418. func.x0 = (use_frame_coordinates && !isndc) ? func.fp.getFrameX() : 0;
  419. func.y0 = (use_frame_coordinates && !isndc) ? func.fp.getFrameY() : 0;
  420. if (nornd) {
  421. func.x = function(x) { return this.x0 + this.fp.grx(x); };
  422. func.y = function(y) { return this.y0 + this.fp.gry(y); };
  423. } else {
  424. func.x = function(x) { return this.x0 + Math.round(this.fp.grx(x)); };
  425. func.y = function(y) { return this.y0 + Math.round(this.fp.gry(y)); };
  426. }
  427. } else if (!use_frame) {
  428. const pp = this.getPadPainter();
  429. func.pad = isndc ? null : pp?.getRootPad(true); // need for NDC conversion
  430. func.padw = pp?.getPadWidth() ?? 10;
  431. func.x = function(value) {
  432. if (this.pad) {
  433. if (this.pad.fLogx)
  434. value = (value > 0) ? Math.log10(value) : this.pad.fUxmin;
  435. value = (value - this.pad.fX1) / (this.pad.fX2 - this.pad.fX1);
  436. }
  437. value *= this.padw;
  438. return this.nornd ? value : Math.round(value);
  439. };
  440. func.padh = pp?.getPadHeight() ?? 10;
  441. func.y = function(value) {
  442. if (this.pad) {
  443. if (this.pad.fLogy)
  444. value = (value > 0) ? Math.log10(value) : this.pad.fUymin;
  445. value = (value - this.pad.fY1) / (this.pad.fY2 - this.pad.fY1);
  446. }
  447. value = (1 - value) * this.padh;
  448. return this.nornd ? value : Math.round(value);
  449. };
  450. } else {
  451. console.error(`Problem to create functor for ${this.getClassName()}`);
  452. func.x = () => 0;
  453. func.y = () => 0;
  454. }
  455. return func;
  456. }
  457. /** @summary Converts x or y coordinate into pad SVG coordinates.
  458. * @desc Only can be used for painting in the pad, means CreateG() should be called without arguments
  459. * @param {string} axis - name like 'x' or 'y'
  460. * @param {number} value - axis value to convert.
  461. * @param {boolean} ndc - is value in NDC coordinates
  462. * @param {boolean} [noround] - skip rounding
  463. * @return {number} value of requested coordinates
  464. * @protected */
  465. axisToSvg(axis, value, ndc, noround) {
  466. const func = this.getAxisToSvgFunc(ndc, noround);
  467. return func[axis](value);
  468. }
  469. /** @summary Converts pad SVG x or y coordinates into axis values.
  470. * @desc Reverse transformation for {@link ObjectPainter#axisToSvg}
  471. * @param {string} axis - name like 'x' or 'y'
  472. * @param {number} coord - graphics coordinate.
  473. * @param {boolean} ndc - kind of return value
  474. * @return {number} value of requested coordinates
  475. * @protected */
  476. svgToAxis(axis, coord, ndc) {
  477. const use_frame = this.draw_g?.property('in_frame');
  478. if (use_frame)
  479. return this.getFramePainter()?.revertAxis(axis, coord) ?? 0;
  480. const pp = this.getPadPainter(),
  481. pad = (ndc || !pp) ? null : pp.getRootPad(true);
  482. let value = !pp ? 0 : ((axis === 'y') ? (1 - coord / pp.getPadHeight()) : coord / pp.getPadWidth());
  483. if (pad) {
  484. if (axis === 'y') {
  485. value = pad.fY1 + value * (pad.fY2 - pad.fY1);
  486. if (pad.fLogy) value = Math.pow(10, value);
  487. } else {
  488. value = pad.fX1 + value * (pad.fX2 - pad.fX1);
  489. if (pad.fLogx) value = Math.pow(10, value);
  490. }
  491. }
  492. return value;
  493. }
  494. /** @summary Returns svg element for the frame in current pad
  495. * @protected */
  496. getFrameSvg(pad_name) {
  497. const layer = this.getLayerSvg('primitives_layer', pad_name);
  498. if (layer.empty()) return layer;
  499. let node = layer.node().firstChild;
  500. while (node) {
  501. const elem = d3_select(node);
  502. if (elem.classed('root_frame')) return elem;
  503. node = node.nextSibling;
  504. }
  505. return d3_select(null);
  506. }
  507. /** @summary Returns frame painter for current pad
  508. * @desc Pad has direct reference on frame if any
  509. * @protected */
  510. getFramePainter() {
  511. return this.getPadPainter()?.getFramePainter();
  512. }
  513. /** @summary Returns painter for main object on the pad.
  514. * @desc Typically it is first histogram drawn on the pad and which draws frame axes
  515. * But it also can be special use-case as TASImage or TGraphPolargram
  516. * @param {boolean} [not_store] - if true, prevent temporary storage of main painter reference
  517. * @protected */
  518. getMainPainter(not_store) {
  519. let res = this.#main_painter?.deref();
  520. if (!res) {
  521. const pp = this.getPadPainter();
  522. res = pp ? pp.getMainPainter() : this.getTopPainter();
  523. this.#main_painter = not_store || !res ? null : new WeakRef(res);
  524. }
  525. return res || null;
  526. }
  527. /** @summary Returns true if this is main painter
  528. * @protected */
  529. isMainPainter() { return this === this.getMainPainter(); }
  530. /** @summary Assign this as main painter on the pad
  531. * @desc Main painter typically responsible for axes drawing
  532. * Should not be used by pad/canvas painters, but rather by objects which are drawing axis
  533. * @protected */
  534. setAsMainPainter(force) {
  535. const pp = this.getPadPainter();
  536. if (!pp)
  537. this.setTopPainter(); // fallback on BasePainter method
  538. else
  539. pp.setMainPainter(this, force);
  540. }
  541. /** @summary Add painter to pad list of painters
  542. * @desc Normally called from {@link ensureTCanvas} function when new painter is created
  543. * @protected */
  544. addToPadPrimitives() {
  545. const pp = this.getPadPainter();
  546. if (!pp || (pp === this))
  547. return null;
  548. if (pp.painters.indexOf(this) < 0)
  549. pp.painters.push(this);
  550. return pp;
  551. }
  552. /** @summary Remove painter from pad list of painters
  553. * @protected */
  554. removeFromPadPrimitives() {
  555. const pp = this.getPadPainter();
  556. if (!pp || (pp === this))
  557. return false;
  558. const k = pp.painters.indexOf(this);
  559. if (k >= 0)
  560. pp.painters.splice(k, 1);
  561. return true;
  562. }
  563. /** @summary Creates marker attributes object
  564. * @desc Can be used to produce markers in painter.
  565. * See {@link TAttMarkerHandler} for more info.
  566. * Instance assigned as this.markeratt data member, recognized by GED editor
  567. * @param {object} args - either TAttMarker or see arguments of {@link TAttMarkerHandler}
  568. * @return {object} created handler
  569. * @protected */
  570. createAttMarker(args) {
  571. if (args === undefined)
  572. args = { attr: this.getObject() };
  573. else if (!isObject(args))
  574. args = { std: true };
  575. else if (args.fMarkerColor !== undefined && args.fMarkerStyle !== undefined && args.fMarkerSize !== undefined)
  576. args = { attr: args, std: false };
  577. if (args.std === undefined)
  578. args.std = true;
  579. if (args.painter === undefined)
  580. args.painter = this;
  581. let handler = args.std ? this.markeratt : null;
  582. if (!handler)
  583. handler = new TAttMarkerHandler(args);
  584. else if (!handler.changed || args.force)
  585. handler.setArgs(args);
  586. if (args.std)
  587. this.markeratt = handler;
  588. return handler;
  589. }
  590. /** @summary Creates line attributes object.
  591. * @desc Can be used to produce lines in painter.
  592. * See {@link TAttLineHandler} for more info.
  593. * Instance assigned as this.lineatt data member, recognized by GED editor
  594. * @param {object} args - either TAttLine or see constructor arguments of {@link TAttLineHandler}
  595. * @protected */
  596. createAttLine(args) {
  597. if (args === undefined)
  598. args = { attr: this.getObject() };
  599. else if (!isObject(args))
  600. args = { std: true };
  601. else if (args.fLineColor !== undefined && args.fLineStyle !== undefined && args.fLineWidth !== undefined)
  602. args = { attr: args, std: false };
  603. if (args.std === undefined)
  604. args.std = true;
  605. if (args.painter === undefined)
  606. args.painter = this;
  607. let handler = args.std ? this.lineatt : null;
  608. if (!handler)
  609. handler = new TAttLineHandler(args);
  610. else if (!handler.changed || args.force)
  611. handler.setArgs(args);
  612. if (args.std)
  613. this.lineatt = handler;
  614. return handler;
  615. }
  616. /** @summary Creates text attributes object.
  617. * @param {object} args - either TAttText or see constructor arguments of {@link TAttTextHandler}
  618. * @protected */
  619. createAttText(args) {
  620. if (args === undefined)
  621. args = { attr: this.getObject() };
  622. else if (!isObject(args))
  623. args = { std: true };
  624. else if (args.fTextFont !== undefined && args.fTextSize !== undefined && args.fTextColor !== undefined)
  625. args = { attr: args, std: false };
  626. if (args.std === undefined)
  627. args.std = true;
  628. if (args.painter === undefined)
  629. args.painter = this;
  630. let handler = args.std ? this.textatt : null;
  631. if (!handler)
  632. handler = new TAttTextHandler(args);
  633. else if (!handler.changed || args.force)
  634. handler.setArgs(args);
  635. if (args.std)
  636. this.textatt = handler;
  637. return handler;
  638. }
  639. /** @summary Creates fill attributes object.
  640. * @desc Method dedicated to create fill attributes, bound to canvas SVG
  641. * otherwise newly created patters will not be usable in the canvas
  642. * See {@link TAttFillHandler} for more info.
  643. * Instance assigned as this.fillatt data member, recognized by GED editors
  644. * @param {object} [args] - for special cases one can specify TAttFill as args or number of parameters
  645. * @param {boolean} [args.std = true] - this is standard fill attribute for object and should be used as this.fillatt
  646. * @param {object} [args.attr = null] - object, derived from TAttFill
  647. * @param {number} [args.pattern = undefined] - integer index of fill pattern
  648. * @param {number} [args.color = undefined] - integer index of fill color
  649. * @param {string} [args.color_as_svg = undefined] - color will be specified as SVG string, not as index from color palette
  650. * @param {number} [args.kind = undefined] - some special kind which is handled differently from normal patterns
  651. * @return created handle
  652. * @protected */
  653. createAttFill(args) {
  654. if (args === undefined)
  655. args = { attr: this.getObject() };
  656. else if (!isObject(args))
  657. args = { std: true };
  658. else if (args._typename && args.fFillColor !== undefined && args.fFillStyle !== undefined)
  659. args = { attr: args, std: false };
  660. if (args.std === undefined)
  661. args.std = true;
  662. if (args.painter === undefined)
  663. args.painter = this;
  664. let handler = args.std ? this.fillatt : null;
  665. if (!args.svg)
  666. args.svg = this.getCanvSvg();
  667. if (!handler)
  668. handler = new TAttFillHandler(args);
  669. else if (!handler.changed || args.force)
  670. handler.setArgs(args);
  671. if (args.std)
  672. this.fillatt = handler;
  673. return handler;
  674. }
  675. /** @summary call function for each painter in the pad
  676. * @desc Iterate over all known painters
  677. * @private */
  678. forEachPainter(userfunc, kind) {
  679. // iterate over all painters from pad list
  680. const pp = this.getPadPainter();
  681. if (pp)
  682. pp.forEachPainterInPad(userfunc, kind);
  683. else {
  684. const painter = this.getTopPainter();
  685. if (painter && (kind !== 'pads'))
  686. userfunc(painter);
  687. }
  688. }
  689. /** @summary indicate that redraw was invoked via interactive action (like context menu or zooming)
  690. * @desc Use to catch such action by GED and by server-side
  691. * @return {Promise} when completed
  692. * @private */
  693. async interactiveRedraw(arg, info, subelem) {
  694. let reason, res;
  695. if (isStr(info) && info.indexOf('exec:'))
  696. reason = info;
  697. if (arg === 'pad')
  698. res = this.redrawPad(reason);
  699. else if (arg !== false)
  700. res = this.redraw(reason);
  701. return getPromise(res).then(() => {
  702. if (arg === 'attribute')
  703. return this.getPadPainter()?.redrawLegend();
  704. }).then(() => {
  705. // inform GED that something changes
  706. const canp = this.getCanvPainter();
  707. if (isFunc(canp?.producePadEvent))
  708. canp.producePadEvent('redraw', this.getPadPainter(), this, null, subelem);
  709. // inform server that draw options changes
  710. if (isFunc(canp?.processChanges))
  711. canp.processChanges(info, this, subelem);
  712. return this;
  713. });
  714. }
  715. /** @summary Redraw all objects in the current pad
  716. * @param {string} [reason] - like 'resize' or 'zoom'
  717. * @return {Promise} when pad redraw completed
  718. * @protected */
  719. async redrawPad(reason) {
  720. return this.getPadPainter()?.redrawPad(reason) ?? false;
  721. }
  722. /** @summary execute selected menu command, either locally or remotely
  723. * @private */
  724. executeMenuCommand(method) {
  725. if (method.fName === 'Inspect')
  726. // primitive inspector, keep it here
  727. return this.showInspector();
  728. return false;
  729. }
  730. /** @summary Invoke method for object via WebCanvas functionality
  731. * @desc Requires that painter marked with object identifier (this.snapid) or identifier provided as second argument
  732. * Canvas painter should exists and in non-readonly mode
  733. * Execution string can look like 'Print()'.
  734. * Many methods call can be chained with 'Print();;Update();;Clear()'
  735. * @private */
  736. submitCanvExec(exec, snapid) {
  737. if (!exec || !isStr(exec)) return;
  738. const canp = this.getCanvPainter();
  739. if (isFunc(canp?.submitExec))
  740. canp.submitExec(this, exec, snapid);
  741. }
  742. /** @summary remove all created draw attributes
  743. * @protected */
  744. deleteAttr() {
  745. delete this.lineatt;
  746. delete this.fillatt;
  747. delete this.markeratt;
  748. }
  749. /** @summary Show object in inspector for provided object
  750. * @protected */
  751. showInspector(/* opt */) {
  752. return false;
  753. }
  754. /** @summary Fill context menu for the object
  755. * @private */
  756. fillContextMenu(menu) {
  757. const cl = this.getClassName(),
  758. name = this.getObjectName(),
  759. p = cl.lastIndexOf('::'),
  760. cl0 = (p > 0) ? cl.slice(p+2) : cl,
  761. hdr = (cl0 && name) ? `${cl0}:${name}` : (cl0 || name || 'object'),
  762. url = cl ? `${urlClassPrefix}${cl.replaceAll('::', '_1_1')}.html` : '';
  763. menu.header(hdr, url);
  764. const size0 = menu.size();
  765. if (isFunc(this.fillContextMenuItems))
  766. this.fillContextMenuItems(menu);
  767. if ((menu.size() > size0) && this.showInspector('check'))
  768. menu.add('Inspect', this.showInspector);
  769. menu.addAttributesMenu(this);
  770. return menu.size() > size0;
  771. }
  772. /** @summary shows objects status
  773. * @desc Either used canvas painter method or globally assigned
  774. * When no parameters are specified, just basic object properties are shown
  775. * @private */
  776. showObjectStatus(name, title, info, info2) {
  777. let cp = this.getCanvPainter();
  778. if (!isFunc(cp?.showCanvasStatus))
  779. cp = null;
  780. if (!cp && !isFunc(internals.showStatus))
  781. return false;
  782. if (this.enlargeMain('state') === 'on')
  783. return false;
  784. if ((name === undefined) && (title === undefined)) {
  785. const obj = this.getObject();
  786. if (!obj) return;
  787. name = this.getItemName() || obj.fName;
  788. title = obj.fTitle || obj._typename;
  789. info = obj._typename;
  790. }
  791. if (cp)
  792. cp.showCanvasStatus(name, title, info, info2);
  793. else
  794. internals.showStatus(name, title, info, info2);
  795. }
  796. /** @summary Redraw object
  797. * @desc Basic method, should be reimplemented in all derived objects
  798. * for the case when drawing should be repeated
  799. * @abstract
  800. * @protected */
  801. redraw(/* reason */) {}
  802. /** @summary Start text drawing
  803. * @desc required before any text can be drawn
  804. * @param {number} font_face - font id as used in ROOT font attributes
  805. * @param {number} font_size - font size as used in ROOT font attributes
  806. * @param {object} [draw_g] - element where text drawn, by default using main object <g> element
  807. * @param {number} [max_font_size] - maximal font size, used when text can be scaled
  808. * @protected */
  809. startTextDrawing(font_face, font_size, draw_g, max_font_size, can_async) {
  810. if (!draw_g) draw_g = this.draw_g;
  811. if (!draw_g || draw_g.empty())
  812. return false;
  813. const font = (font_size === 'font') ? font_face : new FontHandler(font_face, font_size);
  814. if (can_async && font.needLoad())
  815. return font;
  816. font.setPainter(this); // may be required when custom font is used
  817. draw_g.call(font.func);
  818. draw_g.property('draw_text_completed', false) // indicate that draw operations submitted
  819. .property('all_args', []) // array of all submitted args, makes easier to analyze them
  820. .property('text_font', font)
  821. .property('text_factor', 0)
  822. .property('max_text_width', 0) // keep maximal text width, use it later
  823. .property('max_font_size', max_font_size)
  824. .property('_fast_drawing', this.getPadPainter()?.isFastDrawing() ?? false);
  825. if (draw_g.property('_fast_drawing'))
  826. draw_g.property('_font_too_small', (max_font_size && (max_font_size < 5)) || (font.size < 4));
  827. return true;
  828. }
  829. /** @summary Start async text drawing
  830. * @return {Promise} for loading of font if necessary
  831. * @private */
  832. async startTextDrawingAsync(font_face, font_size, draw_g, max_font_size) {
  833. const font = this.startTextDrawing(font_face, font_size, draw_g, max_font_size, true);
  834. if ((font === true) || (font === false))
  835. return font;
  836. return font.load().then(res => {
  837. if (!res)
  838. return false;
  839. return this.startTextDrawing(font, 'font', draw_g, max_font_size);
  840. });
  841. }
  842. /** @summary Apply scaling factor to all drawn text in the <g> element
  843. * @desc Can be applied at any time before finishTextDrawing is called - even in the postprocess callbacks of text draw
  844. * @param {number} factor - scaling factor
  845. * @param {object} [draw_g] - drawing element for the text
  846. * @protected */
  847. scaleTextDrawing(factor, draw_g) {
  848. if (!draw_g) draw_g = this.draw_g;
  849. if (!draw_g || draw_g.empty()) return;
  850. if (factor && (factor > draw_g.property('text_factor')))
  851. draw_g.property('text_factor', factor);
  852. }
  853. /** @summary Analyze if all text draw operations are completed
  854. * @private */
  855. #checkAllTextDrawing(draw_g, resolveFunc, try_optimize) {
  856. const all_args = draw_g.property('all_args') || [];
  857. let missing = 0;
  858. all_args.forEach(arg => { if (!arg.ready) missing++; });
  859. if (missing > 0) {
  860. if (isFunc(resolveFunc)) {
  861. draw_g.node().textResolveFunc = resolveFunc;
  862. draw_g.node().try_optimize = try_optimize;
  863. }
  864. return;
  865. }
  866. draw_g.property('all_args', null); // clear all_args property
  867. // adjust font size (if there are normal text)
  868. const f = draw_g.property('text_factor'),
  869. font = draw_g.property('text_font'),
  870. max_sz = draw_g.property('max_font_size');
  871. let font_size = font.size, any_text = false, only_text = true;
  872. if ((f > 0) && ((f < 0.95) || (f > 1.05)))
  873. font.size = Math.max(1, Math.floor(font.size / f));
  874. if (max_sz && (font.size > max_sz))
  875. font.size = max_sz;
  876. if (font.size !== font_size) {
  877. draw_g.call(font.func);
  878. font_size = font.size;
  879. }
  880. all_args.forEach(arg => {
  881. if (arg.mj_node && arg.mj_func) {
  882. const svg = arg.mj_node.select('svg'); // MathJax svg
  883. arg.mj_func(this, arg.mj_node, svg, arg, font_size, f);
  884. delete arg.mj_node; // remove reference
  885. only_text = false;
  886. } else if (arg.txt_g)
  887. only_text = false;
  888. });
  889. if (!resolveFunc) {
  890. resolveFunc = draw_g.node().textResolveFunc;
  891. try_optimize = draw_g.node().try_optimize;
  892. delete draw_g.node().textResolveFunc;
  893. delete draw_g.node().try_optimize;
  894. }
  895. const optimize_arr = (try_optimize && only_text) ? [] : null;
  896. // now process text and latex drawings
  897. all_args.forEach(arg => {
  898. let txt, is_txt, scale = 1;
  899. if (arg.txt_node) {
  900. txt = arg.txt_node;
  901. delete arg.txt_node;
  902. is_txt = true;
  903. if (optimize_arr !== null) optimize_arr.push(txt);
  904. } else if (arg.txt_g) {
  905. txt = arg.txt_g;
  906. delete arg.txt_g;
  907. is_txt = false;
  908. } else
  909. return;
  910. txt.attr('visibility', null);
  911. any_text = true;
  912. if (arg.width) {
  913. // adjust x position when scale into specified rectangle
  914. if (arg.align[0] === 'middle')
  915. arg.x += arg.width / 2;
  916. else if (arg.align[0] === 'end')
  917. arg.x += arg.width;
  918. }
  919. if (arg.height) {
  920. if (arg.align[1].indexOf('bottom') === 0)
  921. arg.y += arg.height;
  922. else if (arg.align[1] === 'middle')
  923. arg.y += arg.height / 2;
  924. }
  925. let dx = 0, dy = 0;
  926. if (is_txt) {
  927. // handle simple text drawing
  928. if (isNodeJs()) {
  929. if (arg.scale && (f > 0)) { arg.box.width *= 1/f; arg.box.height *= 1/f; }
  930. } else if (!arg.plain && !arg.fast) {
  931. // exact box dimension only required when complex text was build
  932. arg.box = getElementRect(txt, 'bbox');
  933. }
  934. if (arg.plain) {
  935. txt.attr('text-anchor', arg.align[0]);
  936. if (arg.align[1] === 'top')
  937. txt.attr('dy', '.8em');
  938. else if (arg.align[1] === 'middle') {
  939. // if (isNodeJs()) txt.attr('dy', '.4em'); else // old workaround for node.js
  940. txt.attr('dominant-baseline', 'middle');
  941. }
  942. } else {
  943. txt.attr('text-anchor', 'start');
  944. dx = ((arg.align[0] === 'middle') ? -0.5 : ((arg.align[0] === 'end') ? -1 : 0)) * arg.box.width;
  945. dy = ((arg.align[1] === 'top') ? (arg.top_shift || 1) : (arg.align[1] === 'middle') ? (arg.mid_shift || 0.5) : 0) * arg.box.height;
  946. }
  947. } else if (arg.text_rect) {
  948. // handle latex drawing
  949. const box = arg.text_rect;
  950. scale = (f > 0) && (Math.abs(1-f) > 0.01) ? 1/f : 1;
  951. dx = ((arg.align[0] === 'middle') ? -0.5 : ((arg.align[0] === 'end') ? -1 : 0)) * box.width * scale;
  952. if (arg.align[1] === 'top')
  953. dy = -box.y1*scale;
  954. else if (arg.align[1] === 'bottom')
  955. dy = -box.y2*scale;
  956. else if (arg.align[1] === 'middle')
  957. dy = -0.5*(box.y1 + box.y2)*scale;
  958. } else
  959. console.error('text rect not calcualted - please check code');
  960. if (!arg.rotate) {
  961. arg.x += dx;
  962. arg.y += dy;
  963. dx = dy = 0;
  964. }
  965. // use translate and then rotate to avoid complex sign calculations
  966. let trans = makeTranslate(Math.round(arg.x), Math.round(arg.y)) || '';
  967. const dtrans = makeTranslate(Math.round(dx), Math.round(dy)),
  968. append = aaa => { if (trans) trans += ' '; trans += aaa; };
  969. if (arg.rotate)
  970. append(`rotate(${Math.round(arg.rotate)})`);
  971. if (scale !== 1)
  972. append(`scale(${scale.toFixed(3)})`);
  973. if (dtrans)
  974. append(dtrans);
  975. if (trans)
  976. txt.attr('transform', trans);
  977. });
  978. // when no any normal text drawn - remove font attributes
  979. if (!any_text)
  980. font.clearFont(draw_g);
  981. if ((optimize_arr !== null) && (optimize_arr.length > 1)) {
  982. ['fill', 'text-anchor'].forEach(name => {
  983. let first = optimize_arr[0].attr(name);
  984. optimize_arr.forEach(txt_node => {
  985. const value = txt_node.attr(name);
  986. if (!value || (value !== first)) first = undefined;
  987. });
  988. if (first) {
  989. draw_g.attr(name, first);
  990. optimize_arr.forEach(txt_node => { txt_node.attr(name, null); });
  991. }
  992. });
  993. }
  994. // if specified, call resolve function
  995. if (resolveFunc) resolveFunc(this); // IMPORTANT - return painter, may use in draw methods
  996. }
  997. /** @summary Post-process plain text drawing
  998. * @private */
  999. #postprocessDrawText(arg, txt_node) {
  1000. // complete rectangle with very rough size estimations
  1001. arg.box = !isNodeJs() && !settings.ApproxTextSize && !arg.fast
  1002. ? getElementRect(txt_node, 'bbox')
  1003. : (arg.text_rect || { height: Math.round(1.15 * arg.font_size), width: approximateLabelWidth(arg.text, arg.font, arg.font_size) });
  1004. txt_node.attr('visibility', 'hidden'); // hide elements until text drawing is finished
  1005. if (arg.box.width > arg.draw_g.property('max_text_width'))
  1006. arg.draw_g.property('max_text_width', arg.box.width);
  1007. if (arg.scale)
  1008. this.scaleTextDrawing(Math.max(1.05 * arg.box.width / arg.width, arg.box.height / arg.height), arg.draw_g);
  1009. arg.result_width = arg.box.width;
  1010. arg.result_height = arg.box.height;
  1011. if (isFunc(arg.post_process))
  1012. arg.post_process(this);
  1013. return arg.box.width;
  1014. }
  1015. /** @summary Draw text
  1016. * @desc The only legal way to draw text, support plain, latex and math text output
  1017. * @param {object} arg - different text draw options
  1018. * @param {string} arg.text - text to draw
  1019. * @param {number} [arg.align = 12] - int value like 12 or 31
  1020. * @param {string} [arg.align = undefined] - end;bottom
  1021. * @param {number} [arg.x = 0] - x position
  1022. * @param {number} [arg.y = 0] - y position
  1023. * @param {number} [arg.width] - when specified, adjust font size in the specified box
  1024. * @param {number} [arg.height] - when specified, adjust font size in the specified box
  1025. * @param {boolean} [arg.scale = true] - scale into draw box when width and height parameters are specified
  1026. * @param {number} [arg.latex] - 0 - plain text, 1 - normal TLatex, 2 - math
  1027. * @param {string} [arg.color=black] - text color
  1028. * @param {number} [arg.rotate] - rotation angle
  1029. * @param {number} [arg.font_size] - fixed font size
  1030. * @param {object} [arg.draw_g] - element where to place text, if not specified central draw_g container is used
  1031. * @param {function} [arg.post_process] - optional function called when specified text is drawn
  1032. * @protected */
  1033. drawText(arg) {
  1034. if (!arg.text)
  1035. arg.text = '';
  1036. arg.draw_g = arg.draw_g || this.draw_g;
  1037. if (!arg.draw_g || arg.draw_g.empty())
  1038. return;
  1039. const font = arg.draw_g.property('text_font');
  1040. arg.font = font; // use in latex conversion
  1041. if (font) {
  1042. arg.color = arg.color || font.color;
  1043. arg.align = arg.align || font.align;
  1044. arg.rotate = arg.rotate || font.angle;
  1045. }
  1046. let align = ['start', 'middle'];
  1047. if (isStr(arg.align)) {
  1048. align = arg.align.split(';');
  1049. if (align.length === 1)
  1050. align.push('middle');
  1051. } else if (typeof arg.align === 'number') {
  1052. if ((arg.align / 10) >= 3)
  1053. align[0] = 'end';
  1054. else if ((arg.align / 10) >= 2)
  1055. align[0] = 'middle';
  1056. if ((arg.align % 10) === 0)
  1057. align[1] = 'bottom';
  1058. else if ((arg.align % 10) === 1)
  1059. align[1] = 'bottom-base';
  1060. else if ((arg.align % 10) === 3)
  1061. align[1] = 'top';
  1062. } else if (isObject(arg.align) && (arg.align.length === 2))
  1063. align = arg.align;
  1064. if (arg.latex === undefined)
  1065. arg.latex = 1; // 0: text, 1: latex, 2: math
  1066. arg.align = align;
  1067. arg.x = arg.x || 0;
  1068. arg.y = arg.y || 0;
  1069. if (arg.scale !== false)
  1070. arg.scale = arg.width && arg.height && !arg.font_size;
  1071. arg.width = arg.width || 0;
  1072. arg.height = arg.height || 0;
  1073. if (arg.draw_g.property('_fast_drawing')) {
  1074. if (arg.scale) {
  1075. // area too small - ignore such drawing
  1076. if (arg.height < 4)
  1077. return 0;
  1078. } else if (arg.font_size) {
  1079. // font size too small
  1080. if (arg.font_size < 4)
  1081. return 0;
  1082. } else if (arg.draw_g.property('_font_too_small')) {
  1083. // configure font is too small - ignore drawing
  1084. return 0;
  1085. }
  1086. }
  1087. // include drawing into list of all args
  1088. arg.draw_g.property('all_args').push(arg);
  1089. arg.ready = false; // indicates if drawing is ready for post-processing
  1090. let use_mathjax = (arg.latex === 2);
  1091. const cl = constants.Latex;
  1092. if (arg.latex === 1) {
  1093. use_mathjax = (settings.Latex === cl.AlwaysMathJax) ||
  1094. ((settings.Latex === cl.MathJax) && arg.text.match(/[#{\\]/g)) ||
  1095. arg.text.match(/[\\]/g);
  1096. }
  1097. if (!use_mathjax || arg.nomathjax) {
  1098. arg.txt_node = arg.draw_g.append('svg:text');
  1099. if (arg.color)
  1100. arg.txt_node.attr('fill', arg.color);
  1101. if (arg.font_size)
  1102. arg.txt_node.attr('font-size', arg.font_size);
  1103. else
  1104. arg.font_size = font.size;
  1105. arg.plain = !arg.latex || (settings.Latex === cl.Off) || (settings.Latex === cl.Symbols);
  1106. arg.simple_latex = arg.latex && (settings.Latex === cl.Symbols);
  1107. if (!arg.plain || arg.simple_latex || arg.font?.isSymbol) {
  1108. if (arg.simple_latex || isPlainText(arg.text) || arg.plain) {
  1109. arg.simple_latex = true;
  1110. producePlainText(this, arg.txt_node, arg);
  1111. } else {
  1112. arg.txt_node.remove(); // just remove text node
  1113. delete arg.txt_node;
  1114. arg.txt_g = arg.draw_g.append('svg:g');
  1115. produceLatex(this, arg.txt_g, arg);
  1116. }
  1117. arg.ready = true;
  1118. this.#postprocessDrawText(arg, arg.txt_g || arg.txt_node);
  1119. if (arg.draw_g.property('draw_text_completed'))
  1120. this.#checkAllTextDrawing(arg.draw_g); // check if all other elements are completed
  1121. return 0;
  1122. }
  1123. arg.plain = true;
  1124. arg.txt_node.text(arg.text);
  1125. arg.ready = true;
  1126. return this.#postprocessDrawText(arg, arg.txt_node);
  1127. }
  1128. arg.mj_node = arg.draw_g.append('svg:g').attr('visibility', 'hidden'); // hide text until drawing is finished
  1129. produceMathjax(this, arg.mj_node, arg).then(() => {
  1130. arg.ready = true;
  1131. if (arg.draw_g.property('draw_text_completed'))
  1132. this.#checkAllTextDrawing(arg.draw_g);
  1133. });
  1134. return 0;
  1135. }
  1136. /** @summary Finish text drawing
  1137. * @desc Should be called to complete all text drawing operations
  1138. * @param {function} [draw_g] - <g> element for text drawing, this.draw_g used when not specified
  1139. * @return {Promise} when text drawing completed
  1140. * @protected */
  1141. async finishTextDrawing(draw_g, try_optimize) {
  1142. if (!draw_g)
  1143. draw_g = this.draw_g;
  1144. if (!draw_g || draw_g.empty())
  1145. return false;
  1146. draw_g.property('draw_text_completed', true); // mark that text drawing is completed
  1147. return new Promise(resolveFunc => {
  1148. this.#checkAllTextDrawing(draw_g, resolveFunc, try_optimize);
  1149. });
  1150. }
  1151. /** @summary Configure user-defined context menu for the object
  1152. * @desc fillmenu_func will be called when context menu is activated
  1153. * Arguments fillmenu_func are (menu,kind)
  1154. * First is menu object, second is object sub-element like axis 'x' or 'y'
  1155. * Function should return promise with menu when items are filled
  1156. * @param {function} fillmenu_func - function to fill custom context menu for object */
  1157. configureUserContextMenu(fillmenu_func) {
  1158. this.#user_context_menu = isFunc(fillmenu_func) ? fillmenu_func : undefined;
  1159. }
  1160. /** @summary Fill object menu in web canvas
  1161. * @private */
  1162. async fillObjectExecMenu(menu, kind) {
  1163. if (isFunc(this.#user_context_menu))
  1164. return this.#user_context_menu(menu, kind);
  1165. const canvp = this.getCanvPainter();
  1166. if (!this.snapid || !canvp || canvp?.isReadonly() || !canvp?.getWebsocket())
  1167. return menu;
  1168. function doExecMenu(arg) {
  1169. const execp = menu.exec_painter || this,
  1170. cp = execp.getCanvPainter(),
  1171. item = menu.exec_items[parseInt(arg)];
  1172. if (!item?.fName) return;
  1173. // this is special entry, produced by TWebMenuItem, which recognizes editor entries itself
  1174. if (item.fExec === 'Show:Editor') {
  1175. if (isFunc(cp?.activateGed))
  1176. cp.activateGed(execp);
  1177. return;
  1178. }
  1179. if (isFunc(cp?.executeObjectMethod) && cp.executeObjectMethod(execp, item, item.$execid))
  1180. return;
  1181. item.fClassName = execp.getClassName();
  1182. if ((item.$execid.indexOf('#x') > 0) || (item.$execid.indexOf('#y') > 0) || (item.$execid.indexOf('#z') > 0))
  1183. item.fClassName = clTAxis;
  1184. if (execp.executeMenuCommand(item))
  1185. return;
  1186. if (!item.$execid)
  1187. return;
  1188. if (!item.fArgs) {
  1189. return cp?.v7canvas ? cp.submitExec(execp, item.fExec, kind)
  1190. : execp.submitCanvExec(item.fExec, item.$execid);
  1191. }
  1192. menu.showMethodArgsDialog(item).then(args => {
  1193. if (!args || execp.executeMenuCommand(item, args))
  1194. return;
  1195. const exec = item.fExec.slice(0, item.fExec.length - 1) + args + ')';
  1196. if (cp?.v7canvas)
  1197. cp.submitExec(execp, exec, kind);
  1198. else
  1199. cp?.sendWebsocket(`OBJEXEC:${item.$execid}:${exec}`);
  1200. });
  1201. }
  1202. const doFillMenu = (_menu, _reqid, _resolveFunc, reply) => {
  1203. // avoid multiple call of the callback after timeout
  1204. if (menu._got_menu)
  1205. return;
  1206. menu._got_menu = true;
  1207. if (reply && (_reqid !== reply.fId))
  1208. console.error(`missmatch between request ${_reqid} and reply ${reply.fId} identifiers`);
  1209. menu.exec_items = reply?.fItems;
  1210. if (menu.exec_items?.length) {
  1211. if (_menu.size() > 0)
  1212. _menu.separator();
  1213. let lastclname;
  1214. for (let n = 0; n < menu.exec_items.length; ++n) {
  1215. const item = menu.exec_items[n];
  1216. item.$execid = reply.fId;
  1217. item.$menu = menu;
  1218. if (item.fClassName && lastclname && (lastclname !== item.fClassName)) {
  1219. _menu.endsub();
  1220. lastclname = '';
  1221. }
  1222. if (lastclname !== item.fClassName) {
  1223. lastclname = item.fClassName;
  1224. const p = lastclname.lastIndexOf('::'),
  1225. shortname = (p > 0) ? lastclname.slice(p+2) : lastclname;
  1226. _menu.sub(shortname.replace(/[<>]/g, '_'));
  1227. }
  1228. if ((item.fChecked === undefined) || (item.fChecked < 0))
  1229. _menu.add(item.fName, n, doExecMenu);
  1230. else
  1231. _menu.addchk(item.fChecked, item.fName, n, doExecMenu);
  1232. }
  1233. if (lastclname)
  1234. _menu.endsub();
  1235. }
  1236. _resolveFunc(_menu);
  1237. },
  1238. reqid = this.getSnapId(kind);
  1239. menu._got_menu = false;
  1240. // if menu painter differs from this, remember it for further usage
  1241. if (menu.painter)
  1242. menu.exec_painter = (menu.painter !== this) ? this : undefined;
  1243. return new Promise(resolveFunc => {
  1244. let did_resolve = false;
  1245. function handleResolve(res) {
  1246. if (did_resolve) return;
  1247. did_resolve = true;
  1248. resolveFunc(res);
  1249. }
  1250. // set timeout to avoid menu hanging
  1251. setTimeout(() => doFillMenu(menu, reqid, handleResolve), 2000);
  1252. canvp.submitMenuRequest(this, kind, reqid).then(lst => doFillMenu(menu, reqid, handleResolve, lst));
  1253. });
  1254. }
  1255. /** @summary Configure user-defined tooltip handler
  1256. * @desc Hook for the users to get tooltip information when mouse cursor moves over frame area
  1257. * Handler function will be called every time when new data is selected
  1258. * when mouse leave frame area, handler(null) will be called
  1259. * @param {function} handler - function called when tooltip is produced
  1260. * @param {number} [tmout = 100] - delay in ms before tooltip delivered */
  1261. configureUserTooltipHandler(handler, tmout = 100) {
  1262. if (!handler || !isFunc(handler)) {
  1263. this.#user_tooltip_handler = undefined;
  1264. this.#user_tooltip_timeout = undefined;
  1265. } else {
  1266. this.#user_tooltip_handler = handler;
  1267. this.#user_tooltip_timeout = tmout;
  1268. }
  1269. }
  1270. /** @summary Configure user-defined click handler
  1271. * @desc Function will be called every time when frame click was performed
  1272. * As argument, tooltip object with selected bins will be provided
  1273. * If handler function returns true, default handling of click will be disabled
  1274. * @param {function} handler - function called when mouse click is done */
  1275. configureUserClickHandler(handler) {
  1276. const fp = this.getFramePainter();
  1277. if (isFunc(fp?.configureUserClickHandler))
  1278. fp.configureUserClickHandler(handler);
  1279. }
  1280. /** @summary Configure user-defined dblclick handler
  1281. * @desc Function will be called every time when double click was called
  1282. * As argument, tooltip object with selected bins will be provided
  1283. * If handler function returns true, default handling of dblclick (unzoom) will be disabled
  1284. * @param {function} handler - function called when mouse double click is done */
  1285. configureUserDblclickHandler(handler) {
  1286. const fp = this.getFramePainter();
  1287. if (isFunc(fp?.configureUserDblclickHandler))
  1288. fp.configureUserDblclickHandler(handler);
  1289. }
  1290. /** @summary Check if user-defined tooltip function was configured
  1291. * @return {boolean} flag is user tooltip handler was configured */
  1292. hasUserTooltip() {
  1293. return isFunc(this.#user_tooltip_handler);
  1294. }
  1295. /** @summary Provide tooltips data to user-defined function
  1296. * @param {object} data - tooltip data
  1297. * @private */
  1298. provideUserTooltip(data) {
  1299. if (!this.hasUserTooltip())
  1300. return;
  1301. if (this.#user_tooltip_timeout <= 0)
  1302. return this.#user_tooltip_handler(data);
  1303. if (this.#user_toottip_handle) {
  1304. clearTimeout(this.#user_toottip_handle);
  1305. this.#user_toottip_handle = undefined;
  1306. }
  1307. if (!data)
  1308. return this.#user_tooltip_handler(data);
  1309. // only after timeout user function will be called
  1310. this.#user_toottip_handle = setTimeout(() => {
  1311. this.#user_toottip_handle = undefined;
  1312. if (this.#user_tooltip_handler)
  1313. this.#user_tooltip_handler(data);
  1314. }, this.#user_tooltip_timeout);
  1315. }
  1316. /** @summary Provide projection areas
  1317. * @param kind - 'X', 'Y', 'XY' or ''
  1318. * @private */
  1319. async provideSpecialDrawArea(kind) {
  1320. if (kind === this.#special_draw_area)
  1321. return true;
  1322. return this.getCanvPainter().toggleProjection(kind).then(() => {
  1323. this.#special_draw_area = kind;
  1324. return true;
  1325. });
  1326. }
  1327. /** @summary Draw in special projection areas
  1328. * @param obj - object to draw
  1329. * @param opt - draw option
  1330. * @param kind - '', 'X', 'Y'
  1331. * @private */
  1332. async drawInSpecialArea(obj, opt, kind) {
  1333. const canp = this.getCanvPainter();
  1334. if (this.#special_draw_area && isFunc(canp?.drawProjection))
  1335. return canp.drawProjection(kind || this.#special_draw_area, obj, opt);
  1336. return false;
  1337. }
  1338. /** @summary Get tooltip for painter and specified event position
  1339. * @param {Object} evnt - object with clientX and clientY positions
  1340. * @private */
  1341. getToolTip(evnt) {
  1342. if ((evnt?.clientX === undefined) || (evnt?.clientY === undefined))
  1343. return null;
  1344. const frame = this.getFrameSvg();
  1345. if (frame.empty())
  1346. return null;
  1347. const layer = frame.selectChild('.main_layer');
  1348. if (layer.empty())
  1349. return null;
  1350. const pos = d3_pointer(evnt, layer.node()),
  1351. pnt = { touch: false, x: pos[0], y: pos[1] };
  1352. if (isFunc(this.extractToolTip))
  1353. return this.extractToolTip(pnt);
  1354. pnt.disabled = true;
  1355. const res = isFunc(this.processTooltipEvent) ? this.processTooltipEvent(pnt) : null;
  1356. return res?.user_info || res;
  1357. }
  1358. } // class ObjectPainter
  1359. /** @summary Generic text drawing
  1360. * @private */
  1361. function drawRawText(dom, txt /* , opt */) {
  1362. const painter = new BasePainter(dom);
  1363. painter.txt = txt;
  1364. painter.redrawObject = function(obj) {
  1365. this.txt = obj;
  1366. this.drawText();
  1367. return true;
  1368. };
  1369. painter.drawText = async function() {
  1370. let stxt = (this.txt._typename === clTObjString) ? this.txt.fString : this.txt.value;
  1371. if (!isStr(stxt))
  1372. stxt = '<undefined>';
  1373. const mathjax = this.txt.mathjax || (settings.Latex === constants.Latex.AlwaysMathJax);
  1374. if (!mathjax && !('as_is' in this.txt)) {
  1375. const arr = stxt.split('\n');
  1376. stxt = '';
  1377. for (let i = 0; i < arr.length; ++i)
  1378. stxt += `<pre style='margin:0'>${arr[i]}</pre>`;
  1379. }
  1380. const frame = this.selectDom();
  1381. let main = frame.select('div');
  1382. if (main.empty())
  1383. main = frame.append('div').attr('style', 'max-width:100%;max-height:100%;overflow:auto');
  1384. main.html(stxt);
  1385. // (re) set painter to first child element, base painter not requires canvas
  1386. this.setTopPainter();
  1387. if (mathjax)
  1388. typesetMathjax(frame.node());
  1389. return this;
  1390. };
  1391. return painter.drawText();
  1392. }
  1393. /** @summary Returns pad painter (if any) for specified DOM element
  1394. * @param {string|object} dom - id or DOM element
  1395. * @private */
  1396. function getElementPadPainter(dom) {
  1397. return new ObjectPainter(dom).getPadPainter();
  1398. }
  1399. /** @summary Returns canvas painter (if any) for specified DOM element
  1400. * @param {string|object} dom - id or DOM element
  1401. * @private */
  1402. function getElementCanvPainter(dom) {
  1403. return new ObjectPainter(dom).getCanvPainter();
  1404. }
  1405. /** @summary Returns main painter (if any) for specified HTML element - typically histogram painter
  1406. * @param {string|object} dom - id or DOM element
  1407. * @private */
  1408. function getElementMainPainter(dom) {
  1409. return new ObjectPainter(dom).getMainPainter(true);
  1410. }
  1411. /** @summary Save object, drawn in specified element, as JSON.
  1412. * @desc Normally it is TCanvas object with list of primitives
  1413. * @param {string|object} dom - id of top div element or directly DOMElement
  1414. * @return {string} produced JSON string */
  1415. function drawingJSON(dom) {
  1416. return getElementCanvPainter(dom)?.produceJSON() || '';
  1417. }
  1418. let $active_pp = null;
  1419. /** @summary Set active pad painter
  1420. * @desc Normally be used to handle key press events, which are global in the web browser
  1421. * @param {object} args - functions arguments
  1422. * @param {object} args.pp - pad painter
  1423. * @param {boolean} [args.active] - is pad activated or not
  1424. * @private */
  1425. function selectActivePad(args) {
  1426. if (args.active) {
  1427. $active_pp?.getFramePainter()?.setFrameActive(false);
  1428. $active_pp = args.pp;
  1429. $active_pp?.getFramePainter()?.setFrameActive(true);
  1430. } else if ($active_pp === args.pp)
  1431. $active_pp = null;
  1432. }
  1433. /** @summary Returns current active pad
  1434. * @desc Should be used only for keyboard handling
  1435. * @private */
  1436. function getActivePad() {
  1437. return $active_pp;
  1438. }
  1439. /** @summary Check resize of drawn element
  1440. * @param {string|object} dom - id or DOM element
  1441. * @param {boolean|object} arg - options on how to resize
  1442. * @desc As first argument dom one should use same argument as for the drawing
  1443. * As second argument, one could specify 'true' value to force redrawing of
  1444. * the element even after minimal resize
  1445. * Or one just supply object with exact sizes like { width:300, height:200, force:true };
  1446. * @example
  1447. * import { resize } from 'https://root.cern/js/latest/modules/base/ObjectPainter.mjs';
  1448. * resize('drawing', { width: 500, height: 200 });
  1449. * resize(document.querySelector('#drawing'), true); */
  1450. function resize(dom, arg) {
  1451. if (arg === true)
  1452. arg = { force: true };
  1453. else if (!isObject(arg))
  1454. arg = null;
  1455. let done = false;
  1456. new ObjectPainter(dom).forEachPainter(painter => {
  1457. if (!done && isFunc(painter.checkResize))
  1458. done = painter.checkResize(arg);
  1459. });
  1460. return done;
  1461. }
  1462. /** @summary Safely remove all drawings from specified element
  1463. * @param {string|object} dom - id or DOM element
  1464. * @public
  1465. * @example
  1466. * import { cleanup } from 'https://root.cern/js/latest/modules/base/ObjectPainter.mjs';
  1467. * cleanup('drawing');
  1468. * cleanup(document.querySelector('#drawing')); */
  1469. function cleanup(dom) {
  1470. const dummy = new ObjectPainter(dom), lst = [];
  1471. dummy.forEachPainter(p => { if (lst.indexOf(p) < 0) lst.push(p); });
  1472. lst.forEach(p => p.cleanup());
  1473. dummy.selectDom().html('');
  1474. return lst;
  1475. }
  1476. const EAxisBits = {
  1477. kDecimals: BIT(7),
  1478. kTickPlus: BIT(9),
  1479. kTickMinus: BIT(10),
  1480. kAxisRange: BIT(11),
  1481. kCenterTitle: BIT(12),
  1482. kCenterLabels: BIT(14),
  1483. kRotateTitle: BIT(15),
  1484. kPalette: BIT(16),
  1485. kNoExponent: BIT(17),
  1486. kLabelsHori: BIT(18),
  1487. kLabelsVert: BIT(19),
  1488. kLabelsDown: BIT(20),
  1489. kLabelsUp: BIT(21),
  1490. kIsInteger: BIT(22),
  1491. kMoreLogLabels: BIT(23),
  1492. kOppositeTitle: BIT(32) // artificial bit, not possible to set in ROOT
  1493. }, kAxisLabels = 'labels', kAxisNormal = 'normal', kAxisFunc = 'func', kAxisTime = 'time';
  1494. Object.assign(internals.jsroot, { ObjectPainter, cleanup, resize });
  1495. export { getElementPadPainter, getElementCanvPainter, getElementMainPainter, drawingJSON,
  1496. selectActivePad, getActivePad, cleanup, resize, drawRawText,
  1497. ObjectPainter, EAxisBits, kAxisLabels, kAxisNormal, kAxisFunc, kAxisTime };