UNPKG

jsrootdi

Version:
1,446 lines (1,179 loc) 54.1 kB
import { select as d3_select, color as d3_color } from '../d3.mjs'; import { HelveticerRegularJson, Font, WebGLRenderer, WebGLRenderTarget, CanvasTexture, TextureLoader, BufferGeometry, BufferAttribute, Float32BufferAttribute, Vector2, Vector3, Color, Points, PointsMaterial, LineSegments, LineDashedMaterial, LineBasicMaterial, OrbitControls, Raycaster, SVGRenderer } from '../three.mjs'; import { browser, settings, constants, isBatchMode, isNodeJs, isObject, isFunc, isStr, getDocument } from '../core.mjs'; import { getElementRect, getAbsPosInCanvas, makeTranslate } from './BasePainter.mjs'; import { TAttMarkerHandler } from './TAttMarkerHandler.mjs'; import { getSvgLineStyle } from './TAttLineHandler.mjs'; /** @ummary Create three.js Color instance, handles optional opacity * @private */ function getMaterialArgs(color, args) { if (!args || !isObject(args)) args = {}; if (isStr(color) && (((color[0] === '#') && (color.length === 9)) || (color.indexOf('rgba') >= 0))) { const col = d3_color(color); args.color = new Color(col.r, col.g, col.b); args.opacity = col.opacity ?? 1; args.transparent = args.opacity < 1; } else args.color = new Color(color); return args; } const HelveticerRegularFont = new Font(HelveticerRegularJson); function createSVGRenderer(as_is, precision, doc) { if (as_is) { if (doc !== undefined) globalThis.docuemnt = doc; const rndr = new SVGRenderer(); rndr.setPrecision(precision); return rndr; } const excl_style1 = ';stroke-opacity:1;stroke-width:1;stroke-linecap:round', excl_style2 = ';fill-opacity:1', doc_wrapper = { svg_attr: {}, svg_style: {}, path_attr: {}, accPath: '', createElementNS(ns, kind) { if (kind === 'path') { return { _wrapper: this, setAttribute(name, value) { // cut useless fill-opacity:1 at the end of many SVG attributes if ((name === 'style') && value) { const pos1 = value.indexOf(excl_style1); if ((pos1 >= 0) && (pos1 === value.length - excl_style1.length)) value = value.slice(0, value.length - excl_style1.length); const pos2 = value.indexOf(excl_style2); if ((pos2 >= 0) && (pos2 === value.length - excl_style2.length)) value = value.slice(0, value.length - excl_style2.length); } this._wrapper.path_attr[name] = value; } }; } if (kind !== 'svg') { console.error(`not supported element for SVGRenderer ${kind}`); return null; } return { _wrapper: this, childNodes: [], // may be accessed - make dummy style: this.svg_style, // for background color setAttribute(name, value) { this._wrapper.svg_attr[name] = value; }, appendChild(_node) { this._wrapper.accPath += `<path style="${this._wrapper.path_attr.style}" d="${this._wrapper.path_attr.d}"/>`; this._wrapper.path_attr = {}; }, removeChild(_node) { this.childNodes = []; } }; } }; let originalDocument; if (isNodeJs()) { originalDocument = globalThis.document; globalThis.document = doc_wrapper; } const rndr = new SVGRenderer(); if (isNodeJs()) globalThis.document = originalDocument; rndr.doc_wrapper = doc_wrapper; // use it to get final SVG code rndr.originalRender = rndr.render; rndr.render = function(scene, camera) { const originalDocument = globalThis.document; if (isNodeJs()) globalThis.document = this.doc_wrapper; this.originalRender(scene, camera); if (isNodeJs()) globalThis.document = originalDocument; }; rndr.clearHTML = function() { this.doc_wrapper.accPath = ''; }; rndr.makeOuterHTML = function() { const wrap = this.doc_wrapper, _textSizeAttr = `viewBox="${wrap.svg_attr.viewBox}" width="${wrap.svg_attr.width}" height="${wrap.svg_attr.height}"`, _textClearAttr = wrap.svg_style.backgroundColor ? ` style="background:${wrap.svg_style.backgroundColor}"` : ''; return `<svg xmlns="http://www.w3.org/2000/svg" ${_textSizeAttr}${_textClearAttr}>${wrap.accPath}</svg>`; }; rndr.fillTargetSVG = function(svg) { if (isNodeJs()) { const wrap = this.doc_wrapper; svg.setAttribute('viewBox', wrap.svg_attr.viewBox); svg.setAttribute('width', wrap.svg_attr.width); svg.setAttribute('height', wrap.svg_attr.height); svg.style.background = wrap.svg_style.backgroundColor || ''; svg.innerHTML = wrap.accPath; } else { const src = this.domElement; svg.setAttribute('viewBox', src.getAttribute('viewBox')); svg.setAttribute('width', src.getAttribute('width')); svg.setAttribute('height', src.getAttribute('height')); svg.style.background = src.style.backgroundColor; while (src.firstChild) { const elem = src.firstChild; src.removeChild(elem); svg.appendChild(elem); } } }; rndr.setPrecision(precision); return rndr; } /** @ummary Define rendering kind which will be used for rendering of 3D elements * @param {value} [render3d] - preconfigured value, will be used if applicable * @param {value} [is_batch] - is batch mode is configured * @return {value} - rendering kind, see constants.Render3D * @private */ function getRender3DKind(render3d, is_batch) { if (is_batch === undefined) is_batch = isBatchMode(); if (!render3d) render3d = is_batch ? settings.Render3DBatch : settings.Render3D; const rc = constants.Render3D; if (render3d === rc.Default) render3d = is_batch ? rc.WebGLImage : rc.WebGL; if (is_batch && (render3d === rc.WebGL)) render3d = rc.WebGLImage; return render3d; } const Handling3DDrawings = { /** @summary Access current 3d mode * @param {string} [new_value] - when specified, set new 3d mode * @return current value * @private */ access3dKind(new_value) { const svg = this.getPadSvg(); if (svg.empty()) return -1; // returns kind of currently created 3d canvas const kind = svg.property('can3d'); if (new_value !== undefined) svg.property('can3d', new_value); return ((kind === null) || (kind === undefined)) ? -1 : kind; }, /** @summary Returns size which availble for 3D drawing. * @desc One uses frame sizes for the 3D drawing - like TH2/TH3 objects * @private */ getSizeFor3d(can3d /*, render3d */) { if (can3d === undefined) { // analyze which render/embed mode can be used can3d = getRender3DKind(); // all non-webgl elements can be embedded into SVG as is if (can3d !== constants.Render3D.WebGL) can3d = constants.Embed3D.EmbedSVG; else if (settings.Embed3D !== constants.Embed3D.Default) can3d = settings.Embed3D; else if (browser.isFirefox) can3d = constants.Embed3D.Embed; else if (browser.chromeVersion > 95) // version 96 works partially, 97 works fine can3d = constants.Embed3D.Embed; else can3d = constants.Embed3D.Overlay; } const pad = this.getPadSvg(), clname = 'draw3d_' + (this.getPadName() || 'canvas'); if (pad.empty()) { // this is a case when object drawn without canvas const rect = getElementRect(this.selectDom()); if ((rect.height < 10) && (rect.width > 10)) { rect.height = Math.round(0.66 * rect.width); this.selectDom().style('height', rect.height + 'px'); } rect.x = 0; rect.y = 0; rect.clname = clname; rect.can3d = -1; return rect; } const fp = this.getFramePainter(), pp = this.getPadPainter(); let size; if (fp?.mode3d && (can3d > 0)) size = fp.getFrameRect(); else { let elem = (can3d > 0) ? pad : this.getCanvSvg(); size = { x: 0, y: 0, width: elem.property('draw_width'), height: elem.property('draw_height') }; if (Number.isNaN(size.width) || Number.isNaN(size.height)) { size.width = pp.getPadWidth(); size.height = pp.getPadHeight(); } else if (fp && !fp.mode3d) { elem = this.getFrameSvg(); size.x = elem.property('draw_x'); size.y = elem.property('draw_y'); } } size.clname = clname; size.can3d = can3d; const rect = pp?.getPadRect(); if (rect) { // while 3D canvas uses area also for the axis labels, extend area relative to normal frame const dx = Math.round(size.width*0.07), dy = Math.round(size.height*0.05); size.x = Math.max(0, size.x-dx); size.y = Math.max(0, size.y-dy); size.width = Math.min(size.width + 2*dx, rect.width - size.x); size.height = Math.min(size.height + 2*dy, rect.height - size.y); } if (can3d === 1) size = getAbsPosInCanvas(this.getPadSvg(), size); return size; }, /** @summary Clear all 3D drawings * @return can3d value - how webgl canvas was placed * @private */ clear3dCanvas() { const can3d = this.access3dKind(null); if (can3d < 0) { // remove first child from main element - if it is canvas const main = this.selectDom().node(); let chld = main?.firstChild; if (chld && !chld.$jsroot) chld = chld.nextSibling; if (chld?.$jsroot) { delete chld.painter; main.removeChild(chld); } return can3d; } const size = this.getSizeFor3d(can3d); if (size.can3d === 0) { d3_select(this.getCanvSvg().node().nextSibling).remove(); // remove html5 canvas this.getCanvSvg().style('display', null); // show SVG canvas } else { if (this.getPadSvg().empty()) return; this.apply3dSize(size).remove(); this.getFrameSvg().style('display', null); // clear display property } return can3d; }, /** @summary Add 3D canvas * @private */ add3dCanvas(size, canv, webgl) { if (!canv || (size.can3d < -1)) return; if (size.can3d === -1) { // case when 3D object drawn without canvas const main = this.selectDom().node(); if (main !== null) { main.appendChild(canv); canv.painter = this; canv.$jsroot = true; // mark canvas as added by jsroot } return; } if ((size.can3d > 0) && !webgl) size.can3d = constants.Embed3D.EmbedSVG; this.access3dKind(size.can3d); if (size.can3d === 0) { this.getCanvSvg().style('display', 'none'); // hide SVG canvas this.getCanvSvg().node().parentNode.appendChild(canv); // add directly } else { if (this.getPadSvg().empty()) return; // first hide normal frame this.getFrameSvg().style('display', 'none'); const elem = this.apply3dSize(size); elem.attr('title', '').node().appendChild(canv); } }, /** @summary Apply size to 3D elements * @private */ apply3dSize(size, onlyget) { if (size.can3d < 0) return d3_select(null); let elem; if (size.can3d > 1) { elem = this.getLayerSvg(size.clname); if (onlyget) return elem; const svg = this.getPadSvg(); if (size.can3d === constants.Embed3D.EmbedSVG) { // this is SVG mode or image mode - just create group to hold element if (elem.empty()) elem = svg.insert('g', '.primitives_layer').attr('class', size.clname); makeTranslate(elem, size.x, size.y); } else { if (elem.empty()) elem = svg.insert('foreignObject', '.primitives_layer').attr('class', size.clname); elem.attr('x', size.x) .attr('y', size.y) .attr('width', size.width) .attr('height', size.height) .attr('viewBox', `0 0 ${size.width} ${size.height}`) .attr('preserveAspectRatio', 'xMidYMid'); } } else { let prnt = this.getCanvSvg().node().parentNode; elem = d3_select(prnt).select('.' + size.clname); if (onlyget) return elem; // force redraw by resize this.getCanvSvg().property('redraw_by_resize', true); if (elem.empty()) { elem = d3_select(prnt).append('div').attr('class', size.clname) .style('user-select', 'none'); } // our position inside canvas, but to set 'absolute' position we should use // canvas element offset relative to first parent with non-static position // now try to use getBoundingClientRect - it should be more precise const pos0 = prnt.getBoundingClientRect(), doc = getDocument(); while (prnt) { if (prnt === doc) { prnt = null; break; } try { if (getComputedStyle(prnt).position !== 'static') break; } catch (err) { break; } prnt = prnt.parentNode; } const pos1 = prnt?.getBoundingClientRect() ?? { top: 0, left: 0 }, offx = Math.round(pos0.left - pos1.left), offy = Math.round(pos0.top - pos1.top); elem.style('position', 'absolute').style('left', (size.x + offx) + 'px').style('top', (size.y + offy) + 'px').style('width', size.width + 'px').style('height', size.height + 'px'); } return elem; } }; // Handling3DDrawings /** @summary Assigns method to handle 3D drawings inside SVG * @private */ function assign3DHandler(painter) { Object.assign(painter, Handling3DDrawings); } /** @summary Creates renderer for the 3D drawings * @param {value} width - rendering width * @param {value} height - rendering height * @param {value} render3d - render type, see {@link constants.Render3D} * @param {object} args - different arguments for creating 3D renderer * @return {Promise} with renderer object * @private */ async function createRender3D(width, height, render3d, args) { const rc = constants.Render3D, doc = getDocument(); render3d = getRender3DKind(render3d); if (!args) args = { antialias: true, alpha: true }; let promise; if (render3d === rc.SVG) { // SVG rendering const r = createSVGRenderer(false, 0, doc); r.jsroot_dom = doc.createElementNS('http://www.w3.org/2000/svg', 'svg'); promise = Promise.resolve(r); } else if (isNodeJs()) { // try to use WebGL inside node.js - need to create headless context promise = import('canvas').then(node_canvas => { args.canvas = node_canvas.default.createCanvas(width, height); args.canvas.addEventListener = () => {}; // dummy args.canvas.removeEventListener = () => {}; // dummy args.canvas.style = {}; return import('gl'); }).then(node_gl => { const gl = node_gl.default(width, height, { preserveDrawingBuffer: true }); if (!gl) throw Error('Fail to create headless-gl'); args.context = gl; gl.canvas = args.canvas; const r = new WebGLRenderer(args); r.jsroot_output = new WebGLRenderTarget(width, height); r.setRenderTarget(r.jsroot_output); r.jsroot_dom = doc.createElementNS('http://www.w3.org/2000/svg', 'image'); return r; }); } else if (render3d === rc.WebGL) { // interactive WebGL Rendering promise = Promise.resolve(new WebGLRenderer(args)); } else { // rendering with WebGL directly into svg image const r = new WebGLRenderer(args); r.jsroot_dom = doc.createElementNS('http://www.w3.org/2000/svg', 'image'); promise = Promise.resolve(r); } return promise.then(renderer => { if (!renderer.jsroot_dom) renderer.jsroot_dom = renderer.domElement; else renderer.jsroot_custom_dom = true; // res.renderer.setClearColor('#000000', 1); // res.renderer.setClearColor(0x0, 0); renderer.jsroot_render3d = render3d; // which format used to convert into images renderer.jsroot_image_format = 'png'; renderer.originalSetSize = renderer.setSize; // apply size to dom element renderer.setSize = function(width, height, updateStyle) { if (this.jsroot_custom_dom) { this.jsroot_dom.setAttribute('width', width); this.jsroot_dom.setAttribute('height', height); } this.originalSetSize(width, height, updateStyle); }; renderer.setSize(width, height); return renderer; }); } /** @summary Cleanup created renderer object * @private */ function cleanupRender3D(renderer) { if (!renderer) return; if (isNodeJs()) { const ctxt = isFunc(renderer.getContext) ? renderer.getContext() : null, ext = ctxt?.getExtension('STACKGL_destroy_context'); if (isFunc(ext?.destroy)) ext.destroy(); } else { // suppress warnings in Chrome about lost webgl context, not required in firefox if (browser.isChrome && isFunc(renderer.forceContextLoss)) renderer.forceContextLoss(); if (isFunc(renderer.dispose)) renderer.dispose(); } } /** @summary Cleanup previous renderings before doing next one * @desc used together with SVG * @private */ function beforeRender3D(renderer) { if (isFunc(renderer.clearHTML)) renderer.clearHTML(); } /** @summary Post-process result of rendering * @desc used together with SVG or node.js image rendering * @private */ function afterRender3D(renderer) { const rc = constants.Render3D; if (renderer.jsroot_render3d === rc.WebGL) return; if (renderer.jsroot_render3d === rc.SVG) { // case of SVGRenderer renderer.fillTargetSVG(renderer.jsroot_dom); } else if (isNodeJs()) { // this is WebGL rendering in node.js const canvas = renderer.domElement, context = canvas.getContext('2d'), pixels = new Uint8Array(4 * canvas.width * canvas.height); renderer.readRenderTargetPixels(renderer.jsroot_output, 0, 0, canvas.width, canvas.height, pixels); // small code to flip Y scale let indx1 = 0, indx2 = (canvas.height - 1) * 4 * canvas.width, k, d; while (indx1 < indx2) { for (k = 0; k < 4 * canvas.width; ++k) { d = pixels[indx1 + k]; pixels[indx1 + k] = pixels[indx2 + k]; pixels[indx2 + k] = d; } indx1 += 4 * canvas.width; indx2 -= 4 * canvas.width; } const imageData = context.createImageData(canvas.width, canvas.height); imageData.data.set(pixels); context.putImageData(imageData, 0, 0); const format = 'image/' + renderer.jsroot_image_format, dataUrl = canvas.toDataURL(format); renderer.jsroot_dom.setAttribute('href', dataUrl); } else { const dataUrl = renderer.domElement.toDataURL('image/' + renderer.jsroot_image_format); renderer.jsroot_dom.setAttribute('href', dataUrl); } } // ======================================================================================================== /** * @summary Tooltip handler for 3D drawings * * @private */ class TooltipFor3D { /** @summary constructor * @param {object} dom - DOM element * @param {object} canvas - canvas for 3D rendering */ constructor(prnt, canvas) { this.tt = null; this.cont = null; this.lastlbl = ''; this.parent = prnt || getDocument().body; this.canvas = canvas; // we need canvas to recalculate mouse events this.abspos = !prnt; } /** @summary check parent */ checkParent(prnt) { if (prnt && (this.parent !== prnt)) { this.hide(); this.parent = prnt; } } /** @summary extract position from event * @desc can be used to process it later when event is gone */ extract_pos(e) { if (isObject(e) && (e.u !== undefined) && (e.l !== undefined)) return e; const res = { u: 0, l: 0 }; if (this.abspos) { res.l = e.pageX; res.u = e.pageY; } else { res.l = e.offsetX; res.u = e.offsetY; } return res; } /** @summary Method used to define position of next tooltip * @desc event is delivered from canvas, * but position should be calculated relative to the element where tooltip is placed */ pos(e) { if (!this.tt) return; const pos = this.extract_pos(e); if (!this.abspos) { const rect1 = this.parent.getBoundingClientRect(), rect2 = this.canvas.getBoundingClientRect(); if ((rect1.left !== undefined) && (rect2.left!== undefined)) pos.l += (rect2.left-rect1.left); if ((rect1.top !== undefined) && (rect2.top!== undefined)) pos.u += rect2.top-rect1.top; if (pos.l + this.tt.offsetWidth + 3 >= this.parent.offsetWidth) pos.l = this.parent.offsetWidth - this.tt.offsetWidth - 3; if (pos.u + this.tt.offsetHeight + 15 >= this.parent.offsetHeight) pos.u = this.parent.offsetHeight - this.tt.offsetHeight - 15; // one should find parent with non-static position, // all absolute coordinates calculated relative to such node let abs_parent = this.parent; while (abs_parent) { const style = getComputedStyle(abs_parent); if (!style || (style.position !== 'static')) break; if (!abs_parent.parentNode || (abs_parent.parentNode.nodeType !== 1)) break; abs_parent = abs_parent.parentNode; } if (abs_parent && (abs_parent !== this.parent)) { const rect0 = abs_parent.getBoundingClientRect(); pos.l += (rect1.left - rect0.left); pos.u += (rect1.top - rect0.top); } } this.tt.style.top = `${pos.u+15}px`; this.tt.style.left = `${pos.l+3}px`; } /** @summary Show tooltip */ show(v /* , mouse_pos, status_func */) { if (!v) return this.hide(); if (isObject(v) && (v.lines || v.line)) { if (v.only_status) return this.hide(); if (v.line) v = v.line; else { let res = v.lines[0]; for (let n = 1; n < v.lines.length; ++n) res += '<br/>' + v.lines[n]; v = res; } } if (this.tt === null) { const doc = getDocument(); this.tt = doc.createElement('div'); this.tt.setAttribute('style', 'opacity: 1; filter: alpha(opacity=1); position: absolute; display: block; overflow: hidden; z-index: 101;'); this.cont = doc.createElement('div'); this.cont.setAttribute('style', 'display: block; padding: 2px 12px 3px 7px; margin-left: 5px; font-size: 11px; background: #777; color: #fff;'); this.tt.appendChild(this.cont); this.parent.appendChild(this.tt); } if (this.lastlbl !== v) { this.cont.innerHTML = v; this.lastlbl = v; this.tt.style.width = 'auto'; // let it be automatically resizing... } } /** @summary Hide tooltip */ hide() { if (this.tt !== null) this.parent.removeChild(this.tt); this.tt = null; this.lastlbl = ''; } } // class TooltipFor3D /** @summary Create OrbitControls for painter * @private */ function createOrbitControl(painter, camera, scene, renderer, lookat) { const enable_zoom = settings.Zooming && settings.ZoomMouse, enable_select = isFunc(painter.processMouseClick); let control = null; function control_mousedown(evnt) { if (!control) return; // function used to hide some events from orbit control and redirect them to zooming rect if (control.mouse_zoom_mesh) { evnt.stopImmediatePropagation(); evnt.stopPropagation(); return; } // only left-button is considered if ((evnt.button!==undefined) && (evnt.button !== 0)) return; if ((evnt.buttons!==undefined) && (evnt.buttons !== 1)) return; if (control.enable_zoom) { control.mouse_zoom_mesh = control.detectZoomMesh(evnt); if (control.mouse_zoom_mesh) { // just block orbit control evnt.stopImmediatePropagation(); evnt.stopPropagation(); return; } } if (control.enable_select) control.mouse_select_pnt = control.getMousePos(evnt, {}); } function control_mouseup(evnt) { if (!control) return; if (control.mouse_zoom_mesh && control.mouse_zoom_mesh.point2 && control.painter.get3dZoomCoord) { let kind = control.mouse_zoom_mesh.object.zoom, pos1 = control.painter.get3dZoomCoord(control.mouse_zoom_mesh.point, kind), pos2 = control.painter.get3dZoomCoord(control.mouse_zoom_mesh.point2, kind); if (pos1 > pos2) [pos1, pos2] = [pos2, pos1]; if ((kind === 'z') && control.mouse_zoom_mesh.object.use_y_for_z) kind = 'y'; // try to zoom if ((pos1 < pos2) && control.painter.zoom(kind, pos1, pos2)) control.mouse_zoom_mesh = null; } // if selection was drawn, it should be removed and picture rendered again if (control.enable_zoom) control.removeZoomMesh(); // only left-button is considered // if ((evnt.button!==undefined) && (evnt.button !== 0)) return; // if ((evnt.buttons!==undefined) && (evnt.buttons !== 1)) return; if (control.enable_select && control.mouse_select_pnt) { const pnt = control.getMousePos(evnt, {}), same_pnt = (pnt.x === control.mouse_select_pnt.x) && (pnt.y === control.mouse_select_pnt.y); delete control.mouse_select_pnt; if (same_pnt) { const intersects = control.getMouseIntersects(pnt); control.painter.processMouseClick(pnt, intersects, evnt); } } } function render3DFired(painter) { if (!painter || painter.renderer === undefined) return false; return painter.render_tmout !== undefined; // when timeout configured, object is prepared for rendering } function control_mousewheel(evnt) { if (!control) return; // try to handle zoom extra if (render3DFired(control.painter) || control.mouse_zoom_mesh) { evnt.preventDefault(); evnt.stopPropagation(); evnt.stopImmediatePropagation(); return; // already fired redraw, do not react on the mouse wheel } const intersect = control.detectZoomMesh(evnt); if (!intersect) return; evnt.preventDefault(); evnt.stopPropagation(); evnt.stopImmediatePropagation(); if (isFunc(control.painter?.analyzeMouseWheelEvent)) { let kind = intersect.object.zoom, position = intersect.point[kind]; const item = { name: kind, ignore: false }; // z changes from 0..2*size_z3d, others -size_x3d..+size_x3d switch (kind) { case 'x': position = (position + control.painter.size_x3d)/2/control.painter.size_x3d; break; case 'y': position = (position + control.painter.size_y3d)/2/control.painter.size_y3d; break; case 'z': position = position/2/control.painter.size_z3d; break; } control.painter.analyzeMouseWheelEvent(evnt, item, position, false); if ((kind === 'z') && intersect.object.use_y_for_z) kind = 'y'; control.painter.zoom(kind, item.min, item.max); } } // assign own handler before creating OrbitControl if (settings.Zooming && settings.ZoomWheel) renderer.domElement.addEventListener('wheel', control_mousewheel); if (enable_zoom || enable_select) { renderer.domElement.addEventListener('pointerdown', control_mousedown); renderer.domElement.addEventListener('pointerup', control_mouseup); } control = new OrbitControls(camera, renderer.domElement); control.enableDamping = false; control.dampingFactor = 1.0; control.enableZoom = true; control.enableKeys = settings.HandleKeys; if (lookat) { control.target.copy(lookat); control.target0.copy(lookat); control.update(); } control.tooltip = new TooltipFor3D(painter.selectDom().node(), renderer.domElement); control.painter = painter; control.camera = camera; control.scene = scene; control.renderer = renderer; control.raycaster = new Raycaster(); control.raycaster.params.Line.threshold = 10; control.raycaster.params.Points.threshold = 5; control.mouse_zoom_mesh = null; // zoom mesh, currently used in the zooming control.block_ctxt = false; // require to block context menu command appearing after control ends, required in chrome which inject contextmenu when key released control.block_mousemove = false; // when true, tooltip or cursor will not react on mouse move control.cursor_changed = false; control.control_changed = false; control.control_active = false; control.mouse_ctxt = { x: 0, y: 0, on: false }; control.enable_zoom = enable_zoom; control.enable_select = enable_select; control.cleanup = function() { if (settings.Zooming && settings.ZoomWheel) this.domElement.removeEventListener('wheel', control_mousewheel); if (this.enable_zoom || this.enable_select) { this.domElement.removeEventListener('pointerdown', control_mousedown); this.domElement.removeEventListener('pointerup', control_mouseup); } this.domElement.removeEventListener('click', this.lstn_click); this.domElement.removeEventListener('dblclick', this.lstn_dblclick); this.domElement.removeEventListener('contextmenu', this.lstn_contextmenu); this.domElement.removeEventListener('mousemove', this.lstn_mousemove); this.domElement.removeEventListener('mouseleave', this.lstn_mouseleave); this.dispose(); // this is from OrbitControl itself this.tooltip.hide(); delete this.tooltip; delete this.painter; delete this.camera; delete this.scene; delete this.renderer; delete this.raycaster; delete this.mouse_zoom_mesh; }; control.HideTooltip = function() { this.tooltip.hide(); }; control.getMousePos = function(evnt, mouse) { mouse.x = ('offsetX' in evnt) ? evnt.offsetX : evnt.layerX; mouse.y = ('offsetY' in evnt) ? evnt.offsetY : evnt.layerY; mouse.clientX = evnt.clientX; mouse.clientY = evnt.clientY; return mouse; }; control.getOriginDirectionIntersects = function(origin, direction) { this.raycaster.set(origin, direction); let intersects = this.raycaster.intersectObjects(this.scene.children, true); // painter may want to filter intersects if (isFunc(this.painter.filterIntersects)) intersects = this.painter.filterIntersects(intersects); return intersects; }; control.getMouseIntersects = function(mouse) { // domElement gives correct coordinate with canvas render, but isn't always right for webgl renderer if (!this.renderer) return []; const sz = (this.renderer instanceof SVGRenderer) ? this.renderer.domElement : this.renderer.getSize(new Vector2()), pnt = { x: mouse.x / sz.width * 2 - 1, y: -mouse.y / sz.height * 2 + 1 }; this.camera.updateMatrix(); this.camera.updateMatrixWorld(); this.raycaster.setFromCamera(pnt, this.camera); let intersects = this.raycaster.intersectObjects(this.scene.children, true); // painter may want to filter intersects if (isFunc(this.painter.filterIntersects)) intersects = this.painter.filterIntersects(intersects); return intersects; }; control.detectZoomMesh = function(evnt) { const mouse = this.getMousePos(evnt, {}), intersects = this.getMouseIntersects(mouse); if (intersects) { for (let n = 0; n < intersects.length; ++n) { if (intersects[n].object.zoom && !intersects[n].object.zoom_disabled) return intersects[n]; } } return null; }; control.getInfoAtMousePosition = function(mouse_pos) { const intersects = this.getMouseIntersects(mouse_pos); let tip = null, painter = null; for (let i = 0; i < intersects.length; ++i) { if (intersects[i].object.tooltip) { tip = intersects[i].object.tooltip(intersects[i]); painter = intersects[i].object.painter; break; } } if (tip && painter) { return { obj: painter.getObject(), name: painter.getObject().fName, bin: tip.bin, cont: tip.value, binx: tip.ix, biny: tip.iy, binz: tip.iz, grx: (tip.x1+tip.x2)/2, gry: (tip.y1+tip.y2)/2, grz: (tip.z1+tip.z2)/2 }; } }; control.processDblClick = function(evnt) { // first check if zoom mesh clicked const zoom_intersect = this.detectZoomMesh(evnt); if (zoom_intersect && this.painter) { this.painter.unzoom(zoom_intersect.object.use_y_for_z ? 'y' : zoom_intersect.object.zoom); return; } // then check if double-click handler assigned const fp = this.painter?.getFramePainter(); if (isFunc(fp?._dblclick_handler)) { const info = this.getInfoAtMousePosition(this.getMousePos(evnt, {})); if (info) { fp._dblclick_handler(info); return; } } this.reset(); }; control.changeEvent = function() { this.mouse_ctxt.on = false; // disable context menu if any changes where done by orbit control this.painter.render3D(0); this.control_changed = true; }; control.startEvent = function() { this.control_active = true; this.block_ctxt = false; this.mouse_ctxt.on = false; this.tooltip.hide(); // do not reset here, problem of events sequence in orbitcontrol // it issue change/start/stop event when do zooming // control.control_changed = false; }; control.endEvent = function() { this.control_active = false; if (this.mouse_ctxt.on) { this.mouse_ctxt.on = false; this.contextMenu(this.mouse_ctxt, this.getMouseIntersects(this.mouse_ctxt)); } /* else if (this.control_changed) { // react on camera change when required } */ this.control_changed = false; }; control.mainProcessContextMenu = function(evnt) { evnt.preventDefault(); this.getMousePos(evnt, this.mouse_ctxt); if (this.control_active) this.mouse_ctxt.on = true; else if (this.block_ctxt) this.block_ctxt = false; else this.contextMenu(this.mouse_ctxt, this.getMouseIntersects(this.mouse_ctxt)); }; control.contextMenu = function(/* pos, intersects */) { // do nothing, function called when context menu want to be activated }; control.setTooltipEnabled = function(on) { this.block_mousemove = !on; if (on === false) { this.tooltip.hide(); this.removeZoomMesh(); } }; control.removeZoomMesh = function() { if (this.mouse_zoom_mesh?.object.showSelection()) this.painter.render3D(); this.mouse_zoom_mesh = null; // in any case clear mesh, enable orbit control again }; control.mainProcessMouseMove = function(evnt) { if (!this.painter) return; // protect when cleanup if (this.control_active && evnt.buttons && (evnt.buttons & 2)) this.block_ctxt = true; // if right button in control was active, block next context menu if (this.control_active || this.block_mousemove || !isFunc(this.processMouseMove)) return; if (this.mouse_zoom_mesh) { // when working with zoom mesh, need special handling const zoom2 = this.detectZoomMesh(evnt), pnt2 = (zoom2?.object === this.mouse_zoom_mesh.object) ? zoom2.point : this.mouse_zoom_mesh.object.globalIntersect(this.raycaster); if (pnt2) this.mouse_zoom_mesh.point2 = pnt2; if (pnt2 && this.painter.enable_highlight) { if (this.mouse_zoom_mesh.object.showSelection(this.mouse_zoom_mesh.point, pnt2)) this.painter.render3D(0); } this.tooltip.hide(); return; } evnt.preventDefault(); // extract mouse position this.tmout_mouse = this.getMousePos(evnt, {}); this.tmout_ttpos = this.tooltip?.extract_pos(evnt); if (this.tmout_handle) { clearTimeout(this.tmout_handle); delete this.tmout_handle; } if (!this.mouse_tmout) this.delayedProcessMouseMove(); else this.tmout_handle = setTimeout(() => this.delayedProcessMouseMove(), this.mouse_tmout); }; control.delayedProcessMouseMove = function() { // remove handle - allow to trigger new timeout delete this.tmout_handle; if (!this.painter) return; // protect when cleanup const mouse = this.tmout_mouse, intersects = this.getMouseIntersects(mouse), tip = this.processMouseMove(intersects); if (tip) { let name = '', title = '', coord = '', info = ''; if (mouse) coord = mouse.x.toFixed(0) + ',' + mouse.y.toFixed(0); if (isStr(tip)) info = tip; else { name = tip.name; title = tip.title; if (tip.line) info = tip.line; else if (tip.lines) { info = tip.lines.slice(1).join(' '); name = tip.lines[0]; } } this.painter.showObjectStatus(name, title, info, coord); } this.cursor_changed = false; if (tip && this.painter?.isTooltipAllowed()) { this.tooltip.checkParent(this.painter.selectDom().node()); this.tooltip.show(tip, mouse); this.tooltip.pos(this.tmout_ttpos); } else { this.tooltip.hide(); if (intersects) { for (let n = 0; n < intersects.length; ++n) { if (intersects[n].object.zoom && !intersects[n].object.zoom_disabled) this.cursor_changed = true; } } } getDocument().body.style.cursor = this.cursor_changed ? 'pointer' : 'auto'; }; control.mainProcessMouseLeave = function() { if (!this.painter) return; // protect when cleanup // do not enter main event at all if (this.tmout_handle) { clearTimeout(this.tmout_handle); delete this.tmout_handle; } this.tooltip.hide(); if (isFunc(this.processMouseLeave)) this.processMouseLeave(); if (this.cursor_changed) { getDocument().body.style.cursor = 'auto'; this.cursor_changed = false; } }; control.mainProcessDblClick = function(evnt) { // suppress simple click handler if double click detected if (this.single_click_tm) { clearTimeout(this.single_click_tm); delete this.single_click_tm; } this.processDblClick(evnt); }; control.processClick = function(mouse_pos, kind) { delete this.single_click_tm; if (kind === 1) { const fp = this.painter?.getFramePainter(); if (isFunc(fp?._click_handler)) { const info = this.getInfoAtMousePosition(mouse_pos); if (info) { fp._click_handler(info); return; } } } // method assigned in the Eve7 and used for object selection if ((kind === 2) && isFunc(this.processSingleClick)) { const intersects = this.getMouseIntersects(mouse_pos); this.processSingleClick(intersects); } }; control.lstn_click = function(evnt) { // ignore right-mouse click if (evnt.detail === 2) return; if (this.single_click_tm) { clearTimeout(this.single_click_tm); delete this.single_click_tm; } let kind = 0; if (isFunc(this.painter?.getFramePainter()?._click_handler)) kind = 1; // user click handler else if (this.processSingleClick && this.painter?.options?.mouse_click) kind = 2; // eve7 click handler // if normal event, set longer timeout waiting if double click not detected if (kind) this.single_click_tm = setTimeout(this.processClick.bind(this, this.getMousePos(evnt, {}), kind), 300); }.bind(control); control.addEventListener('change', () => control.changeEvent()); control.addEventListener('start', () => control.startEvent()); control.addEventListener('end', () => control.endEvent()); control.lstn_contextmenu = evnt => control.mainProcessContextMenu(evnt); control.lstn_dblclick = evnt => control.mainProcessDblClick(evnt); control.lstn_mousemove = evnt => control.mainProcessMouseMove(evnt); control.lstn_mouseleave = () => control.mainProcessMouseLeave(); renderer.domElement.addEventListener('click', control.lstn_click); renderer.domElement.addEventListener('dblclick', control.lstn_dblclick); renderer.domElement.addEventListener('contextmenu', control.lstn_contextmenu); renderer.domElement.addEventListener('mousemove', control.lstn_mousemove); renderer.domElement.addEventListener('mouseleave', control.lstn_mouseleave); return control; } /** @summary Method cleanup three.js object as much as possible. * @desc Simplify JS engine to remove it from memory * @private */ function disposeThreejsObject(obj, only_childs) { if (!obj) return; if (obj.children) { for (let i = 0; i < obj.children.length; i++) disposeThreejsObject(obj.children[i]); } if (only_childs) { obj.children = []; return; } obj.children = undefined; if (obj.geometry) { obj.geometry.dispose(); obj.geometry = undefined; } if (obj.material) { if (obj.material.map) { obj.material.map.dispose(); obj.material.map = undefined; } obj.material.dispose(); obj.material = undefined; } // cleanup jsroot fields to simplify browser cleanup job delete obj.painter; delete obj.bins_index; delete obj.tooltip; delete obj.stack; // used in geom painter delete obj.drawn_highlight; // special highlight object obj = undefined; } /** @summary Create LineSegments mesh (or only geometry) * @desc If required, calculates lineDistance attribute for dashed geometries * @private */ function createLineSegments(arr, material, index = undefined, only_geometry = false) { const geom = new BufferGeometry(); geom.setAttribute('position', arr instanceof Float32Array ? new BufferAttribute(arr, 3) : new Float32BufferAttribute(arr, 3)); if (index) geom.setIndex(new BufferAttribute(index, 1)); if (material.isLineDashedMaterial) { const v1 = new Vector3(), v2 = new Vector3(); let d = 0, distances = null; if (index) { distances = new Float32Array(index.length); for (let n = 0; n < index.length; n += 2) { const i1 = index[n], i2 = index[n+1]; v1.set(arr[i1], arr[i1+1], arr[i1+2]); v2.set(arr[i2], arr[i2+1], arr[i2+2]); distances[n] = d; d += v2.distanceTo(v1); distances[n+1] = d; } } else { distances = new Float32Array(arr.length/3); for (let n = 0; n < arr.length; n += 6) { v1.set(arr[n], arr[n+1], arr[n+2]); v2.set(arr[n+3], arr[n+4], arr[n+5]); distances[n/3] = d; d += v2.distanceTo(v1); distances[n/3+1] = d; } } geom.setAttribute('lineDistance', new BufferAttribute(distances, 1)); } return only_geometry ? geom : new LineSegments(geom, material); } /** @summary Help structures for calculating Box mesh * @private */ const Box3D = { Vertices: [new Vector3(1, 1, 1), new Vector3(1, 1, 0), new Vector3(1, 0, 1), new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 1, 1), new Vector3(0, 0, 0), new Vector3(0, 0, 1)], Indexes: [0, 2, 1, 2, 3, 1, 4, 6, 5, 6, 7, 5, 4, 5, 1, 5, 0, 1, 7, 6, 2, 6, 3, 2, 5, 7, 0, 7, 2, 0, 1, 3, 4, 3, 6, 4], Normals: [1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1], Segments: [0, 2, 2, 7, 7, 5, 5, 0, 1, 3, 3, 6, 6, 4, 4, 1, 1, 0, 3, 2, 6, 7, 4, 5], // segments addresses Vertices MeshSegments: undefined }; // these segments address vertices from the mesh, we can use positions from box mesh Box3D.MeshSegments = (function() { const arr = new Int32Array(Box3D.Segments.length); for (let n = 0; n < arr.length; ++n) { for (let k = 0; k < Box3D.Indexes.length; ++k) { if (Box3D.Segments[n] === Box3D.Indexes[k]) { arr[n] = k; break; } } } return arr; })(); /** * @summary Abstract interactive control interface for 3D objects * * @abstract * @private */ class InteractiveControl { cleanup() {} extractIndex(/* intersect */) {} setSelected(/* col, indx */) {} setHighlight(/* col, indx */) {} checkHighlightIndex(/* indx */) {} } // class InteractiveControl /** * @summary Special class to control highliht and selection of single points, used in geo painter * * @private */ class PointsControl extends InteractiveControl { /** @summary constructor * @param {object} mesh - draw object */ constructor(mesh) { super(); this.mesh = mesh; } /** @summary cleanup object */ cleanup() { if (!this.mesh) return; delete this.mesh.is_selected; this.createSpecial(null); delete this.mesh; } /** @summary extract intersect index */ extractIndex(intersect) { return intersect && intersect.index!==undefined ? intersect.index : undefined; } /** @summary set selection */ setSelected(col, indx) { const m = this.mesh; if ((m.select_col === col) && (m.select_indx === indx)) { col = null; indx = undefined; } m.select_col = col; m.select_indx = indx; this.createSpecial(col, indx); return true; } /** @summary set highlight */ setHighlight(col, indx) { const m = this.mesh; m.h_index = indx; if (col) this.createSpecial(col, indx); else this.createSpecial(m.select_col, m.select_indx); return true; } /** @summary create special object */ createSpecial(color, index) { const m = this.mesh; if (!color) { if (m.js_special) { m.remove(m.js_special); disposeThreejsObject(m.js_special); delete m.js_special; } return; } if (!m.js_special) { const geom = new BufferGeometry(); geom.setAttribute('position', m.geometry.getAttribute('position')); const material = new PointsMaterial({ size: m.material.size*2, color }); material.sizeAttenuation = m.material.sizeAttenuation; m.js_special = new Points(geom, material); m.js_special.jsroot_special = true; // special object, exclude from intersections m.add(m.js_special); } m.js_special.material.color = new Color(color); if (index !== undefined) m.js_special.geometry.setDrawRange(index, 1); } } // class PointsControl /** * @summary Class for creation of 3D points * * @private */ class PointsCreator { /** @summary constructor * @param {number} number - number of points * @param {boolean} [iswebgl] - if WebGL is used * @param {number} [scale] - scale factor */ constructor(number, iswebgl = true, scale = 1) { this.webgl = iswebgl; this.scale = scale || 1; this.pos = new Float32Array(number*3); this.geom = new BufferGeometry