jsroot
Version:
JavaScript ROOT
1,427 lines (1,221 loc) • 238 kB
JavaScript
import { httpRequest, browser, source_dir, settings, internals, constants, create, clone,
findFunction, isBatchMode, isNodeJs, getDocument, isObject, isFunc, isStr, postponePromise, getPromise,
getKindForType, clTTree, clTNamed, clTList, clTAxis, clTObjArray, clTPolyMarker3D, clTPolyLine3D,
clTGeoVolume, clTGeoNode, clTGeoNodeMatrix, nsREX, nsSVG, kInspect } from '../core.mjs';
import { showProgress, injectStyle, ToolbarIcons } from '../gui/utils.mjs';
import { GUI } from '../gui/lil-gui.mjs';
import { THREE, assign3DHandler, disposeThreejsObject, createOrbitControl,
createLineSegments, InteractiveControl, PointsCreator, importThreeJs,
createRender3D, beforeRender3D, afterRender3D, getRender3DKind, cleanupRender3D,
createTextGeometry } from '../base/base3d.mjs';
import { getColor, getRootColors } from '../base/colors.mjs';
import { DrawOptions } from '../base/BasePainter.mjs';
import { ObjectPainter } from '../base/ObjectPainter.mjs';
import { createMenu, closeMenu } from '../gui/menu.mjs';
import { TAxisPainter } from '../gpad/TAxisPainter.mjs';
import { ensureTCanvas } from '../gpad/TCanvasPainter.mjs';
import { kindGeo, kindEve, kGetMesh, kDeleteMesh,
clTGeoBBox, clTGeoCompositeShape,
geoCfg, geoBITS, ClonedNodes, testGeoBit, setGeoBit, toggleGeoBit, setInvisibleAll,
countNumShapes, getNodeKind, produceRenderOrder, createServerGeometry,
projectGeometry, numGeometryFaces, createMaterial, createFrustum, createProjectionMatrix,
getBoundingBox, provideObjectInfo, isSameStack, checkDuplicates, getObjectName, cleanupShape, getShapeIcon } from './geobase.mjs';
const _ENTIRE_SCENE = 0, _BLOOM_SCENE = 1,
clTGeoManager = 'TGeoManager', clTEveGeoShapeExtract = 'TEveGeoShapeExtract',
clTGeoOverlap = 'TGeoOverlap', clTGeoVolumeAssembly = 'TGeoVolumeAssembly',
clTEveTrack = 'TEveTrack', clTEvePointSet = 'TEvePointSet',
clREveGeoShapeExtract = `${nsREX}REveGeoShapeExtract`;
/** @summary Function used to build hierarchy of elements of overlap object
* @private */
function buildOverlapVolume(overlap) {
const vol = create(clTGeoVolume);
setGeoBit(vol, geoBITS.kVisDaughters, true);
vol.$geoh = true; // workaround, let know browser that we are in volumes hierarchy
vol.fName = '';
const node1 = create(clTGeoNodeMatrix);
node1.fName = overlap.fVolume1.fName || 'Overlap1';
node1.fMatrix = overlap.fMatrix1;
node1.fVolume = overlap.fVolume1;
// node1.fVolume.fLineColor = 2; // color assigned with ctrl.split_colors
const node2 = create(clTGeoNodeMatrix);
node2.fName = overlap.fVolume2.fName || 'Overlap2';
node2.fMatrix = overlap.fMatrix2;
node2.fVolume = overlap.fVolume2;
// node2.fVolume.fLineColor = 3; // color assigned with ctrl.split_colors
vol.fNodes = create(clTList);
vol.fNodes.Add(node1);
vol.fNodes.Add(node2);
return vol;
}
let $comp_col_cnt = 0;
/** @summary Function used to build hierarchy of elements of composite shapes
* @private */
function buildCompositeVolume(comp, maxlvl, side) {
if (maxlvl === undefined)
maxlvl = 1;
if (!side) {
$comp_col_cnt = 0;
side = '';
}
const vol = create(clTGeoVolume);
setGeoBit(vol, geoBITS.kVisThis, true);
setGeoBit(vol, geoBITS.kVisDaughters, true);
if ((side && (comp._typename !== clTGeoCompositeShape)) || (maxlvl <= 0)) {
vol.fName = side;
vol.fLineColor = ($comp_col_cnt++ % 8) + 2;
vol.fShape = comp;
return vol;
}
if (side)
side += '/';
vol.$geoh = true; // workaround, let know browser that we are in volumes hierarchy
vol.fName = '';
const node1 = create(clTGeoNodeMatrix);
setGeoBit(node1, geoBITS.kVisThis, true);
setGeoBit(node1, geoBITS.kVisDaughters, true);
node1.fName = 'Left';
node1.fMatrix = comp.fNode.fLeftMat;
node1.fVolume = buildCompositeVolume(comp.fNode.fLeft, maxlvl - 1, side + 'Left');
const node2 = create(clTGeoNodeMatrix);
setGeoBit(node2, geoBITS.kVisThis, true);
setGeoBit(node2, geoBITS.kVisDaughters, true);
node2.fName = 'Right';
node2.fMatrix = comp.fNode.fRightMat;
node2.fVolume = buildCompositeVolume(comp.fNode.fRight, maxlvl - 1, side + 'Right');
vol.fNodes = create(clTList);
vol.fNodes.Add(node1);
vol.fNodes.Add(node2);
if (!side) $comp_col_cnt = 0;
return vol;
}
/** @summary Provides 3D rendering configuration from histogram painter
* @return {Object} with scene, renderer and other attributes
* @private */
function getHistPainter3DCfg(painter) {
const main = painter?.getFramePainter();
if (painter?.mode3d && isFunc(main?.create3DScene) && main?.renderer) {
let scale_x = 1, scale_y = 1, scale_z = 1,
offset_x = 0, offset_y = 0, offset_z = 0;
if (main.scale_xmax > main.scale_xmin) {
scale_x = 2 * main.size_x3d / (main.scale_xmax - main.scale_xmin);
offset_x = (main.scale_xmax + main.scale_xmin) / 2 * scale_x;
}
if (main.scale_ymax > main.scale_ymin) {
scale_y = 2 * main.size_y3d / (main.scale_ymax - main.scale_ymin);
offset_y = (main.scale_ymax + main.scale_ymin) / 2 * scale_y;
}
if (main.scale_zmax > main.scale_zmin) {
scale_z = 2 * main.size_z3d / (main.scale_zmax - main.scale_zmin);
offset_z = (main.scale_zmax + main.scale_zmin) / 2 * scale_z - main.size_z3d;
}
return {
webgl: main.webgl,
scene: main.scene,
scene_width: main.scene_width,
scene_height: main.scene_height,
toplevel: main.toplevel,
renderer: main.renderer,
camera: main.camera,
scale_x, scale_y, scale_z,
offset_x, offset_y, offset_z
};
}
}
/** @summary find item with geometry painter
* @private */
function findItemWithGeoPainter(hitem, test_changes) {
while (hitem) {
if (isFunc(hitem._painter?.testGeomChanges)) {
if (test_changes)
hitem._painter.testGeomChanges();
return hitem;
}
hitem = hitem._parent;
}
return null;
}
/** @summary provide css style for geo object
* @private */
function provideVisStyle(obj) {
if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract))
return obj.fRnrSelf ? ' geovis_this' : '';
const vis = !testGeoBit(obj, geoBITS.kVisNone) && testGeoBit(obj, geoBITS.kVisThis);
let chld = testGeoBit(obj, geoBITS.kVisDaughters);
if (chld && !obj.fNodes?.arr?.length)
chld = false;
if (vis && chld)
return ' geovis_all';
if (vis)
return ' geovis_this';
if (chld)
return ' geovis_daughters';
return '';
}
/** @summary update icons
* @private */
function updateBrowserIcons(obj, hpainter) {
if (!obj || !hpainter)
return;
hpainter.forEachItem(m => {
// update all items with that volume
if ((obj === m._volume) || (obj === m._geoobj)) {
m._icon = m._icon.split(' ')[0] + provideVisStyle(obj);
hpainter.updateTreeNode(m);
}
});
}
/** @summary Return stack for the item from list of intersection
* @private */
function getIntersectStack(item) {
const obj = item?.object;
if (!obj)
return null;
if (obj.stack)
return obj.stack;
if (obj.stacks && item.instanceId !== undefined && item.instanceId < obj.stacks.length)
return obj.stacks[item.instanceId];
}
/**
* @summary Toolbar for geometry painter
*
* @private
*/
class Toolbar {
/** @summary constructor */
constructor(container, bright, buttons) {
this.bright = bright;
this.buttons = buttons;
this.element = container.append('div').attr('style', 'float: left; box-sizing: border-box; position: relative; bottom: 23px; vertical-align: middle; padding-left: 5px');
}
/** @summary add buttons */
createButtons() {
const buttonsNames = [];
this.buttons.forEach(buttonConfig => {
const buttonName = buttonConfig.name;
if (!buttonName)
throw new Error('must provide button name in button config');
if (buttonsNames.indexOf(buttonName) !== -1)
throw new Error(`button name ${buttonName} is taken`);
buttonsNames.push(buttonName);
const title = buttonConfig.title || buttonConfig.name;
if (!isFunc(buttonConfig.click))
throw new Error('must provide button click() function in button config');
ToolbarIcons.createSVG(this.element, ToolbarIcons[buttonConfig.icon], 16, title, this.bright)
.on('click', buttonConfig.click)
.style('position', 'relative')
.style('padding', '3px 1px');
});
}
/** @summary change brightness */
changeBrightness(bright) {
if (this.bright !== bright) {
this.element.selectAll('*').remove();
this.bright = bright;
this.createButtons();
}
}
/** @summary cleanup toolbar */
cleanup() {
this.element?.remove();
delete this.element;
}
} // class ToolBar
/**
* @summary geometry drawing control
*
* @private
*/
class GeoDrawingControl extends InteractiveControl {
constructor(mesh, bloom) {
super();
this.mesh = mesh?.material ? mesh : null;
this.bloom = bloom;
}
/** @summary set highlight */
setHighlight(col, indx) {
return this.drawSpecial(col, indx);
}
/** @summary draw special */
drawSpecial(col, indx) {
const c = this.mesh;
if (!c?.material)
return;
if (c.isInstancedMesh) {
if (c._highlight_mesh) {
c.remove(c._highlight_mesh);
delete c._highlight_mesh;
}
if (col && indx !== undefined) {
const h = new THREE.Mesh(c.geometry, c.material.clone());
if (this.bloom) {
h.layers.enable(_BLOOM_SCENE);
h.material.emissive = new THREE.Color(0x00ff00);
} else {
h.material.color = new THREE.Color(col);
h.material.opacity = 1.0;
}
const m = new THREE.Matrix4();
c.getMatrixAt(indx, m);
h.applyMatrix4(m);
c.add(h);
h.jsroot_special = true; // exclude from intersections
c._highlight_mesh = h;
}
return true;
}
if (col) {
if (!c.origin) {
c.origin = {
color: c.material.color,
emissive: c.material.emissive,
opacity: c.material.opacity,
width: c.material.linewidth,
size: c.material.size
};
}
if (this.bloom) {
c.layers.enable(_BLOOM_SCENE);
c.material.emissive = new THREE.Color(0x00ff00);
} else {
c.material.color = new THREE.Color(col);
c.material.opacity = 1.0;
}
if (c.hightlightWidthScale && !browser.isWin)
c.material.linewidth = c.origin.width * c.hightlightWidthScale;
if (c.highlightScale)
c.material.size = c.origin.size * c.highlightScale;
return true;
} else if (c.origin) {
if (this.bloom) {
c.material.emissive = c.origin.emissive;
c.layers.enable(_ENTIRE_SCENE);
} else {
c.material.color = c.origin.color;
c.material.opacity = c.origin.opacity;
}
if (c.hightlightWidthScale)
c.material.linewidth = c.origin.width;
if (c.highlightScale)
c.material.size = c.origin.size;
return true;
}
}
} // class GeoDrawingControl
const stageInit = 0, stageCollect = 1, stageWorkerCollect = 2, stageAnalyze = 3, stageCollShapes = 4,
stageStartBuild = 5, stageWorkerBuild = 6, stageBuild = 7, stageBuildReady = 8, stageWaitMain = 9, stageBuildProj = 10;
/**
* @summary Painter class for geometries drawing
*
* @private
*/
class TGeoPainter extends ObjectPainter {
#geo_manager; // TGeoManager instance - if assigned in drawing
#superimpose; // set when superimposed with other 3D drawings
#complete_handler; // callback, assigned by ROOT GeomViewer
#geom_viewer; // true when processing drawing from ROOT GeomViewer
#extra_objects; // TList with extra objects configured for drawing
#webgl; // true when normal WebGL drawing is enabled
#worker; // extra Worker to run different calculations
#worker_ready; // is worker started and initialized
#worker_jobs; // number of submitted to worker jobs
#clones; // instance of ClonedNodes
#clones_owner; // is instance managed by the painter
#draw_nodes; // drawn nodes from geometry
#build_shapes; // build shapes required for drawings
#current_face_limit; // current face limit
#draw_all_nodes; // flag using in drawing
#draw_nodes_again; // flag set if drawing should be started again when completed
#drawing_ready; // if drawing completed
#drawing_log; // current drawing log information
#draw_resolveFuncs; // resolve call-backs for start drawing calls
#start_drawing_time; // time when drawing started
#start_render_tm; // time when rendering started
#first_render_tm; // first time when rendering was performed
#last_render_tm; // last time when rendering was performed
#last_render_meshes; // last value of rendered meshes
#render_resolveFuncs; // resolve call-backs from render calls
#render_tmout; // timeout used in Render3D()
#first_drawing; // true when very first drawing is performed
#full_redrawing; // if full redraw must be performed
#last_clip_cfg; // last config of clip panels
#redraw_timer; // timer used in redraw
#did_update; // flag used in update
#custom_bounding_box; // custom bounding box for extra objects
#clip_planes; // clip planes
#central_painter; // pointer on central painter
#subordinate_painters; // list of subordinate painters
#effectComposer; // extra composer for special effects, used in EVE
#bloomComposer; // extra composer for bloom highlight
#new_draw_nodes; // temporary list of new draw nodes
#new_append_nodes; // temporary list of new append nodes
#more_nodes; // more nodes from ROOT geometry viewer
#provided_more_nodes; // staged more nodes from ROOT geometry viewer
#on_pad; // true when drawing done on TPad
#last_manifest; // last node which was "manifest" via menu
#last_hidden; // last node which was "hidden" via menu
#gui; // dat.GUI instance
#toolbar; // tool buttons
#controls; // orbit control
#highlight_handlers; // highlight handlers
#animating; // set when animation started
#last_camera_position; // last camera position
#fullgeom_proj; // full geometry to produce projection
#selected_mesh; // used in highlight
#fog; // fog for the scene
#lookat; // position to which camera looks at
#scene_size; // stored scene size to control changes
#scene_width; // actual scene width
#scene_height; // actual scene width
#fit_main_area; // true if drawing must fit to DOM
#overall_size; // overall size
#scene; // three.js Scene object
#camera; // three.js camera object
#renderer; // three.js renderer object
#toplevel; // three.js top level Object3D
#camera0pos; // camera on opposite side
#vrControllers; // used in VR display
#controllersMeshes; // used in VR display
#dolly; // used in VR display
#vrDisplay; // used in VR display
#vr_camera_position; // used in VR display
#vr_camera_rotation; // used in VR display
#vr_camera_near; // used in VR display
#standingMatrix; // used in VR display
#raycasterEnd; // used in VR display
#raycasterOrigin; // used in VR display
#adjust_camera_with_render; // flag to readjust camera when rendering
#did_cleanup; // flag set after cleanup
/** @summary Constructor
* @param {object|string} dom - DOM element for drawing or element id
* @param {object} obj - supported TGeo object */
constructor(dom, obj) {
let gm;
if (obj?._typename === clTGeoManager) {
gm = obj;
obj = obj.fMasterVolume;
}
if (obj?._typename && (obj._typename.indexOf(clTGeoVolume) === 0))
obj = { _typename: clTGeoNode, fVolume: obj, fName: obj.fName, $geoh: obj.$geoh, _proxy: true };
super(dom, obj);
this.#superimpose = Boolean(getHistPainter3DCfg(this.getMainPainter()));
this.#geo_manager = gm;
this._no_default_title = true; // do not set title to main DIV
this.mode3d = true; // indication of 3D mode
this.drawing_stage = stageInit; //
this.#drawing_log = 'Init';
this.ctrl = {
clipIntersect: true,
clipVisualize: false,
clip: [{ name: 'x', enabled: false, value: 0, min: -100, max: 100, step: 1 },
{ name: 'y', enabled: false, value: 0, min: -100, max: 100, step: 1 },
{ name: 'z', enabled: false, value: 0, min: -100, max: 100, step: 1 }],
_highlight: 0,
highlight: 0,
highlight_bloom: 0,
highlight_scene: 0,
highlight_color: '#00ff00',
bloom_strength: 1.5,
more: 1,
maxfaces: 0,
vislevel: undefined,
maxnodes: undefined,
dflt_colors: false,
info: { num_meshes: 0, num_faces: 0, num_shapes: 0 },
depthTest: true,
depthMethod: 'dflt',
select_in_view: false,
update_browser: true,
use_fog: false,
light: { kind: 'points', top: false, bottom: false, left: false, right: false, front: false, specular: true, power: 1 },
lightKindItems: [
{ name: 'AmbientLight', value: 'ambient' },
{ name: 'DirectionalLight', value: 'points' },
{ name: 'HemisphereLight', value: 'hemisphere' },
{ name: 'Ambient + Point', value: 'mix' }
],
trans_radial: 0,
trans_z: 0,
scale: new THREE.Vector3(1, 1, 1),
zoom: 1.0, rotatey: 0, rotatez: 0,
depthMethodItems: [
{ name: 'Default', value: 'dflt' },
{ name: 'Raytraicing', value: 'ray' },
{ name: 'Boundary box', value: 'box' },
{ name: 'Mesh size', value: 'size' },
{ name: 'Central point', value: 'pnt' }
],
cameraKindItems: [
{ name: 'Perspective', value: 'perspective' },
{ name: 'Perspective (Floor XOZ)', value: 'perspXOZ' },
{ name: 'Perspective (Floor YOZ)', value: 'perspYOZ' },
{ name: 'Perspective (Floor XOY)', value: 'perspXOY' },
{ name: 'Orthographic (XOY)', value: 'orthoXOY' },
{ name: 'Orthographic (XOZ)', value: 'orthoXOZ' },
{ name: 'Orthographic (ZOY)', value: 'orthoZOY' },
{ name: 'Orthographic (ZOX)', value: 'orthoZOX' },
{ name: 'Orthographic (XnOY)', value: 'orthoXNOY' },
{ name: 'Orthographic (XnOZ)', value: 'orthoXNOZ' },
{ name: 'Orthographic (ZnOY)', value: 'orthoZNOY' },
{ name: 'Orthographic (ZnOX)', value: 'orthoZNOX' }
],
cameraOverlayItems: [
{ name: 'None', value: 'none' },
{ name: 'Bar', value: 'bar' },
{ name: 'Axis', value: 'axis' },
{ name: 'Grid', value: 'grid' },
{ name: 'Grid background', value: 'gridb' },
{ name: 'Grid foreground', value: 'gridf' }
],
camera_kind: 'perspective',
camera_overlay: 'gridb',
rotate: false,
background: settings.DarkMode ? '#000000' : '#ffffff',
can_rotate: true,
_axis: 0,
instancing: 0,
_count: false,
// material properties
wireframe: false,
transparency: 0,
flatShading: false,
roughness: 0.5,
metalness: 0.5,
shininess: 0,
reflectivity: 0.5,
material_kind: 'lambert',
materialKinds: [
{ name: 'MeshLambertMaterial', value: 'lambert', emissive: true, props: [{ name: 'flatShading' }] },
{ name: 'MeshBasicMaterial', value: 'basic' },
{ name: 'MeshStandardMaterial', value: 'standard', emissive: true,
props: [{ name: 'flatShading' }, { name: 'roughness', min: 0, max: 1, step: 0.001 }, { name: 'metalness', min: 0, max: 1, step: 0.001 }] },
{ name: 'MeshPhysicalMaterial', value: 'physical', emissive: true,
props: [{ name: 'flatShading' }, { name: 'roughness', min: 0, max: 1, step: 0.001 }, { name: 'metalness', min: 0, max: 1, step: 0.001 }, { name: 'reflectivity', min: 0, max: 1, step: 0.001 }] },
{ name: 'MeshPhongMaterial', value: 'phong', emissive: true,
props: [{ name: 'flatShading' }, { name: 'shininess', min: 0, max: 100, step: 0.1 }] },
{ name: 'MeshNormalMaterial', value: 'normal', props: [{ name: 'flatShading' }] },
{ name: 'MeshDepthMaterial', value: 'depth' },
{ name: 'MeshMatcapMaterial', value: 'matcap' },
{ name: 'MeshToonMaterial', value: 'toon' }
],
getMaterialCfg() {
let cfg;
this.materialKinds.forEach(item => {
if (item.value === this.material_kind)
cfg = item;
});
return cfg;
}
};
this.#draw_resolveFuncs = [];
this.#render_resolveFuncs = [];
this.cleanup(true);
}
/** @summary Returns scene */
getScene() { return this.#scene; }
/** @summary Returns camera */
getCamera() { return this.#camera; }
/** @summary Returns renderer */
getRenderer() { return this.#renderer; }
/** @summary Returns top Object3D instance */
getTopObject3D() { return this.#toplevel; }
/** @summary Assign geometry viewer mode
* @private */
setGeomViewer(on) { this.#geom_viewer = on; }
/** @summary Assign or remove subordinate painter */
assignSubordinate(painter, do_assign = true) {
if (this.#subordinate_painters === undefined)
this.#subordinate_painters = [];
const pos = this.#subordinate_painters.indexOf(painter);
if (do_assign && pos < 0)
this.#subordinate_painters.push(painter);
else if (!do_assign && pos >= 0)
this.#subordinate_painters.splice(pos, 1);
}
/** @summary Return list of subordinate painters */
getSubordinates() { return this.#subordinate_painters ?? []; }
/** @summary Assign or cleanup central painter */
assignCentral(painter, do_assign = true) {
if (do_assign)
this.#central_painter = painter;
else if (this.#central_painter === painter)
this.#central_painter = undefined;
}
/** @summary Returns central painter */
getCentral() { return this.#central_painter; }
/** @summary Returns extra objects configured for drawing */
getExtraObjects() { return this.#extra_objects; }
/** @summary Function called by framework when dark mode is changed
* @private */
changeDarkMode(mode) {
if ((this.ctrl.background === '#000000') || (this.ctrl.background === '#ffffff'))
this.changedBackground((mode ?? settings.DarkMode) ? '#000000' : '#ffffff');
}
/** @summary Change drawing stage
* @private */
changeStage(value, msg) {
this.drawing_stage = value;
if (!msg) {
switch (value) {
case stageInit: msg = 'Building done'; break;
case stageCollect: msg = 'collect visibles'; break;
case stageWorkerCollect: msg = 'worker collect visibles'; break;
case stageAnalyze: msg = 'Analyse visibles'; break;
case stageCollShapes: msg = 'collect shapes for building'; break;
case stageStartBuild: msg = 'Start build shapes'; break;
case stageWorkerBuild: msg = 'Worker build shapes'; break;
case stageBuild: msg = 'Build shapes'; break;
case stageBuildReady: msg = 'Build ready'; break;
case stageWaitMain: msg = 'Wait for main painter'; break;
case stageBuildProj: msg = 'Build projection'; break;
default: msg = `stage ${value}`;
}
}
this.#drawing_log = msg;
}
/** @summary Check drawing stage */
isStage(value) { return value === this.drawing_stage; }
isBatchMode() { return isBatchMode() || this.batch_mode; }
getControls() { return this.#controls; }
/** @summary Create toolbar */
createToolbar() {
if (this.#toolbar || !this.#webgl || this.ctrl.notoolbar || this.isBatchMode())
return;
const buttonList = [{
name: 'toImage',
title: 'Save as PNG',
icon: 'camera',
click: () => this.createSnapshot()
}, {
name: 'control',
title: 'Toggle control UI',
icon: 'rect',
click: () => this.showControlGui('toggle')
}, {
name: 'enlarge',
title: 'Enlarge geometry drawing',
icon: 'circle',
click: () => this.toggleEnlarge()
}];
// Only show VR icon if WebVR API available.
if (navigator.getVRDisplays) {
buttonList.push({
name: 'entervr',
title: 'Enter VR (It requires a VR Headset connected)',
icon: 'vrgoggles',
click: () => this.toggleVRMode()
});
this.initVRMode();
}
if (settings.ContextMenu) {
buttonList.push({
name: 'menu',
title: 'Show context menu',
icon: 'question',
click: evnt => {
evnt.preventDefault();
evnt.stopPropagation();
if (closeMenu())
return;
createMenu(evnt, this).then(menu => {
menu.painter.fillContextMenu(menu);
menu.show();
});
}
});
}
const bkgr = new THREE.Color(this.ctrl.background);
this.#toolbar = new Toolbar(this.selectDom(), (bkgr.r + bkgr.g + bkgr.b) < 1, buttonList);
this.#toolbar.createButtons();
}
/** @summary Initialize VR mode */
initVRMode() {
// Dolly contains camera and controllers in VR Mode
// Allows moving the user in the scene
this.#dolly = new THREE.Group();
this.#scene.add(this.#dolly);
this.#standingMatrix = new THREE.Matrix4();
// Raycaster temp variables to avoid one per frame allocation.
this.#raycasterEnd = new THREE.Vector3();
this.#raycasterOrigin = new THREE.Vector3();
navigator.getVRDisplays().then(displays => {
const vrDisplay = displays[0];
if (!vrDisplay)
return;
this.#renderer.vr.setDevice(vrDisplay);
this.#vrDisplay = vrDisplay;
if (vrDisplay.stageParameters)
this.#standingMatrix.fromArray(vrDisplay.stageParameters.sittingToStandingTransform);
this.initVRControllersGeometry();
});
}
/** @summary Init VR controllers geometry
* @private */
initVRControllersGeometry() {
const geometry = new THREE.SphereGeometry(0.025, 18, 36),
material = new THREE.MeshBasicMaterial({ color: 'grey', vertexColors: false }),
rayMaterial = new THREE.MeshBasicMaterial({ color: 'fuchsia', vertexColors: false }),
rayGeometry = new THREE.BoxGeometry(0.001, 0.001, 2),
ray1Mesh = new THREE.Mesh(rayGeometry, rayMaterial),
ray2Mesh = new THREE.Mesh(rayGeometry, rayMaterial),
sphere1 = new THREE.Mesh(geometry, material),
sphere2 = new THREE.Mesh(geometry, material);
this.#controllersMeshes = [];
this.#controllersMeshes.push(sphere1);
this.#controllersMeshes.push(sphere2);
ray1Mesh.position.z -= 1;
ray2Mesh.position.z -= 1;
sphere1.add(ray1Mesh);
sphere2.add(ray2Mesh);
this.#dolly.add(sphere1);
this.#dolly.add(sphere2);
// Controller mesh hidden by default
sphere1.visible = false;
sphere2.visible = false;
}
/** @summary Update VR controllers list
* @private */
updateVRControllersList() {
const gamepads = navigator.getGamepads && navigator.getGamepads();
// Has controller list changed?
if (this.#vrControllers && (gamepads.length === this.#vrControllers.length))
return;
// Hide meshes.
this.#controllersMeshes.forEach(mesh => { mesh.visible = false; });
this.#vrControllers = [];
for (let i = 0; i < gamepads.length; ++i) {
if (!gamepads[i] || !gamepads[i].pose)
continue;
this.#vrControllers.push({
gamepad: gamepads[i],
mesh: this.#controllersMeshes[i]
});
this.#controllersMeshes[i].visible = true;
}
}
/** @summary Process VR controller intersection
* @private */
processVRControllerIntersections() {
let intersects = [];
for (let i = 0; i < this.#vrControllers.length; ++i) {
const controller = this.#vrControllers[i].mesh,
end = controller.localToWorld(this.#raycasterEnd.set(0, 0, -1)),
origin = controller.localToWorld(this.#raycasterOrigin.set(0, 0, 0));
end.sub(origin).normalize();
intersects = intersects.concat(this.#controls.getOriginDirectionIntersects(origin, end));
}
// Remove duplicates.
intersects = intersects.filter((item, pos) => { return intersects.indexOf(item) === pos; });
this.#controls.processMouseMove(intersects);
}
/** @summary Update VR controllers
* @private */
updateVRControllers() {
this.updateVRControllersList();
// Update pose.
for (let i = 0; i < this.#vrControllers.length; ++i) {
const controller = this.#vrControllers[i],
orientation = controller.gamepad.pose.orientation,
position = controller.gamepad.pose.position,
controllerMesh = controller.mesh;
if (orientation)
controllerMesh.quaternion.fromArray(orientation);
if (position)
controllerMesh.position.fromArray(position);
controllerMesh.updateMatrix();
controllerMesh.applyMatrix4(this.#standingMatrix);
controllerMesh.matrixWorldNeedsUpdate = true;
}
this.processVRControllerIntersections();
}
/** @summary Toggle VR mode
* @private */
toggleVRMode() {
if (!this.#vrDisplay)
return;
// Toggle VR mode off
if (this.#vrDisplay.isPresenting) {
this.exitVRMode();
return;
}
this.#vr_camera_position = this.#camera.position.clone();
this.#vr_camera_rotation = this.#camera.rotation.clone();
this.#vrDisplay.requestPresent([{ source: this.#renderer.domElement }]).then(() => {
this.#vr_camera_near = this.#camera.near;
this.#dolly.position.set(this.#camera.position.x / 4, -this.#camera.position.y / 8, -this.#camera.position.z / 4);
this.#camera.position.set(0, 0, 0);
this.#dolly.add(this.#camera);
this.#camera.near = 0.1;
this.#camera.updateProjectionMatrix();
this.#renderer.vr.enabled = true;
this.#renderer.setAnimationLoop(() => {
this.updateVRControllers();
this.render3D(0);
});
});
this.#renderer.vr.enabled = true;
window.addEventListener('keydown', evnt => {
// Esc Key turns VR mode off
if (evnt.code === 'Escape')
this.exitVRMode();
});
}
/** @summary Exit VR mode
* @private */
exitVRMode() {
if (!this.#vrDisplay.isPresenting)
return;
this.#renderer.vr.enabled = false;
this.#dolly.remove(this.#camera);
this.#scene.add(this.#camera);
// Restore Camera pose
this.#camera.position.copy(this.#vr_camera_position);
this.#vr_camera_position = undefined;
this.#camera.rotation.copy(this.#vr_camera_rotation);
this.#vr_camera_rotation = undefined;
this.#camera.near = this.#vr_camera_near;
this.#camera.updateProjectionMatrix();
this.#vrDisplay.exitPresent();
}
/** @summary Returns main geometry object */
getGeometry() { return this.getObject(); }
/** @summary Modify visibility of provided node by name */
modifyVisisbility(name, sign) {
if (getNodeKind(this.getGeometry()) !== kindGeo)
return;
if (!name)
return setGeoBit(this.getGeometry().fVolume, geoBITS.kVisThis, (sign === '+'));
let regexp, exact = false;
// arg.node.fVolume
if (name.indexOf('*') < 0) {
regexp = new RegExp('^' + name + '$');
exact = true;
} else {
regexp = new RegExp('^' + name.split('*').join('.*') + '$');
exact = false;
}
this.findNodeWithVolume(regexp, arg => {
setInvisibleAll(arg.node.fVolume, (sign !== '+'));
return exact ? arg : null; // continue search if not exact expression provided
});
}
/** @summary Decode drawing options */
decodeOptions(opt) {
if (!isStr(opt))
opt = '';
if (this.#superimpose && (opt.indexOf('same') === 0))
opt = opt.slice(4);
const res = this.ctrl, macro = opt.indexOf('macro:');
if (macro >= 0) {
let separ = opt.indexOf(';', macro + 6);
if (separ < 0)
separ = opt.length;
res.script_name = opt.slice(macro + 6, separ);
opt = opt.slice(0, macro) + opt.slice(separ + 1);
console.log(`script ${res.script_name} rest ${opt}`);
}
while (true) {
const pp = opt.indexOf('+'), pm = opt.indexOf('-');
if ((pp < 0) && (pm < 0))
break;
let p1 = pp, sign = '+';
if ((p1 < 0) || ((pm >= 0) && (pm < pp))) {
p1 = pm;
sign = '-';
}
let p2 = p1 + 1;
const regexp = /[,; .]/;
while ((p2 < opt.length) && !regexp.test(opt[p2]) && (opt[p2] !== '+') && (opt[p2] !== '-'))
p2++;
const name = opt.substring(p1 + 1, p2);
opt = opt.slice(0, p1) + opt.slice(p2);
this.modifyVisisbility(name, sign);
}
const d = new DrawOptions(opt);
if (d.check('MAIN'))
res.is_main = true;
if (d.check('DUMMY'))
res.dummy = true;
if (d.check('TRACKS'))
res.tracks = true; // only for TGeoManager
if (d.check('SHOWTOP'))
res.showtop = true; // only for TGeoManager
if (d.check('NO_SCREEN'))
res.no_screen = true; // ignore kVisOnScreen bits for visibility
if (d.check('NOINSTANCING'))
res.instancing = -1; // disable usage of InstancedMesh
if (d.check('INSTANCING'))
res.instancing = 1; // force usage of InstancedMesh
if (d.check('ORTHO_CAMERA')) {
res.camera_kind = 'orthoXOY';
res.can_rotate = 0;
}
if (d.check('ORTHO', true)) {
res.camera_kind = 'ortho' + d.part;
res.can_rotate = 0;
}
if (d.check('OVERLAY', true))
res.camera_overlay = d.part.toLowerCase();
if (d.check('CAN_ROTATE'))
res.can_rotate = true;
if (d.check('PERSPECTIVE')) {
res.camera_kind = 'perspective';
res.can_rotate = true;
}
if (d.check('PERSP', true)) {
res.camera_kind = 'persp' + d.part;
res.can_rotate = true;
}
if (d.check('MOUSE_CLICK'))
res.mouse_click = true;
if (d.check('DEPTHRAY') || d.check('DRAY'))
res.depthMethod = 'ray';
if (d.check('DEPTHBOX') || d.check('DBOX'))
res.depthMethod = 'box';
if (d.check('DEPTHPNT') || d.check('DPNT'))
res.depthMethod = 'pnt';
if (d.check('DEPTHSIZE') || d.check('DSIZE'))
res.depthMethod = 'size';
if (d.check('DEPTHDFLT') || d.check('DDFLT'))
res.depthMethod = 'dflt';
if (d.check('ZOOM', true))
res.zoom = d.partAsFloat(0, 100) / 100;
if (d.check('ROTY', true))
res.rotatey = d.partAsFloat();
if (d.check('ROTZ', true))
res.rotatez = d.partAsFloat();
if (d.check('PHONG'))
res.material_kind = 'phong';
if (d.check('LAMBERT'))
res.material_kind = 'lambert';
if (d.check('MATCAP'))
res.material_kind = 'matcap';
if (d.check('TOON'))
res.material_kind = 'toon';
if (d.check('AMBIENT'))
res.light.kind = 'ambient';
const getCamPart = () => {
let neg = 1;
if (d.part[0] === 'N') {
neg = -1;
d.part = d.part.slice(1);
}
return neg * d.partAsFloat();
};
if (d.check('CAMX', true))
res.camx = getCamPart();
if (d.check('CAMY', true))
res.camy = getCamPart();
if (d.check('CAMZ', true))
res.camz = getCamPart();
if (d.check('CAMLX', true))
res.camlx = getCamPart();
if (d.check('CAMLY', true))
res.camly = getCamPart();
if (d.check('CAMLZ', true))
res.camlz = getCamPart();
if (d.check('BLACK'))
res.background = '#000000';
if (d.check('WHITE'))
res.background = '#FFFFFF';
if (d.check('BKGR_', true)) {
let bckgr = null;
if (d.partAsInt(1) > 0)
bckgr = getColor(d.partAsInt());
else {
for (let col = 0; col < 8; ++col) {
if (getColor(col).toUpperCase() === d.part)
bckgr = getColor(col);
}
}
if (bckgr)
res.background = '#' + new THREE.Color(bckgr).getHexString();
}
if (d.check('R3D_', true))
res.Render3D = constants.Render3D.fromString(d.part.toLowerCase());
if (d.check('MORE', true))
res.more = d.partAsInt(0, 2) ?? 2;
if (d.check('ALL')) {
res.more = 100;
res.vislevel = 99;
}
if (d.check('VISLVL', true))
res.vislevel = d.partAsInt();
if (d.check('MAXNODES', true))
res.maxnodes = d.partAsInt();
if (d.check('MAXFACES', true))
res.maxfaces = d.partAsInt();
if (d.check('CONTROLS') || d.check('CTRL'))
res.show_controls = true;
if (d.check('CLIPXYZ'))
res.clip[0].enabled = res.clip[1].enabled = res.clip[2].enabled = true;
if (d.check('CLIPX'))
res.clip[0].enabled = true;
if (d.check('CLIPY'))
res.clip[1].enabled = true;
if (d.check('CLIPZ'))
res.clip[2].enabled = true;
if (d.check('CLIP'))
res.clip[0].enabled = res.clip[1].enabled = res.clip[2].enabled = true;
if (d.check('PROJX', true)) {
res.project = 'x';
if (d.partAsInt(1) > 0)
res.projectPos = d.partAsInt();
res.can_rotate = 0;
}
if (d.check('PROJY', true)) {
res.project = 'y';
if (d.partAsInt(1) > 0)
res.projectPos = d.partAsInt();
res.can_rotate = 0;
}
if (d.check('PROJZ', true)) {
res.project = 'z';
if (d.partAsInt(1) > 0)
res.projectPos = d.partAsInt();
res.can_rotate = 0;
}
if (d.check('DFLT_COLORS') || d.check('DFLT'))
res.dflt_colors = true;
d.check('SSAO'); // deprecated
if (d.check('NOBLOOM'))
res.highlight_bloom = false;
if (d.check('BLOOM'))
res.highlight_bloom = true;
if (d.check('OUTLINE'))
res.outline = true;
if (d.check('NOWORKER'))
res.use_worker = -1;
if (d.check('WORKER'))
res.use_worker = 1;
if (d.check('NOFOG'))
res.use_fog = false;
if (d.check('FOG'))
res.use_fog = true;
if (d.check('NOHIGHLIGHT') || d.check('NOHIGH'))
res.highlight_scene = res.highlight = false;
if (d.check('HIGHLIGHT'))
res.highlight_scene = res.highlight = true;
if (d.check('HSCENEONLY')) {
res.highlight_scene = true;
res.highlight = false;
}
if (d.check('NOHSCENE'))
res.highlight_scene = false;
if (d.check('HSCENE'))
res.highlight_scene = true;
if (d.check('WIREFRAME') || d.check('WIRE'))
res.wireframe = true;
if (d.check('ROTATE'))
res.rotate = true;
if (d.check('INVX') || d.check('INVERTX'))
res.scale.x = -1;
if (d.check('INVY') || d.check('INVERTY'))
res.scale.y = -1;
if (d.check('INVZ') || d.check('INVERTZ'))
res.scale.z = -1;
if (d.check('COUNT'))
res._count = true;
if (d.check('TRANSP', true))
res.transparency = d.partAsInt(0, 100) / 100;
if (d.check('OPACITY', true))
res.transparency = 1 - d.partAsInt(0, 100) / 100;
if (d.check('AXISCENTER') || d.check('AXISC') || d.check('AC'))
res._axis = 2;
if (d.check('AXIS') || d.check('A'))
res._axis = 1;
if (d.check('TRR', true))
res.trans_radial = d.partAsInt() / 100;
if (d.check('TRZ', true))
res.trans_z = d.partAsInt() / 100;
if (d.check('W'))
res.wireframe = true;
if (d.check('Y'))
res._yup = true;
if (d.check('Z'))
res._yup = false;
// when drawing geometry without TCanvas, yup = true by default
if (res._yup === undefined)
res._yup = this.getCanvSvg().empty();
// let reuse for storing origin options
this.setOptions(res, true);
}
/** @summary Activate specified items in the browser */
activateInBrowser(names, force) {
if (isStr(names))
names = [names];
const h = this.getHPainter();
if (h) {
// show browser if it not visible
h.activateItems(names, force);
// if highlight in the browser disabled, suppress in few seconds
if (!this.ctrl.update_browser)
setTimeout(() => h.activateItems([]), 2000);
}
}
/** @summary Return ClonedNodes instance from the painter */
getClones() { return this.#clones; }
/** @summary Assign clones, created outside.
* @desc Used by ROOT geometry painter, where clones are handled by the server */
assignClones(clones, owner = true) {
if (this.#clones_owner)
this.#clones?.cleanup(this.#draw_nodes, this.#build_shapes);
this.#draw_nodes = undefined;
this.#build_shapes = undefined;
this.#drawing_ready = false;
this.#clones = clones;
this.#clones_owner = owner;
}
/** @summary method used to check matrix calculations performance with current three.js model */
testMatrixes() {
let errcnt = 0, totalcnt = 0, totalmax = 0;
const arg = {
domatrix: true,
func: (/* node */) => {
let m2 = this.getmatrix();
const entry = this.copyStack(),
mesh = this.#clones.createObject3D(entry.stack, this.#toplevel, kGetMesh);
if (!mesh)
return true;
totalcnt++;
const m1 = mesh.matrixWorld;
if (m1.equals(m2))
return true;
if ((m1.determinant() > 0) && (m2.determinant() < -0.9)) {
const flip = new THREE.Vector3(1, 1, -1);
m2 = m2.clone().scale(flip);
if (m1.equals(m2))
return true;
}
let max = 0;
for (let k = 0; k < 16; ++k)
max = Math.max(max, Math.abs(m1.elements[k] - m2.elements[k]));
totalmax = Math.max(max, totalmax);
if (max < 1e-4)
return true;
console.log(`${this.#clones.resolveStack(entry.stack).name} maxdiff ${max} determ ${m1.determinant()} ${m2.determinant()}`);
errcnt++;
return false;
}
}, tm1 = new Date().getTime();
this.#clones.scanVisible(arg);
const tm2 = new Date().getTime();
console.log(`Compare matrixes total ${totalcnt} errors ${errcnt} takes ${tm2 - tm1} maxdiff ${totalmax}`);
}
/** @summary Fill context menu */
fillContextMenu(menu) {
menu.header('Draw options');
menu.addchk(this.ctrl.update_browser, 'Browser update', () => {
this.ctrl.update_browser = !this.ctrl.update_browser;
if (!this.ctrl.update_browser)
this.activateInBrowser([]);
});
menu.addchk(this.ctrl.show_controls, 'Show Controls', () => this.showControlGui('toggle'));
menu.sub('Show axes', () => this.setAxesDraw('toggle'));
menu.addchk(this.ctrl._axis === 0, 'off', 0, arg => this.setAxesDraw(parseInt(arg)));
menu.addchk(this.ctrl._axis === 1, 'side', 1, arg => this.setAxesDraw(parseInt(arg)));
menu.addchk(this.ctrl._axis === 2, 'center', 2, arg => this.setAxesDraw(parseInt(arg)));
menu.endsub();
if (this.#geo_manager)
menu.addchk(this.ctrl.showtop, 'Show top volume', () => this.setShowTop(!this.ctrl.showtop));
menu.addchk(this.ctrl.wireframe, 'Wire frame', () => this.toggleWireFrame());
if (!this.getCanvPainter())
menu.addchk(this.isTooltipAllowed(), 'Show tooltips', () => this.setTooltipAllowed('toggle'));
menu.sub('Highlight');
menu.addchk(!this.ctrl.highlight, 'Off', () => {
this.ctrl.highlight = false;
this.changedHighlight();
});
menu.addchk(this.ctrl.highlight && !this.ctrl.highlight_bloom, 'Normal', () => {
this.ctrl.highlight = true;
this.ctrl.highlight_bloom = false;
this.changedHighlight();
});
menu.addchk(this.ctrl.highlight && this.ctrl.highlight_bloom, 'Bloom', () => {
this.ctrl.highlight = true;
this.ctrl.highlight_bloom = true;
this.changedHighlight();
});
menu.separator();
menu.addchk(this.ctrl.highlight_scene, 'Scene', flag => {
this.ctrl.highlight_scene = flag;
this.changedHighlight();
});
menu.endsub();
menu.sub('Camera');
menu.add('Reset position', () => this.focusCamera());
if (!this.ctrl.project)
menu.addchk(this.ctrl.rotate, 'Autorotate', () => this.setAutoRotate(!this.ctrl.rotate));
if (!this.#geom_viewer) {
menu.addchk(this.canRotateCamera(), 'Can rotate', () => this.changeCanRotate(!this.ctrl.can_rotate));
menu.add('Get position', () => menu.info('Position (as url)', '&opt=' + this.produceCameraUrl()));
if (!this.isOrthoCamera()) {
menu.add('Absolute position', () => {
const url = this.produceCameraUrl(true), p = url.indexOf('camlx');
menu.info('Position (as url)', '&opt=' + ((p < 0) ? url : url.slice(0, p) + '\n' + url.slice(p)));
});
}
menu.sub('Kind');
this.ctrl.cameraKindItems.forEach(item =>
menu.addchk(this.ctrl.camera_kind === item.value, item.name, item.value, arg => {
this.ctrl.camera_kind = arg;
this.changeCamera();
}));
menu.endsub();
if (this.isOrthoCamera()) {
menu.sub('Overlay');
this.ctrl.cameraOverlayItems.forEach(item =>
menu.addchk(this.ctrl.camera_overlay === item.value, item.name, item.value, arg => {
this.ctrl.camera_overlay = arg;
this.changeCamera();
}));
menu.endsub();
}
}
menu.endsub();
menu.addchk(this.ctrl.select_in_view, 'Select in view', () => {
this.ctrl.select_in_view = !this.ctrl.select_in_view;
if (this.ctrl.select_in_view)
this.startDrawGeometry();
});
}
/** @summary Method used to set transparency for all geometrical shapes
* @param {number|Function} transparency - one could provide function
* @param {boolean} [skip_render] - if specified, do not perform rendering */
changedGlobalTransparency(transparency) {
const func = isFunc(transparency) ? transparency : null;
if (func || (transparency === undefined))
transparency = this.ctrl.transparency;
this.#toplevel?.traverse(node => {
// ignore all kind of extra elements
if (node?.material?.inherentOpacity === undefined)
return;
const t = func ? func(node) : undefined;
if (t !== undefined)
node.material.opacity = 1 - t;
else
node.material.opacity = Math.min(1 - (transparency || 0), node.material.inherentOpacity);
node.material.depthWrite = node.material.opacity === 1;
node.material.transparent = node.material.opacity < 1;
});
this.render3D();
}
/** @summary Method used to interactively change material kinds */
changedMaterial() {
this.#toplevel?.traverse(node => {
// ignore all kind of extra elements
if (node.material?.inherentArgs !== undefined)
node.material = createMaterial(this.ctrl, node.material.inherentArgs);
});
this.render3D(-1);
}
/** @summary Change for all materials that property */
changeMaterialProperty(name) {
const value = this.ctrl[name];
if (value === undefined)
return console.error('No property ', name);
this.#toplevel?.traverse(node => {
// ignore all kind of extra elements
if (node.material?.inherentArgs === undefined)
return;
if (node.material[name] !== undefined) {
node.material[name] = value;