UNPKG

jsroot

Version:
1,365 lines (1,096 loc) 116 kB
import { gStyle, settings, internals, isFunc, isStr, postponePromise, browser, clTAxis, clTFrame, kNoZoom, urlClassPrefix } from '../core.mjs'; import { select as d3_select, pointer as d3_pointer, pointers as d3_pointers, drag as d3_drag, rgb as d3_rgb } from '../d3.mjs'; import { getElementRect, getAbsPosInCanvas, makeTranslate, addHighlightStyle, getBoxDecorations } from '../base/BasePainter.mjs'; import { getActivePad, ObjectPainter, EAxisBits, kAxisLabels } from '../base/ObjectPainter.mjs'; import { getSvgLineStyle } from '../base/TAttLineHandler.mjs'; import { TAxisPainter } from './TAxisPainter.mjs'; import { FontHandler } from '../base/FontHandler.mjs'; import { createMenu, closeMenu, showPainterMenu, hasMenu } from '../gui/menu.mjs'; import { detectRightButton } from '../gui/utils.mjs'; const logminfactorX = 0.0001, logminfactorY = 3e-4; /** @summary Configure tooltip enable flag for painter * @private */ function setPainterTooltipEnabled(painter, on) { if (!painter) return; const fp = painter.getFramePainter(); if (isFunc(fp?.setTooltipEnabled)) { fp.setTooltipEnabled(on); fp.processFrameTooltipEvent(null); } // this is 3D control object if (isFunc(painter.control?.setTooltipEnabled)) painter.control.setTooltipEnabled(on); } /** @summary Return pointers on touch event * @private */ function get_touch_pointers(event, node) { return event.$touch_arr ?? d3_pointers(event, node); } /** @summary Returns coordinates transformation func * @private */ function getEarthProjectionFunc(id) { switch (id) { // Aitoff2xy case 1: return (l, b) => { const DegToRad = Math.PI/180, alpha2 = (l/2)*DegToRad, delta = b*DegToRad, r2 = Math.sqrt(2), f = 2*r2/Math.PI, cdec = Math.cos(delta), denom = Math.sqrt(1.0 + cdec*Math.cos(alpha2)); return { x: cdec*Math.sin(alpha2)*2.0*r2/denom/f/DegToRad, y: Math.sin(delta)*r2/denom/f/DegToRad }; }; // mercator case 2: return (l, b) => { return { x: l, y: Math.log(Math.tan((Math.PI/2 + b/180*Math.PI)/2)) }; }; // sinusoidal case 3: return (l, b) => { return { x: l*Math.cos(b/180*Math.PI), y: b }; }; // parabolic case 4: return (l, b) => { return { x: l*(2.0*Math.cos(2*b/180*Math.PI/3) - 1), y: 180*Math.sin(b/180*Math.PI/3) }; }; // Mollweide projection case 5: return (l, b) => { const theta0 = b * Math.PI/180; let theta = theta0, num, den; for (let i = 0; i < 100; i++) { num = 2 * theta + Math.sin(2 * theta) - Math.PI * Math.sin(theta0); den = 4 * (Math.cos(theta)**2); if (den < 1e-20) { theta = theta0; break; } theta -= num / den; if (Math.abs(num / den) < 1e-4) break; } return { x: l * Math.cos(theta), y: 90 * Math.sin(theta) }; }; } } /** @summary Unzoom preselected range for main histogram painter * @desc Used with TGraph where Y zooming selected with fMinimum/fMaximum but histogram * axis range can be wider. Or for normal histogram drawing when preselected range smaller than histogram range * @private */ function unzoomHistogramYRange(main) { if (!isFunc(main?.getDimension) || main.getDimension() !== 1) return; const ymin = main.draw_content ? main.hmin : main.ymin, ymax = main.draw_content ? main.hmax : main.ymax; if ((main.zoom_ymin !== main.zoom_ymax) && (ymin !== ymax) && (ymin <= main.zoom_ymin) && (main.zoom_ymax <= ymax)) main.zoom_ymin = main.zoom_ymax = 0; } // global, allow single drag at once let drag_rect = null, drag_kind = '', drag_painter = null; /** @summary Check if dragging performed currently * @private */ function is_dragging(painter, kind) { return drag_rect && (drag_painter === painter) && (drag_kind === kind); } /** @summary Add drag for interactive rectangular elements for painter * @private */ function addDragHandler(_painter, arg) { if (!settings.MoveResize) return; const painter = _painter, pp = painter.getPadPainter(); if (pp?._fast_drawing || pp?.isBatchMode()) return; // cleanup all drag elements when canvas is not editable if (pp?.isEditable() === false) arg.cleanup = true; if (!isFunc(arg.getDrawG)) arg.getDrawG = () => painter?.draw_g; function makeResizeElements(group, handler) { function addElement(cursor, d) { const clname = 'js_' + cursor.replace(/[-]/g, '_'); let elem = group.selectChild('.' + clname); if (arg.cleanup) return elem.remove(); if (elem.empty()) elem = group.append('path').classed(clname, true); elem.style('opacity', 0).style('cursor', cursor).attr('d', d); if (handler) elem.call(handler); } addElement('nw-resize', 'M2,2h15v-5h-20v20h5Z'); addElement('ne-resize', `M${arg.width-2},2h-15v-5h20v20h-5 Z`); addElement('sw-resize', `M2,${arg.height-2}h15v5h-20v-20h5Z`); addElement('se-resize', `M${arg.width-2},${arg.height-2}h-15v5h20v-20h-5Z`); if (!arg.no_change_x) { addElement('w-resize', `M-3,18h5v${Math.max(0, arg.height-2*18)}h-5Z`); addElement('e-resize', `M${arg.width+3},18h-5v${Math.max(0, arg.height-2*18)}h5Z`); } if (!arg.no_change_y) { addElement('n-resize', `M18,-3v5h${Math.max(0, arg.width-2*18)}v-5Z`); addElement('s-resize', `M18,${arg.height+3}v-5h${Math.max(0, arg.width-2*18)}v5Z`); } } const complete_drag = (newx, newy, newwidth, newheight) => { drag_painter = null; drag_kind = ''; if (drag_rect) { drag_rect.remove(); drag_rect = null; } const draw_g = arg.getDrawG(); if (!draw_g) return false; const oldx = arg.x, oldy = arg.y; if (arg.minwidth && newwidth < arg.minwidth) newwidth = arg.minwidth; if (arg.minheight && newheight < arg.minheight) newheight = arg.minheight; const change_size = (newwidth !== arg.width) || (newheight !== arg.height), change_pos = (newx !== oldx) || (newy !== oldy); arg.x = newx; arg.y = newy; arg.width = newwidth; arg.height = newheight; if (!arg.no_transform) makeTranslate(draw_g, newx, newy); setPainterTooltipEnabled(painter, true); makeResizeElements(draw_g); if (change_size || change_pos) { if (change_size && isFunc(arg.resize)) arg.resize(newwidth, newheight); if (change_pos && isFunc(arg.move)) arg.move(newx, newy, newx - oldx, newy - oldy); if (change_size || change_pos) { if (arg.obj) { const rect = arg.pad_rect ?? pp.getPadRect(); arg.obj.fX1NDC = newx / rect.width; arg.obj.fX2NDC = (newx + newwidth) / rect.width; arg.obj.fY1NDC = 1 - (newy + newheight) / rect.height; arg.obj.fY2NDC = 1 - newy / rect.height; arg.obj.$modifiedNDC = true; // indicate that NDC was interactively changed, block in updated } else if (isFunc(arg.move_resize)) arg.move_resize(newx, newy, newwidth, newheight); if (isFunc(arg.redraw)) arg.redraw(arg); } } return change_size || change_pos; }, drag_move = d3_drag().subject(Object), drag_move_off = d3_drag().subject(Object); drag_move_off.on('start', null).on('drag', null).on('end', null); drag_move .on('start', evnt => { if (detectRightButton(evnt.sourceEvent) || drag_kind) return; if (isFunc(arg.is_disabled) && arg.is_disabled('move')) return; closeMenu(); // close menu setPainterTooltipEnabled(painter, false); // disable tooltip evnt.sourceEvent.preventDefault(); evnt.sourceEvent.stopPropagation(); const pad_rect = arg.pad_rect ?? pp.getPadRect(), handle = { x: arg.x, y: arg.y, width: arg.width, height: arg.height, acc_x1: arg.x, acc_y1: arg.y, pad_w: pad_rect.width - arg.width, pad_h: pad_rect.height - arg.height, drag_tm: new Date(), path: `v${arg.height}h${arg.width}v${-arg.height}z`, evnt_x: evnt.x, evnt_y: evnt.y }; drag_painter = painter; drag_kind = 'move'; drag_rect = d3_select(arg.getDrawG().node().parentNode).append('path') .attr('d', `M${handle.acc_x1},${handle.acc_y1}${handle.path}`) .style('cursor', 'move') .style('pointer-events', 'none') // let forward double click to underlying elements .property('drag_handle', handle) .call(addHighlightStyle, true); }).on('drag', evnt => { if (!is_dragging(painter, 'move')) return; evnt.sourceEvent.preventDefault(); evnt.sourceEvent.stopPropagation(); const handle = drag_rect.property('drag_handle'); if (!arg.no_change_x) handle.acc_x1 += evnt.dx; if (!arg.no_change_y) handle.acc_y1 += evnt.dy; handle.x = Math.min(Math.max(handle.acc_x1, 0), handle.pad_w); handle.y = Math.min(Math.max(handle.acc_y1, 0), handle.pad_h); drag_rect.attr('d', `M${handle.x},${handle.y}${handle.path}`); }).on('end', evnt => { if (!is_dragging(painter, 'move')) return; evnt.sourceEvent.stopPropagation(); evnt.sourceEvent.preventDefault(); const handle = drag_rect.property('drag_handle'); if (complete_drag(handle.x, handle.y, arg.width, arg.height) === false) { const spent = (new Date()).getTime() - handle.drag_tm.getTime(); if (arg.ctxmenu && (spent > 600)) showPainterMenu({ clientX: handle.evnt_x, clientY: handle.evnt_y, skip_close: 1 }, painter); else if (arg.canselect && (spent <= 600)) painter.getPadPainter()?.selectObjectPainter(painter); } }); const drag_resize = d3_drag().subject(Object); drag_resize .on('start', function(evnt) { if (detectRightButton(evnt.sourceEvent) || drag_kind) return; if (isFunc(arg.is_disabled) && arg.is_disabled('resize')) return; closeMenu(); // close menu setPainterTooltipEnabled(painter, false); // disable tooltip evnt.sourceEvent.stopPropagation(); evnt.sourceEvent.preventDefault(); const pad_rect = arg.pad_rect ?? pp.getPadRect(), handle = { x: arg.x, y: arg.y, width: arg.width, height: arg.height, acc_x1: arg.x, acc_y1: arg.y, acc_x2: arg.x + arg.width, acc_y2: arg.y + arg.height, pad_w: pad_rect.width, pad_h: pad_rect.height }; drag_painter = painter; drag_kind = 'resize'; drag_rect = d3_select(arg.getDrawG().node().parentNode) .append('rect') .style('cursor', d3_select(this).style('cursor')) .attr('x', handle.acc_x1) .attr('y', handle.acc_y1) .attr('width', handle.acc_x2 - handle.acc_x1) .attr('height', handle.acc_y2 - handle.acc_y1) .property('drag_handle', handle) .call(addHighlightStyle, true); }).on('drag', function(evnt) { if (!is_dragging(painter, 'resize')) return; evnt.sourceEvent.preventDefault(); evnt.sourceEvent.stopPropagation(); const handle = drag_rect.property('drag_handle'), elem = d3_select(this); let dx = evnt.dx, dy = evnt.dy; if (arg.no_change_x) dx = 0; if (arg.no_change_y) dy = 0; if (elem.classed('js_nw_resize')) { handle.acc_x1 += dx; handle.acc_y1 += dy; } else if (elem.classed('js_ne_resize')) { handle.acc_x2 += dx; handle.acc_y1 += dy; } else if (elem.classed('js_sw_resize')) { handle.acc_x1 += dx; handle.acc_y2 += dy; } else if (elem.classed('js_se_resize')) { handle.acc_x2 += dx; handle.acc_y2 += dy; } else if (elem.classed('js_w_resize')) handle.acc_x1 += dx; else if (elem.classed('js_n_resize')) handle.acc_y1 += dy; else if (elem.classed('js_e_resize')) handle.acc_x2 += dx; else if (elem.classed('js_s_resize')) handle.acc_y2 += dy; const x1 = Math.max(0, handle.acc_x1), x2 = Math.min(handle.acc_x2, handle.pad_w), y1 = Math.max(0, handle.acc_y1), y2 = Math.min(handle.acc_y2, handle.pad_h); handle.x = Math.min(x1, x2); handle.y = Math.min(y1, y2); handle.width = Math.abs(x2 - x1); handle.height = Math.abs(y2 - y1); drag_rect.attr('x', handle.x).attr('y', handle.y).attr('width', handle.width).attr('height', handle.height); }).on('end', evnt => { if (!is_dragging(painter, 'resize')) return; evnt.sourceEvent.preventDefault(); const handle = drag_rect.property('drag_handle'); complete_drag(handle.x, handle.y, handle.width, handle.height); }); if (!arg.only_resize) arg.getDrawG().style('cursor', arg.cleanup ? null : 'move').call(arg.cleanup ? drag_move_off : drag_move); if (!arg.only_move) makeResizeElements(arg.getDrawG(), drag_resize); } const TooltipHandler = { /** @desc only canvas info_layer can be used while other pads can overlay * @return layer where frame tooltips are shown */ hints_layer() { return this.getCanvPainter()?.getLayerSvg('info_layer') ?? d3_select(null); }, /** @return true if tooltip is shown, use to prevent some other action */ isTooltipShown() { if (!this.tooltip_enabled || !this.isTooltipAllowed()) return false; const hintsg = this.hints_layer().selectChild('.objects_hints'); return hintsg.empty() ? false : hintsg.property('hints_pad') === this.getPadName(); }, /** @summary set tooltips enabled on/off */ setTooltipEnabled(enabled) { if (enabled !== undefined) this.tooltip_enabled = enabled; }, /** @summary central function which let show selected hints for the object */ processFrameTooltipEvent(pnt, evnt) { if (pnt?.handler) { // special use of interactive handler in the frame painter const rect = this.draw_g?.selectChild('.main_layer'); if (!rect || rect.empty()) pnt = null; // disable else if (pnt.touch && evnt) { const pos = get_touch_pointers(evnt, rect.node()); pnt = (pos && pos.length === 1) ? { touch: true, x: pos[0][0], y: pos[0][1] } : null; } else if (evnt) { const pos = d3_pointer(evnt, rect.node()); pnt = { touch: false, x: pos[0], y: pos[1] }; } } let nhints = 0, nexact = 0, maxlen = 0, lastcolor1 = 0, usecolor1 = false; const hmargin = 3, wmargin = 3, hstep = 1.2, frame_rect = this.getFrameRect(), pp = this.getPadPainter(), pad_width = pp?.getPadWidth(), scale = pp?.getPadScale() ?? 1, textheight = (pnt?.touch ? 15 : 11) * scale, font = new FontHandler(160, textheight), disable_tootlips = !this.isTooltipAllowed() || !this.tooltip_enabled; if (pnt) { pnt.disabled = disable_tootlips; // indicate that highlighting is not required pnt.painters = true; // get also painter } // collect tooltips from pad painter - it has list of all drawn objects const hints = pp?.processPadTooltipEvent(pnt) ?? []; if (pnt && frame_rect) pp.deliverWebCanvasEvent('move', frame_rect.x + pnt.x, frame_rect.y + pnt.y, hints ? hints[0]?.painter?.snapid : ''); for (let n = 0; n < hints.length; ++n) { const hint = hints[n]; if (!hint) continue; if (hint.user_info !== undefined) hint.painter?.provideUserTooltip(hint.user_info); if (!hint.lines || (hint.lines.length === 0)) { hints[n] = null; continue; } // check if fully duplicated hint already exists for (let k = 0; k < n; ++k) { const hprev = hints[k]; let diff = false; if (!hprev || (hprev.lines.length !== hint.lines.length)) continue; for (let l = 0; l < hint.lines.length && !diff; ++l) if (hprev.lines[l] !== hint.lines[l]) diff = true; if (!diff) { hints[n] = null; break; } } if (!hints[n]) continue; nhints++; if (hint.exact) nexact++; hint.lines.forEach(line => { maxlen = Math.max(maxlen, line.length); }); hint.height = Math.round(hint.lines.length * textheight * hstep + 2 * hmargin - textheight * (hstep - 1)); if ((hint.color1 !== undefined) && (hint.color1 !== 'none')) { if ((lastcolor1 !== 0) && (lastcolor1 !== hint.color1)) usecolor1 = true; lastcolor1 = hint.color1; } } let path_name = null, same_path = hints.length > 1; for (let n = 0; n < hints.length; ++n) { const hint = hints[n], p = hint?.lines ? hint.lines[0]?.lastIndexOf('/') : -1; if (p > 0) { const path = hint.lines[0].slice(0, p + 1); if (path_name === null) path_name = path; else if (path_name !== path) same_path = false; } else same_path = false; } const layer = this.hints_layer(), show_only_best = nhints > 15, coordinates = pnt ? Math.round(pnt.x) + ',' + Math.round(pnt.y) : ''; let hintsg = layer.selectChild('.objects_hints'), // group with all tooltips title = '', name = '', info = '', hint0 = null, best_dist2 = 1e10, best_hint = null; // try to select hint with exact match of the position when several hints available for (let k = 0; k < hints.length; ++k) { if (!hints[k]) continue; if (!hint0) hint0 = hints[k]; // select exact hint if this is the only one if (hints[k].exact && (nexact < 2) && (!hint0 || !hint0.exact)) { hint0 = hints[k]; break; } if (!pnt || (hints[k].x === undefined) || (hints[k].y === undefined)) continue; const dist2 = (pnt.x - hints[k].x) ** 2 + (pnt.y - hints[k].y) ** 2; if (dist2 < best_dist2) { best_dist2 = dist2; best_hint = hints[k]; } } if ((!hint0 || !hint0.exact) && (best_dist2 < 400)) hint0 = best_hint; if (hint0) { name = (hint0.lines && hint0.lines.length > 1) ? hint0.lines[0] : hint0.name; title = hint0.title || ''; info = hint0.line; if (!info && hint0.lines) info = hint0.lines.slice(1).join(' '); } this.showObjectStatus(name, title, info, coordinates); // end of closing tooltips if (!pnt || disable_tootlips || (hints.length === 0) || (maxlen === 0) || (show_only_best && !best_hint)) { hintsg.remove(); return; } // we need to set pointer-events=none for all elements while hints // placed in front of so-called interactive rect in frame, used to catch mouse events if (hintsg.empty()) { hintsg = layer.append('svg:g') .attr('class', 'objects_hints') .style('pointer-events', 'none'); } let frame_shift = { x: 0, y: 0 }, trans = frame_rect.transform || ''; if (!pp.iscan) { frame_shift = getAbsPosInCanvas(this.getPadSvg(), frame_shift); trans = `translate(${frame_shift.x},${frame_shift.y}) ${trans}`; } // copy transform attributes from frame itself hintsg.attr('transform', trans) .property('last_point', pnt) .property('hints_pad', this.getPadName()); let viewmode = hintsg.property('viewmode') || '', actualw = 0, posx = pnt.x + frame_rect.hint_delta_x; if (show_only_best || (nhints === 1)) { viewmode = 'single'; posx += 15; } else { // if there are many hints, place them left or right let bleft = 0.5, bright = 0.5; if (viewmode === 'left') bright = 0.7; else if (viewmode === 'right') bleft = 0.3; if (posx <= bleft * frame_rect.width) { viewmode = 'left'; posx = 20; } else if (posx >= bright * frame_rect.width) { viewmode = 'right'; posx = frame_rect.width - 60; } else posx = hintsg.property('startx'); } if (viewmode !== hintsg.property('viewmode')) { hintsg.property('viewmode', viewmode); hintsg.selectAll('*').remove(); } let curry = 10, // normal y coordinate gapy = 10, // y coordinate, taking into account all gaps gapminx = -1111, gapmaxx = -1111; const minhinty = -frame_shift.y, cp = this.getCanvPainter(), maxhinty = cp.getPadHeight() - frame_rect.y - frame_shift.y; for (let n = 0; n < hints.length; ++n) { let hint = hints[n], group = hintsg.selectChild(`.painter_hint_${n}`); if (show_only_best && (hint !== best_hint)) hint = null; if (hint === null) { group.remove(); continue; } const was_empty = group.empty(); if (was_empty) { group = hintsg.append('svg:svg') .attr('class', `painter_hint_${n}`) .attr('opacity', 0) // use attribute, not style to make animation with d3.transition() .style('overflow', 'hidden') .style('pointer-events', 'none'); } if (viewmode === 'single') curry = pnt.touch ? (pnt.y - hint.height - 5) : Math.min(pnt.y + 15, maxhinty - hint.height - 3) + frame_rect.hint_delta_y; else { for (let n2 = 0; (n2 < hints.length) && (gapy < maxhinty); ++n2) { const hint2 = hints[n2]; if (!hint2) continue; if ((hint2.y >= gapy - 5) && (hint2.y <= gapy + hint2.height + 5)) { gapy = hint2.y + 10; n2 = -1; } } if ((gapminx === -1111) && (gapmaxx === -1111)) gapminx = gapmaxx = hint.x; gapminx = Math.min(gapminx, hint.x); gapmaxx = Math.min(gapmaxx, hint.x); } group.attr('x', posx) .attr('y', curry) .property('curry', curry) .property('gapy', gapy); curry += hint.height + 5; gapy += hint.height + 5; if (!was_empty) group.selectAll('*').remove(); group.attr('width', 60) .attr('height', hint.height); const r = group.append('rect') .attr('x', 0) .attr('y', 0) .attr('width', 60) .attr('height', hint.height) .style('fill', 'lightgrey') .style('pointer-events', 'none'); if (nhints > 1) { const col = usecolor1 ? hint.color1 : hint.color2; if (col && (col !== 'none')) r.style('stroke', col); } r.attr('stroke-width', hint.exact ? 3 : 1); for (let l = 0; l < (hint.lines?.length ?? 0); l++) { let line = hint.lines[l]; if (l === 0 && path_name && same_path) line = line.slice(path_name.length); if (line) { const txt = group.append('svg:text') .attr('text-anchor', 'start') .attr('x', wmargin) .attr('y', hmargin + l * textheight * hstep) .attr('dy', '.8em') .style('fill', 'black') .style('pointer-events', 'none') .call(font.func) .text(line), box = getElementRect(txt, 'bbox'); actualw = Math.max(actualw, box.width); } } function translateFn() { // We only use 'd', but list d,i,a as params just to show can have them as params. // Code only really uses d and t. return function(/* d, i, a */) { return function(t) { return t < 0.8 ? '0' : (t - 0.8) * 5; }; }; } if (was_empty) { if (settings.TooltipAnimation > 0) group.transition().duration(settings.TooltipAnimation).attrTween('opacity', translateFn()); else group.attr('opacity', 1); } } actualw += 2 * wmargin; const svgs = hintsg.selectAll('svg'); if ((viewmode === 'right') && (posx + actualw > frame_rect.width - 20)) { posx = frame_rect.width - actualw - 20; svgs.attr('x', posx); } if ((viewmode === 'single') && (posx + actualw > pad_width - frame_rect.x) && (posx > actualw + 20)) { posx -= (actualw + 20); svgs.attr('x', posx); } // if gap not very big, apply gapy coordinate to open view on the histogram if ((viewmode !== 'single') && (gapy < maxhinty) && (gapy !== curry)) { if ((gapminx <= posx + actualw + 5) && (gapmaxx >= posx - 5)) svgs.attr('y', function() { return d3_select(this).property('gapy'); }); } else if ((viewmode !== 'single') && (curry > maxhinty)) { const shift = Math.max((maxhinty - curry - 10), minhinty); if (shift < 0) svgs.attr('y', function() { return d3_select(this).property('curry') + shift; }); } if (actualw > 10) svgs.attr('width', actualw).select('rect').attr('width', actualw); hintsg.property('startx', posx); if (cp._highlight_connect && isFunc(cp.processHighlightConnect)) cp.processHighlightConnect(hints); }, /** @summary Assigns tooltip methods */ assign(painter) { Object.assign(painter, this, { tooltip_enabled: true }); } }, // TooltipHandler /** @summary Set of frame interactivity methods * @private */ FrameInteractive = { /** @summary Adding basic interactivity */ addBasicInteractivity() { TooltipHandler.assign(this); if (!this._frame_rotate && !this._frame_fixpos) { addDragHandler(this, { obj: this, x: this._frame_x, y: this._frame_y, width: this.getFrameWidth(), height: this.getFrameHeight(), is_disabled: kind => { return (kind === 'move') && this.mode3d; }, only_resize: true, minwidth: 20, minheight: 20, redraw: () => this.sizeChanged() }); } const top_rect = this.draw_g.selectChild('path'), main_svg = this.draw_g.selectChild('.main_layer'); top_rect.style('pointer-events', 'visibleFill') // let process mouse events inside frame .style('cursor', 'default'); // show normal cursor main_svg.style('pointer-events', 'visibleFill') .style('cursor', 'default') .property('handlers_set', 0); const pp = this.getPadPainter(), handlers_set = pp?._fast_drawing ? 0 : 1; if (main_svg.property('handlers_set') !== handlers_set) { const close_handler = handlers_set ? this.processFrameTooltipEvent.bind(this, null) : null, mouse_handler = handlers_set ? this.processFrameTooltipEvent.bind(this, { handler: true, touch: false }) : null; main_svg.property('handlers_set', handlers_set) .on('mouseenter', mouse_handler) .on('mousemove', mouse_handler) .on('mouseleave', close_handler); if (browser.touches) { const touch_handler = handlers_set ? this.processFrameTooltipEvent.bind(this, { handler: true, touch: true }) : null; main_svg.on('touchstart', touch_handler) .on('touchmove', touch_handler) .on('touchend', close_handler) .on('touchcancel', close_handler); } } main_svg.attr('x', 0) .attr('y', 0) .attr('width', this.getFrameWidth()) .attr('height', this.getFrameHeight()); const hintsg = this.hints_layer().selectChild('.objects_hints'); // if tooltips were visible before, try to reconstruct them after short timeout if (!hintsg.empty() && this.isTooltipAllowed() && (hintsg.property('hints_pad') === this.getPadName())) setTimeout(this.processFrameTooltipEvent.bind(this, hintsg.property('last_point'), null), 10); }, /** @summary Add interactive handlers */ async addFrameInteractivity(for_second_axes) { const pp = this.getPadPainter(), svg = this.getFrameSvg(); if (pp?._fast_drawing || svg.empty()) return this; if (for_second_axes) { // add extra handlers for second axes const svg_x2 = svg.selectAll('.x2axis_container'), svg_y2 = svg.selectAll('.y2axis_container'); if (settings.ContextMenu) { svg_x2.on('contextmenu', evnt => this.showContextMenu('x2', evnt)); svg_y2.on('contextmenu', evnt => this.showContextMenu('y2', evnt)); } svg_x2.on('mousemove', evnt => this.showAxisStatus('x2', evnt)); svg_y2.on('mousemove', evnt => this.showAxisStatus('y2', evnt)); return this; } const svg_x = svg.selectAll('.xaxis_container'), svg_y = svg.selectAll('.yaxis_container'); this.can_zoom_x = this.can_zoom_y = settings.Zooming; if (pp?.options) { if (pp.options.NoZoomX) this.can_zoom_x = false; if (pp.options.NoZoomY) this.can_zoom_y = false; } if (!svg.property('interactive_set')) { this.addFrameKeysHandler(); this.zoom_kind = 0; // 0 - none, 1 - XY, 2 - only X, 3 - only Y, (+100 for touches) this.zoom_rect = null; this.zoom_origin = null; // original point where zooming started this.zoom_curr = null; // current point for zooming } if (settings.Zooming) { if (settings.ZoomMouse) { svg.on('mousedown', evnt => this.startRectSel(evnt)); svg.on('dblclick', evnt => this.mouseDoubleClick(evnt)); } if (settings.ZoomWheel) svg.on('wheel', evnt => this.mouseWheel(evnt)); } if (browser.touches && ((settings.Zooming && settings.ZoomTouch) || settings.ContextMenu)) svg.on('touchstart', evnt => this.startTouchZoom(evnt)); if (settings.ContextMenu) { if (browser.touches) { svg_x.on('touchstart', evnt => this.startSingleTouchHandling('x', evnt)); svg_y.on('touchstart', evnt => this.startSingleTouchHandling('y', evnt)); } svg.on('contextmenu', evnt => this.showContextMenu('', evnt)); svg_x.on('contextmenu', evnt => this.showContextMenu('x', evnt)); svg_y.on('contextmenu', evnt => this.showContextMenu('y', evnt)); } svg_x.on('mousemove', evnt => this.showAxisStatus('x', evnt)); svg_y.on('mousemove', evnt => this.showAxisStatus('y', evnt)); svg.property('interactive_set', true); return this; }, /** @summary Add keys handler */ addFrameKeysHandler() { if (this.keys_handler || (typeof window === 'undefined')) return; this.keys_handler = evnt => this.processKeyPress(evnt); window.addEventListener('keydown', this.keys_handler, false); }, /** @summary Handle key press */ processKeyPress(evnt) { // no custom keys handling when menu is present if (hasMenu()) return true; const allowed = ['PageUp', 'PageDown', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'PrintScreen', 'Escape', '*'], main = this.selectDom(), pp = this.getPadPainter(); let key = evnt.key; if (!settings.HandleKeys || main.empty() || (this.enabledKeys === false) || (getActivePad() !== pp) || (allowed.indexOf(key) < 0)) return false; if (evnt.shiftKey) key = `Shift ${key}`; if (evnt.altKey) key = `Alt ${key}`; if (evnt.ctrlKey) key = `Ctrl ${key}`; const zoom = { name: 'x', dleft: 0, dright: 0 }; switch (key) { case 'ArrowLeft': zoom.dleft = -1; zoom.dright = 1; break; case 'ArrowRight': zoom.dleft = 1; zoom.dright = -1; break; case 'Ctrl ArrowLeft': zoom.dleft = zoom.dright = -1; break; case 'Ctrl ArrowRight': zoom.dleft = zoom.dright = 1; break; case 'ArrowUp': zoom.name = 'y'; zoom.dleft = 1; zoom.dright = -1; break; case 'ArrowDown': zoom.name = 'y'; zoom.dleft = -1; zoom.dright = 1; break; case 'Ctrl ArrowUp': zoom.name = 'y'; zoom.dleft = zoom.dright = 1; break; case 'Ctrl ArrowDown': zoom.name = 'y'; zoom.dleft = zoom.dright = -1; break; case 'Escape': pp?.enlargePad(null, false, true); return true; } if (zoom.dleft || zoom.dright) { if (!settings.Zooming) return false; // in 3d mode with orbit control ignore simple arrows if (this.mode3d && (key.indexOf('Ctrl') !== 0)) return false; this.analyzeMouseWheelEvent(null, zoom, 0.5); if (zoom.changed) this.zoomSingle(zoom.name, zoom.min, zoom.max, true); evnt.stopPropagation(); evnt.preventDefault(); } else { const func = pp?.findPadButton(key); if (func) { pp.clickPadButton(func); evnt.stopPropagation(); evnt.preventDefault(); } } return true; // just process any key press }, /** @summary Function called when frame is clicked and object selection can be performed * @desc such event can be used to select */ processFrameClick(pnt, dblckick) { const pp = this.getPadPainter(); if (!pp) return; pnt.painters = true; // provide painters reference in the hints pnt.disabled = true; // do not invoke graphics // collect tooltips from pad painter - it has list of all drawn objects const hints = pp.processPadTooltipEvent(pnt); let exact = null, res; for (let k = 0; (k < hints.length) && !exact; ++k) { if (hints[k] && hints[k].exact) exact = hints[k]; } if (exact) { const handler = dblckick ? this._dblclick_handler : this._click_handler; if (isFunc(handler)) res = handler(exact.user_info, pnt); } if (!dblckick) { pp.selectObjectPainter(exact ? exact.painter : this, { x: pnt.x + (this._frame_x || 0), y: pnt.y + (this._frame_y || 0) }); } return res; }, /** @summary Check mouse moving */ shiftMoveHanlder(evnt, pos0) { if (evnt.buttons === this._shifting_buttons) { const frame = this.getFrameSvg(), pos = d3_pointer(evnt, frame.node()), main_svg = this.draw_g.selectChild('.main_layer'), dx = pos0[0] - pos[0], dy = (this.scales_ndim === 1) ? 0 : pos0[1] - pos[1], w = this.getFrameWidth(), h = this.getFrameHeight(); this._shifting_dx = dx; this._shifting_dy = dy; main_svg.attr('viewBox', `${dx} ${dy} ${w} ${h}`); evnt.preventDefault(); evnt.stopPropagation(); } }, /** @summary mouse up handler for shifting */ shiftUpHanlder(evnt) { evnt.preventDefault(); d3_select(window).on('mousemove.shiftHandler', null) .on('mouseup.shiftHandler', null); if ((this._shifting_dx !== undefined) && (this._shifting_dy !== undefined)) this.performScalesShift(); }, /** @summary Shift scales on defined positions */ performScalesShift() { const w = this.getFrameWidth(), h = this.getFrameHeight(), main_svg = this.draw_g.selectChild('.main_layer'), gr = this.getGrFuncs(), xmin = gr.revertAxis('x', this._shifting_dx), xmax = gr.revertAxis('x', this._shifting_dx + w), ymin = gr.revertAxis('y', this._shifting_dy + h), ymax = gr.revertAxis('y', this._shifting_dy); main_svg.attr('viewBox', `0 0 ${w} ${h}`); delete this._shifting_dx; delete this._shifting_dy; setPainterTooltipEnabled(this, true); if (this.scales_ndim === 1) this.zoomSingle('x', xmin, xmax); else this.zoom(xmin, xmax, ymin, ymax); }, /** @summary Start mouse rect zooming */ startRectSel(evnt) { // ignore when touch selection is activated if (this.zoom_kind > 100) return; const frame = this.getFrameSvg(), pos = d3_pointer(evnt, frame.node()); if ((evnt.buttons === 3) || (evnt.button === 1)) { this.clearInteractiveElements(); this._shifting_buttons = evnt.buttons; if (!evnt.$emul) { d3_select(window).on('mousemove.shiftHandler', evnt2 => this.shiftMoveHanlder(evnt2, pos)) .on('mouseup.shiftHandler', evnt2 => this.shiftUpHanlder(evnt2), true); } setPainterTooltipEnabled(this, false); evnt.preventDefault(); evnt.stopPropagation(); return; } // ignore all events from non-left button if (evnt.button !== 0) return; evnt.preventDefault(); this.clearInteractiveElements(); const w = this.getFrameWidth(), h = this.getFrameHeight(); this.zoom_lastpos = pos; this.zoom_curr = [Math.max(0, Math.min(w, pos[0])), Math.max(0, Math.min(h, pos[1]))]; this.zoom_origin = [0, 0]; this.zoom_second = false; if ((pos[0] < 0) || (pos[0] > w)) { this.zoom_second = (pos[0] > w) && this.y2_handle; this.zoom_kind = 3; // only y this.zoom_origin[1] = this.zoom_curr[1]; this.zoom_curr[0] = w; this.zoom_curr[1] += 1; } else if ((pos[1] < 0) || (pos[1] > h)) { this.zoom_second = (pos[1] < 0) && this.x2_handle; this.zoom_kind = 2; // only x this.zoom_origin[0] = this.zoom_curr[0]; this.zoom_curr[0] += 1; this.zoom_curr[1] = h; } else { this.zoom_kind = 1; // x and y this.zoom_origin[0] = this.zoom_curr[0]; this.zoom_origin[1] = this.zoom_curr[1]; } if (!evnt.$emul) { d3_select(window).on('mousemove.zoomRect', evnt2 => this.moveRectSel(evnt2)) .on('mouseup.zoomRect', evnt2 => this.endRectSel(evnt2), true); } this.zoom_rect = null; // disable tooltips in frame painter setPainterTooltipEnabled(this, false); evnt.stopPropagation(); if (this.zoom_kind !== 1) return postponePromise(() => this.startLabelsMove(), 500); }, /** @summary Starts labels move */ startLabelsMove() { if (this.zoom_rect) return; const handle = (this.zoom_kind === 2) ? this.x_handle : this.y_handle; if (!isFunc(handle?.processLabelsMove) || !this.zoom_lastpos) return; if (handle.processLabelsMove('start', this.zoom_lastpos)) this.zoom_labels = handle; }, /** @summary Process mouse rect zooming */ moveRectSel(evnt) { if ((this.zoom_kind === 0) || (this.zoom_kind > 100)) return; evnt.preventDefault(); const m = d3_pointer(evnt, this.getFrameSvg().node()); if (this.zoom_labels) return this.zoom_labels.processLabelsMove('move', m); this.zoom_lastpos[0] = m[0]; this.zoom_lastpos[1] = m[1]; m[0] = Math.max(0, Math.min(this.getFrameWidth(), m[0])); m[1] = Math.max(0, Math.min(this.getFrameHeight(), m[1])); switch (this.zoom_kind) { case 1: this.zoom_curr[0] = m[0]; this.zoom_curr[1] = m[1]; break; case 2: this.zoom_curr[0] = m[0]; break; case 3: this.zoom_curr[1] = m[1]; break; } const x = Math.min(this.zoom_origin[0], this.zoom_curr[0]), y = Math.min(this.zoom_origin[1], this.zoom_curr[1]), w = Math.abs(this.zoom_curr[0] - this.zoom_origin[0]), h = Math.abs(this.zoom_curr[1] - this.zoom_origin[1]); if (!this.zoom_rect) { // ignore small changes, can be switching to labels move if ((this.zoom_kind !== 1) && ((w < 2) || (h < 2))) return; this.zoom_rect = this.getFrameSvg() .append('rect') .style('pointer-events', 'none') .call(addHighlightStyle, true); } this.zoom_rect.attr('x', x).attr('y', y).attr('width', w).attr('height', h); }, /** @summary Finish mouse rect zooming */ endRectSel(evnt) { if ((this.zoom_kind === 0) || (this.zoom_kind > 100)) return; evnt.preventDefault(); if (!evnt.$emul) { d3_select(window).on('mousemove.zoomRect', null) .on('mouseup.zoomRect', null); } const m = d3_pointer(evnt, this.getFrameSvg().node()); let kind = this.zoom_kind, pr; if (this.zoom_labels) this.zoom_labels.processLabelsMove('stop', m); else { const changed = [this.can_zoom_x, this.can_zoom_y]; m[0] = Math.max(0, Math.min(this.getFrameWidth(), m[0])); m[1] = Math.max(0, Math.min(this.getFrameHeight(), m[1])); switch (this.zoom_kind) { case 1: this.zoom_curr[0] = m[0]; this.zoom_curr[1] = m[1]; break; case 2: this.zoom_curr[0] = m[0]; changed[1] = false; break; // only X case 3: this.zoom_curr[1] = m[1]; changed[0] = false; break; // only Y } let xmin, xmax, ymin, ymax, isany = false, namex = 'x', namey = 'y'; if (changed[0] && (Math.abs(this.zoom_curr[0] - this.zoom_origin[0]) > 5)) { if (this.zoom_second && (this.zoom_kind === 2)) namex = 'x2'; const v1 = this.revertAxis(namex, this.zoom_origin[0]), v2 = this.revertAxis(namex, this.zoom_curr[0]); xmin = Math.min(v1, v2); xmax = Math.max(v1, v2); isany = true; } if (changed[1] && (Math.abs(this.zoom_curr[1] - this.zoom_origin[1]) > 5)) { if (this.zoom_second && (this.zoom_kind === 3)) namey = 'y2'; const v1 = this.revertAxis(namey, this.zoom_origin[1]), v2 = this.revertAxis(namey, this.zoom_curr[1]); ymin = Math.min(v1, v2); ymax = Math.max(v1, v2); isany = true; } if (this.swap_xy && !this.zoom_second) [xmin, xmax, ymin, ymax] = [ymin, ymax, xmin, xmax]; if (namex === 'x2') { pr = this.zoomSingle(namex, xmin, xmax, true); kind = 0; } else if (namey === 'y2') { pr = this.zoomSingle(namey, ymin, ymax, true); kind = 0; } else if (isany) { pr = this.zoom(xmin, xmax, ymin, ymax, undefined, undefined, true); kind = 0; } } const pnt = (kind === 1) ? { x: this.zoom_origin[0], y: this.zoom_origin[1] } : null; this.clearInteractiveElements(); // if no zooming was done, select active object instead switch (kind) { case 1: this.processFrameClick(pnt); break; case 2: this.getPadPainter()?.selectObjectPainter(this.x_handle); break; case 3: this.getPadPainter()?.selectObjectPainter(this.y_handle); break; } // return promise - if any return pr; }, /** @summary Handle mouse double click on frame */ mouseDoubleClick(evnt) { evnt.preventDefault(); const m = d3_pointer(evnt, this.getFrameSvg().node()), fw = this.getFrameWidth(), fh = this.getFrameHeight(); this.clearInteractiveElements(); const valid_x = (m[0] >= 0) && (m[0] <= fw), valid_y = (m[1] >= 0) && (m[1] <= fh); if (valid_x && valid_y && this._dblclick_handler) if (this.processFrameClick({ x: m[0], y: m[1] }, true)) return; let kind = (this.can_zoom_x ? 'x' : '') + (this.can_zoom_y ? 'y' : '') + 'z'; if (!valid_x) { if (!this.can_zoom_y) return; kind = this.swap_xy ? 'x' : 'y'; if ((m[0] > fw) && this[kind+'2_handle']) kind += '2'; // let unzoom second axis } else if (!valid_y) { if (!this.can_zoom_x) return; kind = this.swap_xy ? 'y' : 'x'; if ((m[1] < 0) && this[kind+'2_handle']) kind += '2'; // let unzoom second axis } return this.unzoom(kind).then(changed => { if (changed) return; const pp = this.getPadPainter(), rect = this.getFrameRect(); return pp?.selectObjectPainter(pp, { x: m[0] + rect.x, y: m[1] + rect.y, dbl: true }); }); }, /** @summary Start touch zoom */ startTouchZoom(evnt) { evnt.preventDefault(); evnt.stopPropagation(); // in case when zooming was started, block any other kind of events // also prevent zooming together with active dragging if ((this.zoom_kind !== 0) || drag_kind) return; const arr = get_touch_pointers(evnt, this.getFrameSvg().node()); // normally double-touch will be handled // touch with single click used for context menu if (arr.length === 1) { // this is touch with single element const now = new Date().getTime(); let tmdiff = 1e10, dx = 100, dy = 100; if (this.last_touch_time && this.last_touch_pos) { tmdiff = now - this.last_touch_time; dx = Math.abs(arr[0][0] - this.last_touch_pos[0]); dy = Math.abs(arr[0][1] - this.last_touch_pos[1]); } this.last_touch_time = now; this.last_touch_pos = arr[0]; if ((tmdiff < 500) && (dx < 20) && (dy < 20)) { this.clearInteractiveElements(); this.unzoom('xyz'); delete this.last_touch_time; } else if (settings.ContextMenu) this.startSingleTouchHandling('', evnt); } if ((arr.length !== 2) || !settings.Zooming || !settings.ZoomTouch) return; this.clearInteractiveElements(); // clear single touch handler this.endSingleTouchHandling(null); const pnt1 = arr[0], pnt2 = arr[1], w = this.getFrameWidth(), h = this.getFrameHeight(); this.zoom_curr = [Math.min(pnt1[0], pnt2[0]), Math.min(pnt1[1], pnt2[1])]; this.zoom_origin = [Math.max(pnt1[0], pnt2[0]), Math.max(pnt1[1], pnt2[1])]; this.zoom_second = false; if ((this.zoom_curr[0] < 0) || (this.zoom_curr[0] > w)) { this.zoom_second = (this.zoom_curr[0] > w) && this.y2_handle; this.zoom_kind = 103; // only y this.zoom_curr[0] = 0; this.zoom_origin[0] = w; } else if ((this.zoom_origin[1] > h) || (this.zoom_origin[1] < 0)) { this.zoom_second = (this.zoom_origin[1] < 0) && this.x2_handle; this.zoom_kind = 102; // only x this.zoom_curr[1] = 0; this.zoom_origin[1] = h; } else this.zoom_kind = 101; // x and y drag_kind = 'zoom'; // block other possible dragging setPainterTooltipEnabled(this, false); this.zoom_rect = this.getFrameSvg().append('rect') .attr('id', 'zoomRect') .attr('x', this.zoom_curr[0]) .attr('y', this.zoom_curr[1]) .attr('width', this.zoom_origin[0] - this.zoom_curr[0]) .attr('height', this.zoom_origin[1] - this.zoom_curr[1]) .call(addHighlightStyle, true); if (!evnt.$emul) { d3_select(window).on('touchmove.zoomRect', evnt2 => this.moveTouchZoom(evnt2)) .on('touchcancel.zoomRect', evnt2 => this.endTouchZoom(evnt2)) .on('touchend.zoomRect', evnt2 => this.endTouchZoom(evnt2)); } }, /** @summary Move touch zooming */ moveTouchZoom(evnt) { if (this.zoom_kind < 100) return; evnt.preventDefault(); const arr = get_touch_pointers(evnt, this.getFrameSvg().node()); if (arr.length !== 2) return this.clearInteractiveElements(); const pnt1 = arr[0], pnt2 = arr[1]; if (this.zoom_kind !== 103) { this.zoom_curr[0] = Math.min(pnt1[0], pnt2[0]); this.zoom_origin[0] = Math.max(pnt1[0], pnt2[0]); } if (this.zoom_kind !== 102) { this.zoom_curr[1] = Math.min(pnt1[1], pnt2[1]); this.zoom_origin[1] = Math.max(pnt1[1], pnt2[1]); } this.zoom_rect.attr('x', this.zoom_curr[0]) .attr('y', this.zoom_curr[1]) .attr('width', this.zoom_origin[0] - this.zoom_curr[0]) .attr('height', this.zoom_origin[1] - this.zoom_curr[1]); if ((this.zoom_origin[0] - this.zoom_curr[0] > 10) || (this.zoom_origin[1] - this.zoom_curr[1] > 10)) setPainterTooltipEnabled(this, false); evnt.stopPropagation(); }, /** @summary End touch zooming handler */ endTouchZoom(evnt) { if (this.zoom_kind < 100) return; drag_kind = ''; // reset global flag evnt.preventDefault(); if (!evnt.$emul) { d