UNPKG

jsroot

Version:
1,469 lines (1,216 loc) 82 kB
import { constants, settings, isFunc, getDocument, isNodeJs } from '../core.mjs'; import { rgb as d3_rgb } from '../d3.mjs'; import { THREE, assign3DHandler, disposeThreejsObject, createOrbitControl, createLineSegments, Box3D, getMaterialArgs, importThreeJs, createRender3D, beforeRender3D, afterRender3D, getRender3DKind, cleanupRender3D, createSVGRenderer, create3DLineMaterial } from '../base/base3d.mjs'; import { createLatexGeometry } from '../base/latex3d.mjs'; import { kCARTESIAN, kPOLAR, kCYLINDRICAL, kSPHERICAL, kRAPIDITY } from '../hist2d/THistPainter.mjs'; import { buildHist2dContour, buildSurf3D } from '../hist2d/TH2Painter.mjs'; /** @summary Text 3d axis visibility * @private */ function testAxisVisibility(camera, toplevel, fb = false, bb = false) { let top; if (toplevel?.children) { for (let n = 0; n < toplevel.children.length; ++n) { top = toplevel.children[n]; if (top.axis_draw) break; top = undefined; } } if (!top) return; if (!camera) { // this is case when axis drawing want to be removed toplevel.remove(top); return; } const pos = camera.position; let qudrant = 1; if ((pos.x < 0) && (pos.y >= 0)) qudrant = 2; else if ((pos.x >= 0) && (pos.y >= 0)) qudrant = 3; else if ((pos.x >= 0) && (pos.y < 0)) qudrant = 4; const testVisible = (id, range) => { if (id <= qudrant) id += 4; return (id > qudrant) && (id < qudrant + range); }, handleZoomMesh = obj3d => { for (let k = 0; k < obj3d.children?.length; ++k) { if (obj3d.children[k].zoom !== undefined) obj3d.children[k].zoom_disabled = !obj3d.visible; } }; for (let n = 0; n < top.children.length; ++n) { const chld = top.children[n]; if (chld.grid) chld.visible = bb && testVisible(chld.grid, 3); else if (chld.zid) { chld.visible = testVisible(chld.zid, 2); handleZoomMesh(chld); } else if (chld.xyid) { chld.visible = testVisible(chld.xyid, 3); handleZoomMesh(chld); } else if (chld.xyboxid) { let range = 5, shift = 0; if (bb && !fb) { range = 3; shift = -2; } else if (fb && !bb) range = 3; else if (!fb && !bb) range = chld.bottom ? 3 : 0; chld.visible = testVisible(chld.xyboxid + shift, range); if (!chld.visible && chld.bottom && bb) chld.visible = testVisible(chld.xyboxid, 3); } else if (chld.zboxid) { let range = 2, shift = 0; if (fb && bb) range = 5; else if (bb && !fb) range = 4; else if (!bb && fb) { shift = -2; range = 4; } chld.visible = testVisible(chld.zboxid + shift, range); } } } function convertLegoBuf(painter, pos, binsx, binsy) { if (painter.options.System === kCARTESIAN) return pos; const fp = painter.getFramePainter(); let kx = 1 / fp.size_x3d, ky = 1 / fp.size_y3d; if (binsx && binsy) { kx *= binsx / (binsx - 1); ky *= binsy / (binsy - 1); } if (painter.options.System === kPOLAR) { for (let i = 0; i < pos.length; i += 3) { const angle = (1 - pos[i] * kx) * Math.PI, radius = 0.5 + 0.5 * pos[i + 1] * ky; pos[i] = Math.cos(angle) * radius * fp.size_x3d; pos[i + 1] = Math.sin(angle) * radius * fp.size_y3d; } } else if (painter.options.System === kCYLINDRICAL) { for (let i = 0; i < pos.length; i += 3) { const angle = (1 - pos[i] * kx) * Math.PI, radius = 0.5 + pos[i + 2] / fp.size_z3d / 4; pos[i] = Math.cos(angle) * radius * fp.size_x3d; pos[i + 2] = (1 + Math.sin(angle) * radius) * fp.size_z3d; } } else if (painter.options.System === kSPHERICAL) { for (let i = 0; i < pos.length; i += 3) { const phi = (1 + pos[i] * kx) * Math.PI, theta = pos[i + 1] * ky * Math.PI, radius = 0.5 + pos[i + 2] / fp.size_z3d / 4; pos[i] = radius * Math.cos(theta) * Math.cos(phi) * fp.size_x3d; pos[i + 1] = radius * Math.cos(theta) * Math.sin(phi) * fp.size_y3d; pos[i + 2] = (1 + radius * Math.sin(theta)) * fp.size_z3d; } } else if (painter.options.System === kRAPIDITY) { for (let i = 0; i < pos.length; i += 3) { const phi = (1 - pos[i] * kx) * Math.PI, theta = pos[i + 1] * ky * Math.PI, radius = 0.5 + pos[i + 2] / fp.size_z3d / 4; pos[i] = radius * Math.cos(phi) * fp.size_x3d; pos[i + 1] = radius * Math.sin(theta) / Math.cos(theta) * fp.size_y3d / 2; pos[i + 2] = (1 + radius * Math.sin(phi)) * fp.size_z3d; } } return pos; } function createLegoGeom(painter, positions, normals, binsx, binsy) { const geometry = new THREE.BufferGeometry(); if (painter.options.System === kCARTESIAN) { geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); if (normals) geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3)); else geometry.computeVertexNormals(); } else { convertLegoBuf(painter, positions, binsx, binsy); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.computeVertexNormals(); } return geometry; } function create3DCamera(fp, orthographic) { if (fp.camera) { fp.scene.remove(fp.camera); disposeThreejsObject(fp.camera); delete fp.camera; } if (orthographic) fp.camera = new THREE.OrthographicCamera(-1.3 * fp.size_x3d, 1.3 * fp.size_x3d, 2.3 * fp.size_z3d, -0.7 * fp.size_z3d, 0.001, 40 * fp.size_z3d); else fp.camera = new THREE.PerspectiveCamera(45, fp.scene_width / fp.scene_height, 1, 40 * fp.size_z3d); fp.camera.up.set(0, 0, 1); fp.pointLight = new THREE.DirectionalLight(0xffffff, 3); fp.pointLight.position.set(fp.size_x3d / 2, fp.size_y3d / 2, fp.size_z3d / 2); fp.camera.add(fp.pointLight); fp.lookat = new THREE.Vector3(0, 0, orthographic ? 0.3 * fp.size_z3d : 0.8 * fp.size_z3d); fp.scene.add(fp.camera); } /** @summary Returns camera default position * @private */ function getCameraDefaultPosition(fp, first_time) { const pad = fp.getPadPainter()?.getRootPad(true), kz = fp.camera.isOrthographicCamera ? 1 : 1.4; let max3dx = Math.max(0.75 * fp.size_x3d, fp.size_z3d), max3dy = Math.max(0.75 * fp.size_y3d, fp.size_z3d), pos = null; if (first_time) { pos = new THREE.Vector3(); if (max3dx === max3dy) pos.set(-1.6 * max3dx, -3.5 * max3dy, kz * fp.size_z3d); else if (max3dx > max3dy) pos.set(-2 * max3dx, -3.5 * max3dy, kz * fp.size_z3d); else pos.set(-3.5 * max3dx, -2 * max3dy, kz * fp.size_z3d); } if (pad && (first_time || !fp.zoomChangedInteractive())) { if (Number.isFinite(pad.fTheta) && Number.isFinite(pad.fPhi) && ((pad.fTheta !== fp.camera_Theta) || (pad.fPhi !== fp.camera_Phi))) { if (!pos) pos = new THREE.Vector3(); max3dx = 3 * Math.max(fp.size_x3d, fp.size_z3d); max3dy = 3 * Math.max(fp.size_y3d, fp.size_z3d); const phi = (270 - pad.fPhi) / 180 * Math.PI, theta = (pad.fTheta - 10) / 180 * Math.PI; pos.set(max3dx * Math.cos(phi) * Math.cos(theta), max3dy * Math.sin(phi) * Math.cos(theta), fp.size_z3d + (kz - 0.9) * (max3dx + max3dy) * Math.sin(theta)); } } return pos; } /** @summary Set default camera position * @private */ function setCameraPosition(fp, first_time) { const pos = getCameraDefaultPosition(fp, first_time); if (pos) { fp.camera.position.copy(pos); first_time = true; } if (first_time) fp.camera.lookAt(fp.lookat); if (first_time && fp.camera.isOrthographicCamera && fp.scene_width && fp.scene_height) { const screen_ratio = fp.scene_width / fp.scene_height, szx = fp.camera.right - fp.camera.left, szy = fp.camera.top - fp.camera.bottom; if (screen_ratio > szx / szy) { // screen wider than actual geometry const m = (fp.camera.right + fp.camera.left) / 2; fp.camera.left = m - szy * screen_ratio / 2; fp.camera.right = m + szy * screen_ratio / 2; } else { // screen higher than actual geometry const m = (fp.camera.top + fp.camera.bottom) / 2; fp.camera.top = m + szx / screen_ratio / 2; fp.camera.bottom = m - szx / screen_ratio / 2; } } fp.camera.updateProjectionMatrix(); } function getCameraPosition(fp) { const p = fp.camera.position, p0 = fp.lookat, dist = p.distanceTo(p0), dist_xy = Math.sqrt((p.x - p0.x) ** 2 + (p.y - p0.y) ** 2), new_theta = Math.atan2((p.z - p0.z) / dist, dist_xy / dist) / Math.PI * 180, new_phi = 270 - Math.atan2((p.y - p0.y) / dist_xy, (p.x - p0.x) / dist_xy) / Math.PI * 180, pad = fp.getPadPainter()?.getRootPad(true); fp.camera_Phi = new_phi >= 360 ? new_phi - 360 : new_phi; fp.camera_Theta = new_theta; if (pad && Number.isFinite(fp.camera_Phi) && Number.isFinite(fp.camera_Theta)) { pad.fPhi = fp.camera_Phi; pad.fTheta = fp.camera_Theta; } } function create3DControl(fp) { fp.control = createOrbitControl(fp, fp.camera, fp.scene, fp.renderer, fp.lookat); const frame_painter = fp, obj_painter = fp.getMainPainter(); if (fp.access3dKind() === constants.Embed3D.Embed) { // tooltip scaling only need when GL canvas embed into const scale = fp.getCanvPainter()?.getPadScale(); if (scale) fp.control.tooltip?.setScale(scale); } fp.control.processMouseMove = function(intersects) { let tip = null, mesh = null, zoom_mesh = null; const handle_tooltip = frame_painter.isTooltipAllowed(); for (let i = 0; i < intersects.length; ++i) { if (handle_tooltip && isFunc(intersects[i].object?.tooltip)) { tip = intersects[i].object.tooltip(intersects[i]); if (tip) { mesh = intersects[i].object; break; } } else if (intersects[i].object?.zoom && !zoom_mesh) zoom_mesh = intersects[i].object; } if (tip && !tip.use_itself) { const delta_x = 1e-4 * frame_painter.size_x3d, delta_y = 1e-4 * frame_painter.size_y3d, delta_z = 1e-4 * frame_painter.size_z3d; if ((tip.x1 > tip.x2) || (tip.y1 > tip.y2) || (tip.z1 > tip.z2)) console.warn('check 3D hints coordinates'); tip.x1 -= delta_x; tip.x2 += delta_x; tip.y1 -= delta_y; tip.y2 += delta_y; tip.z1 -= delta_z; tip.z2 += delta_z; } frame_painter.highlightBin3D(tip, mesh); if (!tip && zoom_mesh && isFunc(frame_painter.get3dZoomCoord)) { let axis_name = zoom_mesh.zoom; const pnt = zoom_mesh.globalIntersect(this.raycaster), axis_value = frame_painter.get3dZoomCoord(pnt, axis_name); if ((axis_name === 'z') && zoom_mesh.use_y_for_z) axis_name = 'y'; return { name: axis_name, title: 'axis object', line: axis_name + ' : ' + frame_painter.axisAsText(axis_name, axis_value), only_status: true }; } return tip?.lines ? tip : ''; }; fp.control.processMouseLeave = function() { frame_painter.highlightBin3D(null); }; fp.control.contextMenu = function(pos, intersects) { let kind = 'painter', p = obj_painter; if (intersects) { for (let n = 0; n < intersects.length; ++n) { const mesh = intersects[n].object; if (mesh.zoom) { kind = mesh.zoom; p = null; break; } if (isFunc(mesh.painter?.fillContextMenu)) { p = mesh.painter; break; } } } const ofp = obj_painter.getFramePainter(); if (isFunc(ofp?.showContextMenu)) ofp.showContextMenu(kind, pos, p); }; } /** @summary Create all necessary components for 3D drawings in frame painter * @return {Promise} when render3d !== -1 * @private */ function create3DScene(render3d, x3dscale, y3dscale, orthographic) { if (render3d === -1) { if (!this.mode3d) return; if (!isFunc(this.clear3dCanvas)) { console.error(`Strange, why mode3d=${this.mode3d} is configured!`); return; } const res = x3dscale ? this.toplevel : null; if (res) { this.scene?.remove(res); this.toplevel = null; } testAxisVisibility(null, this.toplevel); this.clear3dCanvas(); disposeThreejsObject(this.scene); this.control?.cleanup(); cleanupRender3D(this.renderer); delete this.size_x3d; delete this.size_y3d; delete this.size_z3d; delete this.tooltip_mesh; delete this.scene; delete this.toplevel; delete this.camera; delete this.pointLight; delete this.renderer; delete this.control; if (this.render_tmout) { clearTimeout(this.render_tmout); delete this.render_tmout; } this.mode3d = false; if (this.getG() && !x3dscale) this.createFrameG(); return res; } this.mode3d = true; // indicate 3d mode as hist painter does if ('toplevel' in this) { // it is indication that all 3D object created, just replace it with empty this.scene.remove(this.toplevel); disposeThreejsObject(this.toplevel); delete this.tooltip_mesh; delete this.toplevel; this.control?.hideTooltip(); const newtop = new THREE.Object3D(); this.scene.add(newtop); this.toplevel = newtop; this.resize3D(); // set actual sizes setCameraPosition(this, false); return Promise.resolve(true); } render3d = getRender3DKind(render3d, this.isBatchMode()); assign3DHandler(this); const sz = this.getSizeFor3d(undefined, render3d); this.size_z3d = 100; this.x3dscale = x3dscale || 1; this.y3dscale = y3dscale || 1; const xy3d = (sz.height > 10) && (sz.width > 10) ? Math.round(sz.width / sz.height * this.size_z3d) : this.size_z3d; this.size_x3d = xy3d * this.x3dscale; this.size_y3d = xy3d * this.y3dscale; return importThreeJs().then(() => { // three.js 3D drawing this.scene = new THREE.Scene(); // scene.fog = new Fog(0xffffff, 500, 3000); this.toplevel = new THREE.Object3D(); this.scene.add(this.toplevel); this.scene_width = sz.width; this.scene_height = sz.height; this.scene_x = sz.x ?? 0; this.scene_y = sz.y ?? 0; this.camera_Phi = 30; this.camera_Theta = 30; create3DCamera(this, orthographic); setCameraPosition(this, true); return createRender3D(this.scene_width, this.scene_height, render3d); }).then(r => { this.renderer = r; if (!r) return this; this.webgl = r.jsroot_render3d === constants.Render3D.WebGL; this.add3dCanvas(sz, r.jsroot_dom, this.webgl); this.first_render_tm = 0; this.enable_highlight = false; if (!this.isBatchMode() && this.webgl && !isNodeJs()) create3DControl(this); return this; }); } /** @summary Change camera kind in frame painter * @private */ function change3DCamera(orthographic) { let has_control = false; if (this.control) { this.control.cleanup(); delete this.control; has_control = true; } create3DCamera(this, orthographic); setCameraPosition(this, true); if (has_control) create3DControl(this); this.render3D(); } /** @summary Add 3D mesh to frame painter * @private */ function add3DMesh(mesh, painter, the_only) { if (!mesh) return; if (!this.toplevel) return console.error('3D objects are not yet created in the frame'); if (painter && the_only) this.remove3DMeshes(painter); this.toplevel.add(mesh); mesh.painter = painter; } /** @summary Returns all 3D meshed for specific * @private */ function get3DMeshes(painter) { const arr = []; if (!painter || !this.toplevel) return arr; for (let i = 0; i < this.toplevel.children.length; ++i) { const mesh = this.toplevel.children[i]; if (mesh.painter === painter) arr.push(mesh); } return arr; } /** @summary Remove 3D meshed for specified painter * @private */ function remove3DMeshes(painter) { const arr = this.get3DMeshes(painter); arr.forEach(mesh => { this.toplevel.remove(mesh); disposeThreejsObject(mesh); }); } /** @summary call 3D rendering of the frame * @param {number} tmout - specifies delay, after which actual rendering will be invoked * @desc Timeout used to avoid multiple rendering of the picture when several 3D drawings * superimposed with each other. * If tmout <= 0, rendering performed immediately * If tmout === -1111, immediate rendering with SVG renderer is performed * @private */ function render3D(tmout) { if (tmout === -1111) { // special handling for direct SVG renderer const doc = getDocument(), rrr = createSVGRenderer(false, 0, doc); rrr.setSize(this.scene_width, this.scene_height); rrr.render(this.scene, this.camera); if (rrr.makeOuterHTML) { // use text mode, it is faster const d = doc.createElement('div'); d.innerHTML = rrr.makeOuterHTML(); return d.childNodes[0]; } return rrr.domElement; } if (tmout === undefined) tmout = 5; // by default, rendering happens with timeout const batch_mode = this.isBatchMode(); if ((tmout > 0) && !this.usesvg && !batch_mode) { if (!this.render_tmout) this.render_tmout = setTimeout(() => this.render3D(0), tmout); return; } if (this.render_tmout) { clearTimeout(this.render_tmout); delete this.render_tmout; } if (!this.renderer) return; beforeRender3D(this.renderer); const tm1 = new Date(); testAxisVisibility(this.camera, this.toplevel, this.opt3d?.FrontBox, this.opt3d?.BackBox); // do rendering, most consuming time this.renderer.render(this.scene, this.camera); afterRender3D(this.renderer); const tm2 = new Date(); if (this.first_render_tm === 0) { this.first_render_tm = tm2.getTime() - tm1.getTime(); this.enable_highlight = (this.first_render_tm < 1200) && this.isTooltipAllowed(); if (this.first_render_tm > 500) console.log(`three.js r${THREE.REVISION}, first render tm = ${this.first_render_tm}`); } else getCameraPosition(this); if (this.processRender3D) { this.forEachPainter(objp => { if (isFunc(objp.handleRender3D)) objp.handleRender3D(); }, 'objects'); } } /** @summary Returns assigned render object * @private */ function getRenderer() { return this.renderer; } /** @summary Check is 3D drawing need to be resized * @private */ function resize3D() { const sz = this.getSizeFor3d(this.access3dKind()); this.apply3dSize(sz); if ((this.scene_width === sz.width) && (this.scene_height === sz.height)) return false; if ((sz.width < 10) || (sz.height < 10)) return false; this.scene_width = sz.width; this.scene_height = sz.height; this.camera.aspect = this.scene_width / this.scene_height; this.camera.updateProjectionMatrix(); this.renderer.setSize(this.scene_width, this.scene_height); const xy3d = (sz.height > 10) && (sz.width > 10) ? Math.round(sz.width / sz.height * this.size_z3d) : this.size_z3d, x3d = xy3d * this.x3dscale, y3d = xy3d * this.y3dscale; if ((Math.abs(x3d - this.size_x3d) > 0.15 * this.size_z3d) || (Math.abs(y3d - this.size_y3d) > 0.15 * this.size_z3d)) { this.size_x3d = x3d; this.size_y3d = y3d; this.control?.position0?.copy(getCameraDefaultPosition(this, true)); return 1; // indicate significant resize } return true; } /** @summary Highlight bin in frame painter 3D drawing * @private */ function highlightBin3D(tip, selfmesh) { const want_remove = !tip || (tip.x1 === undefined) || !this.enable_highlight; let changed = false, tooltip_mesh = null, changed_self = true, mainp = this.getMainPainter(); if (!mainp?.provideUserTooltip || !mainp?.hasUserTooltip()) mainp = null; if (this.tooltip_selfmesh) { changed_self = (this.tooltip_selfmesh !== selfmesh); this.tooltip_selfmesh.material.color = this.tooltip_selfmesh.save_color; delete this.tooltip_selfmesh; changed = true; } if (this.tooltip_mesh) { tooltip_mesh = this.tooltip_mesh; this.toplevel.remove(this.tooltip_mesh); delete this.tooltip_mesh; changed = true; } if (want_remove) { if (changed) { this.render3D(); mainp?.provideUserTooltip(null); } return; } if (tip.use_itself) { selfmesh.save_color = selfmesh.material.color; selfmesh.material.color = new THREE.Color(tip.color); this.tooltip_selfmesh = selfmesh; changed = changed_self; } else { changed = true; const indicies = Box3D.Indexes, normals = Box3D.Normals, vertices = Box3D.Vertices, color = new THREE.Color(tip.color ? tip.color : 0xFF0000), opacity = tip.opacity || 1; let pos, norm; if (!tooltip_mesh) { pos = new Float32Array(indicies.length * 3); norm = new Float32Array(indicies.length * 3); const geom = new THREE.BufferGeometry(); geom.setAttribute('position', new THREE.BufferAttribute(pos, 3)); geom.setAttribute('normal', new THREE.BufferAttribute(norm, 3)); const material = new THREE.MeshBasicMaterial({ color, opacity, vertexColors: false }); tooltip_mesh = new THREE.Mesh(geom, material); } else { pos = tooltip_mesh.geometry.attributes.position.array; tooltip_mesh.geometry.attributes.position.needsUpdate = true; tooltip_mesh.material.color = color; tooltip_mesh.material.opacity = opacity; } if (tip.x1 === tip.x2) console.warn(`same tip X ${tip.x1} ${tip.x2}`); if (tip.y1 === tip.y2) console.warn(`same tip Y ${tip.y1} ${tip.y2}`); if (tip.z1 === tip.z2) tip.z2 = tip.z1 + 0.0001; // avoid zero faces for (let k = 0, nn = -3; k < indicies.length; ++k) { const vert = vertices[indicies[k]]; pos[k * 3] = tip.x1 + vert.x * (tip.x2 - tip.x1); pos[k * 3 + 1] = tip.y1 + vert.y * (tip.y2 - tip.y1); pos[k * 3 + 2] = tip.z1 + vert.z * (tip.z2 - tip.z1); if (norm) { if (k % 6 === 0) nn += 3; norm[k * 3] = normals[nn]; norm[k * 3 + 1] = normals[nn + 1]; norm[k * 3 + 2] = normals[nn + 2]; } } this.tooltip_mesh = tooltip_mesh; this.toplevel.add(tooltip_mesh); if (tip.$painter && tip.$painter.options.System !== kCARTESIAN) { convertLegoBuf(tip.$painter, pos); tooltip_mesh.geometry.computeVertexNormals(); } } if (changed) this.render3D(); if (changed && tip.$projection && isFunc(tip.$painter?.redrawProjection)) tip.$painter.redrawProjection(tip.ix - 1, tip.ix, tip.iy - 1, tip.iy); if (changed && mainp?.getObject()) { mainp.provideUserTooltip({ obj: mainp.getObject(), name: mainp.getObject().fName, bin: tip.bin, cont: tip.value, binx: tip.ix, biny: tip.iy, binz: tip.iz, grx: (tip.x1 + tip.x2) / 2, gry: (tip.y1 + tip.y2) / 2, grz: (tip.z1 + tip.z2) / 2 }); } } /** @summary Set options used for 3D drawings * @private */ function set3DOptions(hopt) { this.opt3d = hopt; } /** @summary Draw axes in 3D mode * @private */ function drawXYZ(toplevel, AxisPainter, opts) { if (!opts) opts = { ndim: 2 }; if (opts.drawany === false) opts.draw = false; else opts.drawany = true; const pad = opts.v7 ? null : this.getPadPainter()?.getRootPad(true); let grminx = -this.size_x3d, grmaxx = this.size_x3d, grminy = -this.size_y3d, grmaxy = this.size_y3d, grminz = 0, grmaxz = 2 * this.size_z3d, scalingSize = this.size_z3d, xmin = this.xmin, xmax = this.xmax, ymin = this.ymin, ymax = this.ymax, zmin = this.zmin, zmax = this.zmax, y_zoomed = false, z_zoomed = false; if (!this.size_z3d) { grminx = this.xmin; grmaxx = this.xmax; grminy = this.ymin; grmaxy = this.ymax; grminz = this.zmin; grmaxz = this.zmax; scalingSize = (grmaxz - grminz); } if (('zoom_xmin' in this) && ('zoom_xmax' in this) && (this.zoom_xmin !== this.zoom_xmax)) { xmin = this.zoom_xmin; xmax = this.zoom_xmax; } if (('zoom_ymin' in this) && ('zoom_ymax' in this) && (this.zoom_ymin !== this.zoom_ymax)) { ymin = this.zoom_ymin; ymax = this.zoom_ymax; y_zoomed = true; } if (('zoom_zmin' in this) && ('zoom_zmax' in this) && (this.zoom_zmin !== this.zoom_zmax)) { zmin = this.zoom_zmin; zmax = this.zoom_zmax; z_zoomed = true; } if (opts.use_y_for_z) { this.zmin = this.ymin; this.zmax = this.ymax; zmin = ymin; zmax = ymax; z_zoomed = y_zoomed; ymin = 0; ymax = 1; } // z axis range used for lego plot this.lego_zmin = zmin; this.lego_zmax = zmax; // factor 1.1 used in ROOT for lego plots if ((opts.zmult !== undefined) && !z_zoomed) zmax *= opts.zmult; this.x_handle = new AxisPainter(null, this.xaxis); if (opts.v7) { this.x_handle.setPadPainter(this.getPadPainter()); this.x_handle.assignSnapId(this.getSnapId()); } else if (opts.hist_painter) this.x_handle.setHistPainter(opts.hist_painter, 'x'); this.x_handle.configureAxis('xaxis', this.xmin, this.xmax, xmin, xmax, false, [grminx, grmaxx], { log: pad?.fLogx ?? 0, reverse: opts.reverse_x, logcheckmin: true }); this.x_handle.assignFrameMembers(this, 'x'); this.x_handle.extractDrawAttributes(scalingSize); this.y_handle = new AxisPainter(null, this.yaxis); if (opts.v7) { this.y_handle.setPadPainter(this.getPadPainter()); this.y_handle.assignSnapId(this.getSnapId()); } else if (opts.hist_painter) this.y_handle.setHistPainter(opts.hist_painter, 'y'); this.y_handle.configureAxis('yaxis', this.ymin, this.ymax, ymin, ymax, false, [grminy, grmaxy], { log: pad && !opts.use_y_for_z ? pad.fLogy : 0, reverse: opts.reverse_y, logcheckmin: opts.ndim > 1 }); this.y_handle.assignFrameMembers(this, 'y'); this.y_handle.extractDrawAttributes(scalingSize); this.z_handle = new AxisPainter(null, this.zaxis); if (opts.v7) { this.z_handle.setPadPainter(this.getPadPainter()); this.z_handle.assignSnapId(this.getSnapId()); } else if (opts.hist_painter) this.z_handle.setHistPainter(opts.hist_painter, 'z'); this.z_handle.configureAxis('zaxis', this.zmin, this.zmax, zmin, zmax, false, [grminz, grmaxz], { value_axis: (opts.ndim === 1) || (opts.ndim === 2), log: ((opts.use_y_for_z || (opts.ndim === 2)) ? pad?.fLogv : undefined) ?? pad?.fLogz ?? 0, reverse: opts.reverse_z, logcheckmin: opts.ndim > 2 }); this.z_handle.assignFrameMembers(this, 'z'); this.z_handle.extractDrawAttributes(scalingSize); this.setRootPadRange(pad, true); // set some coordinates typical for 3D projections in ROOT const textMaterials = {}, lineMaterials = {}, xticks = this.x_handle.createTicks(false, true), yticks = this.y_handle.createTicks(false, true), zticks = this.z_handle.createTicks(false, true); let text_scale = 1; function getLineMaterial(handle, kind) { const col = ((kind === 'ticks') ? handle.ticksColor : handle.lineatt.color) || 'black', linewidth = (kind === 'ticks') ? handle.ticksWidth : handle.lineatt.width, name = `${col}_${linewidth}`; if (!lineMaterials[name]) lineMaterials[name] = new THREE.LineBasicMaterial(getMaterialArgs(col, { linewidth, vertexColors: false })); return lineMaterials[name]; } function getTextMaterial(handle, kind, custom_color) { const col = custom_color || ((kind === 'title') ? handle.titleFont?.color : handle.labelsFont?.color) || 'black'; if (!textMaterials[col]) textMaterials[col] = new THREE.MeshBasicMaterial(getMaterialArgs(col, { vertexColors: false })); return textMaterials[col]; } // main element, where all axis elements are placed const top = new THREE.Object3D(); top.axis_draw = true; // mark element as axis drawing toplevel.add(top); let ticks = [], lbls = [], maxtextheight = 0, maxtextwidth = 0; const center_x = this.x_handle.isCenteredLabels(), rotate_x = this.x_handle.isRotateLabels(); while (xticks.next()) { const grx = xticks.grpos; let is_major = xticks.kind === 1, lbl = this.x_handle.format(xticks.tick, 2); if (xticks.last_major()) { if (!this.x_handle.fTitle) lbl = 'x'; } else if (lbl === null) { is_major = false; lbl = ''; } if (is_major && lbl && opts.draw && (!center_x || !xticks.last_major())) { const mod = xticks.get_modifier(); if (mod?.fLabText) lbl = mod.fLabText; const text3d = createLatexGeometry(this, lbl, this.x_handle.labelsFont.size); text3d.computeBoundingBox(); const draw_width = text3d.boundingBox.max.x - text3d.boundingBox.min.x, draw_height = text3d.boundingBox.max.y - text3d.boundingBox.min.y; text3d.center = true; // place central text3d.offsety = this.x_handle.labelsOffset + (grmaxy - grminy) * 0.005; maxtextwidth = Math.max(maxtextwidth, draw_width); maxtextheight = Math.max(maxtextheight, draw_height); if (mod?.fTextColor) text3d.color = this.getColor(mod.fTextColor); text3d.grx = grx; lbls.push(text3d); let space = 0; if (!xticks.last_major()) { space = Math.abs(xticks.next_major_grpos() - grx); if ((draw_width > 0) && (space > 0)) text_scale = Math.min(text_scale, 0.9 * space / draw_width); } if (rotate_x) text3d.rotate = 1; if (center_x) { if (!space) space = Math.min(grx - grminx, grmaxx - grx); text3d.grx += space / 2; } } ticks.push(grx, 0, 0, grx, this.x_handle.ticksSize * (is_major ? -1 : -0.6), 0); } if (this.x_handle.fTitle && opts.draw) { const text3d = createLatexGeometry(this, this.x_handle.fTitle, this.x_handle.titleFont.size); text3d.computeBoundingBox(); text3d.center = this.x_handle.titleCenter; text3d.opposite = this.x_handle.titleOpposite; text3d.offsety = 1.6 * this.x_handle.titleOffset + (grmaxy - grminy) * 0.005; text3d.grx = (grminx + grmaxx) / 2; // default position for centered title text3d.kind = 'title'; if (this.x_handle.isRotateTitle()) text3d.rotate = 2; lbls.push(text3d); } this.get3dZoomCoord = function(point, kind) { // return axis coordinate from intersection point with axis geometry const min = this[`scale_${kind}min`], max = this[`scale_${kind}max`]; let pos = point[kind]; switch (kind) { case 'x': pos = (pos + this.size_x3d) / 2 / this.size_x3d; break; case 'y': pos = (pos + this.size_y3d) / 2 / this.size_y3d; break; case 'z': pos = pos / 2 / this.size_z3d; break; } if (this['log' + kind]) pos = Math.exp(Math.log(min) + pos * (Math.log(max) - Math.log(min))); else pos = min + pos * (max - min); return pos; }; const createZoomMesh = (kind, size_3d, use_y_for_z) => { const geom = new THREE.BufferGeometry(), tsz = Math.max(this[kind + '_handle'].ticksSize, 0.005 * size_3d); let positions; if (kind === 'z') positions = new Float32Array([0, 0, 0, tsz * 4, 0, 2 * size_3d, tsz * 4, 0, 0, 0, 0, 0, 0, 0, 2 * size_3d, tsz * 4, 0, 2 * size_3d]); else positions = new Float32Array([-size_3d, 0, 0, size_3d, -tsz * 4, 0, size_3d, 0, 0, -size_3d, 0, 0, -size_3d, -tsz * 4, 0, size_3d, -tsz * 4, 0]); geom.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geom.computeVertexNormals(); const material = new THREE.MeshBasicMaterial({ transparent: true, vertexColors: false, side: THREE.DoubleSide, opacity: 0 }), mesh = new THREE.Mesh(geom, material); mesh.zoom = kind; mesh.size_3d = size_3d; mesh.tsz = tsz; mesh.use_y_for_z = use_y_for_z; if (kind === 'y') mesh.rotateZ(Math.PI / 2).rotateX(Math.PI); mesh.v1 = new THREE.Vector3(positions[0], positions[1], positions[2]); mesh.v2 = new THREE.Vector3(positions[6], positions[7], positions[8]); mesh.v3 = new THREE.Vector3(positions[3], positions[4], positions[5]); mesh.globalIntersect = function(raycaster) { if (!this.v1 || !this.v2 || !this.v3) return undefined; const plane = new THREE.Plane(); plane.setFromCoplanarPoints(this.v1, this.v2, this.v3); plane.applyMatrix4(this.matrixWorld); const v1 = raycaster.ray.origin.clone(), v2 = v1.clone().addScaledVector(raycaster.ray.direction, 1e10), pnt = plane.intersectLine(new THREE.Line3(v1, v2), new THREE.Vector3()); if (!pnt) return undefined; let min = -this.size_3d, max = this.size_3d; if (this.zoom === 'z') { min = 0; max = 2 * this.size_3d; } if (pnt[this.zoom] < min) pnt[this.zoom] = min; else if (pnt[this.zoom] > max) pnt[this.zoom] = max; return pnt; }; mesh.showSelection = function(pnt1, pnt2) { // used to show selection let tgtmesh = this.children ? this.children[0] : null, gg; if (!pnt1 || !pnt2) { if (tgtmesh) { this.remove(tgtmesh); disposeThreejsObject(tgtmesh); } return tgtmesh; } if (!this.geometry) return false; if (!tgtmesh) { gg = this.geometry.clone(); const pos = gg.getAttribute('position').array; // original vertices [0, 2, 1, 0, 3, 2] if (this.zoom === 'z') pos[6] = pos[3] = pos[15] = this.tsz; else pos[4] = pos[16] = pos[13] = -this.tsz; tgtmesh = new THREE.Mesh(gg, new THREE.MeshBasicMaterial({ color: 0xFF00, side: THREE.DoubleSide, vertexColors: false })); this.add(tgtmesh); } else gg = tgtmesh.geometry; const pos = gg.getAttribute('position').array; if (this.zoom === 'z') { pos[2] = pos[11] = pos[8] = pnt1[this.zoom]; pos[5] = pos[17] = pos[14] = pnt2[this.zoom]; } else { pos[0] = pos[9] = pos[12] = pnt1[this.zoom]; pos[6] = pos[3] = pos[15] = pnt2[this.zoom]; } gg.getAttribute('position').needsUpdate = true; return true; }; return mesh; }; let xcont = new THREE.Object3D(), xtickslines; xcont.position.set(0, grminy, grminz); xcont.rotation.x = 1 / 4 * Math.PI; xcont.xyid = 2; xcont.painter = this.x_handle; if (opts.draw) { xtickslines = createLineSegments(ticks, getLineMaterial(this.x_handle, 'ticks')); xcont.add(xtickslines); } lbls.forEach(lbl => { const dx = lbl.boundingBox.max.x - lbl.boundingBox.min.x, dy = lbl.boundingBox.max.y - lbl.boundingBox.min.y, w = (lbl.rotate === 1) ? dy : dx, posx = lbl.center ? lbl.grx - w / 2 : (lbl.opposite ? grminx : grmaxx - w), posy = -text_scale * (lbl.rotate === 1 ? maxtextwidth : maxtextheight) - this.x_handle.ticksSize - lbl.offsety, m = new THREE.Matrix4(); // matrix to swap y and z scales and shift along z to its position m.set(text_scale, 0, 0, posx, 0, text_scale, 0, posy, 0, 0, 1, 0, 0, 0, 0, 1); const mesh = new THREE.Mesh(lbl, getTextMaterial(this.x_handle, lbl.kind, lbl.color)); if (lbl.rotate) mesh.rotateZ(lbl.rotate * Math.PI / 2); if (lbl.rotate === 1) mesh.translateY(-dy); if (lbl.rotate === 2) mesh.translateX(-dx); mesh.applyMatrix4(m); xcont.add(mesh); }); if (opts.zoom && opts.drawany) xcont.add(createZoomMesh('x', this.size_x3d)); top.add(xcont); xcont = new THREE.Object3D(); xcont.position.set(0, grmaxy, grminz); xcont.rotation.x = 3 / 4 * Math.PI; xcont.painter = this.x_handle; if (opts.draw) xcont.add(new THREE.LineSegments(xtickslines.geometry, xtickslines.material)); lbls.forEach(lbl => { const dx = lbl.boundingBox.max.x - lbl.boundingBox.min.x, dy = lbl.boundingBox.max.y - lbl.boundingBox.min.y, w = (lbl.rotate === 1) ? dy : dx, posx = lbl.center ? lbl.grx + w / 2 : (lbl.opposite ? grminx + w : grmaxx), posy = -text_scale * (lbl.rotate === 1 ? maxtextwidth : maxtextheight) - this.x_handle.ticksSize - lbl.offsety, m = new THREE.Matrix4(); // matrix to swap y and z scales and shift along z to its position m.set(-text_scale, 0, 0, posx, 0, text_scale, 0, posy, 0, 0, -1, 0, 0, 0, 0, 1); const mesh = new THREE.Mesh(lbl, getTextMaterial(this.x_handle, lbl.kind, lbl.color)); if (lbl.rotate) mesh.rotateZ(lbl.rotate * Math.PI / 2); if (lbl.rotate === 1) mesh.translateY(-dy); if (lbl.rotate === 2) mesh.translateX(-dx); mesh.applyMatrix4(m); xcont.add(mesh); }); xcont.xyid = 4; if (opts.zoom && opts.drawany) xcont.add(createZoomMesh('x', this.size_x3d)); top.add(xcont); lbls = []; text_scale = 1; maxtextwidth = maxtextheight = 0; ticks = []; const center_y = this.y_handle.isCenteredLabels(), rotate_y = this.y_handle.isRotateLabels(); while (yticks.next()) { const gry = yticks.grpos; let is_major = (yticks.kind === 1), lbl = this.y_handle.format(yticks.tick, 2); if (yticks.last_major()) { if (!this.y_handle.fTitle) lbl = 'y'; } else if (lbl === null) { is_major = false; lbl = ''; } if (is_major && lbl && opts.draw && (!center_y || !yticks.last_major())) { const mod = yticks.get_modifier(); if (mod?.fLabText) lbl = mod.fLabText; const text3d = createLatexGeometry(this, lbl, this.y_handle.labelsFont.size); text3d.computeBoundingBox(); const draw_width = text3d.boundingBox.max.x - text3d.boundingBox.min.x, draw_height = text3d.boundingBox.max.y - text3d.boundingBox.min.y; text3d.center = true; maxtextwidth = Math.max(maxtextwidth, draw_width); maxtextheight = Math.max(maxtextheight, draw_height); if (mod?.fTextColor) text3d.color = this.getColor(mod.fTextColor); text3d.gry = gry; text3d.offsetx = this.y_handle.labelsOffset + (grmaxx - grminx) * 0.005; lbls.push(text3d); let space = 0; if (!yticks.last_major()) { space = Math.abs(yticks.next_major_grpos() - gry); if (draw_width > 0) text_scale = Math.min(text_scale, 0.9 * space / draw_width); } if (center_y) { if (!space) space = Math.min(gry - grminy, grmaxy - gry); text3d.gry += space / 2; } if (rotate_y) text3d.rotate = 1; } ticks.push(0, gry, 0, this.y_handle.ticksSize * (is_major ? -1 : -0.6), gry, 0); } if (this.y_handle.fTitle && opts.draw) { const text3d = createLatexGeometry(this, this.y_handle.fTitle, this.y_handle.titleFont.size); text3d.computeBoundingBox(); text3d.center = this.y_handle.titleCenter; text3d.opposite = this.y_handle.titleOpposite; text3d.offsetx = 1.6 * this.y_handle.titleOffset + (grmaxx - grminx) * 0.005; text3d.gry = (grminy + grmaxy) / 2; // default position for centered title text3d.kind = 'title'; if (this.y_handle.isRotateTitle()) text3d.rotate = 2; lbls.push(text3d); } if (!opts.use_y_for_z) { let yticksline, ycont = new THREE.Object3D(); ycont.position.set(grminx, 0, grminz); ycont.rotation.y = -1 / 4 * Math.PI; ycont.painter = this.y_handle; if (opts.draw) { yticksline = createLineSegments(ticks, getLineMaterial(this.y_handle, 'ticks')); ycont.add(yticksline); } lbls.forEach(lbl => { const dx = lbl.boundingBox.max.x - lbl.boundingBox.min.x, dy = lbl.boundingBox.max.y - lbl.boundingBox.min.y, w = (lbl.rotate === 1) ? dy : dx, posx = -text_scale * (lbl.rotate === 1 ? maxtextwidth : maxtextheight) - this.y_handle.ticksSize - lbl.offsetx, posy = lbl.center ? lbl.gry + w / 2 : (lbl.opposite ? grminy + w : grmaxy), m = new THREE.Matrix4(); m.set(0, text_scale, 0, posx, -text_scale, 0, 0, posy, 0, 0, 1, 0, 0, 0, 0, 1); const mesh = new THREE.Mesh(lbl, getTextMaterial(this.y_handle, lbl.kind, lbl.color)); if (lbl.rotate) mesh.rotateZ(lbl.rotate * Math.PI / 2); if (lbl.rotate === 1) mesh.translateY(-dy); if (lbl.rotate === 2) mesh.translateX(-dx); mesh.applyMatrix4(m); ycont.add(mesh); }); ycont.xyid = 3; if (opts.zoom && opts.drawany) ycont.add(createZoomMesh('y', this.size_y3d)); top.add(ycont); ycont = new THREE.Object3D(); ycont.position.set(grmaxx, 0, grminz); ycont.rotation.y = -3 / 4 * Math.PI; ycont.painter = this.y_handle; if (opts.draw) ycont.add(new THREE.LineSegments(yticksline.geometry, yticksline.material)); lbls.forEach(lbl => { const dx = lbl.boundingBox.max.x - lbl.boundingBox.min.x, dy = lbl.boundingBox.max.y - lbl.boundingBox.min.y, w = (lbl.rotate === 1) ? dy : dx, posx = -text_scale * (lbl.rotate === 1 ? maxtextwidth : maxtextheight) - this.y_handle.ticksSize - lbl.offsetx, posy = lbl.center ? lbl.gry - w / 2 : (lbl.opposite ? grminy : grmaxy - w), m = new THREE.Matrix4(); m.set(0, text_scale, 0, posx, text_scale, 0, 0, posy, 0, 0, -1, 0, 0, 0, 0, 1); const mesh = new THREE.Mesh(lbl, getTextMaterial(this.y_handle, lbl.kind, lbl.color)); if (lbl.rotate) mesh.rotateZ(lbl.rotate * Math.PI / 2); if (lbl.rotate === 1) mesh.translateY(-dy); if (lbl.rotate === 2) mesh.translateX(-dx); mesh.applyMatrix4(m); ycont.add(mesh); }); ycont.xyid = 1; if (opts.zoom && opts.drawany) ycont.add(createZoomMesh('y', this.size_y3d)); top.add(ycont); } lbls = []; text_scale = 1; ticks = []; // just array, will be used for the buffer geometry let zgridx = null, zgridy = null, lastmajorz = null, maxzlblwidth = 0; const center_z = this.z_handle.isCenteredLabels(), rotate_z = this.z_handle.isRotateLabels(); if (this.size_z3d && opts.drawany) { zgridx = []; zgridy = []; } while (zticks.next()) { const grz = zticks.grpos; let is_major = (zticks.kind === 1), lbl = this.z_handle.format(zticks.tick, 2); if (lbl === null) { is_major = false; lbl = ''; } if (is_major && lbl && opts.draw && (!center_z || !zticks.last_major())) { const mod = zticks.get_modifier(); if (mod?.fLabText) lbl = mod.fLabText; const text3d = createLatexGeometry(this, lbl, this.z_handle.labelsFont.size); text3d.computeBoundingBox(); const draw_width = text3d.boundingBox.max.x - text3d.boundingBox.min.x, draw_height = text3d.boundingBox.max.y - text3d.boundingBox.min.y; text3d.translate(-draw_width, -draw_height / 2, 0); if (mod?.fTextColor) text3d.color = this.getColor(mod.fTextColor); text3d.grz = grz; lbls.push(text3d); if ((lastmajorz !== null) && (draw_height > 0)) text_scale = Math.min(text_scale, 0.9 * (grz - lastmajorz) / draw_height); maxzlblwidth = Math.max(maxzlblwidth, draw_width); lastmajorz = grz; } // create grid if (zgridx && is_major) zgridx.push(grminx, 0, grz, grmaxx, 0, grz); if (zgridy && is_major) zgridy.push(0, grminy, grz, 0, grmaxy, grz); ticks.push(0, 0, grz, this.z_handle.ticksSize * (is_major ? 1 : 0.6), 0, grz); } if (zgridx?.length) { const material = new THREE.LineDashedMaterial({ color: this.x_handle.ticksColor, dashSize: 2, gapSize: 2 }), lines1 = createLineSegments(zgridx, material); lines1.position.set(0, grmaxy, 0); lines1.grid = 2; // mark as grid lines1.visible = false; top.add(lines1); const lines2 = new THREE.LineSegments(lines1.geometry, material); lines2.position.set(0, grminy, 0); lines2.grid = 4; // mark as grid lines2.visible = false; top.add(lines2); } if (zgridy?.length) { const material = new THREE.LineDashedMaterial({ color: this.y_handle.ticksColor, dashSize: 2, gapSize: 2 }), lines1 = createLineSegments(zgridy, material); lines1.position.set(grmaxx, 0, 0); lines1.grid = 3; // mark as grid lines1.visible = false; top.add(lines1); const lines2 = new THREE.LineSegments(lines1.geometry, material); lines2.position.set(grminx, 0, 0); lines2.grid = 1; // mark as grid lines2.visible = false; top.add(lines2); } const zcont = [], zticksline = opts.draw ? createLineSegments(ticks, getLineMaterial(this.z_handle, 'ticks')) : null; for (let n = 0; n < 4; ++n) { zcont.push(new THREE.Object3D()); lbls.forEach((lbl, indx) => { const m = new THREE.Matrix4(), dx = lbl.boundingBox.max.x - lbl.boundingBox.min.x; let grz = lbl.grz; if (center_z) { if (indx < lbls.length - 1) grz = (grz + lbls[indx + 1].grz) / 2; else if (indx > 0) grz = Math.min(1.5 * grz - lbls[indx - 1].grz * 0.5, grmaxz); } // matrix to swap y and z scales and shift along z to its position m.set(-text_scale, 0, 0, this.z_handle.ticksSize + (grmaxx - grminx) * 0.005 + this.z_handle.labelsOffset, 0, 0, 1, 0, 0, text_scale, 0, grz); const mesh = new THREE.Mesh(lbl, getTextMaterial(this.z_handle)); if (rotate_z) mesh.rotateZ(-Math.PI / 2).translateX(dx / 2); mesh.applyMatrix4(m); zcont[n].add(mesh); }); if (this.z_handle.fTitle && opts.draw) { const text3d = createLatexGeometry(this, this.z_handle.fTitle, this.z_handle.titleFont.size); text3d.computeBoundingBox(); const dx = text3d.boundingBox.max.x - text3d.boundingBox.min.x, dy = text3d.boundingBox.max.y - text3d.boundingBox.min.y, rotate = this.z_handle.isRotateTitle(), posz = this.z_handle.titleCenter ? (grmaxz + grminz - dx) / 2 : (this.z_handle.titleOpposite ? grminz : grmaxz - dx) + (rotate ? dx : 0), m = new THREE.Matrix4(); m.set(-text_scale, 0, 0, this.z_handle.ticksSize + (grmaxx - grminx) * 0.005 + maxzlblwidth + this.z_handle.titleOffset, 0, 0, 1, 0, 0, text_scale, 0, posz); const mesh = new THREE.Mesh(text3d, getTextMaterial(this.z_handle, 'title')); mesh.rotateZ(Math.PI * (rotate ? 1.5 : 0.5)); if (rotate) mesh.translateY(-dy); mesh.applyMatrix4(m); zcont[n].add(mesh); } if (opts.draw && zticksline) zcont[n].add(n === 0 ? zticksline : new THREE.LineSegments(zticksline.geometry, zticksline.material)); if (opts.zoom && opts.drawany) zcont[n].add(createZoomMesh('z', this.size_z3d, opts.use_y_for_z)); zcont[n].zid = n + 2; top.add(zcont[n]); zcont[n].painter = this.z_handle; } zcont[0].position.set(grminx, grmaxy, 0); zcont[0].rotation.z = 3 / 4 * Math.PI; zcont[1].position.set(grmaxx, grmaxy, 0); zcont[1].rotation.z = 1 / 4 * Math.PI; zcont[2].position.set(grmaxx, grminy, 0); zcont[2].rotation.z = -1 / 4 * Math.PI; zcont[3].position.set(grminx, grminy, 0); zcont[3].rotation.z = -3 / 4 * Math.PI; if (!opts.drawany) retu