gpad/TPadPainter.mjs

  1. import { gStyle, settings, constants, browser, internals, BIT,
  2. create, toJSON, isBatchMode, loadModules, loadScript, injectCode, isPromise, getPromise, postponePromise,
  3. isObject, isFunc, isStr, clTObjArray, clTColor, clTPad, clTFrame, clTStyle, clTLegend,
  4. clTHStack, clTMultiGraph, clTLegendEntry, nsSVG, kTitle, clTList, urlClassPrefix } from '../core.mjs';
  5. import { select as d3_select, rgb as d3_rgb } from '../d3.mjs';
  6. import { ColorPalette, adoptRootColors, getColorPalette, getGrayColors, extendRootColors,
  7. getRGBfromTColor, decodeWebCanvasColors } from '../base/colors.mjs';
  8. import { prSVG, prJSON, getElementRect, getAbsPosInCanvas, DrawOptions, compressSVG, makeTranslate,
  9. getTDatime, convertDate, svgToImage, getBoxDecorations } from '../base/BasePainter.mjs';
  10. import { ObjectPainter, selectActivePad, getActivePad, isPadPainter } from '../base/ObjectPainter.mjs';
  11. import { TAttLineHandler } from '../base/TAttLineHandler.mjs';
  12. import { addCustomFont } from '../base/FontHandler.mjs';
  13. import { addDragHandler } from './TFramePainter.mjs';
  14. import { createMenu, closeMenu } from '../gui/menu.mjs';
  15. import { ToolbarIcons, registerForResize, saveFile } from '../gui/utils.mjs';
  16. import { BrowserLayout, getHPainter } from '../gui/display.mjs';
  17. const clTButton = 'TButton', kIsGrayscale = BIT(22),
  18. PadButtonsHandler = {
  19. getButtonSize(fact) {
  20. const cp = this.getCanvPainter();
  21. return Math.round((fact || 1) * (cp?.getPadScale() || 1) * (cp === this ? 16 : 12));
  22. },
  23. toggleButtonsVisibility(action, evnt) {
  24. evnt?.preventDefault();
  25. evnt?.stopPropagation();
  26. const group = this.getLayerSvg('btns_layer'),
  27. btn = group.select('[name=\'Toggle\']');
  28. if (btn.empty()) return;
  29. let state = btn.property('buttons_state');
  30. if (btn.property('timout_handler')) {
  31. if (action !== 'timeout')
  32. clearTimeout(btn.property('timout_handler'));
  33. btn.property('timout_handler', null);
  34. }
  35. let is_visible = false;
  36. switch (action) {
  37. case 'enable':
  38. is_visible = true;
  39. this.btns_active_flag = true;
  40. break;
  41. case 'enterbtn':
  42. this.btns_active_flag = true;
  43. return; // do nothing, just cleanup timeout
  44. case 'timeout':
  45. break;
  46. case 'toggle':
  47. state = !state;
  48. btn.property('buttons_state', state);
  49. is_visible = state;
  50. break;
  51. case 'disable':
  52. case 'leavebtn':
  53. this.btns_active_flag = false;
  54. if (!state)
  55. btn.property('timout_handler', setTimeout(() => this.toggleButtonsVisibility('timeout'), 1200));
  56. return;
  57. }
  58. group.selectAll('svg').each(function() {
  59. if (this !== btn.node())
  60. d3_select(this).style('display', is_visible ? '' : 'none');
  61. });
  62. },
  63. alignButtons(btns, width, height) {
  64. const sz0 = this.getButtonSize(1.25), nextx = (btns.property('nextx') || 0) + sz0;
  65. let btns_x, btns_y;
  66. if (btns.property('vertical')) {
  67. btns_x = btns.property('leftside') ? 2 : (width - sz0);
  68. btns_y = height - nextx;
  69. } else {
  70. btns_x = btns.property('leftside') ? 2 : (width - nextx);
  71. btns_y = height - sz0;
  72. }
  73. makeTranslate(btns, btns_x, btns_y);
  74. },
  75. findPadButton(keyname) {
  76. const group = this.getLayerSvg('btns_layer');
  77. let found_func = '';
  78. if (!group.empty()) {
  79. group.selectAll('svg').each(function() {
  80. if (d3_select(this).attr('key') === keyname)
  81. found_func = d3_select(this).attr('name');
  82. });
  83. }
  84. return found_func;
  85. },
  86. removePadButtons() {
  87. const group = this.getLayerSvg('btns_layer');
  88. if (!group.empty()) {
  89. group.selectAll('*').remove();
  90. group.property('nextx', null);
  91. }
  92. },
  93. showPadButtons() {
  94. const group = this.getLayerSvg('btns_layer');
  95. if (group.empty()) return;
  96. // clean all previous buttons
  97. group.selectAll('*').remove();
  98. if (!this._buttons) return;
  99. const istop = this.isTopPad(), y = 0;
  100. let ctrl, x = group.property('leftside') ? this.getButtonSize(1.25) : 0;
  101. if (this.isFastDrawing()) {
  102. ctrl = ToolbarIcons.createSVG(group, ToolbarIcons.circle, this.getButtonSize(), 'enlargePad', false)
  103. .attr('name', 'Enlarge').attr('x', 0).attr('y', 0)
  104. .on('click', evnt => this.clickPadButton('enlargePad', evnt));
  105. } else {
  106. ctrl = ToolbarIcons.createSVG(group, ToolbarIcons.rect, this.getButtonSize(), 'Toggle tool buttons', false)
  107. .attr('name', 'Toggle').attr('x', 0).attr('y', 0)
  108. .property('buttons_state', (settings.ToolBar !== 'popup') || browser.touches)
  109. .on('click', evnt => this.toggleButtonsVisibility('toggle', evnt));
  110. ctrl.node()._mouseenter = () => this.toggleButtonsVisibility('enable');
  111. ctrl.node()._mouseleave = () => this.toggleButtonsVisibility('disable');
  112. for (let k = 0; k < this._buttons.length; ++k) {
  113. const item = this._buttons[k];
  114. let btn = item.btn;
  115. if (isStr(btn))
  116. btn = ToolbarIcons[btn];
  117. if (!btn)
  118. btn = ToolbarIcons.circle;
  119. const svg = ToolbarIcons.createSVG(group, btn, this.getButtonSize(),
  120. item.tooltip + (istop ? '' : (` on pad ${this.getPadName()}`)) + (item.keyname ? ` (keyshortcut ${item.keyname})` : ''), false);
  121. if (group.property('vertical'))
  122. svg.attr('x', y).attr('y', x);
  123. else
  124. svg.attr('x', x).attr('y', y);
  125. svg.attr('name', item.funcname)
  126. .style('display', ctrl.property('buttons_state') ? '' : 'none')
  127. .attr('key', item.keyname || null)
  128. .on('click', evnt => this.clickPadButton(item.funcname, evnt));
  129. svg.node()._mouseenter = () => this.toggleButtonsVisibility('enterbtn');
  130. svg.node()._mouseleave = () => this.toggleButtonsVisibility('leavebtn');
  131. x += this.getButtonSize(1.25);
  132. }
  133. }
  134. group.property('nextx', x);
  135. this.alignButtons(group, this.getPadWidth(), this.getPadHeight());
  136. if (group.property('vertical'))
  137. ctrl.attr('y', x);
  138. else if (!group.property('leftside'))
  139. ctrl.attr('x', x);
  140. },
  141. assign(painter) {
  142. Object.assign(painter, this);
  143. }
  144. }, // PadButtonsHandler
  145. // identifier used in TWebCanvas painter
  146. webSnapIds = { kNone: 0, kObject: 1, kSVG: 2, kSubPad: 3, kColors: 4, kStyle: 5, kFont: 6 };
  147. /** @summary Fill TWebObjectOptions for painter
  148. * @private */
  149. function createWebObjectOptions(painter) {
  150. if (!painter?.getSnapId())
  151. return null;
  152. const obj = { _typename: 'TWebObjectOptions', snapid: painter.getSnapId(), opt: painter.getDrawOpt(true), fcust: '', fopt: [] };
  153. if (isFunc(painter.fillWebObjectOptions))
  154. painter.fillWebObjectOptions(obj);
  155. return obj;
  156. }
  157. /**
  158. * @summary Painter for TPad object
  159. * @private
  160. */
  161. class TPadPainter extends ObjectPainter {
  162. #iscan; // is canvas flag
  163. #pad_name; // name of the pad
  164. #pad; // TPad object
  165. #painters; // painters in the pad
  166. #pad_scale; // scale factor of the pad
  167. #pad_x; // pad x coordinate
  168. #pad_y; // pad y coordinate
  169. #pad_width; // pad width
  170. #pad_height; // pad height
  171. #doing_draw; // drawing handles
  172. #pad_draw_disabled; // disable drawing of the pad
  173. #last_grayscale; // grayscale change flag
  174. #custom_palette; // custom palette
  175. #custom_colors; // custom colors
  176. #custom_palette_indexes; // custom palette indexes
  177. #custom_palette_colors; // custom palette colors
  178. #frame_painter_ref; // frame painter
  179. #main_painter_ref; // main painter on the pad
  180. #snap_primitives; // stored snap primitives from web canvas
  181. #has_execs; // indicate is pad has TExec objects assigned
  182. #deliver_move_events; // deliver move events to server
  183. #readonly; // if changes on pad is not allowed
  184. #num_primitives; // number of primitives
  185. #num_specials; // number of special objects - if counted
  186. #auto_color_cnt; // counter used in assigning auto colors
  187. #auto_palette; // palette for creating of automatic colors
  188. #fixed_size; // fixed size flag
  189. #has_canvas; // indicate if top canvas painter exists
  190. #fast_drawing; // fast drawing flag
  191. #resize_tmout; // timeout handle for resize
  192. #start_draw_tm; // time when start drawing primitives
  193. /** @summary constructor
  194. * @param {object|string} dom - DOM element for drawing or element id
  195. * @param {object} pad - TPad object to draw
  196. * @param {String} [opt] - draw option
  197. * @param {boolean} [iscan] - if TCanvas object
  198. * @param [add_to_primitives] - add pad painter to canvas
  199. * */
  200. constructor(dom, pad, opt, iscan, add_to_primitives) {
  201. super(dom, pad);
  202. this.#pad = pad;
  203. this.#iscan = iscan; // indicate if working with canvas
  204. this.#pad_name = '';
  205. if (!iscan && pad?.fName) {
  206. this.#pad_name = pad.fName.replace(' ', '_'); // avoid empty symbol in pad name
  207. const regexp = /^[A-Za-z][A-Za-z0-9_]*$/;
  208. if (!regexp.test(this.#pad_name) || ((this.#pad_name === 'button') && (pad._typename === clTButton)))
  209. this.#pad_name = 'jsroot_pad_' + internals.id_counter++;
  210. }
  211. this.#painters = []; // complete list of all painters in the pad
  212. this.#has_canvas = true;
  213. this.forEachPainter = this.forEachPainterInPad;
  214. const d = this.selectDom();
  215. if (!d.empty() && d.property('_batch_mode'))
  216. this.batch_mode = true;
  217. if (opt !== undefined)
  218. this.decodeOptions(opt);
  219. if (add_to_primitives) {
  220. if ((add_to_primitives !== 'webpad') && this.getCanvSvg().empty()) {
  221. // one can draw pad without canvas
  222. this.#has_canvas = false;
  223. this.#pad_name = '';
  224. this.setTopPainter();
  225. } else {
  226. // pad painter will be registered in the parent pad
  227. this.addToPadPrimitives();
  228. }
  229. }
  230. if (pad?.$disable_drawing)
  231. this.#pad_draw_disabled = true;
  232. }
  233. /** @summary returns pad painter
  234. * @protected */
  235. getPadPainter() { return this.isTopPad() ? null : super.getPadPainter(); }
  236. /** @summary returns canvas painter
  237. * @protected */
  238. getCanvPainter(try_select) { return this.isTopPad() ? this : super.getCanvPainter(try_select); }
  239. /** @summary Returns pad name
  240. * @protected */
  241. getPadName() { return this.#pad_name; }
  242. /** @summary Indicates that drawing runs in batch mode
  243. * @private */
  244. isBatchMode() {
  245. if (this.batch_mode !== undefined)
  246. return this.batch_mode;
  247. if (isBatchMode())
  248. return true;
  249. return this.isTopPad() ? false : this.getCanvPainter()?.isBatchMode();
  250. }
  251. /** @summary Indicates that is is Root6 pad painter
  252. * @private */
  253. isRoot6() { return true; }
  254. /** @summary Returns true if pad is editable */
  255. isEditable() { return this.#pad?.fEditable ?? true; }
  256. /** @summary Returns true if button */
  257. isButton() { return this.matchObjectType(clTButton); }
  258. /** @summary Returns true if read-only mode is enabled */
  259. isReadonly() { return this.#readonly; }
  260. /** @summary Returns true if it is canvas
  261. * @param {Boolean} [is_online = false] - if specified, checked if it is canvas with configured connection to server */
  262. isCanvas(is_online = false) {
  263. if (!this.#iscan)
  264. return false;
  265. if (is_online === true)
  266. return isFunc(this.getWebsocket) && this.getWebsocket();
  267. return isStr(is_online) ? this.#iscan === is_online : true;
  268. }
  269. /** @summary Returns true if it is canvas or top pad without canvas */
  270. isTopPad() { return this.isCanvas() || !this.#has_canvas; }
  271. /** @summary Canvas main svg element
  272. * @return {object} d3 selection with canvas svg
  273. * @protected */
  274. getCanvSvg() { return this.selectDom().select('.root_canvas'); }
  275. /** @summary Pad svg element
  276. * @return {object} d3 selection with pad svg
  277. * @protected */
  278. getPadSvg() {
  279. const c = this.getCanvSvg();
  280. if (!this.#pad_name || c.empty())
  281. return c;
  282. return c.select('.primitives_layer .__root_pad_' + this.#pad_name);
  283. }
  284. /** @summary Method selects immediate layer under canvas/pad main element
  285. * @param {string} name - layer name lik 'primitives_layer', 'btns_layer', 'info_layer'
  286. * @protected */
  287. getLayerSvg(name) { return this.getPadSvg().selectChild('.' + name); }
  288. /** @summary Returns svg element for the frame in current pad
  289. * @protected */
  290. getFrameSvg() {
  291. const layer = this.getLayerSvg('primitives_layer');
  292. if (layer.empty()) return layer;
  293. let node = layer.node().firstChild;
  294. while (node) {
  295. const elem = d3_select(node);
  296. if (elem.classed('root_frame')) return elem;
  297. node = node.nextSibling;
  298. }
  299. return d3_select(null);
  300. }
  301. /** @summary Returns main painter on the pad
  302. * @desc Typically main painter is TH1/TH2 object which is drawing axes
  303. * @private */
  304. getMainPainter() { return this.#main_painter_ref || null; }
  305. /** @summary Assign main painter on the pad
  306. * @desc Typically main painter is TH1/TH2 object which is drawing axes
  307. * @private */
  308. setMainPainter(painter, force) {
  309. if (!this.#main_painter_ref || force)
  310. this.#main_painter_ref = painter;
  311. }
  312. /** @summary cleanup pad and all primitives inside */
  313. cleanup() {
  314. if (this.#doing_draw)
  315. console.error('pad drawing is not completed when cleanup is called');
  316. this.#painters.forEach(p => p.cleanup());
  317. const svg_p = this.getPadSvg();
  318. if (!svg_p.empty()) {
  319. svg_p.property('pad_painter', null);
  320. if (!this.isCanvas())
  321. svg_p.remove();
  322. }
  323. this.#main_painter_ref = undefined;
  324. this.#frame_painter_ref = undefined;
  325. this.#pad_x = this.#pad_y = this.#pad_width = this.#pad_height = undefined;
  326. this.#doing_draw = undefined;
  327. this.#snap_primitives = undefined;
  328. this.#last_grayscale = undefined;
  329. this.#custom_palette = this.#custom_colors = this.#custom_palette_indexes = this.#custom_palette_colors = undefined;
  330. this.#painters = [];
  331. this.#pad = undefined;
  332. this.#pad_name = undefined;
  333. this.#has_canvas = false;
  334. selectActivePad({ pp: this, active: false });
  335. super.cleanup();
  336. }
  337. /** @summary Returns frame painter inside the pad
  338. * @private */
  339. getFramePainter() { return this.#frame_painter_ref; }
  340. /** @summary Assign actual frame painter
  341. * @private */
  342. setFramePainter(fp, on) {
  343. if (on)
  344. this.#frame_painter_ref = fp;
  345. else if (this.#frame_painter_ref === fp)
  346. this.#frame_painter_ref = undefined;
  347. }
  348. /** @summary get pad width */
  349. getPadWidth() { return this.#pad_width || 0; }
  350. /** @summary get pad height */
  351. getPadHeight() { return this.#pad_height || 0; }
  352. /** @summary get pad height */
  353. getPadScale() { return this.#pad_scale || 1; }
  354. /** @summary get pad rect */
  355. getPadRect() {
  356. return {
  357. x: this.#pad_x || 0,
  358. y: this.#pad_y || 0,
  359. width: this.getPadWidth(),
  360. height: this.getPadHeight()
  361. };
  362. }
  363. /** @summary return pad log state x or y are allowed */
  364. getPadLog(name) {
  365. const pad = this.getRootPad();
  366. if (name === 'x')
  367. return pad?.fLogx;
  368. if (name === 'y')
  369. return pad?.fLogv ?? pad?.fLogy;
  370. return false;
  371. }
  372. /** @summary Returns frame coordinates - also when frame is not drawn */
  373. getFrameRect() {
  374. const fp = this.getFramePainter();
  375. if (fp) return fp.getFrameRect();
  376. const w = this.getPadWidth(),
  377. h = this.getPadHeight(),
  378. rect = {};
  379. if (this.#pad) {
  380. rect.szx = Math.round(Math.max(0, 0.5 - Math.max(this.#pad.fLeftMargin, this.#pad.fRightMargin))*w);
  381. rect.szy = Math.round(Math.max(0, 0.5 - Math.max(this.#pad.fBottomMargin, this.#pad.fTopMargin))*h);
  382. } else {
  383. rect.szx = Math.round(0.5*w);
  384. rect.szy = Math.round(0.5*h);
  385. }
  386. rect.width = 2*rect.szx;
  387. rect.height = 2*rect.szy;
  388. rect.x = Math.round(w/2 - rect.szx);
  389. rect.y = Math.round(h/2 - rect.szy);
  390. rect.hint_delta_x = rect.szx;
  391. rect.hint_delta_y = rect.szy;
  392. rect.transform = makeTranslate(rect.x, rect.y) || '';
  393. return rect;
  394. }
  395. /** @summary return RPad object */
  396. getRootPad(is_root6) {
  397. return (is_root6 === undefined) || is_root6 ? this.#pad : null;
  398. }
  399. /** @summary Cleanup primitives from pad - selector lets define which painters to remove
  400. * @return true if any painter was removed */
  401. cleanPrimitives(selector) {
  402. // remove all primitives
  403. if (selector === true)
  404. selector = () => true;
  405. if (!isFunc(selector))
  406. return false;
  407. let is_any = false;
  408. for (let k = this.#painters.length - 1; k >= 0; --k) {
  409. const subp = this.#painters[k];
  410. if (!subp || selector(subp)) {
  411. subp?.cleanup();
  412. this.#painters.splice(k, 1);
  413. is_any = true;
  414. }
  415. }
  416. return is_any;
  417. }
  418. /** @summary Removes and cleanup specified primitive
  419. * @desc also secondary primitives will be removed
  420. * @return new index to continue loop or -111 if main painter removed
  421. * @private */
  422. removePrimitive(arg, clean_only_secondary) {
  423. let indx, prim;
  424. if (Number.isInteger(arg)) {
  425. indx = arg;
  426. prim = this.#painters[indx];
  427. } else {
  428. indx = this.#painters.indexOf(arg);
  429. prim = arg;
  430. }
  431. if (indx < 0)
  432. return indx;
  433. const arr = [], get_main = clean_only_secondary ? this.getMainPainter() : null;
  434. let resindx = indx - 1; // object removed itself
  435. arr.push(prim);
  436. this.#painters.splice(indx, 1);
  437. // loop to extract all dependent painters
  438. let len0 = 0;
  439. while (len0 < arr.length) {
  440. for (let k = this.#painters.length - 1; k >= 0; --k) {
  441. if (this.#painters[k].isSecondary(arr[len0])) {
  442. arr.push(this.#painters[k]);
  443. this.#painters.splice(k, 1);
  444. if (k < indx) resindx--;
  445. }
  446. }
  447. len0++;
  448. }
  449. arr.forEach(painter => {
  450. if ((painter !== prim) || !clean_only_secondary)
  451. painter.cleanup();
  452. if (this.getMainPainter() === painter) {
  453. this.setMainPainter(undefined, true);
  454. resindx = -111;
  455. }
  456. });
  457. // when main painter disappears because of special cleanup - also reset zooming
  458. if (clean_only_secondary && get_main && !this.getMainPainter())
  459. this.getFramePainter()?.resetZoom();
  460. return resindx;
  461. }
  462. /** @summary returns custom palette associated with pad or top canvas
  463. * @private */
  464. getCustomPalette(no_recursion) {
  465. return this.#custom_palette || (no_recursion ? null : this.getCanvPainter()?.getCustomPalette(true));
  466. }
  467. _getCustomPaletteIndexes() { return this.#custom_palette_indexes; }
  468. /** @summary Provides automatic color
  469. * @desc Uses ROOT colors palette if possible
  470. * @private */
  471. getAutoColor(numprimitives) {
  472. numprimitives = Math.max(numprimitives || (this.#num_primitives || 5) - (this.#num_specials || 0), 2);
  473. let indx = this.#auto_color_cnt ?? 0;
  474. this.#auto_color_cnt = (indx + 1) % numprimitives;
  475. if (indx >= numprimitives) indx = numprimitives - 1;
  476. let indexes = this._getCustomPaletteIndexes();
  477. if (!indexes) {
  478. const cp = this.getCanvPainter();
  479. if ((cp !== this) && isFunc(cp?._getCustomPaletteIndexes))
  480. indexes = cp._getCustomPaletteIndexes();
  481. }
  482. if (indexes?.length) {
  483. const p = Math.round(indx * (indexes.length - 3) / (numprimitives - 1));
  484. return indexes[p];
  485. }
  486. if (!this.#auto_palette)
  487. this.#auto_palette = getColorPalette(settings.Palette, this.isGrayscale());
  488. const palindx = Math.round(indx * (this.#auto_palette.getLength() - 3) / (numprimitives - 1)),
  489. colvalue = this.#auto_palette.getColor(palindx);
  490. return this.addColor(colvalue);
  491. }
  492. /** @summary Returns number of painters
  493. * @protected */
  494. getNumPainters() { return this.#painters.length; }
  495. /** @summary Add painter to pad list of painters
  496. * @protected */
  497. addToPrimitives(painter) {
  498. if (this.#painters.indexOf(painter) < 0)
  499. this.#painters.push(painter);
  500. return this;
  501. }
  502. /** @summary Call function for each painter in pad
  503. * @param {function} userfunc - function to call
  504. * @param {string} kind - 'all' for all objects (default), 'pads' only pads and sub-pads, 'objects' only for object in current pad
  505. * @private */
  506. forEachPainterInPad(userfunc, kind) {
  507. if (!kind)
  508. kind = 'all';
  509. if (kind !== 'objects')
  510. userfunc(this);
  511. for (let k = 0; k < this.#painters.length; ++k) {
  512. const sub = this.#painters[k];
  513. if (isFunc(sub.forEachPainterInPad)) {
  514. if (kind !== 'objects')
  515. sub.forEachPainterInPad(userfunc, kind);
  516. } else if (kind !== 'pads')
  517. userfunc(sub);
  518. }
  519. }
  520. /** @summary register for pad events receiver
  521. * @desc in pad painter, while pad may be drawn without canvas */
  522. registerForPadEvents(receiver) {
  523. this.pad_events_receiver = receiver;
  524. }
  525. /** @summary Generate pad events, normally handled by GED
  526. * @desc in pad painter, while pad may be drawn without canvas
  527. * @private */
  528. producePadEvent(what, padpainter, painter, position) {
  529. if ((what === 'select') && isFunc(this.selectActivePad))
  530. this.selectActivePad(padpainter, painter, position);
  531. if (isFunc(this.pad_events_receiver))
  532. this.pad_events_receiver({ what, padpainter, painter, position });
  533. }
  534. /** @summary method redirect call to pad events receiver */
  535. selectObjectPainter(painter, pos) {
  536. const canp = this.isTopPad() ? this : this.getCanvPainter();
  537. if (painter === undefined)
  538. painter = this;
  539. if (pos && !this.isTopPad())
  540. pos = getAbsPosInCanvas(this.getPadSvg(), pos);
  541. selectActivePad({ pp: this, active: true });
  542. canp?.producePadEvent('select', this, painter, pos);
  543. }
  544. /** @summary Draw pad active border
  545. * @private */
  546. drawActiveBorder(svg_rect, is_active) {
  547. if (is_active !== undefined) {
  548. if (this.is_active_pad === is_active) return;
  549. this.is_active_pad = is_active;
  550. }
  551. if (this.is_active_pad === undefined)
  552. return;
  553. if (!svg_rect)
  554. svg_rect = this.isCanvas() ? this.getCanvSvg().selectChild('.canvas_fillrect') : this.getPadSvg().selectChild('.root_pad_border');
  555. const cp = this.getCanvPainter();
  556. let lineatt = this.is_active_pad && cp?.highlight_gpad ? new TAttLineHandler({ style: 1, width: 1, color: 'red' }) : this.lineatt;
  557. if (!lineatt) lineatt = new TAttLineHandler({ color: 'none' });
  558. svg_rect.call(lineatt.func);
  559. }
  560. /** @summary Set fast drawing property depending on the size
  561. * @private */
  562. setFastDrawing(w, h) {
  563. const was_fast = this.#fast_drawing;
  564. this.#fast_drawing = !this.hasSnapId() && settings.SmallPad && ((w < settings.SmallPad.width) || (h < settings.SmallPad.height));
  565. if (was_fast !== this.#fast_drawing)
  566. this.showPadButtons();
  567. }
  568. /** @summary Return fast drawing flag
  569. * @private */
  570. isFastDrawing() { return this.#fast_drawing; }
  571. /** @summary Returns true if canvas configured with grayscale
  572. * @private */
  573. isGrayscale() {
  574. if (!this.isCanvas())
  575. return false;
  576. return this.#pad?.TestBit(kIsGrayscale) ?? false;
  577. }
  578. /** @summary Returns true if default pad range is configured
  579. * @private */
  580. isDefaultPadRange() {
  581. if (!this.#pad)
  582. return true;
  583. return (this.#pad.fX1 === 0) && (this.#pad.fX2 === 1) && (this.#pad.fY1 === 0) && (this.#pad.fY2 === 1);
  584. }
  585. /** @summary Set grayscale mode for the canvas
  586. * @private */
  587. setGrayscale(flag) {
  588. if (!this.isTopPad())
  589. return;
  590. let changed = false;
  591. if (flag === undefined) {
  592. flag = this.#pad?.TestBit(kIsGrayscale) ?? false;
  593. changed = (this.#last_grayscale !== undefined) && (this.#last_grayscale !== flag);
  594. } else if (flag !== this.#pad?.TestBit(kIsGrayscale)) {
  595. this.#pad?.InvertBit(kIsGrayscale);
  596. changed = true;
  597. }
  598. if (changed)
  599. this.forEachPainter(p => { if (isFunc(p.clearHistPalette)) p.clearHistPalette(); });
  600. this.setColors(flag ? getGrayColors(this.#custom_colors) : this.#custom_colors);
  601. this.#last_grayscale = flag;
  602. this.#custom_palette = this.#custom_palette_colors ? new ColorPalette(this.#custom_palette_colors, flag) : null;
  603. }
  604. /** @summary Set fixed-size canvas
  605. * @private */
  606. _setFixedSize(on) { this.#fixed_size = on; }
  607. /** @summary Create SVG element for canvas */
  608. createCanvasSvg(check_resize, new_size) {
  609. const is_batch = this.isBatchMode(), lmt = 5;
  610. let factor, svg, rect, btns, info, frect;
  611. if (check_resize > 0) {
  612. if (this.#fixed_size)
  613. return check_resize > 1; // flag used to force re-drawing of all sub-pads
  614. svg = this.getCanvSvg();
  615. if (svg.empty())
  616. return false;
  617. factor = svg.property('height_factor');
  618. rect = this.testMainResize(check_resize, null, factor);
  619. if (!rect.changed && (check_resize === 1))
  620. return false;
  621. if (!is_batch)
  622. btns = this.getLayerSvg('btns_layer');
  623. info = this.getLayerSvg('info_layer');
  624. frect = svg.selectChild('.canvas_fillrect');
  625. } else {
  626. const render_to = this.selectDom();
  627. if (render_to.style('position') === 'static')
  628. render_to.style('position', 'relative');
  629. svg = render_to.append('svg')
  630. .attr('class', 'jsroot root_canvas')
  631. .property('pad_painter', this) // this is custom property
  632. .property('redraw_by_resize', false); // could be enabled to force redraw by each resize
  633. this.setTopPainter(); // assign canvas as top painter of that element
  634. if (is_batch)
  635. svg.attr('xmlns', nsSVG);
  636. else if (!this.online_canvas)
  637. svg.append('svg:title').text('ROOT canvas');
  638. if (!is_batch)
  639. svg.style('user-select', settings.UserSelect || null);
  640. if (!is_batch || (this.#pad.fFillStyle > 0))
  641. frect = svg.append('svg:path').attr('class', 'canvas_fillrect');
  642. if (!is_batch) {
  643. frect.style('pointer-events', 'visibleFill')
  644. .on('dblclick', evnt => this.enlargePad(evnt, true))
  645. .on('click', () => this.selectObjectPainter())
  646. .on('mouseenter', () => this.showObjectStatus())
  647. .on('contextmenu', settings.ContextMenu ? evnt => this.padContextMenu(evnt) : null);
  648. }
  649. svg.append('svg:g').attr('class', 'primitives_layer');
  650. info = svg.append('svg:g').attr('class', 'info_layer');
  651. if (!is_batch) {
  652. btns = svg.append('svg:g')
  653. .attr('class', 'btns_layer')
  654. .property('leftside', settings.ToolBarSide === 'left')
  655. .property('vertical', settings.ToolBarVert);
  656. }
  657. factor = 0.66;
  658. if (this.#pad?.fCw && this.#pad?.fCh && (this.#pad?.fCw > 0)) {
  659. factor = this.#pad.fCh / this.#pad.fCw;
  660. if ((factor < 0.1) || (factor > 10)) factor = 0.66;
  661. }
  662. if (this.#fixed_size) {
  663. render_to.style('overflow', 'auto');
  664. rect = { width: this.#pad.fCw, height: this.#pad.fCh };
  665. if (!rect.width || !rect.height)
  666. rect = getElementRect(render_to);
  667. } else
  668. rect = this.testMainResize(2, new_size, factor);
  669. }
  670. this.setGrayscale();
  671. this.createAttFill({ attr: this.#pad });
  672. if ((rect.width <= lmt) || (rect.height <= lmt)) {
  673. if (this.hasSnapId()) {
  674. svg.style('display', 'none');
  675. console.warn(`Hide canvas while geometry too small w=${rect.width} h=${rect.height}`);
  676. }
  677. if (this.#pad_width && this.#pad_height) {
  678. // use last valid dimensions
  679. rect.width = this.#pad_width;
  680. rect.height = this.#pad_height;
  681. } else {
  682. // just to complete drawing.
  683. rect.width = 800;
  684. rect.height = 600;
  685. }
  686. } else
  687. svg.style('display', null);
  688. svg.attr('x', 0).attr('y', 0).style('position', 'absolute');
  689. if (this.#fixed_size)
  690. svg.attr('width', rect.width).attr('height', rect.height);
  691. else
  692. svg.style('width', '100%').style('height', '100%').style('left', 0).style('top', 0).style('bottom', 0).style('right', 0);
  693. svg.style('filter', settings.DarkMode || this.#pad?.$dark ? 'invert(100%)' : null);
  694. this.#pad_scale = settings.CanvasScale || 1;
  695. this.#pad_x = 0;
  696. this.#pad_y = 0;
  697. this.#pad_width = rect.width * this.#pad_scale;
  698. this.#pad_height = rect.height * this.#pad_scale;
  699. svg.attr('viewBox', `0 0 ${this.#pad_width} ${this.#pad_height}`)
  700. .attr('preserveAspectRatio', 'none') // we do not preserve relative ratio
  701. .property('height_factor', factor)
  702. .property('draw_x', this.#pad_x)
  703. .property('draw_y', this.#pad_y)
  704. .property('draw_width', this.#pad_width)
  705. .property('draw_height', this.#pad_height);
  706. this.addPadBorder(svg, frect);
  707. this.setFastDrawing(this.#pad_width * (1 - this.#pad.fLeftMargin - this.#pad.fRightMargin), this.#pad_height * (1 - this.#pad.fBottomMargin - this.#pad.fTopMargin));
  708. if (this.alignButtons && btns)
  709. this.alignButtons(btns, this.#pad_width, this.#pad_height);
  710. let dt = info.selectChild('.canvas_date');
  711. if (!gStyle.fOptDate)
  712. dt.remove();
  713. else {
  714. if (dt.empty())
  715. dt = info.append('text').attr('class', 'canvas_date');
  716. const posy = Math.round(this.#pad_height * (1 - gStyle.fDateY)),
  717. date = new Date();
  718. let posx = Math.round(this.#pad_width * gStyle.fDateX);
  719. if (!is_batch && (posx < 25))
  720. posx = 25;
  721. if (gStyle.fOptDate > 3)
  722. date.setTime(gStyle.fOptDate*1000);
  723. makeTranslate(dt, posx, posy)
  724. .style('text-anchor', 'start')
  725. .text(convertDate(date));
  726. }
  727. const iname = this.getItemName();
  728. if (iname)
  729. this.drawItemNameOnCanvas(iname);
  730. else if (!gStyle.fOptFile)
  731. info.selectChild('.canvas_item').remove();
  732. return true;
  733. }
  734. /** @summary Draw item name on canvas if gStyle.fOptFile is configured
  735. * @private */
  736. drawItemNameOnCanvas(item_name) {
  737. const info = this.getLayerSvg('info_layer');
  738. let df = info.selectChild('.canvas_item');
  739. const fitem = getHPainter().findRootFileForItem(item_name),
  740. fname = (gStyle.fOptFile === 3) ? item_name : ((gStyle.fOptFile === 2) ? fitem?._fullurl : fitem?._name);
  741. if (!gStyle.fOptFile || !fname)
  742. df.remove();
  743. else {
  744. if (df.empty())
  745. df = info.append('text').attr('class', 'canvas_item');
  746. const rect = this.getPadRect();
  747. makeTranslate(df, Math.round(rect.width * (1 - gStyle.fDateX)), Math.round(rect.height * (1 - gStyle.fDateY)))
  748. .style('text-anchor', 'end')
  749. .text(fname);
  750. }
  751. if (((gStyle.fOptDate === 2) || (gStyle.fOptDate === 3)) && fitem?._file) {
  752. info.selectChild('.canvas_date')
  753. .text(convertDate(getTDatime(gStyle.fOptDate === 2 ? fitem._file.fDatimeC : fitem._file.fDatimeM)));
  754. }
  755. }
  756. /** @summary Return true if this pad enlarged */
  757. isPadEnlarged() {
  758. if (this.isTopPad())
  759. return this.enlargeMain('state') === 'on';
  760. return this.getCanvSvg().property('pad_enlarged') === this.#pad;
  761. }
  762. /** @summary Enlarge pad draw element when possible */
  763. enlargePad(evnt, is_dblclick, is_escape) {
  764. evnt?.preventDefault();
  765. evnt?.stopPropagation();
  766. // ignore double click on online canvas itself for enlarge
  767. if (is_dblclick && this.isCanvas(true) && (this.enlargeMain('state') === 'off'))
  768. return;
  769. const svg_can = this.getCanvSvg(),
  770. pad_enlarged = svg_can.property('pad_enlarged');
  771. if (this.isTopPad() || (!pad_enlarged && !this.hasObjectsToDraw() && !this.#painters)) {
  772. if (this.#fixed_size)
  773. return; // canvas cannot be enlarged in such mode
  774. if (!this.enlargeMain(is_escape ? false : 'toggle')) return;
  775. if (this.enlargeMain('state') === 'off')
  776. svg_can.property('pad_enlarged', null);
  777. else
  778. selectActivePad({ pp: this, active: true });
  779. } else if (!pad_enlarged && !is_escape) {
  780. this.enlargeMain(true, true);
  781. svg_can.property('pad_enlarged', this.#pad);
  782. selectActivePad({ pp: this, active: true });
  783. } else if (pad_enlarged === this.#pad) {
  784. this.enlargeMain(false);
  785. svg_can.property('pad_enlarged', null);
  786. } else if (!is_escape && is_dblclick)
  787. console.error('missmatch with pad double click events');
  788. return this.checkResize(true);
  789. }
  790. /** @summary Create main SVG element for pad
  791. * @return true when pad is displayed and all its items should be redrawn */
  792. createPadSvg(only_resize) {
  793. if (this.isTopPad()) {
  794. this.createCanvasSvg(only_resize ? 2 : 0);
  795. return true;
  796. }
  797. const svg_can = this.getCanvSvg(),
  798. width = svg_can.property('draw_width'),
  799. height = svg_can.property('draw_height'),
  800. pad_enlarged = svg_can.property('pad_enlarged'),
  801. pad_visible = !this.#pad_draw_disabled && (!pad_enlarged || (pad_enlarged === this.#pad)),
  802. is_batch = this.isBatchMode();
  803. let w = Math.round(this.#pad.fAbsWNDC * width),
  804. h = Math.round(this.#pad.fAbsHNDC * height),
  805. x = Math.round(this.#pad.fAbsXlowNDC * width),
  806. y = Math.round(height * (1 - this.#pad.fAbsYlowNDC)) - h,
  807. svg_pad, svg_border, btns;
  808. if (pad_enlarged === this.#pad) {
  809. w = width;
  810. h = height;
  811. x = y = 0;
  812. }
  813. if (only_resize) {
  814. svg_pad = this.getPadSvg();
  815. svg_border = svg_pad.selectChild('.root_pad_border');
  816. if (!is_batch)
  817. btns = this.getLayerSvg('btns_layer');
  818. this.addPadInteractive(true);
  819. } else {
  820. svg_pad = svg_can.selectChild('.primitives_layer')
  821. .append('svg:svg') // svg used to blend all drawings outside
  822. .classed('__root_pad_' + this.#pad_name, true)
  823. .attr('pad', this.#pad_name) // set extra attribute to mark pad name
  824. .property('pad_painter', this); // this is custom property
  825. if (!is_batch)
  826. svg_pad.append('svg:title').text('subpad ' + this.#pad_name);
  827. // need to check attributes directly while attributes objects will be created later
  828. if (!is_batch || (this.#pad.fFillStyle > 0) || ((this.#pad.fLineStyle > 0) && (this.#pad.fLineColor > 0)))
  829. svg_border = svg_pad.append('svg:path').attr('class', 'root_pad_border');
  830. if (!is_batch) {
  831. svg_border.style('pointer-events', 'visibleFill') // get events also for not visible rect
  832. .on('dblclick', evnt => this.enlargePad(evnt, true))
  833. .on('click', () => this.selectObjectPainter())
  834. .on('mouseenter', () => this.showObjectStatus())
  835. .on('contextmenu', settings.ContextMenu ? evnt => this.padContextMenu(evnt) : null);
  836. }
  837. svg_pad.append('svg:g').attr('class', 'primitives_layer');
  838. if (!is_batch) {
  839. btns = svg_pad.append('svg:g')
  840. .attr('class', 'btns_layer')
  841. .property('leftside', settings.ToolBarSide !== 'left')
  842. .property('vertical', settings.ToolBarVert);
  843. }
  844. }
  845. this.createAttFill({ attr: this.#pad });
  846. this.createAttLine({ attr: this.#pad, color0: !this.#pad.fBorderMode ? 'none' : '' });
  847. svg_pad.style('display', pad_visible ? null : 'none')
  848. .attr('viewBox', `0 0 ${w} ${h}`) // due to svg
  849. .attr('preserveAspectRatio', 'none') // due to svg, we do not preserve relative ratio
  850. .attr('x', x) // due to svg
  851. .attr('y', y) // due to svg
  852. .attr('width', w) // due to svg
  853. .attr('height', h) // due to svg
  854. .property('draw_x', x) // this is to make similar with canvas
  855. .property('draw_y', y)
  856. .property('draw_width', w)
  857. .property('draw_height', h);
  858. this.#pad_scale = this.getCanvPainter().getPadScale();
  859. this.#pad_x = x;
  860. this.#pad_y = y;
  861. this.#pad_width = w;
  862. this.#pad_height = h;
  863. this.addPadBorder(svg_pad, svg_border, true);
  864. this.setFastDrawing(w * (1 - this.#pad.fLeftMargin - this.#pad.fRightMargin), h * (1 - this.#pad.fBottomMargin - this.#pad.fTopMargin));
  865. // special case of 3D canvas overlay
  866. if (svg_pad.property('can3d') === constants.Embed3D.Overlay) {
  867. this.selectDom().select('.draw3d_' + this.#pad_name)
  868. .style('display', pad_visible ? '' : 'none');
  869. }
  870. if (this.alignButtons && btns)
  871. this.alignButtons(btns, this.#pad_width, this.#pad_height);
  872. return pad_visible;
  873. }
  874. /** @summary Add border decorations
  875. * @private */
  876. addPadBorder(svg_pad, svg_border, draw_line) {
  877. if (!svg_border)
  878. return;
  879. svg_border.attr('d', `M0,0H${this.#pad_width}V${this.#pad_height}H0Z`)
  880. .call(this.fillatt.func);
  881. if (draw_line)
  882. svg_border.call(this.lineatt.func);
  883. this.drawActiveBorder(svg_border);
  884. let svg_border1 = svg_pad.selectChild('.root_pad_border1'),
  885. svg_border2 = svg_pad.selectChild('.root_pad_border2');
  886. if (this.#pad.fBorderMode && this.#pad.fBorderSize) {
  887. const arr = getBoxDecorations(0, 0, this.#pad_width, this.#pad_height, this.#pad.fBorderMode, this.#pad.fBorderSize, this.#pad.fBorderSize);
  888. if (svg_border2.empty())
  889. svg_border2 = svg_pad.insert('svg:path', '.primitives_layer').attr('class', 'root_pad_border2');
  890. if (svg_border1.empty())
  891. svg_border1 = svg_pad.insert('svg:path', '.primitives_layer').attr('class', 'root_pad_border1');
  892. svg_border1.attr('d', arr[0])
  893. .call(this.fillatt.func)
  894. .style('fill', d3_rgb(this.fillatt.color).brighter(0.5).formatRgb());
  895. svg_border2.attr('d', arr[1])
  896. .call(this.fillatt.func)
  897. .style('fill', d3_rgb(this.fillatt.color).darker(0.5).formatRgb());
  898. } else {
  899. svg_border1.remove();
  900. svg_border2.remove();
  901. }
  902. }
  903. /** @summary Add pad interactive features like dragging and resize
  904. * @private */
  905. addPadInteractive(cleanup = false) {
  906. if (isFunc(this.$userInteractive)) {
  907. this.$userInteractive();
  908. delete this.$userInteractive;
  909. }
  910. if (this.isBatchMode() || this.isCanvas() || !this.isEditable())
  911. return;
  912. const svg_can = this.getCanvSvg(),
  913. width = svg_can.property('draw_width'),
  914. height = svg_can.property('draw_height');
  915. addDragHandler(this, {
  916. cleanup, // do cleanup to let assign new handlers later on
  917. x: this.#pad_x, y: this.#pad_y, width: this.#pad_width, height: this.#pad_height, no_transform: true,
  918. only_resize: true,
  919. is_disabled: kind => svg_can.property('pad_enlarged') || this.btns_active_flag ||
  920. (kind === 'move' && (this.options._disable_dragging || this.getFramePainter()?.mode3d)),
  921. getDrawG: () => this.getPadSvg(),
  922. pad_rect: { width, height },
  923. minwidth: 20, minheight: 20,
  924. move_resize: (_x, _y, _w, _h) => {
  925. const x0 = this.#pad.fAbsXlowNDC,
  926. y0 = this.#pad.fAbsYlowNDC,
  927. scale_w = _w / width / this.#pad.fAbsWNDC,
  928. scale_h = _h / height / this.#pad.fAbsHNDC,
  929. shift_x = _x / width - x0,
  930. shift_y = 1 - (_y + _h) / height - y0;
  931. this.forEachPainterInPad(p => {
  932. const subpad = p.getRootPad();
  933. subpad.fAbsXlowNDC += (subpad.fAbsXlowNDC - x0) * (scale_w - 1) + shift_x;
  934. subpad.fAbsYlowNDC += (subpad.fAbsYlowNDC - y0) * (scale_h - 1) + shift_y;
  935. subpad.fAbsWNDC *= scale_w;
  936. subpad.fAbsHNDC *= scale_h;
  937. }, 'pads');
  938. },
  939. redraw: () => this.interactiveRedraw('pad', 'padpos')
  940. });
  941. }
  942. /** @summary Disable pad drawing
  943. * @desc Complete SVG element will be hidden */
  944. disablePadDrawing() {
  945. if (!this.#pad_draw_disabled && !this.isTopPad()) {
  946. this.#pad_draw_disabled = true;
  947. this.createPadSvg(true);
  948. }
  949. }
  950. /** @summary Check if it is special object, which should be handled separately
  951. * @desc It can be TStyle or list of colors or palette object
  952. * @return {boolean} true if any */
  953. checkSpecial(obj) {
  954. if (!obj)
  955. return false;
  956. if (obj._typename === clTStyle) {
  957. Object.assign(gStyle, obj);
  958. return true;
  959. }
  960. const o = this.getOptions(true);
  961. if ((obj._typename === clTObjArray) && (obj.name === 'ListOfColors')) {
  962. if (o?.CreatePalette) {
  963. let arr = [];
  964. for (let n = obj.arr.length - o.CreatePalette; n < obj.arr.length; ++n) {
  965. const col = getRGBfromTColor(obj.arr[n]);
  966. if (!col) { console.log('Fail to create color for palette'); arr = null; break; }
  967. arr.push(col);
  968. }
  969. if (arr.length)
  970. this.#custom_palette = new ColorPalette(arr);
  971. }
  972. if (!o || o.GlobalColors) // set global list of colors
  973. adoptRootColors(obj);
  974. // copy existing colors and extend with new values
  975. this.#custom_colors = o?.LocalColors ? extendRootColors(null, obj) : null;
  976. return true;
  977. }
  978. if ((obj._typename === clTObjArray) && (obj.name === 'CurrentColorPalette')) {
  979. const arr = [], indx = [];
  980. let missing = false;
  981. for (let n = 0; n < obj.arr.length; ++n) {
  982. const col = obj.arr[n];
  983. if (col?._typename === clTColor) {
  984. indx[n] = col.fNumber;
  985. arr[n] = getRGBfromTColor(col);
  986. } else {
  987. console.log(`Missing color with index ${n}`);
  988. missing = true;
  989. }
  990. }
  991. const apply = (!o || (!missing && !o.IgnorePalette));
  992. this.#custom_palette_indexes = apply ? indx : null;
  993. this.#custom_palette_colors = apply ? arr : null;
  994. return true;
  995. }
  996. return false;
  997. }
  998. /** @summary Check if special objects appears in primitives
  999. * @desc it could be list of colors or palette */
  1000. checkSpecialsInPrimitives(can, count_specials) {
  1001. const lst = can?.fPrimitives;
  1002. if (count_specials)
  1003. this.#num_specials = 0;
  1004. if (!lst)
  1005. return;
  1006. for (let i = 0; i < lst.arr?.length; ++i) {
  1007. if (this.checkSpecial(lst.arr[i])) {
  1008. lst.arr[i].$special = true; // mark object as special one, do not use in drawing
  1009. if (count_specials)
  1010. this.#num_specials++;
  1011. }
  1012. }
  1013. }
  1014. /** @summary try to find object by name in list of pad primitives
  1015. * @desc used to find title drawing
  1016. * @private */
  1017. findInPrimitives(objname, objtype) {
  1018. const match = obj => obj && (obj?.fName === objname) && (objtype ? (obj?._typename === objtype) : true),
  1019. snap = this.#snap_primitives?.find(s => match((s.fKind === webSnapIds.kObject) ? s.fSnapshot : null));
  1020. return snap ? snap.fSnapshot : this.#pad?.fPrimitives?.arr.find(match);
  1021. }
  1022. /** @summary Try to find painter for specified object
  1023. * @desc can be used to find painter for some special objects, registered as
  1024. * histogram functions
  1025. * @param {object} selobj - object to which painter should be search, set null to ignore parameter
  1026. * @param {string} [selname] - object name, set to null to ignore
  1027. * @param {string} [seltype] - object type, set to null to ignore
  1028. * @return {object} - painter for specified object (if any)
  1029. * @private */
  1030. findPainterFor(selobj, selname, seltype) {
  1031. return this.#painters.find(p => {
  1032. const pobj = p.getObject();
  1033. if (!pobj) return false;
  1034. if (selobj && (pobj === selobj)) return true;
  1035. if (!selname && !seltype) return false;
  1036. if (selname && (pobj.fName !== selname)) return false;
  1037. if (seltype && (pobj._typename !== seltype)) return false;
  1038. return true;
  1039. });
  1040. }
  1041. /** @summary Return true if any objects beside sub-pads exists in the pad */
  1042. hasObjectsToDraw() {
  1043. return this.#pad?.fPrimitives?.arr?.find(obj => obj._typename !== clTPad);
  1044. }
  1045. /** @summary sync drawing/redrawing/resize of the pad
  1046. * @param {string} kind - kind of draw operation, if true - always queued
  1047. * @return {Promise} when pad is ready for draw operation or false if operation already queued
  1048. * @private */
  1049. syncDraw(kind) {
  1050. const entry = { kind: kind || 'redraw' };
  1051. if (this.#doing_draw === undefined) {
  1052. this.#doing_draw = [entry];
  1053. return Promise.resolve(true);
  1054. }
  1055. // if queued operation registered, ignore next calls, indx === 0 is running operation
  1056. if ((entry.kind !== true) && (this.#doing_draw.findIndex((e, i) => (i > 0) && (e.kind === entry.kind)) > 0))
  1057. return false;
  1058. this.#doing_draw.push(entry);
  1059. return new Promise(resolveFunc => {
  1060. entry.func = resolveFunc;
  1061. });
  1062. }
  1063. /** @summary indicates if painter performing objects draw
  1064. * @private */
  1065. doingDraw() {
  1066. return this.#doing_draw !== undefined;
  1067. }
  1068. /** @summary confirms that drawing is completed, may trigger next drawing immediately
  1069. * @private */
  1070. confirmDraw() {
  1071. if (this.#doing_draw === undefined)
  1072. return console.warn('failure, should not happen');
  1073. this.#doing_draw.shift();
  1074. if (!this.#doing_draw.length)
  1075. this.#doing_draw = undefined;
  1076. else {
  1077. const entry = this.#doing_draw[0];
  1078. if (entry.func) { entry.func(); delete entry.func; }
  1079. }
  1080. }
  1081. /** @summary Draw single primitive */
  1082. async drawObject(/* dom, obj, opt */) {
  1083. console.log('Not possible to draw object without loading of draw.mjs');
  1084. return null;
  1085. }
  1086. /** @summary Draw pad primitives
  1087. * @return {Promise} when drawing completed
  1088. * @private */
  1089. async drawPrimitives(indx) {
  1090. if (indx === undefined) {
  1091. if (this.isCanvas())
  1092. this.#start_draw_tm = new Date().getTime();
  1093. // set number of primitives
  1094. this.#num_primitives = this.#pad?.fPrimitives?.arr?.length || 0;
  1095. // sync to prevent immediate pad redraw during normal drawing sequence
  1096. return this.syncDraw(true).then(() => this.drawPrimitives(0));
  1097. }
  1098. if (!this.#pad || (indx >= this.#num_primitives)) {
  1099. if (this.#start_draw_tm) {
  1100. const spenttm = new Date().getTime() - this.#start_draw_tm;
  1101. if (spenttm > 1000)
  1102. console.log(`Canvas ${this.#pad?.fName || '---'} drawing took ${(spenttm*1e-3).toFixed(2)}s`);
  1103. this.#start_draw_tm = undefined;
  1104. }
  1105. this.confirmDraw();
  1106. return;
  1107. }
  1108. const obj = this.#pad.fPrimitives.arr[indx];
  1109. if (!obj || obj.$special || ((indx > 0) && (obj._typename === clTFrame) && this.getFramePainter()))
  1110. return this.drawPrimitives(indx+1);
  1111. // use of Promise should avoid large call-stack depth when many primitives are drawn
  1112. return this.drawObject(this, obj, this.#pad.fPrimitives.opt[indx]).then(op => {
  1113. if (isObject(op))
  1114. op._primitive = true; // mark painter as belonging to primitives
  1115. return this.drawPrimitives(indx+1);
  1116. });
  1117. }
  1118. /** @summary Divide pad on sub-pads
  1119. * @return {Promise} when finished
  1120. * @private */
  1121. async divide(nx, ny, use_existing) {
  1122. let color = this.#pad.fFillColor;
  1123. if (!use_existing) {
  1124. if (color < 15)
  1125. color = 19;
  1126. else if (color < 20)
  1127. color--;
  1128. }
  1129. if (nx && !ny && use_existing) {
  1130. for (let k = 0; k < nx; ++k) {
  1131. if (!this.getSubPadPainter(k+1)) {
  1132. use_existing = false;
  1133. break;
  1134. }
  1135. }
  1136. if (use_existing)
  1137. return this;
  1138. }
  1139. this.cleanPrimitives(isPadPainter);
  1140. if (!this.#pad.fPrimitives)
  1141. this.#pad.fPrimitives = create(clTList);
  1142. this.#pad.fPrimitives.Clear();
  1143. if ((!nx && !ny) || !this.#pad.Divide(nx, ny, 0.01, 0.01, color))
  1144. return this;
  1145. const drawNext = indx => {
  1146. if (indx >= this.#pad.fPrimitives.arr.length)
  1147. return this;
  1148. return this.drawObject(this, this.#pad.fPrimitives.arr[indx]).then(() => drawNext(indx + 1));
  1149. };
  1150. return drawNext(0);
  1151. }
  1152. /** @summary Return sub-pads painter, only direct childs are checked
  1153. * @private */
  1154. getSubPadPainter(n) {
  1155. for (let k = 0; k < this.#painters.length; ++k) {
  1156. const sub = this.#painters[k];
  1157. if (isPadPainter(sub) && (sub.getRootPad()?.fNumber === n))
  1158. return sub;
  1159. }
  1160. return null;
  1161. }
  1162. /** @summary Process tooltip event in the pad
  1163. * @private */
  1164. processPadTooltipEvent(pnt) {
  1165. const painters = [], hints = [];
  1166. // first count - how many processors are there
  1167. this.#painters?.forEach(obj => {
  1168. if (isFunc(obj.processTooltipEvent))
  1169. painters.push(obj);
  1170. });
  1171. if (pnt) pnt.nproc = painters.length;
  1172. painters.forEach(obj => {
  1173. const hint = obj.processTooltipEvent(pnt) || { user_info: null };
  1174. hints.push(hint);
  1175. if (pnt?.painters)
  1176. hint.painter = obj;
  1177. });
  1178. return hints;
  1179. }
  1180. /** @summary Changes canvas dark mode
  1181. * @private */
  1182. changeDarkMode(mode) {
  1183. this.getCanvSvg().style('filter', (mode ?? settings.DarkMode) ? 'invert(100%)' : null);
  1184. }
  1185. /** @summary Fill pad context menu
  1186. * @private */
  1187. fillContextMenu(menu) {
  1188. const pad = this.getRootPad(true);
  1189. if (!pad)
  1190. return false;
  1191. menu.header(`${pad._typename}::${pad.fName}`, `${urlClassPrefix}${pad._typename}.html`);
  1192. menu.addchk(this.isTooltipAllowed(), 'Show tooltips', () => this.setTooltipAllowed('toggle'));
  1193. menu.addchk(pad.fGridx, 'Grid x', flag => {
  1194. pad.fGridx = flag ? 1 : 0;
  1195. this.interactiveRedraw('pad', `exec:SetGridx(${flag ? 1 : 0})`);
  1196. });
  1197. menu.addchk(pad.fGridy, 'Grid y', flag => {
  1198. pad.fGridy = flag ? 1 : 0;
  1199. this.interactiveRedraw('pad', `exec:SetGridy(${flag ? 1 : 0})`);
  1200. });
  1201. menu.sub('Ticks x');
  1202. menu.addchk(pad.fTickx === 0, 'normal', () => {
  1203. pad.fTickx = 0;
  1204. this.interactiveRedraw('pad', 'exec:SetTickx(0)');
  1205. });
  1206. menu.addchk(pad.fTickx === 1, 'ticks on both sides', () => {
  1207. pad.fTickx = 1;
  1208. this.interactiveRedraw('pad', 'exec:SetTickx(1)');
  1209. });
  1210. menu.addchk(pad.fTickx === 2, 'labels on both sides', () => {
  1211. pad.fTickx = 2;
  1212. this.interactiveRedraw('pad', 'exec:SetTickx(2)');
  1213. });
  1214. menu.endsub();
  1215. menu.sub('Ticks y');
  1216. menu.addchk(pad.fTicky === 0, 'normal', () => {
  1217. pad.fTicky = 0;
  1218. this.interactiveRedraw('pad', 'exec:SetTicky(0)');
  1219. });
  1220. menu.addchk(pad.fTicky === 1, 'ticks on both sides', () => {
  1221. pad.fTicky = 1;
  1222. this.interactiveRedraw('pad', 'exec:SetTicky(1)');
  1223. });
  1224. menu.addchk(pad.fTicky === 2, 'labels on both sides', () => {
  1225. pad.fTicky = 2;
  1226. this.interactiveRedraw('pad', 'exec:SetTicky(2)');
  1227. });
  1228. menu.endsub();
  1229. menu.addchk(pad.fEditable, 'Editable', flag => {
  1230. pad.fEditable = flag;
  1231. this.interactiveRedraw('pad', `exec:SetEditable(${flag})`);
  1232. });
  1233. if (this.isCanvas()) {
  1234. menu.addchk(pad.TestBit(kIsGrayscale), 'Gray scale', flag => {
  1235. this.setGrayscale(flag);
  1236. this.interactiveRedraw('pad', `exec:SetGrayscale(${flag})`);
  1237. });
  1238. }
  1239. menu.sub('Border');
  1240. menu.addSelectMenu('Mode', ['Down', 'Off', 'Up'], pad.fBorderMode + 1, v => {
  1241. pad.fBorderMode = v - 1;
  1242. this.interactiveRedraw(true, `exec:SetBorderMode(${v-1})`);
  1243. }, 'Pad border mode');
  1244. menu.addSizeMenu('Size', 0, 20, 2, pad.fBorderSize, v => {
  1245. pad.fBorderSize = v;
  1246. this.interactiveRedraw(true, `exec:SetBorderSize(${v})`);
  1247. }, 'Pad border size');
  1248. menu.endsub();
  1249. menu.addAttributesMenu(this);
  1250. if (!this.isCanvas(true)) {
  1251. // if not online canvas -
  1252. const do_divide = arg => {
  1253. if (!arg || !isStr(arg))
  1254. return;
  1255. // delete auto_canvas flag to prevent deletion
  1256. if (this.#iscan === 'auto')
  1257. this.#iscan = true;
  1258. this.cleanPrimitives(true);
  1259. if (arg === 'reset')
  1260. return;
  1261. const arr = arg.split('x');
  1262. if (arr.length === 1)
  1263. this.divide(Number.parseInt(arr[0]));
  1264. else if (arr.length === 2)
  1265. this.divide(Number.parseInt(arr[0]), Number.parseInt(arr[1]));
  1266. };
  1267. if (isFunc(this.drawObject))
  1268. menu.add('Build legend', () => this.buildLegend());
  1269. menu.sub('Divide', () => menu.input('Input divide arg', '2x2').then(do_divide), 'Divide on sub-pads');
  1270. ['1x2', '2x1', '2x2', '2x3', '3x2', '3x3', '4x4', 'reset'].forEach(item => menu.add(item, item, do_divide));
  1271. menu.endsub();
  1272. menu.add('Save to gStyle', () => {
  1273. this.fillatt?.saveToStyle(this.isCanvas() ? 'fCanvasColor' : 'fPadColor');
  1274. gStyle.fPadGridX = pad.fGridx;
  1275. gStyle.fPadGridY = pad.fGridy;
  1276. gStyle.fPadTickX = pad.fTickx;
  1277. gStyle.fPadTickY = pad.fTicky;
  1278. gStyle.fOptLogx = pad.fLogx;
  1279. gStyle.fOptLogy = pad.fLogy;
  1280. gStyle.fOptLogz = pad.fLogz;
  1281. }, 'Store pad fill attributes, grid, tick and log scale settings to gStyle');
  1282. if (this.isCanvas()) {
  1283. menu.addSettingsMenu(false, false, arg => {
  1284. if (arg === 'dark') this.changeDarkMode();
  1285. });
  1286. }
  1287. }
  1288. menu.separator();
  1289. if (isFunc(this.hasMenuBar) && isFunc(this.actiavteMenuBar))
  1290. menu.addchk(this.hasMenuBar(), 'Menu bar', flag => this.actiavteMenuBar(flag));
  1291. if (isFunc(this.hasEventStatus) && isFunc(this.activateStatusBar) && isFunc(this.canStatusBar)) {
  1292. if (this.canStatusBar())
  1293. menu.addchk(this.hasEventStatus(), 'Event status', () => this.activateStatusBar('toggle'));
  1294. }
  1295. if (this.enlargeMain() || (!this.isTopPad() && this.hasObjectsToDraw()))
  1296. menu.addchk(this.isPadEnlarged(), 'Enlarge ' + (this.isCanvas() ? 'canvas' : 'pad'), () => this.enlargePad());
  1297. const fname = this.#pad_name || (this.isCanvas() ? 'canvas' : 'pad');
  1298. menu.sub('Save as');
  1299. const fmts = ['svg', 'png', 'jpeg', 'webp'];
  1300. if (internals.makePDF) fmts.push('pdf');
  1301. fmts.forEach(fmt => menu.add(`${fname}.${fmt}`, () => this.saveAs(fmt, this.isCanvas(), `${fname}.${fmt}`)));
  1302. if (this.isCanvas()) {
  1303. menu.separator();
  1304. menu.add(`${fname}.json`, () => this.saveAs('json', true, `${fname}.json`), 'Produce JSON with line spacing');
  1305. menu.add(`${fname}0.json`, () => this.saveAs('json', false, `${fname}0.json`), 'Produce JSON without line spacing');
  1306. }
  1307. menu.endsub();
  1308. return true;
  1309. }
  1310. /** @summary Show pad context menu
  1311. * @private */
  1312. async padContextMenu(evnt) {
  1313. if (evnt.stopPropagation) {
  1314. // this is normal event processing and not emulated jsroot event
  1315. evnt.stopPropagation(); // disable main context menu
  1316. evnt.preventDefault(); // disable browser context menu
  1317. this.getFramePainter()?.setLastEventPos();
  1318. }
  1319. return createMenu(evnt, this).then(menu => {
  1320. this.fillContextMenu(menu);
  1321. return this.fillObjectExecMenu(menu, '');
  1322. }).then(menu => menu.show());
  1323. }
  1324. /** @summary Redraw TLegend object
  1325. * @desc Used when object attributes are changed to ensure that legend is up to date
  1326. * @private */
  1327. async redrawLegend() {
  1328. return this.findPainterFor(null, '', clTLegend)?.redraw();
  1329. }
  1330. /** @summary Redraw pad means redraw ourself
  1331. * @return {Promise} when redrawing ready */
  1332. async redrawPad(reason) {
  1333. const sync_promise = this.syncDraw(reason);
  1334. if (sync_promise === false) {
  1335. console.log(`Prevent redrawing of ${this.#pad.fName}`);
  1336. return false;
  1337. }
  1338. let showsubitems = true;
  1339. const redrawNext = indx => {
  1340. while (indx < this.#painters.length) {
  1341. const sub = this.#painters[indx++];
  1342. let res = 0;
  1343. if (showsubitems || isPadPainter(sub))
  1344. res = sub.redraw(reason);
  1345. if (isPromise(res))
  1346. return res.then(() => redrawNext(indx));
  1347. }
  1348. return true;
  1349. };
  1350. return sync_promise.then(() => {
  1351. if (this.isCanvas())
  1352. this.createCanvasSvg(2);
  1353. else
  1354. showsubitems = this.createPadSvg(true);
  1355. return redrawNext(0);
  1356. }).then(() => {
  1357. this.addPadInteractive();
  1358. this.confirmDraw();
  1359. if (getActivePad() === this)
  1360. this.getCanvPainter()?.producePadEvent('padredraw', this);
  1361. return true;
  1362. });
  1363. }
  1364. /** @summary redraw pad */
  1365. redraw(reason) {
  1366. // intentionally do not return Promise to let re-draw sub-pads in parallel
  1367. this.redrawPad(reason);
  1368. }
  1369. /** @summary Checks if pad should be redrawn by resize
  1370. * @private */
  1371. needRedrawByResize() {
  1372. const elem = this.getPadSvg();
  1373. if (!elem.empty() && elem.property('can3d') === constants.Embed3D.Overlay) return true;
  1374. return this.#painters.findIndex(objp => {
  1375. return isFunc(objp.needRedrawByResize) ? objp.needRedrawByResize() : false;
  1376. }) >= 0;
  1377. }
  1378. /** @summary Check resize of canvas
  1379. * @return {Promise} with result or false */
  1380. checkCanvasResize(size, force) {
  1381. if (this._ignore_resize || !this.isTopPad())
  1382. return false;
  1383. const sync_promise = this.syncDraw('canvas_resize');
  1384. if (sync_promise === false)
  1385. return false;
  1386. if ((size === true) || (size === false)) { force = size; size = null; }
  1387. if (isObject(size) && size.force) force = true;
  1388. if (!force) force = this.needRedrawByResize();
  1389. let changed = false;
  1390. const redrawNext = indx => {
  1391. if (!changed || (indx >= this.#painters.length)) {
  1392. this.confirmDraw();
  1393. return changed;
  1394. }
  1395. return getPromise(this.#painters[indx].redraw(force ? 'redraw' : 'resize')).then(() => redrawNext(indx+1));
  1396. };
  1397. // return sync_promise.then(() => this.ensureBrowserSize(this.#pad?.fCw, this.#pad?.fCh)).then(() => {
  1398. return sync_promise.then(() => {
  1399. changed = this.createCanvasSvg(force ? 2 : 1, size);
  1400. if (changed && this.isCanvas() && this.#pad && this.online_canvas && !this.embed_canvas && !this.isBatchMode()) {
  1401. if (this.#resize_tmout)
  1402. clearTimeout(this.#resize_tmout);
  1403. this.#resize_tmout = setTimeout(() => {
  1404. this.#resize_tmout = undefined;
  1405. if (isFunc(this.sendResized))
  1406. this.sendResized();
  1407. }, 1000); // long enough delay to prevent multiple occurrence
  1408. }
  1409. // if canvas changed, redraw all its subitems.
  1410. // If redrawing was forced for canvas, same applied for sub-elements
  1411. return redrawNext(0);
  1412. });
  1413. }
  1414. /** @summary Update TPad object */
  1415. updateObject(obj) {
  1416. const pad = this.getRootPad();
  1417. if (!obj || !pad)
  1418. return false;
  1419. pad.fBits = obj.fBits;
  1420. pad.fTitle = obj.fTitle;
  1421. pad.fGridx = obj.fGridx;
  1422. pad.fGridy = obj.fGridy;
  1423. pad.fTickx = obj.fTickx;
  1424. pad.fTicky = obj.fTicky;
  1425. pad.fLogx = obj.fLogx;
  1426. pad.fLogy = obj.fLogy;
  1427. pad.fLogz = obj.fLogz;
  1428. pad.fUxmin = obj.fUxmin;
  1429. pad.fUxmax = obj.fUxmax;
  1430. pad.fUymin = obj.fUymin;
  1431. pad.fUymax = obj.fUymax;
  1432. pad.fX1 = obj.fX1;
  1433. pad.fX2 = obj.fX2;
  1434. pad.fY1 = obj.fY1;
  1435. pad.fY2 = obj.fY2;
  1436. // this is main coordinates for sub-pad relative to canvas
  1437. pad.fAbsWNDC = obj.fAbsWNDC;
  1438. pad.fAbsHNDC = obj.fAbsHNDC;
  1439. pad.fAbsXlowNDC = obj.fAbsXlowNDC;
  1440. pad.fAbsYlowNDC = obj.fAbsYlowNDC;
  1441. pad.fLeftMargin = obj.fLeftMargin;
  1442. pad.fRightMargin = obj.fRightMargin;
  1443. pad.fBottomMargin = obj.fBottomMargin;
  1444. pad.fTopMargin = obj.fTopMargin;
  1445. pad.fFillColor = obj.fFillColor;
  1446. pad.fFillStyle = obj.fFillStyle;
  1447. pad.fLineColor = obj.fLineColor;
  1448. pad.fLineStyle = obj.fLineStyle;
  1449. pad.fLineWidth = obj.fLineWidth;
  1450. pad.fPhi = obj.fPhi;
  1451. pad.fTheta = obj.fTheta;
  1452. pad.fEditable = obj.fEditable;
  1453. if (this.isCanvas())
  1454. this.checkSpecialsInPrimitives(obj);
  1455. const fp = this.getFramePainter();
  1456. fp?.updateAttributes(!fp.$modifiedNDC);
  1457. if (!obj.fPrimitives)
  1458. return false;
  1459. let isany = false, p = 0;
  1460. for (let n = 0; n < obj.fPrimitives.arr?.length; ++n) {
  1461. if (obj.fPrimitives.arr[n].$special)
  1462. continue;
  1463. while (p < this.#painters.length) {
  1464. const op = this.#painters[p++];
  1465. if (!op._primitive)
  1466. continue;
  1467. if (op.updateObject(obj.fPrimitives.arr[n], obj.fPrimitives.opt[n]))
  1468. isany = true;
  1469. break;
  1470. }
  1471. }
  1472. return isany;
  1473. }
  1474. /** @summary add legend object to the pad and redraw it
  1475. * @private */
  1476. async buildLegend(x1, y1, x2, y2, title, opt) {
  1477. const lp = this.findPainterFor(null, '', clTLegend);
  1478. if (!lp && !isFunc(this.drawObject))
  1479. return Promise.reject(Error('Not possible to build legend while module draw.mjs was not load'));
  1480. const leg = lp?.getObject() ?? create(clTLegend),
  1481. pad = this.getRootPad(true);
  1482. leg.fPrimitives.Clear();
  1483. for (let k = 0; k < this.#painters.length; ++k) {
  1484. const painter = this.#painters[k],
  1485. obj = painter.getObject();
  1486. if (!obj || obj.fName === kTitle || obj.fName === 'stats' || painter.draw_content === false ||
  1487. obj._typename === clTLegend || obj._typename === clTHStack || obj._typename === clTMultiGraph)
  1488. continue;
  1489. const entry = create(clTLegendEntry);
  1490. entry.fObject = obj;
  1491. entry.fLabel = painter.getItemName();
  1492. if ((opt === 'all') || !entry.fLabel)
  1493. entry.fLabel = obj.fName;
  1494. entry.fOption = '';
  1495. if (!entry.fLabel) continue;
  1496. if (painter.lineatt?.used)
  1497. entry.fOption += 'l';
  1498. if (painter.fillatt?.used)
  1499. entry.fOption += 'f';
  1500. if (painter.markeratt?.used)
  1501. entry.fOption += 'p';
  1502. if (!entry.fOption)
  1503. entry.fOption = 'l';
  1504. leg.fPrimitives.Add(entry);
  1505. }
  1506. if (lp)
  1507. return lp.redraw();
  1508. const szx = 0.4;
  1509. let szy = leg.fPrimitives.arr.length;
  1510. // no entries - no need to draw legend
  1511. if (!szy) return null;
  1512. if (szy > 8) szy = 8;
  1513. szy *= 0.1;
  1514. if ((x1 === x2) || (y1 === y2)) {
  1515. leg.fX1NDC = szx * pad.fLeftMargin + (1 - szx) * (1 - pad.fRightMargin);
  1516. leg.fY1NDC = (1 - szy) * (1 - pad.fTopMargin) + szy * pad.fBottomMargin;
  1517. leg.fX2NDC = 0.99 - pad.fRightMargin;
  1518. leg.fY2NDC = 0.99 - pad.fTopMargin;
  1519. if (opt === undefined)
  1520. opt = 'autoplace';
  1521. } else {
  1522. leg.fX1NDC = x1;
  1523. leg.fY1NDC = y1;
  1524. leg.fX2NDC = x2;
  1525. leg.fY2NDC = y2;
  1526. }
  1527. leg.fFillStyle = 1001;
  1528. leg.fTitle = title ?? '';
  1529. return this.drawObject(this, leg, opt);
  1530. }
  1531. /** @summary Add object painter to list of primitives
  1532. * @private */
  1533. addObjectPainter(objpainter, lst, indx) {
  1534. if (objpainter && lst && lst[indx] && !objpainter.hasSnapId()) {
  1535. // keep snap id in painter, will be used for the
  1536. if (this.#painters.indexOf(objpainter) < 0)
  1537. this.#painters.push(objpainter);
  1538. objpainter.assignSnapId(lst[indx].fObjectID);
  1539. const setSubSnaps = p => {
  1540. if (!p.isPrimary())
  1541. return;
  1542. for (let k = 0; k < this.#painters.length; ++k) {
  1543. const sub = this.#painters[k];
  1544. if (sub.isSecondary(p) && sub.getSecondaryId()) {
  1545. sub.assignSnapId(p.getSnapId() + '#' + sub.getSecondaryId());
  1546. setSubSnaps(sub);
  1547. }
  1548. }
  1549. };
  1550. setSubSnaps(objpainter);
  1551. }
  1552. }
  1553. /** @summary Process snap with style
  1554. * @private */
  1555. processSnapStyle(snap) {
  1556. Object.assign(gStyle, snap.fSnapshot);
  1557. }
  1558. /** @summary Process snap with colors
  1559. * @private */
  1560. processSnapColors(snap) {
  1561. const ListOfColors = decodeWebCanvasColors(snap.fSnapshot.fOper),
  1562. o = this.getOptions(true);
  1563. // set global list of colors
  1564. if (!o || o.GlobalColors)
  1565. adoptRootColors(ListOfColors);
  1566. const greyscale = this.#pad?.TestBit(kIsGrayscale) ?? false,
  1567. colors = extendRootColors(null, ListOfColors, greyscale);
  1568. // copy existing colors and extend with new values
  1569. this.#custom_colors = o?.LocalColors ? colors : null;
  1570. // set palette
  1571. if (snap.fSnapshot.fBuf && (!o || !o.IgnorePalette)) {
  1572. const indexes = [], palette = [];
  1573. for (let n = 0; n < snap.fSnapshot.fBuf.length; ++n) {
  1574. indexes[n] = Math.round(snap.fSnapshot.fBuf[n]);
  1575. palette[n] = colors[indexes[n]];
  1576. }
  1577. this.#custom_palette_indexes = indexes;
  1578. this.#custom_palette_colors = palette;
  1579. this.#custom_palette = new ColorPalette(palette, greyscale);
  1580. } else
  1581. this.#custom_palette = this.#custom_palette_indexes = this.#custom_palette_colors = undefined;
  1582. }
  1583. /** @summary Process snap with custom font
  1584. * @private */
  1585. processSnapFont(snap) {
  1586. const arr = snap.fSnapshot.fOper.split(':');
  1587. addCustomFont(Number.parseInt(arr[0]), arr[1], arr[2], arr[3]);
  1588. }
  1589. /** @summary Process special snaps like colors or style objects
  1590. * @return {Promise} index where processing should start
  1591. * @private */
  1592. processSpecialSnaps(lst) {
  1593. while (lst?.length) {
  1594. const snap = lst[0];
  1595. // gStyle object
  1596. if (snap.fKind === webSnapIds.kStyle) {
  1597. lst.shift();
  1598. this.processSnapStyle(snap);
  1599. } else if (snap.fKind === webSnapIds.kColors) {
  1600. lst.shift();
  1601. this.processSnapColors(snap);
  1602. } else if (snap.fKind === webSnapIds.kFont) {
  1603. lst.shift();
  1604. this.processSnapFont(snap);
  1605. } else
  1606. break;
  1607. }
  1608. }
  1609. /** @summary Function called when drawing next snapshot from the list
  1610. * @return {Promise} for drawing of the snap
  1611. * @private */
  1612. async drawNextSnap(lst, pindx, indx) {
  1613. if (indx === undefined) {
  1614. indx = -1;
  1615. this.#num_primitives = lst?.length ?? 0;
  1616. }
  1617. ++indx; // change to the next snap
  1618. if (!lst || (indx >= lst.length))
  1619. return this;
  1620. const snap = lst[indx], is_subpad = (snap.fKind === webSnapIds.kSubPad);
  1621. // gStyle object
  1622. if (snap.fKind === webSnapIds.kStyle) {
  1623. this.processSnapStyle(snap);
  1624. return this.drawNextSnap(lst, pindx, indx); // call next
  1625. }
  1626. // list of colors
  1627. if (snap.fKind === webSnapIds.kColors) {
  1628. this.processSnapColors(snap);
  1629. return this.drawNextSnap(lst, pindx, indx); // call next
  1630. }
  1631. // try to locate existing object painter, only allowed when redrawing pad snap
  1632. let objpainter, promise;
  1633. while ((pindx !== undefined) && (pindx < this.#painters.length)) {
  1634. const subp = this.#painters[pindx++];
  1635. if (subp.getSnapId() === snap.fObjectID) {
  1636. objpainter = subp;
  1637. break;
  1638. } else if (subp.getSnapId() && !subp.isSecondary() && !is_subpad) {
  1639. console.warn(`Mismatch in snapid between painter ${subp.getSnapId()} secondary: ${subp.isSecondary()} type: ${subp.getClassName()} and primitive ${snap.fObjectID} kind ${snap.fKind} type ${snap.fSnapshot?._typename}`);
  1640. break;
  1641. }
  1642. }
  1643. if (objpainter) {
  1644. // painter exists - try to update drawing
  1645. if (is_subpad)
  1646. promise = objpainter.redrawPadSnap(snap);
  1647. else if (snap.fKind === webSnapIds.kObject) { // object itself
  1648. if (objpainter.updateObject(snap.fSnapshot, snap.fOption, true))
  1649. promise = objpainter.redraw();
  1650. } else if (snap.fKind === webSnapIds.kSVG) { // update SVG
  1651. if (objpainter.updateObject(snap.fSnapshot))
  1652. promise = objpainter.redraw();
  1653. }
  1654. } else if (is_subpad) {
  1655. const subpad = snap.fSnapshot;
  1656. subpad.fPrimitives = null; // clear primitives, they just because of I/O
  1657. const padpainter = new TPadPainter(this, subpad, snap.fOption, false, 'webpad');
  1658. padpainter.assignSnapId(snap.fObjectID);
  1659. padpainter.is_active_pad = Boolean(snap.fActive); // enforce boolean flag
  1660. padpainter.#readonly = snap.fReadOnly ?? false; // readonly flag
  1661. padpainter.#snap_primitives = snap.fPrimitives; // keep list to be able find primitive
  1662. padpainter.#has_execs = snap.fHasExecs ?? false; // are there pad execs, enables some interactive features
  1663. padpainter.processSpecialSnaps(snap.fPrimitives); // need to process style and colors before creating graph elements
  1664. padpainter.createPadSvg();
  1665. if (padpainter.matchObjectType(clTPad) && snap.fPrimitives.length)
  1666. padpainter.addPadButtons(true);
  1667. pindx++; // new painter will be add
  1668. promise = padpainter.drawNextSnap(snap.fPrimitives).then(() => padpainter.addPadInteractive());
  1669. } else if (((snap.fKind === webSnapIds.kObject) || (snap.fKind === webSnapIds.kSVG)) && (snap.fOption !== '__ignore_drawing__')) {
  1670. // here the case of normal drawing
  1671. pindx++; // new painter will be add
  1672. promise = this.drawObject(this, snap.fSnapshot, snap.fOption).then(objp => this.addObjectPainter(objp, lst, indx));
  1673. }
  1674. return getPromise(promise).then(() => this.drawNextSnap(lst, pindx, indx)); // call next
  1675. }
  1676. /** @summary Return painter with specified id
  1677. * @private */
  1678. findSnap(snapid) {
  1679. if (this.getSnapId() === snapid)
  1680. return this;
  1681. if (!this.#painters)
  1682. return null;
  1683. for (let k = 0; k < this.#painters.length; ++k) {
  1684. let sub = this.#painters[k];
  1685. if (isFunc(sub.findSnap))
  1686. sub = sub.findSnap(snapid);
  1687. else if (sub.getSnapId() !== snapid)
  1688. sub = null;
  1689. if (sub)
  1690. return sub;
  1691. }
  1692. return null;
  1693. }
  1694. /** @summary Redraw pad snap
  1695. * @desc Online version of drawing pad primitives
  1696. * for the canvas snapshot contains list of objects
  1697. * as first entry, graphical properties of canvas itself is provided
  1698. * in ROOT6 it also includes primitives, but we ignore them
  1699. * @return {Promise} with pad painter when drawing completed
  1700. * @private */
  1701. async redrawPadSnap(snap) {
  1702. if (!snap?.fPrimitives)
  1703. return this;
  1704. this.is_active_pad = Boolean(snap.fActive); // enforce boolean flag
  1705. this.#readonly = snap.fReadOnly ?? false; // readonly flag
  1706. this.#snap_primitives = snap.fPrimitives; // keep list to be able find primitive
  1707. this.#has_execs = snap.fHasExecs ?? false; // are there pad execs, enables some interactive features
  1708. const first = snap.fSnapshot;
  1709. first.fPrimitives = null; // primitives are not interesting, they are disabled in IO
  1710. // if there are execs in the pad, deliver events to the server
  1711. this.#deliver_move_events = this.#has_execs || first.fExecs?.arr?.length;
  1712. if (!this.hasSnapId()) {
  1713. // first time getting snap, create all gui elements first
  1714. this.assignSnapId(snap.fObjectID);
  1715. this.assignObject(first);
  1716. this.#pad = first; // first object is pad
  1717. // this._setFixedSize(true);
  1718. // if canvas size not specified in batch mode, temporary use 900x700 size
  1719. if (this.isBatchMode() && (!first.fCw || !first.fCh)) { first.fCw = 900; first.fCh = 700; }
  1720. // case of ROOT7 with always dummy TPad as first entry
  1721. if (!first.fCw || !first.fCh)
  1722. this._setFixedSize(false);
  1723. const mainid = this.selectDom().attr('id');
  1724. if (!this.isBatchMode() && this.online_canvas && !this.use_openui && !this.brlayout && mainid && isStr(mainid) && !getHPainter()) {
  1725. this.brlayout = new BrowserLayout(mainid, null, this);
  1726. this.brlayout.create(mainid, true);
  1727. this.setDom(this.brlayout.drawing_divid()); // need to create canvas
  1728. registerForResize(this.brlayout);
  1729. }
  1730. this.processSpecialSnaps(snap.fPrimitives);
  1731. this.createCanvasSvg(0);
  1732. if (!this.isBatchMode())
  1733. this.addPadButtons(true);
  1734. if (typeof snap.fHighlightConnect !== 'undefined')
  1735. this._highlight_connect = snap.fHighlightConnect;
  1736. let pr = Promise.resolve(true);
  1737. if (isStr(snap.fScripts) && snap.fScripts) {
  1738. let src = '', m = null;
  1739. if (snap.fScripts.indexOf('modules:') === 0)
  1740. m = snap.fScripts.slice(8).split(';');
  1741. else if (snap.fScripts.indexOf('load:') === 0)
  1742. src = snap.fScripts.slice(5).split(';');
  1743. else if (snap.fScripts.indexOf('assert:') === 0)
  1744. src = snap.fScripts.slice(7);
  1745. pr = (m !== null) ? loadModules(m) : (src ? loadScript(src) : injectCode(snap.fScripts));
  1746. }
  1747. return pr.then(() => this.drawNextSnap(snap.fPrimitives)).then(() => {
  1748. if (isFunc(this.onCanvasUpdated))
  1749. this.onCanvasUpdated(this);
  1750. return this;
  1751. });
  1752. }
  1753. this.updateObject(first); // update only object attributes
  1754. // apply all changes in the object (pad or canvas)
  1755. if (this.isCanvas())
  1756. this.createCanvasSvg(2);
  1757. else
  1758. this.createPadSvg(true);
  1759. let missmatch = false;
  1760. // match painters with new list of primitives
  1761. if (!snap.fWithoutPrimitives) {
  1762. let i = 0, k = 0;
  1763. while (k < this.#painters.length) {
  1764. const sub = this.#painters[k];
  1765. // skip check secondary painters or painters without snapid
  1766. if (!sub.hasSnapId() || sub.isSecondary()) {
  1767. k++;
  1768. continue; // look only for painters with snapid
  1769. }
  1770. if (i >= snap.fPrimitives.length)
  1771. break;
  1772. const prim = snap.fPrimitives[i];
  1773. // only real objects drawing checked for existing painters
  1774. if ((prim.fKind !== webSnapIds.kSubPad) && (prim.fKind !== webSnapIds.kObject) && (prim.fKind !== webSnapIds.kSVG)) {
  1775. i++;
  1776. continue; // look only for primitives of real objects
  1777. }
  1778. if (prim.fObjectID === sub.getSnapId()) {
  1779. i++;
  1780. k++;
  1781. } else {
  1782. missmatch = true;
  1783. break;
  1784. }
  1785. }
  1786. let cnt = 1000;
  1787. // remove painters without primitives, limit number of checks
  1788. while (!missmatch && (k < this.#painters.length) && (--cnt >= 0)) {
  1789. if (this.removePrimitive(k) === -111)
  1790. missmatch = true;
  1791. }
  1792. if (cnt < 0)
  1793. missmatch = true;
  1794. }
  1795. if (missmatch) {
  1796. const old_painters = this.#painters;
  1797. this.#painters = [];
  1798. old_painters.forEach(objp => objp.cleanup());
  1799. this.setMainPainter(undefined, true);
  1800. if (isFunc(this.removePadButtons))
  1801. this.removePadButtons();
  1802. this.addPadButtons(true);
  1803. }
  1804. return this.drawNextSnap(snap.fPrimitives, missmatch ? undefined : 0).then(() => {
  1805. this.addPadInteractive();
  1806. if (getActivePad() === this)
  1807. this.getCanvPainter()?.producePadEvent('padredraw', this);
  1808. if (isFunc(this.onCanvasUpdated))
  1809. this.onCanvasUpdated(this);
  1810. return this;
  1811. });
  1812. }
  1813. /** @summary Deliver mouse move or click event to the web canvas
  1814. * @private */
  1815. deliverWebCanvasEvent(kind, x, y, snapid) {
  1816. if (!this.is_active_pad || this.doingDraw() || x === undefined || y === undefined)
  1817. return;
  1818. if ((kind === 'move') && !this.#deliver_move_events)
  1819. return;
  1820. const cp = this.getCanvPainter();
  1821. if (!cp || !cp.canSendWebsocket(2) || cp.isReadonly())
  1822. return;
  1823. const msg = JSON.stringify([this.getSnapId(), kind, x.toString(), y.toString(), snapid || '']);
  1824. cp.sendWebsocket(`EVENT:${msg}`);
  1825. }
  1826. /** @summary Create image for the pad
  1827. * @desc Used with web-based canvas to create images for server side
  1828. * @return {Promise} with image data, coded with btoa() function
  1829. * @private */
  1830. async createImage(format) {
  1831. if ((format === 'png') || (format === 'jpeg') || (format === 'svg') || (format === 'webp') || (format === 'pdf')) {
  1832. return this.produceImage(true, format).then(res => {
  1833. if (!res || (format === 'svg')) return res;
  1834. const separ = res.indexOf('base64,');
  1835. return (separ > 0) ? res.slice(separ+7) : '';
  1836. });
  1837. }
  1838. return '';
  1839. }
  1840. /** @summary Collects pad information for TWebCanvas
  1841. * @desc need to update different states
  1842. * @private */
  1843. getWebPadOptions(arg, cp) {
  1844. let is_top = (arg === undefined), elem = null, scan_subpads = true;
  1845. // no any options need to be collected in readonly mode
  1846. if (is_top && this.isReadonly())
  1847. return '';
  1848. if (arg === 'only_this') {
  1849. is_top = true;
  1850. scan_subpads = false;
  1851. } else if (arg === 'with_subpads') {
  1852. is_top = true;
  1853. scan_subpads = true;
  1854. }
  1855. if (is_top) arg = [];
  1856. if (!cp)
  1857. cp = this.isCanvas() ? this : this.getCanvPainter();
  1858. if (this.getSnapId()) {
  1859. elem = { _typename: 'TWebPadOptions', snapid: this.getSnapId(),
  1860. active: Boolean(this.is_active_pad),
  1861. cw: 0, ch: 0, w: [],
  1862. bits: 0, primitives: [],
  1863. logx: this.#pad.fLogx, logy: this.#pad.fLogy, logz: this.#pad.fLogz,
  1864. gridx: this.#pad.fGridx, gridy: this.#pad.fGridy,
  1865. tickx: this.#pad.fTickx, ticky: this.#pad.fTicky,
  1866. mleft: this.#pad.fLeftMargin, mright: this.#pad.fRightMargin,
  1867. mtop: this.#pad.fTopMargin, mbottom: this.#pad.fBottomMargin,
  1868. xlow: 0, ylow: 0, xup: 1, yup: 1,
  1869. zx1: 0, zx2: 0, zy1: 0, zy2: 0, zz1: 0, zz2: 0, phi: 0, theta: 0 };
  1870. if (this.isCanvas()) {
  1871. elem.bits = this.getStatusBits();
  1872. elem.cw = this.getPadWidth();
  1873. elem.ch = this.getPadHeight();
  1874. elem.w = [window.screenLeft, window.screenTop, window.outerWidth, window.outerHeight];
  1875. } else if (cp) {
  1876. const cw = cp.getPadWidth(), ch = cp.getPadHeight(), rect = this.getPadRect();
  1877. elem.cw = cw;
  1878. elem.ch = ch;
  1879. elem.xlow = rect.x / cw;
  1880. elem.ylow = 1 - (rect.y + rect.height) / ch;
  1881. elem.xup = elem.xlow + rect.width / cw;
  1882. elem.yup = elem.ylow + rect.height / ch;
  1883. }
  1884. if ((this.#pad.fTheta !== 30) || (this.#pad.fPhi !== 30)) {
  1885. elem.phi = this.#pad.fPhi;
  1886. elem.theta = this.#pad.fTheta;
  1887. }
  1888. if (this.getPadRanges(elem))
  1889. arg.push(elem);
  1890. else
  1891. console.log(`fail to get ranges for pad ${this.#pad.fName}`);
  1892. }
  1893. this.#painters.forEach(sub => {
  1894. if (isFunc(sub.getWebPadOptions)) {
  1895. if (scan_subpads) sub.getWebPadOptions(arg, cp);
  1896. } else {
  1897. const opt = createWebObjectOptions(sub);
  1898. if (opt)
  1899. elem.primitives.push(opt);
  1900. }
  1901. });
  1902. if (is_top) return toJSON(arg);
  1903. }
  1904. /** @summary returns actual ranges in the pad, which can be applied to the server
  1905. * @private */
  1906. getPadRanges(r) {
  1907. if (!r) return false;
  1908. const fp = this.getFramePainter(),
  1909. p = this.getPadSvg();
  1910. r.ranges = fp?.ranges_set ?? false; // indicate that ranges are assigned
  1911. r.ux1 = r.px1 = r.ranges ? fp.scale_xmin : 0; // need to initialize for JSON reader
  1912. r.uy1 = r.py1 = r.ranges ? fp.scale_ymin : 0;
  1913. r.ux2 = r.px2 = r.ranges ? fp.scale_xmax : 0;
  1914. r.uy2 = r.py2 = r.ranges ? fp.scale_ymax : 0;
  1915. r.uz1 = r.ranges ? (fp.scale_zmin ?? 0) : 0;
  1916. r.uz2 = r.ranges ? (fp.scale_zmax ?? 0) : 0;
  1917. if (fp) {
  1918. if (fp.zoom_xmin !== fp.zoom_xmax) {
  1919. r.zx1 = fp.zoom_xmin; r.zx2 = fp.zoom_xmax;
  1920. }
  1921. if (fp.zoom_ymin !== fp.zoom_ymax) {
  1922. r.zy1 = fp.zoom_ymin; r.zy2 = fp.zoom_ymax;
  1923. }
  1924. if (fp.zoom_zmin !== fp.zoom_zmax) {
  1925. r.zz1 = fp.zoom_zmin; r.zz2 = fp.zoom_zmax;
  1926. }
  1927. }
  1928. if (!r.ranges || p.empty()) return true;
  1929. // calculate user range for full pad
  1930. const func = (log, value, err) => {
  1931. if (!log) return value;
  1932. if (value <= 0) return err;
  1933. value = Math.log10(value);
  1934. if (log > 1) value /= Math.log10(log);
  1935. return value;
  1936. }, frect = fp.getFrameRect();
  1937. r.ux1 = func(fp.logx, r.ux1, 0);
  1938. r.ux2 = func(fp.logx, r.ux2, 1);
  1939. let k = (r.ux2 - r.ux1)/(frect.width || 10);
  1940. r.px1 = r.ux1 - k*frect.x;
  1941. r.px2 = r.px1 + k*this.getPadWidth();
  1942. r.uy1 = func(fp.logy, r.uy1, 0);
  1943. r.uy2 = func(fp.logy, r.uy2, 1);
  1944. k = (r.uy2 - r.uy1)/(frect.height || 10);
  1945. r.py1 = r.uy1 - k*frect.y;
  1946. r.py2 = r.py1 + k*this.getPadHeight();
  1947. return true;
  1948. }
  1949. /** @summary Show context menu for specified item
  1950. * @private */
  1951. itemContextMenu(name) {
  1952. const rrr = this.getPadSvg().node().getBoundingClientRect(),
  1953. evnt = { clientX: rrr.left + 10, clientY: rrr.top + 10 };
  1954. // use timeout to avoid conflict with mouse click and automatic menu close
  1955. if (name === 'pad')
  1956. return postponePromise(() => this.padContextMenu(evnt), 50);
  1957. let selp = null, selkind;
  1958. switch (name) {
  1959. case 'xaxis':
  1960. case 'yaxis':
  1961. case 'zaxis':
  1962. selp = this.getFramePainter();
  1963. selkind = name[0];
  1964. break;
  1965. case 'frame':
  1966. selp = this.getFramePainter();
  1967. break;
  1968. default: {
  1969. const indx = parseInt(name);
  1970. if (Number.isInteger(indx))
  1971. selp = this.#painters[indx];
  1972. }
  1973. }
  1974. if (!isFunc(selp?.fillContextMenu))
  1975. return;
  1976. return createMenu(evnt, selp).then(menu => {
  1977. const offline_menu = selp.fillContextMenu(menu, selkind);
  1978. if (offline_menu || selp.getSnapId())
  1979. return selp.fillObjectExecMenu(menu, selkind).then(() => postponePromise(() => menu.show(), 50));
  1980. });
  1981. }
  1982. /** @summary Save pad as image
  1983. * @param {string} kind - format of saved image like 'png', 'svg' or 'jpeg'
  1984. * @param {boolean} full_canvas - does complete canvas (true) or only frame area (false) should be saved
  1985. * @param {string} [filename] - name of the file which should be stored
  1986. * @desc Normally used from context menu
  1987. * @example
  1988. * import { getElementCanvPainter } from 'https://root.cern/js/latest/modules/base/ObjectPainter.mjs';
  1989. * let canvas_painter = getElementCanvPainter('drawing_div_id');
  1990. * canvas_painter.saveAs('png', true, 'canvas.png'); */
  1991. saveAs(kind, full_canvas, filename) {
  1992. if (!filename)
  1993. filename = (this.#pad_name || (this.isCanvas() ? 'canvas' : 'pad')) + '.' + kind;
  1994. this.produceImage(full_canvas, kind).then(imgdata => {
  1995. if (!imgdata)
  1996. return console.error(`Fail to produce image ${filename}`);
  1997. if ((browser.qt6 || browser.cef3) && this.getSnapId()) {
  1998. console.warn(`sending file ${filename} to server`);
  1999. let res = imgdata;
  2000. if (kind !== 'svg') {
  2001. const separ = res.indexOf('base64,');
  2002. res = (separ > 0) ? res.slice(separ+7) : '';
  2003. }
  2004. if (res)
  2005. this.getCanvPainter()?.sendWebsocket(`SAVE:${filename}:${res}`);
  2006. } else {
  2007. const prefix = (kind === 'svg') ? prSVG : (kind === 'json' ? prJSON : '');
  2008. saveFile(filename, prefix ? prefix + encodeURIComponent(imgdata) : imgdata);
  2009. }
  2010. });
  2011. }
  2012. /** @summary Search active pad
  2013. * @return {Object} pad painter for active pad */
  2014. findActivePad() {
  2015. let active_pp;
  2016. this.forEachPainterInPad(pp => {
  2017. if (pp.is_active_pad && !active_pp)
  2018. active_pp = pp;
  2019. }, 'pads');
  2020. return active_pp;
  2021. }
  2022. /** @summary Produce image for the pad
  2023. * @return {Promise} with created image */
  2024. async produceImage(full_canvas, file_format, args) {
  2025. if (file_format === 'json')
  2026. return isFunc(this.produceJSON) ? this.produceJSON(full_canvas ? 2 : 0) : '';
  2027. const use_frame = (full_canvas === 'frame'),
  2028. elem = use_frame ? this.getFrameSvg() : (full_canvas ? this.getCanvSvg() : this.getPadSvg()),
  2029. painter = (full_canvas && !use_frame) ? this.getCanvPainter() : this,
  2030. items = []; // keep list of replaced elements, which should be moved back at the end
  2031. if (elem.empty())
  2032. return '';
  2033. if (use_frame || !full_canvas) {
  2034. const defs = this.getCanvSvg().selectChild('.canvas_defs');
  2035. if (!defs.empty()) {
  2036. items.push({ prnt: this.getCanvSvg(), defs });
  2037. elem.node().insertBefore(defs.node(), elem.node().firstChild);
  2038. }
  2039. }
  2040. let active_pp = null;
  2041. painter.forEachPainterInPad(pp => {
  2042. if (pp.is_active_pad && !active_pp) {
  2043. active_pp = pp;
  2044. active_pp.drawActiveBorder(null, false);
  2045. }
  2046. if (use_frame) return; // do not make transformations for the frame
  2047. const item = { prnt: pp.getPadSvg() };
  2048. items.push(item);
  2049. // remove buttons from each sub-pad
  2050. const btns = pp.getLayerSvg('btns_layer');
  2051. item.btns_node = btns.node();
  2052. if (item.btns_node) {
  2053. item.btns_prnt = item.btns_node.parentNode;
  2054. item.btns_next = item.btns_node.nextSibling;
  2055. btns.remove();
  2056. }
  2057. const fp = pp.getFramePainter();
  2058. if (!isFunc(fp?.access3dKind))
  2059. return;
  2060. const can3d = fp.access3dKind();
  2061. if ((can3d !== constants.Embed3D.Overlay) && (can3d !== constants.Embed3D.Embed))
  2062. return;
  2063. const main = isFunc(fp.getRenderer) ? fp : fp.getMainPainter(),
  2064. canvas = isFunc(main.getRenderer) ? main.getRenderer()?.domElement : null;
  2065. if (!isFunc(main?.render3D) || !isObject(canvas))
  2066. return;
  2067. const sz2 = fp.getSizeFor3d(constants.Embed3D.Embed); // get size and position of DOM element as it will be embed
  2068. main.render3D(0); // WebGL clears buffers, therefore we should render scene and convert immediately
  2069. const dataUrl = canvas.toDataURL('image/png');
  2070. // remove 3D drawings
  2071. if (can3d === constants.Embed3D.Embed) {
  2072. item.foreign = item.prnt.select('.' + sz2.clname);
  2073. item.foreign.remove();
  2074. }
  2075. const svg_frame = fp.getFrameSvg();
  2076. item.frame_node = svg_frame.node();
  2077. if (item.frame_node) {
  2078. item.frame_next = item.frame_node.nextSibling;
  2079. svg_frame.remove();
  2080. }
  2081. // add svg image
  2082. item.img = item.prnt.insert('image', '.primitives_layer') // create image object
  2083. .attr('x', sz2.x)
  2084. .attr('y', sz2.y)
  2085. .attr('width', canvas.width)
  2086. .attr('height', canvas.height)
  2087. .attr('href', dataUrl);
  2088. }, 'pads');
  2089. let width = elem.property('draw_width'),
  2090. height = elem.property('draw_height'),
  2091. viewBox = '';
  2092. if (use_frame) {
  2093. const fp = this.getFramePainter();
  2094. width = fp.getFrameWidth();
  2095. height = fp.getFrameHeight();
  2096. }
  2097. const scale = this.getPadScale();
  2098. if (scale !== 1) {
  2099. viewBox = ` viewBox="0 0 ${width} ${height}"`;
  2100. width = Math.round(width / scale);
  2101. height = Math.round(height / scale);
  2102. }
  2103. if (settings.DarkMode || this.#pad?.$dark)
  2104. viewBox += ' style="filter: invert(100%)"';
  2105. const arg = (file_format === 'pdf')
  2106. ? { node: elem.node(), width, height, scale, reset_tranform: use_frame }
  2107. : compressSVG(`<svg width="${width}" height="${height}"${viewBox} xmlns="${nsSVG}">${elem.node().innerHTML}</svg>`);
  2108. return svgToImage(arg, file_format, args).then(res => {
  2109. // reactivate border
  2110. active_pp?.drawActiveBorder(null, true);
  2111. for (let k = 0; k < items.length; ++k) {
  2112. const item = items[k];
  2113. item.img?.remove(); // delete embed image
  2114. const prim = item.prnt.selectChild('.primitives_layer');
  2115. if (item.foreign) // reinsert foreign object
  2116. item.prnt.node().insertBefore(item.foreign.node(), prim.node());
  2117. if (item.frame_node) // reinsert frame as first in list of primitives
  2118. prim.node().insertBefore(item.frame_node, item.frame_next);
  2119. if (item.btns_node) // reinsert buttons
  2120. item.btns_prnt.insertBefore(item.btns_node, item.btns_next);
  2121. if (item.defs) // reinsert defs
  2122. item.prnt.node().insertBefore(item.defs.node(), item.prnt.node().firstChild);
  2123. }
  2124. return res;
  2125. });
  2126. }
  2127. /** @summary Process pad button click */
  2128. clickPadButton(funcname, evnt) {
  2129. if (funcname === 'CanvasSnapShot')
  2130. return this.saveAs('png', true);
  2131. if (funcname === 'enlargePad')
  2132. return this.enlargePad();
  2133. if (funcname === 'PadSnapShot')
  2134. return this.saveAs('png', false);
  2135. if (funcname === 'PadContextMenus') {
  2136. evnt?.preventDefault();
  2137. evnt?.stopPropagation();
  2138. if (closeMenu()) return;
  2139. return createMenu(evnt, this).then(menu => {
  2140. menu.header('Menus');
  2141. menu.add(this.isCanvas() ? 'Canvas' : 'Pad', 'pad', this.itemContextMenu);
  2142. if (this.getFramePainter())
  2143. menu.add('Frame', 'frame', this.itemContextMenu);
  2144. const main = this.getMainPainter(); // here pad painter method
  2145. if (main) {
  2146. menu.add('X axis', 'xaxis', this.itemContextMenu);
  2147. menu.add('Y axis', 'yaxis', this.itemContextMenu);
  2148. if (isFunc(main.getDimension) && (main.getDimension() > 1))
  2149. menu.add('Z axis', 'zaxis', this.itemContextMenu);
  2150. }
  2151. if (this.#painters?.length) {
  2152. menu.separator();
  2153. const shown = [];
  2154. this.#painters.forEach((pp, indx) => {
  2155. const obj = pp?.getObject();
  2156. if (!obj || (shown.indexOf(obj) >= 0))
  2157. return;
  2158. let name;
  2159. if (isFunc(pp.getMenuHeader))
  2160. name = pp.getMenuHeader();
  2161. else {
  2162. name = isFunc(pp.getClassName) ? pp.getClassName() : (obj._typename || '');
  2163. if (name) name += '::';
  2164. name += isFunc(pp.getObjectName) ? pp.getObjectName() : (obj.fName || `item${indx}`);
  2165. }
  2166. menu.add(name, indx, this.itemContextMenu);
  2167. shown.push(obj);
  2168. });
  2169. }
  2170. menu.show();
  2171. });
  2172. }
  2173. // click automatically goes to all sub-pads
  2174. // if any painter indicates that processing completed, it returns true
  2175. let done = false;
  2176. const prs = [];
  2177. for (let i = 0; i < this.#painters.length; ++i) {
  2178. const pp = this.#painters[i];
  2179. if (isFunc(pp.clickPadButton))
  2180. prs.push(pp.clickPadButton(funcname, evnt));
  2181. if (!done && isFunc(pp.clickButton)) {
  2182. done = pp.clickButton(funcname);
  2183. if (isPromise(done)) prs.push(done);
  2184. }
  2185. }
  2186. return Promise.all(prs);
  2187. }
  2188. /** @summary Add button to the pad
  2189. * @private */
  2190. addPadButton(btn, tooltip, funcname, keyname) {
  2191. if (!settings.ToolBar || this.isBatchMode()) return;
  2192. if (!this._buttons) this._buttons = [];
  2193. // check if there are duplications
  2194. for (let k = 0; k < this._buttons.length; ++k)
  2195. if (this._buttons[k].funcname === funcname) return;
  2196. this._buttons.push({ btn, tooltip, funcname, keyname });
  2197. if (!this.isTopPad() && funcname.indexOf('Pad') && (funcname !== 'enlargePad')) {
  2198. const cp = this.getCanvPainter();
  2199. if (cp && (cp !== this))
  2200. cp.addPadButton(btn, tooltip, funcname);
  2201. }
  2202. }
  2203. /** @summary Show pad buttons
  2204. * @private */
  2205. showPadButtons() {
  2206. if (!this._buttons)
  2207. return;
  2208. PadButtonsHandler.assign(this);
  2209. this.showPadButtons();
  2210. }
  2211. /** @summary Add buttons for pad or canvas
  2212. * @private */
  2213. addPadButtons(is_online) {
  2214. this.addPadButton('camera', 'Create PNG', this.isCanvas() ? 'CanvasSnapShot' : 'PadSnapShot', 'Ctrl PrintScreen');
  2215. if (settings.ContextMenu)
  2216. this.addPadButton('question', 'Access context menus', 'PadContextMenus');
  2217. const add_enlarge = !this.isTopPad() && this.hasObjectsToDraw();
  2218. if (add_enlarge || this.enlargeMain('verify'))
  2219. this.addPadButton('circle', 'Enlarge canvas', 'enlargePad');
  2220. if (is_online && this.brlayout) {
  2221. this.addPadButton('diamand', 'Toggle Ged', 'ToggleGed');
  2222. this.addPadButton('three_circles', 'Toggle Status', 'ToggleStatus');
  2223. }
  2224. }
  2225. /** @summary Decode pad draw options
  2226. * @private */
  2227. decodeOptions(opt) {
  2228. const pad = this.getObject();
  2229. if (!pad) return;
  2230. const d = new DrawOptions(opt),
  2231. o = this.setOptions({ GlobalColors: true, LocalColors: false, CreatePalette: 0, IgnorePalette: false, RotateFrame: false, FixFrame: false }, opt);
  2232. if (d.check('NOCOLORS') || d.check('NOCOL')) o.GlobalColors = o.LocalColors = false;
  2233. if (d.check('LCOLORS') || d.check('LCOL')) { o.GlobalColors = false; o.LocalColors = true; }
  2234. if (d.check('NOPALETTE') || d.check('NOPAL')) o.IgnorePalette = true;
  2235. if (d.check('ROTATE')) o.RotateFrame = true;
  2236. if (d.check('FIXFRAME')) o.FixFrame = true;
  2237. if (d.check('FIXSIZE') && this.isCanvas())
  2238. this._setFixedSize(true);
  2239. if (d.check('CP', true)) o.CreatePalette = d.partAsInt(0, 0);
  2240. if (d.check('NOZOOMX')) o.NoZoomX = true;
  2241. if (d.check('NOZOOMY')) o.NoZoomY = true;
  2242. if (d.check('GRAYSCALE'))
  2243. pad.SetBit(kIsGrayscale, true);
  2244. function forEach(func, p) {
  2245. if (!p) p = pad;
  2246. func(p);
  2247. const arr = p.fPrimitives?.arr || [];
  2248. for (let i = 0; i < arr.length; ++i) {
  2249. if (arr[i]._typename === clTPad)
  2250. forEach(func, arr[i]);
  2251. }
  2252. }
  2253. if (d.check('NOMARGINS')) forEach(p => { p.fLeftMargin = p.fRightMargin = p.fBottomMargin = p.fTopMargin = 0; });
  2254. if (d.check('WHITE')) forEach(p => { p.fFillColor = 0; });
  2255. if (d.check('LOG2X')) forEach(p => { p.fLogx = 2; p.fUxmin = 0; p.fUxmax = 1; p.fX1 = 0; p.fX2 = 1; });
  2256. if (d.check('LOGX')) forEach(p => { p.fLogx = 1; p.fUxmin = 0; p.fUxmax = 1; p.fX1 = 0; p.fX2 = 1; });
  2257. if (d.check('LOG2Y')) forEach(p => { p.fLogy = 2; p.fUymin = 0; p.fUymax = 1; p.fY1 = 0; p.fY2 = 1; });
  2258. if (d.check('LOGY')) forEach(p => { p.fLogy = 1; p.fUymin = 0; p.fUymax = 1; p.fY1 = 0; p.fY2 = 1; });
  2259. if (d.check('LOG2Z')) forEach(p => { p.fLogz = 2; });
  2260. if (d.check('LOGZ')) forEach(p => { p.fLogz = 1; });
  2261. if (d.check('LOGV')) forEach(p => { p.fLogv = 1; });
  2262. if (d.check('LOG2')) forEach(p => { p.fLogx = p.fLogy = p.fLogz = 2; });
  2263. if (d.check('LOG')) forEach(p => { p.fLogx = p.fLogy = p.fLogz = 1; });
  2264. if (d.check('LNX')) forEach(p => { p.fLogx = 3; p.fUxmin = 0; p.fUxmax = 1; p.fX1 = 0; p.fX2 = 1; });
  2265. if (d.check('LNY')) forEach(p => { p.fLogy = 3; p.fUymin = 0; p.fUymax = 1; p.fY1 = 0; p.fY2 = 1; });
  2266. if (d.check('LN')) forEach(p => { p.fLogx = p.fLogy = p.fLogz = 3; });
  2267. if (d.check('GRIDX')) forEach(p => { p.fGridx = 1; });
  2268. if (d.check('GRIDY')) forEach(p => { p.fGridy = 1; });
  2269. if (d.check('GRID')) forEach(p => { p.fGridx = p.fGridy = 1; });
  2270. if (d.check('TICKX')) forEach(p => { p.fTickx = 1; });
  2271. if (d.check('TICKY')) forEach(p => { p.fTicky = 1; });
  2272. if (d.check('TICKZ')) forEach(p => { p.fTickz = 1; });
  2273. if (d.check('TICK')) forEach(p => { p.fTickx = p.fTicky = 1; });
  2274. ['OTX', 'OTY', 'CTX', 'CTY', 'NOEX', 'NOEY', 'RX', 'RY'].forEach(name => {
  2275. if (d.check(name)) forEach(p => { p['$' + name] = true; });
  2276. });
  2277. if (!d.empty() && pad?.fPrimitives) {
  2278. for (let n = 0; n < pad.fPrimitives.arr.length; ++n) {
  2279. if (d.check(`SUB${n}_`, true))
  2280. pad.fPrimitives.opt[n] = d.part;
  2281. }
  2282. }
  2283. this.storeDrawOpt(opt);
  2284. }
  2285. /** @summary draw TPad object */
  2286. static async draw(dom, pad, opt) {
  2287. const painter = new TPadPainter(dom, pad, opt, false, true);
  2288. painter.createPadSvg();
  2289. if (painter.matchObjectType(clTPad) && (painter.isTopPad() || painter.hasObjectsToDraw()))
  2290. painter.addPadButtons();
  2291. // set active pad
  2292. selectActivePad({ pp: painter, active: true });
  2293. // flag used to prevent immediate pad redraw during first draw
  2294. return painter.drawPrimitives().then(() => {
  2295. painter.showPadButtons();
  2296. painter.addPadInteractive();
  2297. return painter;
  2298. });
  2299. }
  2300. } // class TPadPainter
  2301. export { TPadPainter, PadButtonsHandler, clTButton, kIsGrayscale, createWebObjectOptions, webSnapIds };