UNPKG

jsroot

Version:
1,368 lines (1,129 loc) 228 kB
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 { //