From c52033caad8fa9aff993d0a7e7c5922116747f3c Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 09:40:09 +0200 Subject: [PATCH 01/20] Add protection for missing pad painter --- modules/hist/hist3d.mjs | 4 ++-- modules/hist2d/THistPainter.mjs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/hist/hist3d.mjs b/modules/hist/hist3d.mjs index f1365b72c..d609e5d9c 100644 --- a/modules/hist/hist3d.mjs +++ b/modules/hist/hist3d.mjs @@ -338,7 +338,7 @@ function create3DCamera(fp, orthographic) { /** @summary Returns camera default position * @private */ function getCameraDefaultPosition(fp, first_time) { - const pad = fp.getPadPainter().getRootPad(true), + 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), @@ -891,7 +891,7 @@ function drawXYZ(toplevel, AxisPainter, opts) { else opts.drawany = true; - const pad = opts.v7 ? null : this.getPadPainter().getRootPad(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, diff --git a/modules/hist2d/THistPainter.mjs b/modules/hist2d/THistPainter.mjs index edadd030c..a618b83e3 100644 --- a/modules/hist2d/THistPainter.mjs +++ b/modules/hist2d/THistPainter.mjs @@ -2064,7 +2064,7 @@ class THistPainter extends ObjectPainter { else { if (nlevels < 2) nlevels = gStyle.fNumberContours; - const pad = this.getPadPainter().getRootPad(true), + const pad = this.getPadPainter()?.getRootPad(true), logv = pad?.fLogv ?? ((ndim === 2) && pad?.fLogz); cntr.createNormal(nlevels, logv ?? 0, zminpositive); From 258e740ac4b1efac5d817344514977901e2cea9d Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 09:41:01 +0200 Subject: [PATCH 02/20] Introduce TH2Painter.build3d static method It creates dummy frame and hist painter and performs drawing --- modules/hist/TH2Painter.mjs | 59 +++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/modules/hist/TH2Painter.mjs b/modules/hist/TH2Painter.mjs index 161f41c25..4ef109a24 100644 --- a/modules/hist/TH2Painter.mjs +++ b/modules/hist/TH2Painter.mjs @@ -2,6 +2,7 @@ import { settings, gStyle, clTMultiGraph, kNoZoom } from '../core.mjs'; import { getMaterialArgs, THREE } from '../base/base3d.mjs'; import { assignFrame3DMethods, drawBinsLego, drawBinsError3D, drawBinsContour3D, drawBinsSurf3D } from './hist3d.mjs'; import { TAxisPainter } from '../gpad/TAxisPainter.mjs'; +import { TFramePainter } from '../gpad/TFramePainter.mjs'; import { THistPainter } from '../hist2d/THistPainter.mjs'; import { TH2Painter as TH2Painter2D } from '../hist2d/TH2Painter.mjs'; @@ -294,6 +295,64 @@ class TH2Painter extends TH2Painter2D { .then(() => this); } + /** @summary Build three.js object for the histogram */ + static async build3d(histo, opt) { + const painter = new TH2Painter(null, histo); + painter.decodeOptions(opt); + + const o = painter.getOptions(), logz = false; + if (painter.isTH2Poly()) + o.Lego = 12; + painter.scanContent(); + + let zmult = 1; + + if (o.ohmin && o.ohmax) { + painter.zmin = o.hmin; + painter.zmax = o.hmax; + } else if (o.minimum !== kNoZoom && o.maximum !== kNoZoom) { + painter.zmin = o.minimum; + painter.zmax = o.maximum; + } else if (painter.draw_content || painter.gmaxbin) { + painter.zmin = logz ? painter.gminposbin * 0.3 : painter.gminbin; + painter.zmax = painter.gmaxbin; + zmult = 1 + 2*gStyle.fHistTopMargin; + } + + if (logz && (painter.zmin <= 0)) + painter.zmin = painter.zmax * 1e-5; + + painter.createHistDrawAttributes(true); + + const fp = new TFramePainter(null, null); + assignFrame3DMethods(fp); + + // return dummy frame painter as result + painter.getFramePainter = () => fp; + + return fp.create3DScene(o.Render3D, o.x3dscale, o.y3dscale, o.Ortho).then(() => { + fp.setAxesRanges(histo.fXaxis, painter.xmin, painter.xmax, histo.fYaxis, painter.ymin, painter.ymax, histo.fZaxis, painter.zmin, painter.zmax, painter); + fp.set3DOptions(o); + fp.drawXYZ(fp.toplevel, TAxisPainter, { + ndim: 2, hist_painter: painter, zmult, zoom: false, + draw: true, drawany: o.isCartesian(), + reverse_x: o.RevX, reverse_y: o.RevY + }); + if (painter.isTH2Poly()) + drawTH2PolyLego(painter); + else if (o.Contour) + drawBinsContour3D(painter, true); + else if (o.Surf) + drawBinsSurf3D(painter); + else if (o.Error) + drawBinsError3D(painter); + else + drawBinsLego(painter); + + return fp.toplevel; + }); + } + /** @summary draw TH2 object */ static async draw(dom, histo, opt) { return THistPainter._drawHist(new TH2Painter(dom, histo), opt); From 0cf651e8451cac9652e0e0132c52ff6cf7e674b8 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 09:42:38 +0200 Subject: [PATCH 03/20] Add simple demo for build3d --- demo/hist_build3d.htm | 126 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 demo/hist_build3d.htm diff --git a/demo/hist_build3d.htm b/demo/hist_build3d.htm new file mode 100644 index 000000000..5d89e8f08 --- /dev/null +++ b/demo/hist_build3d.htm @@ -0,0 +1,126 @@ + + + + three.js model for histogram objects + + + + + + + + + + + + + From ba946a33450d70773e389fc29736bbd9c921a545 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 10:16:53 +0200 Subject: [PATCH 04/20] Avoid creation of renderer in build3d While rendering not involved at all, no need to create it --- modules/base/base3d.mjs | 19 ++++++++++++++----- modules/core.mjs | 4 ++++ modules/hist/TH2Painter.mjs | 15 +++++++++++---- modules/hist/hist3d.mjs | 2 ++ 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/modules/base/base3d.mjs b/modules/base/base3d.mjs index 2228d6589..8ab679d53 100644 --- a/modules/base/base3d.mjs +++ b/modules/base/base3d.mjs @@ -229,11 +229,14 @@ function getRender3DKind(render3d, is_batch) { if (is_batch === undefined) is_batch = isBatchMode(); - if (!render3d) render3d = is_batch ? settings.Render3DBatch : settings.Render3D; + if (!render3d) + render3d = is_batch ? settings.Render3DBatch : settings.Render3D; const rc = constants.Render3D; - if (render3d === rc.Default) render3d = is_batch ? rc.WebGLImage : rc.WebGL; - if (is_batch && (render3d === rc.WebGL)) render3d = rc.WebGLImage; + if (render3d === rc.Default) + render3d = is_batch ? rc.WebGLImage : rc.WebGL; + if (is_batch && (render3d === rc.WebGL)) + render3d = rc.WebGLImage; return render3d; } @@ -518,11 +521,14 @@ async function createRender3D(width, height, render3d, args) { render3d = getRender3DKind(render3d); - if (!args) args = { antialias: true, alpha: true }; + if (!args) + args = { antialias: true, alpha: true }; let promise; - if (render3d === rc.SVG) { + if (render3d === rc.None) + promise = Promise.resolve(null); + else if (render3d === rc.SVG) { // SVG rendering const r = createSVGRenderer(false, 0, doc); r.jsroot_dom = doc.createElementNS(nsSVG, 'svg'); @@ -565,6 +571,9 @@ async function createRender3D(width, height, render3d, args) { } return promise.then(renderer => { + if (!renderer) + return renderer; + if (!renderer.jsroot_dom) renderer.jsroot_dom = renderer.domElement; else diff --git a/modules/core.mjs b/modules/core.mjs index 4a984e464..e24e2bac7 100644 --- a/modules/core.mjs +++ b/modules/core.mjs @@ -165,6 +165,8 @@ const constants = { WebGLImage: 2, /** @summary Use SVG rendering, slow, imprecise and not interactive, not recommended */ SVG: 3, + /** @summary Disable renderer, used for three.js model creation, only for internal use recommended */ + None: 4, fromString(s) { if ((s === 'webgl') || (s === 'gl')) return this.WebGL; @@ -172,6 +174,8 @@ const constants = { return this.WebGLImage; if (s === 'svg') return this.SVG; + if (s === 'none') + return this.None; return this.Default; } }, diff --git a/modules/hist/TH2Painter.mjs b/modules/hist/TH2Painter.mjs index 4ef109a24..3400bb295 100644 --- a/modules/hist/TH2Painter.mjs +++ b/modules/hist/TH2Painter.mjs @@ -1,4 +1,4 @@ -import { settings, gStyle, clTMultiGraph, kNoZoom } from '../core.mjs'; +import { settings, constants, gStyle, clTMultiGraph, kNoZoom } from '../core.mjs'; import { getMaterialArgs, THREE } from '../base/base3d.mjs'; import { assignFrame3DMethods, drawBinsLego, drawBinsError3D, drawBinsContour3D, drawBinsSurf3D } from './hist3d.mjs'; import { TAxisPainter } from '../gpad/TAxisPainter.mjs'; @@ -330,12 +330,12 @@ class TH2Painter extends TH2Painter2D { // return dummy frame painter as result painter.getFramePainter = () => fp; - return fp.create3DScene(o.Render3D, o.x3dscale, o.y3dscale, o.Ortho).then(() => { + return fp.create3DScene(constants.Render3D.None, o.x3dscale, o.y3dscale, o.Ortho).then(() => { fp.setAxesRanges(histo.fXaxis, painter.xmin, painter.xmax, histo.fYaxis, painter.ymin, painter.ymax, histo.fZaxis, painter.zmin, painter.zmax, painter); fp.set3DOptions(o); fp.drawXYZ(fp.toplevel, TAxisPainter, { ndim: 2, hist_painter: painter, zmult, zoom: false, - draw: true, drawany: o.isCartesian(), + draw: o.Axis !== -1, drawany: o.isCartesian(), reverse_x: o.RevX, reverse_y: o.RevY }); if (painter.isTH2Poly()) @@ -349,7 +349,14 @@ class TH2Painter extends TH2Painter2D { else drawBinsLego(painter); - return fp.toplevel; + + // correctly cleanup all objects + const res3d = fp.toplevel; + fp.scene.remove(res3d); + fp.toplevel = null; + fp.create3DScene(-1); + + return res3d; }); } diff --git a/modules/hist/hist3d.mjs b/modules/hist/hist3d.mjs index d609e5d9c..e1477d375 100644 --- a/modules/hist/hist3d.mjs +++ b/modules/hist/hist3d.mjs @@ -598,6 +598,8 @@ function create3DScene(render3d, x3dscale, y3dscale, orthographic) { 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); From b2dff9a01e16e46c5660d8c102bc6d86dacd56f1 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 10:44:01 +0200 Subject: [PATCH 05/20] Use common code in build3d and drawing of TH2 --- modules/hist/TH2Painter.mjs | 150 ++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 82 deletions(-) diff --git a/modules/hist/TH2Painter.mjs b/modules/hist/TH2Painter.mjs index 3400bb295..6421fe238 100644 --- a/modules/hist/TH2Painter.mjs +++ b/modules/hist/TH2Painter.mjs @@ -205,6 +205,64 @@ function drawTH2PolyLego(painter) { * @private */ class TH2Painter extends TH2Painter2D { + /** @summary Check range for 3D + * @private */ + checkRangeFor3D(o) { + const pad = this.getPadPainter()?.getRootPad(true), + logz = pad?.fLogv ?? pad?.fLogz; + let zmult = 1; + + if (o.ohmin && o.ohmax) { + this.zmin = o.hmin; + this.zmax = o.hmax; + } else if (o.minimum !== kNoZoom && o.maximum !== kNoZoom) { + this.zmin = o.minimum; + this.zmax = o.maximum; + } else if (this.draw_content || this.gmaxbin) { + this.zmin = logz ? this.gminposbin * 0.3 : this.gminbin; + this.zmax = this.gmaxbin; + zmult = 1 + 2*gStyle.fHistTopMargin; + } + + if (logz && (this.zmin <= 0)) + this.zmin = this.zmax * 1e-5; + + this.createHistDrawAttributes(true); + return zmult; + } + + /** @summary Create frame with axes drawing + * @private */ + async crete3DFrame(fp, render3d, o, histo, zmult) { + assignFrame3DMethods(fp); + return fp.create3DScene(render3d, o.x3dscale, o.y3dscale, o.Ortho).then(() => { + fp.setAxesRanges(histo.fXaxis, this.xmin, this.xmax, histo.fYaxis, this.ymin, this.ymax, histo.fZaxis, this.zmin, this.zmax, this); + fp.set3DOptions(o); + fp.drawXYZ(fp.toplevel, TAxisPainter, { + ndim: 2, hist_painter: this, zmult, zoom: settings.Zooming, + draw: o.Axis !== -1, drawany: o.isCartesian(), + reverse_x: o.RevX, reverse_y: o.RevY + }); + return fp; + }); + } + + /** @summary Create 3D object for histogram bins + * @private */ + draw3DBins(o) { + if (this.isTH2Poly()) + drawTH2PolyLego(this); + else if (o.Contour) + drawBinsContour3D(this, true); + else if (o.Surf) + drawBinsSurf3D(this); + else if (o.Error) + drawBinsError3D(this); + else + drawBinsLego(this); + + } + /** @summary draw TH2 object in 3D mode */ async draw3D(reason) { this.mode3d = true; @@ -226,54 +284,16 @@ class TH2Painter extends TH2Painter2D { } if (full_draw) { - const pad = this.getPadPainter().getRootPad(true), - logz = pad?.fLogv ?? pad?.fLogz; - let zmult = 1; - - if (o.ohmin && o.ohmax) { - this.zmin = o.hmin; - this.zmax = o.hmax; - } else if (o.minimum !== kNoZoom && o.maximum !== kNoZoom) { - this.zmin = o.minimum; - this.zmax = o.maximum; - } else if (this.draw_content || this.gmaxbin) { - this.zmin = logz ? this.gminposbin * 0.3 : this.gminbin; - this.zmax = this.gmaxbin; - zmult = 1 + 2*gStyle.fHistTopMargin; - } + const zmult = this.checkRangeFor3D(o); - if (logz && (this.zmin <= 0)) - this.zmin = this.zmax * 1e-5; - - this.createHistDrawAttributes(true); - - if (is_main) { - assignFrame3DMethods(fp); - pr = fp.create3DScene(o.Render3D, o.x3dscale, o.y3dscale, o.Ortho).then(() => { - fp.setAxesRanges(histo.fXaxis, this.xmin, this.xmax, histo.fYaxis, this.ymin, this.ymax, histo.fZaxis, this.zmin, this.zmax, this); - fp.set3DOptions(o); - fp.drawXYZ(fp.toplevel, TAxisPainter, { - ndim: 2, hist_painter: this, zmult, zoom: settings.Zooming, - draw: o.Axis !== -1, drawany: o.isCartesian(), - reverse_x: o.RevX, reverse_y: o.RevY - }); - }); - } + if (is_main) + pr = this.crete3DFrame(fp, o.Render3D, o, histo, zmult); if (fp.mode3d) { pr = pr.then(() => { - if (this.draw_content) { - if (this.isTH2Poly()) - drawTH2PolyLego(this); - else if (o.Contour) - drawBinsContour3D(this, true); - else if (o.Surf) - drawBinsSurf3D(this); - else if (o.Error) - drawBinsError3D(this); - else - drawBinsLego(this); - } else if (o.Axis && o.Zscale) { + if (this.draw_content) + this.draw3DBins(o); + else if (o.Axis && o.Zscale) { this.getContourLevels(true); this.getHistPalette(); } @@ -305,24 +325,7 @@ class TH2Painter extends TH2Painter2D { o.Lego = 12; painter.scanContent(); - let zmult = 1; - - if (o.ohmin && o.ohmax) { - painter.zmin = o.hmin; - painter.zmax = o.hmax; - } else if (o.minimum !== kNoZoom && o.maximum !== kNoZoom) { - painter.zmin = o.minimum; - painter.zmax = o.maximum; - } else if (painter.draw_content || painter.gmaxbin) { - painter.zmin = logz ? painter.gminposbin * 0.3 : painter.gminbin; - painter.zmax = painter.gmaxbin; - zmult = 1 + 2*gStyle.fHistTopMargin; - } - - if (logz && (painter.zmin <= 0)) - painter.zmin = painter.zmax * 1e-5; - - painter.createHistDrawAttributes(true); + const zmult = painter.checkRangeFor3D(o); const fp = new TFramePainter(null, null); assignFrame3DMethods(fp); @@ -330,26 +333,9 @@ class TH2Painter extends TH2Painter2D { // return dummy frame painter as result painter.getFramePainter = () => fp; - return fp.create3DScene(constants.Render3D.None, o.x3dscale, o.y3dscale, o.Ortho).then(() => { - fp.setAxesRanges(histo.fXaxis, painter.xmin, painter.xmax, histo.fYaxis, painter.ymin, painter.ymax, histo.fZaxis, painter.zmin, painter.zmax, painter); - fp.set3DOptions(o); - fp.drawXYZ(fp.toplevel, TAxisPainter, { - ndim: 2, hist_painter: painter, zmult, zoom: false, - draw: o.Axis !== -1, drawany: o.isCartesian(), - reverse_x: o.RevX, reverse_y: o.RevY - }); - if (painter.isTH2Poly()) - drawTH2PolyLego(painter); - else if (o.Contour) - drawBinsContour3D(painter, true); - else if (o.Surf) - drawBinsSurf3D(painter); - else if (o.Error) - drawBinsError3D(painter); - else - drawBinsLego(painter); - - + return painter.crete3DFrame(fp, constants.Render3D.None, o, histo, zmult).then(() => { + if (painter.draw_content) + painter.draw3DBins(o); // correctly cleanup all objects const res3d = fp.toplevel; fp.scene.remove(res3d); From d092620d9ef65fc0978c714814f50b36f3667498 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 11:10:12 +0200 Subject: [PATCH 06/20] Introduce central build3d function It loads class for specified object type and tries to invoke static build3d in that class. First works with TH2 classes --- modules/draw.mjs | 23 ++++++++++++++++++++++- modules/main.mjs | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/modules/draw.mjs b/modules/draw.mjs index 11ff1290b..d93a1b1cf 100644 --- a/modules/draw.mjs +++ b/modules/draw.mjs @@ -548,6 +548,27 @@ async function redraw(dom, obj, opt) { return draw(dom, obj, opt); } +/** @summary Create three.js model for object + * @param {object} obj - object + * @param {string} opt - draw options + * @return {Promise} with three.js model */ + +async function build3d(obj, opt) { + if (!isObject(obj) || !obj?._typename) + return Promise.reject(Error('not an object in build3d')); + + const handle = getDrawHandle(getKindForType(obj._typename)); + if (!handle?.class) + return Promise.reject(Error(`not able to create three.js for ${obj._typename}`)); + + return handle.class().then(cl => { + if (!isFunc(cl?.build3d)) + return Promise.reject(Error(`painter class for ${obj._typename} does not implement build3d method`)); + + return cl.build3d(obj, opt); + }); +} + /** @summary Scan streamer infos for derived classes * @desc Assign draw functions for such derived classes * @private */ @@ -756,4 +777,4 @@ Object.assign(internals, { addStreamerInfosForPainter, addDrawFunc, setDefaultDr Object.assign(internals.jsroot, { draw, redraw, makeSVG, makeImage, addDrawFunc }); export { addDrawFunc, getDrawHandle, canDrawHandle, getDrawSettings, setDefaultDrawOpt, - draw, redraw, cleanup, makeSVG, makeImage, assignPadPainterDraw }; + draw, redraw,cleanup, build3d, makeSVG, makeImage, assignPadPainterDraw }; diff --git a/modules/main.mjs b/modules/main.mjs index e01fda2dd..ce0b5dcae 100644 --- a/modules/main.mjs +++ b/modules/main.mjs @@ -31,7 +31,7 @@ export { createGeoPainter, TGeoPainter } from './geom/TGeoPainter.mjs'; export { loadOpenui5, registerForResize, setSaveFile, addMoveHandler } from './gui/utils.mjs'; -export { draw, redraw, makeSVG, makeImage, addDrawFunc, setDefaultDrawOpt } from './draw.mjs'; +export { draw, redraw, cleanup, build3d, makeSVG, makeImage, addDrawFunc, setDefaultDrawOpt } from './draw.mjs'; export * from './gpad/TCanvasPainter.mjs'; From c0b9af4c3805d085eace3e6dc0e850e27f38edae Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 11:10:34 +0200 Subject: [PATCH 07/20] Update demo with new build3d function --- demo/hist_build3d.htm | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/demo/hist_build3d.htm b/demo/hist_build3d.htm index 5d89e8f08..0442b1221 100644 --- a/demo/hist_build3d.htm +++ b/demo/hist_build3d.htm @@ -14,7 +14,7 @@ } @@ -23,13 +23,11 @@ From 56246db4202a57c536dc512c20c967fa7c82a7a8 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 14:27:55 +0200 Subject: [PATCH 13/20] Provide build3d function for text/latex Let use latex parser and create THREE.Mesh --- modules/draw.mjs | 9 ++++++--- modules/hist/hist3d.mjs | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/modules/draw.mjs b/modules/draw.mjs index d93a1b1cf..469b87757 100644 --- a/modules/draw.mjs +++ b/modules/draw.mjs @@ -49,7 +49,7 @@ drawFuncs = { lst: [ { name: clTDiamond, sameas: clTPave }, { name: clTLegend, icon: 'img_pavelabel', sameas: clTPave }, { name: clTPaletteAxis, icon: 'img_colz', sameas: clTPave }, - { name: clTLatex, icon: 'img_text', draw: () => import_more().then(h => h.drawText), direct: true }, + { name: clTLatex, icon: 'img_text', draw: () => import_more().then(h => h.drawText), build3d: () => import('./hist/hist3d.mjs').then(h => h.build3dlatex), direct: true }, { name: clTMathText, sameas: clTLatex }, { name: clTText, sameas: clTLatex }, { name: clTLink, sameas: clTText }, @@ -558,9 +558,12 @@ async function build3d(obj, opt) { return Promise.reject(Error('not an object in build3d')); const handle = getDrawHandle(getKindForType(obj._typename)); - if (!handle?.class) + if (!handle?.class && !handle.build3d) return Promise.reject(Error(`not able to create three.js for ${obj._typename}`)); + if (handle.build3d) + return handle.build3d().then(func => func(obj, opt)) + return handle.class().then(cl => { if (!isFunc(cl?.build3d)) return Promise.reject(Error(`painter class for ${obj._typename} does not implement build3d method`)); @@ -777,4 +780,4 @@ Object.assign(internals, { addStreamerInfosForPainter, addDrawFunc, setDefaultDr Object.assign(internals.jsroot, { draw, redraw, makeSVG, makeImage, addDrawFunc }); export { addDrawFunc, getDrawHandle, canDrawHandle, getDrawSettings, setDefaultDrawOpt, - draw, redraw,cleanup, build3d, makeSVG, makeImage, assignPadPainterDraw }; + draw, redraw,cleanup, build3d, makeSVG, makeImage, assignPadPainterDraw }; diff --git a/modules/hist/hist3d.mjs b/modules/hist/hist3d.mjs index 09536f5c6..d19e7529b 100644 --- a/modules/hist/hist3d.mjs +++ b/modules/hist/hist3d.mjs @@ -179,6 +179,19 @@ function createLatexGeometry(painter, lbl, size) { return fullgeom; } +/** @summary Build three.js object for the TLatex + * @private */ +function build3dlatex(obj, opt) { + let geom = createLatexGeometry(null, obj.fName, 20); + + let material = new THREE.MeshBasicMaterial(getMaterialArgs('blue', { vertexColors: false })); + + const mesh = new THREE.Mesh(geom, material); + + return mesh; +} + + /** @summary Text 3d axis visibility * @private */ function testAxisVisibility(camera, toplevel, fb = false, bb = false) { @@ -2402,6 +2415,6 @@ function drawBinsSurf3D(painter, is_v7 = false) { } } -export { assignFrame3DMethods, crete3DFrame, +export { assignFrame3DMethods, crete3DFrame, build3dlatex, drawBinsLego, drawBinsError3D, drawBinsContour3D, drawBinsSurf3D, convertLegoBuf, createLegoGeom }; From bcb9b1df19bc162680a04a96441c919af11e9d1f Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 14:58:30 +0200 Subject: [PATCH 14/20] Handle color, size and align for text build3d --- modules/hist/hist3d.mjs | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/modules/hist/hist3d.mjs b/modules/hist/hist3d.mjs index d19e7529b..6f785852a 100644 --- a/modules/hist/hist3d.mjs +++ b/modules/hist/hist3d.mjs @@ -5,6 +5,7 @@ import { THREE, assign3DHandler, disposeThreejsObject, createOrbitControl, createRender3D, beforeRender3D, afterRender3D, getRender3DKind, cleanupRender3D, getHelveticaFont, createSVGRenderer, create3DLineMaterial } from '../base/base3d.mjs'; import { isPlainText, translateLaTeX, produceLatex } from '../base/latex.mjs'; +import { ObjectPainter } from '../base/ObjectPainter.mjs'; import { kCARTESIAN, kPOLAR, kCYLINDRICAL, kSPHERICAL, kRAPIDITY } from '../hist2d/THistPainter.mjs'; import { buildHist2dContour, buildSurf3D } from '../hist2d/TH2Painter.mjs'; @@ -181,16 +182,34 @@ function createLatexGeometry(painter, lbl, size) { /** @summary Build three.js object for the TLatex * @private */ -function build3dlatex(obj, opt) { - let geom = createLatexGeometry(null, obj.fName, 20); +function build3dlatex(obj) { + const painter = new ObjectPainter(null, obj), + handle = painter.createAttText({ attr: obj }), + valign = handle.align % 10, + halign = handle.align - valign, + text3d = createLatexGeometry(painter, obj.fTitle, handle.getSize() || 10); - let material = new THREE.MeshBasicMaterial(getMaterialArgs('blue', { vertexColors: false })); + text3d.computeBoundingBox(); - const mesh = new THREE.Mesh(geom, material); + let width = text3d.boundingBox.max.x - text3d.boundingBox.min.x, + height = text3d.boundingBox.max.y - text3d.boundingBox.min.y; - return mesh; -} + if (halign === 1) + width = 0; + else if (halign === 2) + width *= 0.5; + + if (valign === 1) + height = 0; + else if (valign === 2) + height *= 0.5; + + text3d.translate(-width, -height, 0); + const material = new THREE.MeshBasicMaterial(getMaterialArgs(handle.color || 'black', { vertexColors: false })); + + return new THREE.Mesh(text3d, material); +} /** @summary Text 3d axis visibility * @private */ From 56ff22f65677224e4ab8bfe838c3b611ee2745e7 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 15:15:08 +0200 Subject: [PATCH 15/20] Add labels drawing to demos --- demo/hist_build3d.htm | 108 ++++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 52 deletions(-) diff --git a/demo/hist_build3d.htm b/demo/hist_build3d.htm index b89549efe..67d05dc65 100644 --- a/demo/hist_build3d.htm +++ b/demo/hist_build3d.htm @@ -23,7 +23,7 @@ From b7ba6f8915488a1d399c3418847ed77c66331565 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 16:03:07 +0200 Subject: [PATCH 16/20] Support build3d for geometry Copy functionality of existing build function Keep old for compatibility --- modules/geom/TGeoPainter.mjs | 289 ++++++++++++++++++----------------- 1 file changed, 150 insertions(+), 139 deletions(-) diff --git a/modules/geom/TGeoPainter.mjs b/modules/geom/TGeoPainter.mjs index 2d9f8fda5..b4bce0b98 100644 --- a/modules/geom/TGeoPainter.mjs +++ b/modules/geom/TGeoPainter.mjs @@ -917,9 +917,8 @@ class TGeoPainter extends ObjectPainter { if (this.#superimpose && (opt.indexOf('same') === 0)) opt = opt.slice(4); - const res = this.ctrl, + const res = this.ctrl, macro = opt.indexOf('macro:'); - macro = opt.indexOf('macro:'); if (macro >= 0) { let separ = opt.indexOf(';', macro+6); if (separ < 0) separ = opt.length; @@ -5477,6 +5476,154 @@ class TGeoPainter extends ObjectPainter { return this.prepareObjectDraw(draw_obj, name_prefix); } + /** @summary Build three.js object */ + static build3d(obj, sopt) { + if (!obj) + return null; + + let opt = null; + if (isStr(sopt)) { + const painter = new TGeoPainter(null, obj); + painter.decodeOptions(sopt); + opt = painter.ctrl; + opt.numfaces = opt.maxfaces; + opt.numnodes = opt.maxnodes; + } else + opt = sopt || {}; + + if (!opt.numfaces) + opt.numfaces = 100000; + if (!opt.numnodes) + opt.numnodes = 1000; + if (!opt.frustum) + opt.frustum = null; + + opt.res_mesh = opt.res_faces = 0; + + if (opt.instancing === undefined) + opt.instancing = -1; + + opt.info = { num_meshes: 0, num_faces: 0 }; + + let clones, visibles; + + if (obj.visibles && obj.nodes && obj.numnodes) { + // case of draw message from geometry viewer + + const nodes = obj.numnodes > 1e6 ? { length: obj.numnodes } : new Array(obj.numnodes); + + obj.nodes.forEach(node => { + nodes[node.id] = ClonedNodes.formatServerElement(node); + }); + + clones = new ClonedNodes(null, nodes); + clones.name_prefix = clones.getNodeName(0); + + // normally only need when making selection, not used in geo viewer + // this.geo_clones.setMaxVisNodes(draw_msg.maxvisnodes); + // this.geo_clones.setVisLevel(draw_msg.vislevel); + // TODO: provide from server + clones.maxdepth = 20; + + const nsegm = obj.cfg?.nsegm || 30; + + for (let cnt = 0; cnt < obj.visibles.length; ++cnt) { + const item = obj.visibles[cnt], rd = item.ri; + + // entry may be provided without shape - it is ok + if (rd) + item.server_shape = rd.server_shape = createServerGeometry(rd, nsegm); + } + + visibles = obj.visibles; + } else { + let shape = null, hide_top = false; + + if (('fShapeBits' in obj) && ('fShapeId' in obj)) { + shape = obj; obj = null; + } else if ((obj._typename === clTGeoVolumeAssembly) || (obj._typename === clTGeoVolume)) + shape = obj.fShape; + else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) + shape = obj.fShape; + else if (obj._typename === clTGeoManager) { + obj = obj.fMasterVolume; + hide_top = !opt.showtop; + shape = obj.fShape; + } else if (obj.fVolume) + shape = obj.fVolume.fShape; + else + obj = null; + + if (opt.composite && shape && (shape._typename === clTGeoCompositeShape) && shape.fNode) + obj = buildCompositeVolume(shape); + + if (!obj && shape) + obj = Object.assign(create(clTNamed), { _typename: clTEveGeoShapeExtract, fTrans: null, fShape: shape, fRGBA: [0, 1, 0, 1], fElements: null, fRnrSelf: true }); + + if (!obj) + return null; + + if (obj._typename.indexOf(clTGeoVolume) === 0) + obj = { _typename: clTGeoNode, fVolume: obj, fName: obj.fName, $geoh: obj.$geoh, _proxy: true }; + + clones = new ClonedNodes(obj); + clones.setVisLevel(opt.vislevel); + clones.setMaxVisNodes(opt.numnodes); + + if (opt.dflt_colors) + clones.setDefaultColors(true); + + const uniquevis = opt.no_screen ? 0 : clones.markVisibles(true); + if (uniquevis <= 0) + clones.markVisibles(false, false, hide_top); + else + clones.markVisibles(true, true, hide_top); // copy bits once and use normal visibility bits + + clones.produceIdShifts(); + + // collect visible nodes + const res = clones.collectVisibles(opt.numfaces, opt.frustum); + + visibles = res.lst; + } + + if (!opt.material_kind) + opt.material_kind = 'lambert'; + if (opt.set_names === undefined) + opt.set_names = true; + + clones.setConfig(opt); + + // collect shapes + const shapes = clones.collectShapes(visibles); + + clones.buildShapes(shapes, opt.numfaces); + + const toplevel = new THREE.Object3D(); + toplevel.clones = clones; // keep reference on JSROOT data + + const colors = getRootColors(); + + if (clones.createInstancedMeshes(opt, toplevel, visibles, shapes, colors)) + return toplevel; + + for (let n = 0; n < visibles.length; ++n) { + const entry = visibles[n]; + if (entry.done) + continue; + + const shape = entry.server_shape || shapes[entry.shapeid]; + if (!shape.ready) { + console.warn('shape marked as not ready when it should'); + break; + } + + clones.createEntryMesh(opt, toplevel, entry, shape, colors); + } + + return toplevel; + } + /** @summary draw TGeo object */ static async draw(dom, obj, opt) { if (!obj) @@ -6026,143 +6173,7 @@ function drawAxis3D() { * // this is three.js object and can be now inserted in the scene */ function build(obj, opt) { - if (!obj) - return null; - - if (!opt) - opt = {}; - if (!opt.numfaces) - opt.numfaces = 100000; - if (!opt.numnodes) - opt.numnodes = 1000; - if (!opt.frustum) - opt.frustum = null; - - opt.res_mesh = opt.res_faces = 0; - - if (opt.instancing === undefined) - opt.instancing = -1; - - opt.info = { num_meshes: 0, num_faces: 0 }; - - let clones, visibles; - - if (obj.visibles && obj.nodes && obj.numnodes) { - // case of draw message from geometry viewer - - const nodes = obj.numnodes > 1e6 ? { length: obj.numnodes } : new Array(obj.numnodes); - - obj.nodes.forEach(node => { - nodes[node.id] = ClonedNodes.formatServerElement(node); - }); - - clones = new ClonedNodes(null, nodes); - clones.name_prefix = clones.getNodeName(0); - - // normally only need when making selection, not used in geo viewer - // this.geo_clones.setMaxVisNodes(draw_msg.maxvisnodes); - // this.geo_clones.setVisLevel(draw_msg.vislevel); - // TODO: provide from server - clones.maxdepth = 20; - - const nsegm = obj.cfg?.nsegm || 30; - - for (let cnt = 0; cnt < obj.visibles.length; ++cnt) { - const item = obj.visibles[cnt], rd = item.ri; - - // entry may be provided without shape - it is ok - if (rd) - item.server_shape = rd.server_shape = createServerGeometry(rd, nsegm); - } - - visibles = obj.visibles; - } else { - let shape = null, hide_top = false; - - if (('fShapeBits' in obj) && ('fShapeId' in obj)) { - shape = obj; obj = null; - } else if ((obj._typename === clTGeoVolumeAssembly) || (obj._typename === clTGeoVolume)) - shape = obj.fShape; - else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) - shape = obj.fShape; - else if (obj._typename === clTGeoManager) { - obj = obj.fMasterVolume; - hide_top = !opt.showtop; - shape = obj.fShape; - } else if (obj.fVolume) - shape = obj.fVolume.fShape; - else - obj = null; - - - if (opt.composite && shape && (shape._typename === clTGeoCompositeShape) && shape.fNode) - obj = buildCompositeVolume(shape); - - if (!obj && shape) - obj = Object.assign(create(clTNamed), { _typename: clTEveGeoShapeExtract, fTrans: null, fShape: shape, fRGBA: [0, 1, 0, 1], fElements: null, fRnrSelf: true }); - - if (!obj) - return null; - - if (obj._typename.indexOf(clTGeoVolume) === 0) - obj = { _typename: clTGeoNode, fVolume: obj, fName: obj.fName, $geoh: obj.$geoh, _proxy: true }; - - clones = new ClonedNodes(obj); - clones.setVisLevel(opt.vislevel); - clones.setMaxVisNodes(opt.numnodes); - - if (opt.dflt_colors) - clones.setDefaultColors(true); - - const uniquevis = opt.no_screen ? 0 : clones.markVisibles(true); - if (uniquevis <= 0) - clones.markVisibles(false, false, hide_top); - else - clones.markVisibles(true, true, hide_top); // copy bits once and use normal visibility bits - - clones.produceIdShifts(); - - // collect visible nodes - const res = clones.collectVisibles(opt.numfaces, opt.frustum); - - visibles = res.lst; - } - - if (!opt.material_kind) - opt.material_kind = 'lambert'; - if (opt.set_names === undefined) - opt.set_names = true; - - clones.setConfig(opt); - - // collect shapes - const shapes = clones.collectShapes(visibles); - - clones.buildShapes(shapes, opt.numfaces); - - const toplevel = new THREE.Object3D(); - toplevel.clones = clones; // keep reference on JSROOT data - - const colors = getRootColors(); - - if (clones.createInstancedMeshes(opt, toplevel, visibles, shapes, colors)) - return toplevel; - - for (let n = 0; n < visibles.length; ++n) { - const entry = visibles[n]; - if (entry.done) - continue; - - const shape = entry.server_shape || shapes[entry.shapeid]; - if (!shape.ready) { - console.warn('shape marked as not ready when it should'); - break; - } - - clones.createEntryMesh(opt, toplevel, entry, shape, colors); - } - - return toplevel; + return TGeoPainter.build3d(obj, opt); } export { ClonedNodes, build, TGeoPainter, GeoDrawingControl, From 24676937e0a49f09960ea53e40831c9cd104ad30 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 16:55:05 +0200 Subject: [PATCH 17/20] Implement build3d for TGraph2D --- modules/hist/TGraph2DPainter.mjs | 32 +++++++++++++++++++++++++++++--- modules/hist/TH2Painter.mjs | 4 ++-- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/modules/hist/TGraph2DPainter.mjs b/modules/hist/TGraph2DPainter.mjs index b983ec7d4..3f7c66e5f 100644 --- a/modules/hist/TGraph2DPainter.mjs +++ b/modules/hist/TGraph2DPainter.mjs @@ -1122,9 +1122,11 @@ class TGraph2DPainter extends ObjectPainter { res.Axis = ''; else if (res.isAny()) { res.Axis = 'lego2'; - if (res.Zscale) res.Axis += 'z'; - } else + if (res.Zscale) + res.Axis += 'z'; + } else { res.Axis = opt; + } this.storeDrawOpt(opt); } @@ -1415,7 +1417,10 @@ class TGraph2DPainter extends ObjectPainter { if (fp.usesvg) scale *= 0.3; - scale *= 7 * Math.max(fp.size_x3d / fp.getFrameWidth(), fp.size_z3d / fp.getFrameHeight()); + const fw = fp.getFrameWidth(), fh = fp.getFrameHeight(); + + if ((fw > 10) && (fh > 10)) + scale *= 7 * Math.max(fp.size_x3d / fw, fp.size_z3d / fh); if (o.Color || (o.Triangles >= 10)) { levels = main.getContourLevels(true); @@ -1577,6 +1582,27 @@ class TGraph2DPainter extends ObjectPainter { }); } + /** @summary Build three.js of TGraph2D object */ + static async build3d(gr, opt) { + const painter = new TGraph2DPainter(null, gr); + painter.decodeOptions(opt, gr); + + if (painter.options.Contour) { + console.error('Contour plot is not 3d'); + return null; + } + + return TH2Painter.build3d(painter.createHistogram(), painter.options.Axis, true).then(hist_painter => { + painter.axes_draw = true; + const fp = hist_painter.getFramePainter(); + + painter.getFramePainter = () => fp; + painter.getMainPainter = () => hist_painter; + + return painter.drawGraph2D().then(() => fp.create3DScene(-1, true)) + }) + } + /** @summary draw TGraph2D object */ static async draw(dom, gr, opt) { const painter = new TGraph2DPainter(dom, gr); diff --git a/modules/hist/TH2Painter.mjs b/modules/hist/TH2Painter.mjs index 77acdf5f7..2a8ec4d6c 100644 --- a/modules/hist/TH2Painter.mjs +++ b/modules/hist/TH2Painter.mjs @@ -300,7 +300,7 @@ class TH2Painter extends TH2Painter2D { } /** @summary Build three.js object for the histogram */ - static async build3d(histo, opt) { + static async build3d(histo, opt, get_painter) { const painter = new TH2Painter(null, histo); painter.decodeOptions(opt); @@ -319,7 +319,7 @@ class TH2Painter extends TH2Painter2D { if (painter.draw_content) painter.draw3DBins(o); - return fp.create3DScene(-1, true); + return get_painter ? painter : fp.create3DScene(-1, true); }); } From f596a1e19b36ab4bb8090fd66cb791b7a977e0a0 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 17:33:51 +0200 Subject: [PATCH 18/20] Support text colors for the latex 3d --- modules/hist/hist3d.mjs | 52 ++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/modules/hist/hist3d.mjs b/modules/hist/hist3d.mjs index 6f785852a..0014d13ee 100644 --- a/modules/hist/hist3d.mjs +++ b/modules/hist/hist3d.mjs @@ -10,15 +10,17 @@ import { kCARTESIAN, kPOLAR, kCYLINDRICAL, kSPHERICAL, kRAPIDITY } from '../hist import { buildHist2dContour, buildSurf3D } from '../hist2d/TH2Painter.mjs'; -function createLatexGeometry(painter, lbl, size) { +function createLatexGeometry(painter, lbl, size, as_array) { const geom_args = { font: getHelveticaFont(), size, height: 0, curveSegments: 5 }; if (THREE.REVISION > 162) geom_args.depth = 0; else geom_args.height = 0; - if (isPlainText(lbl)) - return new THREE.TextGeometry(translateLaTeX(lbl), geom_args); + if (isPlainText(lbl)) { + const res = new THREE.TextGeometry(translateLaTeX(lbl), geom_args); + return as_array ? [res] : res; + } const font_size = size * 100, geoms = []; let stroke_width = 5; @@ -92,6 +94,8 @@ function createLatexGeometry(painter, lbl, size) { this.x += Number.parseInt(value)*0.01; else if ((name === 'y') && (this.kind === 'text')) this.y -= Number.parseInt(value)*0.01; + else if ((name === 'fill') && (this.kind === 'text')) + this.fill = value; else if ((name === 'd') && (this.kind === 'path')) { if (get() !== 'M') return console.error('Not starts with M'); @@ -122,6 +126,7 @@ function createLatexGeometry(painter, lbl, size) { const pos = new Float32Array(pnts); this.geom = new THREE.BufferGeometry(); + this.geom._fill = this.fill; this.geom.setAttribute('position', new THREE.BufferAttribute(pos, 3)); this.geom.scale(0.01, -0.01, 0.01); this.geom.computeVertexNormals(); @@ -135,6 +140,7 @@ function createLatexGeometry(painter, lbl, size) { if (this.kind === 'text') { geom_args.size = Math.round(0.01*this.font_size); this.geom = new THREE.TextGeometry(v, geom_args); + this.geom._fill = this.fill; geoms.push(this.geom); } } @@ -148,11 +154,15 @@ function createLatexGeometry(painter, lbl, size) { if (!geoms.length) { geom_args.size = size; - return new THREE.TextGeometry(translateLaTeX(lbl), geom_args); + const res = new THREE.TextGeometry(translateLaTeX(lbl), geom_args); + return as_array ? [res] : res; } node.translate(); // apply translate attributes + if (as_array) + return geoms; + if (geoms.length === 1) return geoms[0]; @@ -187,12 +197,17 @@ function build3dlatex(obj) { handle = painter.createAttText({ attr: obj }), valign = handle.align % 10, halign = handle.align - valign, - text3d = createLatexGeometry(painter, obj.fTitle, handle.getSize() || 10); + arr3d = createLatexGeometry(painter, obj.fTitle, handle.getSize() || 10, true), + bb = new THREE.Box3().makeEmpty(); - text3d.computeBoundingBox(); + arr3d.forEach(geom => { + geom.computeBoundingBox(); + bb.expandByPoint(geom.boundingBox.max); + bb.expandByPoint(geom.boundingBox.min); + }) - let width = text3d.boundingBox.max.x - text3d.boundingBox.min.x, - height = text3d.boundingBox.max.y - text3d.boundingBox.min.y; + let width = bb.max.x - bb.min.x, + height = bb.max.y - bb.min.y; if (halign === 1) width = 0; @@ -204,11 +219,26 @@ function build3dlatex(obj) { else if (valign === 2) height *= 0.5; - text3d.translate(-width, -height, 0); + const materials = [], + getMaterial = color => { + if (!color) + color = 'black'; + if (!materials[color]) + materials[color] = new THREE.MeshBasicMaterial(getMaterialArgs(color, { vertexColors: false })); + return materials[color]; + }; + - const material = new THREE.MeshBasicMaterial(getMaterialArgs(handle.color || 'black', { vertexColors: false })); + const material0 = new THREE.MeshBasicMaterial(getMaterialArgs(handle.color || 'black', { vertexColors: false })); + + const obj3d = new THREE.Object3D(); + + arr3d.forEach(geom => { + geom.translate(-width, -height, 0); + obj3d.add(new THREE.Mesh(geom, getMaterial(geom._fill || handle.color))); + }); - return new THREE.Mesh(text3d, material); + return arr3d.length === 1 ? obj3d.children[0] : obj3d; } /** @summary Text 3d axis visibility From 56e4f3cb99e2bcf66ed6a5ffe6da07be75d09188 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 17:35:54 +0200 Subject: [PATCH 19/20] Add colors to latex plots --- demo/hist_build3d.htm | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/demo/hist_build3d.htm b/demo/hist_build3d.htm index 67d05dc65..b3fd3359e 100644 --- a/demo/hist_build3d.htm +++ b/demo/hist_build3d.htm @@ -57,9 +57,11 @@ renderer.render( scene, camera ); } - async function create3d(obj, opt, x = 0, lbl = '') { + async function create3d(obj, opt, x = 0, lbl = '', scale = 1) { return build3d(obj, opt).then(obj3d => { obj3d.position.x = x; + if (scale !== 1) + obj3d.scale.set(scale, scale, scale); scene.add(obj3d); @@ -70,13 +72,13 @@ latex.fTextSize = 10; // absolute size return build3d(latex); - }).then(obj3d => { - // latex created in X/Y coordinates - obj3d.geometry.rotateX(Math.PI / 2); + }).then(text3d => { + // latex created in X/Y coordinates, + text3d.traverse(obj3d => obj3d.geometry?.rotateX(Math.PI / 2)); - obj3d.position.x = x; - obj3d.position.z = -100; - scene.add(obj3d); + text3d.position.x = x; + text3d.position.z = -100; + scene.add(text3d); }); } @@ -108,22 +110,30 @@ window.addEventListener( 'resize', onWindowResize, false ); - let filename = 'https://root.cern/js/files/hsimple.root'; + let filename = 'https://root.cern/js/files/hsimple.root', + filename2 = 'https://jsroot.gsi.de/files/graph2d.root'; let file = await openFile(filename); let hist2 = await file.readObject('hpxpy'); - await create3d(hist2, 'lego2', 0, 'TH2 lego plot'); + await create3d(hist2, 'lego2', 150, '#color[2]{TH2} #color[4]{lego} plot'); let tuple = await file.readObject('ntuple'); let hist3 = await treeDraw(tuple, 'px:py:pz;hbins:15'); - await create3d(hist3, 'box3', -400, 'TH3 box plot'); + await create3d(hist3, 'box3', -150, '#color[2]{TH3} #color[4]{box} plot'); let hist1 = await file.readObject('hpx'); - await create3d(hist1, 'lego2', 400, 'TH1 lego plot'); + await create3d(hist1, 'lego2', 400, '#color[2]{TH1} #color[4]{lego} plot'); + + let geom = await httpRequest('https://root.cern/js/files/geom/simple_alice.json.gz', 'object'); + await create3d(geom, '', -400, '#color[2]{Geometry} build', 0.2); + + let file2 = await openFile(filename2); + let gr2 = await file2.readObject('Graph2D'); + await create3d(gr2, 'p', 650, '#color[2]{TGraph2D} drawing with #color[4]{p}'); camera.updateProjectionMatrix(); From 03d5d501231547e643c4bc81f7446a9c3702c89a Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 29 Sep 2025 17:43:50 +0200 Subject: [PATCH 20/20] Build with build3d support --- build/jsroot.js | 844 ++++++++++++++++++++----------- changes.md | 1 + modules/core.mjs | 2 +- modules/draw.mjs | 4 +- modules/geom/TGeoPainter.mjs | 5 +- modules/hist/TGraph2DPainter.mjs | 7 +- modules/hist/TH1Painter.mjs | 1 - modules/hist/TH2Painter.mjs | 6 +- modules/hist/TH3Painter.mjs | 3 +- modules/hist/hist3d.mjs | 10 +- 10 files changed, 566 insertions(+), 317 deletions(-) diff --git a/build/jsroot.js b/build/jsroot.js index 5ee3124b4..9ef43aa8e 100644 --- a/build/jsroot.js +++ b/build/jsroot.js @@ -12,7 +12,7 @@ const version_id = 'dev', /** @summary version date * @desc Release date in format day/month/year like '14/04/2022' */ -version_date = '25/09/2025', +version_date = '29/09/2025', /** @summary version id and date * @desc Produced by concatenation of {@link version_id} and {@link version_date} @@ -173,6 +173,8 @@ const constants$1 = { WebGLImage: 2, /** @summary Use SVG rendering, slow, imprecise and not interactive, not recommended */ SVG: 3, + /** @summary Disable renderer, used for three.js model creation, only for internal use recommended */ + None: 4, fromString(s) { if ((s === 'webgl') || (s === 'gl')) return this.WebGL; @@ -180,6 +182,8 @@ const constants$1 = { return this.WebGLImage; if (s === 'svg') return this.SVG; + if (s === 'none') + return this.None; return this.Default; } }, @@ -684,7 +688,8 @@ function parse$1(json) { if (isArrayProto(proto) > 0) { for (let i = 0; i < value.length; ++i) { const res = unref_value(value[i]); - if (res !== undefined) value[i] = res; + if (res !== undefined) + value[i] = res; } return; } @@ -732,8 +737,10 @@ function parse$1(json) { // compressed coding let nkey = 2, p = 0; while (nkey < len) { - if (ks[nkey][0] === 'p') p = value[ks[nkey++]]; // position - if (ks[nkey][0] !== 'v') throw new Error(`Unexpected member ${ks[nkey]} in array decoding`); + if (ks[nkey][0] === 'p') + p = value[ks[nkey++]]; // position + if (ks[nkey][0] !== 'v') + throw new Error(`Unexpected member ${ks[nkey]} in array decoding`); const v = value[ks[nkey++]]; // value if (typeof v === 'object') { for (let k = 0; k < v.length; ++k) @@ -755,8 +762,10 @@ function parse$1(json) { newfmt = true; const f1 = unref_value(value.first), s1 = unref_value(value.second); - if (f1 !== undefined) value.first = f1; - if (s1 !== undefined) value.second = s1; + if (f1 !== undefined) + value.first = f1; + if (s1 !== undefined) + value.second = s1; value._typename = value.$pair; delete value.$pair; return; // pair object is not counted in the objects map @@ -770,11 +779,13 @@ function parse$1(json) { map.push(value); // add methods to all objects, where _typename is specified - if (value._typename) exports.addMethods(value); + if (value._typename) + exports.addMethods(value); for (let k = 0; k < len; ++k) { const i = ks[k], res = unref_value(value[i]); - if (res !== undefined) value[i] = res; + if (res !== undefined) + value[i] = res; } }; @@ -875,7 +886,7 @@ function decodeUrl(url) { const res = { opts: {}, has(opt) { return this.opts[opt] !== undefined; }, - get(opt, dflt) { const v = this.opts[opt]; return v !== undefined ? v : dflt; } + get(opt, dflt) { return this.opts[opt] ?? dflt; } }; if (!url || !isStr(url)) { @@ -11500,10 +11511,12 @@ class TAttMarkerHandler { mv = ''; // pathological case, but let exclude it else { const m2 = `m${xx-this.lastx},${yy - this.lasty}`; - if (m2.length < mv.length) mv = m2; + if (m2.length < mv.length) + mv = m2; } } - this.lastx = xx + 1; this.lasty = yy; + this.lastx = xx + 1; + this.lasty = yy; return mv + 'h1'; } @@ -11520,9 +11533,12 @@ class TAttMarkerHandler { change(color, style, size) { this.changed = true; - if (color !== undefined) this.color = color; - if ((style !== undefined) && (style >= 0)) this.style = style; - if (size !== undefined) this.size = size; + if (color !== undefined) + this.color = color; + if ((style !== undefined) && (style >= 0)) + this.style = style; + if (size !== undefined) + this.size = size; this._configure(); } @@ -12206,7 +12222,8 @@ class TAttLineHandler { constructor(args) { this.func = this.apply.bind(this); this.used = true; - if (args._typename && (args.fLineStyle !== undefined)) args = { attr: args }; + if (args._typename && (args.fLineStyle !== undefined)) + args = { attr: args }; this.setArgs(args); } @@ -12387,7 +12404,8 @@ class TAttTextHandler { * @param {object} attr - attributes, see {@link TAttTextHandler#setArgs} */ constructor(args) { this.used = true; - if (args._typename && (args.fTextFont !== undefined)) args = { attr: args }; + if (args._typename && (args.fTextFont !== undefined)) + args = { attr: args }; this.setArgs(args); } @@ -13552,7 +13570,8 @@ class ObjectPainter extends BasePainter { txt = arg.txt_node; delete arg.txt_node; is_txt = true; - if (optimize_arr !== null) optimize_arr.push(txt); + if (optimize_arr !== null) + optimize_arr.push(txt); } else if (arg.txt_g) { txt = arg.txt_g; delete arg.txt_g; @@ -73803,11 +73822,14 @@ function getRender3DKind(render3d, is_batch) { if (is_batch === undefined) is_batch = isBatchMode(); - if (!render3d) render3d = is_batch ? settings.Render3DBatch : settings.Render3D; + if (!render3d) + render3d = is_batch ? settings.Render3DBatch : settings.Render3D; const rc = constants$1.Render3D; - if (render3d === rc.Default) render3d = is_batch ? rc.WebGLImage : rc.WebGL; - if (is_batch && (render3d === rc.WebGL)) render3d = rc.WebGLImage; + if (render3d === rc.Default) + render3d = is_batch ? rc.WebGLImage : rc.WebGL; + if (is_batch && (render3d === rc.WebGL)) + render3d = rc.WebGLImage; return render3d; } @@ -73959,7 +73981,7 @@ const Handling3DDrawings = { // case when 3D object drawn without canvas const main = this.selectDom().node(); - if (main !== null) { + if (main) { main.appendChild(canv); canv.painter = this; canv.$jsroot = '3d'; // mark canvas as added by jsroot @@ -74092,11 +74114,14 @@ async function createRender3D(width, height, render3d, args) { render3d = getRender3DKind(render3d); - if (!args) args = { antialias: true, alpha: true }; + if (!args) + args = { antialias: true, alpha: true }; let promise; - if (render3d === rc.SVG) { + if (render3d === rc.None) + promise = Promise.resolve(null); + else if (render3d === rc.SVG) { // SVG rendering const r = createSVGRenderer(false, 0); r.jsroot_dom = doc.createElementNS(nsSVG, 'svg'); @@ -74139,6 +74164,9 @@ async function createRender3D(width, height, render3d, args) { } return promise.then(renderer => { + if (!renderer) + return renderer; + if (!renderer.jsroot_dom) renderer.jsroot_dom = renderer.domElement; else @@ -74309,11 +74337,11 @@ class TooltipFor3D { const rect1 = this.parent.getBoundingClientRect(), rect2 = this.canvas.getBoundingClientRect(); - if ((rect1.left !== undefined) && (rect2.left!== undefined)) - pos.l += (rect2.left-rect1.left); + if ((rect1.left !== undefined) && (rect2.left !== undefined)) + pos.l += (rect2.left - rect1.left); - if ((rect1.top !== undefined) && (rect2.top!== undefined)) - pos.u += rect2.top-rect1.top; + if ((rect1.top !== undefined) && (rect2.top !== undefined)) + pos.u += rect2.top - rect1.top; if (pos.l + this.tt.offsetWidth + 3 >= this.parent.offsetWidth) pos.l = this.parent.offsetWidth - this.tt.offsetWidth - 3; @@ -74382,7 +74410,7 @@ class TooltipFor3D { /** @summary Hide tooltip */ hide() { - if (this.tt !== null) + if (this.tt) this.parent.removeChild(this.tt); this.tt = null; @@ -77666,8 +77694,7 @@ function registerForResize(handle, delay) { if (!handle || isBatchMode() || (typeof window === 'undefined') || (typeof document === 'undefined')) return; - let myInterval = null, myDelay = delay || 300; - if (myDelay < 20) myDelay = 20; + let myInterval = null; function ResizeTimer() { myInterval = null; @@ -77691,8 +77718,9 @@ function registerForResize(handle, delay) { } window.addEventListener('resize', () => { - if (myInterval !== null) clearTimeout(myInterval); - myInterval = setTimeout(ResizeTimer, myDelay); + if (myInterval) + clearTimeout(myInterval); + myInterval = setTimeout(ResizeTimer, Math.max(20, delay || 300)); }); } @@ -79089,8 +79117,10 @@ class JSRootMenu { let ranges = ''; if ((value === undefined) || (value === null)) value = ''; if (kind === 'int') { - if (min !== undefined) ranges += ` min="${min}"`; - if (max !== undefined) ranges += ` max="${max}"`; + if (min !== undefined) + ranges += ` min="${min}"`; + if (max !== undefined) + ranges += ` max="${max}"`; } const main_content = @@ -80134,8 +80164,7 @@ const AxisPainterMethods = { } else item.min = item.max = undefined; - - item.changed = ((item.min !== undefined) && (item.max !== undefined)); + item.changed = (item.min !== undefined) && (item.max !== undefined); return item; } @@ -81708,7 +81737,8 @@ class TooltipHandler extends ObjectPainter { nhints++; - if (hint.exact) nexact++; + if (hint.exact) + nexact++; hint.lines.forEach(line => { maxlen = Math.max(maxlen, line.length); }); @@ -87578,9 +87608,7 @@ class TPadPainter extends ObjectPainter { /** @summary indicates if painter performing objects draw * @private */ - doingDraw() { - return this.#doing_draw !== undefined; - } + doingDraw() { return this.#doing_draw !== undefined; } /** @summary confirms that drawing is completed, may trigger next drawing immediately * @private */ @@ -93908,7 +93936,7 @@ class THistPainter extends ObjectPainter { else { if (nlevels < 2) nlevels = gStyle.fNumberContours; - const pad = this.getPadPainter().getRootPad(true), + const pad = this.getPadPainter()?.getRootPad(true), logv = pad?.fLogv ?? ((ndim === 2) && pad?.fLogz); cntr.createNormal(nlevels, logv ?? 0, zminpositive); @@ -98239,15 +98267,17 @@ let TH2Painter$2 = class TH2Painter extends THistPainter { }; // class TH2Painter -function createLatexGeometry(painter, lbl, size) { +function createLatexGeometry(painter, lbl, size, as_array) { const geom_args = { font: getHelveticaFont(), size, height: 0, curveSegments: 5 }; if (THREE.REVISION > 162) geom_args.depth = 0; else geom_args.height = 0; - if (isPlainText(lbl)) - return new THREE.TextGeometry(translateLaTeX(lbl), geom_args); + if (isPlainText(lbl)) { + const res = new THREE.TextGeometry(translateLaTeX(lbl), geom_args); + return as_array ? [res] : res; + } const font_size = size * 100, geoms = []; let stroke_width = 5; @@ -98321,6 +98351,8 @@ function createLatexGeometry(painter, lbl, size) { this.x += Number.parseInt(value)*0.01; else if ((name === 'y') && (this.kind === 'text')) this.y -= Number.parseInt(value)*0.01; + else if ((name === 'fill') && (this.kind === 'text')) + this.fill = value; else if ((name === 'd') && (this.kind === 'path')) { if (get() !== 'M') return console.error('Not starts with M'); @@ -98351,6 +98383,7 @@ function createLatexGeometry(painter, lbl, size) { const pos = new Float32Array(pnts); this.geom = new THREE.BufferGeometry(); + this.geom._fill = this.fill; this.geom.setAttribute('position', new THREE.BufferAttribute(pos, 3)); this.geom.scale(0.01, -0.01, 0.01); this.geom.computeVertexNormals(); @@ -98364,6 +98397,7 @@ function createLatexGeometry(painter, lbl, size) { if (this.kind === 'text') { geom_args.size = Math.round(0.01*this.font_size); this.geom = new THREE.TextGeometry(v, geom_args); + this.geom._fill = this.fill; geoms.push(this.geom); } } @@ -98377,11 +98411,15 @@ function createLatexGeometry(painter, lbl, size) { if (!geoms.length) { geom_args.size = size; - return new THREE.TextGeometry(translateLaTeX(lbl), geom_args); + const res = new THREE.TextGeometry(translateLaTeX(lbl), geom_args); + return as_array ? [res] : res; } node.translate(); // apply translate attributes + if (as_array) + return geoms; + if (geoms.length === 1) return geoms[0]; @@ -98409,6 +98447,53 @@ function createLatexGeometry(painter, lbl, size) { return fullgeom; } +/** @summary Build three.js object for the TLatex + * @private */ +function build3dlatex(obj) { + const painter = new ObjectPainter(null, obj), + handle = painter.createAttText({ attr: obj }), + valign = handle.align % 10, + halign = handle.align - valign, + arr3d = createLatexGeometry(painter, obj.fTitle, handle.getSize() || 10, true), + bb = new THREE.Box3().makeEmpty(); + + arr3d.forEach(geom => { + geom.computeBoundingBox(); + bb.expandByPoint(geom.boundingBox.max); + bb.expandByPoint(geom.boundingBox.min); + }); + + let width = bb.max.x - bb.min.x, + height = bb.max.y - bb.min.y; + + if (halign === 1) + width = 0; + else if (halign === 2) + width *= 0.5; + + if (valign === 1) + height = 0; + else if (valign === 2) + height *= 0.5; + + const obj3d = new THREE.Object3D(), + materials = [], + getMaterial = color => { + if (!color) + color = 'black'; + if (!materials[color]) + materials[color] = new THREE.MeshBasicMaterial(getMaterialArgs(color, { vertexColors: false })); + return materials[color]; + }; + + arr3d.forEach(geom => { + geom.translate(-width, -height, 0); + obj3d.add(new THREE.Mesh(geom, getMaterial(geom._fill || handle.color))); + }); + + return arr3d.length === 1 ? obj3d.children[0] : obj3d; +} + /** @summary Text 3d axis visibility * @private */ function testAxisVisibility(camera, toplevel, fb = false, bb = false) { @@ -98568,7 +98653,7 @@ function create3DCamera(fp, orthographic) { /** @summary Returns camera default position * @private */ function getCameraDefaultPosition(fp, first_time) { - const pad = fp.getPadPainter().getRootPad(true), + 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), @@ -98740,6 +98825,12 @@ function create3DScene(render3d, x3dscale, y3dscale, orthographic) { return; } + const res = x3dscale ? this.toplevel : null; + if (res) { + this.scene?.remove(res); + this.toplevel = null; + } + testAxisVisibility(null, this.toplevel); this.clear3dCanvas(); @@ -98766,10 +98857,10 @@ function create3DScene(render3d, x3dscale, y3dscale, orthographic) { this.mode3d = false; - if (this.getG()) + if (this.getG() && !x3dscale) this.createFrameG(); - return; + return res; } this.mode3d = true; // indicate 3d mode as hist painter does @@ -98828,6 +98919,8 @@ function create3DScene(render3d, x3dscale, y3dscale, orthographic) { return createRender3D(this.scene_width, this.scene_height, render3d); }).then(r => { this.renderer = r; + if (!r) + return this; this.webgl = r.jsroot_render3d === constants$1.Render3D.WebGL; this.add3dCanvas(sz, r.jsroot_dom, this.webgl); @@ -99113,14 +99206,15 @@ function set3DOptions(hopt) { /** @summary Draw axes in 3D mode * @private */ function drawXYZ(toplevel, AxisPainter, opts) { - if (!opts) opts = { ndim: 2 }; + 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); + 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, @@ -99157,7 +99251,8 @@ function drawXYZ(toplevel, AxisPainter, opts) { 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; + if ((opts.zmult !== undefined) && !z_zoomed) + zmax *= opts.zmult; this.x_handle = new AxisPainter(null, this.xaxis); if (opts.v7) { @@ -99234,7 +99329,8 @@ function drawXYZ(toplevel, AxisPainter, opts) { lbl = this.x_handle.format(xticks.tick, 2); if (xticks.last_major()) { - if (!this.x_handle.fTitle) lbl = 'x'; + if (!this.x_handle.fTitle) + lbl = 'x'; } else if (lbl === null) { is_major = false; lbl = ''; } @@ -99502,7 +99598,8 @@ function drawXYZ(toplevel, AxisPainter, opts) { lbl = this.y_handle.format(yticks.tick, 2); if (yticks.last_major()) { - if (!this.y_handle.fTitle) lbl = 'y'; + if (!this.y_handle.fTitle) + lbl = 'y'; } else if (lbl === null) { is_major = false; lbl = ''; } @@ -99646,11 +99743,15 @@ function drawXYZ(toplevel, AxisPainter, opts) { let is_major = (zticks.kind === 1), lbl = this.z_handle.format(zticks.tick, 2); - if (lbl === null) { is_major = false; lbl = ''; } + 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; + if (mod?.fLabText) + lbl = mod.fLabText; const text3d = createLatexGeometry(this, lbl, this.z_handle.labelsFont.size); text3d.computeBoundingBox(); @@ -99658,7 +99759,8 @@ function drawXYZ(toplevel, AxisPainter, opts) { 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); + if (mod?.fTextColor) + text3d.color = this.getColor(mod.fTextColor); text3d.grz = grz; lbls.push(text3d); @@ -99858,6 +99960,35 @@ function assignFrame3DMethods(fp) { Object.assign(fp, { create3DScene, add3DMesh, remove3DMeshes, getRenderer, render3D, resize3D, change3DCamera, highlightBin3D, set3DOptions, drawXYZ, convert3DtoPadNDC }); } + +/** @summary Create 3D objects in the frame + * @private */ +async function crete3DFrame(painter, AxisPainterClass, render3d = constants$1.Render3D.None) { + const fp = painter.getFramePainter(), + o = painter.getOptions(), + histo = painter.getHisto(), + ndim = painter.getDimension(); + + assignFrame3DMethods(fp); + + return fp.create3DScene(render3d, o.x3dscale, o.y3dscale, o.Ortho).then(() => { + fp.setAxesRanges(histo.fXaxis, painter.xmin, painter.xmax, histo.fYaxis, painter.ymin, painter.ymax, histo.fZaxis, painter.zmin, painter.zmax, painter); + fp.set3DOptions(o); + fp.drawXYZ(fp.toplevel, AxisPainterClass, { + ndim, + use_y_for_z: ndim === 1, + hist_painter: painter, + zmult: o.zmult ?? 1, + zoom: (render3d !== constants$1.Render3D.None) && settings.Zooming, + draw: o.Axis !== -1, + drawany: o.isCartesian(), + reverse_x: o.RevX, + reverse_y: o.RevY + }); + return fp; + }); +} + function _meshLegoToolTip(intersect) { if ((intersect.faceIndex < 0) || (intersect.faceIndex >= this.face_to_bins_index.length)) return null; @@ -99940,8 +100071,7 @@ function drawBinsLego(painter, is_v7 = false) { if ((binz1 >= zmax) || (binz2 < zmin)) return false; - if (test_cutg && - !test_cutg.IsInside(histo.fXaxis.GetBinCoord(ii + 0.5), histo.fYaxis.GetBinCoord(jj + 0.5))) + if (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(ii + 0.5), histo.fYaxis.GetBinCoord(jj + 0.5))) return false; reduced = (binz2 === zmin) || (binz1 >= binz2); @@ -99983,7 +100113,8 @@ function drawBinsLego(painter, is_v7 = false) { zmax = levels[nlevel+1]; // artificially extend last level of color palette to maximal visible value - if (palette && (nlevel === levels.length - 2) && zmax < axis_zmax) zmax = axis_zmax; + if (palette && (nlevel === levels.length - 2) && zmax < axis_zmax) + zmax = axis_zmax; const grzmin = fp.grz(zmin), grzmax = fp.grz(zmax); let numvertices = 0, num2vertices = 0; @@ -100586,6 +100717,19 @@ function drawBinsSurf3D(painter, is_v7 = false) { } } +var hist3d = /*#__PURE__*/Object.freeze({ +__proto__: null, +assignFrame3DMethods: assignFrame3DMethods, +build3dlatex: build3dlatex, +convertLegoBuf: convertLegoBuf, +createLegoGeom: createLegoGeom, +crete3DFrame: crete3DFrame, +drawBinsContour3D: drawBinsContour3D, +drawBinsError3D: drawBinsError3D, +drawBinsLego: drawBinsLego, +drawBinsSurf3D: drawBinsSurf3D +}); + /** @summary Assign `evalPar` function for TF1 object * @private */ @@ -101018,7 +101162,8 @@ let TH1Painter$2 = class TH1Painter extends THistPainter { let stat_sumw = 0, stat_sumw2 = 0, stat_sumwx = 0, stat_sumwx2 = 0, stat_sumwy = 0, stat_sumwy2 = 0, i, xx, w, xmax = null, wmax = null; - if (!isFunc(cond)) cond = null; + if (!isFunc(cond)) + cond = null; for (i = left; i < right; ++i) { xx = xaxis.GetBinCoord(i + 0.5); @@ -102168,9 +102313,9 @@ class TH1Painter extends TH1Painter$2 { const fp = this.getFramePainter(), // who makes axis drawing is_main = this.isMainPainter(), // is main histogram - histo = this.getHisto(), - o = this.getOptions(), - zmult = 1 + 2*gStyle.fHistTopMargin; + o = this.getOptions(); + + o.zmult = 1 + 2*gStyle.fHistTopMargin; let pr = Promise.resolve(true), full_draw = true; if (reason === 'resize') { @@ -102187,17 +102332,8 @@ class TH1Painter extends TH1Painter$2 { this.scanContent(reason === 'zoom'); // may be required for axis drawings - if (is_main) { - assignFrame3DMethods(fp); - pr = fp.create3DScene(o.Render3D, o.x3dscale, o.y3dscale, o.Ortho).then(() => { - fp.setAxesRanges(histo.fXaxis, this.xmin, this.xmax, histo.fYaxis, this.ymin, this.ymax, histo.fZaxis, 0, 0, this); - fp.set3DOptions(o); - fp.drawXYZ(fp.toplevel, TAxisPainter, { - ndim: 1, hist_painter: this, use_y_for_z: true, zmult, zoom: settings.Zooming, - draw: (o.Axis !== -1), drawany: o.isCartesian() - }); - }); - } + if (is_main) + pr = crete3DFrame(this, TAxisPainter, o.Render3D); if (fp.mode3d) { pr = pr.then(() => { @@ -102217,6 +102353,24 @@ class TH1Painter extends TH1Painter$2 { .then(() => this); } + /** @summary Build three.js object for the histogram */ + static async build3d(histo, opt) { + const painter = new TH1Painter(null, histo); + painter.decodeOptions(opt); + painter.scanContent(); + + painter.createHistDrawAttributes(true); + painter.options.zmult = 1 + 2*gStyle.fHistTopMargin; + + const fp = new TFramePainter(null, null); + painter.getFramePainter = () => fp; + + return crete3DFrame(painter, TAxisPainter) + .then(() => drawBinsLego(painter)) + .then(() => fp.create3DScene(-1, true)); + } + + /** @summary draw TH1 object */ static async draw(dom, histo, opt) { return THistPainter._drawHist(new TH1Painter(dom, histo), opt); @@ -102427,13 +102581,53 @@ function drawTH2PolyLego(painter) { * @private */ class TH2Painter extends TH2Painter$2 { + /** @summary Check range for 3D + * @private */ + checkRangeFor3D(o) { + const pad = this.getPadPainter()?.getRootPad(true), + logz = pad?.fLogv ?? pad?.fLogz; + let zmult = 1; + + if (o.ohmin && o.ohmax) { + this.zmin = o.hmin; + this.zmax = o.hmax; + } else if (o.minimum !== kNoZoom && o.maximum !== kNoZoom) { + this.zmin = o.minimum; + this.zmax = o.maximum; + } else if (this.draw_content || this.gmaxbin) { + this.zmin = logz ? this.gminposbin * 0.3 : this.gminbin; + this.zmax = this.gmaxbin; + zmult = 1 + 2*gStyle.fHistTopMargin; + } + + if (logz && (this.zmin <= 0)) + this.zmin = this.zmax * 1e-5; + + this.createHistDrawAttributes(true); + return zmult; + } + + /** @summary Create 3D object for histogram bins + * @private */ + draw3DBins(o) { + if (this.isTH2Poly()) + drawTH2PolyLego(this); + else if (o.Contour) + drawBinsContour3D(this, true); + else if (o.Surf) + drawBinsSurf3D(this); + else if (o.Error) + drawBinsError3D(this); + else + drawBinsLego(this); + } + /** @summary draw TH2 object in 3D mode */ async draw3D(reason) { this.mode3d = true; const fp = this.getFramePainter(), // who makes axis drawing is_main = this.isMainPainter(), // is main histogram - histo = this.getHisto(), o = this.getOptions(); let pr = Promise.resolve(true), full_draw = true; @@ -102448,54 +102642,16 @@ class TH2Painter extends TH2Painter$2 { } if (full_draw) { - const pad = this.getPadPainter().getRootPad(true), - logz = pad?.fLogv ?? pad?.fLogz; - let zmult = 1; - - if (o.ohmin && o.ohmax) { - this.zmin = o.hmin; - this.zmax = o.hmax; - } else if (o.minimum !== kNoZoom && o.maximum !== kNoZoom) { - this.zmin = o.minimum; - this.zmax = o.maximum; - } else if (this.draw_content || this.gmaxbin) { - this.zmin = logz ? this.gminposbin * 0.3 : this.gminbin; - this.zmax = this.gmaxbin; - zmult = 1 + 2*gStyle.fHistTopMargin; - } - - if (logz && (this.zmin <= 0)) - this.zmin = this.zmax * 1e-5; - - this.createHistDrawAttributes(true); + o.zmult = this.checkRangeFor3D(o); - if (is_main) { - assignFrame3DMethods(fp); - pr = fp.create3DScene(o.Render3D, o.x3dscale, o.y3dscale, o.Ortho).then(() => { - fp.setAxesRanges(histo.fXaxis, this.xmin, this.xmax, histo.fYaxis, this.ymin, this.ymax, histo.fZaxis, this.zmin, this.zmax, this); - fp.set3DOptions(o); - fp.drawXYZ(fp.toplevel, TAxisPainter, { - ndim: 2, hist_painter: this, zmult, zoom: settings.Zooming, - draw: o.Axis !== -1, drawany: o.isCartesian(), - reverse_x: o.RevX, reverse_y: o.RevY - }); - }); - } + if (is_main) + pr = crete3DFrame(this, TAxisPainter, o.Render3D); if (fp.mode3d) { pr = pr.then(() => { - if (this.draw_content) { - if (this.isTH2Poly()) - drawTH2PolyLego(this); - else if (o.Contour) - drawBinsContour3D(this, true); - else if (o.Surf) - drawBinsSurf3D(this); - else if (o.Error) - drawBinsError3D(this); - else - drawBinsLego(this); - } else if (o.Axis && o.Zscale) { + if (this.draw_content) + this.draw3DBins(o); + else if (o.Axis && o.Zscale) { this.getContourLevels(true); this.getHistPalette(); } @@ -102517,6 +102673,30 @@ class TH2Painter extends TH2Painter$2 { .then(() => this); } + /** @summary Build three.js object for the histogram */ + static async build3d(histo, opt, get_painter) { + const painter = new TH2Painter(null, histo); + painter.decodeOptions(opt); + + const o = painter.getOptions(); + if (painter.isTH2Poly()) + o.Lego = 12; + painter.scanContent(); + + o.zmult = painter.checkRangeFor3D(o); + + const fp = new TFramePainter(null, null); + // return dummy frame painter as result + painter.getFramePainter = () => fp; + + return crete3DFrame(painter, TAxisPainter).then(() => { + if (painter.draw_content) + painter.draw3DBins(o); + + return get_painter ? painter : fp.create3DScene(-1, true); + }); + } + /** @summary draw TH2 object */ static async draw(dom, histo, opt) { return THistPainter._drawHist(new TH2Painter(dom, histo), opt); @@ -103155,7 +103335,6 @@ class TH3Painter extends THistPainter { /** @summary Redraw TH3 histogram */ async redraw(reason) { const fp = this.getFramePainter(), // who makes axis and 3D drawing - histo = this.getHisto(), o = this.getOptions(); let pr = Promise.resolve(true), full_draw = true; @@ -103170,20 +103349,13 @@ class TH3Painter extends THistPainter { } if (full_draw) { - assignFrame3DMethods(fp); - pr = fp.create3DScene(o.Render3D, o.x3dscale, o.y3dscale, o.Ortho).then(() => { - fp.setAxesRanges(histo.fXaxis, this.xmin, this.xmax, histo.fYaxis, this.ymin, this.ymax, histo.fZaxis, this.zmin, this.zmax, this); - fp.set3DOptions(o); - fp.drawXYZ(fp.toplevel, TAxisPainter, { - ndim: 3, hist_painter: this, zoom: settings.Zooming, - draw: o.Axis !== -1, drawany: o.isCartesian() - }); - return this.draw3DBins(); - }).then(() => { - fp.render3D(); - this.updateStatWebCanvas(); - fp.addKeysHandler(); - }); + pr = crete3DFrame(this, TAxisPainter, o.Render3D) + .then(() => this.draw3DBins()) + .then(() => { + fp.render3D(); + this.updateStatWebCanvas(); + fp.addKeysHandler(); + }); } if (this.isMainPainter()) @@ -103298,6 +103470,21 @@ class TH3Painter extends THistPainter { }); } + /** @summary Build three.js object for the histogram */ + static async build3d(histo, opt) { + const painter = new TH3Painter(null, histo); + painter.mode3d = true; + painter.decodeOptions(opt); + painter.scanContent(); + + const fp = new TFramePainter(null, null); + painter.getFramePainter = () => fp; + + return crete3DFrame(painter, TAxisPainter) + .then(() => painter.draw3DBins()) + .then(() => fp.create3DScene(-1, true)); + } + /** @summary draw TH3 object */ static async draw(dom, histo, opt) { const painter = new TH3Painter(dom, histo); @@ -105174,11 +105361,9 @@ class RTreeMapTooltip { if (isLeaf && node.fType !== undefined) content += `Type: ${node.fType}
`; - if (!isLeaf) content += `Children: ${node.fNChildren}
`; - const obj = this.painter.getObject(); if (obj.fNodes && obj.fNodes.length > 0) { const totalSize = obj.fNodes[0].fSize, @@ -105917,7 +106102,8 @@ class Node { this.divider.splitPolygon(polygons[i], this.polygons, this.polygons, front, back); } - if (nodeid !== undefined) this.maxnodeid = nodeid; + if (nodeid !== undefined) + this.maxnodeid = nodeid; if (front.length) this.front = new Node(front); @@ -106176,7 +106362,8 @@ class Geometry { } this.tree = new Node(polygons, nodeid); - if (nodeid !== undefined) this.maxid = this.tree.maxnodeid; + if (nodeid !== undefined) + this.maxid = this.tree.maxnodeid; } subtract(other_tree) { @@ -108425,8 +108612,7 @@ createGeometry = function(shape, limit = 0) { let place = ''; if (e.stack !== undefined) { place = e.stack.split('\n')[0]; - if (place.indexOf(e.message) >= 0) place = e.stack.split('\n')[1]; - else place = 'at: ' + place; + place = place.indexOf(e.message) >= 0 ? e.stack.split('\n')[1] : 'at: ' + place; } geoWarn(`${shape._typename} err: ${e.message} ${place}`); } @@ -113837,9 +114023,8 @@ class TGeoPainter extends ObjectPainter { if (this.#superimpose && (opt.indexOf('same') === 0)) opt = opt.slice(4); - const res = this.ctrl, + const res = this.ctrl, macro = opt.indexOf('macro:'); - macro = opt.indexOf('macro:'); if (macro >= 0) { let separ = opt.indexOf(';', macro+6); if (separ < 0) separ = opt.length; @@ -114893,17 +115078,19 @@ class TGeoPainter extends ObjectPainter { const obj = intersects[n].object; let unique = obj.visible && (getIntersectStack(intersects[n]) || (obj.geo_name !== undefined)); - if (unique && obj.material && (obj.material.opacity !== undefined)) - unique = (obj.material.opacity >= 0.1); + if (unique && (obj.material?.opacity !== undefined)) + unique = obj.material.opacity >= 0.1; - if (obj.jsroot_special) unique = false; + if (obj.jsroot_special) + unique = false; for (let k = 0; (k < n) && unique; ++k) { if (intersects[k].object === obj) unique = false; } - if (!unique) intersects.splice(n, 1); + if (!unique) + intersects.splice(n, 1); } const clip = this.ctrl.clip; @@ -118394,6 +118581,153 @@ class TGeoPainter extends ObjectPainter { return this.prepareObjectDraw(draw_obj, name_prefix); } + /** @summary Build three.js object */ + static build3d(obj, sopt) { + if (!obj) + return null; + + let opt = sopt || {}; + if (isStr(sopt)) { + const painter = new TGeoPainter(null, obj); + painter.decodeOptions(sopt); + opt = painter.ctrl; + opt.numfaces = opt.maxfaces; + opt.numnodes = opt.maxnodes; + } + + if (!opt.numfaces) + opt.numfaces = 100000; + if (!opt.numnodes) + opt.numnodes = 1000; + if (!opt.frustum) + opt.frustum = null; + + opt.res_mesh = opt.res_faces = 0; + + if (opt.instancing === undefined) + opt.instancing = -1; + + opt.info = { num_meshes: 0, num_faces: 0 }; + + let clones, visibles; + + if (obj.visibles && obj.nodes && obj.numnodes) { + // case of draw message from geometry viewer + + const nodes = obj.numnodes > 1e6 ? { length: obj.numnodes } : new Array(obj.numnodes); + + obj.nodes.forEach(node => { + nodes[node.id] = ClonedNodes.formatServerElement(node); + }); + + clones = new ClonedNodes(null, nodes); + clones.name_prefix = clones.getNodeName(0); + + // normally only need when making selection, not used in geo viewer + // this.geo_clones.setMaxVisNodes(draw_msg.maxvisnodes); + // this.geo_clones.setVisLevel(draw_msg.vislevel); + // TODO: provide from server + clones.maxdepth = 20; + + const nsegm = obj.cfg?.nsegm || 30; + + for (let cnt = 0; cnt < obj.visibles.length; ++cnt) { + const item = obj.visibles[cnt], rd = item.ri; + + // entry may be provided without shape - it is ok + if (rd) + item.server_shape = rd.server_shape = createServerGeometry(rd, nsegm); + } + + visibles = obj.visibles; + } else { + let shape = null, hide_top = false; + + if (('fShapeBits' in obj) && ('fShapeId' in obj)) { + shape = obj; obj = null; + } else if ((obj._typename === clTGeoVolumeAssembly) || (obj._typename === clTGeoVolume)) + shape = obj.fShape; + else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) + shape = obj.fShape; + else if (obj._typename === clTGeoManager) { + obj = obj.fMasterVolume; + hide_top = !opt.showtop; + shape = obj.fShape; + } else if (obj.fVolume) + shape = obj.fVolume.fShape; + else + obj = null; + + if (opt.composite && shape && (shape._typename === clTGeoCompositeShape) && shape.fNode) + obj = buildCompositeVolume(shape); + + if (!obj && shape) + obj = Object.assign(create$1(clTNamed), { _typename: clTEveGeoShapeExtract, fTrans: null, fShape: shape, fRGBA: [0, 1, 0, 1], fElements: null, fRnrSelf: true }); + + if (!obj) + return null; + + if (obj._typename.indexOf(clTGeoVolume) === 0) + obj = { _typename: clTGeoNode, fVolume: obj, fName: obj.fName, $geoh: obj.$geoh, _proxy: true }; + + clones = new ClonedNodes(obj); + clones.setVisLevel(opt.vislevel); + clones.setMaxVisNodes(opt.numnodes); + + if (opt.dflt_colors) + clones.setDefaultColors(true); + + const uniquevis = opt.no_screen ? 0 : clones.markVisibles(true); + if (uniquevis <= 0) + clones.markVisibles(false, false, hide_top); + else + clones.markVisibles(true, true, hide_top); // copy bits once and use normal visibility bits + + clones.produceIdShifts(); + + // collect visible nodes + const res = clones.collectVisibles(opt.numfaces, opt.frustum); + + visibles = res.lst; + } + + if (!opt.material_kind) + opt.material_kind = 'lambert'; + if (opt.set_names === undefined) + opt.set_names = true; + + clones.setConfig(opt); + + // collect shapes + const shapes = clones.collectShapes(visibles); + + clones.buildShapes(shapes, opt.numfaces); + + const toplevel = new THREE.Object3D(); + toplevel.clones = clones; // keep reference on JSROOT data + + const colors = getRootColors(); + + if (clones.createInstancedMeshes(opt, toplevel, visibles, shapes, colors)) + return toplevel; + + for (let n = 0; n < visibles.length; ++n) { + const entry = visibles[n]; + if (entry.done) + continue; + + const shape = entry.server_shape || shapes[entry.shapeid]; + if (!shape.ready) { + console.warn('shape marked as not ready when it should'); + break; + } + + clones.createEntryMesh(opt, toplevel, entry, shape, colors); + } + + return toplevel; + } + /** @summary draw TGeo object */ static async draw(dom, obj, opt) { if (!obj) @@ -118943,143 +119277,7 @@ function drawAxis3D() { * // this is three.js object and can be now inserted in the scene */ function build(obj, opt) { - if (!obj) - return null; - - if (!opt) - opt = {}; - if (!opt.numfaces) - opt.numfaces = 100000; - if (!opt.numnodes) - opt.numnodes = 1000; - if (!opt.frustum) - opt.frustum = null; - - opt.res_mesh = opt.res_faces = 0; - - if (opt.instancing === undefined) - opt.instancing = -1; - - opt.info = { num_meshes: 0, num_faces: 0 }; - - let clones, visibles; - - if (obj.visibles && obj.nodes && obj.numnodes) { - // case of draw message from geometry viewer - - const nodes = obj.numnodes > 1e6 ? { length: obj.numnodes } : new Array(obj.numnodes); - - obj.nodes.forEach(node => { - nodes[node.id] = ClonedNodes.formatServerElement(node); - }); - - clones = new ClonedNodes(null, nodes); - clones.name_prefix = clones.getNodeName(0); - - // normally only need when making selection, not used in geo viewer - // this.geo_clones.setMaxVisNodes(draw_msg.maxvisnodes); - // this.geo_clones.setVisLevel(draw_msg.vislevel); - // TODO: provide from server - clones.maxdepth = 20; - - const nsegm = obj.cfg?.nsegm || 30; - - for (let cnt = 0; cnt < obj.visibles.length; ++cnt) { - const item = obj.visibles[cnt], rd = item.ri; - - // entry may be provided without shape - it is ok - if (rd) - item.server_shape = rd.server_shape = createServerGeometry(rd, nsegm); - } - - visibles = obj.visibles; - } else { - let shape = null, hide_top = false; - - if (('fShapeBits' in obj) && ('fShapeId' in obj)) { - shape = obj; obj = null; - } else if ((obj._typename === clTGeoVolumeAssembly) || (obj._typename === clTGeoVolume)) - shape = obj.fShape; - else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) - shape = obj.fShape; - else if (obj._typename === clTGeoManager) { - obj = obj.fMasterVolume; - hide_top = !opt.showtop; - shape = obj.fShape; - } else if (obj.fVolume) - shape = obj.fVolume.fShape; - else - obj = null; - - - if (opt.composite && shape && (shape._typename === clTGeoCompositeShape) && shape.fNode) - obj = buildCompositeVolume(shape); - - if (!obj && shape) - obj = Object.assign(create$1(clTNamed), { _typename: clTEveGeoShapeExtract, fTrans: null, fShape: shape, fRGBA: [0, 1, 0, 1], fElements: null, fRnrSelf: true }); - - if (!obj) - return null; - - if (obj._typename.indexOf(clTGeoVolume) === 0) - obj = { _typename: clTGeoNode, fVolume: obj, fName: obj.fName, $geoh: obj.$geoh, _proxy: true }; - - clones = new ClonedNodes(obj); - clones.setVisLevel(opt.vislevel); - clones.setMaxVisNodes(opt.numnodes); - - if (opt.dflt_colors) - clones.setDefaultColors(true); - - const uniquevis = opt.no_screen ? 0 : clones.markVisibles(true); - if (uniquevis <= 0) - clones.markVisibles(false, false, hide_top); - else - clones.markVisibles(true, true, hide_top); // copy bits once and use normal visibility bits - - clones.produceIdShifts(); - - // collect visible nodes - const res = clones.collectVisibles(opt.numfaces, opt.frustum); - - visibles = res.lst; - } - - if (!opt.material_kind) - opt.material_kind = 'lambert'; - if (opt.set_names === undefined) - opt.set_names = true; - - clones.setConfig(opt); - - // collect shapes - const shapes = clones.collectShapes(visibles); - - clones.buildShapes(shapes, opt.numfaces); - - const toplevel = new THREE.Object3D(); - toplevel.clones = clones; // keep reference on JSROOT data - - const colors = getRootColors(); - - if (clones.createInstancedMeshes(opt, toplevel, visibles, shapes, colors)) - return toplevel; - - for (let n = 0; n < visibles.length; ++n) { - const entry = visibles[n]; - if (entry.done) - continue; - - const shape = entry.server_shape || shapes[entry.shapeid]; - if (!shape.ready) { - console.warn('shape marked as not ready when it should'); - break; - } - - clones.createEntryMesh(opt, toplevel, entry, shape, colors); - } - - return toplevel; + return TGeoPainter.build3d(obj, opt); } var TGeoPainter$1 = /*#__PURE__*/Object.freeze({ @@ -124003,7 +124201,7 @@ class TDrawSelector extends TSelector { break; case 'mon': case 'monitor': - args.monitoring = (intvalue !== undefined) ? intvalue : 5000; + args.monitoring = intvalue ?? 5000; break; case 'player': args.player = true; @@ -164432,7 +164630,7 @@ drawFuncs = { lst: [ { name: clTDiamond, sameas: clTPave }, { name: clTLegend, icon: 'img_pavelabel', sameas: clTPave }, { name: clTPaletteAxis, icon: 'img_colz', sameas: clTPave }, - { name: clTLatex, icon: 'img_text', draw: () => import_more().then(h => h.drawText), direct: true }, + { name: clTLatex, icon: 'img_text', draw: () => import_more().then(h => h.drawText), build3d: () => Promise.resolve().then(function () { return hist3d; }).then(h => h.build3dlatex), direct: true }, { name: clTMathText, sameas: clTLatex }, { name: clTText, sameas: clTLatex }, { name: clTLink, sameas: clTText }, @@ -164930,6 +165128,30 @@ async function redraw(dom, obj, opt) { return draw(dom, obj, opt); } +/** @summary Create three.js model for object + * @param {object} obj - object + * @param {string} opt - draw options + * @return {Promise} with three.js model */ + +async function build3d(obj, opt) { + if (!isObject(obj) || !obj?._typename) + return Promise.reject(Error('not an object in build3d')); + + const handle = getDrawHandle(getKindForType(obj._typename)); + if (!handle?.class && !handle.build3d) + return Promise.reject(Error(`not able to create three.js for ${obj._typename}`)); + + if (handle.build3d) + return handle.build3d().then(func => func(obj, opt)); + + return handle.class().then(cl => { + if (!isFunc(cl?.build3d)) + return Promise.reject(Error(`painter class for ${obj._typename} does not implement build3d method`)); + + return cl.build3d(obj, opt); + }); +} + /** @summary Scan streamer infos for derived classes * @desc Assign draw functions for such derived classes * @private */ @@ -168734,7 +168956,10 @@ class HierarchyPainter extends BasePainter { opt = (separ > 0) ? opt.slice(separ+1) : ''; let canarray = true; - if (part[0] === '#') { part = part.slice(1); canarray = false; } + if (part[0] === '#') { + part = part.slice(1); + canarray = false; + } const val = d.get(part, null); @@ -172583,7 +172808,8 @@ class TGraph2DPainter extends ObjectPainter { res.Axis = ''; else if (res.isAny()) { res.Axis = 'lego2'; - if (res.Zscale) res.Axis += 'z'; + if (res.Zscale) + res.Axis += 'z'; } else res.Axis = opt; @@ -172876,7 +173102,10 @@ class TGraph2DPainter extends ObjectPainter { if (fp.usesvg) scale *= 0.3; - scale *= 7 * Math.max(fp.size_x3d / fp.getFrameWidth(), fp.size_z3d / fp.getFrameHeight()); + const fw = fp.getFrameWidth(), fh = fp.getFrameHeight(); + + if ((fw > 10) && (fh > 10)) + scale *= 7 * Math.max(fp.size_x3d / fw, fp.size_z3d / fh); if (o.Color || (o.Triangles >= 10)) { levels = main.getContourLevels(true); @@ -173038,6 +173267,27 @@ class TGraph2DPainter extends ObjectPainter { }); } + /** @summary Build three.js of TGraph2D object */ + static async build3d(gr, opt) { + const painter = new TGraph2DPainter(null, gr); + painter.decodeOptions(opt, gr); + + if (painter.options.Contour) { + console.error('Contour plot is not 3d'); + return null; + } + + return TH2Painter.build3d(painter.createHistogram(), painter.options.Axis, true).then(hist_painter => { + painter.axes_draw = true; + const fp = hist_painter.getFramePainter(); + + painter.getFramePainter = () => fp; + painter.getMainPainter = () => hist_painter; + + return painter.drawGraph2D().then(() => fp.create3DScene(-1, true)); + }); + } + /** @summary draw TGraph2D object */ static async draw(dom, gr, opt) { const painter = new TGraph2DPainter(dom, gr); @@ -176420,7 +176670,8 @@ class TSplinePainter extends ObjectPainter { } const name = this.getObjectHint(); - if (name) res.lines.push(name); + if (name) + res.lines.push(name); res.lines.push(`x = ${funcs.axisAsText('x', xx)}`, `y = ${funcs.axisAsText('y', yy)}`); if (knot !== null) { @@ -179274,7 +179525,8 @@ class RAxisPainter extends RObjectPainter { let toffset = this.v7EvalAttr('timeOffset'); if (toffset !== undefined) { toffset = parseFloat(toffset); - if (Number.isFinite(toffset)) this.timeoffset = toffset*1000; + if (Number.isFinite(toffset)) + this.timeoffset = toffset*1000; } } else if (this.axis?.fLabelsIndex) { this.kind = kAxisLabels; @@ -179803,9 +180055,11 @@ class RAxisPainter extends RObjectPainter { if (this.handle.kind === 1) { // if not showing labels, not show large tick - if ((this.kind === kAxisLabels) || (this.format(this.handle.tick, true) !== null)) h1 = this.ticksSize; + if ((this.kind === kAxisLabels) || (this.format(this.handle.tick, true) !== null)) + h1 = this.ticksSize; - if (main_draw) this.ticks.push(grpos); // keep graphical positions of major ticks + if (main_draw) + this.ticks.push(grpos); // keep graphical positions of major ticks } if (ticks_plusminus > 0) @@ -183621,7 +183875,10 @@ class RCanvasPainter extends RPadPainter { return handle !== undefined; if (tm === 'reset') { - if (handle) { clearTimeout(handle); delete this.#websocket._tmouts[name]; } + if (handle) { + clearTimeout(handle); + delete this.#websocket._tmouts[name]; + } } else if (!handle && Number.isInteger(tm)) this.#websocket._tmouts[name] = setTimeout(() => { delete this.#websocket._tmouts[name]; }, tm); } @@ -185020,6 +185277,7 @@ exports.assignContextMenu = assignContextMenu; exports.atob_func = atob_func; exports.browser = browser; exports.btoa_func = btoa_func; +exports.build3d = build3d; exports.buildGUI = buildGUI; exports.buildSvgCurve = buildSvgCurve; exports.clTAnnotation = clTAnnotation; diff --git a/changes.md b/changes.md index 88f1f8e1f..4ddc81792 100644 --- a/changes.md +++ b/changes.md @@ -3,6 +3,7 @@ ## Changes in dev 1. RNtuple support, thanks to Kriti Mahajan (https://github.com/Krmjn09) 1. Implement RTreeMapPainter to display RNTuple structure, thanks to Patryk Pilichowski (https://github.com/magnustymoteus) +1. Implement `build3d` function for supported classes #368 1. Let use hex colors in histogram draw options like "fill_00ff00" or "line_77aa1166" 1. Let configure exact axis ticks position via draw option like "xticks:[-3,-1,1,3]" 1. Support gStyle.fBarOffset for `TGraph` bar drawing diff --git a/modules/core.mjs b/modules/core.mjs index e24e2bac7..449e1ff20 100644 --- a/modules/core.mjs +++ b/modules/core.mjs @@ -4,7 +4,7 @@ const version_id = 'dev', /** @summary version date * @desc Release date in format day/month/year like '14/04/2022' */ -version_date = '25/09/2025', +version_date = '29/09/2025', /** @summary version id and date * @desc Produced by concatenation of {@link version_id} and {@link version_date} diff --git a/modules/draw.mjs b/modules/draw.mjs index 469b87757..a7deef94f 100644 --- a/modules/draw.mjs +++ b/modules/draw.mjs @@ -562,7 +562,7 @@ async function build3d(obj, opt) { return Promise.reject(Error(`not able to create three.js for ${obj._typename}`)); if (handle.build3d) - return handle.build3d().then(func => func(obj, opt)) + return handle.build3d().then(func => func(obj, opt)); return handle.class().then(cl => { if (!isFunc(cl?.build3d)) @@ -780,4 +780,4 @@ Object.assign(internals, { addStreamerInfosForPainter, addDrawFunc, setDefaultDr Object.assign(internals.jsroot, { draw, redraw, makeSVG, makeImage, addDrawFunc }); export { addDrawFunc, getDrawHandle, canDrawHandle, getDrawSettings, setDefaultDrawOpt, - draw, redraw,cleanup, build3d, makeSVG, makeImage, assignPadPainterDraw }; + draw, redraw, cleanup, build3d, makeSVG, makeImage, assignPadPainterDraw }; diff --git a/modules/geom/TGeoPainter.mjs b/modules/geom/TGeoPainter.mjs index b4bce0b98..e4bf1cf22 100644 --- a/modules/geom/TGeoPainter.mjs +++ b/modules/geom/TGeoPainter.mjs @@ -5481,15 +5481,14 @@ class TGeoPainter extends ObjectPainter { if (!obj) return null; - let opt = null; + let opt = sopt || {}; if (isStr(sopt)) { const painter = new TGeoPainter(null, obj); painter.decodeOptions(sopt); opt = painter.ctrl; opt.numfaces = opt.maxfaces; opt.numnodes = opt.maxnodes; - } else - opt = sopt || {}; + } if (!opt.numfaces) opt.numfaces = 100000; diff --git a/modules/hist/TGraph2DPainter.mjs b/modules/hist/TGraph2DPainter.mjs index 3f7c66e5f..aad395274 100644 --- a/modules/hist/TGraph2DPainter.mjs +++ b/modules/hist/TGraph2DPainter.mjs @@ -1124,9 +1124,8 @@ class TGraph2DPainter extends ObjectPainter { res.Axis = 'lego2'; if (res.Zscale) res.Axis += 'z'; - } else { + } else res.Axis = opt; - } this.storeDrawOpt(opt); } @@ -1599,8 +1598,8 @@ class TGraph2DPainter extends ObjectPainter { painter.getFramePainter = () => fp; painter.getMainPainter = () => hist_painter; - return painter.drawGraph2D().then(() => fp.create3DScene(-1, true)) - }) + return painter.drawGraph2D().then(() => fp.create3DScene(-1, true)); + }); } /** @summary draw TGraph2D object */ diff --git a/modules/hist/TH1Painter.mjs b/modules/hist/TH1Painter.mjs index 0a59329e0..0ff29d36f 100644 --- a/modules/hist/TH1Painter.mjs +++ b/modules/hist/TH1Painter.mjs @@ -17,7 +17,6 @@ class TH1Painter extends TH1Painter2D { const fp = this.getFramePainter(), // who makes axis drawing is_main = this.isMainPainter(), // is main histogram - histo = this.getHisto(), o = this.getOptions(); o.zmult = 1 + 2*gStyle.fHistTopMargin; diff --git a/modules/hist/TH2Painter.mjs b/modules/hist/TH2Painter.mjs index 2a8ec4d6c..fbcc3ab51 100644 --- a/modules/hist/TH2Painter.mjs +++ b/modules/hist/TH2Painter.mjs @@ -1,4 +1,4 @@ -import { settings, constants, gStyle, clTMultiGraph, kNoZoom } from '../core.mjs'; +import { gStyle, clTMultiGraph, kNoZoom } from '../core.mjs'; import { getMaterialArgs, THREE } from '../base/base3d.mjs'; import { crete3DFrame, drawBinsLego, drawBinsError3D, drawBinsContour3D, drawBinsSurf3D } from './hist3d.mjs'; import { TAxisPainter } from '../gpad/TAxisPainter.mjs'; @@ -244,7 +244,6 @@ class TH2Painter extends TH2Painter2D { drawBinsError3D(this); else drawBinsLego(this); - } /** @summary draw TH2 object in 3D mode */ @@ -253,7 +252,6 @@ class TH2Painter extends TH2Painter2D { const fp = this.getFramePainter(), // who makes axis drawing is_main = this.isMainPainter(), // is main histogram - histo = this.getHisto(), o = this.getOptions(); let pr = Promise.resolve(true), full_draw = true; @@ -304,7 +302,7 @@ class TH2Painter extends TH2Painter2D { const painter = new TH2Painter(null, histo); painter.decodeOptions(opt); - const o = painter.getOptions(), logz = false; + const o = painter.getOptions(); if (painter.isTH2Poly()) o.Lego = 12; painter.scanContent(); diff --git a/modules/hist/TH3Painter.mjs b/modules/hist/TH3Painter.mjs index d13b1a619..abeb33c9b 100644 --- a/modules/hist/TH3Painter.mjs +++ b/modules/hist/TH3Painter.mjs @@ -1,4 +1,4 @@ -import { gStyle, settings, kInspect, clTF1, clTF3, clTProfile3D, BIT, isFunc } from '../core.mjs'; +import { gStyle, kInspect, clTF1, clTF3, clTProfile3D, BIT, isFunc } from '../core.mjs'; import { TRandom, floatToString } from '../base/BasePainter.mjs'; import { ensureTCanvas } from '../gpad/TCanvasPainter.mjs'; import { TAxisPainter } from '../gpad/TAxisPainter.mjs'; @@ -636,7 +636,6 @@ class TH3Painter extends THistPainter { /** @summary Redraw TH3 histogram */ async redraw(reason) { const fp = this.getFramePainter(), // who makes axis and 3D drawing - histo = this.getHisto(), o = this.getOptions(); let pr = Promise.resolve(true), full_draw = true; diff --git a/modules/hist/hist3d.mjs b/modules/hist/hist3d.mjs index 0014d13ee..e5d0e1329 100644 --- a/modules/hist/hist3d.mjs +++ b/modules/hist/hist3d.mjs @@ -204,7 +204,7 @@ function build3dlatex(obj) { geom.computeBoundingBox(); bb.expandByPoint(geom.boundingBox.max); bb.expandByPoint(geom.boundingBox.min); - }) + }); let width = bb.max.x - bb.min.x, height = bb.max.y - bb.min.y; @@ -219,7 +219,8 @@ function build3dlatex(obj) { else if (valign === 2) height *= 0.5; - const materials = [], + const obj3d = new THREE.Object3D(), + materials = [], getMaterial = color => { if (!color) color = 'black'; @@ -228,11 +229,6 @@ function build3dlatex(obj) { return materials[color]; }; - - const material0 = new THREE.MeshBasicMaterial(getMaterialArgs(handle.color || 'black', { vertexColors: false })); - - const obj3d = new THREE.Object3D(); - arr3d.forEach(geom => { geom.translate(-width, -height, 0); obj3d.add(new THREE.Mesh(geom, getMaterial(geom._fill || handle.color)));