jsroot
Version:
JavaScript ROOT
1,368 lines (1,129 loc) • 228 kB
JavaScript
import { httpRequest, browser, source_dir, settings, internals, constants, create, clone,
findFunction, isBatchMode, isNodeJs, getDocument, isObject, isFunc, isStr, postponePromise, getPromise,
prROOT, 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,
clTGeoBBox, clTGeoCompositeShape,
geoCfg, geoBITS, ClonedNodes, testGeoBit, setGeoBit, toggleGeoBit, setInvisibleAll,
countNumShapes, getNodeKind, produceRenderOrder, createServerGeometry,
projectGeometry, countGeometryFaces, 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 _splitColors
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 _splitColors
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 3d painter
* @private */
function findItemWithPainter(hitem, funcname) {
while (hitem) {
if (hitem._painter?._camera) {
if (funcname && isFunc(hitem._painter[funcname]))
hitem._painter[funcname]();
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) return;
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 {
/** @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);
if (getHistPainter3DCfg(this.getMainPainter()))
this.superimpose = true;
if (gm) 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.cleanup(true);
}
/** @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; }
/** @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._previousCameraPosition = this._camera.position.clone();
this._previousCameraRotation = this._camera.rotation.clone();
this._vrDisplay.requestPresent([{ source: this._renderer.domElement }]).then(() => {
this._previousCameraNear = 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._previousCameraPosition);
this._previousCameraPosition = undefined;
this._camera.rotation.copy(this._previousCameraRotation);
this._previousCameraRotation = undefined;
this._camera.near = this._previousCameraNear;
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()) !== 0) 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('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.options = res;
}
/** @summary Activate specified items in the browser */
activateInBrowser(names, force) {
if (isStr(names)) names = [names];
if (this._hpainter) {
// show browser if it not visible
this._hpainter.activateItems(names, force);
// if highlight in the browser disabled, suppress in few seconds
if (!this.ctrl.update_browser)
setTimeout(() => this._hpainter.activateItems([]), 2000);
}
}
/** @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, 'mesh');
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();
/* let cnt = */ 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;
node.material.needsUpdate = true;
}
});
this.render3D();
}
/** @summary Reset transformation */
resetTransformation() {
this.changedTransformation('reset');
}
/** @summary Method should be called when transformation parameters were changed */
changedTransformation(arg) {
if (!this._toplevel) return;
const ctrl = this.ctrl,
translation = new THREE.Matrix4(),
vect2 = new THREE.Vector3();
if (arg === 'reset')
ctrl.trans_z = ctrl.trans_radial = 0;
this._toplevel.traverse(mesh => {
if (mesh.stack !== undefined) {
const node = mesh.parent;
if (arg === 'reset') {
if (node.matrix0) {
node.matrix.copy(node.matrix0);
node.matrix.decompose(node.position, node.quaternion, node.scale);
node.matrixWorldNeedsUpdate = true;
}
delete node.matrix0;
delete node.vect0;
delete node.vect1;
delete node.minvert;
return;
}
if (node.vect0 === undefined) {
node.matrix0 = node.matrix.clone();
node.minvert = new THREE.Matrix4().copy(node.matrixWorld).invert();
const box3 = getBoundingBox(mesh, null, true),
signz = mesh._flippedMesh ? -1 : 1;
// real center of mesh in local coordinates
node.vect0 = new THREE.Vector3((box3.max.x + box3.min.x) / 2, (box3.max.y + box3.min.y) / 2, signz * (box3.max.z + box3.min.z) / 2).applyMatrix4(node.matrixWorld);
node.vect1 = new THREE.Vector3(0, 0, 0).applyMatrix4(node.minvert);
}
vect2.set(ctrl.trans_radial * node.vect0.x, ctrl.trans_radial * node.vect0.y, ctrl.trans_z * node.vect0.z).applyMatrix4(node.minvert).sub(node.vect1);
node.matrix.multiplyMatrices(node.matrix0, translation.makeTranslation(vect2.x, vect2.y, vect2.z));
node.matrix.decompose(node.position, node.quaternion, node.scale);
node.matrixWorldNeedsUpdate = true;
} else if (mesh.stacks !== undefined) {
mesh.instanceMatrix.needsUpdate = true;
if (arg === 'reset') {
mesh.trans?.forEach((item, i) => {
mesh.setMatrixAt(i, item.matrix0);
});
delete mesh.trans;
return;
}
if (mesh.trans === undefined) {
mesh.trans = new Array(mesh.count);
mesh.geometry.computeBoundingBox();
for (let i = 0; i < mesh.count; i++) {
const item = {
matrix0: new THREE.Matrix4(),
minvert: new THREE.Matrix4()
};
mesh.trans[i] = item;
mesh.getMatrixAt(i, item.matrix0);
item.minvert.copy(item.matrix0).invert();
const box3 = new THREE.Box3().copy(mesh.geometry.boundingBox).applyMatrix4(item.matrix0);
item.vect0 = new THREE.Vector3((box3.max.x + box3.min.x) / 2, (box3.max.y + box3.min.y) / 2, (box3.max.z + box3.min.z) / 2);
item.vect1 = new THREE.Vector3(0, 0, 0).applyMatrix4(item.minvert);
}
}
const mm = new THREE.Matrix4();
mesh.trans?.forEach((item, i) => {
vect2.set(ctrl.trans_radial * item.vect0.x, ctrl.trans_radial * item.vect0.y, ctrl.trans_z * item.vect0.z).applyMatrix4(item.minvert).sub(item.vect1);
mm.multiplyMatrices(item.matrix0, translation.makeTranslation(vect2.x, vect2.y, vect2.z));
mesh.setMatrixAt(i, mm);
});
}
});
this._toplevel.updateMatrixWorld();
// axes drawing always triggers rendering
if (arg !== 'norender')
this.drawAxesAndOverlay();
}
/** @summary Should be called when auto rotate property changed */
changedAutoRotate() {
this.autorotate(2.5);
}
/** @summary Method should be called when changing axes drawing */
changedAxes() {
if (isStr(this.ctrl._axis))
this.ctrl._axis = parseInt(this.ctrl._axis);
this.drawAxesAndOverlay();
}
/** @summary Method should be called to change background color */
changedBackground(val) {
if (val !== undefined)
this.ctrl.background = val;
this._scene.background = new THREE.Color(this.ctrl.background);
this._renderer.setClearColor(this._scene.background, 1);
this.render3D(0);
if (this._toolbar) {
const bkgr = new THREE.Color(this.ctrl.background);
this._toolbar.changeBrightness((bkgr.r + bkgr.g + bkgr.b) < 1);
}
}
/** @summary Display control GUI */
showControlGui(on) {
// while complete geo drawing can be removed until dat is loaded - just check and ignore callback
if (!this.ctrl) return;
if (on === 'toggle')
on = !this._gui;
else if (on === undefined)
on = this.ctrl.show_controls;
this.ctrl.show_controls = on;
if (this._gui) {
if (!on) {
this._gui.destroy();
delete this._gui;
}
return;
}
if (!on || !this._renderer)
return;
const main = this.selectDom();
if (main.style('position') === 'static')
main.style('position', 'relative');
this._gui = new GUI({ container: main.node(), closeFolders: true, width: Math.min(300, this._scene_width / 2),
title: 'Settings' });
const dom = this._gui.domElement;
dom.style.position = 'absolute';
dom.style.top = 0;
dom.style.right = 0;
this._gui.painter = this;
const makeLil = items => {
const lil = {};
items.forEach(i => { lil[i.name] = i.value; });
return lil;
};
if (!this.ctrl.project) {
const selection = this._gui.addFolder('Selection');
if (!this.ctrl.maxnodes)
this.ctrl.maxnodes = this._clones?.getMaxVisNodes() ?? 10000;
if (!this.ctrl.vislevel)
this.ctrl.vislevel = this._clones?.getVisLevel() ?? 3;
if (!this.ctrl.maxfaces)
this.ctrl.maxfaces = 200000 * this.ctrl.more;
this.ctrl.more = 1;
selection.add(this.ctrl, 'vislevel', 1, 99, 1)
.name('Visibility level')
.listen().onChange(() => this.startRedraw(500));
selection.add(this.ctrl, 'maxnodes', 0, 500000, 1000)
.name('Visible nodes')
.listen().onChange(() => this.startRedraw(500));
selection.add(this.ctrl, 'maxfaces', 0, 5000000, 100000)
.name('Max faces')
.listen().onChange(() => this.startRedraw(500));
}
if (this.ctrl.project) {
const bound = this.getGeomBoundingBox(this.getProjectionSource(), 0.01),
axis = this.ctrl.project;
if (this.ctrl.projectPos === undefined)
this.ctrl.projectPos = (bound.min[axis] + bound.max[axis])/2;
this._gui.add(this.ctrl, 'projectPos', bound.min[axis], bound.max[axis])
.name(axis.toUpperCase() + ' projection')
.onChange(() => this.startDrawGeometry());
} else {
//