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();
this.geom.setAttribute('position', new BufferAttribute(this.pos, 3));
this.indx = 0;
}
/** @summary Add point */
addPoint(x, y, z) {
this.pos[this.indx] = x;
this.pos[this.indx+1] = y;
this.pos[this.indx+2] = z;
this.indx += 3;
}
/** @summary Create points */
createPoints(args) {
if (!isObject(args))
args = { color: args };
if (!args.color)
args.color = 'black';
let k = 1;
// special dots
if (!args.style) k = 1.1; else
if (args.style === 1) k = 0.3; else
if (args.style === 6) k = 0.5; else
if (args.style === 7) k = 0.7;
const makePoints = texture => {
const material_args = { size: 3*this.scale*k };
if (texture) {
material_args.map = texture;
material_args.transparent = true;
} else
material_args.color = args.color || 'black';
const pnts = new Points(this.geom, new PointsMaterial(material_args));
pnts.nvertex = 1;
return pnts;
};
// this is plain creation of points, no need for texture loading
if (k !== 1) {
const res = makePoints();
return this.noPromise ? res : Promise.resolve(res);
}
const handler = new TAttMarkerHandler({ style: args.style, color: args.color, size: 7 }),
w = handler.fill ? 1 : 7,
imgdata = '<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg">' +
`<path d="${handler.create(32, 32)}" style="stroke: ${handler.getStrokeColor()}; stroke-width: ${w}; fill: ${handler.getFillColor()}"></path>`+
'</svg>',
dataUrl = 'data:image/svg+xml;charset=utf8,' + (isNodeJs() ? imgdata : encodeURIComponent(imgdata));
let promise;
if (isNodeJs()) {
promise = import('canvas').then(handle => handle.default.loadImage(dataUrl).then(img => {
const canvas = handle.default.createCanvas(64, 64),
ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, 64, 64);
return new CanvasTexture(canvas);
}));
} else if (this.noPromise) {
// only for v6 support
return makePoints(new TextureLoader().load(dataUrl));
} else {
promise = new Promise((resolveFunc, rejectFunc) => {
const loader = new TextureLoader();
// eslint-disable-next-line prefer-promise-reject-errors
loader.load(dataUrl, res => resolveFunc(res), undefined, () => rejectFunc());
});
}
return promise.then(makePoints);
}
} // class PointsCreator
/** @summary Create material for 3D line
* @desc Takes into account dashed properties
* @private */
function create3DLineMaterial(painter, arg, is_v7 = false) {
if (!painter || !arg) return null;
let color, lstyle, lwidth;
if (isStr(arg) || is_v7) {
color = painter.v7EvalColor(arg+'color', 'black');
lstyle = parseInt(painter.v7EvalAttr(arg+'style', 0));
lwidth = parseInt(painter.v7EvalAttr(arg+'width', 1));
} else {
color = painter.getColor(arg.fLineColor);
lstyle = arg.fLineStyle;
lwidth = arg.fLineWidth;
}
const style = lstyle ? getSvgLineStyle(lstyle) : '',
dash = style ? style.split(',') : [],
material = (dash && dash.length >= 2)
? new LineDashedMaterial({ color, dashSize: parseInt(dash[0]), gapSize: parseInt(dash[1]) })
: new LineBasicMaterial({ color });
if (lwidth && (lwidth > 1)) material.linewidth = lwidth;
return material;
}
export { assign3DHandler, disposeThreejsObject, createOrbitControl,
createLineSegments, create3DLineMaterial, Box3D, getMaterialArgs,
createRender3D, beforeRender3D, afterRender3D, getRender3DKind, cleanupRender3D,
HelveticerRegularFont, InteractiveControl, PointsControl, PointsCreator, createSVGRenderer };