geom/TGeoPainter.mjs

  1. import { httpRequest, browser, source_dir, settings, internals, constants, create, clone,
  2. findFunction, isBatchMode, isNodeJs, getDocument, isObject, isFunc, isStr, postponePromise, getPromise,
  3. prROOT, clTNamed, clTList, clTAxis, clTObjArray, clTPolyMarker3D, clTPolyLine3D,
  4. clTGeoVolume, clTGeoNode, clTGeoNodeMatrix, nsREX, nsSVG, kInspect } from '../core.mjs';
  5. import { showProgress, injectStyle, ToolbarIcons } from '../gui/utils.mjs';
  6. import { GUI } from '../gui/lil-gui.mjs';
  7. import { THREE, assign3DHandler, disposeThreejsObject, createOrbitControl,
  8. createLineSegments, InteractiveControl, PointsCreator, importThreeJs,
  9. createRender3D, beforeRender3D, afterRender3D, getRender3DKind, cleanupRender3D,
  10. createTextGeometry } from '../base/base3d.mjs';
  11. import { getColor, getRootColors } from '../base/colors.mjs';
  12. import { DrawOptions } from '../base/BasePainter.mjs';
  13. import { ObjectPainter } from '../base/ObjectPainter.mjs';
  14. import { createMenu, closeMenu } from '../gui/menu.mjs';
  15. import { TAxisPainter } from '../gpad/TAxisPainter.mjs';
  16. import { ensureTCanvas } from '../gpad/TCanvasPainter.mjs';
  17. import { kindGeo, kindEve,
  18. clTGeoBBox, clTGeoCompositeShape,
  19. geoCfg, geoBITS, ClonedNodes, testGeoBit, setGeoBit, toggleGeoBit, setInvisibleAll,
  20. countNumShapes, getNodeKind, produceRenderOrder, createServerGeometry,
  21. projectGeometry, countGeometryFaces, createMaterial, createFrustum, createProjectionMatrix,
  22. getBoundingBox, provideObjectInfo, isSameStack, checkDuplicates, getObjectName, cleanupShape, getShapeIcon } from './geobase.mjs';
  23. const _ENTIRE_SCENE = 0, _BLOOM_SCENE = 1,
  24. clTGeoManager = 'TGeoManager', clTEveGeoShapeExtract = 'TEveGeoShapeExtract',
  25. clTGeoOverlap = 'TGeoOverlap', clTGeoVolumeAssembly = 'TGeoVolumeAssembly',
  26. clTEveTrack = 'TEveTrack', clTEvePointSet = 'TEvePointSet',
  27. clREveGeoShapeExtract = `${nsREX}REveGeoShapeExtract`;
  28. /** @summary Function used to build hierarchy of elements of overlap object
  29. * @private */
  30. function buildOverlapVolume(overlap) {
  31. const vol = create(clTGeoVolume);
  32. setGeoBit(vol, geoBITS.kVisDaughters, true);
  33. vol.$geoh = true; // workaround, let know browser that we are in volumes hierarchy
  34. vol.fName = '';
  35. const node1 = create(clTGeoNodeMatrix);
  36. node1.fName = overlap.fVolume1.fName || 'Overlap1';
  37. node1.fMatrix = overlap.fMatrix1;
  38. node1.fVolume = overlap.fVolume1;
  39. // node1.fVolume.fLineColor = 2; // color assigned with ctrl.split_colors
  40. const node2 = create(clTGeoNodeMatrix);
  41. node2.fName = overlap.fVolume2.fName || 'Overlap2';
  42. node2.fMatrix = overlap.fMatrix2;
  43. node2.fVolume = overlap.fVolume2;
  44. // node2.fVolume.fLineColor = 3; // color assigned with ctrl.split_colors
  45. vol.fNodes = create(clTList);
  46. vol.fNodes.Add(node1);
  47. vol.fNodes.Add(node2);
  48. return vol;
  49. }
  50. let $comp_col_cnt = 0;
  51. /** @summary Function used to build hierarchy of elements of composite shapes
  52. * @private */
  53. function buildCompositeVolume(comp, maxlvl, side) {
  54. if (maxlvl === undefined) maxlvl = 1;
  55. if (!side) {
  56. $comp_col_cnt = 0;
  57. side = '';
  58. }
  59. const vol = create(clTGeoVolume);
  60. setGeoBit(vol, geoBITS.kVisThis, true);
  61. setGeoBit(vol, geoBITS.kVisDaughters, true);
  62. if ((side && (comp._typename !== clTGeoCompositeShape)) || (maxlvl <= 0)) {
  63. vol.fName = side;
  64. vol.fLineColor = ($comp_col_cnt++ % 8) + 2;
  65. vol.fShape = comp;
  66. return vol;
  67. }
  68. if (side) side += '/';
  69. vol.$geoh = true; // workaround, let know browser that we are in volumes hierarchy
  70. vol.fName = '';
  71. const node1 = create(clTGeoNodeMatrix);
  72. setGeoBit(node1, geoBITS.kVisThis, true);
  73. setGeoBit(node1, geoBITS.kVisDaughters, true);
  74. node1.fName = 'Left';
  75. node1.fMatrix = comp.fNode.fLeftMat;
  76. node1.fVolume = buildCompositeVolume(comp.fNode.fLeft, maxlvl-1, side + 'Left');
  77. const node2 = create(clTGeoNodeMatrix);
  78. setGeoBit(node2, geoBITS.kVisThis, true);
  79. setGeoBit(node2, geoBITS.kVisDaughters, true);
  80. node2.fName = 'Right';
  81. node2.fMatrix = comp.fNode.fRightMat;
  82. node2.fVolume = buildCompositeVolume(comp.fNode.fRight, maxlvl-1, side + 'Right');
  83. vol.fNodes = create(clTList);
  84. vol.fNodes.Add(node1);
  85. vol.fNodes.Add(node2);
  86. if (!side) $comp_col_cnt = 0;
  87. return vol;
  88. }
  89. /** @summary Provides 3D rendering configuration from histogram painter
  90. * @return {Object} with scene, renderer and other attributes
  91. * @private */
  92. function getHistPainter3DCfg(painter) {
  93. const main = painter?.getFramePainter();
  94. if (painter?.mode3d && isFunc(main?.create3DScene) && main?.renderer) {
  95. let scale_x = 1, scale_y = 1, scale_z = 1,
  96. offset_x = 0, offset_y = 0, offset_z = 0;
  97. if (main.scale_xmax > main.scale_xmin) {
  98. scale_x = 2 * main.size_x3d/(main.scale_xmax - main.scale_xmin);
  99. offset_x = (main.scale_xmax + main.scale_xmin) / 2 * scale_x;
  100. }
  101. if (main.scale_ymax > main.scale_ymin) {
  102. scale_y = 2 * main.size_y3d/(main.scale_ymax - main.scale_ymin);
  103. offset_y = (main.scale_ymax + main.scale_ymin) / 2 * scale_y;
  104. }
  105. if (main.scale_zmax > main.scale_zmin) {
  106. scale_z = 2 * main.size_z3d/(main.scale_zmax - main.scale_zmin);
  107. offset_z = (main.scale_zmax + main.scale_zmin) / 2 * scale_z - main.size_z3d;
  108. }
  109. return {
  110. webgl: main.webgl,
  111. scene: main.scene,
  112. scene_width: main.scene_width,
  113. scene_height: main.scene_height,
  114. toplevel: main.toplevel,
  115. renderer: main.renderer,
  116. camera: main.camera,
  117. scale_x, scale_y, scale_z,
  118. offset_x, offset_y, offset_z
  119. };
  120. }
  121. }
  122. /** @summary find item with geometry painter
  123. * @private */
  124. function findItemWithGeoPainter(hitem, test_changes) {
  125. while (hitem) {
  126. if (isFunc(hitem._painter?.testGeomChanges)) {
  127. if (test_changes)
  128. hitem._painter.testGeomChanges();
  129. return hitem;
  130. }
  131. hitem = hitem._parent;
  132. }
  133. return null;
  134. }
  135. /** @summary provide css style for geo object
  136. * @private */
  137. function provideVisStyle(obj) {
  138. if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract))
  139. return obj.fRnrSelf ? ' geovis_this' : '';
  140. const vis = !testGeoBit(obj, geoBITS.kVisNone) && testGeoBit(obj, geoBITS.kVisThis);
  141. let chld = testGeoBit(obj, geoBITS.kVisDaughters);
  142. if (chld && !obj.fNodes?.arr?.length)
  143. chld = false;
  144. if (vis && chld)
  145. return ' geovis_all';
  146. if (vis)
  147. return ' geovis_this';
  148. if (chld)
  149. return ' geovis_daughters';
  150. return '';
  151. }
  152. /** @summary update icons
  153. * @private */
  154. function updateBrowserIcons(obj, hpainter) {
  155. if (!obj || !hpainter) return;
  156. hpainter.forEachItem(m => {
  157. // update all items with that volume
  158. if ((obj === m._volume) || (obj === m._geoobj)) {
  159. m._icon = m._icon.split(' ')[0] + provideVisStyle(obj);
  160. hpainter.updateTreeNode(m);
  161. }
  162. });
  163. }
  164. /** @summary Return stack for the item from list of intersection
  165. * @private */
  166. function getIntersectStack(item) {
  167. const obj = item?.object;
  168. if (!obj) return null;
  169. if (obj.stack)
  170. return obj.stack;
  171. if (obj.stacks && item.instanceId !== undefined && item.instanceId < obj.stacks.length)
  172. return obj.stacks[item.instanceId];
  173. }
  174. /**
  175. * @summary Toolbar for geometry painter
  176. *
  177. * @private
  178. */
  179. class Toolbar {
  180. /** @summary constructor */
  181. constructor(container, bright, buttons) {
  182. this.bright = bright;
  183. this.buttons = buttons;
  184. this.element = container.append('div').attr('style', 'float: left; box-sizing: border-box; position: relative; bottom: 23px; vertical-align: middle; padding-left: 5px');
  185. }
  186. /** @summary add buttons */
  187. createButtons() {
  188. const buttonsNames = [];
  189. this.buttons.forEach(buttonConfig => {
  190. const buttonName = buttonConfig.name;
  191. if (!buttonName)
  192. throw new Error('must provide button name in button config');
  193. if (buttonsNames.indexOf(buttonName) !== -1)
  194. throw new Error(`button name ${buttonName} is taken`);
  195. buttonsNames.push(buttonName);
  196. const title = buttonConfig.title || buttonConfig.name;
  197. if (!isFunc(buttonConfig.click))
  198. throw new Error('must provide button click() function in button config');
  199. ToolbarIcons.createSVG(this.element, ToolbarIcons[buttonConfig.icon], 16, title, this.bright)
  200. .on('click', buttonConfig.click)
  201. .style('position', 'relative')
  202. .style('padding', '3px 1px');
  203. });
  204. }
  205. /** @summary change brightness */
  206. changeBrightness(bright) {
  207. if (this.bright === bright) return;
  208. this.element.selectAll('*').remove();
  209. this.bright = bright;
  210. this.createButtons();
  211. }
  212. /** @summary cleanup toolbar */
  213. cleanup() {
  214. this.element?.remove();
  215. delete this.element;
  216. }
  217. } // class ToolBar
  218. /**
  219. * @summary geometry drawing control
  220. *
  221. * @private
  222. */
  223. class GeoDrawingControl extends InteractiveControl {
  224. constructor(mesh, bloom) {
  225. super();
  226. this.mesh = mesh?.material ? mesh : null;
  227. this.bloom = bloom;
  228. }
  229. /** @summary set highlight */
  230. setHighlight(col, indx) {
  231. return this.drawSpecial(col, indx);
  232. }
  233. /** @summary draw special */
  234. drawSpecial(col, indx) {
  235. const c = this.mesh;
  236. if (!c?.material) return;
  237. if (c.isInstancedMesh) {
  238. if (c._highlight_mesh) {
  239. c.remove(c._highlight_mesh);
  240. delete c._highlight_mesh;
  241. }
  242. if (col && indx !== undefined) {
  243. const h = new THREE.Mesh(c.geometry, c.material.clone());
  244. if (this.bloom) {
  245. h.layers.enable(_BLOOM_SCENE);
  246. h.material.emissive = new THREE.Color(0x00ff00);
  247. } else {
  248. h.material.color = new THREE.Color(col);
  249. h.material.opacity = 1.0;
  250. }
  251. const m = new THREE.Matrix4();
  252. c.getMatrixAt(indx, m);
  253. h.applyMatrix4(m);
  254. c.add(h);
  255. h.jsroot_special = true; // exclude from intersections
  256. c._highlight_mesh = h;
  257. }
  258. return true;
  259. }
  260. if (col) {
  261. if (!c.origin) {
  262. c.origin = {
  263. color: c.material.color,
  264. emissive: c.material.emissive,
  265. opacity: c.material.opacity,
  266. width: c.material.linewidth,
  267. size: c.material.size
  268. };
  269. }
  270. if (this.bloom) {
  271. c.layers.enable(_BLOOM_SCENE);
  272. c.material.emissive = new THREE.Color(0x00ff00);
  273. } else {
  274. c.material.color = new THREE.Color(col);
  275. c.material.opacity = 1.0;
  276. }
  277. if (c.hightlightWidthScale && !browser.isWin)
  278. c.material.linewidth = c.origin.width * c.hightlightWidthScale;
  279. if (c.highlightScale)
  280. c.material.size = c.origin.size * c.highlightScale;
  281. return true;
  282. } else if (c.origin) {
  283. if (this.bloom) {
  284. c.material.emissive = c.origin.emissive;
  285. c.layers.enable(_ENTIRE_SCENE);
  286. } else {
  287. c.material.color = c.origin.color;
  288. c.material.opacity = c.origin.opacity;
  289. }
  290. if (c.hightlightWidthScale)
  291. c.material.linewidth = c.origin.width;
  292. if (c.highlightScale)
  293. c.material.size = c.origin.size;
  294. return true;
  295. }
  296. }
  297. } // class GeoDrawingControl
  298. const stageInit = 0, stageCollect = 1, stageWorkerCollect = 2, stageAnalyze = 3, stageCollShapes = 4,
  299. stageStartBuild = 5, stageWorkerBuild = 6, stageBuild = 7, stageBuildReady = 8, stageWaitMain = 9, stageBuildProj = 10;
  300. /**
  301. * @summary Painter class for geometries drawing
  302. *
  303. * @private
  304. */
  305. class TGeoPainter extends ObjectPainter {
  306. #geo_manager; // TGeoManager instance - if assigned in drawing
  307. #superimpose; // set when superimposed with other 3D drawings
  308. #complete_handler; // callback, assigned by ROOT GeomViewer
  309. #geom_viewer; // true when processing drawing from ROOT GeomViewer
  310. #extra_objects; // TList with extra objects configured for drawing
  311. #webgl; // true when normal WebGL drawing is enabled
  312. #worker; // extra Worker to run different calculations
  313. #worker_ready; // is worker started and initialized
  314. #worker_jobs; // number of submitted to worker jobs
  315. #clones; // instance of ClonedNodes
  316. #clones_owner; // is instance managed by the painter
  317. #draw_nodes; // drawn nodes from geometry
  318. #build_shapes; // build shapes required for drawings
  319. #current_face_limit; // current face limit
  320. #draw_all_nodes; // flag using in drawing
  321. #draw_nodes_again; // flag set if drawing should be started again when completed
  322. #drawing_ready; // if drawing completed
  323. #drawing_log; // current drawing log information
  324. #draw_resolveFuncs; // resolve call-backs for start drawing calls
  325. #start_drawing_time; // time when drawing started
  326. #start_render_tm; // time when rendering started
  327. #first_render_tm; // first time when rendering was performed
  328. #last_render_tm; // last time when rendering was performed
  329. #last_render_meshes; // last value of rendered meshes
  330. #render_resolveFuncs; // resolve call-backs from render calls
  331. #render_tmout; // timeout used in Render3D()
  332. #first_drawing; // true when very first drawing is performed
  333. #full_redrawing; // if full redraw must be performed
  334. #last_clip_cfg; // last config of clip panels
  335. #redraw_timer; // timer used in redraw
  336. #did_update; // flag used in update
  337. #custom_bounding_box; // custom bounding box for extra objects
  338. #clip_planes; // clip planes
  339. #central_painter; // pointer on central painter
  340. #subordinate_painters; // list of subordinate painters
  341. #effectComposer; // extra composer for special effects, used in EVE
  342. #bloomComposer; // extra composer for bloom highlight
  343. #new_draw_nodes; // temporary list of new draw nodes
  344. #new_append_nodes; // temporary list of new append nodes
  345. #more_nodes; // more nodes from ROOT geometry viewer
  346. #provided_more_nodes; // staged more nodes from ROOT geometry viewer
  347. #on_pad; // true when drawing done on TPad
  348. #last_manifest; // last node which was "manifest" via menu
  349. #last_hidden; // last node which was "hidden" via menu
  350. #gui; // dat.GUI instance
  351. #toolbar; // tool buttons
  352. #controls; // orbit control
  353. #highlight_handlers; // highlight handlers
  354. #animating; // set when animation started
  355. #last_camera_position; // last camera position
  356. #fullgeom_proj; // full geometry to produce projection
  357. #selected_mesh; // used in highlight
  358. #fog; // fog for the scene
  359. #lookat; // position to which camera looks at
  360. #scene_size; // stored scene size to control changes
  361. #scene_width; // actual scene width
  362. #scene_height; // actual scene width
  363. #fit_main_area; // true if drawing must fit to DOM
  364. #overall_size; // overall size
  365. #scene; // three.js Scene object
  366. #camera; // three.js camera object
  367. #renderer; // three.js renderer object
  368. #toplevel; // three.js top level Object3D
  369. #camera0pos; // camera on opposite side
  370. #vrControllers; // used in VR display
  371. #controllersMeshes; // used in VR display
  372. #dolly; // used in VR display
  373. #vrDisplay; // used in VR display
  374. #vr_camera_position; // used in VR display
  375. #vr_camera_rotation; // used in VR display
  376. #vr_camera_near; // used in VR display
  377. #standingMatrix; // used in VR display
  378. #raycasterEnd; // used in VR display
  379. #raycasterOrigin; // used in VR display
  380. #adjust_camera_with_render; // flag to readjust camera when rendering
  381. #did_cleanup; // flag set after cleanup
  382. /** @summary Constructor
  383. * @param {object|string} dom - DOM element for drawing or element id
  384. * @param {object} obj - supported TGeo object */
  385. constructor(dom, obj) {
  386. let gm;
  387. if (obj?._typename === clTGeoManager) {
  388. gm = obj;
  389. obj = obj.fMasterVolume;
  390. }
  391. if (obj?._typename && (obj._typename.indexOf(clTGeoVolume) === 0))
  392. obj = { _typename: clTGeoNode, fVolume: obj, fName: obj.fName, $geoh: obj.$geoh, _proxy: true };
  393. super(dom, obj);
  394. this.#superimpose = Boolean(getHistPainter3DCfg(this.getMainPainter()));
  395. this.#geo_manager = gm;
  396. this._no_default_title = true; // do not set title to main DIV
  397. this.mode3d = true; // indication of 3D mode
  398. this.drawing_stage = stageInit; //
  399. this.#drawing_log = 'Init';
  400. this.ctrl = {
  401. clipIntersect: true,
  402. clipVisualize: false,
  403. clip: [{ name: 'x', enabled: false, value: 0, min: -100, max: 100, step: 1 },
  404. { name: 'y', enabled: false, value: 0, min: -100, max: 100, step: 1 },
  405. { name: 'z', enabled: false, value: 0, min: -100, max: 100, step: 1 }],
  406. _highlight: 0,
  407. highlight: 0,
  408. highlight_bloom: 0,
  409. highlight_scene: 0,
  410. highlight_color: '#00ff00',
  411. bloom_strength: 1.5,
  412. more: 1,
  413. maxfaces: 0,
  414. vislevel: undefined,
  415. maxnodes: undefined,
  416. dflt_colors: false,
  417. info: { num_meshes: 0, num_faces: 0, num_shapes: 0 },
  418. depthTest: true,
  419. depthMethod: 'dflt',
  420. select_in_view: false,
  421. update_browser: true,
  422. use_fog: false,
  423. light: { kind: 'points', top: false, bottom: false, left: false, right: false, front: false, specular: true, power: 1 },
  424. lightKindItems: [
  425. { name: 'AmbientLight', value: 'ambient' },
  426. { name: 'DirectionalLight', value: 'points' },
  427. { name: 'HemisphereLight', value: 'hemisphere' },
  428. { name: 'Ambient + Point', value: 'mix' }
  429. ],
  430. trans_radial: 0,
  431. trans_z: 0,
  432. scale: new THREE.Vector3(1, 1, 1),
  433. zoom: 1.0, rotatey: 0, rotatez: 0,
  434. depthMethodItems: [
  435. { name: 'Default', value: 'dflt' },
  436. { name: 'Raytraicing', value: 'ray' },
  437. { name: 'Boundary box', value: 'box' },
  438. { name: 'Mesh size', value: 'size' },
  439. { name: 'Central point', value: 'pnt' }
  440. ],
  441. cameraKindItems: [
  442. { name: 'Perspective', value: 'perspective' },
  443. { name: 'Perspective (Floor XOZ)', value: 'perspXOZ' },
  444. { name: 'Perspective (Floor YOZ)', value: 'perspYOZ' },
  445. { name: 'Perspective (Floor XOY)', value: 'perspXOY' },
  446. { name: 'Orthographic (XOY)', value: 'orthoXOY' },
  447. { name: 'Orthographic (XOZ)', value: 'orthoXOZ' },
  448. { name: 'Orthographic (ZOY)', value: 'orthoZOY' },
  449. { name: 'Orthographic (ZOX)', value: 'orthoZOX' },
  450. { name: 'Orthographic (XnOY)', value: 'orthoXNOY' },
  451. { name: 'Orthographic (XnOZ)', value: 'orthoXNOZ' },
  452. { name: 'Orthographic (ZnOY)', value: 'orthoZNOY' },
  453. { name: 'Orthographic (ZnOX)', value: 'orthoZNOX' }
  454. ],
  455. cameraOverlayItems: [
  456. { name: 'None', value: 'none' },
  457. { name: 'Bar', value: 'bar' },
  458. { name: 'Axis', value: 'axis' },
  459. { name: 'Grid', value: 'grid' },
  460. { name: 'Grid background', value: 'gridb' },
  461. { name: 'Grid foreground', value: 'gridf' }
  462. ],
  463. camera_kind: 'perspective',
  464. camera_overlay: 'gridb',
  465. rotate: false,
  466. background: settings.DarkMode ? '#000000' : '#ffffff',
  467. can_rotate: true,
  468. _axis: 0,
  469. instancing: 0,
  470. _count: false,
  471. // material properties
  472. wireframe: false,
  473. transparency: 0,
  474. flatShading: false,
  475. roughness: 0.5,
  476. metalness: 0.5,
  477. shininess: 0,
  478. reflectivity: 0.5,
  479. material_kind: 'lambert',
  480. materialKinds: [
  481. { name: 'MeshLambertMaterial', value: 'lambert', emissive: true, props: [{ name: 'flatShading' }] },
  482. { name: 'MeshBasicMaterial', value: 'basic' },
  483. { name: 'MeshStandardMaterial', value: 'standard', emissive: true,
  484. props: [{ name: 'flatShading' }, { name: 'roughness', min: 0, max: 1, step: 0.001 }, { name: 'metalness', min: 0, max: 1, step: 0.001 }] },
  485. { name: 'MeshPhysicalMaterial', value: 'physical', emissive: true,
  486. 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 }] },
  487. { name: 'MeshPhongMaterial', value: 'phong', emissive: true,
  488. props: [{ name: 'flatShading' }, { name: 'shininess', min: 0, max: 100, step: 0.1 }] },
  489. { name: 'MeshNormalMaterial', value: 'normal', props: [{ name: 'flatShading' }] },
  490. { name: 'MeshDepthMaterial', value: 'depth' },
  491. { name: 'MeshMatcapMaterial', value: 'matcap' },
  492. { name: 'MeshToonMaterial', value: 'toon' }
  493. ],
  494. getMaterialCfg() {
  495. let cfg;
  496. this.materialKinds.forEach(item => {
  497. if (item.value === this.material_kind)
  498. cfg = item;
  499. });
  500. return cfg;
  501. }
  502. };
  503. this.#draw_resolveFuncs = [];
  504. this.#render_resolveFuncs = [];
  505. this.cleanup(true);
  506. }
  507. /** @summary Returns scene */
  508. getScene() { return this.#scene; }
  509. /** @summary Returns camera */
  510. getCamera() { return this.#camera; }
  511. /** @summary Returns renderer */
  512. getRenderer() { return this.#renderer; }
  513. /** @summary Returns top Object3D instance */
  514. getTopObject3D() { return this.#toplevel; }
  515. /** @summary Assign or remove subordinate painter */
  516. assignSubordinate(painter, do_assign = true) {
  517. if (this.#subordinate_painters === undefined)
  518. this.#subordinate_painters = [];
  519. const pos = this.#subordinate_painters.indexOf(painter);
  520. if (do_assign && pos < 0)
  521. this.#subordinate_painters.push(painter);
  522. else if (!do_assign && pos >= 0)
  523. this.#subordinate_painters.splice(pos, 1);
  524. }
  525. /** @summary Return list of subordinate painters */
  526. getSubordinates() { return this.#subordinate_painters ?? []; }
  527. /** @summary Assign or cleanup central painter */
  528. assignCentral(painter, do_assign = true) {
  529. if (do_assign)
  530. this.#central_painter = painter;
  531. else if (this.#central_painter === painter)
  532. this.#central_painter = undefined;
  533. }
  534. /** @summary Returns central painter */
  535. getCentral() { return this.#central_painter; }
  536. /** @summary Returns extra objects configured for drawing */
  537. getExtraObjects() { return this.#extra_objects; }
  538. /** @summary Function called by framework when dark mode is changed
  539. * @private */
  540. changeDarkMode(mode) {
  541. if ((this.ctrl.background === '#000000') || (this.ctrl.background === '#ffffff'))
  542. this.changedBackground((mode ?? settings.DarkMode) ? '#000000' : '#ffffff');
  543. }
  544. /** @summary Change drawing stage
  545. * @private */
  546. changeStage(value, msg) {
  547. this.drawing_stage = value;
  548. if (!msg) {
  549. switch (value) {
  550. case stageInit: msg = 'Building done'; break;
  551. case stageCollect: msg = 'collect visibles'; break;
  552. case stageWorkerCollect: msg = 'worker collect visibles'; break;
  553. case stageAnalyze: msg = 'Analyse visibles'; break;
  554. case stageCollShapes: msg = 'collect shapes for building'; break;
  555. case stageStartBuild: msg = 'Start build shapes'; break;
  556. case stageWorkerBuild: msg = 'Worker build shapes'; break;
  557. case stageBuild: msg = 'Build shapes'; break;
  558. case stageBuildReady: msg = 'Build ready'; break;
  559. case stageWaitMain: msg = 'Wait for main painter'; break;
  560. case stageBuildProj: msg = 'Build projection'; break;
  561. default: msg = `stage ${value}`;
  562. }
  563. }
  564. this.#drawing_log = msg;
  565. }
  566. /** @summary Check drawing stage */
  567. isStage(value) { return value === this.drawing_stage; }
  568. isBatchMode() { return isBatchMode() || this.batch_mode; }
  569. getControls() { return this.#controls; }
  570. /** @summary Create toolbar */
  571. createToolbar() {
  572. if (this.#toolbar || !this.#webgl || this.ctrl.notoolbar || this.isBatchMode())
  573. return;
  574. const buttonList = [{
  575. name: 'toImage',
  576. title: 'Save as PNG',
  577. icon: 'camera',
  578. click: () => this.createSnapshot()
  579. }, {
  580. name: 'control',
  581. title: 'Toggle control UI',
  582. icon: 'rect',
  583. click: () => this.showControlGui('toggle')
  584. }, {
  585. name: 'enlarge',
  586. title: 'Enlarge geometry drawing',
  587. icon: 'circle',
  588. click: () => this.toggleEnlarge()
  589. }];
  590. // Only show VR icon if WebVR API available.
  591. if (navigator.getVRDisplays) {
  592. buttonList.push({
  593. name: 'entervr',
  594. title: 'Enter VR (It requires a VR Headset connected)',
  595. icon: 'vrgoggles',
  596. click: () => this.toggleVRMode()
  597. });
  598. this.initVRMode();
  599. }
  600. if (settings.ContextMenu) {
  601. buttonList.push({
  602. name: 'menu',
  603. title: 'Show context menu',
  604. icon: 'question',
  605. click: evnt => {
  606. evnt.preventDefault();
  607. evnt.stopPropagation();
  608. if (closeMenu()) return;
  609. createMenu(evnt, this).then(menu => {
  610. menu.painter.fillContextMenu(menu);
  611. menu.show();
  612. });
  613. }
  614. });
  615. }
  616. const bkgr = new THREE.Color(this.ctrl.background);
  617. this.#toolbar = new Toolbar(this.selectDom(), (bkgr.r + bkgr.g + bkgr.b) < 1, buttonList);
  618. this.#toolbar.createButtons();
  619. }
  620. /** @summary Initialize VR mode */
  621. initVRMode() {
  622. // Dolly contains camera and controllers in VR Mode
  623. // Allows moving the user in the scene
  624. this.#dolly = new THREE.Group();
  625. this.#scene.add(this.#dolly);
  626. this.#standingMatrix = new THREE.Matrix4();
  627. // Raycaster temp variables to avoid one per frame allocation.
  628. this.#raycasterEnd = new THREE.Vector3();
  629. this.#raycasterOrigin = new THREE.Vector3();
  630. navigator.getVRDisplays().then(displays => {
  631. const vrDisplay = displays[0];
  632. if (!vrDisplay) return;
  633. this.#renderer.vr.setDevice(vrDisplay);
  634. this.#vrDisplay = vrDisplay;
  635. if (vrDisplay.stageParameters)
  636. this.#standingMatrix.fromArray(vrDisplay.stageParameters.sittingToStandingTransform);
  637. this.initVRControllersGeometry();
  638. });
  639. }
  640. /** @summary Init VR controllers geometry
  641. * @private */
  642. initVRControllersGeometry() {
  643. const geometry = new THREE.SphereGeometry(0.025, 18, 36),
  644. material = new THREE.MeshBasicMaterial({ color: 'grey', vertexColors: false }),
  645. rayMaterial = new THREE.MeshBasicMaterial({ color: 'fuchsia', vertexColors: false }),
  646. rayGeometry = new THREE.BoxGeometry(0.001, 0.001, 2),
  647. ray1Mesh = new THREE.Mesh(rayGeometry, rayMaterial),
  648. ray2Mesh = new THREE.Mesh(rayGeometry, rayMaterial),
  649. sphere1 = new THREE.Mesh(geometry, material),
  650. sphere2 = new THREE.Mesh(geometry, material);
  651. this.#controllersMeshes = [];
  652. this.#controllersMeshes.push(sphere1);
  653. this.#controllersMeshes.push(sphere2);
  654. ray1Mesh.position.z -= 1;
  655. ray2Mesh.position.z -= 1;
  656. sphere1.add(ray1Mesh);
  657. sphere2.add(ray2Mesh);
  658. this.#dolly.add(sphere1);
  659. this.#dolly.add(sphere2);
  660. // Controller mesh hidden by default
  661. sphere1.visible = false;
  662. sphere2.visible = false;
  663. }
  664. /** @summary Update VR controllers list
  665. * @private */
  666. updateVRControllersList() {
  667. const gamepads = navigator.getGamepads && navigator.getGamepads();
  668. // Has controller list changed?
  669. if (this.#vrControllers && (gamepads.length === this.#vrControllers.length)) return;
  670. // Hide meshes.
  671. this.#controllersMeshes.forEach(mesh => { mesh.visible = false; });
  672. this.#vrControllers = [];
  673. for (let i = 0; i < gamepads.length; ++i) {
  674. if (!gamepads[i] || !gamepads[i].pose) continue;
  675. this.#vrControllers.push({
  676. gamepad: gamepads[i],
  677. mesh: this.#controllersMeshes[i]
  678. });
  679. this.#controllersMeshes[i].visible = true;
  680. }
  681. }
  682. /** @summary Process VR controller intersection
  683. * @private */
  684. processVRControllerIntersections() {
  685. let intersects = [];
  686. for (let i = 0; i < this.#vrControllers.length; ++i) {
  687. const controller = this.#vrControllers[i].mesh,
  688. end = controller.localToWorld(this.#raycasterEnd.set(0, 0, -1)),
  689. origin = controller.localToWorld(this.#raycasterOrigin.set(0, 0, 0));
  690. end.sub(origin).normalize();
  691. intersects = intersects.concat(this.#controls.getOriginDirectionIntersects(origin, end));
  692. }
  693. // Remove duplicates.
  694. intersects = intersects.filter((item, pos) => { return intersects.indexOf(item) === pos; });
  695. this.#controls.processMouseMove(intersects);
  696. }
  697. /** @summary Update VR controllers
  698. * @private */
  699. updateVRControllers() {
  700. this.updateVRControllersList();
  701. // Update pose.
  702. for (let i = 0; i < this.#vrControllers.length; ++i) {
  703. const controller = this.#vrControllers[i],
  704. orientation = controller.gamepad.pose.orientation,
  705. position = controller.gamepad.pose.position,
  706. controllerMesh = controller.mesh;
  707. if (orientation) controllerMesh.quaternion.fromArray(orientation);
  708. if (position) controllerMesh.position.fromArray(position);
  709. controllerMesh.updateMatrix();
  710. controllerMesh.applyMatrix4(this.#standingMatrix);
  711. controllerMesh.matrixWorldNeedsUpdate = true;
  712. }
  713. this.processVRControllerIntersections();
  714. }
  715. /** @summary Toggle VR mode
  716. * @private */
  717. toggleVRMode() {
  718. if (!this.#vrDisplay)
  719. return;
  720. // Toggle VR mode off
  721. if (this.#vrDisplay.isPresenting) {
  722. this.exitVRMode();
  723. return;
  724. }
  725. this.#vr_camera_position = this.#camera.position.clone();
  726. this.#vr_camera_rotation = this.#camera.rotation.clone();
  727. this.#vrDisplay.requestPresent([{ source: this.#renderer.domElement }]).then(() => {
  728. this.#vr_camera_near = this.#camera.near;
  729. this.#dolly.position.set(this.#camera.position.x/4, -this.#camera.position.y/8, -this.#camera.position.z/4);
  730. this.#camera.position.set(0, 0, 0);
  731. this.#dolly.add(this.#camera);
  732. this.#camera.near = 0.1;
  733. this.#camera.updateProjectionMatrix();
  734. this.#renderer.vr.enabled = true;
  735. this.#renderer.setAnimationLoop(() => {
  736. this.updateVRControllers();
  737. this.render3D(0);
  738. });
  739. });
  740. this.#renderer.vr.enabled = true;
  741. window.addEventListener('keydown', evnt => {
  742. // Esc Key turns VR mode off
  743. if (evnt.code === 'Escape') this.exitVRMode();
  744. });
  745. }
  746. /** @summary Exit VR mode
  747. * @private */
  748. exitVRMode() {
  749. if (!this.#vrDisplay.isPresenting)
  750. return;
  751. this.#renderer.vr.enabled = false;
  752. this.#dolly.remove(this.#camera);
  753. this.#scene.add(this.#camera);
  754. // Restore Camera pose
  755. this.#camera.position.copy(this.#vr_camera_position);
  756. this.#vr_camera_position = undefined;
  757. this.#camera.rotation.copy(this.#vr_camera_rotation);
  758. this.#vr_camera_rotation = undefined;
  759. this.#camera.near = this.#vr_camera_near;
  760. this.#camera.updateProjectionMatrix();
  761. this.#vrDisplay.exitPresent();
  762. }
  763. /** @summary Returns main geometry object */
  764. getGeometry() { return this.getObject(); }
  765. /** @summary Modify visibility of provided node by name */
  766. modifyVisisbility(name, sign) {
  767. if (getNodeKind(this.getGeometry()) !== kindGeo)
  768. return;
  769. if (!name)
  770. return setGeoBit(this.getGeometry().fVolume, geoBITS.kVisThis, (sign === '+'));
  771. let regexp, exact = false;
  772. // arg.node.fVolume
  773. if (name.indexOf('*') < 0) {
  774. regexp = new RegExp('^'+name+'$');
  775. exact = true;
  776. } else {
  777. regexp = new RegExp('^' + name.split('*').join('.*') + '$');
  778. exact = false;
  779. }
  780. this.findNodeWithVolume(regexp, arg => {
  781. setInvisibleAll(arg.node.fVolume, (sign !== '+'));
  782. return exact ? arg : null; // continue search if not exact expression provided
  783. });
  784. }
  785. /** @summary Decode drawing options */
  786. decodeOptions(opt) {
  787. if (!isStr(opt))
  788. opt = '';
  789. if (this.#superimpose && (opt.indexOf('same') === 0))
  790. opt = opt.slice(4);
  791. const res = this.ctrl,
  792. macro = opt.indexOf('macro:');
  793. if (macro >= 0) {
  794. let separ = opt.indexOf(';', macro+6);
  795. if (separ < 0) separ = opt.length;
  796. res.script_name = opt.slice(macro+6, separ);
  797. opt = opt.slice(0, macro) + opt.slice(separ+1);
  798. console.log(`script ${res.script_name} rest ${opt}`);
  799. }
  800. while (true) {
  801. const pp = opt.indexOf('+'), pm = opt.indexOf('-');
  802. if ((pp < 0) && (pm < 0)) break;
  803. let p1 = pp, sign = '+';
  804. if ((p1 < 0) || ((pm >= 0) && (pm < pp))) { p1 = pm; sign = '-'; }
  805. let p2 = p1+1;
  806. const regexp = /[,; .]/;
  807. while ((p2 < opt.length) && !regexp.test(opt[p2]) && (opt[p2] !== '+') && (opt[p2] !== '-')) p2++;
  808. const name = opt.substring(p1+1, p2);
  809. opt = opt.slice(0, p1) + opt.slice(p2);
  810. this.modifyVisisbility(name, sign);
  811. }
  812. const d = new DrawOptions(opt);
  813. if (d.check('MAIN')) res.is_main = true;
  814. if (d.check('DUMMY')) res.dummy = true;
  815. if (d.check('TRACKS')) res.tracks = true; // only for TGeoManager
  816. if (d.check('SHOWTOP')) res.showtop = true; // only for TGeoManager
  817. if (d.check('NO_SCREEN')) res.no_screen = true; // ignore kVisOnScreen bits for visibility
  818. if (d.check('NOINSTANCING')) res.instancing = -1; // disable usage of InstancedMesh
  819. if (d.check('INSTANCING')) res.instancing = 1; // force usage of InstancedMesh
  820. if (d.check('ORTHO_CAMERA')) { res.camera_kind = 'orthoXOY'; res.can_rotate = 0; }
  821. if (d.check('ORTHO', true)) { res.camera_kind = 'ortho' + d.part; res.can_rotate = 0; }
  822. if (d.check('OVERLAY', true)) res.camera_overlay = d.part.toLowerCase();
  823. if (d.check('CAN_ROTATE')) res.can_rotate = true;
  824. if (d.check('PERSPECTIVE')) { res.camera_kind = 'perspective'; res.can_rotate = true; }
  825. if (d.check('PERSP', true)) { res.camera_kind = 'persp' + d.part; res.can_rotate = true; }
  826. if (d.check('MOUSE_CLICK')) res.mouse_click = true;
  827. if (d.check('DEPTHRAY') || d.check('DRAY')) res.depthMethod = 'ray';
  828. if (d.check('DEPTHBOX') || d.check('DBOX')) res.depthMethod = 'box';
  829. if (d.check('DEPTHPNT') || d.check('DPNT')) res.depthMethod = 'pnt';
  830. if (d.check('DEPTHSIZE') || d.check('DSIZE')) res.depthMethod = 'size';
  831. if (d.check('DEPTHDFLT') || d.check('DDFLT')) res.depthMethod = 'dflt';
  832. if (d.check('ZOOM', true)) res.zoom = d.partAsFloat(0, 100) / 100;
  833. if (d.check('ROTY', true)) res.rotatey = d.partAsFloat();
  834. if (d.check('ROTZ', true)) res.rotatez = d.partAsFloat();
  835. if (d.check('PHONG')) res.material_kind = 'phong';
  836. if (d.check('LAMBERT')) res.material_kind = 'lambert';
  837. if (d.check('MATCAP')) res.material_kind = 'matcap';
  838. if (d.check('TOON')) res.material_kind = 'toon';
  839. if (d.check('AMBIENT')) res.light.kind = 'ambient';
  840. const getCamPart = () => {
  841. let neg = 1;
  842. if (d.part[0] === 'N') {
  843. neg = -1;
  844. d.part = d.part.slice(1);
  845. }
  846. return neg * d.partAsFloat();
  847. };
  848. if (d.check('CAMX', true)) res.camx = getCamPart();
  849. if (d.check('CAMY', true)) res.camy = getCamPart();
  850. if (d.check('CAMZ', true)) res.camz = getCamPart();
  851. if (d.check('CAMLX', true)) res.camlx = getCamPart();
  852. if (d.check('CAMLY', true)) res.camly = getCamPart();
  853. if (d.check('CAMLZ', true)) res.camlz = getCamPart();
  854. if (d.check('BLACK')) res.background = '#000000';
  855. if (d.check('WHITE')) res.background = '#FFFFFF';
  856. if (d.check('BKGR_', true)) {
  857. let bckgr = null;
  858. if (d.partAsInt(1) > 0)
  859. bckgr = getColor(d.partAsInt());
  860. else {
  861. for (let col = 0; col < 8; ++col) {
  862. if (getColor(col).toUpperCase() === d.part)
  863. bckgr = getColor(col);
  864. }
  865. }
  866. if (bckgr) res.background = '#' + new THREE.Color(bckgr).getHexString();
  867. }
  868. if (d.check('R3D_', true))
  869. res.Render3D = constants.Render3D.fromString(d.part.toLowerCase());
  870. if (d.check('MORE', true)) res.more = d.partAsInt(0, 2) ?? 2;
  871. if (d.check('ALL')) { res.more = 100; res.vislevel = 99; }
  872. if (d.check('VISLVL', true)) res.vislevel = d.partAsInt();
  873. if (d.check('MAXNODES', true)) res.maxnodes = d.partAsInt();
  874. if (d.check('MAXFACES', true)) res.maxfaces = d.partAsInt();
  875. if (d.check('CONTROLS') || d.check('CTRL')) res.show_controls = true;
  876. if (d.check('CLIPXYZ')) res.clip[0].enabled = res.clip[1].enabled = res.clip[2].enabled = true;
  877. if (d.check('CLIPX')) res.clip[0].enabled = true;
  878. if (d.check('CLIPY')) res.clip[1].enabled = true;
  879. if (d.check('CLIPZ')) res.clip[2].enabled = true;
  880. if (d.check('CLIP')) res.clip[0].enabled = res.clip[1].enabled = res.clip[2].enabled = true;
  881. if (d.check('PROJX', true)) { res.project = 'x'; if (d.partAsInt(1) > 0) res.projectPos = d.partAsInt(); res.can_rotate = 0; }
  882. if (d.check('PROJY', true)) { res.project = 'y'; if (d.partAsInt(1) > 0) res.projectPos = d.partAsInt(); res.can_rotate = 0; }
  883. if (d.check('PROJZ', true)) { res.project = 'z'; if (d.partAsInt(1) > 0) res.projectPos = d.partAsInt(); res.can_rotate = 0; }
  884. if (d.check('DFLT_COLORS') || d.check('DFLT')) res.dflt_colors = true;
  885. d.check('SSAO'); // deprecated
  886. if (d.check('NOBLOOM')) res.highlight_bloom = false;
  887. if (d.check('BLOOM')) res.highlight_bloom = true;
  888. if (d.check('OUTLINE')) res.outline = true;
  889. if (d.check('NOWORKER')) res.use_worker = -1;
  890. if (d.check('WORKER')) res.use_worker = 1;
  891. if (d.check('NOFOG')) res.use_fog = false;
  892. if (d.check('FOG')) res.use_fog = true;
  893. if (d.check('NOHIGHLIGHT') || d.check('NOHIGH')) res.highlight_scene = res.highlight = false;
  894. if (d.check('HIGHLIGHT')) res.highlight_scene = res.highlight = true;
  895. if (d.check('HSCENEONLY')) { res.highlight_scene = true; res.highlight = false; }
  896. if (d.check('NOHSCENE')) res.highlight_scene = false;
  897. if (d.check('HSCENE')) res.highlight_scene = true;
  898. if (d.check('WIREFRAME') || d.check('WIRE')) res.wireframe = true;
  899. if (d.check('ROTATE')) res.rotate = true;
  900. if (d.check('INVX') || d.check('INVERTX')) res.scale.x = -1;
  901. if (d.check('INVY') || d.check('INVERTY')) res.scale.y = -1;
  902. if (d.check('INVZ') || d.check('INVERTZ')) res.scale.z = -1;
  903. if (d.check('COUNT')) res._count = true;
  904. if (d.check('TRANSP', true))
  905. res.transparency = d.partAsInt(0, 100)/100;
  906. if (d.check('OPACITY', true))
  907. res.transparency = 1 - d.partAsInt(0, 100)/100;
  908. if (d.check('AXISCENTER') || d.check('AXISC') || d.check('AC')) res._axis = 2;
  909. if (d.check('AXIS') || d.check('A')) res._axis = 1;
  910. if (d.check('TRR', true)) res.trans_radial = d.partAsInt()/100;
  911. if (d.check('TRZ', true)) res.trans_z = d.partAsInt()/100;
  912. if (d.check('W')) res.wireframe = true;
  913. if (d.check('Y')) res._yup = true;
  914. if (d.check('Z')) res._yup = false;
  915. // when drawing geometry without TCanvas, yup = true by default
  916. if (res._yup === undefined)
  917. res._yup = this.getCanvSvg().empty();
  918. // let reuse for storing origin options
  919. this.setOptions(res, true);
  920. }
  921. /** @summary Activate specified items in the browser */
  922. activateInBrowser(names, force) {
  923. if (isStr(names))
  924. names = [names];
  925. const h = this.getHPainter();
  926. if (h) {
  927. // show browser if it not visible
  928. h.activateItems(names, force);
  929. // if highlight in the browser disabled, suppress in few seconds
  930. if (!this.ctrl.update_browser)
  931. setTimeout(() => h.activateItems([]), 2000);
  932. }
  933. }
  934. /** @summary Return ClonedNodes instance from the painter */
  935. getClones() { return this.#clones; }
  936. /** @summary Assign clones, created outside.
  937. * @desc Used by ROOT geometry painter, where clones are handled by the server */
  938. assignClones(clones, owner = true) {
  939. if (this.#clones_owner)
  940. this.#clones?.cleanup(this.#draw_nodes, this.#build_shapes);
  941. this.#draw_nodes = undefined;
  942. this.#build_shapes = undefined;
  943. this.#drawing_ready = false;
  944. this.#clones = clones;
  945. this.#clones_owner = owner;
  946. }
  947. /** @summary method used to check matrix calculations performance with current three.js model */
  948. testMatrixes() {
  949. let errcnt = 0, totalcnt = 0, totalmax = 0;
  950. const arg = {
  951. domatrix: true,
  952. func: (/* node */) => {
  953. let m2 = this.getmatrix();
  954. const entry = this.copyStack(),
  955. mesh = this.#clones.createObject3D(entry.stack, this.#toplevel, 'mesh');
  956. if (!mesh) return true;
  957. totalcnt++;
  958. const m1 = mesh.matrixWorld;
  959. if (m1.equals(m2)) return true;
  960. if ((m1.determinant() > 0) && (m2.determinant() < -0.9)) {
  961. const flip = new THREE.Vector3(1, 1, -1);
  962. m2 = m2.clone().scale(flip);
  963. if (m1.equals(m2)) return true;
  964. }
  965. let max = 0;
  966. for (let k = 0; k < 16; ++k)
  967. max = Math.max(max, Math.abs(m1.elements[k] - m2.elements[k]));
  968. totalmax = Math.max(max, totalmax);
  969. if (max < 1e-4) return true;
  970. console.log(`${this.#clones.resolveStack(entry.stack).name} maxdiff ${max} determ ${m1.determinant()} ${m2.determinant()}`);
  971. errcnt++;
  972. return false;
  973. }
  974. }, tm1 = new Date().getTime();
  975. this.#clones.scanVisible(arg);
  976. const tm2 = new Date().getTime();
  977. console.log(`Compare matrixes total ${totalcnt} errors ${errcnt} takes ${tm2-tm1} maxdiff ${totalmax}`);
  978. }
  979. /** @summary Fill context menu */
  980. fillContextMenu(menu) {
  981. menu.header('Draw options');
  982. menu.addchk(this.ctrl.update_browser, 'Browser update', () => {
  983. this.ctrl.update_browser = !this.ctrl.update_browser;
  984. if (!this.ctrl.update_browser) this.activateInBrowser([]);
  985. });
  986. menu.addchk(this.ctrl.show_controls, 'Show Controls', () => this.showControlGui('toggle'));
  987. menu.sub('Show axes', () => this.setAxesDraw('toggle'));
  988. menu.addchk(this.ctrl._axis === 0, 'off', 0, arg => this.setAxesDraw(parseInt(arg)));
  989. menu.addchk(this.ctrl._axis === 1, 'side', 1, arg => this.setAxesDraw(parseInt(arg)));
  990. menu.addchk(this.ctrl._axis === 2, 'center', 2, arg => this.setAxesDraw(parseInt(arg)));
  991. menu.endsub();
  992. if (this.#geo_manager)
  993. menu.addchk(this.ctrl.showtop, 'Show top volume', () => this.setShowTop(!this.ctrl.showtop));
  994. menu.addchk(this.ctrl.wireframe, 'Wire frame', () => this.toggleWireFrame());
  995. if (!this.getCanvPainter())
  996. menu.addchk(this.isTooltipAllowed(), 'Show tooltips', () => this.setTooltipAllowed('toggle'));
  997. menu.sub('Highlight');
  998. menu.addchk(!this.ctrl.highlight, 'Off', () => {
  999. this.ctrl.highlight = false;
  1000. this.changedHighlight();
  1001. });
  1002. menu.addchk(this.ctrl.highlight && !this.ctrl.highlight_bloom, 'Normal', () => {
  1003. this.ctrl.highlight = true;
  1004. this.ctrl.highlight_bloom = false;
  1005. this.changedHighlight();
  1006. });
  1007. menu.addchk(this.ctrl.highlight && this.ctrl.highlight_bloom, 'Bloom', () => {
  1008. this.ctrl.highlight = true;
  1009. this.ctrl.highlight_bloom = true;
  1010. this.changedHighlight();
  1011. });
  1012. menu.separator();
  1013. menu.addchk(this.ctrl.highlight_scene, 'Scene', flag => {
  1014. this.ctrl.highlight_scene = flag;
  1015. this.changedHighlight();
  1016. });
  1017. menu.endsub();
  1018. menu.sub('Camera');
  1019. menu.add('Reset position', () => this.focusCamera());
  1020. if (!this.ctrl.project)
  1021. menu.addchk(this.ctrl.rotate, 'Autorotate', () => this.setAutoRotate(!this.ctrl.rotate));
  1022. if (!this.#geom_viewer) {
  1023. menu.addchk(this.canRotateCamera(), 'Can rotate', () => this.changeCanRotate(!this.ctrl.can_rotate));
  1024. menu.add('Get position', () => menu.info('Position (as url)', '&opt=' + this.produceCameraUrl()));
  1025. if (!this.isOrthoCamera()) {
  1026. menu.add('Absolute position', () => {
  1027. const url = this.produceCameraUrl(true), p = url.indexOf('camlx');
  1028. menu.info('Position (as url)', '&opt=' + ((p < 0) ? url : url.slice(0, p) + '\n' + url.slice(p)));
  1029. });
  1030. }
  1031. menu.sub('Kind');
  1032. this.ctrl.cameraKindItems.forEach(item =>
  1033. menu.addchk(this.ctrl.camera_kind === item.value, item.name, item.value, arg => {
  1034. this.ctrl.camera_kind = arg;
  1035. this.changeCamera();
  1036. }));
  1037. menu.endsub();
  1038. if (this.isOrthoCamera()) {
  1039. menu.sub('Overlay');
  1040. this.ctrl.cameraOverlayItems.forEach(item =>
  1041. menu.addchk(this.ctrl.camera_overlay === item.value, item.name, item.value, arg => {
  1042. this.ctrl.camera_overlay = arg;
  1043. this.changeCamera();
  1044. }));
  1045. menu.endsub();
  1046. }
  1047. }
  1048. menu.endsub();
  1049. menu.addchk(this.ctrl.select_in_view, 'Select in view', () => {
  1050. this.ctrl.select_in_view = !this.ctrl.select_in_view;
  1051. if (this.ctrl.select_in_view) this.startDrawGeometry();
  1052. });
  1053. }
  1054. /** @summary Method used to set transparency for all geometrical shapes
  1055. * @param {number|Function} transparency - one could provide function
  1056. * @param {boolean} [skip_render] - if specified, do not perform rendering */
  1057. changedGlobalTransparency(transparency) {
  1058. const func = isFunc(transparency) ? transparency : null;
  1059. if (func || (transparency === undefined))
  1060. transparency = this.ctrl.transparency;
  1061. this.#toplevel?.traverse(node => {
  1062. // ignore all kind of extra elements
  1063. if (node?.material?.inherentOpacity === undefined)
  1064. return;
  1065. const t = func ? func(node) : undefined;
  1066. if (t !== undefined)
  1067. node.material.opacity = 1 - t;
  1068. else
  1069. node.material.opacity = Math.min(1 - (transparency || 0), node.material.inherentOpacity);
  1070. node.material.depthWrite = node.material.opacity === 1;
  1071. node.material.transparent = node.material.opacity < 1;
  1072. });
  1073. this.render3D();
  1074. }
  1075. /** @summary Method used to interactively change material kinds */
  1076. changedMaterial() {
  1077. this.#toplevel?.traverse(node => {
  1078. // ignore all kind of extra elements
  1079. if (node.material?.inherentArgs !== undefined)
  1080. node.material = createMaterial(this.ctrl, node.material.inherentArgs);
  1081. });
  1082. this.render3D(-1);
  1083. }
  1084. /** @summary Change for all materials that property */
  1085. changeMaterialProperty(name) {
  1086. const value = this.ctrl[name];
  1087. if (value === undefined)
  1088. return console.error('No property ', name);
  1089. this.#toplevel?.traverse(node => {
  1090. // ignore all kind of extra elements
  1091. if (node.material?.inherentArgs === undefined) return;
  1092. if (node.material[name] !== undefined) {
  1093. node.material[name] = value;
  1094. node.material.needsUpdate = true;
  1095. }
  1096. });
  1097. this.render3D();
  1098. }
  1099. /** @summary Reset transformation */
  1100. resetTransformation() {
  1101. this.changedTransformation('reset');
  1102. }
  1103. /** @summary Method should be called when transformation parameters were changed */
  1104. changedTransformation(arg) {
  1105. if (!this.#toplevel) return;
  1106. const ctrl = this.ctrl,
  1107. translation = new THREE.Matrix4(),
  1108. vect2 = new THREE.Vector3();
  1109. if (arg === 'reset')
  1110. ctrl.trans_z = ctrl.trans_radial = 0;
  1111. this.#toplevel.traverse(mesh => {
  1112. if (mesh.stack !== undefined) {
  1113. const node = mesh.parent;
  1114. if (arg === 'reset') {
  1115. if (node.matrix0) {
  1116. node.matrix.copy(node.matrix0);
  1117. node.matrix.decompose(node.position, node.quaternion, node.scale);
  1118. node.matrixWorldNeedsUpdate = true;
  1119. }
  1120. delete node.matrix0;
  1121. delete node.vect0;
  1122. delete node.vect1;
  1123. delete node.minvert;
  1124. return;
  1125. }
  1126. if (node.vect0 === undefined) {
  1127. node.matrix0 = node.matrix.clone();
  1128. node.minvert = new THREE.Matrix4().copy(node.matrixWorld).invert();
  1129. const box3 = getBoundingBox(mesh, null, true),
  1130. signz = mesh._flippedMesh ? -1 : 1;
  1131. // real center of mesh in local coordinates
  1132. 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);
  1133. node.vect1 = new THREE.Vector3(0, 0, 0).applyMatrix4(node.minvert);
  1134. }
  1135. 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);
  1136. node.matrix.multiplyMatrices(node.matrix0, translation.makeTranslation(vect2.x, vect2.y, vect2.z));
  1137. node.matrix.decompose(node.position, node.quaternion, node.scale);
  1138. node.matrixWorldNeedsUpdate = true;
  1139. } else if (mesh.stacks !== undefined) {
  1140. mesh.instanceMatrix.needsUpdate = true;
  1141. if (arg === 'reset') {
  1142. mesh.trans?.forEach((item, i) => {
  1143. mesh.setMatrixAt(i, item.matrix0);
  1144. });
  1145. delete mesh.trans;
  1146. return;
  1147. }
  1148. if (mesh.trans === undefined) {
  1149. mesh.trans = new Array(mesh.count);
  1150. mesh.geometry.computeBoundingBox();
  1151. for (let i = 0; i < mesh.count; i++) {
  1152. const item = {
  1153. matrix0: new THREE.Matrix4(),
  1154. minvert: new THREE.Matrix4()
  1155. };
  1156. mesh.trans[i] = item;
  1157. mesh.getMatrixAt(i, item.matrix0);
  1158. item.minvert.copy(item.matrix0).invert();
  1159. const box3 = new THREE.Box3().copy(mesh.geometry.boundingBox).applyMatrix4(item.matrix0);
  1160. 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);
  1161. item.vect1 = new THREE.Vector3(0, 0, 0).applyMatrix4(item.minvert);
  1162. }
  1163. }
  1164. const mm = new THREE.Matrix4();
  1165. mesh.trans?.forEach((item, i) => {
  1166. 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);
  1167. mm.multiplyMatrices(item.matrix0, translation.makeTranslation(vect2.x, vect2.y, vect2.z));
  1168. mesh.setMatrixAt(i, mm);
  1169. });
  1170. }
  1171. });
  1172. this.#toplevel.updateMatrixWorld();
  1173. // axes drawing always triggers rendering
  1174. if (arg !== 'norender')
  1175. this.drawAxesAndOverlay();
  1176. }
  1177. /** @summary Should be called when auto rotate property changed */
  1178. changedAutoRotate() {
  1179. this.autorotate(2.5);
  1180. }
  1181. /** @summary Method should be called when changing axes drawing */
  1182. changedAxes() {
  1183. if (isStr(this.ctrl._axis))
  1184. this.ctrl._axis = parseInt(this.ctrl._axis);
  1185. this.drawAxesAndOverlay();
  1186. }
  1187. /** @summary Method should be called to change background color */
  1188. changedBackground(val) {
  1189. if (val !== undefined)
  1190. this.ctrl.background = val;
  1191. this.#scene.background = new THREE.Color(this.ctrl.background);
  1192. this.#renderer.setClearColor(this.#scene.background, 1);
  1193. this.render3D(0);
  1194. if (this.#toolbar) {
  1195. const bkgr = new THREE.Color(this.ctrl.background);
  1196. this.#toolbar.changeBrightness((bkgr.r + bkgr.g + bkgr.b) < 1);
  1197. }
  1198. }
  1199. /** @summary Display control GUI */
  1200. showControlGui(on) {
  1201. // while complete geo drawing can be removed until dat is loaded - just check and ignore callback
  1202. if (!this.ctrl) return;
  1203. if (on === 'toggle')
  1204. on = !this.#gui;
  1205. else if (on === undefined)
  1206. on = this.ctrl.show_controls;
  1207. this.ctrl.show_controls = on;
  1208. if (this.#gui) {
  1209. if (!on) {
  1210. this.#gui.destroy();
  1211. this.#gui = undefined;
  1212. }
  1213. return;
  1214. }
  1215. if (!on || !this.#renderer)
  1216. return;
  1217. const main = this.selectDom();
  1218. if (main.style('position') === 'static')
  1219. main.style('position', 'relative');
  1220. this.#gui = new GUI({ container: main.node(), closeFolders: true, width: Math.min(300, this.#scene_width / 2),
  1221. title: 'Settings' });
  1222. const dom = this.#gui.domElement;
  1223. dom.style.position = 'absolute';
  1224. dom.style.top = 0;
  1225. dom.style.right = 0;
  1226. this.#gui.painter = this;
  1227. const makeLil = items => {
  1228. const lil = {};
  1229. items.forEach(i => { lil[i.name] = i.value; });
  1230. return lil;
  1231. };
  1232. if (!this.ctrl.project) {
  1233. const selection = this.#gui.addFolder('Selection');
  1234. if (!this.ctrl.maxnodes)
  1235. this.ctrl.maxnodes = this.#clones?.getMaxVisNodes() ?? 10000;
  1236. if (!this.ctrl.vislevel)
  1237. this.ctrl.vislevel = this.#clones?.getVisLevel() ?? 3;
  1238. if (!this.ctrl.maxfaces)
  1239. this.ctrl.maxfaces = 200000 * this.ctrl.more;
  1240. this.ctrl.more = 1;
  1241. selection.add(this.ctrl, 'vislevel', 1, 99, 1)
  1242. .name('Visibility level')
  1243. .listen().onChange(() => this.startRedraw(500));
  1244. selection.add(this.ctrl, 'maxnodes', 0, 500000, 1000)
  1245. .name('Visible nodes')
  1246. .listen().onChange(() => this.startRedraw(500));
  1247. selection.add(this.ctrl, 'maxfaces', 0, 5000000, 100000)
  1248. .name('Max faces')
  1249. .listen().onChange(() => this.startRedraw(500));
  1250. }
  1251. if (this.ctrl.project) {
  1252. const bound = this.getGeomBoundingBox(this.getProjectionSource(), 0.01),
  1253. axis = this.ctrl.project;
  1254. if (this.ctrl.projectPos === undefined)
  1255. this.ctrl.projectPos = (bound.min[axis] + bound.max[axis])/2;
  1256. this.#gui.add(this.ctrl, 'projectPos', bound.min[axis], bound.max[axis])
  1257. .name(axis.toUpperCase() + ' projection')
  1258. .onChange(() => this.startDrawGeometry());
  1259. } else {
  1260. // Clipping Options
  1261. const clipFolder = this.#gui.addFolder('Clipping');
  1262. for (let naxis = 0; naxis < 3; ++naxis) {
  1263. const cc = this.ctrl.clip[naxis],
  1264. axisC = cc.name.toUpperCase();
  1265. clipFolder.add(cc, 'enabled')
  1266. .name('Enable ' + axisC)
  1267. .listen().onChange(() => this.changedClipping(-1));
  1268. clipFolder.add(cc, 'value', cc.min, cc.max, cc.step)
  1269. .name(axisC + ' position')
  1270. .listen().onChange(() => this.changedClipping(naxis));
  1271. }
  1272. clipFolder.add(this.ctrl, 'clipIntersect').name('Clip intersection')
  1273. .onChange(() => this.changedClipping(-1));
  1274. clipFolder.add(this.ctrl, 'clipVisualize').name('Visualize')
  1275. .onChange(() => this.changedClipping(-1));
  1276. }
  1277. // Scene Options
  1278. const scene = this.#gui.addFolder('Scene');
  1279. // following items used in handlers and cannot be constants
  1280. let light_pnts = null, strength = null, hcolor = null, overlay = null;
  1281. scene.add(this.ctrl.light, 'kind', makeLil(this.ctrl.lightKindItems)).name('Light')
  1282. .listen().onChange(() => {
  1283. light_pnts.show(this.ctrl.light.kind === 'mix' || this.ctrl.light.kind === 'points');
  1284. this.changedLight();
  1285. });
  1286. this.ctrl.light._pnts = this.ctrl.light.specular ? 0 : (this.ctrl.light.front ? 1 : 2);
  1287. light_pnts = scene.add(this.ctrl.light, '_pnts', { specular: 0, front: 1, box: 2 })
  1288. .name('Positions')
  1289. .show(this.ctrl.light.kind === 'mix' || this.ctrl.light.kind === 'points')
  1290. .onChange(v => {
  1291. this.ctrl.light.specular = (v === 0);
  1292. this.ctrl.light.front = (v === 1);
  1293. this.ctrl.light.top = this.ctrl.light.bottom = this.ctrl.light.left = this.ctrl.light.right = (v === 2);
  1294. this.changedLight();
  1295. });
  1296. scene.add(this.ctrl.light, 'power', 0, 10, 0.01).name('Power')
  1297. .listen().onChange(() => this.changedLight());
  1298. scene.add(this.ctrl, 'use_fog').name('Fog')
  1299. .listen().onChange(() => this.changedUseFog());
  1300. // Appearance Options
  1301. const appearance = this.#gui.addFolder('Appearance');
  1302. this.ctrl._highlight = !this.ctrl.highlight ? 0 : this.ctrl.highlight_bloom ? 2 : 1;
  1303. appearance.add(this.ctrl, '_highlight', { none: 0, normal: 1, bloom: 2 }).name('Highlight Selection')
  1304. .listen().onChange(() => {
  1305. this.changedHighlight(this.ctrl._highlight);
  1306. strength.show(this.ctrl._highlight === 2);
  1307. hcolor.show(this.ctrl._highlight === 1);
  1308. });
  1309. hcolor = appearance.addColor(this.ctrl, 'highlight_color').name('Hightlight color')
  1310. .show(this.ctrl._highlight === 1);
  1311. strength = appearance.add(this.ctrl, 'bloom_strength', 0, 3).name('Bloom strength')
  1312. .listen().onChange(() => this.changedHighlight())
  1313. .show(this.ctrl._highlight === 2);
  1314. appearance.addColor(this.ctrl, 'background').name('Background')
  1315. .onChange(col => this.changedBackground(col));
  1316. appearance.add(this.ctrl, '_axis', { none: 0, side: 1, center: 2 }).name('Axes')
  1317. .onChange(() => this.changedAxes());
  1318. if (!this.ctrl.project) {
  1319. appearance.add(this.ctrl, 'rotate').name('Autorotate')
  1320. .listen().onChange(() => this.changedAutoRotate());
  1321. }
  1322. // Material options
  1323. const material = this.#gui.addFolder('Material');
  1324. let material_props = [];
  1325. const addMaterialProp = () => {
  1326. material_props.forEach(f => f.destroy());
  1327. material_props = [];
  1328. const props = this.ctrl.getMaterialCfg()?.props;
  1329. if (!props) return;
  1330. props.forEach(prop => {
  1331. const f = material.add(this.ctrl, prop.name, prop.min, prop.max, prop.step).onChange(() => {
  1332. this.changeMaterialProperty(prop.name);
  1333. });
  1334. material_props.push(f);
  1335. });
  1336. };
  1337. material.add(this.ctrl, 'material_kind', makeLil(this.ctrl.materialKinds)).name('Kind')
  1338. .listen().onChange(() => {
  1339. addMaterialProp();
  1340. this.ensureBloom(false);
  1341. this.changedMaterial();
  1342. this.changedHighlight(); // for some materials bloom will not work
  1343. });
  1344. material.add(this.ctrl, 'transparency', 0, 1, 0.001).name('Transparency')
  1345. .listen().onChange(value => this.changedGlobalTransparency(value));
  1346. material.add(this.ctrl, 'wireframe').name('Wireframe')
  1347. .listen().onChange(() => this.changedWireFrame());
  1348. material.add(this, 'showMaterialDocu').name('Docu from threejs.org');
  1349. addMaterialProp();
  1350. // Camera options
  1351. const camera = this.#gui.addFolder('Camera');
  1352. camera.add(this.ctrl, 'camera_kind', makeLil(this.ctrl.cameraKindItems))
  1353. .name('Kind').listen().onChange(() => {
  1354. overlay.show(this.ctrl.camera_kind.indexOf('ortho') === 0);
  1355. this.changeCamera();
  1356. });
  1357. camera.add(this.ctrl, 'can_rotate').name('Can rotate')
  1358. .listen().onChange(() => this.changeCanRotate());
  1359. camera.add(this, 'focusCamera').name('Reset position');
  1360. overlay = camera.add(this.ctrl, 'camera_overlay', makeLil(this.ctrl.cameraOverlayItems))
  1361. .name('Overlay').listen().onChange(() => this.changeCamera())
  1362. .show(this.ctrl.camera_kind.indexOf('ortho') === 0);
  1363. // Advanced Options
  1364. if (this.#webgl) {
  1365. const advanced = this.#gui.addFolder('Advanced');
  1366. advanced.add(this.ctrl, 'depthTest').name('Depth test')
  1367. .listen().onChange(() => this.changedDepthTest());
  1368. advanced.add(this.ctrl, 'depthMethod', makeLil(this.ctrl.depthMethodItems))
  1369. .name('Rendering order')
  1370. .onChange(method => this.changedDepthMethod(method));
  1371. advanced.add(this, 'resetAdvanced').name('Reset');
  1372. }
  1373. // Transformation Options
  1374. if (!this.ctrl.project) {
  1375. const transform = this.#gui.addFolder('Transform');
  1376. transform.add(this.ctrl, 'trans_z', 0.0, 3.0, 0.01)
  1377. .name('Z axis')
  1378. .listen().onChange(() => this.changedTransformation());
  1379. transform.add(this.ctrl, 'trans_radial', 0.0, 3.0, 0.01)
  1380. .name('Radial')
  1381. .listen().onChange(() => this.changedTransformation());
  1382. transform.add(this, 'resetTransformation').name('Reset');
  1383. if (this.ctrl.trans_z || this.ctrl.trans_radial) transform.open();
  1384. }
  1385. }
  1386. /** @summary show material documentation from https://threejs.org */
  1387. showMaterialDocu() {
  1388. const cfg = this.ctrl.getMaterialCfg();
  1389. if (cfg?.name && typeof window !== 'undefined')
  1390. window.open('https://threejs.org/docs/index.html#api/en/materials/' + cfg.name, '_blank');
  1391. }
  1392. /** @summary Should be called when configuration of highlight is changed */
  1393. changedHighlight(arg) {
  1394. if (arg !== undefined) {
  1395. this.ctrl.highlight = Boolean(arg);
  1396. if (this.ctrl.highlight)
  1397. this.ctrl.highlight_bloom = (arg === 2);
  1398. }
  1399. this.ensureBloom();
  1400. if (!this.ctrl.highlight)
  1401. this.highlightMesh(null);
  1402. this.getSubordinates()?.forEach(p => {
  1403. p.ctrl.highlight = this.ctrl.highlight;
  1404. p.ctrl.highlight_bloom = this.ctrl.highlight_bloom;
  1405. p.ctrl.bloom_strength = this.ctrl.bloom_strength;
  1406. p.changedHighlight();
  1407. });
  1408. }
  1409. /** @summary Handle change of can rotate */
  1410. changeCanRotate(on) {
  1411. if (on !== undefined)
  1412. this.ctrl.can_rotate = on;
  1413. if (this.#controls)
  1414. this.#controls.enableRotate = this.ctrl.can_rotate;
  1415. }
  1416. /** @summary Change use fog property */
  1417. changedUseFog() {
  1418. this.#scene.fog = this.ctrl.use_fog ? this.#fog : null;
  1419. this.render3D();
  1420. }
  1421. /** @summary Handle change of camera kind */
  1422. changeCamera() {
  1423. // force control recreation
  1424. if (this.#controls) {
  1425. this.#controls.cleanup();
  1426. this.#controls = undefined;
  1427. }
  1428. this.ensureBloom(false);
  1429. // recreate camera
  1430. this.createCamera();
  1431. this.createSpecialEffects();
  1432. // set proper position
  1433. this.adjustCameraPosition(true);
  1434. // this.drawOverlay();
  1435. this.addOrbitControls();
  1436. this.render3D();
  1437. // this.#scene_size = undefined; // ensure reassign of camera position
  1438. // this.#first_drawing = true;
  1439. // this.startDrawGeometry(true);
  1440. }
  1441. /** @summary create bloom effect */
  1442. ensureBloom(on) {
  1443. if (on === undefined) {
  1444. if (this.ctrl.highlight_bloom === 0)
  1445. this.ctrl.highlight_bloom = this.#webgl && !browser.android;
  1446. on = this.ctrl.highlight_bloom && this.ctrl.getMaterialCfg()?.emissive;
  1447. }
  1448. if (on && !this.#bloomComposer) {
  1449. this.#camera.layers.enable(_BLOOM_SCENE);
  1450. this.#bloomComposer = new THREE.EffectComposer(this.#renderer);
  1451. this.#bloomComposer.addPass(new THREE.RenderPass(this.#scene, this.#camera));
  1452. const pass = new THREE.UnrealBloomPass(new THREE.Vector2(this.#scene_width, this.#scene_height), 1.5, 0.4, 0.85);
  1453. pass.threshold = 0;
  1454. pass.radius = 0;
  1455. pass.renderToScreen = true;
  1456. this.#bloomComposer.addPass(pass);
  1457. this.#renderer.autoClear = false;
  1458. } else if (!on && this.#bloomComposer) {
  1459. this.#bloomComposer.dispose();
  1460. this.#bloomComposer = undefined;
  1461. if (this.#renderer)
  1462. this.#renderer.autoClear = true;
  1463. this.#camera?.layers.disable(_BLOOM_SCENE);
  1464. this.#camera?.layers.set(_ENTIRE_SCENE);
  1465. }
  1466. if (this.#bloomComposer?.passes)
  1467. this.#bloomComposer.passes[1].strength = this.ctrl.bloom_strength;
  1468. }
  1469. /** @summary Show context menu for orbit control
  1470. * @private */
  1471. orbitContext(evnt, intersects) {
  1472. createMenu(evnt, this).then(menu => {
  1473. let numitems = 0, numnodes = 0, cnt = 0;
  1474. if (intersects) {
  1475. for (let n = 0; n < intersects.length; ++n) {
  1476. if (getIntersectStack(intersects[n])) numnodes++;
  1477. if (intersects[n].geo_name) numitems++;
  1478. }
  1479. }
  1480. if (numnodes + numitems === 0)
  1481. this.fillContextMenu(menu);
  1482. else {
  1483. const many = (numnodes + numitems) > 1;
  1484. if (many) menu.header((numitems > 0) ? 'Items' : 'Nodes');
  1485. for (let n = 0; n < intersects.length; ++n) {
  1486. const obj = intersects[n].object,
  1487. stack = getIntersectStack(intersects[n]);
  1488. let name, itemname, hdr;
  1489. if (obj.geo_name) {
  1490. itemname = obj.geo_name;
  1491. if (itemname.indexOf('<prnt>') === 0)
  1492. itemname = (this.getItemName() || 'top') + itemname.slice(6);
  1493. name = itemname.slice(itemname.lastIndexOf('/')+1);
  1494. if (!name) name = itemname;
  1495. hdr = name;
  1496. } else if (stack) {
  1497. name = this.#clones.getStackName(stack);
  1498. itemname = this.getStackFullName(stack);
  1499. hdr = this.getItemName();
  1500. if (name.indexOf('Nodes/') === 0)
  1501. hdr = name.slice(6);
  1502. else if (name)
  1503. hdr = name;
  1504. else if (!hdr)
  1505. hdr = 'header';
  1506. } else
  1507. continue;
  1508. cnt++;
  1509. menu.add((many ? 'sub:' : 'header:') + hdr, itemname, arg => this.activateInBrowser([arg], true));
  1510. menu.add('Browse', itemname, arg => this.activateInBrowser([arg], true));
  1511. if (this.getHPainter())
  1512. menu.add('Inspect', itemname, arg => this.getHPainter().display(arg, kInspect));
  1513. if (isFunc(this.hidePhysicalNode)) {
  1514. menu.add('Hide', itemname, arg => this.hidePhysicalNode([arg]));
  1515. if (cnt > 1) {
  1516. menu.add('Hide all before', n, indx => {
  1517. const items = [];
  1518. for (let i = 0; i < indx; ++i) {
  1519. const stack2 = getIntersectStack(intersects[i]);
  1520. if (stack2) items.push(this.getStackFullName(stack2));
  1521. }
  1522. this.hidePhysicalNode(items);
  1523. });
  1524. }
  1525. } else if (obj.geo_name) {
  1526. menu.add('Hide', n, indx => {
  1527. const mesh = intersects[indx].object;
  1528. mesh.visible = false; // just disable mesh
  1529. if (mesh.geo_object) mesh.geo_object.$hidden_via_menu = true; // and hide object for further redraw
  1530. menu.painter.render3D();
  1531. }, 'Hide this physical node');
  1532. if (many) menu.endsub();
  1533. continue;
  1534. }
  1535. const wireframe = this.accessObjectWireFrame(obj);
  1536. if (wireframe !== undefined) {
  1537. menu.addchk(wireframe, 'Wireframe', n, indx => {
  1538. const m = intersects[indx].object.material;
  1539. m.wireframe = !m.wireframe;
  1540. this.render3D();
  1541. }, 'Toggle wireframe mode for the node');
  1542. }
  1543. if (cnt > 1) {
  1544. menu.add('Manifest', n, indx => {
  1545. if (this.#last_manifest)
  1546. this.#last_manifest.wireframe = !this.#last_manifest.wireframe;
  1547. this.#last_hidden?.forEach(obj2 => { obj2.visible = true; });
  1548. this.#last_hidden = [];
  1549. for (let i = 0; i < indx; ++i)
  1550. this.#last_hidden.push(intersects[i].object);
  1551. this.#last_hidden.forEach(obj2 => { obj2.visible = false; });
  1552. this.#last_manifest = intersects[indx].object.material;
  1553. this.#last_manifest.wireframe = !this.#last_manifest.wireframe;
  1554. this.render3D();
  1555. }, 'Manifest selected node');
  1556. }
  1557. menu.add('Focus', n, indx => {
  1558. this.focusCamera(intersects[indx].object);
  1559. });
  1560. if (!this.#geom_viewer) {
  1561. menu.add('Hide', n, indx => {
  1562. const resolve = this.#clones.resolveStack(intersects[indx].object.stack);
  1563. if (resolve.obj && (resolve.node.kind === kindGeo) && resolve.obj.fVolume) {
  1564. setGeoBit(resolve.obj.fVolume, geoBITS.kVisThis, false);
  1565. updateBrowserIcons(resolve.obj.fVolume, this.getHPainter());
  1566. } else if (resolve.obj && (resolve.node.kind === kindEve)) {
  1567. resolve.obj.fRnrSelf = false;
  1568. updateBrowserIcons(resolve.obj, this.getHPainter());
  1569. }
  1570. this.testGeomChanges();// while many volumes may disappear, recheck all of them
  1571. }, 'Hide all logical nodes of that kind');
  1572. menu.add('Hide only this', n, indx => {
  1573. this.#clones.setPhysNodeVisibility(getIntersectStack(intersects[indx]), false);
  1574. this.testGeomChanges();
  1575. }, 'Hide only this physical node');
  1576. if (n > 1) {
  1577. menu.add('Hide all before', n, indx => {
  1578. for (let k = 0; k < indx; ++k)
  1579. this.#clones.setPhysNodeVisibility(getIntersectStack(intersects[k]), false);
  1580. this.testGeomChanges();
  1581. }, 'Hide all physical nodes before that');
  1582. }
  1583. }
  1584. if (many) menu.endsub();
  1585. }
  1586. }
  1587. menu.show();
  1588. });
  1589. }
  1590. /** @summary Filter some objects from three.js intersects array */
  1591. filterIntersects(intersects) {
  1592. if (!intersects?.length)
  1593. return intersects;
  1594. // check redirection
  1595. for (let n = 0; n < intersects.length; ++n) {
  1596. if (intersects[n].object.geo_highlight)
  1597. intersects[n].object = intersects[n].object.geo_highlight;
  1598. }
  1599. // remove all elements without stack - indicator that this is geometry object
  1600. // also remove all objects which are mostly transparent
  1601. for (let n = intersects.length - 1; n >= 0; --n) {
  1602. const obj = intersects[n].object;
  1603. let unique = obj.visible && (getIntersectStack(intersects[n]) || (obj.geo_name !== undefined));
  1604. if (unique && obj.material && (obj.material.opacity !== undefined))
  1605. unique = (obj.material.opacity >= 0.1);
  1606. if (obj.jsroot_special) unique = false;
  1607. for (let k = 0; (k < n) && unique; ++k) {
  1608. if (intersects[k].object === obj)
  1609. unique = false;
  1610. }
  1611. if (!unique) intersects.splice(n, 1);
  1612. }
  1613. const clip = this.ctrl.clip;
  1614. if (clip[0].enabled || clip[1].enabled || clip[2].enabled) {
  1615. const clippedIntersects = [];
  1616. for (let i = 0; i < intersects.length; ++i) {
  1617. const point = intersects[i].point, special = (intersects[i].object.type === 'Points');
  1618. let clipped = true;
  1619. if (clip[0].enabled && ((this.#clip_planes[0].normal.dot(point) > this.#clip_planes[0].constant) ^ special)) clipped = false;
  1620. if (clip[1].enabled && ((this.#clip_planes[1].normal.dot(point) > this.#clip_planes[1].constant) ^ special)) clipped = false;
  1621. if (clip[2].enabled && (this.#clip_planes[2].normal.dot(point) > this.#clip_planes[2].constant)) clipped = false;
  1622. if (!clipped) clippedIntersects.push(intersects[i]);
  1623. }
  1624. intersects = clippedIntersects;
  1625. }
  1626. return intersects;
  1627. }
  1628. /** @summary test camera position
  1629. * @desc function analyzes camera position and start redraw of geometry
  1630. * if objects in view may be changed */
  1631. testCameraPositionChange() {
  1632. if (!this.ctrl.select_in_view || this.#draw_all_nodes)
  1633. return;
  1634. const matrix = createProjectionMatrix(this.#camera),
  1635. frustum = createFrustum(matrix);
  1636. // check if overall bounding box seen
  1637. if (!frustum.CheckBox(this.getGeomBoundingBox()))
  1638. this.startDrawGeometry();
  1639. }
  1640. /** @summary Resolve stack */
  1641. resolveStack(stack) {
  1642. return this.#clones && stack ? this.#clones.resolveStack(stack) : null;
  1643. }
  1644. /** @summary Returns stack full name
  1645. * @desc Includes item name of top geo object */
  1646. getStackFullName(stack) {
  1647. const mainitemname = this.getItemName(),
  1648. sub = this.resolveStack(stack);
  1649. if (!sub || !sub.name)
  1650. return mainitemname;
  1651. return mainitemname ? mainitemname + '/' + sub.name : sub.name;
  1652. }
  1653. /** @summary Add handler which will be called when element is highlighted in geometry drawing
  1654. * @desc Handler should have highlightMesh function with same arguments as TGeoPainter */
  1655. addHighlightHandler(handler) {
  1656. if (!isFunc(handler?.highlightMesh))
  1657. return;
  1658. if (!this.#highlight_handlers)
  1659. this.#highlight_handlers = [];
  1660. this.#highlight_handlers.push(handler);
  1661. }
  1662. /** @summary perform mesh highlight */
  1663. highlightMesh(active_mesh, color, geo_object, geo_index, geo_stack, no_recursive) {
  1664. if (geo_object) {
  1665. active_mesh = active_mesh ? [active_mesh] : [];
  1666. const extras = this.getExtrasContainer();
  1667. if (extras) {
  1668. extras.traverse(obj3d => {
  1669. if ((obj3d.geo_object === geo_object) && (active_mesh.indexOf(obj3d) < 0))
  1670. active_mesh.push(obj3d);
  1671. });
  1672. }
  1673. } else if (geo_stack && this.#toplevel) {
  1674. active_mesh = [];
  1675. this.#toplevel.traverse(mesh => {
  1676. if ((mesh instanceof THREE.Mesh) && isSameStack(mesh.stack, geo_stack))
  1677. active_mesh.push(mesh);
  1678. });
  1679. } else
  1680. active_mesh = active_mesh ? [active_mesh] : [];
  1681. if (!active_mesh.length)
  1682. active_mesh = null;
  1683. if (active_mesh) {
  1684. // check if highlight is disabled for correspondent objects kinds
  1685. if (active_mesh[0].geo_object) {
  1686. if (!this.ctrl.highlight_scene)
  1687. active_mesh = null;
  1688. } else
  1689. if (!this.ctrl.highlight)
  1690. active_mesh = null;
  1691. }
  1692. if (!no_recursive) {
  1693. // check all other painters
  1694. if (active_mesh) {
  1695. if (!geo_object) geo_object = active_mesh[0].geo_object;
  1696. if (!geo_stack) geo_stack = active_mesh[0].stack;
  1697. }
  1698. const lst = this.#highlight_handlers || this.getCentral()?.getSubordinates().concat([this.getCentral()]) || this.getSubordinates();
  1699. for (let k = 0; k < lst?.length; ++k) {
  1700. if (lst[k] !== this)
  1701. lst[k].highlightMesh(null, color, geo_object, geo_index, geo_stack, true);
  1702. }
  1703. }
  1704. const curr_mesh = this.#selected_mesh;
  1705. if (!curr_mesh && !active_mesh)
  1706. return false;
  1707. const get_ctrl = mesh => mesh.get_ctrl ? mesh.get_ctrl() : new GeoDrawingControl(mesh, this.ctrl.highlight_bloom && this.#bloomComposer);
  1708. let same = false;
  1709. // check if selections are the same
  1710. if (curr_mesh && active_mesh && (curr_mesh.length === active_mesh.length)) {
  1711. same = true;
  1712. for (let k = 0; (k < curr_mesh.length) && same; ++k) {
  1713. if ((curr_mesh[k] !== active_mesh[k]) || get_ctrl(curr_mesh[k]).checkHighlightIndex(geo_index))
  1714. same = false;
  1715. }
  1716. }
  1717. if (same)
  1718. return Boolean(curr_mesh);
  1719. curr_mesh?.forEach(mesh => get_ctrl(mesh).setHighlight());
  1720. this.#selected_mesh = active_mesh;
  1721. active_mesh?.forEach(mesh => get_ctrl(mesh).setHighlight(color || new THREE.Color(this.ctrl.highlight_color), geo_index));
  1722. this.render3D(0);
  1723. return Boolean(active_mesh);
  1724. }
  1725. /** @summary handle mouse click event */
  1726. processMouseClick(pnt, intersects, evnt) {
  1727. if (!intersects.length)
  1728. return;
  1729. const mesh = intersects[0].object;
  1730. if (!mesh.get_ctrl)
  1731. return;
  1732. const ctrl = mesh.get_ctrl(),
  1733. click_indx = ctrl.extractIndex(intersects[0]);
  1734. ctrl.evnt = evnt;
  1735. if (ctrl.setSelected('blue', click_indx))
  1736. this.render3D();
  1737. ctrl.evnt = null;
  1738. }
  1739. /** @summary Configure mouse delay, required for complex geometries */
  1740. setMouseTmout(val) {
  1741. if (this.ctrl)
  1742. this.ctrl.mouse_tmout = val;
  1743. if (this.#controls)
  1744. this.#controls.mouse_tmout = val;
  1745. }
  1746. /** @summary Configure depth method, used for render order production.
  1747. * @param {string} method - Allowed values: 'ray', 'box','pnt', 'size', 'dflt' */
  1748. setDepthMethod(method) {
  1749. if (this.ctrl)
  1750. this.ctrl.depthMethod = method;
  1751. }
  1752. /** @summary Returns if camera can rotated */
  1753. canRotateCamera() {
  1754. if (this.ctrl.can_rotate === false)
  1755. return false;
  1756. if (!this.ctrl.can_rotate && (this.isOrthoCamera() || this.ctrl.project))
  1757. return false;
  1758. return true;
  1759. }
  1760. /** @summary Add orbit control */
  1761. addOrbitControls() {
  1762. if (this.#controls || !this.#webgl || this.isBatchMode() || this.#superimpose || isNodeJs()) return;
  1763. if (!this.getCanvPainter())
  1764. this.setTooltipAllowed(settings.Tooltip);
  1765. this.#controls = createOrbitControl(this, this.#camera, this.#scene, this.#renderer, this.#lookat);
  1766. this.#controls.mouse_tmout = this.ctrl.mouse_tmout; // set larger timeout for geometry processing
  1767. if (!this.canRotateCamera())
  1768. this.#controls.enableRotate = false;
  1769. this.#controls.contextMenu = this.orbitContext.bind(this);
  1770. this.#controls.processMouseMove = intersects => {
  1771. // painter already cleaned up, ignore any incoming events
  1772. if (!this.ctrl || !this.#controls) return;
  1773. let active_mesh = null, tooltip = null, resolve = null, names = [], geo_object, geo_index, geo_stack;
  1774. // try to find mesh from intersections
  1775. for (let k = 0; k < intersects.length; ++k) {
  1776. const obj = intersects[k].object, stack = getIntersectStack(intersects[k]);
  1777. if (!obj || !obj.visible) continue;
  1778. let info = null;
  1779. if (obj.geo_object)
  1780. info = obj.geo_name;
  1781. else if (stack)
  1782. info = this.getStackFullName(stack);
  1783. if (!info) continue;
  1784. if (info.indexOf('<prnt>') === 0)
  1785. info = this.getItemName() + info.slice(6);
  1786. names.push(info);
  1787. if (!active_mesh) {
  1788. active_mesh = obj;
  1789. tooltip = info;
  1790. geo_object = obj.geo_object;
  1791. if (obj.get_ctrl) {
  1792. geo_index = obj.get_ctrl().extractIndex(intersects[k]);
  1793. if ((geo_index !== undefined) && isStr(tooltip))
  1794. tooltip += ' indx:' + JSON.stringify(geo_index);
  1795. }
  1796. geo_stack = stack;
  1797. if (geo_stack) {
  1798. resolve = this.resolveStack(geo_stack);
  1799. if (obj.stacks) geo_index = intersects[k].instanceId;
  1800. }
  1801. }
  1802. }
  1803. this.highlightMesh(active_mesh, undefined, geo_object, geo_index);
  1804. if (this.ctrl.update_browser) {
  1805. if (this.ctrl.highlight && tooltip) names = [tooltip];
  1806. this.activateInBrowser(names);
  1807. }
  1808. if (!resolve?.obj)
  1809. return tooltip;
  1810. const lines = provideObjectInfo(resolve.obj);
  1811. lines.unshift(tooltip);
  1812. return { name: resolve.obj.fName, title: resolve.obj.fTitle || resolve.obj._typename, lines };
  1813. };
  1814. this.#controls.processMouseLeave = function() {
  1815. this.processMouseMove([]); // to disable highlight and reset browser
  1816. };
  1817. this.#controls.processDblClick = () => {
  1818. // painter already cleaned up, ignore any incoming events
  1819. if (!this.ctrl || !this.#controls) return;
  1820. if (this.#last_manifest) {
  1821. this.#last_manifest.wireframe = !this.#last_manifest.wireframe;
  1822. if (this.#last_hidden)
  1823. this.#last_hidden.forEach(obj => { obj.visible = true; });
  1824. this.#last_hidden = undefined;
  1825. this.#last_manifest = undefined;
  1826. } else
  1827. this.adjustCameraPosition(true);
  1828. this.render3D();
  1829. };
  1830. }
  1831. /** @summary Main function in geometry creation loop
  1832. * @desc Returns:
  1833. * - false when nothing todo
  1834. * - true if one could perform next action immediately
  1835. * - 1 when call after short timeout required
  1836. * - 2 when call must be done from processWorkerReply */
  1837. nextDrawAction() {
  1838. if (!this.#clones || this.isStage(stageInit))
  1839. return false;
  1840. if (this.isStage(stageCollect)) {
  1841. if (this.#geom_viewer) {
  1842. this.#draw_all_nodes = false;
  1843. this.changeStage(stageAnalyze);
  1844. return true;
  1845. }
  1846. // wait until worker is really started
  1847. if (this.ctrl.use_worker > 0) {
  1848. if (!this.#worker) {
  1849. this.startWorker();
  1850. return 1;
  1851. }
  1852. if (!this.#worker_ready)
  1853. return 1;
  1854. }
  1855. // first copy visibility flags and check how many unique visible nodes exists
  1856. let numvis = this.#first_drawing ? this.#clones.countVisibles() : 0,
  1857. matrix = null, frustum = null;
  1858. if (!numvis)
  1859. numvis = this.#clones.markVisibles(false, false, Boolean(this.#geo_manager) && !this.ctrl.showtop);
  1860. if (this.ctrl.select_in_view && !this.#first_drawing) {
  1861. // extract camera projection matrix for selection
  1862. matrix = createProjectionMatrix(this.#camera);
  1863. frustum = createFrustum(matrix);
  1864. // check if overall bounding box seen
  1865. if (frustum.CheckBox(this.getGeomBoundingBox())) {
  1866. matrix = null; // not use camera for the moment
  1867. frustum = null;
  1868. }
  1869. }
  1870. this.#current_face_limit = this.ctrl.maxfaces;
  1871. if (matrix) this.#current_face_limit *= 1.25;
  1872. // here we decide if we need worker for the drawings
  1873. // main reason - too large geometry and large time to scan all camera positions
  1874. let need_worker = !this.isBatchMode() && browser.isChrome && ((numvis > 10000) || (matrix && (this.#clones.scanVisible() > 1e5)));
  1875. // worker does not work when starting from file system
  1876. if (need_worker && source_dir.indexOf('file://') === 0) {
  1877. console.log('disable worker for jsroot from file system');
  1878. need_worker = false;
  1879. }
  1880. if (need_worker && !this.#worker && (this.ctrl.use_worker >= 0))
  1881. this.startWorker(); // we starting worker, but it may not be ready so fast
  1882. if (!need_worker || !this.#worker_ready) {
  1883. const res = this.#clones.collectVisibles(this.#current_face_limit, frustum);
  1884. this.#new_draw_nodes = res.lst;
  1885. this.#draw_all_nodes = res.complete;
  1886. this.changeStage(stageAnalyze);
  1887. return true;
  1888. }
  1889. const job = {
  1890. collect: this.#current_face_limit, // indicator for the command
  1891. flags: this.#clones.getVisibleFlags(),
  1892. matrix: matrix ? matrix.elements : null,
  1893. vislevel: this.#clones.getVisLevel(),
  1894. maxvisnodes: this.#clones.getMaxVisNodes()
  1895. };
  1896. this.submitToWorker(job);
  1897. this.changeStage(stageWorkerCollect);
  1898. return 2; // we now waiting for the worker reply
  1899. }
  1900. if (this.isStage(stageWorkerCollect)) {
  1901. // do nothing, we are waiting for worker reply
  1902. return 2;
  1903. }
  1904. if (this.isStage(stageAnalyze)) {
  1905. // here we merge new and old list of nodes for drawing,
  1906. // normally operation is fast and can be implemented with one c
  1907. if (this.#new_append_nodes) {
  1908. this.#new_draw_nodes = this.#draw_nodes.concat(this.#new_append_nodes);
  1909. this.#new_append_nodes = undefined;
  1910. } else if (this.#draw_nodes) {
  1911. let del;
  1912. if (this.#geom_viewer)
  1913. del = this.#draw_nodes;
  1914. else
  1915. del = this.#clones.mergeVisibles(this.#new_draw_nodes, this.#draw_nodes);
  1916. // remove should be fast, do it here
  1917. for (let n = 0; n < del.length; ++n)
  1918. this.#clones.createObject3D(del[n].stack, this.#toplevel, 'delete_mesh');
  1919. if (del.length)
  1920. this.#drawing_log = `Delete ${del.length} nodes`;
  1921. }
  1922. this.#draw_nodes = this.#new_draw_nodes;
  1923. this.#new_draw_nodes = undefined;
  1924. this.changeStage(stageCollShapes);
  1925. return true;
  1926. }
  1927. if (this.isStage(stageCollShapes)) {
  1928. // collect shapes
  1929. const shapes = this.#clones.collectShapes(this.#draw_nodes);
  1930. // merge old and new list with produced shapes
  1931. this.#build_shapes = this.#clones.mergeShapesLists(this.#build_shapes, shapes);
  1932. this.changeStage(stageStartBuild);
  1933. return true;
  1934. }
  1935. if (this.isStage(stageStartBuild)) {
  1936. // this is building of geometries,
  1937. // one can ask worker to build them or do it ourself
  1938. if (this.canSubmitToWorker()) {
  1939. const job = { limit: this.#current_face_limit, shapes: [] };
  1940. let cnt = 0;
  1941. for (let n = 0; n < this.#build_shapes.length; ++n) {
  1942. const item = this.#build_shapes[n];
  1943. // only submit not-done items
  1944. if (item.ready || item.geom) {
  1945. // this is place holder for existing geometry
  1946. job.shapes.push({ id: item.id, ready: true, nfaces: countGeometryFaces(item.geom), refcnt: item.refcnt });
  1947. } else {
  1948. job.shapes.push(clone(item, null, true));
  1949. cnt++;
  1950. }
  1951. }
  1952. if (cnt > 0) {
  1953. // only if some geom missing, submit job to the worker
  1954. this.submitToWorker(job);
  1955. this.changeStage(stageWorkerBuild);
  1956. return 2;
  1957. }
  1958. }
  1959. this.changeStage(stageBuild);
  1960. }
  1961. if (this.isStage(stageWorkerBuild)) {
  1962. // waiting shapes from the worker, worker should activate our code
  1963. return 2;
  1964. }
  1965. if (this.isStage(stageBuild) || this.isStage(stageBuildReady)) {
  1966. if (this.isStage(stageBuild)) {
  1967. // building shapes
  1968. const res = this.#clones.buildShapes(this.#build_shapes, this.#current_face_limit, 500);
  1969. if (res.done) {
  1970. this.ctrl.info.num_shapes = this.#build_shapes.length;
  1971. this.changeStage(stageBuildReady);
  1972. } else {
  1973. this.ctrl.info.num_shapes = res.shapes;
  1974. this.#drawing_log = `Creating: ${res.shapes} / ${this.#build_shapes.length} shapes, ${res.faces} faces`;
  1975. return true;
  1976. // if (res.notusedshapes < 30) return true;
  1977. }
  1978. }
  1979. // final stage, create all meshes
  1980. const tm0 = new Date().getTime(),
  1981. toplevel = this.ctrl.project ? this.#fullgeom_proj : this.#toplevel;
  1982. let build_instanced = false, ready = true;
  1983. if (!this.ctrl.project)
  1984. build_instanced = this.#clones.createInstancedMeshes(this.ctrl, toplevel, this.#draw_nodes, this.#build_shapes, getRootColors());
  1985. if (!build_instanced) {
  1986. for (let n = 0; n < this.#draw_nodes.length; ++n) {
  1987. const entry = this.#draw_nodes[n];
  1988. if (entry.done) continue;
  1989. // shape can be provided with entry itself
  1990. const shape = entry.server_shape || this.#build_shapes[entry.shapeid];
  1991. this.createEntryMesh(entry, shape, toplevel);
  1992. const tm1 = new Date().getTime();
  1993. if (tm1 - tm0 > 500) { ready = false; break; }
  1994. }
  1995. }
  1996. if (ready) {
  1997. if (this.ctrl.project) {
  1998. this.changeStage(stageBuildProj);
  1999. return true;
  2000. }
  2001. this.changeStage(stageInit);
  2002. return false;
  2003. }
  2004. if (!this.isStage(stageBuild))
  2005. this.#drawing_log = `Building meshes ${this.ctrl.info.num_meshes} / ${this.ctrl.info.num_faces}`;
  2006. return true;
  2007. }
  2008. if (this.isStage(stageWaitMain)) {
  2009. // wait for main painter to be ready
  2010. if (!this.getCentral()) {
  2011. this.changeStage(stageInit, 'Lost main painter');
  2012. return false;
  2013. }
  2014. if (!this.getCentral().isDrawingReady())
  2015. return 1;
  2016. this.changeStage(stageBuildProj); // just do projection
  2017. }
  2018. if (this.isStage(stageBuildProj)) {
  2019. this.doProjection();
  2020. this.changeStage(stageInit);
  2021. return false;
  2022. }
  2023. console.error(`never come here, stage ${this.drawing_stage}`);
  2024. return false;
  2025. }
  2026. /** @summary Insert appropriate mesh for given entry */
  2027. createEntryMesh(entry, shape, toplevel) {
  2028. // workaround for the TGeoOverlap, where two branches should get predefined color
  2029. if (this.ctrl.split_colors && entry.stack) {
  2030. if (entry.stack[0] === 0)
  2031. entry.custom_color = 'green';
  2032. else if (entry.stack[0] === 1)
  2033. entry.custom_color = 'blue';
  2034. }
  2035. this.#clones.createEntryMesh(this.ctrl, toplevel, entry, shape, getRootColors());
  2036. return true;
  2037. }
  2038. /** @summary used by geometry viewer to show more nodes
  2039. * @desc These nodes excluded from selection logic and always inserted into the model
  2040. * Shape already should be created and assigned to the node */
  2041. appendMoreNodes(nodes, from_drawing) {
  2042. if (!this.isStage(stageInit) && !from_drawing) {
  2043. this.#provided_more_nodes = nodes;
  2044. return;
  2045. }
  2046. // delete old nodes
  2047. if (this.#more_nodes) {
  2048. for (let n = 0; n < this.#more_nodes.length; ++n) {
  2049. const entry = this.#more_nodes[n],
  2050. obj3d = this.#clones.createObject3D(entry.stack, this.#toplevel, 'delete_mesh');
  2051. disposeThreejsObject(obj3d);
  2052. cleanupShape(entry.server_shape);
  2053. delete entry.server_shape;
  2054. }
  2055. this.#more_nodes = undefined;
  2056. }
  2057. if (!nodes) return;
  2058. const real_nodes = [];
  2059. for (let k = 0; k < nodes.length; ++k) {
  2060. const entry = nodes[k],
  2061. shape = entry.server_shape;
  2062. if (!shape?.ready) continue;
  2063. if (this.createEntryMesh(entry, shape, this.#toplevel))
  2064. real_nodes.push(entry);
  2065. }
  2066. // remember additional nodes only if they include shape - otherwise one can ignore them
  2067. if (real_nodes.length)
  2068. this.#more_nodes = real_nodes;
  2069. if (!from_drawing)
  2070. this.render3D();
  2071. }
  2072. /** @summary Returns hierarchy of 3D objects used to produce projection.
  2073. * @desc Typically external master painter is used, but also internal data can be used */
  2074. getProjectionSource() {
  2075. if (this.#clones_owner)
  2076. return this.#fullgeom_proj;
  2077. if (!this.getCentral()?.isDrawingReady()) {
  2078. console.warn('MAIN PAINTER NOT READY WHEN DO PROJECTION');
  2079. return null;
  2080. }
  2081. return this.getCentral().#toplevel;
  2082. }
  2083. /** @summary Extend custom geometry bounding box */
  2084. extendCustomBoundingBox(box) {
  2085. if (!box) return;
  2086. if (!this.#custom_bounding_box)
  2087. this.#custom_bounding_box = new THREE.Box3().makeEmpty();
  2088. const origin = this.#custom_bounding_box.clone();
  2089. this.#custom_bounding_box.union(box);
  2090. if (!this.#custom_bounding_box.equals(origin))
  2091. this.#adjust_camera_with_render = true;
  2092. }
  2093. /** @summary Calculate geometry bounding box */
  2094. getGeomBoundingBox(topitem, scalar) {
  2095. const box3 = new THREE.Box3(), check_any = !this.#clones;
  2096. if (topitem === undefined)
  2097. topitem = this.#toplevel;
  2098. box3.makeEmpty();
  2099. if (this.#custom_bounding_box && (topitem === this.#toplevel)) {
  2100. box3.union(this.#custom_bounding_box);
  2101. return box3;
  2102. }
  2103. if (!topitem) {
  2104. box3.min.x = box3.min.y = box3.min.z = -1;
  2105. box3.max.x = box3.max.y = box3.max.z = 1;
  2106. return box3;
  2107. }
  2108. topitem.traverse(mesh => {
  2109. if (check_any || (mesh.stack && (mesh instanceof THREE.Mesh)) ||
  2110. (mesh.main_track && (mesh instanceof THREE.LineSegments)) || (mesh.stacks && (mesh instanceof THREE.InstancedMesh)))
  2111. getBoundingBox(mesh, box3);
  2112. });
  2113. if (scalar === 'original') {
  2114. box3.translate(new THREE.Vector3(-topitem.position.x, -topitem.position.y, -topitem.position.z));
  2115. box3.min.multiply(new THREE.Vector3(1/topitem.scale.x, 1/topitem.scale.y, 1/topitem.scale.z));
  2116. box3.max.multiply(new THREE.Vector3(1/topitem.scale.x, 1/topitem.scale.y, 1/topitem.scale.z));
  2117. } else if (scalar !== undefined)
  2118. box3.expandByVector(box3.getSize(new THREE.Vector3()).multiplyScalar(scalar));
  2119. return box3;
  2120. }
  2121. /** @summary Create geometry projection */
  2122. doProjection() {
  2123. const toplevel = this.getProjectionSource();
  2124. if (!toplevel) return false;
  2125. disposeThreejsObject(this.#toplevel, true);
  2126. // let axis = this.ctrl.project;
  2127. if (this.ctrl.projectPos === undefined) {
  2128. const bound = this.getGeomBoundingBox(toplevel),
  2129. min = bound.min[this.ctrl.project], max = bound.max[this.ctrl.project];
  2130. let mean = (min + max)/2;
  2131. if ((min < 0) && (max > 0) && (Math.abs(mean) < 0.2*Math.max(-min, max))) mean = 0; // if middle is around 0, use 0
  2132. this.ctrl.projectPos = mean;
  2133. }
  2134. toplevel.traverse(mesh => {
  2135. if (!(mesh instanceof THREE.Mesh) || !mesh.stack) return;
  2136. const geom2 = projectGeometry(mesh.geometry, mesh.parent.absMatrix || mesh.parent.matrixWorld, this.ctrl.project, this.ctrl.projectPos, mesh._flippedMesh);
  2137. if (!geom2) return;
  2138. const mesh2 = new THREE.Mesh(geom2, mesh.material.clone());
  2139. this.#toplevel.add(mesh2);
  2140. mesh2.stack = mesh.stack;
  2141. });
  2142. return true;
  2143. }
  2144. /** @summary Should be invoked when light configuration changed */
  2145. changedLight(box) {
  2146. if (!this.#camera)
  2147. return;
  2148. const need_render = !box;
  2149. if (!box) box = this.getGeomBoundingBox();
  2150. const sizex = box.max.x - box.min.x,
  2151. sizey = box.max.y - box.min.y,
  2152. sizez = box.max.z - box.min.z,
  2153. plights = [], p = (this.ctrl.light.power ?? 1) * 0.5;
  2154. if (this.#camera._lights !== this.ctrl.light.kind) {
  2155. // remove all childs and recreate only necessary lights
  2156. disposeThreejsObject(this.#camera, true);
  2157. this.#camera._lights = this.ctrl.light.kind;
  2158. switch (this.#camera._lights) {
  2159. case 'ambient' : this.#camera.add(new THREE.AmbientLight(0xefefef, p)); break;
  2160. case 'hemisphere' : this.#camera.add(new THREE.HemisphereLight(0xffffbb, 0x080820, p)); break;
  2161. case 'mix': this.#camera.add(new THREE.AmbientLight(0xefefef, p));
  2162. // eslint-disable-next-line no-fallthrough
  2163. default: // 6 point lights
  2164. for (let n = 0; n < 6; ++n) {
  2165. const l = new THREE.DirectionalLight(0xefefef, p);
  2166. this.#camera.add(l);
  2167. l._id = n;
  2168. }
  2169. }
  2170. }
  2171. for (let k = 0; k < this.#camera.children.length; ++k) {
  2172. const light = this.#camera.children[k];
  2173. let enabled = false;
  2174. if (light.isAmbientLight || light.isHemisphereLight) {
  2175. light.intensity = p;
  2176. continue;
  2177. }
  2178. if (!light.isDirectionalLight) continue;
  2179. switch (light._id) {
  2180. case 0: light.position.set(sizex/5, sizey/5, sizez/5); enabled = this.ctrl.light.specular; break;
  2181. case 1: light.position.set(0, 0, sizez/2); enabled = this.ctrl.light.front; break;
  2182. case 2: light.position.set(0, 2*sizey, 0); enabled = this.ctrl.light.top; break;
  2183. case 3: light.position.set(0, -2*sizey, 0); enabled = this.ctrl.light.bottom; break;
  2184. case 4: light.position.set(-2*sizex, 0, 0); enabled = this.ctrl.light.left; break;
  2185. case 5: light.position.set(2*sizex, 0, 0); enabled = this.ctrl.light.right; break;
  2186. }
  2187. light.power = enabled ? p*Math.PI*4 : 0;
  2188. if (enabled) plights.push(light);
  2189. }
  2190. // keep light power of all sources constant
  2191. plights.forEach(ll => { ll.power = p*4*Math.PI/plights.length; });
  2192. if (need_render) this.render3D();
  2193. }
  2194. /** @summary Returns true if orthographic camera is used */
  2195. isOrthoCamera() {
  2196. return this.ctrl.camera_kind.indexOf('ortho') === 0;
  2197. }
  2198. /** @summary Create configured camera */
  2199. createCamera() {
  2200. if (this.#camera) {
  2201. this.#scene.remove(this.#camera);
  2202. disposeThreejsObject(this.#camera);
  2203. this.#camera = undefined;
  2204. }
  2205. if (this.isOrthoCamera())
  2206. this.#camera = new THREE.OrthographicCamera(-this.#scene_width/2, this.#scene_width/2, this.#scene_height/2, -this.#scene_height/2, 1, 10000);
  2207. else {
  2208. this.#camera = new THREE.PerspectiveCamera(25, this.#scene_width / this.#scene_height, 1, 10000);
  2209. this.#camera.up = this.ctrl._yup ? new THREE.Vector3(0, 1, 0) : new THREE.Vector3(0, 0, 1);
  2210. }
  2211. // Light - add default directional light, adjust later
  2212. const light = new THREE.DirectionalLight(0xefefef, 0.1);
  2213. light.position.set(10, 10, 10);
  2214. this.#camera.add(light);
  2215. this.#scene.add(this.#camera);
  2216. }
  2217. /** @summary Create special effects */
  2218. createSpecialEffects() {
  2219. if (this.#webgl && this.ctrl.outline && isFunc(this.createOutline)) {
  2220. // code used with jsroot-based geometry drawing in EVE7, not important any longer
  2221. this.#effectComposer = new THREE.EffectComposer(this.#renderer);
  2222. this.#effectComposer.addPass(new THREE.RenderPass(this.#scene, this.#camera));
  2223. this.createOutline(this.#scene, this.#camera, this.#scene_width, this.#scene_height);
  2224. }
  2225. this.ensureBloom();
  2226. }
  2227. /** @summary Return current effect composer */
  2228. getEffectComposer() { return this.#effectComposer; }
  2229. /** @summary Initial scene creation */
  2230. async createScene(w, h, render3d) {
  2231. if (this.#superimpose) {
  2232. const cfg = getHistPainter3DCfg(this.getMainPainter());
  2233. if (cfg?.renderer) {
  2234. this.#scene = cfg.scene;
  2235. this.#scene_width = cfg.scene_width;
  2236. this.#scene_height = cfg.scene_height;
  2237. this.#renderer = cfg.renderer;
  2238. this.#webgl = (this.#renderer.jsroot_render3d === constants.Render3D.WebGL);
  2239. this.#toplevel = new THREE.Object3D();
  2240. this.#scene.add(this.#toplevel);
  2241. if (cfg.scale_x || cfg.scale_y || cfg.scale_z)
  2242. this.#toplevel.scale.set(cfg.scale_x, cfg.scale_y, cfg.scale_z);
  2243. if (cfg.offset_x || cfg.offset_y || cfg.offset_z)
  2244. this.#toplevel.position.set(cfg.offset_x, cfg.offset_y, cfg.offset_z);
  2245. this.#toplevel.updateMatrix();
  2246. this.#toplevel.updateMatrixWorld();
  2247. this.#camera = cfg.camera;
  2248. }
  2249. return this.#renderer?.jsroot_dom;
  2250. }
  2251. return importThreeJs().then(() => {
  2252. // three.js 3D drawing
  2253. this.#scene = new THREE.Scene();
  2254. this.#fog = new THREE.Fog(0xffffff, 1, 10000);
  2255. this.#scene.fog = this.ctrl.use_fog ? this.#fog : null;
  2256. this.#scene.overrideMaterial = new THREE.MeshLambertMaterial({ color: 0x7000ff, vertexColors: false, transparent: true, opacity: 0.2, depthTest: false });
  2257. this.#scene_width = w;
  2258. this.#scene_height = h;
  2259. this.createCamera();
  2260. this.#selected_mesh = null;
  2261. this.#overall_size = 10;
  2262. this.#toplevel = new THREE.Object3D();
  2263. this.#scene.add(this.#toplevel);
  2264. this.#scene.background = new THREE.Color(this.ctrl.background);
  2265. return createRender3D(w, h, render3d, { antialias: true, logarithmicDepthBuffer: false, preserveDrawingBuffer: true });
  2266. }).then(r => {
  2267. this.#renderer = r;
  2268. if (this.batch_format)
  2269. r.jsroot_image_format = this.batch_format;
  2270. this.#webgl = (r.jsroot_render3d === constants.Render3D.WebGL);
  2271. if (isFunc(r.setPixelRatio) && !isNodeJs() && !browser.android)
  2272. r.setPixelRatio(window.devicePixelRatio);
  2273. r.setSize(w, h, !this.#fit_main_area);
  2274. r.localClippingEnabled = true;
  2275. r.setClearColor(this.#scene.background, 1);
  2276. if (this.#fit_main_area && this.#webgl) {
  2277. r.domElement.style.width = '100%';
  2278. r.domElement.style.height = '100%';
  2279. const main = this.selectDom();
  2280. if (main.style('position') === 'static')
  2281. main.style('position', 'relative');
  2282. }
  2283. this.#animating = false;
  2284. this.ctrl.doubleside = false; // both sides need for clipping
  2285. this.createSpecialEffects();
  2286. if (this.#fit_main_area && !this.#webgl) {
  2287. // create top-most SVG for geometry drawings
  2288. const doc = getDocument(),
  2289. svg = doc.createElementNS(nsSVG, 'svg');
  2290. svg.setAttribute('width', w);
  2291. svg.setAttribute('height', h);
  2292. svg.appendChild(this.#renderer.jsroot_dom);
  2293. return svg;
  2294. }
  2295. return this.#renderer.jsroot_dom;
  2296. });
  2297. }
  2298. /** @summary Start geometry drawing */
  2299. startDrawGeometry(force) {
  2300. if (!force && !this.isStage(stageInit)) {
  2301. this.#draw_nodes_again = true;
  2302. return;
  2303. }
  2304. if (this.#clones_owner)
  2305. this.#clones?.setDefaultColors(this.ctrl.dflt_colors);
  2306. this.#last_render_tm = this.#start_render_tm = new Date().getTime();
  2307. this.#last_render_meshes = 0;
  2308. this.changeStage(stageCollect);
  2309. this.#drawing_ready = false;
  2310. this.ctrl.info.num_meshes = 0;
  2311. this.ctrl.info.num_faces = 0;
  2312. this.ctrl.info.num_shapes = 0;
  2313. this.#selected_mesh = null;
  2314. if (this.ctrl.project) {
  2315. if (this.#clones_owner) {
  2316. if (this.#fullgeom_proj)
  2317. this.changeStage(stageBuildProj);
  2318. else
  2319. this.#fullgeom_proj = new THREE.Object3D();
  2320. } else
  2321. this.changeStage(stageWaitMain);
  2322. }
  2323. this.#last_manifest = undefined;
  2324. this.#last_hidden = undefined; // clear list of hidden objects
  2325. this.#draw_nodes_again = undefined; // forget about such flag
  2326. this.continueDraw();
  2327. }
  2328. /** @summary reset all kind of advanced features like depth test */
  2329. resetAdvanced() {
  2330. this.ctrl.depthTest = true;
  2331. this.ctrl.clipIntersect = true;
  2332. this.ctrl.depthMethod = 'ray';
  2333. this.changedDepthMethod('norender');
  2334. this.changedDepthTest();
  2335. }
  2336. /** @summary returns maximal dimension */
  2337. getOverallSize(force) {
  2338. if (!this.#overall_size || force || this.#custom_bounding_box) {
  2339. const box = this.getGeomBoundingBox();
  2340. // if detect of coordinates fails - ignore
  2341. if (!Number.isFinite(box.min.x)) return 1000;
  2342. this.#overall_size = 2 * Math.max(box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z);
  2343. }
  2344. return this.#overall_size;
  2345. }
  2346. /** @summary Create png image with drawing snapshot. */
  2347. createSnapshot(filename) {
  2348. if (!this.#renderer) return;
  2349. this.render3D(0);
  2350. const dataUrl = this.#renderer.domElement.toDataURL('image/png');
  2351. if (filename === 'asis') return dataUrl;
  2352. dataUrl.replace('image/png', 'image/octet-stream');
  2353. const doc = getDocument(),
  2354. link = doc.createElement('a');
  2355. if (isStr(link.download)) {
  2356. doc.body.appendChild(link); // Firefox requires the link to be in the body
  2357. link.download = filename || 'geometry.png';
  2358. link.href = dataUrl;
  2359. link.click();
  2360. doc.body.removeChild(link); // remove the link when done
  2361. }
  2362. }
  2363. /** @summary Returns url parameters defining camera position.
  2364. * @desc Either absolute position are provided (arg === true) or zoom, roty, rotz parameters */
  2365. produceCameraUrl(arg) {
  2366. if (!this.#camera)
  2367. return '';
  2368. if (this.#camera.isOrthographicCamera) {
  2369. const zoom = Math.round(this.#camera.zoom * 100);
  2370. return this.ctrl.camera_kind + (zoom === 100 ? '' : `,zoom=${zoom}`);
  2371. }
  2372. let kind = '';
  2373. if (this.ctrl.camera_kind !== 'perspective')
  2374. kind = this.ctrl.camera_kind + ',';
  2375. if (arg === true) {
  2376. const p = this.#camera.position, t = this.#controls?.target;
  2377. if (!p || !t) return '';
  2378. const conv = v => {
  2379. let s = '';
  2380. if (v < 0) { s = 'n'; v = -v; }
  2381. return s + v.toFixed(0);
  2382. };
  2383. let res = `${kind}camx${conv(p.x)},camy${conv(p.y)},camz${conv(p.z)}`;
  2384. if (t.x || t.y || t.z) res += `,camlx${conv(t.x)},camly${conv(t.y)},camlz${conv(t.z)}`;
  2385. return res;
  2386. }
  2387. if (!this.#lookat || !this.#camera0pos)
  2388. return '';
  2389. const pos1 = new THREE.Vector3().add(this.#camera0pos).sub(this.#lookat),
  2390. pos2 = new THREE.Vector3().add(this.#camera.position).sub(this.#lookat),
  2391. zoom = Math.min(10000, Math.max(1, this.ctrl.zoom * pos2.length() / pos1.length() * 100));
  2392. pos1.normalize();
  2393. pos2.normalize();
  2394. const quat = new THREE.Quaternion(), euler = new THREE.Euler();
  2395. quat.setFromUnitVectors(pos1, pos2);
  2396. euler.setFromQuaternion(quat, 'YZX');
  2397. let roty = euler.y / Math.PI * 180,
  2398. rotz = euler.z / Math.PI * 180;
  2399. if (roty < 0) roty += 360;
  2400. if (rotz < 0) rotz += 360;
  2401. return `${kind}roty${roty.toFixed(0)},rotz${rotz.toFixed(0)},zoom${zoom.toFixed(0)}`;
  2402. }
  2403. /** @summary Calculates current zoom factor */
  2404. calculateZoom() {
  2405. if (this.#camera0pos && this.#camera && this.#lookat) {
  2406. const pos1 = new THREE.Vector3().add(this.#camera0pos).sub(this.#lookat),
  2407. pos2 = new THREE.Vector3().add(this.#camera.position).sub(this.#lookat);
  2408. return pos2.length() / pos1.length();
  2409. }
  2410. return 0;
  2411. }
  2412. /** @summary Place camera to default position,
  2413. * @param arg - true forces camera readjustment, 'first' is called when suppose to be first after complete drawing
  2414. * @param keep_zoom - tries to keep zooming factor of the camera */
  2415. adjustCameraPosition(arg, keep_zoom) {
  2416. if (!this.#toplevel || this.#superimpose)
  2417. return;
  2418. const force = (arg === true),
  2419. first_time = (arg === 'first') || force,
  2420. only_set = (arg === 'only_set'),
  2421. box = this.getGeomBoundingBox();
  2422. // let box2 = new THREE.Box3().makeEmpty();
  2423. // box2.expandByObject(this.#toplevel, true);
  2424. // console.log('min,max', box.min.x, box.max.x, box.min.y, box.max.y, box.min.z, box.max.z );
  2425. // if detect of coordinates fails - ignore
  2426. if (!Number.isFinite(box.min.x)) {
  2427. console.log('FAILS to get geometry bounding box');
  2428. return;
  2429. }
  2430. const sizex = box.max.x - box.min.x,
  2431. sizey = box.max.y - box.min.y,
  2432. sizez = box.max.z - box.min.z,
  2433. midx = (box.max.x + box.min.x)/2,
  2434. midy = (box.max.y + box.min.y)/2,
  2435. midz = (box.max.z + box.min.z)/2,
  2436. more = this.ctrl._axis || (this.ctrl.camera_overlay === 'bar') ? 0.2 : 0.1;
  2437. if (this.#scene_size && !force) {
  2438. function test(v1, v2, scale) {
  2439. if (!scale) scale = Math.abs((v1+v2)/2);
  2440. return scale <= 1e-20 ? true : Math.abs(v2-v1)/scale > 0.01;
  2441. };
  2442. const d = this.#scene_size;
  2443. if (!test(sizex, d.sizex) && !test(sizey, d.sizey) && !test(sizez, d.sizez) &&
  2444. !test(midx, d.midx, d.sizex) && !test(midy, d.midy, d.sizey) && !test(midz, d.midz, d.sizez)) {
  2445. if (this.ctrl.select_in_view)
  2446. this.startDrawGeometry();
  2447. return;
  2448. }
  2449. }
  2450. this.#scene_size = { sizex, sizey, sizez, midx, midy, midz };
  2451. this.#overall_size = 2 * Math.max(sizex, sizey, sizez);
  2452. this.#camera.near = this.#overall_size / 350;
  2453. this.#camera.far = this.#overall_size * 100;
  2454. this.#fog.near = this.#overall_size * 0.5;
  2455. this.#fog.far = this.#overall_size * 5;
  2456. if (first_time) {
  2457. for (let naxis = 0; naxis < 3; ++naxis) {
  2458. const cc = this.ctrl.clip[naxis];
  2459. cc.min = box.min[cc.name];
  2460. cc.max = box.max[cc.name];
  2461. const sz = cc.max - cc.min;
  2462. cc.max += sz*0.01;
  2463. cc.min -= sz*0.01;
  2464. if (sz > 100)
  2465. cc.step = 0.1;
  2466. else if (sz > 1)
  2467. cc.step = 0.001;
  2468. else
  2469. cc.step = undefined;
  2470. if (!cc.value)
  2471. cc.value = (cc.min + cc.max) / 2;
  2472. else if (cc.value < cc.min)
  2473. cc.value = cc.min;
  2474. else if (cc.value > cc.max)
  2475. cc.value = cc.max;
  2476. }
  2477. }
  2478. let k = 2*this.ctrl.zoom;
  2479. const max_all = Math.max(sizex, sizey, sizez),
  2480. sign = this.ctrl.camera_kind.indexOf('N') > 0 ? -1 : 1;
  2481. this.#lookat = new THREE.Vector3(midx, midy, midz);
  2482. this.#camera0pos = new THREE.Vector3(-2*max_all, 0, 0); // virtual 0 position, where rotation starts
  2483. this.#camera.updateMatrixWorld();
  2484. this.#camera.updateProjectionMatrix();
  2485. if ((this.ctrl.rotatey || this.ctrl.rotatez) && this.ctrl.can_rotate) {
  2486. const prev_zoom = this.calculateZoom();
  2487. if (keep_zoom && prev_zoom) k = 2*prev_zoom;
  2488. const euler = new THREE.Euler(0, this.ctrl.rotatey/180*Math.PI, this.ctrl.rotatez/180*Math.PI, 'YZX');
  2489. this.#camera.position.set(-k*max_all, 0, 0);
  2490. this.#camera.position.applyEuler(euler);
  2491. this.#camera.position.add(new THREE.Vector3(midx, midy, midz));
  2492. if (keep_zoom && prev_zoom) {
  2493. const actual_zoom = this.calculateZoom();
  2494. k *= prev_zoom/actual_zoom;
  2495. this.#camera.position.set(-k*max_all, 0, 0);
  2496. this.#camera.position.applyEuler(euler);
  2497. this.#camera.position.add(new THREE.Vector3(midx, midy, midz));
  2498. }
  2499. } else if (this.ctrl.camx !== undefined && this.ctrl.camy !== undefined && this.ctrl.camz !== undefined) {
  2500. this.#camera.position.set(this.ctrl.camx, this.ctrl.camy, this.ctrl.camz);
  2501. this.#lookat.set(this.ctrl.camlx || 0, this.ctrl.camly || 0, this.ctrl.camlz || 0);
  2502. this.ctrl.camx = this.ctrl.camy = this.ctrl.camz = this.ctrl.camlx = this.ctrl.camly = this.ctrl.camlz = undefined;
  2503. } else if ((this.ctrl.camera_kind === 'orthoXOY') || (this.ctrl.camera_kind === 'orthoXNOY')) {
  2504. this.#camera.up.set(0, 1, 0);
  2505. this.#camera.position.set(sign < 0 ? midx*2 : 0, 0, midz + sign*sizez*2);
  2506. this.#lookat.set(sign < 0 ? midx*2 : 0, 0, midz);
  2507. this.#camera.left = box.min.x - more*sizex;
  2508. this.#camera.right = box.max.x + more*sizex;
  2509. this.#camera.top = box.max.y + more*sizey;
  2510. this.#camera.bottom = box.min.y - more*sizey;
  2511. if (!keep_zoom) this.#camera.zoom = this.ctrl.zoom || 1;
  2512. this.#camera.orthoSign = sign;
  2513. this.#camera.orthoZ = [midz, sizez/2];
  2514. } else if ((this.ctrl.camera_kind === 'orthoXOZ') || (this.ctrl.camera_kind === 'orthoXNOZ')) {
  2515. this.#camera.up.set(0, 0, 1);
  2516. this.#camera.position.set(sign < 0 ? midx*2 : 0, midy - sign*sizey*2, 0);
  2517. this.#lookat.set(sign < 0 ? midx*2 : 0, midy, 0);
  2518. this.#camera.left = box.min.x - more*sizex;
  2519. this.#camera.right = box.max.x + more*sizex;
  2520. this.#camera.top = box.max.z + more*sizez;
  2521. this.#camera.bottom = box.min.z - more*sizez;
  2522. if (!keep_zoom) this.#camera.zoom = this.ctrl.zoom || 1;
  2523. this.#camera.orthoIndicies = [0, 2, 1];
  2524. this.#camera.orthoRotation = geom => geom.rotateX(Math.PI/2);
  2525. this.#camera.orthoSign = sign;
  2526. this.#camera.orthoZ = [midy, -sizey/2];
  2527. } else if ((this.ctrl.camera_kind === 'orthoZOY') || (this.ctrl.camera_kind === 'orthoZNOY')) {
  2528. this.#camera.up.set(0, 1, 0);
  2529. this.#camera.position.set(midx - sign*sizex*2, 0, sign < 0 ? midz*2 : 0);
  2530. this.#lookat.set(midx, 0, sign < 0 ? midz*2 : 0);
  2531. this.#camera.left = box.min.z - more*sizez;
  2532. this.#camera.right = box.max.z + more*sizez;
  2533. this.#camera.top = box.max.y + more*sizey;
  2534. this.#camera.bottom = box.min.y - more*sizey;
  2535. if (!keep_zoom) this.#camera.zoom = this.ctrl.zoom || 1;
  2536. this.#camera.orthoIndicies = [2, 1, 0];
  2537. this.#camera.orthoRotation = geom => geom.rotateY(-Math.PI/2);
  2538. this.#camera.orthoSign = sign;
  2539. this.#camera.orthoZ = [midx, -sizex/2];
  2540. } else if ((this.ctrl.camera_kind === 'orthoZOX') || (this.ctrl.camera_kind === 'orthoZNOX')) {
  2541. this.#camera.up.set(1, 0, 0);
  2542. this.#camera.position.set(0, midy - sign*sizey*2, sign > 0 ? midz*2 : 0);
  2543. this.#lookat.set(0, midy, sign > 0 ? midz*2 : 0);
  2544. this.#camera.left = box.min.z - more*sizez;
  2545. this.#camera.right = box.max.z + more*sizez;
  2546. this.#camera.top = box.max.x + more*sizex;
  2547. this.#camera.bottom = box.min.x - more*sizex;
  2548. if (!keep_zoom) this.#camera.zoom = this.ctrl.zoom || 1;
  2549. this.#camera.orthoIndicies = [2, 0, 1];
  2550. this.#camera.orthoRotation = geom => geom.rotateX(Math.PI/2).rotateY(Math.PI/2);
  2551. this.#camera.orthoSign = sign;
  2552. this.#camera.orthoZ = [midy, -sizey/2];
  2553. } else if (this.ctrl.project) {
  2554. switch (this.ctrl.project) {
  2555. case 'x': this.#camera.position.set(k*1.5*Math.max(sizey, sizez), 0, 0); break;
  2556. case 'y': this.#camera.position.set(0, k*1.5*Math.max(sizex, sizez), 0); break;
  2557. case 'z': this.#camera.position.set(0, 0, k*1.5*Math.max(sizex, sizey)); break;
  2558. }
  2559. } else if (this.ctrl.camera_kind === 'perspXOZ') {
  2560. this.#camera.up.set(0, 1, 0);
  2561. this.#camera.position.set(midx - 3*max_all, midy, midz);
  2562. } else if (this.ctrl.camera_kind === 'perspYOZ') {
  2563. this.#camera.up.set(1, 0, 0);
  2564. this.#camera.position.set(midx, midy - 3*max_all, midz);
  2565. } else if (this.ctrl.camera_kind === 'perspXOY') {
  2566. this.#camera.up.set(0, 0, 1);
  2567. this.#camera.position.set(midx - 3*max_all, midy, midz);
  2568. } else if (this.ctrl._yup) {
  2569. this.#camera.up.set(0, 1, 0);
  2570. this.#camera.position.set(midx-k*Math.max(sizex, sizez), midy+k*sizey, midz-k*Math.max(sizex, sizez));
  2571. } else {
  2572. this.#camera.up.set(0, 0, 1);
  2573. this.#camera.position.set(midx-k*Math.max(sizex, sizey), midy-k*Math.max(sizex, sizey), midz+k*sizez);
  2574. }
  2575. if (this.#camera.isOrthographicCamera && this.isOrthoCamera() && this.#scene_width && this.#scene_height) {
  2576. const screen_ratio = this.#scene_width / this.#scene_height,
  2577. szx = this.#camera.right - this.#camera.left, szy = this.#camera.top - this.#camera.bottom;
  2578. if (screen_ratio > szx / szy) {
  2579. // screen wider than actual geometry
  2580. const m = (this.#camera.right + this.#camera.left) / 2;
  2581. this.#camera.left = m - szy * screen_ratio / 2;
  2582. this.#camera.right = m + szy * screen_ratio / 2;
  2583. } else {
  2584. // screen higher than actual geometry
  2585. const m = (this.#camera.top + this.#camera.bottom) / 2;
  2586. this.#camera.top = m + szx / screen_ratio / 2;
  2587. this.#camera.bottom = m - szx / screen_ratio / 2;
  2588. }
  2589. }
  2590. this.#camera.lookAt(this.#lookat);
  2591. this.#camera.updateProjectionMatrix();
  2592. this.changedLight(box);
  2593. if (this.#controls) {
  2594. this.#controls.target.copy(this.#lookat);
  2595. if (!only_set) this.#controls.update();
  2596. }
  2597. // recheck which elements to draw
  2598. if (this.ctrl.select_in_view && !only_set)
  2599. this.startDrawGeometry();
  2600. }
  2601. /** @summary Specifies camera position as rotation around geometry center */
  2602. setCameraPosition(rotatey, rotatez, zoom) {
  2603. if (!this.ctrl) return;
  2604. this.ctrl.rotatey = rotatey || 0;
  2605. this.ctrl.rotatez = rotatez || 0;
  2606. let preserve_zoom = false;
  2607. if (zoom && Number.isFinite(zoom))
  2608. this.ctrl.zoom = zoom;
  2609. else
  2610. preserve_zoom = true;
  2611. this.adjustCameraPosition(false, preserve_zoom);
  2612. }
  2613. /** @summary Specifies camera position and point to which it looks to
  2614. @desc Both specified in absolute coordinates */
  2615. setCameraPositionAndLook(camx, camy, camz, lookx, looky, lookz) {
  2616. if (!this.ctrl)
  2617. return;
  2618. this.ctrl.camx = camx;
  2619. this.ctrl.camy = camy;
  2620. this.ctrl.camz = camz;
  2621. this.ctrl.camlx = lookx;
  2622. this.ctrl.camly = looky;
  2623. this.ctrl.camlz = lookz;
  2624. this.adjustCameraPosition(false);
  2625. }
  2626. /** @summary focus on item */
  2627. focusOnItem(itemname) {
  2628. if (!itemname || !this.#clones)
  2629. return;
  2630. const stack = this.#clones.findStackByName(itemname);
  2631. if (stack)
  2632. this.focusCamera(this.#clones.resolveStack(stack, true), false);
  2633. }
  2634. /** @summary focus camera on specified position */
  2635. focusCamera(focus, autoClip) {
  2636. if (this.ctrl.project || this.isOrthoCamera()) {
  2637. this.adjustCameraPosition(true);
  2638. return this.render3D();
  2639. }
  2640. let box = new THREE.Box3();
  2641. if (focus === undefined)
  2642. box = this.getGeomBoundingBox();
  2643. else if (focus instanceof THREE.Mesh)
  2644. box.setFromObject(focus);
  2645. else {
  2646. const center = new THREE.Vector3().setFromMatrixPosition(focus.matrix),
  2647. node = focus.node,
  2648. halfDelta = new THREE.Vector3(node.fDX, node.fDY, node.fDZ).multiplyScalar(0.5);
  2649. box.min = center.clone().sub(halfDelta);
  2650. box.max = center.clone().add(halfDelta);
  2651. }
  2652. const sizex = box.max.x - box.min.x,
  2653. sizey = box.max.y - box.min.y,
  2654. sizez = box.max.z - box.min.z,
  2655. midx = (box.max.x + box.min.x)/2,
  2656. midy = (box.max.y + box.min.y)/2,
  2657. midz = (box.max.z + box.min.z)/2;
  2658. let position, frames = 50, step = 0;
  2659. if (this.ctrl._yup)
  2660. position = new THREE.Vector3(midx-2*Math.max(sizex, sizez), midy+2*sizey, midz-2*Math.max(sizex, sizez));
  2661. else
  2662. position = new THREE.Vector3(midx-2*Math.max(sizex, sizey), midy-2*Math.max(sizex, sizey), midz+2*sizez);
  2663. const target = new THREE.Vector3(midx, midy, midz),
  2664. oldTarget = this.#controls.target,
  2665. // Amount to change camera position at each step
  2666. posIncrement = position.sub(this.#camera.position).divideScalar(frames),
  2667. // Amount to change 'lookAt' so it will end pointed at target
  2668. targetIncrement = target.sub(oldTarget).divideScalar(frames);
  2669. autoClip = autoClip && this.#webgl;
  2670. // Automatic Clipping
  2671. if (autoClip) {
  2672. for (let axis = 0; axis < 3; ++axis) {
  2673. const cc = this.ctrl.clip[axis];
  2674. if (!cc.enabled) { cc.value = cc.min; cc.enabled = true; }
  2675. cc.inc = ((cc.min + cc.max) / 2 - cc.value) / frames;
  2676. }
  2677. this.updateClipping();
  2678. }
  2679. this.#animating = true;
  2680. const animate = () => {
  2681. if (this.#animating === undefined)
  2682. return;
  2683. if (this.#animating)
  2684. requestAnimationFrame(animate);
  2685. else if (!this.#geom_viewer)
  2686. this.startDrawGeometry();
  2687. const smoothFactor = -Math.cos((2.0*Math.PI*step)/frames) + 1.0;
  2688. this.#camera.position.add(posIncrement.clone().multiplyScalar(smoothFactor));
  2689. oldTarget.add(targetIncrement.clone().multiplyScalar(smoothFactor));
  2690. this.#lookat = oldTarget;
  2691. this.#camera.lookAt(this.#lookat);
  2692. this.#camera.updateProjectionMatrix();
  2693. const tm1 = new Date().getTime();
  2694. if (autoClip) {
  2695. for (let axis = 0; axis < 3; ++axis)
  2696. this.ctrl.clip[axis].value += this.ctrl.clip[axis].inc * smoothFactor;
  2697. this.updateClipping();
  2698. } else
  2699. this.render3D(0);
  2700. const tm2 = new Date().getTime();
  2701. if ((step === 0) && (tm2-tm1 > 200)) frames = 20;
  2702. step++;
  2703. this.#animating = step < frames;
  2704. };
  2705. animate();
  2706. // this.#controls.update();
  2707. }
  2708. /** @summary activate auto rotate */
  2709. autorotate(speed) {
  2710. const rotSpeed = (speed === undefined) ? 2.0 : speed;
  2711. let last = new Date();
  2712. const animate = () => {
  2713. if (!this.#renderer || !this.ctrl) return;
  2714. const current = new Date();
  2715. if (this.ctrl.rotate)
  2716. requestAnimationFrame(animate);
  2717. if (this.#controls) {
  2718. this.#controls.autoRotate = this.ctrl.rotate;
  2719. this.#controls.autoRotateSpeed = rotSpeed * (current.getTime() - last.getTime()) / 16.6666;
  2720. this.#controls.update();
  2721. }
  2722. last = new Date();
  2723. this.render3D(0);
  2724. };
  2725. if (this.#webgl) animate();
  2726. }
  2727. /** @summary called at the end of scene drawing */
  2728. completeScene() {
  2729. }
  2730. /** @summary Drawing with 'count' option
  2731. * @desc Scans hierarchy and check for unique nodes
  2732. * @return {Promise} with object drawing ready */
  2733. async drawCount(unqievis, clonetm) {
  2734. const makeTime = tm => (this.isBatchMode() ? 'anytime' : tm.toString()) + ' ms',
  2735. res = ['Unique nodes: ' + this.#clones.nodes.length,
  2736. 'Unique visible: ' + unqievis,
  2737. 'Time to clone: ' + makeTime(clonetm)];
  2738. // need to fill cached value line numvischld
  2739. this.#clones.scanVisible();
  2740. let nshapes = 0;
  2741. const arg = {
  2742. clones: this.#clones,
  2743. cnt: [],
  2744. func(node) {
  2745. if (this.cnt[this.last] === undefined)
  2746. this.cnt[this.last] = 1;
  2747. else
  2748. this.cnt[this.last]++;
  2749. nshapes += countNumShapes(this.clones.getNodeShape(node.id));
  2750. return true;
  2751. }
  2752. };
  2753. let tm1 = new Date().getTime(),
  2754. numvis = this.#clones.scanVisible(arg),
  2755. tm2 = new Date().getTime();
  2756. res.push(`Total visible nodes: ${numvis}`, `Total shapes: ${nshapes}`);
  2757. for (let lvl = 0; lvl < arg.cnt.length; ++lvl) {
  2758. if (arg.cnt[lvl] !== undefined)
  2759. res.push(` lvl${lvl}: ${arg.cnt[lvl]}`);
  2760. }
  2761. res.push(`Time to scan: ${makeTime(tm2-tm1)}`, '', 'Check timing for matrix calculations ...');
  2762. const elem = this.selectDom().style('overflow', 'auto');
  2763. if (this.isBatchMode())
  2764. elem.property('_json_object_', res);
  2765. else
  2766. res.forEach(str => elem.append('p').text(str));
  2767. return postponePromise(() => {
  2768. arg.domatrix = true;
  2769. tm1 = new Date().getTime();
  2770. numvis = this.#clones.scanVisible(arg);
  2771. tm2 = new Date().getTime();
  2772. const last_str = `Time to scan with matrix: ${makeTime(tm2-tm1)}`;
  2773. if (this.isBatchMode())
  2774. res.push(last_str);
  2775. else
  2776. elem.append('p').text(last_str);
  2777. return this;
  2778. }, 100);
  2779. }
  2780. /** @summary Handle drop operation
  2781. * @desc opt parameter can include function name like opt$func_name
  2782. * Such function should be possible to find via {@link findFunction}
  2783. * Function has to return Promise with objects to draw on geometry
  2784. * By default function with name 'extract_geo_tracks' is checked
  2785. * @return {Promise} handling of drop operation */
  2786. async performDrop(obj, itemname, hitem, opt) {
  2787. if (obj?.$kind === 'TTree') {
  2788. // drop tree means function call which must extract tracks from provided tree
  2789. let funcname = 'extract_geo_tracks';
  2790. if (opt && opt.indexOf('$') > 0) {
  2791. funcname = opt.slice(0, opt.indexOf('$'));
  2792. opt = opt.slice(opt.indexOf('$')+1);
  2793. }
  2794. const func = findFunction(funcname);
  2795. if (!func) return Promise.reject(Error(`Function ${funcname} not found`));
  2796. return func(obj, opt).then(tracks => {
  2797. if (!tracks) return this;
  2798. // FIXME: probably tracks should be remembered?
  2799. return this.drawExtras(tracks, '', false).then(() => {
  2800. this.updateClipping(true);
  2801. return this.render3D(100);
  2802. });
  2803. });
  2804. }
  2805. return this.drawExtras(obj, itemname).then(is_any => {
  2806. if (!is_any) return this;
  2807. if (hitem) hitem._painter = this; // set for the browser item back pointer
  2808. return this.render3D(100);
  2809. });
  2810. }
  2811. /** @summary function called when mouse is going over the item in the browser */
  2812. mouseOverHierarchy(on, itemname, hitem) {
  2813. if (!this.ctrl)
  2814. return; // protection for cleaned-up painter
  2815. const obj = hitem._obj;
  2816. // let's highlight tracks and hits only for the time being
  2817. if (!obj || (obj._typename !== clTEveTrack && obj._typename !== clTEvePointSet && obj._typename !== clTPolyMarker3D)) return;
  2818. this.highlightMesh(null, 0x00ff00, on ? obj : null);
  2819. }
  2820. /** @summary clear extra drawn objects like tracks or hits */
  2821. clearExtras() {
  2822. this.getExtrasContainer('delete');
  2823. this.#extra_objects = undefined; // workaround, later will be normal function
  2824. this.render3D();
  2825. }
  2826. /** @summary Register extra objects like tracks or hits
  2827. * @desc Rendered after main geometry volumes are created
  2828. * Check if object already exists to prevent duplication */
  2829. addExtra(obj, itemname) {
  2830. if (!this.#extra_objects)
  2831. this.#extra_objects = create(clTList);
  2832. if (this.#extra_objects.arr.indexOf(obj) >= 0)
  2833. return false;
  2834. this.#extra_objects.Add(obj, itemname);
  2835. delete obj.$hidden_via_menu; // remove previous hidden property
  2836. return true;
  2837. }
  2838. /** @summary manipulate visibility of extra objects, used for HierarchyPainter
  2839. * @private */
  2840. extraObjectVisible(hpainter, hitem, toggle) {
  2841. if (!this.#extra_objects)
  2842. return;
  2843. const itemname = hpainter.itemFullName(hitem);
  2844. let indx = this.#extra_objects.opt.indexOf(itemname);
  2845. if ((indx < 0) && hitem._obj) {
  2846. indx = this.#extra_objects.arr.indexOf(hitem._obj);
  2847. // workaround - if object found, replace its name
  2848. if (indx >= 0) this.#extra_objects.opt[indx] = itemname;
  2849. }
  2850. if (indx < 0)
  2851. return;
  2852. const obj = this.#extra_objects.arr[indx];
  2853. let res = Boolean(obj.$hidden_via_menu);
  2854. if (toggle) {
  2855. obj.$hidden_via_menu = res;
  2856. res = !res;
  2857. let mesh = null;
  2858. // either found painted object or just draw once again
  2859. this.#toplevel.traverse(node => { if (node.geo_object === obj) mesh = node; });
  2860. if (mesh) {
  2861. mesh.visible = res;
  2862. this.render3D();
  2863. } else if (res) {
  2864. this.drawExtras(obj, '', false).then(() => {
  2865. this.updateClipping(true);
  2866. this.render3D();
  2867. });
  2868. }
  2869. }
  2870. return res;
  2871. }
  2872. /** @summary Draw extra object like tracks
  2873. * @return {Promise} for ready */
  2874. async drawExtras(obj, itemname, add_objects, not_wait_render) {
  2875. // if object was hidden via menu, do not redraw it with next draw call
  2876. if (!obj?._typename || (!add_objects && obj.$hidden_via_menu))
  2877. return false;
  2878. let do_render = false;
  2879. if (add_objects === undefined) {
  2880. add_objects = true;
  2881. do_render = true;
  2882. } else if (not_wait_render)
  2883. do_render = true;
  2884. let promise = false;
  2885. if ((obj._typename === clTList) || (obj._typename === clTObjArray)) {
  2886. if (!obj.arr) return false;
  2887. const parr = [];
  2888. for (let n = 0; n < obj.arr.length; ++n) {
  2889. const sobj = obj.arr[n];
  2890. let sname = obj.opt ? obj.opt[n] : '';
  2891. if (!sname) sname = (itemname || '<prnt>') + `/[${n}]`;
  2892. parr.push(this.drawExtras(sobj, sname, add_objects));
  2893. }
  2894. promise = Promise.all(parr).then(ress => ress.indexOf(true) >= 0);
  2895. } else if (obj._typename === 'Mesh') {
  2896. // adding mesh as is
  2897. this.addToExtrasContainer(obj);
  2898. promise = Promise.resolve(true);
  2899. } else if (obj._typename === 'TGeoTrack') {
  2900. if (!add_objects || this.addExtra(obj, itemname))
  2901. promise = this.drawGeoTrack(obj, itemname);
  2902. } else if (obj._typename === clTPolyLine3D) {
  2903. if (!add_objects || this.addExtra(obj, itemname))
  2904. promise = this.drawPolyLine(obj, itemname);
  2905. } else if ((obj._typename === clTEveTrack) || (obj._typename === `${nsREX}REveTrack`)) {
  2906. if (!add_objects || this.addExtra(obj, itemname))
  2907. promise = this.drawEveTrack(obj, itemname);
  2908. } else if ((obj._typename === clTEvePointSet) || (obj._typename === `${nsREX}REvePointSet`) || (obj._typename === clTPolyMarker3D)) {
  2909. if (!add_objects || this.addExtra(obj, itemname))
  2910. promise = this.drawHit(obj, itemname);
  2911. } else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) {
  2912. if (!add_objects || this.addExtra(obj, itemname))
  2913. promise = this.drawExtraShape(obj, itemname);
  2914. }
  2915. return getPromise(promise).then(is_any => {
  2916. if (!is_any || !do_render)
  2917. return is_any;
  2918. this.updateClipping(true);
  2919. const pr = this.render3D(100, not_wait_render ? 'nopromise' : false);
  2920. return not_wait_render ? this : pr;
  2921. });
  2922. }
  2923. /** @summary returns container for extra objects */
  2924. getExtrasContainer(action, name) {
  2925. if (!this.#toplevel) return null;
  2926. if (!name) name = 'tracks';
  2927. let extras = null;
  2928. const lst = [];
  2929. for (let n = 0; n < this.#toplevel.children.length; ++n) {
  2930. const chld = this.#toplevel.children[n];
  2931. if (!chld._extras) continue;
  2932. if (action === 'collect') { lst.push(chld); continue; }
  2933. if (chld._extras === name) { extras = chld; break; }
  2934. }
  2935. if (action === 'collect') {
  2936. for (let k = 0; k < lst.length; ++k)
  2937. this.#toplevel.remove(lst[k]);
  2938. return lst;
  2939. }
  2940. if (action === 'delete') {
  2941. if (extras) this.#toplevel.remove(extras);
  2942. disposeThreejsObject(extras);
  2943. return null;
  2944. }
  2945. if ((action !== 'get') && !extras) {
  2946. extras = new THREE.Object3D();
  2947. extras._extras = name;
  2948. this.#toplevel.add(extras);
  2949. }
  2950. return extras;
  2951. }
  2952. /** @summary add object to extras container.
  2953. * @desc If fail, dispose object */
  2954. addToExtrasContainer(obj, name) {
  2955. const container = this.getExtrasContainer('', name);
  2956. if (container)
  2957. container.add(obj);
  2958. else {
  2959. console.warn('Fail to add object to extras');
  2960. disposeThreejsObject(obj);
  2961. }
  2962. }
  2963. /** @summary drawing TGeoTrack */
  2964. drawGeoTrack(track, itemname) {
  2965. if (!track?.fNpoints) return false;
  2966. const linewidth = browser.isWin ? 1 : (track.fLineWidth || 1), // line width not supported on windows
  2967. color = getColor(track.fLineColor) || '#ff00ff',
  2968. npoints = Math.round(track.fNpoints/4), // each track point has [x,y,z,t] coordinate
  2969. buf = new Float32Array((npoints-1)*6),
  2970. projv = this.ctrl.projectPos,
  2971. projx = (this.ctrl.project === 'x'),
  2972. projy = (this.ctrl.project === 'y'),
  2973. projz = (this.ctrl.project === 'z');
  2974. for (let k = 0, pos = 0; k < npoints-1; ++k, pos+=6) {
  2975. buf[pos] = projx ? projv : track.fPoints[k*4];
  2976. buf[pos+1] = projy ? projv : track.fPoints[k*4+1];
  2977. buf[pos+2] = projz ? projv : track.fPoints[k*4+2];
  2978. buf[pos+3] = projx ? projv : track.fPoints[k*4+4];
  2979. buf[pos+4] = projy ? projv : track.fPoints[k*4+5];
  2980. buf[pos+5] = projz ? projv : track.fPoints[k*4+6];
  2981. }
  2982. const lineMaterial = new THREE.LineBasicMaterial({ color, linewidth }),
  2983. line = createLineSegments(buf, lineMaterial);
  2984. line.defaultOrder = line.renderOrder = 1000000; // to bring line to the front
  2985. line.geo_name = itemname;
  2986. line.geo_object = track;
  2987. line.hightlightWidthScale = 2;
  2988. if (itemname?.indexOf('<prnt>/Tracks') === 0)
  2989. line.main_track = true;
  2990. this.addToExtrasContainer(line);
  2991. return true;
  2992. }
  2993. /** @summary drawing TPolyLine3D */
  2994. drawPolyLine(line, itemname) {
  2995. if (!line) return false;
  2996. const linewidth = browser.isWin ? 1 : (line.fLineWidth || 1),
  2997. color = getColor(line.fLineColor) || '#ff00ff',
  2998. npoints = line.fN,
  2999. fP = line.fP,
  3000. buf = new Float32Array((npoints-1)*6),
  3001. projv = this.ctrl.projectPos,
  3002. projx = (this.ctrl.project === 'x'),
  3003. projy = (this.ctrl.project === 'y'),
  3004. projz = (this.ctrl.project === 'z');
  3005. for (let k = 0, pos = 0; k < npoints-1; ++k, pos += 6) {
  3006. buf[pos] = projx ? projv : fP[k*3];
  3007. buf[pos+1] = projy ? projv : fP[k*3+1];
  3008. buf[pos+2] = projz ? projv : fP[k*3+2];
  3009. buf[pos+3] = projx ? projv : fP[k*3+3];
  3010. buf[pos+4] = projy ? projv : fP[k*3+4];
  3011. buf[pos+5] = projz ? projv : fP[k*3+5];
  3012. }
  3013. const lineMaterial = new THREE.LineBasicMaterial({ color, linewidth }),
  3014. line3d = createLineSegments(buf, lineMaterial);
  3015. line3d.defaultOrder = line3d.renderOrder = 1000000; // to bring line to the front
  3016. line3d.geo_name = itemname;
  3017. line3d.geo_object = line;
  3018. line3d.hightlightWidthScale = 2;
  3019. this.addToExtrasContainer(line3d);
  3020. return true;
  3021. }
  3022. /** @summary Drawing TEveTrack */
  3023. drawEveTrack(track, itemname) {
  3024. if (!track || (track.fN <= 0)) return false;
  3025. const linewidth = browser.isWin ? 1 : (track.fLineWidth || 1),
  3026. color = getColor(track.fLineColor) || '#ff00ff',
  3027. buf = new Float32Array((track.fN-1)*6),
  3028. projv = this.ctrl.projectPos,
  3029. projx = (this.ctrl.project === 'x'),
  3030. projy = (this.ctrl.project === 'y'),
  3031. projz = (this.ctrl.project === 'z');
  3032. for (let k = 0, pos = 0; k < track.fN-1; ++k, pos+=6) {
  3033. buf[pos] = projx ? projv : track.fP[k*3];
  3034. buf[pos+1] = projy ? projv : track.fP[k*3+1];
  3035. buf[pos+2] = projz ? projv : track.fP[k*3+2];
  3036. buf[pos+3] = projx ? projv : track.fP[k*3+3];
  3037. buf[pos+4] = projy ? projv : track.fP[k*3+4];
  3038. buf[pos+5] = projz ? projv : track.fP[k*3+5];
  3039. }
  3040. const lineMaterial = new THREE.LineBasicMaterial({ color, linewidth }),
  3041. line = createLineSegments(buf, lineMaterial);
  3042. line.defaultOrder = line.renderOrder = 1000000; // to bring line to the front
  3043. line.geo_name = itemname;
  3044. line.geo_object = track;
  3045. line.hightlightWidthScale = 2;
  3046. this.addToExtrasContainer(line);
  3047. return true;
  3048. }
  3049. /** @summary Drawing different hits types like TPolyMarker3D */
  3050. async drawHit(hit, itemname) {
  3051. if (!hit || !hit.fN || (hit.fN < 0))
  3052. return false;
  3053. // make hit size scaling factor of overall geometry size
  3054. // otherwise it is not possible to correctly see hits at all
  3055. const nhits = hit.fN,
  3056. projv = this.ctrl.projectPos,
  3057. projx = (this.ctrl.project === 'x'),
  3058. projy = (this.ctrl.project === 'y'),
  3059. projz = (this.ctrl.project === 'z'),
  3060. hit_scale = Math.max(hit.fMarkerSize * this.getOverallSize() * (this.getOptions().dummy ? 0.015 : 0.005), 0.2),
  3061. pnts = new PointsCreator(nhits, this.#webgl, hit_scale);
  3062. for (let i = 0; i < nhits; i++) {
  3063. pnts.addPoint(projx ? projv : hit.fP[i*3],
  3064. projy ? projv : hit.fP[i*3+1],
  3065. projz ? projv : hit.fP[i*3+2]);
  3066. }
  3067. return pnts.createPoints({ color: getColor(hit.fMarkerColor) || '#0000ff', style: hit.fMarkerStyle }).then(mesh => {
  3068. mesh.defaultOrder = mesh.renderOrder = 1000000; // to bring points to the front
  3069. mesh.highlightScale = 2;
  3070. mesh.geo_name = itemname;
  3071. mesh.geo_object = hit;
  3072. this.addToExtrasContainer(mesh);
  3073. return true; // indicate that rendering should be done
  3074. });
  3075. }
  3076. /** @summary Draw extra shape on the geometry */
  3077. drawExtraShape(obj, itemname) {
  3078. // eslint-disable-next-line no-use-before-define
  3079. const mesh = build(obj);
  3080. if (!mesh) return false;
  3081. mesh.geo_name = itemname;
  3082. mesh.geo_object = obj;
  3083. this.addToExtrasContainer(mesh);
  3084. return true;
  3085. }
  3086. /** @summary Search for specified node
  3087. * @private */
  3088. findNodeWithVolume(name, action, prnt, itemname, volumes) {
  3089. let first_level = false, res = null;
  3090. if (!prnt) {
  3091. prnt = this.getGeometry();
  3092. if (!prnt && (getNodeKind(prnt) !== kindGeo))
  3093. return null;
  3094. itemname = this.#geo_manager ? prnt.fName : '';
  3095. first_level = true;
  3096. volumes = [];
  3097. } else {
  3098. if (itemname) itemname += '/';
  3099. itemname += prnt.fName;
  3100. }
  3101. if (!prnt.fVolume || prnt.fVolume._searched) return null;
  3102. if (name.test(prnt.fVolume.fName)) {
  3103. res = action({ node: prnt, item: itemname });
  3104. if (res) return res;
  3105. }
  3106. prnt.fVolume._searched = true;
  3107. volumes.push(prnt.fVolume);
  3108. if (prnt.fVolume.fNodes) {
  3109. for (let n = 0, len = prnt.fVolume.fNodes.arr.length; n < len; ++n) {
  3110. res = this.findNodeWithVolume(name, action, prnt.fVolume.fNodes.arr[n], itemname, volumes);
  3111. if (res) break;
  3112. }
  3113. }
  3114. if (first_level) {
  3115. for (let n = 0, len = volumes.length; n < len; ++n)
  3116. delete volumes[n]._searched;
  3117. }
  3118. return res;
  3119. }
  3120. /** @summary Search for created shape for nodeid
  3121. * @desc Used in ROOT geometry painter */
  3122. findNodeShape(nodeid) {
  3123. if ((nodeid !== undefined) && !this.#draw_nodes) {
  3124. for (let k = 0; k < this.#draw_nodes.length; ++k) {
  3125. const item = this.geo_painter._draw_nodes[k];
  3126. if ((item.nodeid === nodeid) && item.server_shape)
  3127. return item.server_shape;
  3128. }
  3129. }
  3130. }
  3131. /** @summary Process script option - load and execute some gGeoManager-related calls */
  3132. async loadMacro(script_name) {
  3133. const result = { obj: this.getGeometry(), prefix: '' };
  3134. if (this.#geo_manager)
  3135. result.prefix = result.obj.fName;
  3136. if (!script_name || (script_name.length < 3) || (getNodeKind(result.obj) !== kindGeo))
  3137. return result;
  3138. const mgr = {
  3139. GetVolume: name => {
  3140. const regexp = new RegExp('^'+name+'$'),
  3141. currnode = this.findNodeWithVolume(regexp, arg => arg);
  3142. if (!currnode) console.log(`Did not found ${name} volume`);
  3143. // return proxy object with several methods, typically used in ROOT geom scripts
  3144. return {
  3145. found: currnode,
  3146. fVolume: currnode?.node?.fVolume,
  3147. InvisibleAll(flag) {
  3148. setInvisibleAll(this.fVolume, flag);
  3149. },
  3150. Draw() {
  3151. if (!this.found || !this.fVolume) return;
  3152. result.obj = this.found.node;
  3153. result.prefix = this.found.item;
  3154. console.log(`Select volume for drawing ${this.fVolume.fName} ${result.prefix}`);
  3155. },
  3156. SetTransparency(lvl) {
  3157. if (this.fVolume?.fMedium?.fMaterial)
  3158. this.fVolume.fMedium.fMaterial.fFillStyle = 3000 + lvl;
  3159. },
  3160. SetLineColor(col) {
  3161. if (this.fVolume) this.fVolume.fLineColor = col;
  3162. }
  3163. };
  3164. },
  3165. DefaultColors: () => {
  3166. this.ctrl.dflt_colors = true;
  3167. },
  3168. SetMaxVisNodes: limit => {
  3169. if (!this.ctrl.maxnodes)
  3170. this.ctrl.maxnodes = parseInt(limit) || 0;
  3171. },
  3172. SetVisLevel: limit => {
  3173. if (!this.ctrl.vislevel)
  3174. this.ctrl.vislevel = parseInt(limit) || 0;
  3175. }
  3176. };
  3177. showProgress(`Loading macro ${script_name}`);
  3178. return httpRequest(script_name, 'text').then(script => {
  3179. const lines = script.split('\n');
  3180. let indx = 0;
  3181. while (indx < lines.length) {
  3182. let line = lines[indx++].trim();
  3183. if (line.indexOf('//') === 0)
  3184. continue;
  3185. if (line.indexOf('gGeoManager') < 0) continue;
  3186. line = line.replace('->GetVolume', '.GetVolume');
  3187. line = line.replace('->InvisibleAll', '.InvisibleAll');
  3188. line = line.replace('->SetMaxVisNodes', '.SetMaxVisNodes');
  3189. line = line.replace('->DefaultColors', '.DefaultColors');
  3190. line = line.replace('->Draw', '.Draw');
  3191. line = line.replace('->SetTransparency', '.SetTransparency');
  3192. line = line.replace('->SetLineColor', '.SetLineColor');
  3193. line = line.replace('->SetVisLevel', '.SetVisLevel');
  3194. if (line.indexOf('->') >= 0)
  3195. continue;
  3196. try {
  3197. const func = new Function('gGeoManager', line);
  3198. func(mgr);
  3199. } catch {
  3200. console.error(`Problem by processing ${line}`);
  3201. }
  3202. }
  3203. return result;
  3204. }).catch(() => {
  3205. console.error(`Fail to load ${script_name}`);
  3206. return result;
  3207. });
  3208. }
  3209. /** @summary Extract shapes from draw message of geometry painter
  3210. * @desc For the moment used in batch production */
  3211. extractRawShapes(draw_msg, recreate) {
  3212. let nodes = null, old_gradpersegm = 0;
  3213. // array for descriptors for each node
  3214. // if array too large (>1M), use JS object while only ~1K nodes are expected to be used
  3215. if (recreate) {
  3216. // if (draw_msg.kind !== 'draw') return false;
  3217. nodes = (draw_msg.numnodes > 1e6) ? { length: draw_msg.numnodes } : new Array(draw_msg.numnodes); // array for all nodes
  3218. }
  3219. draw_msg.nodes.forEach(node => {
  3220. node = ClonedNodes.formatServerElement(node);
  3221. if (nodes)
  3222. nodes[node.id] = node;
  3223. else
  3224. this.#clones.updateNode(node);
  3225. });
  3226. if (recreate) {
  3227. this.assignClones(new ClonedNodes(null, nodes), true);
  3228. this.#clones.name_prefix = this.#clones.getNodeName(0);
  3229. this.#clones.setConfig(this.ctrl);
  3230. // normally only need when making selection, not used in geo viewer
  3231. // this.geo#clones.setMaxVisNodes(draw_msg.maxvisnodes);
  3232. // this.geo#clones.setVisLevel(draw_msg.vislevel);
  3233. // TODO: provide from server
  3234. this.#clones.maxdepth = 20;
  3235. }
  3236. let nsegm = 0;
  3237. if (draw_msg.cfg)
  3238. nsegm = draw_msg.cfg.nsegm;
  3239. if (nsegm) {
  3240. old_gradpersegm = geoCfg('GradPerSegm');
  3241. geoCfg('GradPerSegm', 360 / Math.max(nsegm, 6));
  3242. }
  3243. for (let cnt = 0; cnt < draw_msg.visibles.length; ++cnt) {
  3244. const item = draw_msg.visibles[cnt], rd = item.ri;
  3245. // entry may be provided without shape - it is ok
  3246. if (rd)
  3247. item.server_shape = rd.server_shape = createServerGeometry(rd, nsegm);
  3248. }
  3249. if (old_gradpersegm)
  3250. geoCfg('GradPerSegm', old_gradpersegm);
  3251. return true;
  3252. }
  3253. /** @summary Prepare drawings
  3254. * @desc Return value used as promise for painter */
  3255. async prepareObjectDraw(draw_obj, name_prefix) {
  3256. // if did cleanup - ignore all kind of activity
  3257. if (this.#did_cleanup)
  3258. return null;
  3259. if (name_prefix === '__geom_viewer_append__') {
  3260. this.#new_append_nodes = draw_obj;
  3261. this.ctrl.use_worker = 0;
  3262. this.#geom_viewer = true; // indicate that working with geom viewer
  3263. } else if ((name_prefix === '__geom_viewer_selection__') && this.#clones) {
  3264. // these are selection done from geom viewer
  3265. this.#new_draw_nodes = draw_obj;
  3266. this.ctrl.use_worker = 0;
  3267. this.#geom_viewer = true; // indicate that working with geom viewer
  3268. } else if (this.getCentral())
  3269. this.assignClones(this.getCentral().getClones(), false);
  3270. else if (!draw_obj)
  3271. this.assignClones(undefined, undefined);
  3272. else {
  3273. this.#start_drawing_time = new Date().getTime();
  3274. this.assignClones(new ClonedNodes(draw_obj), true);
  3275. let lvl = this.ctrl.vislevel, maxnodes = this.ctrl.maxnodes;
  3276. if (this.#geo_manager) {
  3277. if (!lvl && this.#geo_manager.fVisLevel)
  3278. lvl = this.#geo_manager.fVisLevel;
  3279. if (!maxnodes)
  3280. maxnodes = this.#geo_manager.fMaxVisNodes;
  3281. }
  3282. this.#clones.setVisLevel(lvl);
  3283. this.#clones.setMaxVisNodes(maxnodes, this.ctrl.more);
  3284. this.#clones.setConfig(this.ctrl);
  3285. this.#clones.name_prefix = name_prefix;
  3286. const hide_top_volume = Boolean(this.#geo_manager) && !this.ctrl.showtop;
  3287. let uniquevis = this.ctrl.no_screen ? 0 : this.#clones.markVisibles(true, false, hide_top_volume);
  3288. if (uniquevis <= 0)
  3289. uniquevis = this.#clones.markVisibles(false, false, hide_top_volume);
  3290. else
  3291. uniquevis = this.#clones.markVisibles(true, true, hide_top_volume); // copy bits once and use normal visibility bits
  3292. this.#clones.produceIdShifts();
  3293. const spent = new Date().getTime() - this.#start_drawing_time;
  3294. if (!this.#scene && (settings.Debug || spent > 1000))
  3295. console.log(`Creating clones ${this.#clones.nodes.length} takes ${spent} ms uniquevis ${uniquevis}`);
  3296. if (this.ctrl._count)
  3297. return this.drawCount(uniquevis, spent);
  3298. }
  3299. let promise = Promise.resolve(true);
  3300. if (!this.#scene) {
  3301. this.#first_drawing = true;
  3302. const pp = this.getPadPainter();
  3303. this.#on_pad = Boolean(pp);
  3304. if (this.#on_pad) {
  3305. let size, render3d, fp;
  3306. promise = ensureTCanvas(this, '3d').then(() => {
  3307. if (pp.fillatt?.color)
  3308. this.ctrl.background = pp.fillatt.color;
  3309. fp = this.getFramePainter();
  3310. this.batch_mode = pp.isBatchMode();
  3311. render3d = getRender3DKind(undefined, this.batch_mode);
  3312. assign3DHandler(fp);
  3313. fp.mode3d = true;
  3314. size = fp.getSizeFor3d(undefined, render3d);
  3315. this.#fit_main_area = (size.can3d === -1);
  3316. return this.createScene(size.width, size.height, render3d)
  3317. .then(dom => fp.add3dCanvas(size, dom, render3d === constants.Render3D.WebGL));
  3318. });
  3319. } else {
  3320. const dom = this.selectDom('origin');
  3321. this.batch_mode = isBatchMode() || (!dom.empty() && dom.property('_batch_mode'));
  3322. this.batch_format = dom.property('_batch_format');
  3323. const render3d = getRender3DKind(this.getOptions().Render3D, this.batch_mode);
  3324. // activate worker
  3325. if ((this.ctrl.use_worker > 0) && !this.batch_mode)
  3326. this.startWorker();
  3327. assign3DHandler(this);
  3328. const size = this.getSizeFor3d(undefined, render3d);
  3329. this.#fit_main_area = (size.can3d === -1);
  3330. promise = this.createScene(size.width, size.height, render3d)
  3331. .then(dom2 => this.add3dCanvas(size, dom2, this.#webgl));
  3332. }
  3333. }
  3334. return promise.then(() => {
  3335. // this is limit for the visible faces, number of volumes does not matter
  3336. if (this.#first_drawing && !this.ctrl.maxfaces)
  3337. this.ctrl.maxfaces = 200000 * this.ctrl.more;
  3338. // set top painter only when first child exists
  3339. this.setAsMainPainter();
  3340. this.createToolbar();
  3341. // just draw extras and complete drawing if there are no main model
  3342. if (!this.#clones)
  3343. return this.completeDraw();
  3344. return new Promise(resolveFunc => {
  3345. this.#draw_resolveFuncs.push(resolveFunc);
  3346. this.showDrawInfo('Drawing geometry');
  3347. this.startDrawGeometry(true);
  3348. });
  3349. });
  3350. }
  3351. /** @summary methods show info when first geometry drawing is performed */
  3352. showDrawInfo(msg) {
  3353. if (this.isBatchMode() || !this.#first_drawing || !this.#start_drawing_time)
  3354. return;
  3355. const main = this.#renderer.domElement.parentNode;
  3356. if (!main) return;
  3357. let info = main.querySelector('.geo_info');
  3358. if (!msg)
  3359. info?.remove();
  3360. else {
  3361. const spent = (new Date().getTime() - this.#start_drawing_time)*1e-3;
  3362. if (!info) {
  3363. info = getDocument().createElement('p');
  3364. info.setAttribute('class', 'geo_info');
  3365. info.setAttribute('style', 'position: absolute; text-align: center; vertical-align: middle; top: 45%; left: 40%; color: red; font-size: 150%;');
  3366. main.append(info);
  3367. }
  3368. info.innerHTML = `${msg}, ${spent.toFixed(1)}s`;
  3369. }
  3370. }
  3371. /** @summary Reentrant method to perform geometry drawing step by step */
  3372. continueDraw() {
  3373. // nothing to do - exit
  3374. if (this.isStage(stageInit)) return;
  3375. const tm0 = new Date().getTime(),
  3376. interval = this.#first_drawing ? 1000 : 200;
  3377. let now = tm0;
  3378. while (true) {
  3379. const res = this.nextDrawAction();
  3380. if (!res) break;
  3381. now = new Date().getTime();
  3382. // stop creation after 100 sec, render as is
  3383. if (now - this.#start_render_tm > 1e5) {
  3384. this.changeStage(stageInit, 'Abort build after 100s');
  3385. break;
  3386. }
  3387. // if we are that fast, do next action
  3388. if ((res === true) && (now - tm0 < interval))
  3389. continue;
  3390. if ((now - tm0 > interval) || (res === 1) || (res === 2)) {
  3391. showProgress(this.#drawing_log);
  3392. this.showDrawInfo(this.#drawing_log);
  3393. if (this.#first_drawing && this.#webgl && (this.ctrl.info.num_meshes - this.#last_render_meshes > 100) && (now - this.#last_render_tm > 2.5*interval)) {
  3394. this.adjustCameraPosition();
  3395. this.render3D(-1);
  3396. this.#last_render_meshes = this.ctrl.info.num_meshes;
  3397. }
  3398. if (res !== 2)
  3399. setTimeout(() => this.continueDraw(), (res === 1) ? 100 : 1);
  3400. return;
  3401. }
  3402. }
  3403. const take_time = now - this.#start_render_tm;
  3404. if ((this.#first_drawing || this.#full_redrawing) && (settings.Debug || take_time > 1000))
  3405. console.log(`Create tm = ${take_time} meshes ${this.ctrl.info.num_meshes} faces ${this.ctrl.info.num_faces}`);
  3406. if (take_time > 300) {
  3407. showProgress('Rendering geometry');
  3408. this.showDrawInfo('Rendering');
  3409. return setTimeout(() => this.completeDraw(true), 10);
  3410. }
  3411. this.completeDraw(true);
  3412. }
  3413. /** @summary Checks camera position and recalculate rendering order if needed
  3414. * @param force - if specified, forces calculations of render order */
  3415. testCameraPosition(force) {
  3416. this.#camera.updateMatrixWorld();
  3417. this.drawOverlay();
  3418. const origin = this.#camera.position.clone();
  3419. if (!force && this.#last_camera_position) {
  3420. // if camera position does not changed a lot, ignore such change
  3421. const dist = this.#last_camera_position.distanceTo(origin);
  3422. if (dist < (this.#overall_size || 1000)*1e-4) return;
  3423. }
  3424. this.#last_camera_position = origin; // remember current camera position
  3425. if (this.ctrl._axis) {
  3426. const vect = (this.#controls?.target || this.#lookat).clone().sub(this.#camera.position).normalize();
  3427. this.getExtrasContainer('get', 'axis')?.traverse(obj3d => {
  3428. if (isFunc(obj3d._axis_flip))
  3429. obj3d._axis_flip(vect);
  3430. });
  3431. }
  3432. if (!this.ctrl.project)
  3433. produceRenderOrder(this.#toplevel, origin, this.ctrl.depthMethod, this.#clones);
  3434. }
  3435. /** @summary Call 3D rendering of the geometry
  3436. * @param tmout - specifies delay, after which actual rendering will be invoked
  3437. * @param [measure] - when true, for the first time printout rendering time
  3438. * @return {Promise} when tmout bigger than 0 is specified
  3439. * @desc Timeout used to avoid multiple rendering of the picture when several 3D drawings
  3440. * superimposed with each other. If tmout <= 0, rendering performed immediately
  3441. * Several special values are used:
  3442. * -1 - force recheck of rendering order based on camera position */
  3443. render3D(tmout, measure) {
  3444. if (!this.#renderer) {
  3445. if (this.#did_cleanup)
  3446. console.warn('try to render after cleanup');
  3447. else
  3448. console.warn('renderer object not exists - check code');
  3449. return this;
  3450. }
  3451. const ret_promise = (tmout !== undefined) && (tmout > 0) && (measure !== 'nopromise');
  3452. if (tmout === undefined)
  3453. tmout = 5; // by default, rendering happens with timeout
  3454. if ((tmout > 0) && this.#webgl) {
  3455. if (this.isBatchMode())
  3456. tmout = 1; // use minimal timeout in batch mode
  3457. if (ret_promise) {
  3458. return new Promise(resolveFunc => {
  3459. this.#render_resolveFuncs.push(resolveFunc);
  3460. if (!this.#render_tmout)
  3461. this.#render_tmout = setTimeout(() => this.render3D(0), tmout);
  3462. });
  3463. }
  3464. if (!this.#render_tmout)
  3465. this.#render_tmout = setTimeout(() => this.render3D(0), tmout);
  3466. return this;
  3467. }
  3468. if (this.#render_tmout) {
  3469. clearTimeout(this.#render_tmout);
  3470. this.#render_tmout = undefined;
  3471. }
  3472. beforeRender3D(this.#renderer);
  3473. const tm1 = new Date();
  3474. if (this.#adjust_camera_with_render) {
  3475. this.adjustCameraPosition('only_set');
  3476. this.#adjust_camera_with_render = undefined;
  3477. }
  3478. this.testCameraPosition(tmout === -1);
  3479. // its needed for outlinePass - do rendering, most consuming time
  3480. if (this.#webgl && this.#effectComposer?.passes)
  3481. this.#effectComposer.render();
  3482. else if (this.#webgl && this.#bloomComposer?.passes) {
  3483. this.#renderer.clear();
  3484. this.#camera.layers.set(_BLOOM_SCENE);
  3485. this.#bloomComposer.render();
  3486. this.#renderer.clearDepth();
  3487. this.#camera.layers.set(_ENTIRE_SCENE);
  3488. this.#renderer.render(this.#scene, this.#camera);
  3489. } else
  3490. this.#renderer.render(this.#scene, this.#camera);
  3491. const tm2 = new Date();
  3492. this.#last_render_tm = tm2.getTime();
  3493. if ((this.#first_render_tm === 0) && (measure === true)) {
  3494. this.#first_render_tm = tm2.getTime() - tm1.getTime();
  3495. if (this.#first_render_tm > 500)
  3496. console.log(`three.js r${THREE.REVISION}, first render tm = ${this.#first_render_tm}`);
  3497. }
  3498. afterRender3D(this.#renderer);
  3499. const arr = this.#render_resolveFuncs;
  3500. this.#render_resolveFuncs = [];
  3501. arr?.forEach(func => func(this));
  3502. }
  3503. /** @summary Start geo worker */
  3504. async startWorker() {
  3505. if (this.#worker)
  3506. return;
  3507. this.#worker_ready = false;
  3508. this.#worker_jobs = 0; // counter how many requests send to worker
  3509. let pr;
  3510. if (isNodeJs()) {
  3511. pr = import('node:worker_threads').then(h => {
  3512. const wrk = new h.Worker(source_dir.slice(7) + 'modules/geom/nodeworker.mjs', { type: 'module' });
  3513. wrk.on('message', msg => this.processWorkerReply(msg));
  3514. return wrk;
  3515. });
  3516. } else {
  3517. // Finally use ES6 module, see https://www.codedread.com/blog/archives/2017/10/19/web-workers-can-be-es6-modules-too/
  3518. const wrk = new Worker(source_dir + 'modules/geom/geoworker.mjs', { type: 'module' });
  3519. wrk.onmessage = e => this.processWorkerReply(e?.data);
  3520. pr = Promise.resolve(wrk);
  3521. }
  3522. return pr.then(wrk => {
  3523. this.#worker = wrk;
  3524. // send initialization message with clones
  3525. wrk.postMessage({
  3526. init: true, // indicate init command for worker
  3527. tm0: new Date().getTime(),
  3528. vislevel: this.#clones.getVisLevel(),
  3529. maxvisnodes: this.#clones.getMaxVisNodes(),
  3530. clones: this.#clones.nodes,
  3531. sortmap: this.#clones.sortmap
  3532. });
  3533. });
  3534. }
  3535. /** @summary check if one can submit request to worker
  3536. * @private */
  3537. canSubmitToWorker(force) {
  3538. return this.#worker ? this.#worker_ready && ((this.#worker_jobs === 0) || force) : false;
  3539. }
  3540. /** @summary submit request to worker
  3541. * @private */
  3542. submitToWorker(job) {
  3543. if (!this.#worker)
  3544. return false;
  3545. this.#worker_jobs++;
  3546. job.tm0 = new Date().getTime();
  3547. this.#worker.postMessage(job);
  3548. }
  3549. /** @summary process reply from worker
  3550. * @private */
  3551. processWorkerReply(job) {
  3552. if (!job || !isObject(job))
  3553. return;
  3554. if (job.log)
  3555. return console.log(`geo: ${job.log}`);
  3556. if (job.progress)
  3557. return showProgress(job.progress);
  3558. job.tm3 = new Date().getTime();
  3559. if (job.init) {
  3560. this.#worker_ready = true;
  3561. return;
  3562. }
  3563. this.#worker_jobs--;
  3564. if ('collect' in job) {
  3565. this.#new_draw_nodes = job.new_nodes;
  3566. this.#draw_all_nodes = job.complete;
  3567. this.changeStage(stageAnalyze);
  3568. // invoke methods immediately
  3569. return this.continueDraw();
  3570. }
  3571. if ('shapes' in job) {
  3572. for (let n=0; n<job.shapes.length; ++n) {
  3573. const item = job.shapes[n],
  3574. origin = this.#build_shapes[n];
  3575. if (item.buf_pos && item.buf_norm) {
  3576. if (!item.buf_pos.length)
  3577. origin.geom = null;
  3578. else if (item.buf_pos.length !== item.buf_norm.length) {
  3579. console.error(`item.buf_pos.length ${item.buf_pos.length} !== item.buf_norm.length ${item.buf_norm.length}`);
  3580. origin.geom = null;
  3581. } else {
  3582. origin.geom = new THREE.BufferGeometry();
  3583. origin.geom.setAttribute('position', new THREE.BufferAttribute(item.buf_pos, 3));
  3584. origin.geom.setAttribute('normal', new THREE.BufferAttribute(item.buf_norm, 3));
  3585. }
  3586. origin.ready = true;
  3587. origin.nfaces = item.nfaces;
  3588. }
  3589. }
  3590. job.tm4 = new Date().getTime();
  3591. this.changeStage(stageBuild); // first check which shapes are used, than build meshes
  3592. // invoke methods immediately
  3593. return this.continueDraw();
  3594. }
  3595. }
  3596. /** @summary start draw geometries on central and all subordinate painters
  3597. * @private */
  3598. testGeomChanges() {
  3599. if (this.getCentral()) {
  3600. console.warn('Get testGeomChanges call for subordinate painter');
  3601. return this.getCentral().testGeomChanges();
  3602. }
  3603. this.startDrawGeometry();
  3604. this.getSubordinates()?.forEach(p => p.startDrawGeometry());
  3605. }
  3606. /** @summary Draw axes and camera overlay */
  3607. drawAxesAndOverlay(norender) {
  3608. const res1 = this.drawAxes(),
  3609. res2 = this.drawOverlay();
  3610. if (!res1 && !res2)
  3611. return norender ? null : this.render3D();
  3612. return this.changedDepthMethod(norender ? 'norender' : undefined);
  3613. }
  3614. /** @summary Draw overlay for the orthographic cameras */
  3615. drawOverlay() {
  3616. this.getExtrasContainer('delete', 'overlay');
  3617. if (!this.isOrthoCamera() || (this.ctrl.camera_overlay === 'none'))
  3618. return false;
  3619. const zoom = 0.5 / this.#camera.zoom,
  3620. midx = (this.#camera.left + this.#camera.right) / 2,
  3621. midy = (this.#camera.bottom + this.#camera.top) / 2,
  3622. xmin = midx - (this.#camera.right - this.#camera.left) * zoom,
  3623. xmax = midx + (this.#camera.right - this.#camera.left) * zoom,
  3624. ymin = midy - (this.#camera.top - this.#camera.bottom) * zoom,
  3625. ymax = midy + (this.#camera.top - this.#camera.bottom) * zoom,
  3626. tick_size = (ymax - ymin) * 0.02,
  3627. text_size = (ymax - ymin) * 0.015,
  3628. grid_gap = (ymax - ymin) * 0.001,
  3629. x1 = xmin + text_size * 5, x2 = xmax - text_size * 5,
  3630. y1 = ymin + text_size * 3, y2 = ymax - text_size * 3,
  3631. x_handle = new TAxisPainter(null, create(clTAxis));
  3632. x_handle.configureAxis('xaxis', x1, x2, x1, x2, false, [x1, x2],
  3633. { log: 0, reverse: false });
  3634. const y_handle = new TAxisPainter(null, create(clTAxis));
  3635. y_handle.configureAxis('yaxis', y1, y2, y1, y2, false, [y1, y2],
  3636. { log: 0, reverse: false });
  3637. const ii = this.#camera.orthoIndicies ?? [0, 1, 2];
  3638. let buf, pos, midZ = 0, gridZ = 0;
  3639. if (this.#camera.orthoZ)
  3640. gridZ = midZ = this.#camera.orthoZ[0];
  3641. const addPoint = (x, y, z) => {
  3642. buf[pos+ii[0]] = x;
  3643. buf[pos+ii[1]] = y;
  3644. buf[pos+ii[2]] = z ?? gridZ;
  3645. pos += 3;
  3646. }, createText = (lbl, size) => {
  3647. const text3d = createTextGeometry(lbl, size);
  3648. text3d.computeBoundingBox();
  3649. text3d._width = text3d.boundingBox.max.x - text3d.boundingBox.min.x;
  3650. text3d._height = text3d.boundingBox.max.y - text3d.boundingBox.min.y;
  3651. text3d.translate(-text3d._width/2, -text3d._height/2, 0);
  3652. if (this.#camera.orthoSign < 0)
  3653. text3d.rotateY(Math.PI);
  3654. if (isFunc(this.#camera.orthoRotation))
  3655. this.#camera.orthoRotation(text3d);
  3656. return text3d;
  3657. }, createTextMesh = (geom, material, x, y, z) => {
  3658. const tgt = [0, 0, 0];
  3659. tgt[ii[0]] = x;
  3660. tgt[ii[1]] = y;
  3661. tgt[ii[2]] = z ?? gridZ;
  3662. const mesh = new THREE.Mesh(geom, material);
  3663. mesh.translateX(tgt[0]).translateY(tgt[1]).translateZ(tgt[2]);
  3664. return mesh;
  3665. };
  3666. if (this.ctrl.camera_overlay === 'bar') {
  3667. const container = this.getExtrasContainer('create', 'overlay');
  3668. let ox1 = xmin * 0.15 + xmax * 0.85,
  3669. ox2 = xmin * 0.05 + xmax * 0.95;
  3670. const oy1 = ymax * 0.9 + ymin * 0.1,
  3671. oy2 = ymax * 0.86 + ymin * 0.14,
  3672. ticks = x_handle.createTicks();
  3673. if (ticks.major?.length > 1) {
  3674. ox1 = ticks.major.at(-2);
  3675. ox2 = ticks.major.at(-1);
  3676. }
  3677. buf = new Float32Array(3*6);
  3678. pos = 0;
  3679. addPoint(ox1, oy1, midZ);
  3680. addPoint(ox1, oy2, midZ);
  3681. addPoint(ox1, (oy1 + oy2) / 2, midZ);
  3682. addPoint(ox2, (oy1 + oy2) / 2, midZ);
  3683. addPoint(ox2, oy1, midZ);
  3684. addPoint(ox2, oy2, midZ);
  3685. const lineMaterial = new THREE.LineBasicMaterial({ color: 'green' }),
  3686. textMaterial = new THREE.MeshBasicMaterial({ color: 'green', vertexColors: false });
  3687. container.add(createLineSegments(buf, lineMaterial));
  3688. const text3d = createText(x_handle.format(ox2 - ox1, true), Math.abs(oy2 - oy1));
  3689. container.add(createTextMesh(text3d, textMaterial, (ox2 + ox1) / 2, (oy1 + oy2) / 2 + text3d._height * 0.8, midZ));
  3690. return true;
  3691. }
  3692. const show_grid = this.ctrl.camera_overlay.indexOf('grid') === 0;
  3693. if (show_grid && this.#camera.orthoZ) {
  3694. if (this.ctrl.camera_overlay === 'gridf')
  3695. gridZ += this.#camera.orthoSign * this.#camera.orthoZ[1];
  3696. else if (this.ctrl.camera_overlay === 'gridb')
  3697. gridZ -= this.#camera.orthoSign * this.#camera.orthoZ[1];
  3698. }
  3699. if ((this.ctrl.camera_overlay === 'axis') || show_grid) {
  3700. const container = this.getExtrasContainer('create', 'overlay'),
  3701. lineMaterial = new THREE.LineBasicMaterial({ color: new THREE.Color('black') }),
  3702. gridMaterial1 = show_grid ? new THREE.LineBasicMaterial({ color: new THREE.Color(0xbbbbbb) }) : null,
  3703. gridMaterial2 = show_grid ? new THREE.LineDashedMaterial({ color: new THREE.Color(0xdddddd), dashSize: grid_gap, gapSize: grid_gap }) : null,
  3704. textMaterial = new THREE.MeshBasicMaterial({ color: 'black', vertexColors: false }),
  3705. xticks = x_handle.createTicks();
  3706. while (xticks.next()) {
  3707. const x = xticks.tick, k = (xticks.kind === 1) ? 1.0 : 0.6;
  3708. if (show_grid) {
  3709. buf = new Float32Array(2*3); pos = 0;
  3710. addPoint(x, ymax - k*tick_size - grid_gap);
  3711. addPoint(x, ymin + k*tick_size + grid_gap);
  3712. container.add(createLineSegments(buf, xticks.kind === 1 ? gridMaterial1 : gridMaterial2));
  3713. }
  3714. buf = new Float32Array(4*3); pos = 0;
  3715. addPoint(x, ymax);
  3716. addPoint(x, ymax - k*tick_size);
  3717. addPoint(x, ymin);
  3718. addPoint(x, ymin + k*tick_size);
  3719. container.add(createLineSegments(buf, lineMaterial));
  3720. if (xticks.kind !== 1) continue;
  3721. const text3d = createText(x_handle.format(x, true), text_size);
  3722. container.add(createTextMesh(text3d, textMaterial, x, ymax - tick_size - text_size/2 - text3d._height/2));
  3723. container.add(createTextMesh(text3d, textMaterial, x, ymin + tick_size + text_size/2 + text3d._height/2));
  3724. }
  3725. const yticks = y_handle.createTicks();
  3726. while (yticks.next()) {
  3727. const y = yticks.tick, k = (yticks.kind === 1) ? 1.0 : 0.6;
  3728. if (show_grid) {
  3729. buf = new Float32Array(2*3); pos = 0;
  3730. addPoint(xmin + k*tick_size + grid_gap, y);
  3731. addPoint(xmax - k*tick_size - grid_gap, y);
  3732. container.add(createLineSegments(buf, yticks.kind === 1 ? gridMaterial1 : gridMaterial2));
  3733. }
  3734. buf = new Float32Array(4*3); pos = 0;
  3735. addPoint(xmin, y);
  3736. addPoint(xmin + k*tick_size, y);
  3737. addPoint(xmax, y);
  3738. addPoint(xmax - k*tick_size, y);
  3739. container.add(createLineSegments(buf, lineMaterial));
  3740. if (yticks.kind !== 1) continue;
  3741. const text3d = createText(y_handle.format(y, true), text_size);
  3742. container.add(createTextMesh(text3d, textMaterial, xmin + tick_size + text_size/2 + text3d._width/2, y));
  3743. container.add(createTextMesh(text3d, textMaterial, xmax - tick_size - text_size/2 - text3d._width/2, y));
  3744. }
  3745. return true;
  3746. }
  3747. return false;
  3748. }
  3749. /** @summary Draw axes if configured, otherwise just remove completely */
  3750. drawAxes() {
  3751. this.getExtrasContainer('delete', 'axis');
  3752. if (!this.ctrl._axis)
  3753. return false;
  3754. const box = this.getGeomBoundingBox(this.#toplevel, this.#superimpose ? 'original' : undefined),
  3755. container = this.getExtrasContainer('create', 'axis'),
  3756. text_size = 0.02 * Math.max(box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z),
  3757. center = [0, 0, 0],
  3758. names = ['x', 'y', 'z'],
  3759. labels = ['X', 'Y', 'Z'],
  3760. colors = ['red', 'green', 'blue'],
  3761. ortho = this.isOrthoCamera(),
  3762. ckind = this.ctrl.camera_kind ?? 'perspective';
  3763. if (this.ctrl._axis === 2) {
  3764. for (let naxis = 0; naxis < 3; ++naxis) {
  3765. const name = names[naxis];
  3766. if ((box.min[name] <= 0) && (box.max[name] >= 0)) continue;
  3767. center[naxis] = (box.min[name] + box.max[name])/2;
  3768. }
  3769. }
  3770. for (let naxis = 0; naxis < 3; ++naxis) {
  3771. // exclude axis which is not seen
  3772. if (ortho && ckind.indexOf(labels[naxis]) < 0) continue;
  3773. const buf = new Float32Array(6),
  3774. color = colors[naxis],
  3775. name = names[naxis],
  3776. valueToString = val => {
  3777. if (!val) return '0';
  3778. const lg = Math.log10(Math.abs(val));
  3779. if (lg < 0) {
  3780. if (lg > -1) return val.toFixed(2);
  3781. if (lg > -2) return val.toFixed(3);
  3782. } else {
  3783. if (lg < 2) return val.toFixed(1);
  3784. if (lg < 4) return val.toFixed(0);
  3785. }
  3786. return val.toExponential(2);
  3787. },
  3788. lbl = valueToString(box.max[name]) + ' ' + labels[naxis];
  3789. buf[0] = box.min.x;
  3790. buf[1] = box.min.y;
  3791. buf[2] = box.min.z;
  3792. buf[3] = box.min.x;
  3793. buf[4] = box.min.y;
  3794. buf[5] = box.min.z;
  3795. switch (naxis) {
  3796. case 0: buf[3] = box.max.x; break;
  3797. case 1: buf[4] = box.max.y; break;
  3798. case 2: buf[5] = box.max.z; break;
  3799. }
  3800. if (this.ctrl._axis === 2) {
  3801. for (let k = 0; k < 6; ++k)
  3802. if ((k % 3) !== naxis) buf[k] = center[k%3];
  3803. }
  3804. const lineMaterial = new THREE.LineBasicMaterial({ color });
  3805. let mesh = createLineSegments(buf, lineMaterial);
  3806. mesh._no_clip = true; // skip from clipping
  3807. container.add(mesh);
  3808. const textMaterial = new THREE.MeshBasicMaterial({ color, vertexColors: false });
  3809. if ((center[naxis] === 0) && (center[naxis] >= box.min[name]) && (center[naxis] <= box.max[name])) {
  3810. if ((this.ctrl._axis !== 2) || (naxis === 0)) {
  3811. const geom = ortho ? new THREE.CircleGeometry(text_size*0.25) : new THREE.SphereGeometry(text_size*0.25);
  3812. mesh = new THREE.Mesh(geom, textMaterial);
  3813. mesh.translateX(naxis === 0 ? center[0] : buf[0]);
  3814. mesh.translateY(naxis === 1 ? center[1] : buf[1]);
  3815. mesh.translateZ(naxis === 2 ? center[2] : buf[2]);
  3816. mesh._no_clip = true;
  3817. container.add(mesh);
  3818. }
  3819. }
  3820. let text3d = createTextGeometry(lbl, text_size);
  3821. mesh = new THREE.Mesh(text3d, textMaterial);
  3822. mesh._no_clip = true; // skip from clipping
  3823. function setSideRotation(mesh2, normal) {
  3824. mesh2._other_side = false;
  3825. mesh2._axis_norm = normal ?? new THREE.Vector3(1, 0, 0);
  3826. mesh2._axis_flip = function(vect) {
  3827. const other_side = vect.dot(this._axis_norm) < 0;
  3828. if (this._other_side !== other_side) {
  3829. this._other_side = other_side;
  3830. this.rotateY(Math.PI);
  3831. }
  3832. };
  3833. }
  3834. function setTopRotation(mesh2, first_angle = -1) {
  3835. mesh2._last_angle = first_angle;
  3836. mesh2._axis_flip = function(vect) {
  3837. let angle;
  3838. switch (this._axis_name) {
  3839. case 'x': angle = -Math.atan2(vect.y, vect.z); break;
  3840. case 'y': angle = -Math.atan2(vect.z, vect.x); break;
  3841. default: angle = Math.atan2(vect.y, vect.x);
  3842. }
  3843. angle = Math.round(angle / Math.PI * 2 + 2) % 4;
  3844. if (this._last_angle !== angle) {
  3845. this.rotateX((angle - this._last_angle) * Math.PI/2);
  3846. this._last_angle = angle;
  3847. }
  3848. };
  3849. }
  3850. let textbox = new THREE.Box3().setFromObject(mesh);
  3851. text3d.translate(-textbox.max.x*0.5, -textbox.max.y/2, 0);
  3852. mesh.translateX(buf[3]);
  3853. mesh.translateY(buf[4]);
  3854. mesh.translateZ(buf[5]);
  3855. mesh._axis_name = name;
  3856. if (naxis === 0) {
  3857. if (ortho && ckind.indexOf('OX') > 0)
  3858. setTopRotation(mesh, 0);
  3859. else if (ortho ? ckind.indexOf('OY') > 0 : this.ctrl._yup)
  3860. setSideRotation(mesh, new THREE.Vector3(0, 0, -1));
  3861. else {
  3862. setSideRotation(mesh, new THREE.Vector3(0, 1, 0));
  3863. mesh.rotateX(Math.PI/2);
  3864. }
  3865. mesh.translateX(text_size*0.5 + textbox.max.x*0.5);
  3866. } else if (naxis === 1) {
  3867. if (ortho ? ckind.indexOf('OY') > 0 : this.ctrl._yup) {
  3868. setTopRotation(mesh, 2);
  3869. mesh.rotateX(-Math.PI/2);
  3870. mesh.rotateY(-Math.PI/2);
  3871. mesh.translateX(text_size*0.5 + textbox.max.x*0.5);
  3872. } else {
  3873. setSideRotation(mesh);
  3874. mesh.rotateX(Math.PI/2);
  3875. mesh.rotateY(-Math.PI/2);
  3876. mesh.translateX(-textbox.max.x*0.5 - text_size*0.5);
  3877. }
  3878. } else if (naxis === 2) {
  3879. if (ortho ? ckind.indexOf('OZ') < 0 : this.ctrl._yup) {
  3880. const zox = ortho && (ckind.indexOf('ZOX') > 0 || ckind.indexOf('ZNOX') > 0);
  3881. setSideRotation(mesh, zox ? new THREE.Vector3(0, -1, 0) : undefined);
  3882. mesh.rotateY(-Math.PI/2);
  3883. if (zox) mesh.rotateX(-Math.PI/2);
  3884. } else {
  3885. setTopRotation(mesh);
  3886. mesh.rotateX(Math.PI/2);
  3887. mesh.rotateZ(Math.PI/2);
  3888. }
  3889. mesh.translateX(text_size*0.5 + textbox.max.x*0.5);
  3890. }
  3891. container.add(mesh);
  3892. text3d = createTextGeometry(valueToString(box.min[name]), text_size);
  3893. mesh = new THREE.Mesh(text3d, textMaterial);
  3894. mesh._no_clip = true; // skip from clipping
  3895. textbox = new THREE.Box3().setFromObject(mesh);
  3896. text3d.translate(-textbox.max.x*0.5, -textbox.max.y/2, 0);
  3897. mesh._axis_name = name;
  3898. mesh.translateX(buf[0]);
  3899. mesh.translateY(buf[1]);
  3900. mesh.translateZ(buf[2]);
  3901. if (naxis === 0) {
  3902. if (ortho && ckind.indexOf('OX') > 0)
  3903. setTopRotation(mesh, 0);
  3904. else if (ortho ? ckind.indexOf('OY') > 0 : this.ctrl._yup)
  3905. setSideRotation(mesh, new THREE.Vector3(0, 0, -1));
  3906. else {
  3907. setSideRotation(mesh, new THREE.Vector3(0, 1, 0));
  3908. mesh.rotateX(Math.PI/2);
  3909. }
  3910. mesh.translateX(-text_size*0.5 - textbox.max.x*0.5);
  3911. } else if (naxis === 1) {
  3912. if (ortho ? ckind.indexOf('OY') > 0 : this.ctrl._yup) {
  3913. setTopRotation(mesh, 2);
  3914. mesh.rotateX(-Math.PI/2);
  3915. mesh.rotateY(-Math.PI/2);
  3916. mesh.translateX(-textbox.max.x*0.5 - text_size*0.5);
  3917. } else {
  3918. setSideRotation(mesh);
  3919. mesh.rotateX(Math.PI/2);
  3920. mesh.rotateY(-Math.PI/2);
  3921. mesh.translateX(textbox.max.x*0.5 + text_size*0.5);
  3922. }
  3923. } else if (naxis === 2) {
  3924. if (ortho ? ckind.indexOf('OZ') < 0 : this.ctrl._yup) {
  3925. const zox = ortho && (ckind.indexOf('ZOX') > 0 || ckind.indexOf('ZNOX') > 0);
  3926. setSideRotation(mesh, zox ? new THREE.Vector3(0, -1, 0) : undefined);
  3927. mesh.rotateY(-Math.PI/2);
  3928. if (zox) mesh.rotateX(-Math.PI/2);
  3929. } else {
  3930. setTopRotation(mesh);
  3931. mesh.rotateX(Math.PI/2);
  3932. mesh.rotateZ(Math.PI/2);
  3933. }
  3934. mesh.translateX(-textbox.max.x*0.5 - text_size*0.5);
  3935. }
  3936. container.add(mesh);
  3937. }
  3938. // after creating axes trigger rendering and recalculation of depth
  3939. return true;
  3940. }
  3941. /** @summary Set axes visibility 0 - off, 1 - on, 2 - centered */
  3942. setAxesDraw(on) {
  3943. if (on === 'toggle')
  3944. this.ctrl._axis = this.ctrl._axis ? 0 : 1;
  3945. else
  3946. this.ctrl._axis = (typeof on === 'number') ? on : (on ? 1 : 0);
  3947. return this.drawAxesAndOverlay();
  3948. }
  3949. /** @summary Set auto rotate mode */
  3950. setAutoRotate(on) {
  3951. if (this.ctrl.project) return;
  3952. if (on !== undefined) this.ctrl.rotate = on;
  3953. this.autorotate(2.5);
  3954. }
  3955. /** @summary Toggle wireframe mode */
  3956. toggleWireFrame() {
  3957. this.ctrl.wireframe = !this.ctrl.wireframe;
  3958. this.changedWireFrame();
  3959. }
  3960. /** @summary Specify wireframe mode */
  3961. setWireFrame(on) {
  3962. this.ctrl.wireframe = Boolean(on);
  3963. this.changedWireFrame();
  3964. }
  3965. /** @summary Specify showtop draw options, relevant only for TGeoManager */
  3966. setShowTop(on) {
  3967. this.ctrl.showtop = Boolean(on);
  3968. return this.startRedraw();
  3969. }
  3970. /** @summary Should be called when configuration of particular axis is changed */
  3971. changedClipping(naxis = -1) {
  3972. if ((naxis < 0) || this.ctrl.clip[naxis]?.enabled)
  3973. this.updateClipping(false, true);
  3974. }
  3975. /** @summary Should be called when depth test flag is changed */
  3976. changedDepthTest() {
  3977. if (!this.#toplevel) return;
  3978. const flag = this.ctrl.depthTest;
  3979. this.#toplevel.traverse(node => {
  3980. if (node instanceof THREE.Mesh)
  3981. node.material.depthTest = flag;
  3982. });
  3983. this.render3D(0);
  3984. }
  3985. /** @summary Should be called when depth method is changed */
  3986. changedDepthMethod(arg) {
  3987. // force recalculation of render order
  3988. this.#last_camera_position = undefined;
  3989. if (arg !== 'norender')
  3990. return this.render3D();
  3991. }
  3992. /** @summary Assign clipping attributes to the meshes - supported only for webgl */
  3993. updateClipping(without_render, force_traverse) {
  3994. // do not try clipping with SVG renderer
  3995. if (this.#renderer?.jsroot_render3d === constants.Render3D.SVG) return;
  3996. if (!this.#clip_planes) {
  3997. this.#clip_planes = [new THREE.Plane(new THREE.Vector3(1, 0, 0), 0),
  3998. new THREE.Plane(new THREE.Vector3(0, this.ctrl._yup ? -1 : 1, 0), 0),
  3999. new THREE.Plane(new THREE.Vector3(0, 0, this.ctrl._yup ? 1 : -1), 0)];
  4000. }
  4001. const clip = this.ctrl.clip,
  4002. clip_constants = [-1 * clip[0].value, clip[1].value, (this.ctrl._yup ? -1 : 1) * clip[2].value],
  4003. container = this.getExtrasContainer(this.ctrl.clipVisualize ? '' : 'delete', 'clipping');
  4004. let panels = [], changed = false,
  4005. clip_cfg = this.ctrl.clipIntersect ? 16 : 0;
  4006. for (let k = 0; k < 3; ++k) {
  4007. if (clip[k].enabled)
  4008. clip_cfg += 2 << k;
  4009. if (this.#clip_planes[k].constant !== clip_constants[k]) {
  4010. if (clip[k].enabled) changed = true;
  4011. this.#clip_planes[k].constant = clip_constants[k];
  4012. }
  4013. if (clip[k].enabled)
  4014. panels.push(this.#clip_planes[k]);
  4015. if (container && clip[k].enabled) {
  4016. const helper = new THREE.PlaneHelper(this.#clip_planes[k], (clip[k].max - clip[k].min));
  4017. helper._no_clip = true;
  4018. container.add(helper);
  4019. }
  4020. }
  4021. if (!panels.length)
  4022. panels = null;
  4023. if (this.#last_clip_cfg !== clip_cfg)
  4024. changed = true;
  4025. this.#last_clip_cfg = clip_cfg;
  4026. const any_clipping = Boolean(panels), ci = this.ctrl.clipIntersect,
  4027. material_side = any_clipping ? THREE.DoubleSide : THREE.FrontSide;
  4028. if (force_traverse || changed) {
  4029. this.#scene.traverse(node => {
  4030. if (!node._no_clip && (node.material?.clippingPlanes !== undefined)) {
  4031. if (node.material.clippingPlanes !== panels) {
  4032. node.material.clipIntersection = ci;
  4033. node.material.clippingPlanes = panels;
  4034. node.material.needsUpdate = true;
  4035. }
  4036. if (node.material.emissive !== undefined) {
  4037. if (node.material.side !== material_side) {
  4038. node.material.side = material_side;
  4039. node.material.needsUpdate = true;
  4040. }
  4041. }
  4042. }
  4043. });
  4044. }
  4045. this.ctrl.doubleside = any_clipping;
  4046. if (!without_render) this.render3D(0);
  4047. return changed;
  4048. }
  4049. /** @summary Assign callback, invoked every time when drawing is completed
  4050. * @desc Used together with web-based geometry viewer
  4051. * @private */
  4052. setCompleteHandler(callback) {
  4053. this.#complete_handler = callback;
  4054. }
  4055. /** @summary Completes drawing procedure
  4056. * @return {Promise} for ready */
  4057. async completeDraw(close_progress) {
  4058. let first_time = false, full_redraw = false, check_extras = true;
  4059. if (!this.ctrl) {
  4060. console.warn('ctrl object does not exist in completeDraw - something went wrong');
  4061. return this;
  4062. }
  4063. let promise;
  4064. if (!this.#clones) {
  4065. check_extras = false;
  4066. // if extra object where append, redraw them at the end
  4067. this.getExtrasContainer('delete'); // delete old container
  4068. promise = this.drawExtras(this.getCentral()?.getExtraObjects() || this.getExtraObjects(), '', false);
  4069. } else if ((this.#first_drawing || this.#full_redrawing) && this.ctrl.tracks && this.#geo_manager)
  4070. promise = this.drawExtras(this.#geo_manager.fTracks, '<prnt>/Tracks');
  4071. else
  4072. promise = Promise.resolve(true);
  4073. return promise.then(() => {
  4074. if (this.#full_redrawing) {
  4075. this.adjustCameraPosition('first');
  4076. this.#full_redrawing = false;
  4077. full_redraw = true;
  4078. this.changedDepthMethod('norender');
  4079. }
  4080. if (this.#first_drawing) {
  4081. this.adjustCameraPosition('first');
  4082. this.showDrawInfo();
  4083. this.#first_drawing = false;
  4084. first_time = true;
  4085. full_redraw = true;
  4086. }
  4087. if (first_time)
  4088. this.completeScene();
  4089. if (full_redraw && (this.ctrl.trans_radial || this.ctrl.trans_z))
  4090. this.changedTransformation('norender');
  4091. if (full_redraw)
  4092. return this.drawAxesAndOverlay(true);
  4093. }).then(() => {
  4094. this.#scene.overrideMaterial = null;
  4095. if (this.#provided_more_nodes !== undefined) {
  4096. this.appendMoreNodes(this.#provided_more_nodes, true);
  4097. this.#provided_more_nodes = undefined;
  4098. }
  4099. if (check_extras) {
  4100. // if extra object where append, redraw them at the end
  4101. this.getExtrasContainer('delete'); // delete old container
  4102. return this.drawExtras(this.getCentral()?.getExtraObjects() || this.getExtraObjects(), '', false);
  4103. }
  4104. }).then(() => {
  4105. this.updateClipping(true); // do not render
  4106. this.render3D(0, true);
  4107. if (close_progress)
  4108. showProgress();
  4109. this.addOrbitControls();
  4110. if (first_time && !this.isBatchMode()) {
  4111. // after first draw check if highlight can be enabled
  4112. if (this.ctrl.highlight === 0)
  4113. this.ctrl.highlight = (this.#first_render_tm < 1000);
  4114. // also highlight of scene object can be assigned at the first draw
  4115. if (this.ctrl.highlight_scene === 0)
  4116. this.ctrl.highlight_scene = this.ctrl.highlight;
  4117. // if rotation was enabled, do it
  4118. if (this.#webgl && this.ctrl.rotate && !this.ctrl.project)
  4119. this.autorotate(2.5);
  4120. if (this.#webgl && this.ctrl.show_controls)
  4121. this.showControlGui(true);
  4122. }
  4123. this.setAsMainPainter();
  4124. const arr = this.#draw_resolveFuncs;
  4125. this.#draw_resolveFuncs = [];
  4126. arr?.forEach(func => func(this));
  4127. if (isFunc(this.#complete_handler))
  4128. this.#complete_handler(this);
  4129. if (this.#draw_nodes_again)
  4130. this.startDrawGeometry(); // relaunch drawing
  4131. else
  4132. this.#drawing_ready = true; // indicate that drawing is completed
  4133. return this;
  4134. });
  4135. }
  4136. /** @summary Returns true if geometry drawing is completed */
  4137. isDrawingReady() { return this.#drawing_ready ?? false; }
  4138. /** @summary Remove already drawn node. Used by geom viewer */
  4139. removeDrawnNode(nodeid) {
  4140. if (!this.#draw_nodes) return;
  4141. const new_nodes = [];
  4142. for (let n = 0; n < this.#draw_nodes.length; ++n) {
  4143. const entry = this.#draw_nodes[n];
  4144. if ((entry.nodeid === nodeid) || this.#clones.isIdInStack(nodeid, entry.stack))
  4145. this.#clones.createObject3D(entry.stack, this.#toplevel, 'delete_mesh');
  4146. else
  4147. new_nodes.push(entry);
  4148. }
  4149. if (new_nodes.length < this.#draw_nodes.length) {
  4150. this.#draw_nodes = new_nodes;
  4151. this.render3D();
  4152. }
  4153. }
  4154. /** @summary Cleanup geometry painter */
  4155. cleanup(first_time) {
  4156. if (!first_time) {
  4157. let can3d = 0;
  4158. if (!this.#superimpose) {
  4159. this.clearTopPainter(); // remove as pointer
  4160. if (this.#on_pad) {
  4161. const fp = this.getFramePainter();
  4162. if (fp?.mode3d) {
  4163. fp.clear3dCanvas();
  4164. fp.mode3d = false;
  4165. }
  4166. } else if (isFunc(this.clear3dCanvas))
  4167. can3d = this.clear3dCanvas(); // remove 3d canvas from main HTML element
  4168. disposeThreejsObject(this.#scene);
  4169. }
  4170. this.#toolbar?.cleanup(); // remove toolbar
  4171. disposeThreejsObject(this.#fullgeom_proj);
  4172. this.#controls?.cleanup();
  4173. this.#gui?.destroy();
  4174. this.#worker?.terminate();
  4175. this.#animating = undefined;
  4176. const obj = this.getGeometry();
  4177. if (obj && this.ctrl.is_main) {
  4178. if (obj.$geo_painter === this)
  4179. delete obj.$geo_painter;
  4180. else if (obj.fVolume?.$geo_painter === this)
  4181. delete obj.fVolume.$geo_painter;
  4182. }
  4183. this.#central_painter?.assignSubordinate(this, false);
  4184. this.#subordinate_painters?.forEach(p => {
  4185. p.assignCentral(this, false);
  4186. if (p.getClones() === this.getClones())
  4187. p.assignClones(undefined, undefined);
  4188. });
  4189. this.#geo_manager = undefined;
  4190. this.#highlight_handlers = undefined;
  4191. super.cleanup();
  4192. delete this.ctrl;
  4193. this.#did_cleanup = true;
  4194. if (can3d < 0)
  4195. this.selectDom().html('');
  4196. }
  4197. this.#central_painter = undefined;
  4198. this.#subordinate_painters = [];
  4199. const arr = this.#render_resolveFuncs;
  4200. this.#render_resolveFuncs = [];
  4201. arr?.forEach(func => func(this));
  4202. if (!this.#superimpose)
  4203. cleanupRender3D(this.#renderer);
  4204. this.ensureBloom(false);
  4205. this.#effectComposer = undefined;
  4206. this.#scene = undefined;
  4207. this.#scene_size = undefined;
  4208. this.#scene_width = 0;
  4209. this.#scene_height = 0;
  4210. this.#renderer = null;
  4211. this.#toplevel = null;
  4212. this.#fullgeom_proj = undefined;
  4213. this.#fog = undefined;
  4214. this.#camera= undefined;
  4215. this.#camera0pos = undefined;
  4216. this.#lookat = undefined;
  4217. this.#selected_mesh = undefined;
  4218. this.#custom_bounding_box = undefined;
  4219. this.assignClones(undefined, undefined);
  4220. this.#new_draw_nodes = undefined;
  4221. this.#new_append_nodes = undefined;
  4222. this.#last_camera_position = undefined;
  4223. this.#on_pad = undefined;
  4224. this.#start_render_tm = 0;
  4225. this.#first_render_tm = 0; // time needed for first rendering
  4226. this.#last_render_tm = 0;
  4227. this.changeStage(stageInit, 'cleanup');
  4228. this.#drawing_log = undefined;
  4229. this.#gui = undefined;
  4230. this.#controls = undefined;
  4231. this.#toolbar = undefined;
  4232. this.#worker = undefined;
  4233. }
  4234. /** @summary perform resize */
  4235. performResize(width, height) {
  4236. if ((this.#scene_width === width) && (this.#scene_height === height))
  4237. return false;
  4238. if ((width < 10) || (height < 10)) return false;
  4239. this.#scene_width = width;
  4240. this.#scene_height = height;
  4241. if (this.#camera && this.#renderer) {
  4242. if (this.#camera.isPerspectiveCamera)
  4243. this.#camera.aspect = this.#scene_width / this.#scene_height;
  4244. else if (this.#camera.isOrthographicCamera)
  4245. this.adjustCameraPosition(true, true);
  4246. this.#camera.updateProjectionMatrix();
  4247. this.#renderer.setSize(this.#scene_width, this.#scene_height, !this.#fit_main_area);
  4248. this.#effectComposer?.setSize(this.#scene_width, this.#scene_height);
  4249. this.#bloomComposer?.setSize(this.#scene_width, this.#scene_height);
  4250. if (this.isStage(stageInit))
  4251. this.render3D();
  4252. }
  4253. return true;
  4254. }
  4255. /** @summary Check if HTML element was resized and drawing need to be adjusted */
  4256. checkResize(arg) {
  4257. const cp = this.getCanvPainter();
  4258. // firefox is the only browser which correctly supports resize of embedded canvas,
  4259. // for others we should force canvas redrawing at every step
  4260. if (cp && !cp.checkCanvasResize(arg))
  4261. return false;
  4262. const sz = this.getSizeFor3d();
  4263. return this.performResize(sz.width, sz.height);
  4264. }
  4265. /** @summary Toggle enlarge state */
  4266. toggleEnlarge() {
  4267. if (this.enlargeMain('toggle'))
  4268. this.checkResize();
  4269. }
  4270. /** @summary either change mesh wireframe or return current value
  4271. * @return undefined when wireframe cannot be accessed
  4272. * @private */
  4273. accessObjectWireFrame(obj, on) {
  4274. if (!obj?.material)
  4275. return;
  4276. if ((on !== undefined) && obj.stack)
  4277. obj.material.wireframe = on;
  4278. return obj.material.wireframe;
  4279. }
  4280. /** @summary handle wireframe flag change in GUI
  4281. * @private */
  4282. changedWireFrame() {
  4283. this.#scene?.traverse(obj => this.accessObjectWireFrame(obj, this.ctrl.wireframe));
  4284. this.render3D();
  4285. }
  4286. /** @summary Update object in geo painter */
  4287. updateObject(obj) {
  4288. if ((obj === 'same') || !obj?._typename)
  4289. return false;
  4290. if (obj === this.getObject())
  4291. return true;
  4292. let gm;
  4293. if (obj._typename === clTGeoManager) {
  4294. gm = obj;
  4295. obj = obj.fMasterVolume;
  4296. }
  4297. if (obj._typename.indexOf(clTGeoVolume) === 0)
  4298. obj = { _typename: clTGeoNode, fVolume: obj, fName: obj.fName, $geoh: obj.$geoh, _proxy: true };
  4299. if (this.#geo_manager && gm) {
  4300. this.#geo_manager = gm;
  4301. this.assignObject(obj);
  4302. this.#did_update = true;
  4303. return true;
  4304. }
  4305. if (!this.matchObjectType(obj._typename))
  4306. return false;
  4307. this.assignObject(obj);
  4308. this.#did_update = true;
  4309. return true;
  4310. }
  4311. /** @summary Cleanup TGeo drawings */
  4312. clearDrawings() {
  4313. this.assignClones(undefined, undefined);
  4314. this.#extra_objects = undefined;
  4315. this.#last_clip_cfg = undefined;
  4316. // only remove all childs from top level object
  4317. disposeThreejsObject(this.#toplevel, true);
  4318. this.#full_redrawing = true;
  4319. }
  4320. /** @summary Redraw TGeo object inside TPad */
  4321. redraw() {
  4322. if (this.#superimpose) {
  4323. const cfg = getHistPainter3DCfg(this.getMainPainter());
  4324. if (cfg) {
  4325. this.#toplevel.scale.set(cfg.scale_x ?? 1, cfg.scale_y ?? 1, cfg.scale_z ?? 1);
  4326. this.#toplevel.position.set(cfg.offset_x ?? 0, cfg.offset_y ?? 0, cfg.offset_z ?? 0);
  4327. this.#toplevel.updateMatrix();
  4328. this.#toplevel.updateMatrixWorld();
  4329. }
  4330. }
  4331. if (this.#did_update)
  4332. return this.startRedraw();
  4333. const fp = this.#on_pad ? this.getFramePainter() : null;
  4334. if (!fp)
  4335. return Promise.resolve(false);
  4336. const sz = fp.getSizeFor3d(fp.access3dKind());
  4337. fp.apply3dSize(sz);
  4338. return this.performResize(sz.width, sz.height);
  4339. }
  4340. /** @summary Redraw TGeo object */
  4341. redrawObject(obj, opt) {
  4342. if (!this.updateObject(obj, opt))
  4343. return false;
  4344. return this.startRedraw();
  4345. }
  4346. /** @summary Start geometry redraw */
  4347. startRedraw(tmout) {
  4348. if (this.#redraw_timer) {
  4349. clearTimeout(this.#redraw_timer);
  4350. this.#redraw_timer = undefined;
  4351. }
  4352. if (tmout) {
  4353. this.#redraw_timer = setTimeout(() => this.startRedraw(), tmout);
  4354. return;
  4355. }
  4356. this.#did_update = undefined;
  4357. this.clearDrawings();
  4358. const draw_obj = this.getGeometry(),
  4359. name_prefix = this.#geo_manager ? draw_obj.fName : '';
  4360. return this.prepareObjectDraw(draw_obj, name_prefix);
  4361. }
  4362. /** @summary draw TGeo object */
  4363. static async draw(dom, obj, opt) {
  4364. if (!obj) return null;
  4365. let shape = null, extras = null, extras_path = '', is_eve = false;
  4366. if (('fShapeBits' in obj) && ('fShapeId' in obj)) {
  4367. shape = obj; obj = null;
  4368. } else if ((obj._typename === clTGeoVolumeAssembly) || (obj._typename === clTGeoVolume))
  4369. shape = obj.fShape;
  4370. else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) {
  4371. shape = obj.fShape; is_eve = true;
  4372. } else if (obj._typename === clTGeoManager)
  4373. shape = obj.fMasterVolume.fShape;
  4374. else if (obj._typename === clTGeoOverlap) {
  4375. extras = obj.fMarker;
  4376. extras_path = '<prnt>/Marker';
  4377. obj = buildOverlapVolume(obj);
  4378. if (!opt) opt = 'wire';
  4379. } else if ('fVolume' in obj) {
  4380. if (obj.fVolume) shape = obj.fVolume.fShape;
  4381. } else
  4382. obj = null;
  4383. if (isStr(opt) && opt.indexOf('comp') === 0 && shape && (shape._typename === clTGeoCompositeShape) && shape.fNode) {
  4384. let maxlvl = 1;
  4385. opt = opt.slice(4);
  4386. if (opt[0] === 'x') { maxlvl = 999; opt = opt.slice(1) + '_vislvl999'; }
  4387. obj = buildCompositeVolume(shape, maxlvl);
  4388. }
  4389. if (!obj && shape)
  4390. obj = Object.assign(create(clTNamed), { _typename: clTEveGeoShapeExtract, fTrans: null, fShape: shape, fRGBA: [0, 1, 0, 1], fElements: null, fRnrSelf: true });
  4391. if (!obj)
  4392. return null;
  4393. // eslint-disable-next-line no-use-before-define
  4394. const painter = createGeoPainter(dom, obj, opt);
  4395. if (painter.ctrl.is_main && !obj.$geo_painter)
  4396. obj.$geo_painter = painter;
  4397. if (!painter.ctrl.is_main && painter.ctrl.project && obj.$geo_painter) {
  4398. painter.assignCentral(obj.$geo_painter);
  4399. obj.$geo_painter.assignSubordinate(painter);
  4400. }
  4401. if (is_eve && (!painter.ctrl.vislevel || (painter.ctrl.vislevel < 9)))
  4402. painter.ctrl.vislevel = 9;
  4403. if (extras) {
  4404. painter.ctrl.split_colors = true;
  4405. painter.addExtra(extras, extras_path);
  4406. }
  4407. return painter.loadMacro(painter.ctrl.script_name).then(arg => painter.prepareObjectDraw(arg.obj, arg.prefix));
  4408. }
  4409. } // class TGeoPainter
  4410. let add_settings = false;
  4411. /** @summary Get icon for the browser
  4412. * @private */
  4413. function getBrowserIcon(hitem, hpainter) {
  4414. let icon = '';
  4415. switch (hitem._kind) {
  4416. case prROOT + clTEveTrack: icon = 'img_evetrack'; break;
  4417. case prROOT + clTEvePointSet: icon = 'img_evepoints'; break;
  4418. case prROOT + clTPolyMarker3D: icon = 'img_evepoints'; break;
  4419. }
  4420. if (icon) {
  4421. const drawitem = findItemWithGeoPainter(hitem);
  4422. if (drawitem?._painter?.extraObjectVisible(hpainter, hitem))
  4423. icon += ' geovis_this';
  4424. }
  4425. return icon;
  4426. }
  4427. /** @summary handle click on browser icon
  4428. * @private */
  4429. function browserIconClick(hitem, hpainter) {
  4430. if (hitem._volume) {
  4431. if (hitem._more && hitem._volume.fNodes?.arr?.length)
  4432. toggleGeoBit(hitem._volume, geoBITS.kVisDaughters);
  4433. else
  4434. toggleGeoBit(hitem._volume, geoBITS.kVisThis);
  4435. updateBrowserIcons(hitem._volume, hpainter);
  4436. findItemWithGeoPainter(hitem, true);
  4437. return false; // no need to update icon - we did it ourself
  4438. }
  4439. if (hitem._geoobj && ((hitem._geoobj._typename === clTEveGeoShapeExtract) || (hitem._geoobj._typename === clREveGeoShapeExtract))) {
  4440. hitem._geoobj.fRnrSelf = !hitem._geoobj.fRnrSelf;
  4441. updateBrowserIcons(hitem._geoobj, hpainter);
  4442. findItemWithGeoPainter(hitem, true);
  4443. return false; // no need to update icon - we did it ourself
  4444. }
  4445. // first check that geo painter assigned with the item
  4446. const drawitem = findItemWithGeoPainter(hitem),
  4447. newstate = drawitem?._painter?.extraObjectVisible(hpainter, hitem, true);
  4448. // return true means browser should update icon for the item
  4449. return newstate !== undefined;
  4450. }
  4451. /** @summary Create geo-related css entries
  4452. * @private */
  4453. function injectGeoStyle() {
  4454. if (!add_settings && isFunc(internals.addDrawFunc)) {
  4455. add_settings = true;
  4456. // indication that draw and hierarchy is loaded, create css
  4457. internals.addDrawFunc({ name: clTEvePointSet, icon_get: getBrowserIcon, icon_click: browserIconClick });
  4458. internals.addDrawFunc({ name: clTEveTrack, icon_get: getBrowserIcon, icon_click: browserIconClick });
  4459. }
  4460. function img(name, code) {
  4461. return `.jsroot .img_${name} { display: inline-block; height: 16px; width: 16px; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQ${code}'); }`;
  4462. }
  4463. injectStyle(`
  4464. ${img('geoarb8', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB1SURBVBjTdY6rEYAwEETTy6lzK8/Fo+Jj18dTAjUgaQGfGiggtRDE8RtY93Zu514If2nzk2ux9c5TZkwXbiWTUavzws69oBfpYBrMT4r0Jhsw+QfRgQSw+CaKRsKsnV+SaF8MN49RBSgPUxO85PMl5n4tfGUH2gghs2uPAeQAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
  4465. ${img('geocombi', 'CAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAABIAAAASABGyWs+AAAAlUlEQVQoz5VQMQ4CMQyzEUNnBqT7Bo+4nZUH8gj+welWJsQDkHoCEYakTXMHSFiq2jqu4xRAEl2A7w4myWzpzCSZRZ658ldKu1hPnFsequBIc/hcLli3l52MAIANtpWrDsv8waGTW6BPuFtsdZArXyFuj33TQpazGEQF38phipnLgItxRcAoOeNpzv4PTXnC42fb//AGI5YqfQAU8dkAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
  4466. ${img('geocone', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACRSURBVBjTdY+xDcNACEVvEm/ggo6Olva37IB0C3iEzJABvAHFTXBDeJRwthMnUvylk44vPjxK+afeokX0flQhJO7L4pafSOMxzaxIKc/Tc7SIjNLyieyZSjBzc4DqMZI0HTMonWPBNlogOLeuewbg9c0hOiIqH7DKmTCuFykjHe4XOzQ58XVMGxzt575tKzd6AX9yMkcWyPlsAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
  4467. ${img('geogtra', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACCSURBVBjTVc+hDQMxDAVQD1FyqCQk0MwsCwQEG3+eCW6B0FvheDboFMGepTlVitPP/Cz5y0S/mNkw8pySU9INJDDH4vM4Usm5OrQXasXtkA+tQF+zxfcDY8EVwgNeiwmA37TEccK5oLOwQtuCj7BM2Fq7iGrxVqJbSsH+GzXs+798AThwKMh3/6jDAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
  4468. ${img('geomedium', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAABVQTFRFAAAAAAAAMDAww8PDWKj/////gICAG0/C4AAAAAF0Uk5TAEDm2GYAAAABYktHRAX4b+nHAAAACXBIWXMAAABIAAAASABGyWs+AAAAXElEQVQI102MwRGAMAgEuQ6IDwvQCjQdhAl/H7ED038JHhkd3dcOLAgESFARaAqnEB3yrj6QSEym1RbbOKinN+8q2Esui1GaX7VXSi4RUbxHRbER8X6O5Pg/fLgBBzMN8HfXD3AAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
  4469. ${img('geopara', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABtSURBVBjTY2DADq5MT7+CzD9kaKjp+QhJYIWqublhMbKAgpOnZxWSQJdsVJTndCSBKoWoAM/VSALpqlEBAYeQBKJAAsi2BGgCBZDdEWUYFZCOLFBlGOWJ7AyGFeaotjIccopageK3R12PGHABACTYHWd0tGw6AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
  4470. ${img('georotation', 'CAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAABIAAAASABGyWs+AAAAiklEQVQoz2NgYGBgYGDg+A/BmIAFIvyDEbs0AwMTAwHACLPiB5QVBTdpGSOSCZjScDcgc4z+32BgYGBgEGIQw3QDLkdCTZD8/xJFeBfDVxQT/j9n/MeIrMCNIRBJwX8GRuzGM/yHKMAljeILNFOuMTyEisEUMKIqucrwB2oyIhyQpH8y/MZrLWkAAHFzIHIc0Q5yAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
  4471. ${img('geotranslation', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABESURBVBjTY2DgYGAAYzjgAAIQgSLAgSwAAcrWUUCAJBAVhSpgBAQumALGCJPAAsriHIS0IAQ4UAU4cGphQBWwZSAOAADGJBKdZk/rHQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
  4472. ${img('geotrd2', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABsSURBVBjTbY+xDcAwCARZx6UraiaAmpoRvIIb75PWI2QITxIiRQKk0CCO/xcA/NZ9LRs7RkJEYg3QxczUwoGsXiMAoe8lAelqRWFNKpiNXZLAalRDd0f3TMgeMckABKsCDmu+442RddeHz9cf9jUkW8smGn8AAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
  4473. ${img('geovolume', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAB5QTFRFAAAAMDAw///Ay8uc/7+Q/4BgmJh4gIDgAAD/////CZb2ugAAAAF0Uk5TAEDm2GYAAAABYktHRAnx2aXsAAAACXBIWXMAAABIAAAASABGyWs+AAAAR0lEQVQI12NggAEBIBAEQgYGQUYQAyIGIhgwAZMSGCgwMJuEKimFOhswsKWAGG4JDGxJIBk1EEO9o6NIDVkEpgauC24ODAAASQ8Pkj/retYAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
  4474. ${img('geoassembly', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAA9QTFRFAAAAMDAw/wAAAAD/////jEo0BQAAAAF0Uk5TAEDm2GYAAAABYktHRASPaNlRAAAACXBIWXMAAABIAAAASABGyWs+AAAAOklEQVQI12NggAFGRgEgEBRgEBSAMhgYGQQEgAR+oARGDIwCIAYjUL0A2DQQg9nY2ABVBKoGrgsDAADxzgNboMz8zQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
  4475. ${img('geocomposite', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABuSURBVBjTY2AgF2hqgQCCr+0V4O7hFmgCF7CJyKysKkmxhfGNLaw9SppqAi2gfMuY5Agrl+ZaC6iAUXRJZX6Ic0klTMA5urapPFY5NRcmYKFqWl8S5RobBRNg0PbNT3a1dDGH8RlM3LysTRjIBwAG6xrzJt11BAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
  4476. ${img('geoctub', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACESURBVBjTdc+xDcMwDARA7cKKHTuWX37LHaw+vQbQAJomA7j2DB7FhCMFCZB8pxPwJEv5kQcZW+3HencRBekak4aaMQIi8YJdAQ1CMeE0UBkuaLMETklQ9Alhka0JzzXWqLVBuQYPpWcVuBbZjZafNRYcDk9o/b07bvhINz+/zxu1/M0FSRcmAk/HaIcAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
  4477. ${img('geohype', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACKSURBVBjTbU+rFQQhDKQSDDISEYuMREfHx6eHKMpYuf5qoIQt5bgDblfcuJk3nySEhSvceDV3c/ejT66lspopE9pXyIlkCrHMBACpu1DClekQAREi/loviCnF/NhRwJLaQ6hVhPjB8bOCsjlnNnNl0FWJVWxAqGzHONRHpu5Ml+nQ+8GzNW9n+Is3eg80Nk0iiwoAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
  4478. ${img('geomixture', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAACFQTFRFAAAAAAAAKysrVVUA//8B//8AgICAqqpV398gv79A////VYJtlwAAAAF0Uk5TAEDm2GYAAAABYktHRApo0PRWAAAACXBIWXMAAABIAAAASABGyWs+AAAAXklEQVQI12NgwASCQsJCgoZAhoADq1tKIJAhEpDGxpYIZKgxsLElgBhibAkOCY4gKTaGkPRGIEPUIYEBrEaAIY0tDawmgYWNgREkkjCVjRWkWCUhLY0FJCIIBljsBgCZTAykgaRiRwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
  4479. ${img('geopcon', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACJSURBVBjTdc+hGcQwCIZhhjl/rkgWiECj8XgGyAbZoD5LdIRMkEnKkV575n75Pp8AgLU54dmh6mauelyAL2Qzxfe2sklioq6FacFAcRFXYhwJHdU5rDD2hEYB/CmoJVRMiIJqgtENuoqA8ltAlYAqRH4d1tGkwzTqN2gA7Nv+fUwkgZ/3mg34txM+szzATJS1HQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
  4480. ${img('geosphere', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACFSURBVBjTdY+xEcQwCAQp5QNFjpQ5vZACFBFTADFFfKYCXINzlUAJruXll2ekxDAEt9zcANFbXb2mqm56dxsymAH0yccAJaeNi0h5QGyfxGJmivMPjj0nmLsbRmyFCss3rlbpcUjfS8wLUNRcJyCF6uqg2IvYCnoKC7f1kSbA6riTz7evfwj3Ml+H3KBqAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
  4481. ${img('geotrap', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB5SURBVBjTbY+hFYAwDETZB1OJi4yNPp0JqjtAZ2AELL5DdABmIS2PtLxHXH7u7l2W5W+uHMHpGiCHLYR1yw4SCZMIXBOJWVSjK7QDDAu4g8OBmAKK4sAEDdR3rw8YmcUcrEijKKhl7lN1IQPn9ExlgU6/WEyc75+5AYK0KY5oHBDfAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
  4482. ${img('geotubeseg', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACBSURBVBjTdc+hEcQwDARA12P6QFBQ9LDwcXEVkA7SQTr4BlJBakgpsWdsh/wfux3NSCrlV86Mlrxmz1pBWq3bAHwETohxABVmDZADQp1BE+wDNnGywzHgmHDOreJNTDH3Xn3CVX0dpu2MHcIFBkYp/gKsQ8SCQ72V+36/+2aWf3kAQfgshnpXF0wAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
  4483. ${img('geoxtru', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABcSURBVBjTY2AgEmhpeZV56vmWwQW00QUYwAJlSAI6XmVqukh8PT1bT03PchhXX09Pr9wQIQDiJ+ZowgWAXD3bck+QQDlCQTkDQgCoxA/ERBKwhbDglgA1lDMQDwCc/Rvq8nYsWgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
  4484. ${img('geobbox', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB/SURBVBjTVc+hEYAwDAXQLlNRF1tVGxn9NRswQiSSCdgDyQBM0FlIIb2WuL77uf6E8E0N02wKYRwDciTKREVvB04GuZSyOMCABRB1WGzF3uDNQTvs/RcDtJXT4fSEXA5XoiQt0ttVSm8Co2psIOvoimjAOqBmFtH5wEP2373TPIvTK1nrpULXAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
  4485. ${img('geoconeseg', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB4SURBVBjTdc6hEcAgDAXQbFNZXHQkFlkd/30myAIMwAws0gmYpVzvoFyv/S5P/B+izzQ387ZA2pkDnvsU1SQLVIFrOM4JFmEaYp2gCQbmPEGODhJ8jt7Am47hwgrzInGAifa/elUZnQLY00iU30BZAV+BWi2VfnIBv1osbHH8jX0AAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
  4486. ${img('geoeltu', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACGSURBVBjTdY+hFYUwDEU7xq9CIXC4uNjY6KczQXeoYgVMR2ABRmCGjvIp/6dgiEruueedvBDuOR57LQnKyc8CJmKO+N8bieIUPtmBWjIIx8XDBHYCipsnql1g2D0UP2OoDqwBncf+RdZmzFMHizRjog7KZYzawd4Ay93lEAPWR7WAvNbwMl/XwSxBV8qCjgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
  4487. ${img('geomaterial', 'CAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAABIAAAASABGyWs+AAAAbElEQVQoz62QMRbAIAhDP319Xon7j54qHSyCtaMZFCUkRjgDIdRU9yZUCfg8ut5aAHdcxtoNurmgA3ABNKIR9KimhSukPe2qxcCYC0pfFXx/aFWo7i42KKItOpopqvvnLzJmtlZTS7EfGAfwAM4EQbLIGV0sAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
  4488. ${img('geoparab', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB/SURBVBjTbY+xDYAwDAQ9UAp3X7p0m9o9dUZgA9oMwAjpMwMzMAnYBAQSX9mn9+tN9KOtzsWsLOvYCziUGNX3nnCLJRzKPgeYrhPW7FJNLUB3YJazYKQKTnBaxgXRzNmJcrt7XCHQp9kEB1wfELEir/KGj4Foh8A+/zW1nf51AFabKZuWK+mNAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
  4489. ${img('geopgon', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABwSURBVBjTY2AgDlwAAzh3sX1sPRDEeuwDc+8V2dsHgQQ8LCzq74HkLSzs7Yva2tLt7S3sN4MNiDUGKQmysCi6BzWkzcI+PdY+aDPCljZlj1iFOUjW1tvHLjYuQhJIt5/DcAFZYLH9YnSn7iPST9gAACbsJth21haFAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
  4490. ${img('geotorus', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACGSURBVBjTjY+hFcMwDEQ9SkFggXGIoejhw+LiGkBDlHoAr+AhgjNL5byChuXeE7gvPelUyjOds/f5Zw0ggfj5KVCPMBWeyx+SbQ1XUriAC2XfpWWxjQQEZasRtRHiCUAj3qN4JaolUJppzh4q7dUTdHFXW/tH9OuswWm3nI7tc08+/eGLl758ey9KpKrNOQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
  4491. ${img('geotrd1', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB/SURBVBjTbc6xDQMhDAVQ9qH6lUtal65/zQ5IDMAMmYAZrmKGm4FJzlEQQUo+bvwkG4fwm9lbodV7w40Y4WGfSxQiXiJlQfZOjWRb8Ioi3tKuBQMCo7+9N72BzPsfAuoTdUP9QN8wgOQwvsfWmHzpeT5BKydMNW0nhJGvGf7mAc5WKO9e5N2dAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
  4492. ${img('geotube', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACGSURBVBjTRc+tEcAwCAXgLFNbWeSzSDQazw5doWNUZIOM0BEyS/NHy10E30HyklKvWnJ+0le3sJoKn3X2z7GRuvG++YRyMMDt0IIKUXMzxbnugJi5m9K1gNnGBOUFElAWGMaKIKI4xoQggl00gT+A9hXWgDwnfqgsHRAx2m+8bfjfdyrx5AtsSjpwu+M2RgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
  4493. ${img('evepoints', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAABJQTFRF////n4mJcEdKRDMzcEdH////lLE/CwAAAAF0Uk5TAEDm2GYAAAABYktHRACIBR1IAAAACXBIWXMAAABIAAAASABGyWs+AAAAI0lEQVQI12NgIAowIpgKEJIZLiAgAKWZGQzQ9UGlWIizBQgAN4IAvGtVrTcAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTYtMDktMDJUMTU6MDQ6MzgrMDI6MDDPyc7hAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE2LTA5LTAyVDE1OjA0OjM4KzAyOjAwvpR2XQAAAABJRU5ErkJggg==')}
  4494. ${img('evetrack', 'CAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAABIAAAASABGyWs+AAAAqElEQVQoz32RMQrCQBBFf4IgSMB0IpGkMpVHCFh7BbHIGTyVhU0K8QYewEKsbVJZaCUiPAsXV8Puzhaz7H8zs5+JUDjikLilQr5zpCRl5xMXZNScQE5gSMGaz70jjUAJcw5c3UBMTsUe+9Kzf065SbropeLXimWfDIgoab/tOyPGzOhz53+oSWcSGh7UdB2ZNKXBZdgAuUdEKJYmrEILyVgG6pE2tEHgDfe42rbjYzSHAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE2LTA5LTAyVDE1OjA0OjQ3KzAyOjAwM0S3EQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNi0wOS0wMlQxNTowNDo0NyswMjowMEIZD60AAAAASUVORK5CYII=')}
  4495. .jsroot .geovis_this { background-color: lightgreen; }
  4496. .jsroot .geovis_daughters { background-color: lightblue; }
  4497. .jsroot .geovis_all { background-color: yellow; }`);
  4498. }
  4499. /** @summary Create geo painter
  4500. * @private */
  4501. function createGeoPainter(dom, obj, opt) {
  4502. injectGeoStyle();
  4503. geoCfg('GradPerSegm', settings.GeoGradPerSegm);
  4504. geoCfg('CompressComp', settings.GeoCompressComp);
  4505. const painter = new TGeoPainter(dom, obj);
  4506. painter.decodeOptions(opt); // indicator of initialization
  4507. return painter;
  4508. }
  4509. /** @summary provide menu for geo object
  4510. * @private */
  4511. function provideMenu(menu, item, hpainter) {
  4512. if (!item._geoobj) return false;
  4513. const obj = item._geoobj, vol = item._volume,
  4514. iseve = ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract));
  4515. if (!vol && !iseve)
  4516. return false;
  4517. menu.separator();
  4518. const scanEveVisible = (obj2, arg, skip_this) => {
  4519. if (!arg) arg = { visible: 0, hidden: 0 };
  4520. if (!skip_this) {
  4521. if (arg.assign !== undefined)
  4522. obj2.fRnrSelf = arg.assign;
  4523. else if (obj2.fRnrSelf)
  4524. arg.vis++;
  4525. else
  4526. arg.hidden++;
  4527. }
  4528. if (obj2.fElements) {
  4529. for (let n = 0; n < obj2.fElements.arr.length; ++n)
  4530. scanEveVisible(obj2.fElements.arr[n], arg, false);
  4531. }
  4532. return arg;
  4533. }, toggleEveVisibility = arg => {
  4534. if (arg === 'self') {
  4535. obj.fRnrSelf = !obj.fRnrSelf;
  4536. item._icon = item._icon.split(' ')[0] + provideVisStyle(obj);
  4537. hpainter.updateTreeNode(item);
  4538. } else {
  4539. scanEveVisible(obj, { assign: (arg === 'true') }, true);
  4540. hpainter.forEachItem(m => {
  4541. // update all child items
  4542. if (m._geoobj && m._icon) {
  4543. m._icon = item._icon.split(' ')[0] + provideVisStyle(m._geoobj);
  4544. hpainter.updateTreeNode(m);
  4545. }
  4546. }, item);
  4547. }
  4548. findItemWithGeoPainter(item, true);
  4549. }, toggleMenuBit = arg => {
  4550. toggleGeoBit(vol, arg);
  4551. const newname = item._icon.split(' ')[0] + provideVisStyle(vol);
  4552. hpainter.forEachItem(m => {
  4553. // update all items with that volume
  4554. if (item._volume === m._volume) {
  4555. m._icon = newname;
  4556. hpainter.updateTreeNode(m);
  4557. }
  4558. });
  4559. hpainter.updateTreeNode(item);
  4560. findItemWithGeoPainter(item, true);
  4561. }, drawitem = findItemWithGeoPainter(item),
  4562. fullname = drawitem ? hpainter.itemFullName(item, drawitem) : '';
  4563. if ((item._geoobj._typename.indexOf(clTGeoNode) === 0) && drawitem) {
  4564. menu.add('Focus', () => {
  4565. if (drawitem && isFunc(drawitem._painter?.focusOnItem))
  4566. drawitem._painter.focusOnItem(fullname);
  4567. });
  4568. }
  4569. if (iseve) {
  4570. menu.addchk(obj.fRnrSelf, 'Visible', 'self', toggleEveVisibility);
  4571. const res = scanEveVisible(obj, undefined, true);
  4572. if (res.hidden + res.visible > 0)
  4573. menu.addchk((res.hidden === 0), 'Daughters', res.hidden ? 'true' : 'false', toggleEveVisibility);
  4574. } else {
  4575. const stack = drawitem?._painter?.getClones()?.findStackByName(fullname),
  4576. phys_vis = stack ? drawitem._painter.getClones().getPhysNodeVisibility(stack) : null,
  4577. is_visible = testGeoBit(vol, geoBITS.kVisThis);
  4578. menu.addchk(testGeoBit(vol, geoBITS.kVisNone), 'Invisible', geoBITS.kVisNone, toggleMenuBit);
  4579. if (stack) {
  4580. const changePhysVis = arg => {
  4581. drawitem._painter.getClones().setPhysNodeVisibility(stack, (arg === 'off') ? false : arg);
  4582. findItemWithGeoPainter(item, true);
  4583. };
  4584. menu.sub('Physical vis', 'Physical node visibility - only for this instance');
  4585. menu.addchk(phys_vis?.visible, 'on', 'on', changePhysVis, 'Enable visibility of phys node');
  4586. menu.addchk(phys_vis && !phys_vis.visible, 'off', 'off', changePhysVis, 'Disable visibility of physical node');
  4587. menu.add('reset', 'clear', changePhysVis, 'Reset custom visibility of physical node');
  4588. menu.add('reset all', 'clearall', changePhysVis, 'Reset all custom settings for all nodes');
  4589. menu.endsub();
  4590. }
  4591. menu.addchk(is_visible, 'Logical vis',
  4592. geoBITS.kVisThis, toggleMenuBit, 'Logical node visibility - all instances');
  4593. menu.addchk(testGeoBit(vol, geoBITS.kVisDaughters), 'Daughters',
  4594. geoBITS.kVisDaughters, toggleMenuBit, 'Logical node daugthers visibility');
  4595. }
  4596. return true;
  4597. }
  4598. let createItem = null;
  4599. /** @summary create list entity for geo object
  4600. * @private */
  4601. function createList(parent, lst, name, title) {
  4602. if (!lst?.arr?.length)
  4603. return;
  4604. const list_item = {
  4605. _name: name,
  4606. _kind: prROOT + clTList,
  4607. _title: title,
  4608. _more: true,
  4609. _geoobj: lst,
  4610. _parent: parent,
  4611. _get(item /* , itemname */) {
  4612. return Promise.resolve(item._geoobj || null);
  4613. },
  4614. _expand(node, lst2) {
  4615. // only childs
  4616. if (lst2.fVolume)
  4617. lst2 = lst2.fVolume.fNodes;
  4618. if (!lst2.arr)
  4619. return false;
  4620. node._childs = [];
  4621. checkDuplicates(null, lst2.arr);
  4622. for (const n in lst2.arr)
  4623. createItem(node, lst2.arr[n]);
  4624. return true;
  4625. }
  4626. };
  4627. if (!parent._childs)
  4628. parent._childs = [];
  4629. parent._childs.push(list_item);
  4630. }
  4631. /** @summary Expand geo object
  4632. * @private */
  4633. function expandGeoObject(parent, obj) {
  4634. injectGeoStyle();
  4635. if (!parent || !obj) return false;
  4636. const isnode = (obj._typename.indexOf(clTGeoNode) === 0),
  4637. isvolume = (obj._typename.indexOf(clTGeoVolume) === 0),
  4638. ismanager = (obj._typename === clTGeoManager),
  4639. iseve = ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)),
  4640. isoverlap = (obj._typename === clTGeoOverlap);
  4641. if (!isnode && !isvolume && !ismanager && !iseve && !isoverlap) return false;
  4642. if (parent._childs) return true;
  4643. if (ismanager) {
  4644. createList(parent, obj.fMaterials, 'Materials', 'list of materials');
  4645. createList(parent, obj.fMedia, 'Media', 'list of media');
  4646. createList(parent, obj.fTracks, 'Tracks', 'list of tracks');
  4647. createList(parent, obj.fOverlaps, 'Overlaps', 'list of detected overlaps');
  4648. createItem(parent, obj.fMasterVolume);
  4649. return true;
  4650. }
  4651. if (isoverlap) {
  4652. createItem(parent, obj.fVolume1);
  4653. createItem(parent, obj.fVolume2);
  4654. createItem(parent, obj.fMarker, 'Marker');
  4655. return true;
  4656. }
  4657. let volume, subnodes, shape;
  4658. if (iseve) {
  4659. subnodes = obj.fElements?.arr;
  4660. shape = obj.fShape;
  4661. } else {
  4662. volume = isnode ? obj.fVolume : obj;
  4663. subnodes = volume?.fNodes?.arr;
  4664. shape = volume?.fShape;
  4665. }
  4666. if (!subnodes && (shape?._typename === clTGeoCompositeShape) && shape?.fNode) {
  4667. if (!parent._childs) {
  4668. createItem(parent, shape.fNode.fLeft, 'Left');
  4669. createItem(parent, shape.fNode.fRight, 'Right');
  4670. }
  4671. return true;
  4672. }
  4673. if (!subnodes)
  4674. return false;
  4675. checkDuplicates(obj, subnodes);
  4676. for (let i = 0; i < subnodes.length; ++i)
  4677. createItem(parent, subnodes[i]);
  4678. return true;
  4679. }
  4680. /** @summary create hierarchy item for geo object
  4681. * @private */
  4682. createItem = function(node, obj, name) {
  4683. const sub = {
  4684. _kind: prROOT + obj._typename,
  4685. _name: name || getObjectName(obj),
  4686. _title: obj.fTitle,
  4687. _parent: node,
  4688. _geoobj: obj,
  4689. _get(item /* ,itemname */) {
  4690. // mark object as belong to the hierarchy, require to
  4691. if (item._geoobj) item._geoobj.$geoh = true;
  4692. return Promise.resolve(item._geoobj);
  4693. }
  4694. };
  4695. let volume, shape, subnodes, iseve = false;
  4696. if (obj._typename === 'TGeoMaterial')
  4697. sub._icon = 'img_geomaterial';
  4698. else if (obj._typename === 'TGeoMedium')
  4699. sub._icon = 'img_geomedium';
  4700. else if (obj._typename === 'TGeoMixture')
  4701. sub._icon = 'img_geomixture';
  4702. else if ((obj._typename.indexOf(clTGeoNode) === 0) && obj.fVolume) {
  4703. sub._title = 'node:' + obj._typename;
  4704. if (obj.fTitle) sub._title += ' ' + obj.fTitle;
  4705. volume = obj.fVolume;
  4706. } else if (obj._typename.indexOf(clTGeoVolume) === 0)
  4707. volume = obj;
  4708. else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) {
  4709. iseve = true;
  4710. shape = obj.fShape;
  4711. subnodes = obj.fElements ? obj.fElements.arr : null;
  4712. } else if ((obj.fShapeBits !== undefined) && (obj.fShapeId !== undefined))
  4713. shape = obj;
  4714. if (volume) {
  4715. shape = volume.fShape;
  4716. subnodes = volume.fNodes ? volume.fNodes.arr : null;
  4717. }
  4718. if (volume || shape || subnodes) {
  4719. if (volume) sub._volume = volume;
  4720. if (subnodes) {
  4721. sub._more = true;
  4722. sub._expand = expandGeoObject;
  4723. } else if (shape && (shape._typename === clTGeoCompositeShape) && shape.fNode) {
  4724. sub._more = true;
  4725. sub._shape = shape;
  4726. sub._expand = function(node2 /* , obj */) {
  4727. createItem(node2, node2._shape.fNode.fLeft, 'Left');
  4728. createItem(node2, node2._shape.fNode.fRight, 'Right');
  4729. return true;
  4730. };
  4731. }
  4732. if (!sub._title && (obj._typename !== clTGeoVolume))
  4733. sub._title = obj._typename;
  4734. if (shape) {
  4735. if (sub._title === '')
  4736. sub._title = shape._typename;
  4737. sub._icon = getShapeIcon(shape);
  4738. } else
  4739. sub._icon = sub._more ? 'img_geocombi' : 'img_geobbox';
  4740. if (volume)
  4741. sub._icon += provideVisStyle(volume);
  4742. else if (iseve)
  4743. sub._icon += provideVisStyle(obj);
  4744. sub._menu = provideMenu;
  4745. sub._icon_click = browserIconClick;
  4746. }
  4747. if (!node._childs)
  4748. node._childs = [];
  4749. if (!sub._name) {
  4750. if (isStr(node._name)) {
  4751. sub._name = node._name;
  4752. if (sub._name.at(-1) === 's')
  4753. sub._name = sub._name.slice(0, sub._name.length - 1);
  4754. sub._name += '_' + node._childs.length;
  4755. } else
  4756. sub._name = 'item_' + node._childs.length;
  4757. }
  4758. node._childs.push(sub);
  4759. return sub;
  4760. };
  4761. /** @summary Draw dummy geometry
  4762. * @private */
  4763. async function drawDummy3DGeom(painter) {
  4764. const shape = create(clTNamed);
  4765. shape._typename = clTGeoBBox;
  4766. shape.fDX = 1e-10;
  4767. shape.fDY = 1e-10;
  4768. shape.fDZ = 1e-10;
  4769. shape.fShapeId = 1;
  4770. shape.fShapeBits = 0;
  4771. shape.fOrigin = [0, 0, 0];
  4772. const obj = Object.assign(create(clTNamed),
  4773. { _typename: clTEveGeoShapeExtract,
  4774. fTrans: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
  4775. fShape: shape, fRGBA: [0, 0, 0, 0], fElements: null, fRnrSelf: false }),
  4776. pp = painter.getPadPainter(),
  4777. pad = pp?.getRootPad(true),
  4778. opt = 'dummy;' + (pad?.fFillColor && (pad?.fFillStyle > 1000) ? 'bkgr_' + pad.fFillColor : '');
  4779. return TGeoPainter.draw(pp || painter.getDom(), obj, opt);
  4780. }
  4781. /** @summary Direct draw function for TAxis3D
  4782. * @private */
  4783. function drawAxis3D() {
  4784. const main = this.getMainPainter();
  4785. if (isFunc(main?.setAxesDraw))
  4786. return main.setAxesDraw(true);
  4787. console.error('no geometry painter found to toggle TAxis3D drawing');
  4788. }
  4789. /** @summary Build three.js model for given geometry object
  4790. * @param {Object} obj - TGeo-related object
  4791. * @param {Object} [opt] - options
  4792. * @param {Number} [opt.vislevel] - visibility level like TGeoManager, when not specified - show all
  4793. * @param {Number} [opt.numnodes=1000] - maximal number of visible nodes
  4794. * @param {Number} [opt.numfaces=100000] - approx maximal number of created triangles
  4795. * @param {Number} [opt.instancing=-1] - <0 disable use of InstancedMesh, =0 only for large geometries, >0 enforce usage of InstancedMesh
  4796. * @param {boolean} [opt.doubleside=false] - use double-side material
  4797. * @param {boolean} [opt.wireframe=false] - show wireframe for created shapes
  4798. * @param {boolean} [opt.transparency=0] - make nodes transparent
  4799. * @param {boolean} [opt.dflt_colors=false] - use default ROOT colors
  4800. * @param {boolean} [opt.set_names=true] - set names to all Object3D instances
  4801. * @param {boolean} [opt.set_origin=false] - set TGeoNode/TGeoVolume as Object3D.userData
  4802. * @return {object} Object3D with created model
  4803. * @example
  4804. * import { build } from 'https://root.cern/js/latest/modules/geom/TGeoPainter.mjs';
  4805. * let obj3d = build(obj);
  4806. * // this is three.js object and can be now inserted in the scene
  4807. */
  4808. function build(obj, opt) {
  4809. if (!obj) return null;
  4810. if (!opt) opt = {};
  4811. if (!opt.numfaces) opt.numfaces = 100000;
  4812. if (!opt.numnodes) opt.numnodes = 1000;
  4813. if (!opt.frustum) opt.frustum = null;
  4814. opt.res_mesh = opt.res_faces = 0;
  4815. if (opt.instancing === undefined)
  4816. opt.instancing = -1;
  4817. opt.info = { num_meshes: 0, num_faces: 0 };
  4818. let clones, visibles;
  4819. if (obj.visibles && obj.nodes && obj.numnodes) {
  4820. // case of draw message from geometry viewer
  4821. const nodes = obj.numnodes > 1e6 ? { length: obj.numnodes } : new Array(obj.numnodes);
  4822. obj.nodes.forEach(node => {
  4823. nodes[node.id] = ClonedNodes.formatServerElement(node);
  4824. });
  4825. clones = new ClonedNodes(null, nodes);
  4826. clones.name_prefix = clones.getNodeName(0);
  4827. // normally only need when making selection, not used in geo viewer
  4828. // this.geo_clones.setMaxVisNodes(draw_msg.maxvisnodes);
  4829. // this.geo_clones.setVisLevel(draw_msg.vislevel);
  4830. // TODO: provide from server
  4831. clones.maxdepth = 20;
  4832. const nsegm = obj.cfg?.nsegm || 30;
  4833. for (let cnt = 0; cnt < obj.visibles.length; ++cnt) {
  4834. const item = obj.visibles[cnt], rd = item.ri;
  4835. // entry may be provided without shape - it is ok
  4836. if (rd)
  4837. item.server_shape = rd.server_shape = createServerGeometry(rd, nsegm);
  4838. }
  4839. visibles = obj.visibles;
  4840. } else {
  4841. let shape = null, hide_top = false;
  4842. if (('fShapeBits' in obj) && ('fShapeId' in obj)) {
  4843. shape = obj; obj = null;
  4844. } else if ((obj._typename === clTGeoVolumeAssembly) || (obj._typename === clTGeoVolume))
  4845. shape = obj.fShape;
  4846. else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract))
  4847. shape = obj.fShape;
  4848. else if (obj._typename === clTGeoManager) {
  4849. obj = obj.fMasterVolume;
  4850. hide_top = !opt.showtop;
  4851. shape = obj.fShape;
  4852. } else if (obj.fVolume)
  4853. shape = obj.fVolume.fShape;
  4854. else
  4855. obj = null;
  4856. if (opt.composite && shape && (shape._typename === clTGeoCompositeShape) && shape.fNode)
  4857. obj = buildCompositeVolume(shape);
  4858. if (!obj && shape)
  4859. obj = Object.assign(create(clTNamed), { _typename: clTEveGeoShapeExtract, fTrans: null, fShape: shape, fRGBA: [0, 1, 0, 1], fElements: null, fRnrSelf: true });
  4860. if (!obj) return null;
  4861. if (obj._typename.indexOf(clTGeoVolume) === 0)
  4862. obj = { _typename: clTGeoNode, fVolume: obj, fName: obj.fName, $geoh: obj.$geoh, _proxy: true };
  4863. clones = new ClonedNodes(obj);
  4864. clones.setVisLevel(opt.vislevel);
  4865. clones.setMaxVisNodes(opt.numnodes);
  4866. if (opt.dflt_colors)
  4867. clones.setDefaultColors(true);
  4868. const uniquevis = opt.no_screen ? 0 : clones.markVisibles(true);
  4869. if (uniquevis <= 0)
  4870. clones.markVisibles(false, false, hide_top);
  4871. else
  4872. clones.markVisibles(true, true, hide_top); // copy bits once and use normal visibility bits
  4873. clones.produceIdShifts();
  4874. // collect visible nodes
  4875. const res = clones.collectVisibles(opt.numfaces, opt.frustum);
  4876. visibles = res.lst;
  4877. }
  4878. if (!opt.material_kind)
  4879. opt.material_kind = 'lambert';
  4880. if (opt.set_names === undefined)
  4881. opt.set_names = true;
  4882. clones.setConfig(opt);
  4883. // collect shapes
  4884. const shapes = clones.collectShapes(visibles);
  4885. clones.buildShapes(shapes, opt.numfaces);
  4886. const toplevel = new THREE.Object3D();
  4887. toplevel.clones = clones; // keep reference on JSROOT data
  4888. const colors = getRootColors();
  4889. if (clones.createInstancedMeshes(opt, toplevel, visibles, shapes, colors))
  4890. return toplevel;
  4891. for (let n = 0; n < visibles.length; ++n) {
  4892. const entry = visibles[n];
  4893. if (entry.done) continue;
  4894. const shape = entry.server_shape || shapes[entry.shapeid];
  4895. if (!shape.ready) {
  4896. console.warn('shape marked as not ready when it should');
  4897. break;
  4898. }
  4899. clones.createEntryMesh(opt, toplevel, entry, shape, colors);
  4900. }
  4901. return toplevel;
  4902. }
  4903. export { ClonedNodes, build, TGeoPainter, GeoDrawingControl,
  4904. expandGeoObject, createGeoPainter, drawAxis3D, drawDummy3DGeom, produceRenderOrder };