From 84e8f0abfc70a38f8d24a5ceb9eea1bdff227c2f Mon Sep 17 00:00:00 2001 From: Matevz Tadel Date: Fri, 16 Oct 2026 22:38:43 -0700 Subject: [PATCH 1/7] [eve7] Update RenderCore to 2.3 - Culls a subtree by a bounding sphere around an object and everything below it, which the element tree of renderer objects needs. - Group.self holds a group's own drawing, and Group.visibleChildren hides the other children. - Computes the bounds of 2D-vertex geometry correctly and drops cached bounds on new vertices. - Draws the outline only of selected objects that were drawn in that pass. - No longer contains REveCameraControls; ROOT has its own in ui5/eve7/lib since 609e7d24490. Assisted-by: Claude:Opus 5.5 --- builtins/rendercore/CMakeLists.txt | 4 ++-- builtins/rendercore/RenderCore-2.2.tar.gz | Bin 112140 -> 0 bytes builtins/rendercore/RenderCore-2.3.tar.gz | Bin 0 -> 109745 bytes 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 builtins/rendercore/RenderCore-2.2.tar.gz create mode 100644 builtins/rendercore/RenderCore-2.3.tar.gz diff --git a/builtins/rendercore/CMakeLists.txt b/builtins/rendercore/CMakeLists.txt index baa8227c0082d..01029bbb8c742 100644 --- a/builtins/rendercore/CMakeLists.txt +++ b/builtins/rendercore/CMakeLists.txt @@ -1,7 +1,7 @@ ExternalProject_Add( RENDERCORE - URL ${CMAKE_SOURCE_DIR}/builtins/rendercore/RenderCore-2.2.tar.gz - URL_HASH SHA256=79de20fb064638cfa0831893ab022df6a39d2392776061c81b0996a55b59cd11 + URL ${CMAKE_SOURCE_DIR}/builtins/rendercore/RenderCore-2.3.tar.gz + URL_HASH SHA256=1f5ff8f33e0c381e00a0472a51d6232a4bbcede1e5cfd167889e185079b57ab9 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/builtins/rendercore/RenderCore-2.2.tar.gz b/builtins/rendercore/RenderCore-2.2.tar.gz deleted file mode 100644 index 46ec9d379e37003989d4e1e2581a061769a057f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112140 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 z 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 diff --git a/builtins/rendercore/RenderCore-2.3.tar.gz b/builtins/rendercore/RenderCore-2.3.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..b8ae90cac7889e94cafc33d5db6d8ad62b03cda8 GIT binary patch literal 109745 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

Icbcv=wB3mlgF=m3wNvhZR*Ati#w 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 Date: Fri, 16 Oct 2026 22:38:31 -0700 Subject: [PATCH 2/7] [eve7] Compute bounding boxes on the server and stream them The client needs a box for every drawing to bound and cull it. Seven classes had one, through TAttBBox, in three layouts; the rest had none. - REveElement holds the box and its state, with ComputeBBox() and AssertBBox(). Removed TAttBBox from seven classes, and the unused TAtt3D from REveCaloViz and REveDigitSet. - WriteCoreJson() writes the box with the render data, as "bbox", or "bbox_not_in_world" for screen-space elements. kCBObjProps invalidates it. - Removed the boxes in render-data normals and own JSON; the client reads el.bbox. - BBoxInit() starts from +-infinity; a box that no point reached is kBBoxNone. Added BBoxCheckBox(centre, half sizes), and REveVectorT::SetMin(), SetMax(). - Fixed the boxes of REveGeoShape and REveBoxSet. The box of REveProjectionManager is the union of the projected ones. - Renamed kCBTransBBox, StampTransBBox() to kCBTrans, StampTrans(). - Added gtests. Assisted-by: Claude:Opus 5.5 --- graf3d/eve7/inc/ROOT/REveBox.hxx | 4 +- graf3d/eve7/inc/ROOT/REveCalo.hxx | 8 +- graf3d/eve7/inc/ROOT/REveDigitSet.hxx | 8 +- graf3d/eve7/inc/ROOT/REveElement.hxx | 57 ++++- graf3d/eve7/inc/ROOT/REveGeoTopNode.hxx | 1 + graf3d/eve7/inc/ROOT/REveJetCone.hxx | 2 +- graf3d/eve7/inc/ROOT/REveLogo.hxx | 1 + graf3d/eve7/inc/ROOT/REvePointSet.hxx | 8 +- graf3d/eve7/inc/ROOT/REveProjectionBases.hxx | 2 +- .../eve7/inc/ROOT/REveProjectionManager.hxx | 6 +- graf3d/eve7/inc/ROOT/REveSMorph.hxx | 4 +- graf3d/eve7/inc/ROOT/REveShape.hxx | 7 +- graf3d/eve7/inc/ROOT/REveStraightLineSet.hxx | 6 +- graf3d/eve7/inc/ROOT/REveVector.hxx | 18 ++ graf3d/eve7/src/REveBox.cxx | 4 +- graf3d/eve7/src/REveBoxSet.cxx | 33 ++- graf3d/eve7/src/REveCalo.cxx | 41 ++-- graf3d/eve7/src/REveElement.cxx | 92 +++++++- graf3d/eve7/src/REveGeoShape.cxx | 16 +- graf3d/eve7/src/REveGeoTopNode.cxx | 44 ++-- graf3d/eve7/src/REveJetCone.cxx | 4 +- graf3d/eve7/src/REveLine.cxx | 2 +- graf3d/eve7/src/REvePointSet.cxx | 38 +-- graf3d/eve7/src/REvePolygonSetProjected.cxx | 14 +- graf3d/eve7/src/REveProjectionAxis.cxx | 6 +- graf3d/eve7/src/REveProjectionBases.cxx | 15 +- graf3d/eve7/src/REveProjectionManager.cxx | 54 +++-- graf3d/eve7/src/REveSMorph.cxx | 12 +- graf3d/eve7/src/REveScene.cxx | 7 +- graf3d/eve7/src/REveStraightLineSet.cxx | 13 +- graf3d/eve7/src/REveText.cxx | 5 +- graf3d/eve7/src/REveTrack.cxx | 23 +- graf3d/eve7/src/REveTrackProjected.cxx | 2 +- graf3d/eve7/src/REveTrans.cxx | 2 +- graf3d/eve7/test/change_cycle.cxx | 2 +- graf3d/eve7/test/streaming.cxx | 220 +++++++++++++++++- ui5/eve7/controller/Summary.controller.js | 2 +- ui5/eve7/lib/EveElementsRCore.js | 29 ++- ui5/eve7/lib/EveManager.js | 6 +- ui5/eve7/lib/EveScene.js | 4 +- 40 files changed, 554 insertions(+), 268 deletions(-) diff --git a/graf3d/eve7/inc/ROOT/REveBox.hxx b/graf3d/eve7/inc/ROOT/REveBox.hxx index db4c2c53c4daa..2cc1b08126f69 100644 --- a/graf3d/eve7/inc/ROOT/REveBox.hxx +++ b/graf3d/eve7/inc/ROOT/REveBox.hxx @@ -41,7 +41,7 @@ public: const Float_t* GetVertex(Int_t i) const { return fVertices[i]; } - // For TAttBBox: + // From REveElement: void ComputeBBox() override; Int_t WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) override; @@ -78,7 +78,7 @@ public: void BuildRenderData() override; Int_t WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) override; - // For TAttBBox: + // From REveElement: void ComputeBBox() override; Bool_t GetDebugCornerPoints(); diff --git a/graf3d/eve7/inc/ROOT/REveCalo.hxx b/graf3d/eve7/inc/ROOT/REveCalo.hxx index af366d2936a46..7b9ec00bf429c 100644 --- a/graf3d/eve7/inc/ROOT/REveCalo.hxx +++ b/graf3d/eve7/inc/ROOT/REveCalo.hxx @@ -16,8 +16,6 @@ #include "ROOT/REveProjectionBases.hxx" #include "ROOT/REveProjectionManager.hxx" -#include "TAtt3D.h" -#include "TAttBBox.h" #include "ROOT/REveCaloData.hxx" class TClass; @@ -26,11 +24,7 @@ namespace ROOT { namespace Experimental { class REveRGBAPalette; -class REveCaloViz : public REveElement, - public TAtt3D, - public TAttBBox, - public REveProjectable -{ +class REveCaloViz : public REveElement, public REveProjectable { private: REveCaloViz& operator=(const REveCaloViz&) = delete; diff --git a/graf3d/eve7/inc/ROOT/REveDigitSet.hxx b/graf3d/eve7/inc/ROOT/REveDigitSet.hxx index 6fe182041afc2..ca303a7f8aec1 100644 --- a/graf3d/eve7/inc/ROOT/REveDigitSet.hxx +++ b/graf3d/eve7/inc/ROOT/REveDigitSet.hxx @@ -13,8 +13,6 @@ #define ROOT_REveDigitSet #include "TNamed.h" -#include "TAtt3D.h" -#include "TAttBBox.h" #include "ROOT/REveUtil.hxx" #include "ROOT/REveElement.hxx" @@ -28,11 +26,7 @@ class TRefArray; namespace ROOT { namespace Experimental { -class REveDigitSet : public REveElement, - public TAtt3D, - public TAttBBox, - public REveSecondarySelectable -{ +class REveDigitSet : public REveElement, public REveSecondarySelectable { friend class REveDigitSetEditor; friend class REveDigitSetGL; diff --git a/graf3d/eve7/inc/ROOT/REveElement.hxx b/graf3d/eve7/inc/ROOT/REveElement.hxx index 884998ab7c0ce..94efc94f02206 100644 --- a/graf3d/eve7/inc/ROOT/REveElement.hxx +++ b/graf3d/eve7/inc/ROOT/REveElement.hxx @@ -17,6 +17,7 @@ #include #include "TString.h" +#include #include #include #include @@ -99,6 +100,38 @@ protected: std::unique_ptr fRenderData;/// &stack, std::set& outStack); void SetVisLevel(int); diff --git a/graf3d/eve7/inc/ROOT/REveJetCone.hxx b/graf3d/eve7/inc/ROOT/REveJetCone.hxx index e3abba90de2d8..fbc9eace8883b 100644 --- a/graf3d/eve7/inc/ROOT/REveJetCone.hxx +++ b/graf3d/eve7/inc/ROOT/REveJetCone.hxx @@ -95,7 +95,7 @@ public: void BuildRenderData() override; - // For TAttBBox: + // From REveElement: void ComputeBBox() override; // Projected: diff --git a/graf3d/eve7/inc/ROOT/REveLogo.hxx b/graf3d/eve7/inc/ROOT/REveLogo.hxx index e6cb18d7074e3..8727733bd62bb 100644 --- a/graf3d/eve7/inc/ROOT/REveLogo.hxx +++ b/graf3d/eve7/inc/ROOT/REveLogo.hxx @@ -82,6 +82,7 @@ public: Int_t WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) override; void BuildRenderData() override; + void ComputeBBox() override { BBoxNotInWorld(); } }; } // namespace Experimental diff --git a/graf3d/eve7/inc/ROOT/REvePointSet.hxx b/graf3d/eve7/inc/ROOT/REvePointSet.hxx index 7605f74437d87..5295e3da6f256 100644 --- a/graf3d/eve7/inc/ROOT/REvePointSet.hxx +++ b/graf3d/eve7/inc/ROOT/REvePointSet.hxx @@ -18,7 +18,6 @@ #include #include -#include #include @@ -32,12 +31,7 @@ namespace Experimental { // external TRef or a number of integer indices. //////////////////////////////////////////////////////////////////////////////// -class REvePointSet : public REveElement, - public REveProjectable, - public TAttMarker, - public TAttBBox, - public REveSecondarySelectable -{ +class REvePointSet : public REveElement, public REveProjectable, public TAttMarker, public REveSecondarySelectable { friend class REvePointSetArray; private: diff --git a/graf3d/eve7/inc/ROOT/REveProjectionBases.hxx b/graf3d/eve7/inc/ROOT/REveProjectionBases.hxx index 6aaf49057fc4f..032240f88d35c 100644 --- a/graf3d/eve7/inc/ROOT/REveProjectionBases.hxx +++ b/graf3d/eve7/inc/ROOT/REveProjectionBases.hxx @@ -88,7 +88,7 @@ protected: REveProjectable *fProjectable{nullptr}; // link to original object Float_t fDepth{0.}; // z coordinate - void SetDepthCommon(Float_t d, REveElement *el, Float_t *bbox); + void SetDepthCommon(Float_t d, REveElement *el); virtual void SetDepthLocal(Float_t d); public: diff --git a/graf3d/eve7/inc/ROOT/REveProjectionManager.hxx b/graf3d/eve7/inc/ROOT/REveProjectionManager.hxx index 5fc5fe828435c..912949149b7e7 100644 --- a/graf3d/eve7/inc/ROOT/REveProjectionManager.hxx +++ b/graf3d/eve7/inc/ROOT/REveProjectionManager.hxx @@ -14,7 +14,6 @@ #include #include -#include namespace ROOT { namespace Experimental { @@ -24,10 +23,7 @@ namespace Experimental { /// Manager class for steering of projections and managing projected objects. //////////////////////////////////////////////////////////////////////////////// -class REveProjectionManager : public REveElement, - public REveAuntAsList, - public TAttBBox -{ +class REveProjectionManager : public REveElement, public REveAuntAsList { private: REveProjectionManager(const REveProjectionManager &) = delete; REveProjectionManager &operator=(const REveProjectionManager &) = delete; diff --git a/graf3d/eve7/inc/ROOT/REveSMorph.hxx b/graf3d/eve7/inc/ROOT/REveSMorph.hxx index a9ceb6fb4cdc9..343c2984249cb 100644 --- a/graf3d/eve7/inc/ROOT/REveSMorph.hxx +++ b/graf3d/eve7/inc/ROOT/REveSMorph.hxx @@ -14,15 +14,13 @@ #include -#include - #include #include namespace ROOT { namespace Experimental { -class REveSMorph : public REveElement, public TAttBBox { +class REveSMorph : public REveElement { private: REveSMorph(const REveSMorph &) = delete; REveSMorph &operator=(const REveSMorph &) = delete; diff --git a/graf3d/eve7/inc/ROOT/REveShape.hxx b/graf3d/eve7/inc/ROOT/REveShape.hxx index 0eab0cb64c3b5..453de1081f772 100644 --- a/graf3d/eve7/inc/ROOT/REveShape.hxx +++ b/graf3d/eve7/inc/ROOT/REveShape.hxx @@ -15,7 +15,6 @@ #include #include -#include "TAttBBox.h" #include "TColor.h" namespace ROOT { @@ -26,9 +25,7 @@ namespace Experimental { // Abstract base-class for 2D/3D shapes. // ========================================================================= -class REveShape : public REveElement, - public TAttBBox -{ +class REveShape : public REveElement { private: REveShape(const REveShape &) = delete; REveShape &operator=(const REveShape &) = delete; @@ -81,7 +78,7 @@ public: // ---------------------------------------------------------------- - // Abstract function from TAttBBox: + // From REveElement: // virtual void ComputeBBox(); // ---------------------------------------------------------------- diff --git a/graf3d/eve7/inc/ROOT/REveStraightLineSet.hxx b/graf3d/eve7/inc/ROOT/REveStraightLineSet.hxx index 068145797185d..70c6bf376f117 100644 --- a/graf3d/eve7/inc/ROOT/REveStraightLineSet.hxx +++ b/graf3d/eve7/inc/ROOT/REveStraightLineSet.hxx @@ -15,7 +15,6 @@ #include "TNamed.h" #include "TAttMarker.h" #include "TAttLine.h" -#include "TAttBBox.h" #include "ROOT/REveElement.hxx" #include "ROOT/REveProjectionBases.hxx" @@ -37,9 +36,7 @@ class REveStraightLineSet : public REveElement, public REveProjectable, public TAttLine, public TAttMarker, - public TAttBBox, - public REveSecondarySelectable -{ + public REveSecondarySelectable { private: REveStraightLineSet(const REveStraightLineSet&) = delete; REveStraightLineSet& operator=(const REveStraightLineSet&) = delete; @@ -121,7 +118,6 @@ public: void ComputeBBox() override; }; - /////////////////////////////////////////////////////////////////////////////// /// REveStraightLineSetProjected /// Projected copy of a REveStraightLineSet. diff --git a/graf3d/eve7/inc/ROOT/REveVector.hxx b/graf3d/eve7/inc/ROOT/REveVector.hxx index 48f12799db836..81bf5f1b54d5a 100644 --- a/graf3d/eve7/inc/ROOT/REveVector.hxx +++ b/graf3d/eve7/inc/ROOT/REveVector.hxx @@ -13,6 +13,7 @@ #define ROOT7_REveVector #include "TMath.h" +#include #include class TVector3; @@ -88,6 +89,23 @@ public: void NegateXYZ() { fX = - fX; fY = -fY; fZ = -fZ; } TT Normalize(TT length=1); + /// Set each component to the smaller of its own and that of `v`. + REveVectorT &SetMin(const REveVectorT &v) + { + fX = std::min(fX, v.fX); + fY = std::min(fY, v.fY); + fZ = std::min(fZ, v.fZ); + return *this; + } + /// Set each component to the larger of its own and that of `v`. + REveVectorT &SetMax(const REveVectorT &v) + { + fX = std::max(fX, v.fX); + fY = std::max(fY, v.fY); + fZ = std::max(fZ, v.fZ); + return *this; + } + TT Phi() const; TT Theta() const; TT CosTheta() const; diff --git a/graf3d/eve7/src/REveBox.cxx b/graf3d/eve7/src/REveBox.cxx index 84138441a5248..f2172914acc7b 100644 --- a/graf3d/eve7/src/REveBox.cxx +++ b/graf3d/eve7/src/REveBox.cxx @@ -155,7 +155,7 @@ REveBoxProjected::~REveBoxProjected() } //////////////////////////////////////////////////////////////////////////////// -/// Compute bounding-box, virtual from TAttBBox. +/// Fill the bounding box (virtual from REveElement). void REveBoxProjected::ComputeBBox() { @@ -169,7 +169,7 @@ void REveBoxProjected::ComputeBBox() void REveBoxProjected::SetDepthLocal(Float_t d) { - SetDepthCommon(d, this, fBBox); + SetDepthCommon(d, this); } //////////////////////////////////////////////////////////////////////////////// diff --git a/graf3d/eve7/src/REveBoxSet.cxx b/graf3d/eve7/src/REveBoxSet.cxx index 938417089a716..8062acc299d36 100644 --- a/graf3d/eve7/src/REveBoxSet.cxx +++ b/graf3d/eve7/src/REveBoxSet.cxx @@ -17,7 +17,9 @@ #include "ROOT/REveTrans.hxx" #include "TRandom.h" +#include #include +#include #include @@ -243,7 +245,7 @@ void REveBoxSet::AddHex(const REveVector& pos, Float_t r, Float_t angle, Float_t hex->fMat[i] = t[i]; } //////////////////////////////////////////////////////////////////////////////// -/// Fill bounding-box information of the base-class TAttBBox (virtual method). +/// Fill the bounding box (virtual from REveElement). /// If member 'REveFrameBox* fFrame' is set, frame's corners are used as bbox. void REveBoxSet::ComputeBBox() @@ -268,6 +270,13 @@ void REveBoxSet::ComputeBBox() BBoxInit(); + // The client draws an instance as pos + M * (def * v), with v in the shape + // template, which is within [-1, 1]^3, and M the instance's scale or matrix. + // The box of the positions is widened on each axis by the largest reach of + // any instance along that axis. + Float_t reach[3] = {0, 0, 0}; + const Float_t def[3] = {std::abs(fDefWidth), std::abs(fDefHeight), std::abs(fDefDepth)}; + REveChunkManager::iterator bi(fPlex); switch (fBoxType) { @@ -287,7 +296,9 @@ void REveBoxSet::ComputeBBox() while (bi.next()) { InstancedScaled_t& b = * (InstancedScaled_t*) bi(); BBoxCheckPoint(b.fX, b.fY, b.fZ); - BBoxCheckPoint(b.fX + b.fW, b.fY + b.fH , b.fZ + b.fD); + reach[0] = std::max(reach[0], def[0] * std::abs(b.fW)); + reach[1] = std::max(reach[1], def[1] * std::abs(b.fH)); + reach[2] = std::max(reach[2], def[2] * std::abs(b.fD)); } break; } @@ -297,8 +308,9 @@ void REveBoxSet::ComputeBBox() while (bi.next()) { Instanced_t& b = * (Instanced_t*) bi(); BBoxCheckPoint(b.fX, b.fY, b.fZ); - BBoxCheckPoint(b.fX + fDefWidth, b.fY + fDefHeight , b.fZ + fDefDepth); } + for (Int_t k = 0; k < 3; ++k) + reach[k] = def[k]; break; } @@ -308,6 +320,10 @@ void REveBoxSet::ComputeBBox() InstancedScaledRotated_t& b = * (InstancedScaledRotated_t*) bi(); float* a = b.fMat; BBoxCheckPoint(a[12], a[13], a[14]); + // Column-major: column j of M is a[4j], a[4j+1], a[4j+2]. + for (Int_t k = 0; k < 3; ++k) + reach[k] = std::max(reach[k], + std::abs(a[k]) * def[0] + std::abs(a[4 + k]) * def[1] + std::abs(a[8 + k]) * def[2]); } break; } @@ -318,6 +334,11 @@ void REveBoxSet::ComputeBBox() } } // end switch box-type + + for (Int_t k = 0; k < 3; ++k) { + fBBoxMin[k] -= reach[k]; + fBBoxMax[k] += reach[k]; + } } //////////////////////////////////////////////////////////////////////////////// @@ -411,12 +432,6 @@ void REveBoxSet::BuildRenderData() fRenderData->ResizeV(4 * fTexX * fTexY); } - // save bbox in render data normals - ComputeBBox(); - float* bb = GetBBox(); - fRenderData->PushN(bb[0], bb[1], bb[2]); - fRenderData->PushN(bb[3], bb[4], bb[5]); - REveElement::BuildRenderData(); } diff --git a/graf3d/eve7/src/REveCalo.cxx b/graf3d/eve7/src/REveCalo.cxx index fd74931890b33..4d263d86a7832 100644 --- a/graf3d/eve7/src/REveCalo.cxx +++ b/graf3d/eve7/src/REveCalo.cxx @@ -702,7 +702,7 @@ void REveCalo3D::BuildCellIdCache() } //////////////////////////////////////////////////////////////////////////////// -/// Fill bounding-box information of the base-class TAttBBox (virtual method). +/// Fill the bounding box (virtual from REveElement). /// If member 'REveFrameBox* fFrame' is set, frame's corners are used as bbox. void REveCalo3D::ComputeBBox() @@ -710,13 +710,10 @@ void REveCalo3D::ComputeBBox() BBoxInit(); Float_t th = (fData) ? GetValToHeight() * fData->GetMaxVal(fPlotEt) : 0; + Float_t r = fBarrelRadius + th; - fBBox[0] = -fBarrelRadius - th; - fBBox[1] = fBarrelRadius + th; - fBBox[2] = fBBox[0]; - fBBox[3] = fBBox[1]; - fBBox[4] = fEndCapPosB - th; - fBBox[5] = fEndCapPosF + th; + fBBoxMin.Set(-r, -r, fEndCapPosB - th); + fBBoxMax.Set(r, r, fEndCapPosF + th); } /** \class REveCalo2D @@ -981,7 +978,7 @@ Float_t REveCalo2D::GetValToHeight() const } //////////////////////////////////////////////////////////////////////////////// -/// Fill bounding-box information of the base-class TAttBBox (virtual method). +/// Fill the bounding box (virtual from REveElement). /// If member 'REveFrameBox* fFrame' is set, frame's corners are used as bbox. void REveCalo2D::ComputeBBox() @@ -1493,24 +1490,21 @@ void REveCaloLego::BuildCellIdCache() } //////////////////////////////////////////////////////////////////////////////// -/// Fill bounding-box information of the base-class TAttBBox (virtual method). +/// Fill the bounding box (virtual from REveElement). /// If member 'REveFrameBox* fFrame' is set, frame's corners are used as bbox. void REveCaloLego::ComputeBBox() { - - // fBBox = Float_t[6] X(min,max), Y(min,max), Z(min,max) - BBoxZero(); Float_t ex = 1.2; // 20% offset for axis labels Float_t a = 0.5*ex; - fBBox[0] = -a; - fBBox[1] = a; - fBBox[2] = -a; - fBBox[3] = a; + fBBoxMin.fX = -a; + fBBoxMax.fX = a; + fBBoxMin.fY = -a; + fBBoxMax.fY = a; // scaling is relative to shortest XY axis Double_t em, eM, pm, pM; @@ -1519,19 +1513,18 @@ void REveCaloLego::ComputeBBox() Double_t r = (eM-em)/(pM-pm); if (r<1) { - fBBox[2] /= r; - fBBox[3] /= r; + fBBoxMin.fY /= r; + fBBoxMax.fY /= r; } else { - fBBox[0] *= r; - fBBox[1] *= r; + fBBoxMin.fX *= r; + fBBoxMax.fX *= r; } - fBBox[4] = 0; + fBBoxMin.fZ = 0; if (fScaleAbs && !fData->Empty()) - fBBox[5] = GetMaxVal()*GetValToHeight(); + fBBoxMax.fZ = GetMaxVal() * GetValToHeight(); else - fBBox[5] = fMaxTowerH; + fBBoxMax.fZ = fMaxTowerH; } - diff --git a/graf3d/eve7/src/REveElement.cxx b/graf3d/eve7/src/REveElement.cxx index 9304f3cde2656..eb91751dd2cea 100644 --- a/graf3d/eve7/src/REveElement.cxx +++ b/graf3d/eve7/src/REveElement.cxx @@ -29,6 +29,9 @@ #include #include +#include +#include +#include #include @@ -862,7 +865,7 @@ void REveElement::DestroyMainTrans() void REveElement::SetTransMatrix(Double_t* carr) { RefMainTrans().SetFrom(carr); - StampTransBBox(); + StampTrans(); } //////////////////////////////////////////////////////////////////////////////// @@ -871,7 +874,7 @@ void REveElement::SetTransMatrix(Double_t* carr) void REveElement::SetTransMatrix(const TGeoMatrix& mat) { RefMainTrans().SetFrom(mat); - StampTransBBox(); + StampTrans(); } //////////////////////////////////////////////////////////////////////////////// @@ -1463,6 +1466,9 @@ void REveElement::RecheckImpliedSelections() void REveElement::AddStamp(UChar_t bits) { + if (bits & kCBObjProps) + fBBoxState = kBBoxInvalid; + if (fDestructing == kNone && fScene && fScene->IsAcceptingChanges()) { if (gDebug > 0) @@ -1477,6 +1483,71 @@ void REveElement::AddStamp(UChar_t bits) } } +//////////////////////////////////////////////////////////////////////////////// +/// Start filling the bounding box with BBoxCheckPoint(): min at +infinity, +/// max at -infinity, so the first point sets both. A box that no point +/// reached stays inverted, and AssertBBox() reports it as kBBoxNone. + +void REveElement::BBoxInit() +{ + const Float_t inf = std::numeric_limits::infinity(); + fBBoxMin.Set(inf, inf, inf); + fBBoxMax.Set(-inf, -inf, -inf); + fBBoxState = kBBoxSet; +} + +//////////////////////////////////////////////////////////////////////////////// +/// Set the bounding box to a cube of half-side `epsilon` at (`x`, `y`, `z`); +/// with the defaults, a point at the origin. + +void REveElement::BBoxZero(Float_t epsilon, Float_t x, Float_t y, Float_t z) +{ + fBBoxMin.Set(x - epsilon, y - epsilon, z - epsilon); + fBBoxMax.Set(x + epsilon, y + epsilon, z + epsilon); + fBBoxState = kBBoxSet; +} + +//////////////////////////////////////////////////////////////////////////////// +/// Widen every side of the bounding box shorter than `epsilon` to `epsilon`, +/// keeping its centre. + +void REveElement::AssertBBoxExtents(Float_t epsilon) +{ + if (fBBoxMin.fX > fBBoxMax.fX) + return; // empty, see BBoxInit() + for (Int_t i = 0; i < 3; ++i) { + if (fBBoxMax[i] - fBBoxMin[i] < epsilon) { + Float_t c = 0.5f * (fBBoxMin[i] + fBBoxMax[i]); + fBBoxMin[i] = c - 0.5f * epsilon; + fBBoxMax[i] = c + 0.5f * epsilon; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +/// Compute the bounding box in the element's own frame, before fMainTrans. +/// Overrides fill it with BBoxInit() and BBoxCheckPoint(), or BBoxZero(), or +/// call BBoxNotInWorld() for screen-space content. This default is for an +/// element with no geometry of its own. + +void REveElement::ComputeBBox() +{ + fBBoxState = kBBoxNone; +} + +//////////////////////////////////////////////////////////////////////////////// +/// Compute the bounding box if it is invalid, and return its state. + +REveElement::EBBoxState REveElement::AssertBBox() +{ + if (fBBoxState == kBBoxInvalid) { + ComputeBBox(); + if (fBBoxState == kBBoxSet && fBBoxMin.fX > fBBoxMax.fX) + fBBoxState = kBBoxNone; // no point reached the box, see BBoxInit() + } + return fBBoxState; +} + //////////////////////////////////////////////////////////////////////////////// /// Write transformation Matrix to render data //////////////////////////////////////////////////////////////////////////////// @@ -1489,7 +1560,7 @@ void REveElement::BuildRenderData() } //////////////////////////////////////////////////////////////////////////////// -/// Write the main transformation and its motion for a kCBTransBBox update. +/// Write the main transformation and its motion for a kCBTrans update. /// No render data is written; the client applies it to the object it already /// has. It goes out as JSON, so no offset is reserved in the binary blob for a /// message that carries no geometry. @@ -1571,6 +1642,21 @@ Int_t REveElement::WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) j["render_data"] = rd; ret = fRenderData->GetBinarySize(); + + // kCBObjProps invalidates the box, see AddStamp(). + switch (AssertBBox()) { + case kBBoxSet: + j["bbox"] = {fBBoxMin.fX, fBBoxMin.fY, fBBoxMin.fZ, fBBoxMax.fX, fBBoxMax.fY, fBBoxMax.fZ}; + break; + case kBBoxNotInWorld: j["bbox_not_in_world"] = true; break; + default: { + static std::mutex mtx; + static std::set warned; + std::lock_guard lock(mtx); + if (warned.insert(IsA()->GetName()).second) + ::Warning("REveElement::WriteCoreJson", "%s has render data but no bounding box.", IsA()->GetName()); + } + } } } diff --git a/graf3d/eve7/src/REveGeoShape.cxx b/graf3d/eve7/src/REveGeoShape.cxx index cc420c9f47ad3..9d7c7893c0091 100644 --- a/graf3d/eve7/src/REveGeoShape.cxx +++ b/graf3d/eve7/src/REveGeoShape.cxx @@ -222,14 +222,11 @@ void REveGeoShape::SetShape(TGeoShape *s) void REveGeoShape::ComputeBBox() { + BBoxInit(); TGeoBBox *bb = dynamic_cast(fShape); if (bb) { - BBoxInit(); const Double_t *o = bb->GetOrigin(); - BBoxCheckPoint(o[0] - bb->GetDX(), o[0] - bb->GetDY(), o[0] - bb->GetDZ()); - BBoxCheckPoint(o[0] + bb->GetDX(), o[0] + bb->GetDY(), o[0] + bb->GetDZ()); - } else { - BBoxZero(); + BBoxCheckBox(REveVector(o[0], o[1], o[2]), REveVector(bb->GetDX(), bb->GetDY(), bb->GetDZ())); } } @@ -474,22 +471,17 @@ void REveGeoShapeProjected::UpdateProjection() } //////////////////////////////////////////////////////////////////////////////// -/// Override of virtual method from TAttBBox. +/// Fill the bounding box (virtual from REveElement). void REveGeoShapeProjected::ComputeBBox() { + BBoxInit(); if (fBuff && fBuff->NbPnts() > 0) { - BBoxInit(); - Double_t *p = fBuff->fPnts; for (UInt_t i = 0; i < fBuff->NbPnts(); ++i, p+=3) { BBoxCheckPoint(p[0], p[1], p[2]); } } - else - { - BBoxZero(); - } } diff --git a/graf3d/eve7/src/REveGeoTopNode.cxx b/graf3d/eve7/src/REveGeoTopNode.cxx index 11c24bd1d0f3f..ee2499c758a26 100644 --- a/graf3d/eve7/src/REveGeoTopNode.cxx +++ b/graf3d/eve7/src/REveGeoTopNode.cxx @@ -742,6 +742,26 @@ void REveGeoTopNodeViz::BuildRenderData() GetRenderData()->PushV(pc); } } +//////////////////////////////////////////////////////////////////////////////// +/// Fill the bounding box (virtual from REveElement) from the box of the apex +/// node's shape. Called from WriteCoreJson() inside its REveGeoManagerHolder. + +void REveGeoTopNodeViz::ComputeBBox() +{ + TGeoNode *top = fGeoData ? fGeoData->fDesc.GetApexNode() : nullptr; + TGeoShape *shape = top ? top->GetVolume()->GetShape() : nullptr; + if (shape) + shape->ComputeBBox(); + TGeoBBox *box = dynamic_cast(shape); + if (!box) { + BBoxZero(); + return; + } + const Double_t *o = box->GetOrigin(); + BBoxInit(); + BBoxCheckBox(REveVector(o[0], o[1], o[2]), REveVector(box->GetDX(), box->GetDY(), box->GetDZ())); +} + //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void REveGeoTopNodeViz::SetGeoData(REveGeoTopNodeData *d, bool rebuild) @@ -830,30 +850,6 @@ int REveGeoTopNodeViz::WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) j["nodeVisibility"] = nodeVisibility; j["fSecondarySelect"] = fAlwaysSecSelect; - - - - // ship bounding box info - TGeoNode* top = fGeoData->fDesc.GetApexNode(); - TGeoVolume *vol = top->GetVolume(); - TGeoShape *shape = vol->GetShape(); - shape->ComputeBBox(); - TGeoBBox *box = dynamic_cast(shape); - if (box) { - const Double_t *origin = box->GetOrigin(); - - // printf("BBox center: (%f, %f, %f)\n", origin[0], origin[1], origin[2]); - //printf("origin lengths: (%f, %f, %f)\n", origin[0], origin[1], origin[2]); - - auto jbb = json::array(); - jbb.push_back(origin[0] - box->GetDX()); - jbb.push_back(origin[0] + box->GetDX()); - jbb.push_back(origin[1] - box->GetDY()); - jbb.push_back(origin[1] + box->GetDY()); - jbb.push_back(origin[2] - box->GetDZ()); - jbb.push_back(origin[2] + box->GetDZ()); - j["bbox"] = jbb; - } // std::cout << "Write Core json " << j.dump(1) << "\n"; return ret; } diff --git a/graf3d/eve7/src/REveJetCone.cxx b/graf3d/eve7/src/REveJetCone.cxx index a355c130b820d..f9224d23ecedb 100644 --- a/graf3d/eve7/src/REveJetCone.cxx +++ b/graf3d/eve7/src/REveJetCone.cxx @@ -331,7 +331,7 @@ void REveJetConeProjected::BuildRenderData() } //////////////////////////////////////////////////////////////////////////////// -/// Compute bounding-box, virtual from TAttBBox. +/// Fill the bounding box (virtual from REveElement). void REveJetConeProjected::ComputeBBox() { @@ -352,7 +352,7 @@ void REveJetConeProjected::ComputeBBox() void REveJetConeProjected::SetDepthLocal(Float_t d) { - SetDepthCommon(d, this, fBBox); + SetDepthCommon(d, this); } //////////////////////////////////////////////////////////////////////////////// diff --git a/graf3d/eve7/src/REveLine.cxx b/graf3d/eve7/src/REveLine.cxx index 4e711e47fe950..72cd806f55241 100644 --- a/graf3d/eve7/src/REveLine.cxx +++ b/graf3d/eve7/src/REveLine.cxx @@ -352,7 +352,7 @@ void REveLineProjected::SetProjection(REveProjectionManager* mng, void REveLineProjected::SetDepthLocal(Float_t d) { - SetDepthCommon(d, this, fBBox); + SetDepthCommon(d, this); for (Int_t i = 0; i < fSize; ++i) fPoints[i].fZ = fDepth; diff --git a/graf3d/eve7/src/REvePointSet.cxx b/graf3d/eve7/src/REvePointSet.cxx index 90896e7c24feb..5052eceaef1bf 100644 --- a/graf3d/eve7/src/REvePointSet.cxx +++ b/graf3d/eve7/src/REvePointSet.cxx @@ -43,12 +43,8 @@ REveProjectionManager class. //////////////////////////////////////////////////////////////////////////////// /// Constructor. -REvePointSet::REvePointSet(const std::string& name, const std::string& title, Int_t n_points) : - REveElement(name, title), - REveProjectable(), - TAttMarker(), - TAttBBox(), - REveSecondarySelectable() +REvePointSet::REvePointSet(const std::string &name, const std::string &title, Int_t n_points) + : REveElement(name, title), REveProjectable(), TAttMarker(), REveSecondarySelectable() { fMarkerStyle = 20; @@ -63,12 +59,8 @@ REvePointSet::REvePointSet(const std::string& name, const std::string& title, In //////////////////////////////////////////////////////////////////////////////// /// Copy constructor. -REvePointSet::REvePointSet(const REvePointSet& e) : - REveElement(e), - REveProjectable(e), - TAttMarker(e), - TAttBBox(e), - REveSecondarySelectable() +REvePointSet::REvePointSet(const REvePointSet &e) + : REveElement(e), REveProjectable(e), TAttMarker(e), REveSecondarySelectable() { fAlwaysSecSelect = e.GetAlwaysSecSelect(); ClonePoints(e); @@ -244,13 +236,7 @@ void REvePointSet::BuildRenderData() fRenderData->PushV(&fPoints[i].fX, 3); fRenderData->PushV(0); } - fRenderData->ResizeV(4*fTexX*fTexY); - - // save bbox in render data normals - ComputeBBox(); - float* bb = GetBBox(); - fRenderData->PushN(bb[0], bb[1], bb[2]); - fRenderData->PushN(bb[3], bb[4], bb[5]); + fRenderData->ResizeV(4 * fTexX * fTexY); } else { fRenderData = std::make_unique("makeHit", 3*fSize); fRenderData->PushV(&fPoints[0].fX, 3*fSize); @@ -265,15 +251,9 @@ void REvePointSet::BuildRenderData() void REvePointSet::ComputeBBox() { - if (fSize > 0) { - BBoxInit(); - for (auto &p : fPoints) - { - BBoxCheckPoint(p.fX, p.fY, p.fZ); - } - } else { - BBoxZero(); - } + BBoxInit(); + for (Int_t i = 0; i < fSize; ++i) + BBoxCheckPoint(fPoints[i]); } //////////////////////////////////////////////////////////////////////////////// @@ -556,7 +536,7 @@ void REvePointSetProjected::SetProjection(REveProjectionManager* proj, void REvePointSetProjected::SetDepthLocal(Float_t d) { - SetDepthCommon(d, this, fBBox); + SetDepthCommon(d, this); // XXXX rewrite diff --git a/graf3d/eve7/src/REvePolygonSetProjected.cxx b/graf3d/eve7/src/REvePolygonSetProjected.cxx index ac850f41823ed..bd39c23a0770a 100644 --- a/graf3d/eve7/src/REvePolygonSetProjected.cxx +++ b/graf3d/eve7/src/REvePolygonSetProjected.cxx @@ -154,17 +154,13 @@ void REvePolygonSetProjected::BuildRenderData() } //////////////////////////////////////////////////////////////////////////////// -/// Override of virtual method from TAttBBox. +/// Fill the bounding box (virtual from REveElement). void REvePolygonSetProjected::ComputeBBox() { - if (!fPnts.empty()) { - BBoxInit(); - for (unsigned pi = 0; pi < fPnts.size(); ++pi) - BBoxCheckPoint(fPnts[pi].fX, fPnts[pi].fY, fPnts[pi].fZ); - } else { - BBoxZero(); - } + BBoxInit(); + for (unsigned pi = 0; pi < fPnts.size(); ++pi) + BBoxCheckPoint(fPnts[pi]); } //////////////////////////////////////////////////////////////////////////////// @@ -185,7 +181,7 @@ void REvePolygonSetProjected::SetProjection(REveProjectionManager* mng, void REvePolygonSetProjected::SetDepthLocal(Float_t d) { - SetDepthCommon(d, this, fBBox); + SetDepthCommon(d, this); for (unsigned i = 0; i < fPnts.size(); ++i) fPnts[i].fZ = fDepth; diff --git a/graf3d/eve7/src/REveProjectionAxis.cxx b/graf3d/eve7/src/REveProjectionAxis.cxx index 08bb60b63c916..671839ec1dae7 100644 --- a/graf3d/eve7/src/REveProjectionAxis.cxx +++ b/graf3d/eve7/src/REveProjectionAxis.cxx @@ -122,12 +122,10 @@ void REveProjectionAxis::BuildTicks(Int_t ax) if (!proj) return; - fManager->AssertBBox(); - Float_t *bb = fManager->GetBBox(); - if (!bb) + if (fManager->AssertBBox() != REveElement::kBBoxSet) return; - Float_t pmin_t = bb[ax * 2], pmax_t = bb[ax * 2 + 1]; + Float_t pmin_t = fManager->GetBBoxMin()[ax], pmax_t = fManager->GetBBoxMax()[ax]; if (pmax_t <= pmin_t) return; diff --git a/graf3d/eve7/src/REveProjectionBases.cxx b/graf3d/eve7/src/REveProjectionBases.cxx index 89519d562ea04..4a4298c4e850b 100644 --- a/graf3d/eve7/src/REveProjectionBases.cxx +++ b/graf3d/eve7/src/REveProjectionBases.cxx @@ -224,19 +224,14 @@ void REveProjected::SetDepth(Float_t d) } //////////////////////////////////////////////////////////////////////////////// -/// Utility function to update the z-values of the bounding-box. -/// As this is an abstract interface, the element and bbox pointers -/// must be passed from outside. +/// Set the depth and invalidate the bounding box of `el`, whose vertices the +/// caller moves to the new depth. -void REveProjected::SetDepthCommon(Float_t d, REveElement* el, Float_t* bbox) +void REveProjected::SetDepthCommon(Float_t d, REveElement *el) { - Float_t delta = d - fDepth; fDepth = d; - if (bbox) { - bbox[4] += delta; - bbox[5] += delta; - el->StampTransBBox(); - } + el->ResetBBox(); + el->StampTrans(); } //////////////////////////////////////////////////////////////////////////////// diff --git a/graf3d/eve7/src/REveProjectionManager.cxx b/graf3d/eve7/src/REveProjectionManager.cxx index 873bea72ac714..453bad66f466b 100644 --- a/graf3d/eve7/src/REveProjectionManager.cxx +++ b/graf3d/eve7/src/REveProjectionManager.cxx @@ -20,6 +20,8 @@ #include "TClass.h" +#include + #include /** \class REveProjectionManager @@ -211,11 +213,8 @@ Bool_t REveProjectionManager::ShouldImport(REveElement *el) void REveProjectionManager::UpdateDependentElements(REveElement * /*root*/) { - for (auto &d: fDependentEls) { - TAttBBox* bbox = dynamic_cast(d); - if (bbox) - bbox->ComputeBBox(); - } + for (auto &d : fDependentEls) + d->ComputeBBox(); } //////////////////////////////////////////////////////////////////////////////// @@ -289,7 +288,7 @@ REveElement* REveProjectionManager::ImportElements(REveElement* el, AssertBBox(); ProjectChildrenRecurse(new_el); AssertBBoxExtents(0.1); - StampTransBBox(); + StampTrans(); UpdateDependentElements(new_el); @@ -321,7 +320,7 @@ REveElement* REveProjectionManager::SubImportElements(REveElement* el, AssertBBox(); ProjectChildrenRecurse(new_el); AssertBBoxExtents(0.1); - StampTransBBox(); + StampTrans(); UpdateDependentElements(new_el); } @@ -356,7 +355,7 @@ Int_t REveProjectionManager::SubImportChildren(REveElement* el, REveElement* pro for (auto &nel: new_els) ProjectChildrenRecurse(nel); AssertBBoxExtents(0.1); - StampTransBBox(); + StampTrans(); UpdateDependentElements(proj_parent); } @@ -374,14 +373,11 @@ void REveProjectionManager::ProjectChildrenRecurse(REveElement* el) if (pted) { pted->UpdateProjection(); - TAttBBox* bb = dynamic_cast(pted); - if (bb) - { - Float_t* b = bb->AssertBBox(); - BBoxCheckPoint(b[0], b[2], b[4]); - BBoxCheckPoint(b[1], b[3], b[5]); - } el->StampObjProps(); + if (el->AssertBBox() == kBBoxSet) { + BBoxCheckPoint(el->GetBBoxMin()); + BBoxCheckPoint(el->GetBBoxMax()); + } } for (auto &c : el->RefChildren()) ProjectChildrenRecurse(c); @@ -400,22 +396,18 @@ void REveProjectionManager::ProjectChildren() for (auto &n : fNieces) ProjectChildrenRecurse(n); AssertBBoxExtents(0.1); - StampTransBBox(); + StampTrans(); UpdateDependentElements(this); } //////////////////////////////////////////////////////////////////////////////// -/// Virtual from TAttBBox; fill bounding-box information. -/// -/// The bounding-box information is kept coherent during addition of -/// projected elements and projection parameter updates. This is -/// called only in case the manager has not been populated at all. +/// Fill the bounding box (virtual from REveElement) with the union of the +/// boxes of the projected elements below the children and the nieces. +/// ImportElements() and ProjectChildren() also widen it as they project. void REveProjectionManager::ComputeBBox() { - static const REveException eH("REveProjectionManager::ComputeBBox "); - if ( ! HasChildren() && ! HasNieces()) { BBoxZero(); @@ -423,4 +415,20 @@ void REveProjectionManager::ComputeBBox() } BBoxInit(); + + std::function add = [&](REveElement *el) { + if (dynamic_cast(el) && el->AssertBBox() == kBBoxSet && + el->GetBBoxMin().fX <= el->GetBBoxMax().fX) { + BBoxCheckPoint(el->GetBBoxMin()); + BBoxCheckPoint(el->GetBBoxMax()); + } + for (auto &c : el->RefChildren()) + add(c); + }; + for (auto &c : fChildren) + add(c); + for (auto &n : fNieces) + add(n); + + AssertBBoxExtents(0.1); } diff --git a/graf3d/eve7/src/REveSMorph.cxx b/graf3d/eve7/src/REveSMorph.cxx index 4706556b9f50a..a13a50deab6e3 100644 --- a/graf3d/eve7/src/REveSMorph.cxx +++ b/graf3d/eve7/src/REveSMorph.cxx @@ -29,7 +29,7 @@ coordinates, so only the parameters are streamed. The surface is generated at unit size. Radius, position and orientation are in the element's transformation, so changing them streams one matrix under -kCBTransBBox and the client rebuilds nothing. Every other setter stamps +kCBTrans and the client rebuilds nothing. Every other setter stamps kCBObjProps and forces a rebuild. The parametrisation, with ct = cos(theta), st = sin(theta): @@ -64,13 +64,13 @@ REveSMorph::REveSMorph(const std::string &n, const std::string &t) : REveElement } //////////////////////////////////////////////////////////////////////////////// -/// Uniform scale on the main transformation. Stamps kCBTransBBox only: the +/// Uniform scale on the main transformation. Stamps kCBTrans only: the /// surface is built at unit size, so the client rebuilds nothing. void REveSMorph::SetRadius(Float_t r) { RefMainTrans().SetScale(r, r, r); - StampTransBBox(); + StampTrans(); } //////////////////////////////////////////////////////////////////////////////// @@ -102,12 +102,6 @@ Int_t REveSMorph::WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) j["fTexYOff"] = fTexYOff; // clang-format on - // Bounding box as JSON, min triple then max triple, for RC.Box3. The client - // could measure its own geometry; the box is sent because it is known here. - ComputeBBox(); - const Float_t *bb = GetBBox(); - j["bbox"] = {bb[0], bb[2], bb[4], bb[1], bb[3], bb[5]}; - return ret; } diff --git a/graf3d/eve7/src/REveScene.cxx b/graf3d/eve7/src/REveScene.cxx index aa1576900291e..b2382a0690eaf 100644 --- a/graf3d/eve7/src/REveScene.cxx +++ b/graf3d/eve7/src/REveScene.cxx @@ -228,7 +228,7 @@ void REveScene::StreamJsonRecurse(REveElement *el, nlohmann::json &jarr) } //////////////////////////////////////////////////////////////////////////////// -/// Move every element whose only pending change is kCBTransBBox from the change +/// Move every element whose only pending change is kCBTrans from the change /// list into `arr`, for REveManager::SendMotionChanges(). The remaining /// elements are streamed as ordinary, acknowledged scene changes. @@ -240,7 +240,7 @@ void REveScene::StreamMotionChanges(nlohmann::json &arr) List_t keep; for (auto &el : fChangedElements) { - if (el->GetChangeBits() != REveElement::kCBTransBBox) { + if (el->GetChangeBits() != REveElement::kCBTrans) { keep.push_back(el); continue; } @@ -320,8 +320,7 @@ void REveScene::StreamRepresentationChanges() el->WriteCoreJson(jobj, -1); } - if (bits & kCBTransBBox) - { + if (bits & kCBTrans) { el->WriteTransJson(jobj); } } diff --git a/graf3d/eve7/src/REveStraightLineSet.cxx b/graf3d/eve7/src/REveStraightLineSet.cxx index 25fc573976168..29e8ff4785178 100644 --- a/graf3d/eve7/src/REveStraightLineSet.cxx +++ b/graf3d/eve7/src/REveStraightLineSet.cxx @@ -187,6 +187,9 @@ Int_t REveStraightLineSet::WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) void REveStraightLineSet::BuildRenderData() { int nVertices = fLinePlex.Size() * 2 + fMarkerPlex.Size(); + if (nVertices == 0) + return; + fRenderData = std::make_unique("makeStraightLineSet", 3 * nVertices, 0, nVertices); // printf("REveStraightLineSet::BuildRenderData id = %d \n", GetElementId()); @@ -224,16 +227,10 @@ TClass* REveStraightLineSet::ProjectedClass(const REveProjection*) const } //////////////////////////////////////////////////////////////////////////////// -/// Compute bounding-box. -/// Virtual from TAttBBox. +/// Fill the bounding box (virtual from REveElement). void REveStraightLineSet::ComputeBBox() { - if (fLinePlex.Size() == 0 && fMarkerPlex.Size() == 0) { - BBoxZero(); - return; - } - BBoxInit(); REveChunkManager::iterator li(fLinePlex); @@ -280,7 +277,7 @@ void REveStraightLineSetProjected::SetProjection(REveProjectionManager* mng, void REveStraightLineSetProjected::SetDepthLocal(Float_t d) { - SetDepthCommon(d, this, fBBox); + SetDepthCommon(d, this); REveChunkManager::iterator li(fLinePlex); while (li.next()) diff --git a/graf3d/eve7/src/REveText.cxx b/graf3d/eve7/src/REveText.cxx index 1b22252e868f7..b86052f2f5116 100644 --- a/graf3d/eve7/src/REveText.cxx +++ b/graf3d/eve7/src/REveText.cxx @@ -107,11 +107,12 @@ void REveText::BuildRenderData() } //////////////////////////////////////////////////////////////////////////////// -/// Compute bounding-box of the data. +/// Text has no box: its extent is known only to the client, which lays it +/// out. It is not culled. void REveText::ComputeBBox() { - //BBoxInit(); + BBoxNotInWorld(); } diff --git a/graf3d/eve7/src/REveTrack.cxx b/graf3d/eve7/src/REveTrack.cxx index 4816bf344d671..b2d0b3bc39a80 100644 --- a/graf3d/eve7/src/REveTrack.cxx +++ b/graf3d/eve7/src/REveTrack.cxx @@ -222,24 +222,11 @@ REveTrack::~REveTrack() void REveTrack::ComputeBBox() { - if (fSize > 0 || ! fPathMarks.empty()) - { - BBoxInit(); - Int_t n = fSize; - Float_t *p = & fPoints[0].fX; - for (Int_t i = 0; i < n; ++i, p += 3) - { - BBoxCheckPoint(p); - } - for (const auto &pm: fPathMarks) - { - BBoxCheckPoint(pm.fV.fX, pm.fV.fY, pm.fV.fZ); - } - } - else - { - BBoxZero(); - } + BBoxInit(); + for (Int_t i = 0; i < fSize; ++i) + BBoxCheckPoint(fPoints[i]); + for (const auto &pm : fPathMarks) + BBoxCheckPoint(pm.fV.fX, pm.fV.fY, pm.fV.fZ); } //////////////////////////////////////////////////////////////////////////////// diff --git a/graf3d/eve7/src/REveTrackProjected.cxx b/graf3d/eve7/src/REveTrackProjected.cxx index f635cee3575e7..8385947605ad6 100644 --- a/graf3d/eve7/src/REveTrackProjected.cxx +++ b/graf3d/eve7/src/REveTrackProjected.cxx @@ -52,7 +52,7 @@ void REveTrackProjected::SetProjection(REveProjectionManager* mng, REveProjectab void REveTrackProjected::SetDepthLocal(Float_t d) { - SetDepthCommon(d, this, fBBox); + SetDepthCommon(d, this); for (Int_t i = 0; i < fSize; ++i) { diff --git a/graf3d/eve7/src/REveTrans.cxx b/graf3d/eve7/src/REveTrans.cxx index dbd23d84c6c06..aa1afbedba2f9 100644 --- a/graf3d/eve7/src/REveTrans.cxx +++ b/graf3d/eve7/src/REveTrans.cxx @@ -44,7 +44,7 @@ using namespace ROOT::Experimental; //////////////////////////////////////////////////////////////////////////////// /// Declare how this transformation is changing; position still comes from the /// matrix. Does not stamp the owning element, which must be stamped with -/// kCBTransBBox for the motion to be streamed. +/// kCBTrans for the motion to be streamed. void REveTrans::SetMotion(const REveVectorD &vel, const REveVectorD &acc, Double_t max_dt) { diff --git a/graf3d/eve7/test/change_cycle.cxx b/graf3d/eve7/test/change_cycle.cxx index 4cc19cc42ff92..527f210e49f56 100644 --- a/graf3d/eve7/test/change_cycle.cxx +++ b/graf3d/eve7/test/change_cycle.cxx @@ -158,7 +158,7 @@ TEST(REveChangeCycle, TransOnlyGoesToMotion) eve->BeginChange(); moved->RefMainTrans().SetPos(1, 2, 3); - moved->StampTransBBox(); + moved->StampTrans(); edited->StampObjProps(); nlohmann::json arr = nlohmann::json::array(); diff --git a/graf3d/eve7/test/streaming.cxx b/graf3d/eve7/test/streaming.cxx index 9e100e4f5ce63..1c981aae636ab 100644 --- a/graf3d/eve7/test/streaming.cxx +++ b/graf3d/eve7/test/streaming.cxx @@ -15,8 +15,15 @@ #include #include #include +#include +#include +#include +#include +#include #include +#include "TGeoTube.h" + #include #include @@ -62,16 +69,20 @@ TEST(REveStreaming, PointSet) EXPECT_EQ(bin % 4, 0); if (eve->IsRCore()) { - // One texel of 4 floats per point, and the bbox, min then max, as normals. + // One texel of 4 floats per point. EXPECT_EQ(j["fSize"], 10); int tx = j["fTexX"], ty = j["fTexY"]; EXPECT_GE(tx * ty, 10); EXPECT_EQ(rd["vert_size"], 4 * tx * ty); - EXPECT_EQ(rd["norm_size"], 6); + EXPECT_EQ(rd["norm_size"], 0); } else { EXPECT_EQ(rd["vert_size"], 30); } + // The bounding box, min then max, in the core fields. + ASSERT_TRUE(j.contains("bbox")); + EXPECT_EQ(j["bbox"], nlohmann::json({0, 0, 0, 9, 18, 27})); + delete ps; } @@ -160,3 +171,208 @@ TEST(REveStreaming, SceneStream) EXPECT_FLOAT_EQ(v[2], 3.f); } } + +// Stamping kCBObjProps invalidates the bounding box; the next AssertBBox() +// computes it from the current geometry. +TEST(REveStreaming, BBoxInvalidatedByObjProps) +{ + REX::REveManager::Create(); + auto ps = MakePoints("points", 3); + + EXPECT_EQ(ps->GetBBoxState(), REX::REveElement::kBBoxInvalid); + ASSERT_EQ(ps->AssertBBox(), REX::REveElement::kBBoxSet); + EXPECT_FLOAT_EQ(ps->GetBBoxMax().fZ, 6.f); + + ps->SetNextPoint(10, 20, 30); + ps->StampObjProps(); + EXPECT_EQ(ps->GetBBoxState(), REX::REveElement::kBBoxInvalid); + ASSERT_EQ(ps->AssertBBox(), REX::REveElement::kBBoxSet); + EXPECT_FLOAT_EQ(ps->GetBBoxMax().fZ, 30.f); + + delete ps; +} + +// The box covers the points set, not the capacity: SetNextPoint() doubles it +// with zero-filled slots, and Reset() keeps the old values. +TEST(REveStreaming, PointSetBBoxCoversPointsOnly) +{ + REX::REveManager::Create(); + auto ps = new REX::REvePointSet("points"); + for (int i = 0; i < 3; ++i) + ps->SetNextPoint(10 + i, 20 + i, 30 + i); + + ASSERT_EQ(ps->AssertBBox(), REX::REveElement::kBBoxSet); + EXPECT_FLOAT_EQ(ps->GetBBoxMin().fX, 10.f); + EXPECT_FLOAT_EQ(ps->GetBBoxMin().fZ, 30.f); + + ps->Reset(8); + ps->SetNextPoint(5, 5, 5); + ASSERT_EQ(ps->AssertBBox(), REX::REveElement::kBBoxSet); + EXPECT_FLOAT_EQ(ps->GetBBoxMax().fX, 5.f); + EXPECT_FLOAT_EQ(ps->GetBBoxMin().fX, 5.f); + + delete ps; +} + +// An empty element has no box, so the projection manager's box does not take +// it in, and it sends neither render data nor a box. +TEST(REveStreaming, EmptyElementsHaveNoBox) +{ + REX::REveManager::Create(); + auto ps = new REX::REvePointSet("points"); + auto ls = new REX::REveStraightLineSet("lines"); + + for (REX::REveElement *e : {(REX::REveElement *)ps, (REX::REveElement *)ls}) { + EXPECT_EQ(e->AssertBBox(), REX::REveElement::kBBoxNone); + nlohmann::json j; + EXPECT_EQ(e->WriteCoreJson(j, 0), 0); + EXPECT_FALSE(j.contains("render_data")); + EXPECT_FALSE(j.contains("bbox")); + } + + delete ps; + delete ls; +} + +// Text has no box, and the client is told so. +TEST(REveStreaming, TextNotInWorld) +{ + REX::REveManager::Create(); + auto t = new REX::REveText("label"); + t->SetText("hello"); + + nlohmann::json j; + t->WriteCoreJson(j, 0); + EXPECT_FALSE(j.contains("bbox")); + EXPECT_EQ(j["bbox_not_in_world"], true); + + delete t; +} + +// An REveBox streams its transformation, and its box stays in its own frame. +TEST(REveStreaming, BoxTransAndBBox) +{ + REX::REveManager::Create(); + auto b = new REX::REveBox("box"); + const float h = 0.5f; + const float v[8][3] = {{-h, -h, -h}, {-h, h, -h}, {h, h, -h}, {h, -h, -h}, + {-h, -h, h}, {-h, h, h}, {h, h, h}, {h, -h, h}}; + for (int i = 0; i < 8; ++i) + b->SetVertex(i, v[i][0], v[i][1], v[i][2]); + b->RefMainTrans().SetPos(100, 200, 300); + + nlohmann::json j; + b->WriteCoreJson(j, 0); + ASSERT_TRUE(j.contains("render_data")); + EXPECT_EQ(j["render_data"]["trans_size"], 16); + EXPECT_EQ(j["bbox"], nlohmann::json({-h, -h, -h, h, h, h})); + + delete b; +} + +// A geo shape's box is the TGeoBBox of its shape, about the shape's origin on +// each axis. A tube segment from 0 to 90 degrees has its origin off zero in x +// and y. +TEST(REveStreaming, GeoShapeBBoxAboutOrigin) +{ + REX::REveManager::Create(); + auto gs = new REX::REveGeoShape("tubeseg"); + { + // Register the shape with REve's geo manager, which REveGeoShape deletes + // it from; another manager would keep a dangling pointer to it. + REX::REveGeoManagerHolder gmgr(REX::REveGeoShape::GetGeoManager()); + gs->SetShape(new TGeoTubeSeg(5, 10, 3, 0, 90)); + } + + ASSERT_EQ(gs->AssertBBox(), REX::REveElement::kBBoxSet); + EXPECT_FLOAT_EQ(gs->GetBBoxMin().fX, 0.f); + EXPECT_FLOAT_EQ(gs->GetBBoxMin().fY, 0.f); + EXPECT_FLOAT_EQ(gs->GetBBoxMin().fZ, -3.f); + EXPECT_FLOAT_EQ(gs->GetBBoxMax().fX, 10.f); + EXPECT_FLOAT_EQ(gs->GetBBoxMax().fY, 10.f); + EXPECT_FLOAT_EQ(gs->GetBBoxMax().fZ, 3.f); + + delete gs; +} + +// The box of an instanced box set holds what the client draws: each instance +// at its position, reaching ShapeSize times its scale, or its matrix applied to +// ShapeSize, either way. +TEST(REveStreaming, BoxSetBBoxHoldsInstances) +{ + REX::REveManager::Create(); + auto expectBox = [](REX::REveElement *e, REX::REveVector lo, REX::REveVector hi) { + ASSERT_EQ(e->AssertBBox(), REX::REveElement::kBBoxSet); + for (int k = 0; k < 3; ++k) { + EXPECT_NEAR(e->GetBBoxMin()[k], lo[k], 1e-4) << e->GetCName() << " min " << k; + EXPECT_NEAR(e->GetBBoxMax()[k], hi[k], 1e-4) << e->GetCName() << " max " << k; + } + }; + + auto fixed = new REX::REveBoxSet("fixed"); + fixed->Reset(REX::REveBoxSet::kBT_Instanced, kFALSE, 4); + fixed->SetDefWidth(2); + fixed->AddInstance(10, 10, 10); + expectBox(fixed, {8, 9, 9}, {12, 11, 11}); + + auto scaled = new REX::REveBoxSet("scaled"); + scaled->Reset(REX::REveBoxSet::kBT_InstancedScaled, kFALSE, 4); + scaled->AddInstanceScaled(10, 10, 10, 1, 2, 3); + scaled->AddInstanceScaled(-10, 0, 0, 1, 1, 1); + expectBox(scaled, {-11, -2, -3}, {11, 12, 13}); + + // A cone with its apex at the origin, 60 long along y, radius 5. Its box is + // that of the template cube [-1, 1]^3 under the cone's matrix. + auto cone = new REX::REveBoxSet("cone"); + cone->Reset(REX::REveBoxSet::kBT_InstancedScaledRotated, kFALSE, 4); + cone->AddCone(REX::REveVector(0, 0, 0), REX::REveVector(0, 60, 0), 5); + expectBox(cone, {-5, -60, -5}, {5, 60, 5}); + + delete fixed; + delete scaled; + delete cone; +} + +// An element with no geometry of its own has no render data and sends no box. +namespace { +// Fills its box from no points at all. +class EmptyBoxElement : public REX::REveElement { +public: + EmptyBoxElement() : REX::REveElement("empty") {} + void ComputeBBox() override { BBoxInit(); } +}; +} // namespace + +// BBoxInit() starts from +-infinity: a box no point reached is reported as +// kBBoxNone and not streamed, and a point beyond any finite start sets the box. +TEST(REveStreaming, BBoxFromInfinity) +{ + REX::REveManager::Create(); + + auto e = new EmptyBoxElement; + EXPECT_EQ(e->AssertBBox(), REX::REveElement::kBBoxNone); + + auto ps = new REX::REvePointSet("far"); + ps->SetNextPoint(2e6, -3e6, 4e6); + ASSERT_EQ(ps->AssertBBox(), REX::REveElement::kBBoxSet); + EXPECT_FLOAT_EQ(ps->GetBBoxMin().fX, 2e6); + EXPECT_FLOAT_EQ(ps->GetBBoxMax().fY, -3e6); + EXPECT_FLOAT_EQ(ps->GetBBoxMin().fZ, 4e6); + + delete e; + delete ps; +} + +TEST(REveStreaming, GroupingElementNoBBox) +{ + REX::REveManager::Create(); + auto g = new REX::REveElement("group"); + + nlohmann::json j; + g->WriteCoreJson(j, 0); + EXPECT_FALSE(j.contains("render_data")); + EXPECT_FALSE(j.contains("bbox")); + EXPECT_EQ(g->AssertBBox(), REX::REveElement::kBBoxNone); + + delete g; +} diff --git a/ui5/eve7/controller/Summary.controller.js b/ui5/eve7/controller/Summary.controller.js index 2d4e7ca1ee8f3..d945c0974954f 100644 --- a/ui5/eve7/controller/Summary.controller.js +++ b/ui5/eve7/controller/Summary.controller.js @@ -331,7 +331,7 @@ sap.ui.define([ this.rebuild = true; }, - /** Everything except kCBTransBBox: the tree shows structure and + /** Everything except kCBTrans: the tree shows structure and * properties, and a moving element's transform is neither. See * EveManager.callSceneElementChange. */ changeBitMask: 1 | 4 | 8 | 16, // ColorSelection | ObjProps | Visibility | Added diff --git a/ui5/eve7/lib/EveElementsRCore.js b/ui5/eve7/lib/EveElementsRCore.js index dd1952d329ea3..e036fc3ceff1a 100644 --- a/ui5/eve7/lib/EveElementsRCore.js +++ b/ui5/eve7/lib/EveElementsRCore.js @@ -587,6 +587,16 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function (EveManager) { return mat; } + /** Give `geo` the element's box, `el.bbox` (min, then max, in the + * element's own frame, computed on the server). Does nothing for an + * element without one. */ + RcSetElementBBox(geo, el) { + let bb = el.bbox; + if (bb && bb.length === 6) + geo.setExternalBoundingBox(new RC.Box3(new RC.Vector3(bb[0], bb[1], bb[2]), + new RC.Vector3(bb[3], bb[4], bb[5]))); + } + RcMakeZSprite(colIdx, sSize, nInstance, vbuff, instX, instY, textureName) { let col = RcCol(colIdx); sSize *= this.POINT_SIZE_FAC; @@ -784,9 +794,7 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function (EveManager) { rnr_data.vtxBuff, hit.fTexX, hit.fTexY, txName); - // bounding box - let bb = rnr_data.nrmBuff; - s.geometry.setExternalBoundingBox(new RC.Box3(new RC.Vector3(bb[0], bb[2], bb[4]), new RC.Vector3(bb[1], bb[3], bb[5]))); + this.RcSetElementBBox(s.geometry, hit); this.RcPickable(hit, s); @@ -1045,12 +1053,7 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function (EveManager) { mat._specular = new RC.Color(0.12, 0.12, 0.12); mat._shininess = 24; - // The server's analytic bounding box, sent as JSON in min-then-max - // order rather than in the normals channel that makeBoxSet and makeHit use. - let bb = el.bbox; - if (bb && bb.length >= 6) - geo.setExternalBoundingBox(new RC.Box3(new RC.Vector3(bb[0], bb[1], bb[2]), - new RC.Vector3(bb[3], bb[4], bb[5]))); + this.RcSetElementBBox(geo, el); let mesh = new RC.Mesh(geo, mat); @@ -1373,9 +1376,7 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function (EveManager) { else mesh = this.makeFreeBoxSet(boxset, rnr_data); - // bounding box - let bb = rnr_data.nrmBuff; - mesh.geometry.setExternalBoundingBox(new RC.Box3(new RC.Vector3(bb[0], bb[2], bb[4]), new RC.Vector3(bb[1], bb[3], bb[5]))); + this.RcSetElementBBox(mesh.geometry, boxset); return mesh; } @@ -1711,9 +1712,7 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function (EveManager) { let g = new RC.Geometry(); - // bounding box - let bb = topNode.bbox; - g.setExternalBoundingBox(new RC.Box3(new RC.Vector3(bb[0], bb[2], bb[4]), new RC.Vector3(bb[1], bb[3], bb[5]))); + this.RcSetElementBBox(g, topNode); // let vatt = new RC.BufferAttribute(topNode.shapeVertices, 3); // g.vertices = new RC.BufferAttribute(topNode.shapeVertices, 3); diff --git a/ui5/eve7/lib/EveManager.js b/ui5/eve7/lib/EveManager.js index 2e416e603f54e..ac3ccfe4e8d25 100644 --- a/ui5/eve7/lib/EveManager.js +++ b/ui5/eve7/lib/EveManager.js @@ -25,7 +25,7 @@ sap.ui.define([], function() { this.global_selection_id = null; this.global_highlight_id = null; - this.EChangeBits = { "kCBColorSelection": 1, "kCBTransBBox": 2, "kCBObjProps": 4, "kCBVisibility": 8, "kCBAdded": 16 }; + this.EChangeBits = { "kCBColorSelection": 1, "kCBTrans": 2, "kCBObjProps": 4, "kCBVisibility": 8, "kCBAdded": 16 }; this.controllers = []; this.gl_controllers = []; @@ -233,7 +233,7 @@ sap.ui.define([], function() { } /** Dispatch an element change to the scene's receivers, skipping any whose - * `changeBitMask` excludes its bits. Summary masks out kCBTransBBox, which + * `changeBitMask` excludes its bits. Summary masks out kCBTrans, which * a moving element streams several times a second. */ callSceneElementChange(scene, em) { if ( ! scene.$receivers) return; @@ -455,7 +455,7 @@ sap.ui.define([], function() { delete em.render_data; Object.assign(obj, em); } - if (em.changeBit & this.EChangeBits.kCBTransBBox) { + if (em.changeBit & this.EChangeBits.kCBTrans) { // Transformation update: no render data is streamed. // render_data.matrix is updated here because a later rebuild, // e.g. on a colour change in the same message, reads it. diff --git a/ui5/eve7/lib/EveScene.js b/ui5/eve7/lib/EveScene.js index d608ce0b90ef3..1bd4dba51af1e 100644 --- a/ui5/eve7/lib/EveScene.js +++ b/ui5/eve7/lib/EveScene.js @@ -257,7 +257,7 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { } /** Apply one element of a "Motion" message: the same update as a - * kCBTransBBox change, outside the BeginChanges/EndChanges cycle. */ + * kCBTrans change, outside the BeginChanges/EndChanges cycle. */ sceneElementMotion(msg) { let el = this.mgr.GetElement(msg.fElementId); @@ -346,7 +346,7 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { // Update the transformation in place. When kCBObjProps is also set the // server sends render data instead of a matrix, and the rebuild below // carries the new position. - if (msg.changeBit & this.mgr.EChangeBits.kCBTransBBox) { + if (msg.changeBit & this.mgr.EChangeBits.kCBTrans) { this.updateElementTrans(el, msg); } From 0ff02db3ee64e3804b7ae66b11d0c1ccecf932ec Mon Sep 17 00:00:00 2001 From: Matevz Tadel Date: Fri, 16 Oct 2026 22:38:31 -0700 Subject: [PATCH 3/7] [eve7] Project through the ancestors' matrices; stream depth and name changes Projections used only the model's own matrix, while the 3D view composes those of its ancestors. Depth and name changes did not reach the client. - Added REveProjectionManager::GetModelTrans(), the model's matrix chain in the manager's frame, and used it in all projected classes. - REveGeoShape's buffer stays in the shape's frame; REveJetConeProjected projects the apex. - Import warns when the receiver's frame differs from the manager's. - SetDepthCommon() stamps kCBObjProps. - Renamed kCBColorSelection to kCBCoreJson. Name and title changes stamp it; EveScene then calls refresh(el) of the drawing or rebuilds it. - Added REveElement::ComposeSceneTrans(), the matrices of an element and its mothers up to its scene, and AuntChanged(), which an aunt calls on its nieces. The projection manager uses both; REveProjectionAxis follows it through AuntChanged() after every import and reprojection. - Added gtests. Assisted-by: Claude:Opus 5.5 --- graf3d/eve7/inc/ROOT/REveElement.hxx | 12 +- graf3d/eve7/inc/ROOT/REveProjectionAxis.hxx | 1 + .../eve7/inc/ROOT/REveProjectionManager.hxx | 2 + graf3d/eve7/src/REveBox.cxx | 4 +- graf3d/eve7/src/REveDigitSet.cxx | 2 +- graf3d/eve7/src/REveElement.cxx | 29 +- graf3d/eve7/src/REveEllipsoid.cxx | 6 +- graf3d/eve7/src/REveGeoShape.cxx | 17 +- graf3d/eve7/src/REveJetCone.cxx | 34 ++- graf3d/eve7/src/REveLine.cxx | 4 +- graf3d/eve7/src/REvePointSet.cxx | 3 +- graf3d/eve7/src/REvePolygonSetProjected.cxx | 10 +- graf3d/eve7/src/REveProjectionAxis.cxx | 12 + graf3d/eve7/src/REveProjectionBases.cxx | 11 +- graf3d/eve7/src/REveProjectionManager.cxx | 125 +++++++- graf3d/eve7/src/REveScene.cxx | 5 +- graf3d/eve7/src/REveStraightLineSet.cxx | 3 +- graf3d/eve7/src/REveTrackProjected.cxx | 3 +- graf3d/eve7/test/change_cycle.cxx | 24 ++ graf3d/eve7/test/projection.cxx | 266 ++++++++++++++++++ .../visualisation/eve7/projection_axes.C | 8 - ui5/eve7/controller/Summary.controller.js | 8 +- ui5/eve7/lib/EveManager.js | 11 +- ui5/eve7/lib/EveScene.js | 15 +- 24 files changed, 532 insertions(+), 83 deletions(-) diff --git a/graf3d/eve7/inc/ROOT/REveElement.hxx b/graf3d/eve7/inc/ROOT/REveElement.hxx index 94efc94f02206..d7b276c9cf125 100644 --- a/graf3d/eve7/inc/ROOT/REveElement.hxx +++ b/graf3d/eve7/inc/ROOT/REveElement.hxx @@ -188,6 +188,8 @@ public: virtual void AddAunt(REveAunt *au); virtual void RemoveAunt(REveAunt *au); + /// Called by an aunt that holds this element when the aunt has changed. + virtual void AuntChanged(REveAunt * /*au*/) {} virtual void CheckReferenceCount(const std::string &from = ""); AuntList_t &RefAunts() { return fAunts; } @@ -288,6 +290,7 @@ public: virtual void SetTransMatrix(Double_t *carr); virtual void SetTransMatrix(const TGeoMatrix &mat); + Int_t ComposeSceneTrans(REveTrans &t) const; // clang-format off virtual Int_t WriteCoreJson(nlohmann::json &cj, Int_t rnr_offset); @@ -367,14 +370,13 @@ public: // Change-stamping and change bits //--------------------------------- - // kCBTrans streams the transformation as JSON only, and the client applies - // it to the object it already has. A change that invalidates geometry must - // stamp kCBObjProps, which also invalidates the bounding box. + // A change that invalidates geometry must stamp kCBObjProps, which also + // invalidates the bounding box. kCBCoreJson and kCBTrans send no render data. // clang-format off enum EChangeBits { - kCBColorSelection = BIT(0), // Main color or select/hilite state changed. + kCBCoreJson = BIT(0), // Core JSON changed (colour, name, ...); render data did not. kCBTrans = BIT(1), // Transformation changed; cheap update, no rebuild. kCBObjProps = BIT(2), // Object changed, requires dropping its display-lists. kCBVisibility = BIT(3), // Rendering of self/children changed. @@ -398,7 +400,7 @@ protected: static void AppendMirErrorString(std::string_view err_str); public: - void StampColorSelection() { AddStamp(kCBColorSelection); } + void StampCoreJson() { AddStamp(kCBCoreJson); } void StampTrans() { AddStamp(kCBTrans); } void StampObjProps() { diff --git a/graf3d/eve7/inc/ROOT/REveProjectionAxis.hxx b/graf3d/eve7/inc/ROOT/REveProjectionAxis.hxx index 6892b840d2d72..88b72b170599b 100644 --- a/graf3d/eve7/inc/ROOT/REveProjectionAxis.hxx +++ b/graf3d/eve7/inc/ROOT/REveProjectionAxis.hxx @@ -108,6 +108,7 @@ public: void SetDistortionLabel(REveText *t); void RemoveAunt(REveAunt *au) override; + void AuntChanged(REveAunt *au) override; void UpdateDistortionLabel(); Int_t WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) override; diff --git a/graf3d/eve7/inc/ROOT/REveProjectionManager.hxx b/graf3d/eve7/inc/ROOT/REveProjectionManager.hxx index 912949149b7e7..059e1928aa996 100644 --- a/graf3d/eve7/inc/ROOT/REveProjectionManager.hxx +++ b/graf3d/eve7/inc/ROOT/REveProjectionManager.hxx @@ -74,6 +74,8 @@ public: virtual void ProjectChildren(); virtual void ProjectChildrenRecurse(REveElement *el); + const REveTrans *GetModelTrans(REveElement *model, REveTrans &t); + void ComputeBBox() override; }; diff --git a/graf3d/eve7/src/REveBox.cxx b/graf3d/eve7/src/REveBox.cxx index f2172914acc7b..52316da940080 100644 --- a/graf3d/eve7/src/REveBox.cxx +++ b/graf3d/eve7/src/REveBox.cxx @@ -12,6 +12,7 @@ #include #include #include +#include #include "TClass.h" @@ -198,7 +199,8 @@ void REveBoxProjected::UpdateProjection() vVector2_t pp[2]; { REveProjection *projection = fManager->GetProjection(); - REveTrans *trans = box->PtrMainTrans(kFALSE); + REveTrans mt; + const REveTrans *trans = fManager->GetModelTrans(box, mt); REveVector pbuf; for (Int_t i = 0; i < 8; ++i) diff --git a/graf3d/eve7/src/REveDigitSet.cxx b/graf3d/eve7/src/REveDigitSet.cxx index 86e628a6ff5fb..3c5e85e61ceed 100644 --- a/graf3d/eve7/src/REveDigitSet.cxx +++ b/graf3d/eve7/src/REveDigitSet.cxx @@ -176,7 +176,7 @@ void REveDigitSet::SetMainColor(Color_t color) else if (fFrame) { fFrame->SetFrameColor(color); - fFrame->StampBackPtrElements(kCBColorSelection); + fFrame->StampBackPtrElements(kCBCoreJson); } } diff --git a/graf3d/eve7/src/REveElement.cxx b/graf3d/eve7/src/REveElement.cxx index eb91751dd2cea..7dcf4014ce51f 100644 --- a/graf3d/eve7/src/REveElement.cxx +++ b/graf3d/eve7/src/REveElement.cxx @@ -290,12 +290,12 @@ void REveElement::SetNameTitle(const std::string& name, const std::string& title //////////////////////////////////////////////////////////////////////////////// /// Virtual function called when a name or title of the element has -/// been changed. +/// been changed. Stamps kCBCoreJson, which streams both to the client. /// If you override this, call also the version of your direct base-class. void REveElement::NameTitleChanged() { - // Should send out some message. Need a new stamp type? + StampCoreJson(); } //////////////////////////////////////////////////////////////////////////////// @@ -408,7 +408,7 @@ void REveElement::CopyVizParams(const REveElement* el) fMainTransparency = el->fMainTransparency; if (fMainColorPtr == & fDefaultColor) fDefaultColor = el->GetMainColor(); - AddStamp(kCBColorSelection | kCBObjProps); + AddStamp(kCBCoreJson | kCBObjProps); } //////////////////////////////////////////////////////////////////////////////// @@ -731,7 +731,7 @@ void REveElement::SetMainColor(Color_t color) if (fMainColorPtr) { *fMainColorPtr = color; - StampColorSelection(); + StampCoreJson(); } PropagateMainColorToProjecteds(color, old_color); @@ -783,7 +783,7 @@ void REveElement::SetMainTransparency(Char_t t) if (t > 100) t = 100; fMainTransparency = t; - StampColorSelection(); + StampCoreJson(); PropagateMainTransparencyToProjecteds(t, old_t); } @@ -877,6 +877,25 @@ void REveElement::SetTransMatrix(const TGeoMatrix& mat) StampTrans(); } +//////////////////////////////////////////////////////////////////////////////// +/// Compose into `t` the matrices of this element and of its mothers up to, +/// not including, its REveScene: the frame the client draws the element in. +/// Returns the number of matrices; `t` is set only when it is non-zero. + +Int_t REveElement::ComposeSceneTrans(REveTrans &t) const +{ + Int_t n = 0; + for (const REveElement *el = this; el && !dynamic_cast(el); el = el->fMother) { + if (const REveTrans *m = el->fMainTrans.get()) { + if (n++ == 0) + t = *m; + else + t.MultLeft(*m); + } + } + return n; +} + //////////////////////////////////////////////////////////////////////////////// /// Check if el can be added to this element. /// diff --git a/graf3d/eve7/src/REveEllipsoid.cxx b/graf3d/eve7/src/REveEllipsoid.cxx index 56d9f4a5772a8..ff3c1b46177d0 100644 --- a/graf3d/eve7/src/REveEllipsoid.cxx +++ b/graf3d/eve7/src/REveEllipsoid.cxx @@ -147,7 +147,8 @@ void REveEllipsoidProjected::DrawArchProjected(float phiStart, float phiEnd, flo float REveEllipsoidProjected::GetEllipseSurface (const REveVector& v1, const REveVector& v2) { REveEllipsoid& orig = * dynamic_cast(fProjectable); - REveTrans * trans = orig.PtrMainTrans(kFALSE); + REveTrans mt; + const REveTrans *trans = fManager->GetModelTrans(&orig, mt); REveProjection& proj = * fManager->GetProjection(); // project center of ellipse @@ -244,7 +245,8 @@ void REveEllipsoidProjected::UpdateProjection() REveProjection& proj = * fManager->GetProjection(); REveEllipsoid& orig = * dynamic_cast(fProjectable); - REveTrans *trans = orig.PtrMainTrans(kFALSE); + REveTrans mt; + const REveTrans *trans = fManager->GetModelTrans(&orig, mt); // Lines Int_t num_lines = (int)fArchPnts.size(); diff --git a/graf3d/eve7/src/REveGeoShape.cxx b/graf3d/eve7/src/REveGeoShape.cxx index 9d7c7893c0091..dc6ea746acfe9 100644 --- a/graf3d/eve7/src/REveGeoShape.cxx +++ b/graf3d/eve7/src/REveGeoShape.cxx @@ -367,8 +367,9 @@ TClass *REveGeoShape::ProjectedClass(const REveProjection* p) const } //////////////////////////////////////////////////////////////////////////////// -/// Create a TBuffer3D suitable for presentation of the shape. -/// Transformation matrix is also applied. +/// Create a TBuffer3D suitable for presentation of the shape, in the shape's +/// own frame. The projected classes apply the shape's matrix and those of its +/// ancestors, through REveProjectionManager::GetModelTrans(). std::unique_ptr REveGeoShape::MakeBuffer3D() { @@ -384,14 +385,6 @@ std::unique_ptr REveGeoShape::MakeBuffer3D() REveGeoManagerHolder gmgr(fgGeoManager, fNSegments); buff.reset(fShape->MakeBuffer3D()); - REveTrans &mx = RefMainTrans(); - if (mx.GetUseTrans()) { - Int_t n = buff->NbPnts(); - Double_t *pnts = buff->fPnts; - for(Int_t k = 0; k < n; ++k) { - mx.MultiplyIP(&pnts[3*k]); - } - } return buff; } @@ -460,10 +453,12 @@ void REveGeoShapeProjected::UpdateProjection() { fBuff->SetSectionsValid(TBuffer3D::kCore | TBuffer3D::kRawSizes | TBuffer3D::kRaw); + REveTrans mt; + const REveTrans *trans = fManager->GetModelTrans(gre, mt); Double_t *p = fBuff->fPnts; for (UInt_t i = 0; i < fBuff->NbPnts(); ++i, p+=3) { - prj->ProjectPointdv(p, 0); + prj->ProjectPointdv(trans, p, p, 0); } } diff --git a/graf3d/eve7/src/REveJetCone.cxx b/graf3d/eve7/src/REveJetCone.cxx index f9224d23ecedb..93ee20f4f756a 100644 --- a/graf3d/eve7/src/REveJetCone.cxx +++ b/graf3d/eve7/src/REveJetCone.cxx @@ -264,6 +264,8 @@ void REveJetConeProjected::BuildRenderData() REveProjection *P = GetManager()->GetProjection(); REveJetCone *C = dynamic_cast(GetProjectable()); + REveTrans mt; + const REveTrans *T = GetManager()->GetModelTrans(C, mt); if (P->GetType() == REveProjection::kPT_RPhi || P->GetType() == REveProjection::kPT_RhoZ) { @@ -280,7 +282,7 @@ void REveJetConeProjected::BuildRenderData() V[2] = C->CalcBaseVec(TMath::PiOver2()); for (Int_t i = 0; i < 3; ++i) - P->ProjectVector(V[i], fDepth); + P->ProjectVector(T, V[i], fDepth); } else // RhoZ { @@ -304,7 +306,7 @@ void REveJetConeProjected::BuildRenderData() } for (auto &v : V) - P->ProjectVector(v, fDepth); + P->ProjectVector(T, v, fDepth); std::sort(V.begin() + 1, V.end(), [](const auto &a, const auto &b) -> bool { return a.Phi() < b.Phi(); }); } @@ -318,13 +320,15 @@ void REveJetConeProjected::BuildRenderData() fRenderData = std::make_unique("makeJet", 3 * NP); - fRenderData->PushV(C->fApex); + REveVector apex = C->fApex; + P->ProjectVector(T, apex, fDepth); + fRenderData->PushV(apex); Float_t angle_step = TMath::TwoPi() / C->fNDiv; Float_t angle = 0; for (Int_t i = 0; i < C->fNDiv; ++i, angle += angle_step) { auto tv = C->CalcBaseVec(angle); - P->ProjectVector(tv, fDepth); + P->ProjectVector(T, tv, fDepth); fRenderData->PushV(tv); } } @@ -339,12 +343,24 @@ void REveJetConeProjected::ComputeBBox() REveJetCone *cone = dynamic_cast(fProjectable); REveProjection *proj = GetManager()->GetProjection(); + REveTrans mt; + const REveTrans *T = GetManager()->GetModelTrans(cone, mt); REveVector v; - v = cone->fApex; proj->ProjectVector(v, fDepth); BBoxCheckPoint(v); - v = cone->CalcBaseVec(0); proj->ProjectVector(v, fDepth); BBoxCheckPoint(v); - v = cone->CalcBaseVec(TMath::PiOver2()); proj->ProjectVector(v, fDepth); BBoxCheckPoint(v); - v = cone->CalcBaseVec(TMath::Pi()); proj->ProjectVector(v, fDepth); BBoxCheckPoint(v); - v = cone->CalcBaseVec(TMath::Pi() + TMath::PiOver2()); proj->ProjectVector(v, fDepth); BBoxCheckPoint(v); + v = cone->fApex; + proj->ProjectVector(T, v, fDepth); + BBoxCheckPoint(v); + v = cone->CalcBaseVec(0); + proj->ProjectVector(T, v, fDepth); + BBoxCheckPoint(v); + v = cone->CalcBaseVec(TMath::PiOver2()); + proj->ProjectVector(T, v, fDepth); + BBoxCheckPoint(v); + v = cone->CalcBaseVec(TMath::Pi()); + proj->ProjectVector(T, v, fDepth); + BBoxCheckPoint(v); + v = cone->CalcBaseVec(TMath::Pi() + TMath::PiOver2()); + proj->ProjectVector(T, v, fDepth); + BBoxCheckPoint(v); } //////////////////////////////////////////////////////////////////////////////// diff --git a/graf3d/eve7/src/REveLine.cxx b/graf3d/eve7/src/REveLine.cxx index 72cd806f55241..9f20b64315db9 100644 --- a/graf3d/eve7/src/REveLine.cxx +++ b/graf3d/eve7/src/REveLine.cxx @@ -12,6 +12,7 @@ #include #include #include +#include #include "TClass.h" @@ -366,7 +367,8 @@ void REveLineProjected::UpdateProjection() { REveProjection& proj = * fManager->GetProjection(); REveLine & als = * dynamic_cast(fProjectable); - REveTrans *tr = als.PtrMainTrans(kFALSE); + REveTrans mt; + const REveTrans *tr = fManager->GetModelTrans(&als, mt); Int_t n = als.GetSize(); Reset(n); diff --git a/graf3d/eve7/src/REvePointSet.cxx b/graf3d/eve7/src/REvePointSet.cxx index 5052eceaef1bf..6788a27c84dff 100644 --- a/graf3d/eve7/src/REvePointSet.cxx +++ b/graf3d/eve7/src/REvePointSet.cxx @@ -557,7 +557,8 @@ void REvePointSetProjected::UpdateProjection() { REveProjection &proj = * fManager->GetProjection(); REvePointSet &ps = * dynamic_cast(fProjectable); - REveTrans *tr = ps.PtrMainTrans(kFALSE); + REveTrans mt; + const REveTrans *tr = fManager->GetModelTrans(&ps, mt); fAlwaysSecSelect = ps.GetAlwaysSecSelect(); diff --git a/graf3d/eve7/src/REvePolygonSetProjected.cxx b/graf3d/eve7/src/REvePolygonSetProjected.cxx index bd39c23a0770a..ec116ca7484a3 100644 --- a/graf3d/eve7/src/REvePolygonSetProjected.cxx +++ b/graf3d/eve7/src/REvePolygonSetProjected.cxx @@ -14,6 +14,7 @@ #include #include #include +#include #include "TBuffer3D.h" #include "TBuffer3DTypes.h" @@ -217,11 +218,18 @@ std::vector REvePolygonSetProjected::ProjectAndReducePoints() { REveProjection* projection = fManager->GetProjection(); + // The buffer is in the model's frame. + REveTrans mt; + const REveTrans *trans = fManager->GetModelTrans(dynamic_cast(fProjectable), mt); + Int_t buffN = fBuff->NbPnts(); std::vector pnts; pnts.resize(buffN); for (Int_t i = 0; i < buffN; ++i) { - pnts[i].Set(fBuff->fPnts[3*i],fBuff->fPnts[3*i+1], fBuff->fPnts[3*i+2]); + Double_t p[3] = {fBuff->fPnts[3 * i], fBuff->fPnts[3 * i + 1], fBuff->fPnts[3 * i + 2]}; + if (trans) + trans->MultiplyIP(p); + pnts[i].Set(p[0], p[1], p[2]); projection->ProjectPoint(pnts[i].fX, pnts[i].fY, pnts[i].fZ, 0, REveProjection::kPP_Plane); } diff --git a/graf3d/eve7/src/REveProjectionAxis.cxx b/graf3d/eve7/src/REveProjectionAxis.cxx index 671839ec1dae7..2f60e970a3c3c 100644 --- a/graf3d/eve7/src/REveProjectionAxis.cxx +++ b/graf3d/eve7/src/REveProjectionAxis.cxx @@ -281,6 +281,18 @@ void REveProjectionAxis::RemoveAunt(REveAunt *au) REveText::RemoveAunt(au); } +//////////////////////////////////////////////////////////////////////////////// +/// Follow the projection manager: its box and distortion set the ticks and the +/// distortion read-out. + +void REveProjectionAxis::AuntChanged(REveAunt *au) +{ + if (au != fManager) + return; + UpdateTicks(); + UpdateDistortionLabel(); +} + //////////////////////////////////////////////////////////////////////////////// /// Rewrite the read-out. Shown as distortion * 1000, the same scaling /// REveProjectionManager uses in its own name. diff --git a/graf3d/eve7/src/REveProjectionBases.cxx b/graf3d/eve7/src/REveProjectionBases.cxx index 4a4298c4e850b..1d87ad6b735af 100644 --- a/graf3d/eve7/src/REveProjectionBases.cxx +++ b/graf3d/eve7/src/REveProjectionBases.cxx @@ -134,7 +134,7 @@ void REveProjectable::PropagateMainColor(Color_t color, Color_t old_color) auto p_as_el = proj->GetProjectedAsElement(); if (p_as_el->GetMainColor() == old_color) { p_as_el->SetMainColor(color); - p_as_el->StampColorSelection(); + p_as_el->StampCoreJson(); } } } @@ -149,7 +149,7 @@ void REveProjectable::PropagateMainTransparency(Char_t t, Char_t old_t) auto p_as_el = proj->GetProjectedAsElement(); if (p_as_el->GetMainTransparency() == old_t) { p_as_el->SetMainTransparency(t); - p_as_el->StampColorSelection(); + p_as_el->StampCoreJson(); } } } @@ -224,14 +224,13 @@ void REveProjected::SetDepth(Float_t d) } //////////////////////////////////////////////////////////////////////////////// -/// Set the depth and invalidate the bounding box of `el`, whose vertices the -/// caller moves to the new depth. +/// Set the depth and stamp `el` for a rebuild. The caller moves the vertices of +/// `el` to the new depth, which changes its render data and its bounding box. void REveProjected::SetDepthCommon(Float_t d, REveElement *el) { fDepth = d; - el->ResetBBox(); - el->StampTrans(); + el->StampObjProps(); } //////////////////////////////////////////////////////////////////////////////// diff --git a/graf3d/eve7/src/REveProjectionManager.cxx b/graf3d/eve7/src/REveProjectionManager.cxx index 453bad66f466b..6453d2695d898 100644 --- a/graf3d/eve7/src/REveProjectionManager.cxx +++ b/graf3d/eve7/src/REveProjectionManager.cxx @@ -10,16 +10,20 @@ *************************************************************************/ #include -#include #include #include #include +#include +#include #include "TBuffer3D.h" #include "TBuffer3DTypes.h" #include "TClass.h" +#include +#include +#include #include #include @@ -36,6 +40,66 @@ and updates projected scene accordingly. using namespace ROOT::Experimental; namespace REX = ROOT::Experimental; +namespace { + +//////////////////////////////////////////////////////////////////////////////// +/// Warn if `el` has a transformation other than identity. The vertices of a +/// projected element are final, so the client must not transform them again. +/// Identity is allowed: REveStraightLineSet's constructor sets one. + +void WarnIfTransformed(REveElement *el) +{ + static const REveTrans unit; + if (el->HasMainTrans() && std::memcmp(el->RefMainTrans().Array(), unit.Array(), 16 * sizeof(Double_t)) != 0) { + ::Warning("REveProjectionManager::ImportElements", + "'%s' has a transformation, which the client applies to its projected vertices again.", el->GetCName()); + } +} + +//////////////////////////////////////////////////////////////////////////////// +/// Warn if projected elements put under `receiver` are drawn in a frame other +/// than that of `mng`. GetModelTrans() projects into the frame of the manager, +/// and the client places the projected elements with the matrices of +/// `receiver` and of its mothers, so the two products must be equal. + +void WarnIfFrameDiffers(REveProjectionManager *mng, REveElement *receiver) +{ + REveTrans wm, wr; + const int nm = mng->ComposeSceneTrans(wm); + const int nr = receiver->ComposeSceneTrans(wr); + if (nm == 0 && nr == 0) + return; + + // Tolerance for the rounding of the inversion, relative to the translations. + Double_t scale = 1; + for (int i = 12; i < 15; ++i) + scale = std::max({scale, std::abs(wm.Array()[i]), std::abs(wr.Array()[i])}); + + // d = inverse(W(manager)) * W(receiver), the identity when the frames agree. + REveTrans d; + if (nm > 0) { + wm.Invert(); + d = wm; + if (nr > 0) + d.MultRight(wr); + } else { + d = wr; + } + + static const REveTrans unit; + for (int i = 0; i < 16; ++i) { + if (std::abs(d.Array()[i] - unit.Array()[i]) > 1e-6 * scale) { + ::Warning("REveProjectionManager::ImportElements", + "'%s' places projected elements in a frame other than that of '%s', in which they are " + "projected; the client draws them transformed.", + receiver->GetCName(), mng->GetCName()); + return; + } + } +} + +} // namespace + //////////////////////////////////////////////////////////////////////////////// /// Constructor. @@ -84,7 +148,8 @@ void REveProjectionManager::RemoveDependent(REveElement *el) //////////////////////////////////////////////////////////////////////////////// /// Add `steps` * 1e-4 to the distortion of the current projection, clamped at -/// zero, reproject, and refresh the REveProjectionAxis nieces. Called by MIR. +/// zero, and reproject, which also refreshes the REveProjectionAxis nieces. +/// Called by MIR. void REveProjectionManager::BumpDistortion(Int_t steps) { @@ -98,14 +163,6 @@ void REveProjectionManager::BumpDistortion(Int_t steps) UpdateName(); ProjectChildren(); - - // Refresh the axes among the nieces. - for (auto &n : fNieces) { - if (auto ax = dynamic_cast(n)) { - ax->UpdateTicks(); - ax->UpdateDistortionLabel(); - } - } } void REveProjectionManager::UpdateName() @@ -209,12 +266,17 @@ Bool_t REveProjectionManager::ShouldImport(REveElement *el) } //////////////////////////////////////////////////////////////////////////////// -/// Update dependent elements' bounding boxes. +/// Update dependent elements' bounding boxes, and tell the nieces, such as an +/// REveProjectionAxis, that the manager changed. Called after every import and +/// reprojection. void REveProjectionManager::UpdateDependentElements(REveElement * /*root*/) { for (auto &d : fDependentEls) d->ComputeBBox(); + + for (auto &n : fNieces) + n->AuntChanged(this); } //////////////////////////////////////////////////////////////////////////////// @@ -254,6 +316,7 @@ REveElement* REveProjectionManager::ImportElementsRecurse(REveElement* el, new_el->SetPickable (el->IsPickable()); parent->AddElement(new_el); + WarnIfTransformed(new_el); REveCompound *cmpnd = dynamic_cast(el); REveCompound *cmpnd_pr = dynamic_cast(new_el); @@ -282,13 +345,13 @@ REveElement* REveProjectionManager::ImportElementsRecurse(REveElement* el, REveElement* REveProjectionManager::ImportElements(REveElement* el, REveElement* ext_list) { + WarnIfFrameDiffers(this, ext_list ? ext_list : this); REveElement* new_el = ImportElementsRecurse(el, ext_list ? ext_list : this); if (new_el) { AssertBBox(); ProjectChildrenRecurse(new_el); AssertBBoxExtents(0.1); - StampTrans(); UpdateDependentElements(new_el); @@ -314,13 +377,13 @@ REveElement* REveProjectionManager::ImportElements(REveElement* el, REveElement* REveProjectionManager::SubImportElements(REveElement* el, REveElement* proj_parent) { + WarnIfFrameDiffers(this, proj_parent); REveElement* new_el = ImportElementsRecurse(el, proj_parent); if (new_el) { AssertBBox(); ProjectChildrenRecurse(new_el); AssertBBoxExtents(0.1); - StampTrans(); UpdateDependentElements(new_el); } @@ -342,6 +405,7 @@ REveElement* REveProjectionManager::SubImportElements(REveElement* el, Int_t REveProjectionManager::SubImportChildren(REveElement* el, REveElement* proj_parent) { + WarnIfFrameDiffers(this, proj_parent); List_t new_els; for (auto &c: el->RefChildren()) { auto new_el = ImportElementsRecurse(c, proj_parent); @@ -355,7 +419,6 @@ Int_t REveProjectionManager::SubImportChildren(REveElement* el, REveElement* pro for (auto &nel: new_els) ProjectChildrenRecurse(nel); AssertBBoxExtents(0.1); - StampTrans(); UpdateDependentElements(proj_parent); } @@ -396,11 +459,43 @@ void REveProjectionManager::ProjectChildren() for (auto &n : fNieces) ProjectChildrenRecurse(n); AssertBBoxExtents(0.1); - StampTrans(); UpdateDependentElements(this); } +//////////////////////////////////////////////////////////////////////////////// +/// Return the matrix that takes the coordinates of `model` into the frame of +/// this manager, in which the projection is computed: +/// inverse(W(this)) * W(model), with W(x) from x->ComposeSceneTrans(), as the +/// client composes them for the 3D view. When the two chains do not meet, their +/// tops are taken to be in the same frame. +/// +/// Returns nullptr when neither chain has a matrix, and the model's own matrix +/// when it is the only one. Otherwise fills `t` and returns it, so `t` must +/// outlive the result. + +const REveTrans *REveProjectionManager::GetModelTrans(REveElement *model, REveTrans &t) +{ + REveTrans wm; + const int nm = ComposeSceneTrans(wm); + const int n = model->ComposeSceneTrans(t); + + if (nm == 0) { + if (n == 0) + return nullptr; + if (n == 1 && model->PtrMainTrans(kFALSE)) + return model->PtrMainTrans(kFALSE); + return &t; + } + + wm.Invert(); + if (n == 0) + t = wm; + else + t.MultLeft(wm); + return &t; +} + //////////////////////////////////////////////////////////////////////////////// /// Fill the bounding box (virtual from REveElement) with the union of the /// boxes of the projected elements below the children and the nieces. diff --git a/graf3d/eve7/src/REveScene.cxx b/graf3d/eve7/src/REveScene.cxx index b2382a0690eaf..6bc97059be02f 100644 --- a/graf3d/eve7/src/REveScene.cxx +++ b/graf3d/eve7/src/REveScene.cxx @@ -315,9 +315,8 @@ void REveScene::StreamRepresentationChanges() jobj["fRnrChildren"] = el->GetRnrChildren(); } - if (bits & kCBColorSelection) - { - el->WriteCoreJson(jobj, -1); + if (bits & kCBCoreJson) { + el->WriteCoreJson(jobj, -1); } if (bits & kCBTrans) { diff --git a/graf3d/eve7/src/REveStraightLineSet.cxx b/graf3d/eve7/src/REveStraightLineSet.cxx index 29e8ff4785178..f16f07ecb0e4e 100644 --- a/graf3d/eve7/src/REveStraightLineSet.cxx +++ b/graf3d/eve7/src/REveStraightLineSet.cxx @@ -304,7 +304,8 @@ void REveStraightLineSetProjected::UpdateProjection() REveProjection& proj = * fManager->GetProjection(); REveStraightLineSet& orig = * dynamic_cast(fProjectable); - REveTrans *trans = orig.PtrMainTrans(kFALSE); + REveTrans mt; + const REveTrans *trans = fManager->GetModelTrans(&orig, mt); BBoxClear(); diff --git a/graf3d/eve7/src/REveTrackProjected.cxx b/graf3d/eve7/src/REveTrackProjected.cxx index 8385947605ad6..b61713f5d56a0 100644 --- a/graf3d/eve7/src/REveTrackProjected.cxx +++ b/graf3d/eve7/src/REveTrackProjected.cxx @@ -115,7 +115,8 @@ Int_t REveTrackProjected::GetBreakPointIdx(Int_t start) void REveTrackProjected::MakeTrack(Bool_t recurse) { REveTrack *otrack = dynamic_cast(fProjectable); - REveTrans *trans = otrack->PtrMainTrans(kFALSE); + REveTrans mt; + const REveTrans *trans = fManager->GetModelTrans(otrack, mt); REveProjection *projection = fManager->GetProjection(); fBreakPoints.clear(); diff --git a/graf3d/eve7/test/change_cycle.cxx b/graf3d/eve7/test/change_cycle.cxx index 527f210e49f56..b351d6aa6dbdc 100644 --- a/graf3d/eve7/test/change_cycle.cxx +++ b/graf3d/eve7/test/change_cycle.cxx @@ -125,6 +125,30 @@ TEST(REveChangeCycle, AddedElementIsStreamed) EXPECT_EQ((int)out.fBinarySize, bin); } +// A name change stamps kCBCoreJson, which streams the new name without +// render data. +TEST(REveChangeCycle, NameStreamsCoreJson) +{ + auto eve = REX::REveManager::Create(); + auto s = NewScene("cc_name"); + auto ps = MakePoints("points", 3); + s->AddElement(ps); + + eve->BeginChange(); + ps->SetName("renamed"); + EXPECT_EQ(ps->GetChangeBits(), REX::REveElement::kCBCoreJson); + auto out = EndChangeKeepingOutput(s); + + auto &j = out.fJson; + ASSERT_EQ(j["arr"].size(), 1u); + auto &e = j["arr"][0]; + EXPECT_EQ(e["fElementId"], ps->GetElementId()); + EXPECT_EQ(e["changeBit"], REX::REveElement::kCBCoreJson); + EXPECT_EQ(e["fName"], "renamed"); + EXPECT_FALSE(e.contains("render_data")); + EXPECT_EQ(out.fBinarySize, 0u); +} + // The id reported as removed is the removed element's, not its mother's. TEST(REveChangeCycle, RemovedElementIsReported) { diff --git a/graf3d/eve7/test/projection.cxx b/graf3d/eve7/test/projection.cxx index 644f797d1741a..3f330e6e58b97 100644 --- a/graf3d/eve7/test/projection.cxx +++ b/graf3d/eve7/test/projection.cxx @@ -10,14 +10,29 @@ #include "gtest/gtest.h" +#include + #include #include +#include +#include +#include +#include +#include +#include +#include #include +#include #include #include #include #include +#include "TGeoBBox.h" + +#include +#include + namespace REX = ROOT::Experimental; TEST(REveProjection, RPhiWithoutDistortion) @@ -125,3 +140,254 @@ TEST(REveProjection, ImportAndUpdate) EXPECT_FLOAT_EQ(pr->RefPoint(0).fX, 4.f); EXPECT_FLOAT_EQ(pr->RefPoint(0).fY, -3.f); } + +// A depth change moves the projected vertices, so it stamps kCBObjProps and +// the client gets the new geometry and box. A REveLine also covers the import +// of a line, whose depth is set before it has points. +TEST(REveProjection, DepthStampsObjProps) +{ + auto eve = REX::REveManager::Create(); + auto src = eve->SpawnNewScene("depth_src"); + auto dst = eve->SpawnNewScene("depth_dst"); + auto win = eve->GetWebWindow(); + dst->AddSubscriber(std::make_unique(999999u, win)); + + auto line = new REX::REveLine("model"); + line->SetNextPoint(3, 4, 5); + line->SetNextPoint(-6, 8, -1); + src->AddElement(line); + + auto mng = new REX::REveProjectionManager(REX::REveProjection::kPT_RPhi); + auto pr = dynamic_cast(mng->ImportElements(line, dst)); + ASSERT_NE(pr, nullptr); + ASSERT_EQ(pr->GetSize(), 2); + + eve->BeginChange(); + dynamic_cast(pr)->SetDepth(-7); + EXPECT_TRUE(pr->GetChangeBits() & REX::REveElement::kCBObjProps); + EXPECT_FLOAT_EQ(pr->RefPoint(0).fZ, -7.f); + EXPECT_FLOAT_EQ(pr->RefPoint(1).fZ, -7.f); + ASSERT_EQ(pr->AssertBBox(), REX::REveElement::kBBoxSet); + EXPECT_FLOAT_EQ(pr->GetBBoxMin().fZ, -7.f); + EXPECT_FLOAT_EQ(pr->GetBBoxMax().fZ, -7.f); + eve->EndChange(); +} + +// Projected vertices are in the frame of the manager, so import warns when +// they are placed under an element whose matrices differ from the manager's. +// The identity that REveStraightLineSet's constructor sets does not warn; an +// unexpected warning fails the test. +TEST(REveProjection, ImportWarnsAboutTransformation) +{ + auto eve = REX::REveManager::Create(); + auto src = eve->SpawnNewScene("trans_src"); + auto dst = eve->SpawnNewScene("trans_dst"); + + auto ls = new REX::REveStraightLineSet("model"); + ls->AddLine(0, 0, 0, 10, 10, 10); + src->AddElement(ls); + + auto mng = new REX::REveProjectionManager(REX::REveProjection::kPT_RPhi); + ASSERT_NE(mng->ImportElements(ls, dst), nullptr); + + auto holder = new REX::REveElement("holder"); + holder->RefMainTrans().SetPos(100, 0, 0); + dst->AddElement(holder); + + ROOT::TestSupport::CheckDiagsRAII diags; + diags.requiredDiag(kWarning, "REveProjectionManager::ImportElements", "'holder' places projected elements", false); + ASSERT_NE(mng->ImportElements(ls, holder), nullptr); +} + +// A manager with a matrix projects in its own frame and the client draws its +// children with the same matrix, so importing into it does not warn. Importing +// into an element outside its chain, without that matrix, does. +TEST(REveProjection, ImportIntoTransformedManager) +{ + auto eve = REX::REveManager::Create(); + auto src = eve->SpawnNewScene("tm_src"); + auto dst = eve->SpawnNewScene("tm_dst"); + + auto ls = new REX::REveStraightLineSet("model"); + ls->AddLine(0, 0, 0, 10, 10, 10); + src->AddElement(ls); + + auto mng = new REX::REveProjectionManager(REX::REveProjection::kPT_RPhi); + dst->AddElement(mng); + mng->RefMainTrans().SetPos(0, 0, 50); + + auto other = new REX::REveElement("other"); + dst->AddElement(other); + + { + ROOT::TestSupport::CheckDiagsRAII diags; + ASSERT_NE(mng->ImportElements(ls), nullptr); + } + { + ROOT::TestSupport::CheckDiagsRAII diags; + diags.requiredDiag(kWarning, "REveProjectionManager::ImportElements", "'other' places projected elements", false); + ASSERT_NE(mng->ImportElements(ls, other), nullptr); + } +} + +// The projection takes a model's coordinates through the matrices of the model +// and of its ancestors, as the 3D view does, for an import and for a child +// projected later through ProjectChild(). RPhi without distortion keeps x and +// y, so the projected points are the world points. +TEST(REveProjection, ModelTransIncludesAncestors) +{ + auto eve = REX::REveManager::Create(); + auto src = eve->SpawnNewScene("mt_src"); + auto dst = eve->SpawnNewScene("mt_dst"); + + auto holder = new REX::REveCompound("holder"); + holder->RefMainTrans().SetPos(100, 0, 0); + src->AddElement(holder); + + auto ps = new REX::REvePointSet("model"); + ps->SetNextPoint(1, 2, 0); + ps->RefMainTrans().SetPos(0, 10, 0); + holder->AddElement(ps); + + auto mng = new REX::REveProjectionManager(REX::REveProjection::kPT_RPhi); + ASSERT_NE(mng->ImportElements(holder, dst), nullptr); + ASSERT_EQ(ps->RefProjecteds().size(), 1u); + auto pr = dynamic_cast(ps->RefProjecteds().front()); + ASSERT_NE(pr, nullptr); + EXPECT_FLOAT_EQ(pr->RefPoint(0).fX, 101.f); + EXPECT_FLOAT_EQ(pr->RefPoint(0).fY, 12.f); + + auto ps2 = new REX::REvePointSet("later"); + ps2->SetNextPoint(3, 4, 0); + holder->AddElement(ps2); + holder->ProjectChild(ps2); + ASSERT_EQ(ps2->RefProjecteds().size(), 1u); + auto pr2 = dynamic_cast(ps2->RefProjecteds().front()); + ASSERT_NE(pr2, nullptr); + EXPECT_FLOAT_EQ(pr2->RefPoint(0).fX, 103.f); + EXPECT_FLOAT_EQ(pr2->RefPoint(0).fY, 4.f); +} + +// The projection is computed in the frame of the manager: a matrix on the +// manager's chain is inverted. Without one, a model whose only matrix is its +// own gets that matrix itself, as from PtrMainTrans(kFALSE). +TEST(REveProjection, ModelTransInManagerFrame) +{ + REX::REveManager::Create(); + auto holder = new REX::REveElement("holder"); + holder->RefMainTrans().SetPos(100, 0, 0); + auto ps = new REX::REvePointSet("model"); + ps->RefMainTrans().SetPos(0, 10, 0); + holder->AddElement(ps); + auto lone = new REX::REvePointSet("lone"); + lone->RefMainTrans().SetPos(5, 0, 0); + auto bare = new REX::REvePointSet("bare"); + + auto mng = new REX::REveProjectionManager(REX::REveProjection::kPT_RPhi); + REX::REveTrans t; + EXPECT_EQ(mng->GetModelTrans(bare, t), nullptr); + EXPECT_EQ(mng->GetModelTrans(lone, t), lone->PtrMainTrans(kFALSE)); + + auto m = mng->GetModelTrans(ps, t); + ASSERT_EQ(m, &t); + EXPECT_DOUBLE_EQ(m->GetPos().X(), 100.); + EXPECT_DOUBLE_EQ(m->GetPos().Y(), 10.); + + mng->RefMainTrans().SetPos(100, 0, 0); + m = mng->GetModelTrans(ps, t); + ASSERT_EQ(m, &t); + EXPECT_NEAR(m->GetPos().X(), 0., 1e-12); + EXPECT_DOUBLE_EQ(m->GetPos().Y(), 10.); +} + +// A projected jet cone takes the matrices of the model and its ancestors, in +// its vertices and in its bounding box. RPhi without distortion is linear, so a +// jet under a holder moved by 100 in x projects to the plain jet moved by 100. +TEST(REveProjection, JetConeModelTrans) +{ + auto eve = REX::REveManager::Create(); + auto src = eve->SpawnNewScene("jet_src"); + auto dst = eve->SpawnNewScene("jet_dst"); + + auto make_jet = [](const char *name) { + auto jet = new REX::REveJetCone(name); + jet->SetCylinder(250, 300); + jet->AddEllipticCone(0.5, 1.0, 0.1, 0.2); + return jet; + }; + auto plain = make_jet("plain"); + src->AddElement(plain); + auto holder = new REX::REveElement("holder"); + holder->RefMainTrans().SetPos(100, 0, 0); + src->AddElement(holder); + auto moved = make_jet("moved"); + holder->AddElement(moved); + + auto mng = new REX::REveProjectionManager(REX::REveProjection::kPT_RPhi); + mng->ImportElements(plain, dst); + mng->ImportElements(holder, dst); + auto pp = dynamic_cast(plain->RefProjecteds().front()); + auto pm = dynamic_cast(moved->RefProjecteds().front()); + ASSERT_NE(pp, nullptr); + ASSERT_NE(pm, nullptr); + + auto vertices = [](REX::REveElement *el) { + el->BuildRenderData(); + auto rd = el->GetRenderData(); + std::vector v(rd->SizeV()); + std::vector buf(rd->GetBinarySize()); + rd->Write(buf.data(), buf.size()); + std::copy_n(reinterpret_cast(buf.data()), v.size(), v.begin()); + return v; + }; + auto vp = vertices(pp), vm = vertices(pm); + ASSERT_EQ(vp.size(), vm.size()); + ASSERT_GE(vp.size(), 9u); + for (size_t i = 0; i < vp.size(); i += 3) { + EXPECT_NEAR(vm[i], vp[i] + 100, 1e-3) << "vertex " << i / 3; + EXPECT_NEAR(vm[i + 1], vp[i + 1], 1e-3) << "vertex " << i / 3; + } + + ASSERT_EQ(pp->AssertBBox(), REX::REveElement::kBBoxSet); + ASSERT_EQ(pm->AssertBBox(), REX::REveElement::kBBoxSet); + EXPECT_NEAR(pm->GetBBoxMin().fX, pp->GetBBoxMin().fX + 100, 1e-3); + EXPECT_NEAR(pm->GetBBoxMax().fX, pp->GetBBoxMax().fX + 100, 1e-3); + EXPECT_NEAR(pm->GetBBoxMin().fY, pp->GetBBoxMin().fY, 1e-3); + EXPECT_NEAR(pm->GetBBoxMax().fY, pp->GetBBoxMax().fY, 1e-3); +} + +// A projected geo shape takes the matrices of the shape and of its ancestors, +// for a 2D projection (REvePolygonSetProjected) and for a 3D one +// (REveGeoShapeProjected). Neither RPhi without distortion nor the 3D +// projection moves x and y, so the projected box is the box of the world +// points: 5 x 6 half-sizes around (100, 10). +TEST(REveProjection, GeoShapeModelTrans) +{ + auto eve = REX::REveManager::Create(); + auto src = eve->SpawnNewScene("geo_src"); + + auto holder = new REX::REveElement("holder"); + holder->RefMainTrans().SetPos(100, 0, 0); + src->AddElement(holder); + auto shape = new REX::REveGeoShape("box"); + { + // Register the shape with REve's geo manager, see GeoShapeBBoxAboutOrigin. + REX::REveGeoManagerHolder gmgr(REX::REveGeoShape::GetGeoManager()); + shape->SetShape(new TGeoBBox(5, 6, 7)); + } + shape->RefMainTrans().SetPos(0, 10, 0); + holder->AddElement(shape); + + for (auto type : {REX::REveProjection::kPT_RPhi, REX::REveProjection::kPT_3D}) { + auto dst = eve->SpawnNewScene("geo_dst"); + auto mng = new REX::REveProjectionManager(type); + mng->ImportElements(holder, dst); + auto pr = dynamic_cast(shape->RefProjecteds().back()); + ASSERT_NE(pr, nullptr); + ASSERT_EQ(pr->AssertBBox(), REX::REveElement::kBBoxSet) << "projection " << type; + EXPECT_NEAR(pr->GetBBoxMin().fX, 95.f, 1e-3) << "projection " << type; + EXPECT_NEAR(pr->GetBBoxMax().fX, 105.f, 1e-3) << "projection " << type; + EXPECT_NEAR(pr->GetBBoxMin().fY, 4.f, 1e-3) << "projection " << type; + EXPECT_NEAR(pr->GetBBoxMax().fY, 16.f, 1e-3) << "projection " << type; + } +} diff --git a/tutorials/visualisation/eve7/projection_axes.C b/tutorials/visualisation/eve7/projection_axes.C index 54b017d926db4..04ae18f40e14d 100644 --- a/tutorials/visualisation/eve7/projection_axes.C +++ b/tutorials/visualisation/eve7/projection_axes.C @@ -62,14 +62,6 @@ void pa_distortion(float d) mng->UpdateName(); mng->ProjectChildren(); } - // Update the ticks and the read-out, as REveProjectionManager::BumpDistortion() - // does for the overlay buttons. - for (auto ax : {gPaAxisRPhi, gPaAxisRhoZ}) { - if (!ax) - continue; - ax->UpdateTicks(); - ax->UpdateDistortionLabel(); - } } //------------------------------------------------------------------------------ diff --git a/ui5/eve7/controller/Summary.controller.js b/ui5/eve7/controller/Summary.controller.js index d945c0974954f..fde83e80ce0fd 100644 --- a/ui5/eve7/controller/Summary.controller.js +++ b/ui5/eve7/controller/Summary.controller.js @@ -334,7 +334,7 @@ sap.ui.define([ /** Everything except kCBTrans: the tree shows structure and * properties, and a moving element's transform is neither. See * EveManager.callSceneElementChange. */ - changeBitMask: 1 | 4 | 8 | 16, // ColorSelection | ObjProps | Visibility | Added + changeBitMask: 1 | 4 | 8 | 16, // CoreJson | ObjProps | Visibility | Added sceneElementChange: function(msg) { @@ -345,8 +345,12 @@ sap.ui.define([ let elem = this.mgr.GetElement(msg.fElementId); - if (newelem && elem) + if (newelem && elem) { + // createModel() copies the name and title; kCBCoreJson may change them. + newelem.fName = elem.fName; + newelem.fTitle = elem.fTitle || elem.fName; this.setElementsAttributes(newelem, elem); + } // console.log('SUMMURY: detect changed', elem.id, elem.path); diff --git a/ui5/eve7/lib/EveManager.js b/ui5/eve7/lib/EveManager.js index ac3ccfe4e8d25..57409fea4bea9 100644 --- a/ui5/eve7/lib/EveManager.js +++ b/ui5/eve7/lib/EveManager.js @@ -25,7 +25,7 @@ sap.ui.define([], function() { this.global_selection_id = null; this.global_highlight_id = null; - this.EChangeBits = { "kCBColorSelection": 1, "kCBTrans": 2, "kCBObjProps": 4, "kCBVisibility": 8, "kCBAdded": 16 }; + this.EChangeBits = { "kCBCoreJson": 1, "kCBTrans": 2, "kCBObjProps": 4, "kCBVisibility": 8, "kCBAdded": 16 }; this.controllers = []; this.gl_controllers = []; @@ -440,18 +440,15 @@ sap.ui.define([], function() { } if (em.changeBit & this.EChangeBits.kCBObjProps) { + // Optional keys: clear the old values; the message brings the + // current ones, if any. delete obj.render_data; - // AMT note ... the REveSelection changes fall here - // I think this should be a separate change bit - // Yes, or maybe Color should be be json only, ObjProps for RnrData. - // Although some classes might have an extended json part -- - // and this case could be separated out. delete obj.sel_list; jQuery.extend(obj, em); this.ParseUpdateTriggersAndProcessPostStream(obj); } else { - if (em.changeBit & this.EChangeBits.kCBColorSelection) { + if (em.changeBit & this.EChangeBits.kCBCoreJson) { delete em.render_data; Object.assign(obj, em); } diff --git a/ui5/eve7/lib/EveScene.js b/ui5/eve7/lib/EveScene.js index 1bd4dba51af1e..2620ef979b930 100644 --- a/ui5/eve7/lib/EveScene.js +++ b/ui5/eve7/lib/EveScene.js @@ -350,10 +350,19 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { this.updateElementTrans(el, msg); } - // other change bits + // New render data needs a new drawing. A core JSON change keeps the + // render data: a drawing whose maker attached refresh(el) updates + // itself, any other is rebuilt from the cached render data. if (el.render_data) { - if ((el.changeBit & this.mgr.EChangeBits.kCBObjProps) || (el.changeBit & this.mgr.EChangeBits.kCBColorSelection)) - { + let rebuild = !!(el.changeBit & this.mgr.EChangeBits.kCBObjProps); + if (!rebuild && (el.changeBit & this.mgr.EChangeBits.kCBCoreJson)) { + let obj3d = this.getObj3D(el.fElementId); + if (obj3d && typeof obj3d.refresh === 'function') + obj3d.refresh(el); + else + rebuild = true; + } + if (rebuild) { this.need_bbox_update = true; this.replaceElement(el); } From df59fc574ef4cb322de2a964a155275b017a2899 Mon Sep 17 00:00:00 2001 From: Matevz Tadel Date: Fri, 16 Oct 2026 22:38:31 -0700 Subject: [PATCH 4/7] [eve7] Build the element tree as a tree of renderer objects Drawings were placed flat in the scene, so an element's matrix did not reach its children and nothing could bound or cull a subtree. - Each element has a node that carries its matrix: its drawing when it has no children, else a group whose self is the drawing and whose other children are the children's nodes. - fRnrSelf hides the drawing, fRnrChildren the group's other children. On three.js a kids object under the group does the latter. - A leaf that gets a child is wrapped in a group, which takes its matrix and motion. - The server streams every matrix as JSON, a base64 string of its 16 doubles; REveRenderData no longer carries it (removed SetMatrix(), "trans_size"). It sends no old drawing for an emptied element. - The client places elements by their nodes' matrices. A full update sets the matrix, identity when there is none; moves and motion set the node; a rebuild without new state keeps it. - The client copies render data out of the message buffer, so the buffer is freed. - The scene box is the union of the server boxes, and near and far follow moving elements. The orthographic camera also fits the width. - Frustum-culled instanced hits and box sets. - sceneElementChange() reads the message's change bits. With the element's stale bits, a visibility change rebuilt the drawing, on master too. - Removed the null checks of EveScene.glctrl. - Added a gtest. Assisted-by: Claude:Opus 5.5 --- graf3d/eve7/inc/ROOT/REveRenderData.hxx | 6 +- graf3d/eve7/src/REveElement.cxx | 36 +- graf3d/eve7/src/REveRenderData.cxx | 14 - graf3d/eve7/test/change_cycle.cxx | 17 +- graf3d/eve7/test/streaming.cxx | 36 +- graf3d/eve7/test/trans_motion.cxx | 13 +- ui5/eve7/lib/EveElementsRCore.js | 20 +- ui5/eve7/lib/EveManager.js | 39 ++- ui5/eve7/lib/EveScene.js | 434 ++++++++++++++++-------- ui5/eve7/lib/GlViewerRCore.js | 81 ++++- ui5/eve7/lib/Motion.js | 21 +- 11 files changed, 498 insertions(+), 219 deletions(-) diff --git a/graf3d/eve7/inc/ROOT/REveRenderData.hxx b/graf3d/eve7/inc/ROOT/REveRenderData.hxx index 9b9c4a019c1d3..06fb6ce2218c6 100644 --- a/graf3d/eve7/inc/ROOT/REveRenderData.hxx +++ b/graf3d/eve7/inc/ROOT/REveRenderData.hxx @@ -27,7 +27,6 @@ private: std::vector fVertexBuffer; std::vector fNormalBuffer; std::vector fIndexBuffer; - std::vector fMatrix; public: // If Primitive_e is changed, change also definition in EveElements.js. @@ -86,8 +85,6 @@ public: void PushI(std::vector &v) { fIndexBuffer.insert(fIndexBuffer.end(), v.begin(), v.end()); } - void SetMatrix(const double *arr); - const std::string GetRnrFunc() const { return fRnrFunc; } void ResizeV(int s) { fVertexBuffer.resize(s); } @@ -95,9 +92,8 @@ public: int SizeV() const { return fVertexBuffer.size(); } int SizeN() const { return fNormalBuffer.size(); } int SizeI() const { return fIndexBuffer.size(); } - int SizeT() const { return fMatrix.size(); } - int GetBinarySize() { return (SizeV() + SizeN() + SizeT()) * sizeof(float) + SizeI() * sizeof(int); } + int GetBinarySize() { return (SizeV() + SizeN()) * sizeof(float) + SizeI() * sizeof(int); } int Write(char *msg, int maxlen); diff --git a/graf3d/eve7/src/REveElement.cxx b/graf3d/eve7/src/REveElement.cxx index 7dcf4014ce51f..202af31eeb1fd 100644 --- a/graf3d/eve7/src/REveElement.cxx +++ b/graf3d/eve7/src/REveElement.cxx @@ -25,6 +25,7 @@ #include "TClass.h" #include "TPRegexp.h" #include "TROOT.h" +#include "TBase64.h" #include "TColor.h" #include @@ -1568,16 +1569,27 @@ REveElement::EBBoxState REveElement::AssertBBox() } //////////////////////////////////////////////////////////////////////////////// -/// Write transformation Matrix to render data -//////////////////////////////////////////////////////////////////////////////// +/// Build fRenderData, what the client draws. An element with nothing to draw +/// leaves it empty, as this base does. The matrix goes as JSON, see +/// WriteCoreJson(). void REveElement::BuildRenderData() { - if (fMainTrans && fRenderData) { - fRenderData->SetMatrix(fMainTrans->Array()); - } } +namespace { + +//////////////////////////////////////////////////////////////////////////////// +/// The 16 doubles of `t`, column-major, as base64 of their bytes. Exact, and +/// about half the size of the numbers as text. + +std::string TransBase64(const REveTrans &t) +{ + return TBase64::Encode(reinterpret_cast(t.Array()), 16 * sizeof(Double_t)).Data(); +} + +} // namespace + //////////////////////////////////////////////////////////////////////////////// /// Write the main transformation and its motion for a kCBTrans update. /// No render data is written; the client applies it to the object it already @@ -1586,10 +1598,8 @@ void REveElement::BuildRenderData() void REveElement::WriteTransJson(nlohmann::json &cj) { - if (fMainTrans.get()) { - const Double_t *m = fMainTrans->Array(); - cj["matrix"] = std::vector(m, m + 16); - } + if (fMainTrans) + cj["matrix"] = TransBase64(*fMainTrans); if (fMainTrans && fMainTrans->HasMotion()) { const REveDeltaTrans &d = *fMainTrans->GetDeltaTrans(); @@ -1646,6 +1656,9 @@ Int_t REveElement::WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) Int_t ret = 0; if (rnr_offset >= 0) { + // An element with nothing to draw builds no render data. Without the + // reset it would send the render data of its previous contents again. + fRenderData.reset(); BuildRenderData(); if (fRenderData) { @@ -1656,7 +1669,6 @@ Int_t REveElement::WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) rd["vert_size"] = fRenderData->SizeV(); rd["norm_size"] = fRenderData->SizeN(); rd["index_size"] = fRenderData->SizeI(); - rd["trans_size"] = fRenderData->SizeT(); j["render_data"] = rd; @@ -1677,6 +1689,10 @@ Int_t REveElement::WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) } } } + + // With or without render data: the client sets it on the element's node. + if (fMainTrans) + j["matrix"] = TransBase64(*fMainTrans); } return ret; diff --git a/graf3d/eve7/src/REveRenderData.cxx b/graf3d/eve7/src/REveRenderData.cxx index f4dd21b6d2207..cd3805e15ee8a 100644 --- a/graf3d/eve7/src/REveRenderData.cxx +++ b/graf3d/eve7/src/REveRenderData.cxx @@ -55,9 +55,6 @@ int REveRenderData::Write(char *msg, int maxlen) off += len; }; - if (!fMatrix.empty()) - append(&fMatrix[0], fMatrix.size() * sizeof(float)); - if (!fVertexBuffer.empty()) append(&fVertexBuffer[0], fVertexBuffer.size() * sizeof(float)); @@ -70,17 +67,6 @@ int REveRenderData::Write(char *msg, int maxlen) return off; } -///////////////////////////////////////////////////////////////////////////////////////// -/// Set transformation matrix - -void REveRenderData::SetMatrix(const double *arr) -{ - fMatrix.reserve(16); - for (int i = 0; i < 16; ++i) { - fMatrix.push_back(arr[i]); - } -} - ///////////////////////////////////////////////////////////////////////////////////////// /// Calculate texture dimensions to hold nel elements with given alignment on x axis. /// Static function. diff --git a/graf3d/eve7/test/change_cycle.cxx b/graf3d/eve7/test/change_cycle.cxx index b351d6aa6dbdc..99fe41d3cee79 100644 --- a/graf3d/eve7/test/change_cycle.cxx +++ b/graf3d/eve7/test/change_cycle.cxx @@ -21,9 +21,12 @@ #include #include +#include "TBase64.h" + #include #include +#include #include namespace REX = ROOT::Experimental; @@ -191,11 +194,15 @@ TEST(REveChangeCycle, TransOnlyGoesToMotion) ASSERT_EQ(arr.size(), 1u); EXPECT_EQ(arr[0]["fElementId"], moved->GetElementId()); EXPECT_EQ(arr[0]["fSceneId"], s->GetElementId()); - ASSERT_EQ(arr[0]["matrix"].size(), 16u); - // Column-major, translation in elements 12 to 14. - EXPECT_DOUBLE_EQ(arr[0]["matrix"][12].get(), 1.); - EXPECT_DOUBLE_EQ(arr[0]["matrix"][13].get(), 2.); - EXPECT_DOUBLE_EQ(arr[0]["matrix"][14].get(), 3.); + // Base64 of 16 doubles, column-major, translation in elements 12 to 14. + ASSERT_TRUE(arr[0]["matrix"].is_string()); + const TString bytes = TBase64::Decode(arr[0]["matrix"].get().c_str()); + ASSERT_EQ(bytes.Length(), 16 * (int)sizeof(Double_t)); + Double_t m[16]; + std::memcpy(m, bytes.Data(), sizeof(m)); + EXPECT_DOUBLE_EQ(m[12], 1.); + EXPECT_DOUBLE_EQ(m[13], 2.); + EXPECT_DOUBLE_EQ(m[14], 3.); EXPECT_TRUE(arr[0]["mot"].is_null()); EXPECT_EQ(moved->GetChangeBits(), 0); diff --git a/graf3d/eve7/test/streaming.cxx b/graf3d/eve7/test/streaming.cxx index 1c981aae636ab..7f1dc37b10628 100644 --- a/graf3d/eve7/test/streaming.cxx +++ b/graf3d/eve7/test/streaming.cxx @@ -24,6 +24,8 @@ #include "TGeoTube.h" +#include "TBase64.h" + #include #include @@ -249,7 +251,8 @@ TEST(REveStreaming, TextNotInWorld) delete t; } -// An REveBox streams its transformation, and its box stays in its own frame. +// An REveBox streams its transformation as JSON, base64 of 16 doubles, and its +// box stays in its own frame. TEST(REveStreaming, BoxTransAndBBox) { REX::REveManager::Create(); @@ -264,7 +267,15 @@ TEST(REveStreaming, BoxTransAndBBox) nlohmann::json j; b->WriteCoreJson(j, 0); ASSERT_TRUE(j.contains("render_data")); - EXPECT_EQ(j["render_data"]["trans_size"], 16); + EXPECT_FALSE(j["render_data"].contains("trans_size")); + ASSERT_TRUE(j["matrix"].is_string()); + const TString bytes = TBase64::Decode(j["matrix"].get().c_str()); + ASSERT_EQ(bytes.Length(), 16 * (int)sizeof(Double_t)); + Double_t m[16]; + std::memcpy(m, bytes.Data(), sizeof(m)); + EXPECT_DOUBLE_EQ(m[12], 100); + EXPECT_DOUBLE_EQ(m[13], 200); + EXPECT_DOUBLE_EQ(m[14], 300); EXPECT_EQ(j["bbox"], nlohmann::json({-h, -h, -h, h, h, h})); delete b; @@ -333,6 +344,27 @@ TEST(REveStreaming, BoxSetBBoxHoldsInstances) delete cone; } +// An element emptied of its contents sends no render data, not the render data +// of what it held before. +TEST(REveStreaming, EmptiedElementSendsNoRenderData) +{ + REX::REveManager::Create(); + auto ps = MakePoints("points", 5); + + nlohmann::json j; + ps->WriteCoreJson(j, 0); + ASSERT_TRUE(j.contains("render_data")); + + ps->Reset(0); + nlohmann::json je; + EXPECT_EQ(ps->WriteCoreJson(je, 0), 0); + EXPECT_FALSE(je.contains("render_data")); + EXPECT_FALSE(je.contains("bbox")); + EXPECT_EQ(ps->GetRenderData(), nullptr); + + delete ps; +} + // An element with no geometry of its own has no render data and sends no box. namespace { // Fills its box from no points at all. diff --git a/graf3d/eve7/test/trans_motion.cxx b/graf3d/eve7/test/trans_motion.cxx index 34c58f8b7644d..03aa9dba1ba61 100644 --- a/graf3d/eve7/test/trans_motion.cxx +++ b/graf3d/eve7/test/trans_motion.cxx @@ -15,8 +15,12 @@ #include #include +#include "TBase64.h" + #include +#include + namespace REX = ROOT::Experimental; TEST(REveTransMotion, SetMotion) @@ -102,8 +106,13 @@ TEST(REveTransMotion, TransJson) nlohmann::json j; el->WriteTransJson(j); - ASSERT_EQ(j["matrix"].size(), 16u); - EXPECT_DOUBLE_EQ(j["matrix"][12].get(), 4.); + // Base64 of 16 doubles, translation in elements 12 to 14. + ASSERT_TRUE(j["matrix"].is_string()); + const TString bytes = TBase64::Decode(j["matrix"].get().c_str()); + ASSERT_EQ(bytes.Length(), 16 * (int)sizeof(Double_t)); + Double_t mt[16]; + std::memcpy(mt, bytes.Data(), sizeof(mt)); + EXPECT_DOUBLE_EQ(mt[12], 4.); auto &m = j["mot"]; ASSERT_TRUE(m.is_object()); EXPECT_EQ(m["vel"], nlohmann::json({1., 2., 3.})); diff --git a/ui5/eve7/lib/EveElementsRCore.js b/ui5/eve7/lib/EveElementsRCore.js index e036fc3ceff1a..1dce725b8fdb5 100644 --- a/ui5/eve7/lib/EveElementsRCore.js +++ b/ui5/eve7/lib/EveElementsRCore.js @@ -588,13 +588,14 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function (EveManager) { } /** Give `geo` the element's box, `el.bbox` (min, then max, in the - * element's own frame, computed on the server). Does nothing for an + * element's own frame, computed on the server). Returns false for an * element without one. */ RcSetElementBBox(geo, el) { let bb = el.bbox; - if (bb && bb.length === 6) - geo.setExternalBoundingBox(new RC.Box3(new RC.Vector3(bb[0], bb[1], bb[2]), - new RC.Vector3(bb[3], bb[4], bb[5]))); + if (!bb || bb.length !== 6) return false; + geo.setExternalBoundingBox(new RC.Box3(new RC.Vector3(bb[0], bb[1], bb[2]), + new RC.Vector3(bb[3], bb[4], bb[5]))); + return true; } RcMakeZSprite(colIdx, sSize, nInstance, vbuff, instX, instY, textureName) { @@ -794,7 +795,9 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function (EveManager) { rnr_data.vtxBuff, hit.fTexX, hit.fTexY, txName); - this.RcSetElementBBox(s.geometry, hit); + // The instances are placed from a texture, so only the server's box + // bounds them; with it the object can be culled. + if (this.RcSetElementBBox(s.geometry, hit)) s.frustumCulled = true; this.RcPickable(hit, s); @@ -886,8 +889,7 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function (EveManager) { () => this.viewer.request_render() ); - // No position here: REveElement::BuildRenderData writes fMainTrans into - // the render data and EveScene applies it with setMatrixFromArray(). + // No position here: EveScene sets the element's matrix on its node. // Movable if pickable; resizable unless the element turns it off. text.resizable = (el.fResizable === undefined) ? true : !!el.fResizable; if (el.fPickable) this.RcPickable(el, text); @@ -1376,7 +1378,9 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function (EveManager) { else mesh = this.makeFreeBoxSet(boxset, rnr_data); - this.RcSetElementBBox(mesh.geometry, boxset); + // Instanced boxes are placed from a texture, so only the server's box + // bounds them; with it the object can be culled. + if (this.RcSetElementBBox(mesh.geometry, boxset)) mesh.frustumCulled = true; return mesh; } diff --git a/ui5/eve7/lib/EveManager.js b/ui5/eve7/lib/EveManager.js index 57409fea4bea9..d75adcb26546b 100644 --- a/ui5/eve7/lib/EveManager.js +++ b/ui5/eve7/lib/EveManager.js @@ -9,6 +9,16 @@ sap.ui.define([], function() { class EveManager { + /** An element's matrix as streamed by REveElement: base64 of its 16 + * doubles, column-major. Returns a Float64Array, or null for none. */ + static decodeMatrix(m) { + if (!m) return null; + const bin = atob(m), bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; ++i) + bytes[i] = bin.charCodeAt(i); + return new Float64Array(bytes.buffer, 0, 16); + } + constructor() { this.map = {}; // use object, not array @@ -443,7 +453,10 @@ sap.ui.define([], function() { // Optional keys: clear the old values; the message brings the // current ones, if any. delete obj.render_data; + delete obj.bbox; + delete obj.bbox_not_in_world; delete obj.sel_list; + delete obj.matrix; jQuery.extend(obj, em); this.ParseUpdateTriggersAndProcessPostStream(obj); } @@ -453,12 +466,9 @@ sap.ui.define([], function() { Object.assign(obj, em); } if (em.changeBit & this.EChangeBits.kCBTrans) { - // Transformation update: no render data is streamed. - // render_data.matrix is updated here because a later rebuild, - // e.g. on a colour change in the same message, reads it. + // Matrix and motion; EveScene sets them on the element's node. + delete obj.matrix; Object.assign(obj, em); - if (em.matrix && obj.render_data) - obj.render_data.matrix = em.matrix; } } @@ -513,20 +523,18 @@ sap.ui.define([], function() { if (off !== lastoff) console.error('Element ' + elem.fName + ' offset mismatch ' + off + ' != ' + lastoff); - if (rd.trans_size) { - rd.matrix = new Float32Array(rawdata, off, rd.trans_size); - off += rd.trans_size*4; - } + // Copies, not views: the message's buffer is then freed, and each + // element's arrays go with the element. if (rd.vert_size) { - rd.vtxBuff = new Float32Array(rawdata, off, rd.vert_size); + rd.vtxBuff = new Float32Array(rawdata, off, rd.vert_size).slice(); off += rd.vert_size*4; } if (rd.norm_size) { - rd.nrmBuff = new Float32Array(rawdata, off, rd.norm_size); + rd.nrmBuff = new Float32Array(rawdata, off, rd.norm_size).slice(); off += rd.norm_size*4; } if (rd.index_size) { - rd.idxBuff = new Uint32Array(rawdata, off, rd.index_size); + rd.idxBuff = new Uint32Array(rawdata, off, rd.index_size).slice(); off += rd.index_size*4; } @@ -810,10 +818,9 @@ sap.ui.define([], function() { let obj = this.map[em.fElementId]; if (!obj) continue; // removed, or not yet created - // Keep the model in step: a later rebuild reads render_data.matrix, - // and a stale one would put the object back where it used to be. - if (em.matrix && obj.render_data) - obj.render_data.matrix = em.matrix; + // The element mirrors what the server sent; a view that is built + // again, see EveScene.redrawScene(), places its node from it. + if (em.matrix) obj.matrix = em.matrix; else delete obj.matrix; obj.mot = em.mot; let scene = this.GetElement(em.fSceneId); diff --git a/ui5/eve7/lib/EveScene.js b/ui5/eve7/lib/EveScene.js index 2620ef979b930..142e1aae4dfda 100644 --- a/ui5/eve7/lib/EveScene.js +++ b/ui5/eve7/lib/EveScene.js @@ -6,6 +6,8 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { "use strict"; + const kIdentity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; + /// constructor, handle for REveScene class class EveScene { @@ -16,11 +18,18 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { this.id = sceneInfo.fSceneId; this.glctrl = glctrl; this.creator = glctrl.viewer.creator; - this.id2obj_map = new Map; // base on element id + this.id2obj_map = new Map; // element id -> the object that draws the element + this.id2node_map = new Map; // element id -> the element's node, see makeNode() + + // Raised when an element is added, removed or rebuilt; the viewer then + // recomputes this scene's box. + this.content_stamp = 0; + // Raised when an element's matrix changes, from the server or by Motion; + // near and far follow the scene's current box. + this.trans_stamp = 0; this.IsOverlay = mgr.GetElement(sceneInfo.fSceneId).IsOverlay; this.first_time = true; - this.need_visibility_update = false; // register ourself for scene events this.mgr.RegisterSceneReceiver(this.id, this); @@ -56,16 +65,6 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { obj3d.geo_object = elem.fMasterId || elem.fElementId; obj3d.geo_name = elem.fName; // used for highlight - if (elem.render_data.matrix) { - if (this.mgr.is_rcore) { - obj3d.setMatrixFromArray(elem.render_data.matrix); - } else { - obj3d.matrix.fromArray(elem.render_data.matrix); - obj3d.updateMatrixWorld(true); - obj3d.matrixAutoUpdate = false; - } - } - return obj3d; } } @@ -79,67 +78,212 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { return this.id2obj_map.get(elementId); } - create3DObjects(all_ancestor_children_visible, prnt, res3d) + //------------------------------------------------------------------------ + // The element tree + // + // Each element has a node, in id2node_map, that carries its matrix. A + // leaf that draws is its own node. Any other element has a group node: + // its `self` is the element's drawing, at identity, and its other + // children are the nodes of the element's children. fRnrSelf is the + // drawing's `visible`, fRnrChildren the group's `visibleChildren`. + // + // Motion moves nodes, and a scene's box is the server boxes moved by the + // world matrices of their nodes. + // + // three.js has no `self` and `visibleChildren`. There the node helpers + // keep the children's nodes in a `kids` object and use its `visible`. + //------------------------------------------------------------------------ + + /** A new group node for `elem`, with its matrix. It has no eve_el, so + * picking passes it by. */ + makeNode(elem) { - if (prnt === undefined) { - prnt = this.mgr.GetElement(this.id); - res3d = []; + let node = this.glctrl.viewer.make_object("el " + elem.fElementId); + if (!this.mgr.is_rcore) node.eve_group = true; + this.setNodeMatrix(node, elem); + this.nodeSetChildrenVisible(node, elem.fRnrChildren); + return node; + } + + /** Add `child` to `node`, a group node or the scene container. */ + nodeAdd(node, child) + { + if (this.mgr.is_rcore || !node.eve_group) { node.add(child); return; } + if (!node.eve_kids) { + node.eve_kids = this.glctrl.viewer.make_object("kids"); + node.eve_kids.eve_owner = node; + node.eve_kids.visible = node.eve_vis_children !== false; + node.add(node.eve_kids); } + node.eve_kids.add(child); + } - if (!prnt || !prnt.childs) return res3d; + /** The node `child` belongs to; on three.js the group, not its `kids`. */ + nodeParent(child) + { + let p = child.parent; + return (p && p.eve_owner) || p; + } - for (let k = 0; k < prnt.childs.length; ++k) - { - let elem = prnt.childs[k]; - if (elem.render_data) - { - let fname = elem.render_data.rnr_func; - if ( ! this.creator[fname]) - { - console.error("Function " + fname + " missing in creator"); - } - else - { - let obj3d = this.makeGLRepresentation(elem); - if (obj3d) - { - // MT - should maintain hierarchy ???? - // Easier to remove ... but might need sub-class of - // Object3D to separate "graphical" children and structural children. + /** Make `obj` the drawing of group node `node`, at identity, in place of + * the previous one; null removes it. */ + nodeSetSelf(node, obj) + { + if (this.mgr.is_rcore) { node.self = obj; return; } + if (node.eve_self) node.remove(node.eve_self); + node.eve_self = obj; + if (!obj) return; + node.add(obj); + obj.matrix.identity(); + obj.matrixAutoUpdate = false; + obj.updateMatrixWorld(true); + } - res3d.push(obj3d); + /** Show or hide the children of group node `node`; not its drawing. */ + nodeSetChildrenVisible(node, vis) + { + if (this.mgr.is_rcore) { node.visibleChildren = vis; return; } + node.eve_vis_children = vis; + if (node.eve_kids) node.eve_kids.visible = vis; + } - this.id2obj_map.set(elem.fElementId, obj3d); + /** Replace `obj`, the own node of `elem`, by a group node with `obj` as + * its drawing. The group takes obj's current matrix, motion included. */ + wrapInGroup(obj, elem) + { + let node = this.glctrl.viewer.make_object("el " + elem.fElementId); + if (this.mgr.is_rcore) { + node.setMatrixFromArray(obj.matrix.elements); + } else { + node.eve_group = true; + node.matrix.copy(obj.matrix); + node.matrixAutoUpdate = false; + } + this.nodeSetChildrenVisible(node, elem.fRnrChildren); + let parent = obj.parent; + if (parent) { parent.remove(obj); parent.add(node); } + this.nodeSetSelf(node, obj); + if (!this.mgr.is_rcore) node.updateMatrixWorld(true); + return node; + } - obj3d.visible = elem.fRnrSelf && all_ancestor_children_visible; - obj3d.all_ancestor_children_visible = all_ancestor_children_visible; - } - } + /** Make the node of `elem` under `parentNode` and register it, see + * "The element tree" above. */ + addElementNode(elem, parentNode) + { + let node = null; + if (!(elem.childs && elem.childs.length)) { + node = this.makeContent(elem); + if (node) { + this.setNodeMatrix(node, elem); + node.visible = elem.fRnrSelf; + this.id2obj_map.set(elem.fElementId, node); } + } + if (!node) { + node = this.makeNode(elem); + this.addContent(elem, node); + } + this.nodeAdd(parentNode, node); + this.id2node_map.set(elem.fElementId, node); + return node; + } - this.create3DObjects(elem.fRnrChildren && all_ancestor_children_visible, elem, res3d); + /** Give `elem`, a leaf, a group node, so that it can hold children. */ + promoteToGroup(elem) + { + let node = this.wrapInGroup(this.id2node_map.get(elem.fElementId), elem); + this.id2node_map.set(elem.fElementId, node); + + // The group carries the matrix now, so the trajectory moves it. + let mo = this.glctrl.viewer.motion; + if (mo) mo.retarget(elem.fElementId, node); + return node; + } + + /** Set the matrix of `elem`'s full state on `node`; identity when it has + * none. Moves and motion set the node themselves. */ + setNodeMatrix(node, elem) + { + this.applyMatrix(node, EveManager.decodeMatrix(elem.matrix) || kIdentity); + } + + /** Set the 16 values of `m`, column-major, as the matrix of `node`. */ + applyMatrix(node, m) + { + if (this.mgr.is_rcore) { + node.setMatrixFromArray(m); + } else { + node.matrix.fromArray(m); + node.matrixAutoUpdate = false; + node.updateMatrixWorld(true); + } + } + + /** Make the object that draws `elem`, or null if it does not draw. */ + makeContent(elem) + { + if (!elem.render_data) return null; + if (!this.creator[elem.render_data.rnr_func]) { + console.error("Function " + elem.render_data.rnr_func + " missing in creator"); + return null; } + return this.makeGLRepresentation(elem) || null; + } - return res3d; + /** Make the drawing of `elem` and set it as the drawing of group node + * `node`. Returns it, or null when `elem` does not draw. */ + addContent(elem, node) + { + let obj3d = this.makeContent(elem); + if (!obj3d) return null; + obj3d.visible = elem.fRnrSelf; + this.nodeSetSelf(node, obj3d); + this.id2obj_map.set(elem.fElementId, obj3d); + return obj3d; + } + + /** The node to put the node of `elem` under: the mother's, made a group + * if it is a leaf. Until the mother's node exists, the scene container; + * the mother adopts it when it is added. */ + parentNodeOf(elem) + { + if (elem.fMotherId !== this.id) { + let pn = this.id2node_map.get(elem.fMotherId); + if (pn && pn === this.id2obj_map.get(elem.fMotherId)) + pn = this.promoteToGroup(this.mgr.GetElement(elem.fMotherId)); + if (pn) return pn; + } + return this.glctrl.getSceneContainer(this); + } + + /** Build the nodes and drawing objects of `elem` and everything below it. */ + buildSubtree(elem, parentNode) + { + let node = this.addElementNode(elem, parentNode); + if (elem.childs) + for (let c of elem.childs) + this.buildSubtree(c, node); } //============================================================================== - /** method insert all objects into three.js container */ + /** Build the scene's element tree under the scene container. */ redrawScene() { let eveScene = this.mgr.GetElement(this.id); - if (!this.glctrl) return; - - let res3d = this.create3DObjects(true); - if ( ! res3d.length && this.first_time) return; + let childs = (eveScene && eveScene.childs) || []; + if (!childs.length && this.first_time) return; let cont = this.glctrl.getSceneContainer(this); while (cont.children.length > 0) cont.remove(cont.children[0]); + this.id2obj_map.clear(); + this.id2node_map.clear(); - for (let k = 0; k < res3d.length; ++k) - cont.add(res3d[k]); + for (let c of childs) + this.buildSubtree(c, cont); + ++this.content_stamp; this.applySelectionOnSceneCreate(this.mgr.global_selection_id); this.applySelectionOnSceneCreate(this.mgr.global_highlight_id); @@ -149,8 +293,6 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { removeScene() { - if (!this.glctrl) return; - let cont = this.glctrl.getSceneContainer(this); while (cont.children.length > 0) cont.remove(cont.children[0]); @@ -159,25 +301,14 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { this.first_time = true; } - update3DObjectsVisibility(arr, all_ancestor_children_visible) + /** Apply fRnrSelf to the element's drawing and fRnrChildren to its group + * node. */ + applyVisibility(el) { - if (!arr) return; - - for (let k = 0; k < arr.length; ++k) - { - let elem = arr[k]; - if (elem.render_data) - { - let obj3d = this.getObj3D(elem.fElementId); - if (obj3d) - { - obj3d.visible = elem.fRnrSelf && all_ancestor_children_visible; - obj3d.all_ancestor_children_visible = all_ancestor_children_visible; - } - } - - this.update3DObjectsVisibility(elem.childs, elem.fRnrChildren && all_ancestor_children_visible); - } + let obj3d = this.getObj3D(el.fElementId); + if (obj3d) obj3d.visible = el.fRnrSelf; + let node = this.id2node_map.get(el.fElementId); + if (node && node !== obj3d) this.nodeSetChildrenVisible(node, el.fRnrChildren); } onSceneCreate(/* id */) @@ -195,65 +326,62 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { endChanges() { - if (this.glctrl) { - if (this.need_visibility_update) { - let p = this.mgr.GetElement(this.id); - this.update3DObjectsVisibility(p.childs, true); - this.need_visibility_update = false; - } - - // Recompute the scene bounding box only after elements were added, - // removed or rebuilt, not after changes that only moved them. The - // 3D axis is sized from this box and would otherwise follow moving - // objects. REveViewer::SetAxesBBox() fixes the axis extent instead. - this.glctrl.viewer.request_render(this.need_bbox_update); - this.need_bbox_update = false; - } + // Recompute the scene box after content changes, not after moves: the + // 3D axis is sized from it and would follow moving objects. + // REveViewer::SetAxesBBox() fixes the axis extent instead. + this.glctrl.viewer.request_render(this.need_bbox_update); + this.need_bbox_update = false; } elementAdded(el) { this.need_bbox_update = true; - if ( ! this.glctrl) return; - - let obj3d = this.makeGLRepresentation(el); - if ( ! obj3d) return; - - // let scene = this.mgr.GetElement(this.id); - let container = this.glctrl.getSceneContainer(this); - - container.add(obj3d); - - this.id2obj_map.set(el.fElementId, obj3d); - - this.need_visibility_update = true; + ++this.content_stamp; + let node = this.addElementNode(el, this.parentNodeOf(el)); + + // Adopt the nodes of children that were added before this element. + if (el.childs) + for (let c of el.childs) { + let cn = this.id2node_map.get(c.fElementId); + if (cn && this.nodeParent(cn) !== node) { + if (cn.parent) cn.parent.remove(cn); + this.nodeAdd(node, cn); + } + } } - replaceElement(el) { - if (!this.glctrl) return; - + /** Rebuild the drawing of `el`. With `keepMatrix`, for a rebuild without + * new state, the node keeps its matrix. */ + replaceElement(el, keepMatrix) { // Drop the trajectory: it refers to the object about to be replaced. // The next transformation update registers it on the new object. let mo = this.glctrl.viewer.motion; if (mo) mo.remove(el.fElementId); - let container = this.glctrl.getSceneContainer(this); + ++this.content_stamp; + + let node = this.id2node_map.get(el.fElementId); + if (!node) { this.elementAdded(el); return; } try { let obj3d = this.getObj3D(el.fElementId); - - if(obj3d) container.remove(obj3d); - - obj3d = this.makeGLRepresentation(el); - if (obj3d) { - container.add(obj3d); - this.id2obj_map.set(el.fElementId, obj3d); + this.id2obj_map.delete(el.fElementId); + if (obj3d && obj3d === node) { + // The element is its own node: the new drawing takes its place. + let m = keepMatrix ? Array.from(node.matrix.elements) : null; + let parent = node.parent; + if (parent) parent.remove(node); + this.id2node_map.delete(el.fElementId); + let n = this.addElementNode(el, parent || this.glctrl.getSceneContainer(this)); + if (m) this.applyMatrix(n, m); + } else { + if (!keepMatrix) this.setNodeMatrix(node, el); + if (!this.addContent(el, node)) this.nodeSetSelf(node, null); } } catch (e) { console.error("replace element", e); } - this.need_visibility_update = true; } /** Apply one element of a "Motion" message: the same update as a @@ -274,35 +402,51 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { this.glctrl.viewer.request_render(); } + /** Remove the drawing of `elem`, which no longer draws. A leaf gets an + * empty group node in its place. */ + dropContent(elem) + { + let obj3d = this.getObj3D(elem.fElementId); + if (!obj3d) return; + this.id2obj_map.delete(elem.fElementId); + this.need_bbox_update = true; + ++this.content_stamp; + + let node = this.id2node_map.get(elem.fElementId); + if (obj3d === node) { + node = this.wrapInGroup(obj3d, elem); + this.id2node_map.set(elem.fElementId, node); + let mo = this.glctrl.viewer.motion; + if (mo) mo.retarget(elem.fElementId, node); + } + this.nodeSetSelf(node, null); + } + /** Apply a transformation-only change to the existing renderer object by * setting msg.matrix on it. Requests no render; the callers do. */ updateElementTrans(el, msg) { - let obj3d = this.getObj3D(msg.fElementId); - if (!obj3d) return; + let node = this.id2node_map.get(msg.fElementId); + if (!node) return; - let mot_viewer = this.glctrl ? this.glctrl.viewer : null; + let mo = this.glctrl.viewer.motion; - if (msg.matrix) { - if (this.mgr.is_rcore) { - obj3d.setMatrixFromArray(msg.matrix); - } else { - obj3d.matrix.fromArray(msg.matrix); - obj3d.updateMatrixWorld(true); - } - } + // Every transformation update carries the matrix; none means identity. + this.applyMatrix(node, EveManager.decodeMatrix(msg.matrix) || kIdentity); + ++this.trans_stamp; // After the matrix is applied: Motion takes the start position from it. // Every transformation update carries "mot"; null clears the trajectory. - if (mot_viewer && mot_viewer.motion && msg.mot !== undefined) - mot_viewer.motion.update(msg.fElementId, obj3d, msg.mot); + if (mo && msg.mot !== undefined) + mo.update(msg.fElementId, node, msg.mot, this); } elementsRemoved(ids) { this.need_bbox_update = true; + ++this.content_stamp; - let mo = this.glctrl ? this.glctrl.viewer.motion : null; + let mo = this.glctrl.viewer.motion; for (let i = 0; i < ids.length; i++) { @@ -311,18 +455,14 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { // Drop the trajectory. Nothing else removes it, and a live // trajectory keeps Motion's render loop running. if (mo) mo.remove(elId); - let obj3d = this.getObj3D(elId); - if (!obj3d) { - let el = this.mgr.GetElement(elId); - if (el && el.render_data) { - console.warn("EveScene.elementsRemoved can't find obj3d ", this.mgr.GetElement(el)); - } - continue; - } - let container = this.glctrl.getSceneContainer(this); - container.remove(obj3d); + // Removing the node removes the subtree below it from the scene. + let node = this.id2node_map.get(elId); + if (node && node.parent) node.parent.remove(node); + this.id2node_map.delete(elId); + let obj3d = this.getObj3D(elId); + if (!obj3d) continue; this.id2obj_map.delete(elId); if (!this.mgr.is_rcore) { @@ -340,22 +480,30 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { // visibility if (msg.changeBit & this.mgr.EChangeBits.kCBVisibility) { - this.need_visibility_update = true; + this.applyVisibility(el); } - // Update the transformation in place. When kCBObjProps is also set the - // server sends render data instead of a matrix, and the rebuild below - // carries the new position. + // Update the transformation in place. if (msg.changeBit & this.mgr.EChangeBits.kCBTrans) { this.updateElementTrans(el, msg); } - // New render data needs a new drawing. A core JSON change keeps the - // render data: a drawing whose maker attached refresh(el) updates - // itself, any other is rebuilt from the cached render data. + // A full update without render data: drop the drawing, set the matrix. + if (!el.render_data && (msg.changeBit & this.mgr.EChangeBits.kCBObjProps)) { + this.dropContent(el); + let node = this.id2node_map.get(el.fElementId); + if (node) { + this.setNodeMatrix(node, el); + ++this.trans_stamp; + } + } + + // A full update rebuilds the drawing. A core JSON change calls + // refresh(el) of a drawing that has one, else rebuilds it from the + // cached render data and keeps the node's matrix. if (el.render_data) { - let rebuild = !!(el.changeBit & this.mgr.EChangeBits.kCBObjProps); - if (!rebuild && (el.changeBit & this.mgr.EChangeBits.kCBCoreJson)) { + let rebuild = !!(msg.changeBit & this.mgr.EChangeBits.kCBObjProps); + if (!rebuild && (msg.changeBit & this.mgr.EChangeBits.kCBCoreJson)) { let obj3d = this.getObj3D(el.fElementId); if (obj3d && typeof obj3d.refresh === 'function') obj3d.refresh(el); @@ -364,7 +512,7 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { } if (rebuild) { this.need_bbox_update = true; - this.replaceElement(el); + this.replaceElement(el, !(msg.changeBit & this.mgr.EChangeBits.kCBObjProps)); } } } diff --git a/ui5/eve7/lib/GlViewerRCore.js b/ui5/eve7/lib/GlViewerRCore.js index 581b0faca2d92..67d2db5aa6f08 100644 --- a/ui5/eve7/lib/GlViewerRCore.js +++ b/ui5/eve7/lib/GlViewerRCore.js @@ -241,6 +241,8 @@ sap.ui.define([ this.scene = new RC.Scene(); this.overlay_scene = new RC.Scene(); this.scene_bbox = new RC.Box3(); + this.scene_bboxes = new WeakMap(); // EveScene -> { stamp, box }, see sceneBBox() + this.moved_bboxes = new WeakMap(); // EveScene -> { stamp, trans, box }, see nearFarBBox() this.lights = new RC.Group; this.lights.name = "Light container"; @@ -583,17 +585,17 @@ sap.ui.define([ setTimeout(this.syncCamTransTimer.bind(this), 5000); } - /** Set scene_bbox from the scene. An empty scene gets a fallback box of - * +-100. Returns true when the scene was empty. */ + /** Set scene_bbox to the union of the boxes of the scenes this viewer + * shows. Lights, the axis and the centre marker are outside the scenes + * and do not count. An empty union gets a fallback box of +-100. + * Returns true when the union was empty. */ recalcSceneBBox() { - // Exclude the axis: it is built from this box, and its ticks reach past - // it, so including it would grow the box on every rebuild. Its label - // vertices are screen-space vec2, which expandByObject would read as 3D. - const ax = this.axis3d ? this.axis3d.group : null; - if (ax) this.scene.remove(ax); - this.scene_bbox.setFromObject( this.scene ); - if (ax) this.scene.add(ax); + this.scene.updateMatrixWorld(); + this.scene_bbox.makeEmpty(); + for (const s of this.controller.created_scenes || []) + if (!s.IsOverlay) + this.scene_bbox.union(this.sceneBBox(s)); const empty = this.scene_bbox.isEmpty(); if (empty) { @@ -608,6 +610,62 @@ sap.ui.define([ return empty; } + /** The box of EveScene `s`: the boxes the server sent for its elements, + * in each element's frame, moved into world space by the matrix of the + * element's node. Elements count whether visible or not; text and other + * screen-space elements send no box. Kept until s.content_stamp changes. */ + sceneBBox(s) + { + let c = this.scene_bboxes.get(s); + if (c && c.stamp === s.content_stamp) return c.box; + + const box = this.computeSceneBBox(s); + this.scene_bboxes.set(s, { stamp: s.content_stamp, trans: s.trans_stamp, box: box }); + return box; + } + + /** The box near and far are fitted to: render_bbox, grown by the box of + * every scene whose elements moved since its sceneBBox() was made, at + * their current matrices. A declared axes_bbox is used as it is. */ + nearFarBBox() + { + const base = this.render_bbox || this.scene_bbox; + if (this.axes_bbox) return base; + + let box = null; + for (const s of this.controller.created_scenes || []) { + if (s.IsOverlay) continue; + const c = this.scene_bboxes.get(s); + if (!c || (c.stamp === s.content_stamp && c.trans === s.trans_stamp)) continue; + + let m = this.moved_bboxes.get(s); + if (!m || m.stamp !== s.content_stamp || m.trans !== s.trans_stamp) { + this.scene.updateMatrixWorld(); + m = { stamp: s.content_stamp, trans: s.trans_stamp, box: this.computeSceneBBox(s) }; + this.moved_bboxes.set(s, m); + } + (box ||= base.clone()).union(m.box); + } + return box || base; + } + + /** The boxes the server sent for the elements of EveScene `s`, moved + * into world space by the matrices of the elements' nodes. */ + computeSceneBBox(s) + { + const mgr = this.get_manager(); + let box = new RC.Box3(), eb = new RC.Box3(); + for (const [id, node] of s.id2node_map) { + const el = mgr.GetElement(id); + const bb = el && el.bbox; + if (!Array.isArray(bb)) continue; + eb.min.set(bb[0], bb[1], bb[2]); + eb.max.set(bb[3], bb[4], bb[5]); + box.union(eb.applyMatrix4(node.matrixWorld)); + } + return box; + } + /** The box the camera's near and far planes are fitted to: axes_bbox or * scene_bbox, widened by Axis3D.getRenderMargin() so that axis ticks and * labels outside the box are not clipped. Call it when the scene extent @@ -685,7 +743,8 @@ sap.ui.define([ else { this.controls.setCamBaseMtx(new RC.Vector3(v1[0], v1[1], v1[2]), new RC.Vector3(v2[0], v2[1], v2[2])); - let ey = 1.02 * extV.y; + // Fit the height, or the width if that is what limits the panel. + let ey = 1.02 * Math.max(extV.y, extV.x * this.get_height() / this.get_width()); let ex = ey / this.get_height() * this.get_width(); this.camera._left = -ex; this.camera._right = ex; @@ -854,7 +913,7 @@ sap.ui.define([ this.positionCameraAndLights(); } if (this.camera.isPerspectiveCamera) { - this.camera.optimizeNearFar(this.render_bbox || this.scene_bbox); + this.camera.optimizeNearFar(this.nearFarBBox()); } if (this.canvas.width <= 0 || this.canvas.height <= 0) return; diff --git a/ui5/eve7/lib/Motion.js b/ui5/eve7/lib/Motion.js index 499e6010e989a..0b7b0141a43c9 100644 --- a/ui5/eve7/lib/Motion.js +++ b/ui5/eve7/lib/Motion.js @@ -90,6 +90,7 @@ sap.ui.define([], function() { m[o] = r[cIdx*3]; m[o+1] = r[cIdx*3+1]; m[o+2] = r[cIdx*3+2]; } e.obj3d.matrixChanged(); + if (e.scene) ++e.scene.trans_stamp; } this.viewer.request_render(); } @@ -126,8 +127,10 @@ sap.ui.define([], function() { //-------------------------------------------------------------------- - /** Take the `mot` block of a transformation update. Null clears it. */ - update(id, obj3d, mot) { + /** Take the `mot` block of a transformation update. Null clears it. + * `scene` is the EveScene of `obj3d`; moving the object raises its + * trans_stamp. */ + update(id, obj3d, mot, scene) { if (!mot) { this.remove(id); return; } this.noteServerTime(mot.t0); @@ -137,6 +140,7 @@ sap.ui.define([], function() { this.objects.set(id, { obj3d: obj3d, + scene: scene, // Where the server says it was at t0 -- the matrix it sent in the // same message, before anything here has touched it. The rotation // is kept as the three basis columns, scale and all. @@ -157,6 +161,13 @@ sap.ui.define([], function() { this.objects.delete(id); } + /** Move the trajectory of element `id` to `obj3d`, which takes over the + * element's matrix, as when EveScene gives a leaf a group node. */ + retarget(id, obj3d) { + const e = this.objects.get(id); + if (e) e.obj3d = obj3d; + } + clear() { this.objects.clear(); this.stop(); @@ -246,7 +257,11 @@ sap.ui.define([], function() { touched = true; } - if (touched) { e.obj3d.matrixChanged(); moved = true; } + if (touched) { + e.obj3d.matrixChanged(); + if (e.scene) ++e.scene.trans_stamp; + moved = true; + } } return moved; } From e14316a773a85e39c0eb6e8a931e51bfdaf9500b Mon Sep 17 00:00:00 2001 From: Matevz Tadel Date: Tue, 20 Oct 2026 11:42:00 -0700 Subject: [PATCH 5/7] [eve7] Keep motion through full updates and for new clients A moving element lost its motion on a full update, and a new client or a new viewer showed no motion until the next transformation change. The client's clock came from motion start times, which are old for a new client. - Wrote the motion next to the matrix in a full write, which holds the whole transformation. Added kCBFullRebuild as an alias of kCBObjProps. - InitMainTrans() and DestroyMainTrans() stamp kCBTrans; InitMainTrans() also clears the motion. - Added server_time to scene and Motion messages, as the last field with a fixed width. REveUtil::AppendServerTime() adds it after the dump and StampServerTime() sets it in place just before each send, so streaming time does not count. The full stream carries it in a trailer entry, which the client takes off. - Moved the server clock from Motion to EveManager, which feeds it from server_time. - Kept the trajectory on the element's node through rebuilds. A full update and a new node register it from the element's motion, or drop it. - Renamed the message key mot to motion. Assisted-by: Claude:Opus 5.5 --- graf3d/eve7/inc/ROOT/REveElement.hxx | 3 +- graf3d/eve7/inc/ROOT/REveUtil.hxx | 3 ++ graf3d/eve7/src/REveElement.cxx | 47 +++++++++++++------- graf3d/eve7/src/REveManager.cxx | 7 ++- graf3d/eve7/src/REveScene.cxx | 4 ++ graf3d/eve7/src/REveUtil.cxx | 35 +++++++++++++++ graf3d/eve7/test/change_cycle.cxx | 66 +++++++++++++++++++++++++++- graf3d/eve7/test/streaming.cxx | 4 +- graf3d/eve7/test/trans_motion.cxx | 37 ++++++++++++++-- ui5/eve7/lib/EveManager.js | 43 +++++++++++++++++- ui5/eve7/lib/EveScene.js | 34 ++++++++++---- ui5/eve7/lib/Motion.js | 49 +++++---------------- 12 files changed, 262 insertions(+), 70 deletions(-) diff --git a/graf3d/eve7/inc/ROOT/REveElement.hxx b/graf3d/eve7/inc/ROOT/REveElement.hxx index d7b276c9cf125..7a90ff973dcb8 100644 --- a/graf3d/eve7/inc/ROOT/REveElement.hxx +++ b/graf3d/eve7/inc/ROOT/REveElement.hxx @@ -378,7 +378,8 @@ public: { kCBCoreJson = BIT(0), // Core JSON changed (colour, name, ...); render data did not. kCBTrans = BIT(1), // Transformation changed; cheap update, no rebuild. - kCBObjProps = BIT(2), // Object changed, requires dropping its display-lists. + kCBObjProps = BIT(2), // Full rebuild: render data and the full state, transformation included. + kCBFullRebuild = kCBObjProps, kCBVisibility = BIT(3), // Rendering of self/children changed. kCBElementAdded = BIT(4) // Element was added to a new parent. // kCBElementRemoved = BIT() // Element was removed from a parent. diff --git a/graf3d/eve7/inc/ROOT/REveUtil.hxx b/graf3d/eve7/inc/ROOT/REveUtil.hxx index dabb60e63749d..67a1cb6cbcf86 100644 --- a/graf3d/eve7/inc/ROOT/REveUtil.hxx +++ b/graf3d/eve7/inc/ROOT/REveUtil.hxx @@ -14,6 +14,7 @@ #include "REveTypes.hxx" +#include #include #include @@ -62,6 +63,8 @@ public: // Math utilities static double ServerTimeMs(); + static void AppendServerTime(std::string &msg); + static void StampServerTime(std::string &msg); static Bool_t IsU1IntervalContainedByMinMax(Float_t minM, Float_t maxM, Float_t minQ, Float_t maxQ); static Bool_t IsU1IntervalOverlappingByMinMax(Float_t minM, Float_t maxM, Float_t minQ, Float_t maxQ); diff --git a/graf3d/eve7/src/REveElement.cxx b/graf3d/eve7/src/REveElement.cxx index 202af31eeb1fd..937040b3619b4 100644 --- a/graf3d/eve7/src/REveElement.cxx +++ b/graf3d/eve7/src/REveElement.cxx @@ -837,27 +837,32 @@ REveTrans &REveElement::RefMainTrans() } //////////////////////////////////////////////////////////////////////////////// -/// Initialize the main transformation to identity matrix. +/// Initialize the main transformation to identity matrix, without motion. /// If can_edit is true (default), the user will be able to edit the /// transformation parameters via GUI. void REveElement::InitMainTrans(Bool_t can_edit) { - if (fMainTrans) + if (fMainTrans) { fMainTrans->UnitTrans(); - else + fMainTrans->ClearMotion(); + } else { fMainTrans = std::make_unique(); + } fCanEditMainTrans = can_edit; + StampTrans(); } //////////////////////////////////////////////////////////////////////////////// /// Destroy the main transformation matrix, it will always be taken /// as identity. Editing of transformation parameters is disabled. +/// The motion goes with it. void REveElement::DestroyMainTrans() { fMainTrans.reset(nullptr); fCanEditMainTrans = kFALSE; + StampTrans(); } //////////////////////////////////////////////////////////////////////////////// @@ -1588,6 +1593,23 @@ std::string TransBase64(const REveTrans &t) return TBase64::Encode(reinterpret_cast(t.Array()), 16 * sizeof(Double_t)).Data(); } +//////////////////////////////////////////////////////////////////////////////// +/// The motion of `d` as JSON: start time, velocity, acceleration, spin and +/// trust window, see REveTrans::SetMotion(). + +nlohmann::json MotionJson(const REveDeltaTrans &d) +{ + const REveVectorD &v = d.fVel, &a = d.fAcc, &s = d.fSpinAxis; + // clang-format off + return { {"t0", d.fMotionT0}, + {"vel", {v.fX, v.fY, v.fZ}}, + {"acc", {a.fX, a.fY, a.fZ}}, + {"axis", {s.fX, s.fY, s.fZ}}, + {"rate", d.fSpinRate}, + {"max_dt", d.fMaxDt} }; + // clang-format on +} + } // namespace //////////////////////////////////////////////////////////////////////////////// @@ -1602,20 +1624,11 @@ void REveElement::WriteTransJson(nlohmann::json &cj) cj["matrix"] = TransBase64(*fMainTrans); if (fMainTrans && fMainTrans->HasMotion()) { - const REveDeltaTrans &d = *fMainTrans->GetDeltaTrans(); - const REveVectorD &v = d.fVel, &a = d.fAcc, &s = d.fSpinAxis; - // clang-format off - cj["mot"] = { {"t0", d.fMotionT0}, - {"vel", {v.fX, v.fY, v.fZ}}, - {"acc", {a.fX, a.fY, a.fZ}}, - {"axis", {s.fX, s.fY, s.fZ}}, - {"rate", d.fSpinRate}, - {"max_dt", d.fMaxDt} }; - // clang-format on + cj["motion"] = MotionJson(*fMainTrans->GetDeltaTrans()); } else { // Sent explicitly so that a stopped element cancels the client's // trajectory: a missing field in a partial update means unchanged. - cj["mot"] = nullptr; + cj["motion"] = nullptr; } } @@ -1691,8 +1704,12 @@ Int_t REveElement::WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) } // With or without render data: the client sets it on the element's node. - if (fMainTrans) + // A full write holds the whole transformation, so no motion means none. + if (fMainTrans) { j["matrix"] = TransBase64(*fMainTrans); + if (fMainTrans->HasMotion()) + j["motion"] = MotionJson(*fMainTrans->GetDeltaTrans()); + } } return ret; diff --git a/graf3d/eve7/src/REveManager.cxx b/graf3d/eve7/src/REveManager.cxx index 81029a0ec1cb0..765f1af1cbc72 100644 --- a/graf3d/eve7/src/REveManager.cxx +++ b/graf3d/eve7/src/REveManager.cxx @@ -885,6 +885,7 @@ void REveManager::StreamToNewConnection(unsigned connid) fWorld->StreamElements(); printf(" sending json, len = %d\n", (int)fWorld->fOutputJson.size()); + REveUtil::StampServerTime(fWorld->fOutputJson); Send(connid, fWorld->fOutputJson); printf(" for now assume world-scene has no render data, binary-size=%d\n", fWorld->fTotalBinarySize); assert(fWorld->fTotalBinarySize == 0); @@ -901,6 +902,7 @@ void REveManager::StreamToNewConnection(unsigned connid) scene->StreamElements(); printf(" sending json, len = %d\n", (int)scene->fOutputJson.size()); + REveUtil::StampServerTime(scene->fOutputJson); Send(connid, scene->fOutputJson); if (scene->fTotalBinarySize > 0) { @@ -1186,9 +1188,12 @@ void REveManager::SendMotionChanges() // clang-format on std::string data = msg.dump(); + REveUtil::AppendServerTime(data); - for (auto &conn : fConnList) + for (auto &conn : fConnList) { + REveUtil::StampServerTime(data); fWebWindow->Send(conn.fId, data); + } } //////////////////////////////////////////////////////////////////////////////// diff --git a/graf3d/eve7/src/REveScene.cxx b/graf3d/eve7/src/REveScene.cxx index 6bc97059be02f..95cc14fa13277 100644 --- a/graf3d/eve7/src/REveScene.cxx +++ b/graf3d/eve7/src/REveScene.cxx @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -181,6 +182,7 @@ void REveScene::StreamElements() jarr.front()["fTotalBinarySize"] = fTotalBinarySize; fOutputJson = jarr.dump(); + REveUtil::AppendServerTime(fOutputJson); // stamped again just before each send } void REveScene::StreamJsonRecurse(REveElement *el, nlohmann::json &jarr) @@ -347,6 +349,7 @@ void REveScene::StreamRepresentationChanges() nlohmann::json msg = { {"header", jhdr}, {"arr", jarr}}; fOutputJson = msg.dump(); + REveUtil::AppendServerTime(fOutputJson); // stamped again just before each send if (gDebug > 0) Info("REveScene::StreamRepresentationChanges", "class: %s changes %s ...", GetCName(), msg.dump(1).c_str() ); @@ -356,6 +359,7 @@ void REveScene::SendChangesToSubscribers() { for (auto && client : fSubscribers) { if (!fOutputJson.empty()) { + REveUtil::StampServerTime(fOutputJson); if (gDebug > 0) printf(" sending json, len = %d --> to conn_id = %d\n", (int) fOutputJson.size(), client->fId); client->fWebWindow->Send(client->fId, fOutputJson); diff --git a/graf3d/eve7/src/REveUtil.cxx b/graf3d/eve7/src/REveUtil.cxx index ed1b39b1a64a6..9ad1c5bf42ac6 100644 --- a/graf3d/eve7/src/REveUtil.cxx +++ b/graf3d/eve7/src/REveUtil.cxx @@ -24,6 +24,7 @@ #include "TColor.h" #include +#include #include "TROOT.h" #include "TInterpreter.h" @@ -46,6 +47,40 @@ double REveUtil::ServerTimeMs() return std::chrono::duration(clock::now() - origin).count(); } +namespace { +// Width of the server_time value in characters: up to 12 integer digits of ms, +// the point and three decimals. +constexpr int kServerTimeWidth = 16; +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +/// Append "server_time" to the JSON message `msg`, a non-empty object or array: +/// as its last key, or as a last entry, a trailer. The value has a fixed width, +/// padded with spaces, so StampServerTime() finds it from the end and sets it +/// in place just before the message is sent. The client takes it as the server +/// clock at send-out, see ServerTimeMs(). + +void REveUtil::AppendServerTime(std::string &msg) +{ + const bool is_array = msg.back() == ']'; + msg.pop_back(); + msg += is_array ? ",{\"server_time\":" : ",\"server_time\":"; + msg.append(kServerTimeWidth, ' '); + msg += is_array ? "}]" : "}"; + StampServerTime(msg); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Set the "server_time" that AppendServerTime() put at the end of `msg` to now. + +void REveUtil::StampServerTime(std::string &msg) +{ + const std::size_t tail = msg.back() == ']' ? 2 : 1; + char buf[kServerTimeWidth + 1]; + snprintf(buf, sizeof(buf), "%*.3f", kServerTimeWidth, ServerTimeMs()); + msg.replace(msg.size() - tail - kServerTimeWidth, kServerTimeWidth, buf, kServerTimeWidth); +} + namespace REX = ROOT::Experimental; diff --git a/graf3d/eve7/test/change_cycle.cxx b/graf3d/eve7/test/change_cycle.cxx index 99fe41d3cee79..f676eaad862e2 100644 --- a/graf3d/eve7/test/change_cycle.cxx +++ b/graf3d/eve7/test/change_cycle.cxx @@ -203,7 +203,7 @@ TEST(REveChangeCycle, TransOnlyGoesToMotion) EXPECT_DOUBLE_EQ(m[12], 1.); EXPECT_DOUBLE_EQ(m[13], 2.); EXPECT_DOUBLE_EQ(m[14], 3.); - EXPECT_TRUE(arr[0]["mot"].is_null()); + EXPECT_TRUE(arr[0]["motion"].is_null()); EXPECT_EQ(moved->GetChangeBits(), 0); EXPECT_NE(edited->GetChangeBits(), 0); @@ -214,3 +214,67 @@ TEST(REveChangeCycle, TransOnlyGoesToMotion) EXPECT_EQ(j["arr"][0]["fElementId"], edited->GetElementId()); EXPECT_EQ(j["header"]["numRepresentationChanged"], 1); } + +// A full update writes the whole transformation, the motion included, so a +// motion set in the same cycle is not lost; without a motion "motion" is absent. +// The header carries the server time, the client's clock for motion. +TEST(REveChangeCycle, FullUpdateCarriesMotion) +{ + auto eve = REX::REveManager::Create(); + auto s = NewScene("cc_full_motion"); + auto ps = MakePoints("points", 2); + s->AddElement(ps); + + eve->BeginChange(); + ps->RefMainTrans().SetMotion({0, 0, 10}, {0, 0, 0}, 1.); + ps->StampTrans(); + ps->StampObjProps(); + auto out = EndChangeKeepingOutput(s); + + auto &j = out.fJson; + EXPECT_TRUE(j["server_time"].is_number()); + ASSERT_EQ(j["arr"].size(), 1u); + auto &e = j["arr"][0]; + EXPECT_TRUE(e["matrix"].is_string()); + ASSERT_TRUE(e["motion"].is_object()); + EXPECT_EQ(e["motion"]["vel"], nlohmann::json({0., 0., 10.})); + + eve->BeginChange(); + ps->RefMainTrans().ClearMotion(); + ps->StampObjProps(); + auto out2 = EndChangeKeepingOutput(s); + ASSERT_EQ(out2.fJson["arr"].size(), 1u); + EXPECT_FALSE(out2.fJson["arr"][0].contains("motion")); + + // The full stream for a new client carries the motion and the time too. + ps->RefMainTrans().SetMotion({1, 0, 0}, {0, 0, 0}, 1.); + s->StreamElements(); + auto full = nlohmann::json::parse(s->GetOutputJson()); + EXPECT_TRUE(full.back()["server_time"].is_number()); + bool found = false; + for (auto &el : full) + if (el.contains("fElementId") && el["fElementId"] == ps->GetElementId()) { + found = true; + EXPECT_TRUE(el["motion"].is_object()); + } + EXPECT_TRUE(found); +} + +// Creating and destroying the main transformation are transformation changes. +TEST(REveChangeCycle, MainTransInitAndDestroyStamp) +{ + auto eve = REX::REveManager::Create(); + auto s = NewScene("cc_trans_stamp"); + auto el = new REX::REveElement("holder"); + s->AddElement(el); + + eve->BeginChange(); + el->InitMainTrans(); + EXPECT_TRUE(el->GetChangeBits() & REX::REveElement::kCBTrans); + eve->EndChange(); + + eve->BeginChange(); + el->DestroyMainTrans(); + EXPECT_TRUE(el->GetChangeBits() & REX::REveElement::kCBTrans); + eve->EndChange(); +} diff --git a/graf3d/eve7/test/streaming.cxx b/graf3d/eve7/test/streaming.cxx index 7f1dc37b10628..b91d5dfdc664b 100644 --- a/graf3d/eve7/test/streaming.cxx +++ b/graf3d/eve7/test/streaming.cxx @@ -142,7 +142,9 @@ TEST(REveStreaming, SceneStream) auto j = nlohmann::json::parse(s->GetOutputJson()); ASSERT_TRUE(j.is_array()); - ASSERT_EQ(j.size(), 4u); + // Header, scene, two elements, and the trailer with the server time. + ASSERT_EQ(j.size(), 5u); + EXPECT_TRUE(j[4]["server_time"].is_number()); EXPECT_EQ(j[0]["content"], "REveScene::StreamElements"); EXPECT_EQ(j[0]["fSceneId"], s->GetElementId()); diff --git a/graf3d/eve7/test/trans_motion.cxx b/graf3d/eve7/test/trans_motion.cxx index 03aa9dba1ba61..2bd6227afd369 100644 --- a/graf3d/eve7/test/trans_motion.cxx +++ b/graf3d/eve7/test/trans_motion.cxx @@ -19,7 +19,10 @@ #include +#include #include +#include +#include namespace REX = ROOT::Experimental; @@ -99,7 +102,7 @@ TEST(REveTransMotion, TransJson) nlohmann::json j0; el->WriteTransJson(j0); EXPECT_FALSE(j0.contains("matrix")); - EXPECT_TRUE(j0["mot"].is_null()); + EXPECT_TRUE(j0["motion"].is_null()); el->RefMainTrans().SetPos(4, 5, 6); el->RefMainTrans().SetMotion({1, 2, 3}, {0, -1, 0}, {2, 0, 0}, 0.5, 0.25); @@ -113,7 +116,7 @@ TEST(REveTransMotion, TransJson) Double_t mt[16]; std::memcpy(mt, bytes.Data(), sizeof(mt)); EXPECT_DOUBLE_EQ(mt[12], 4.); - auto &m = j["mot"]; + auto &m = j["motion"]; ASSERT_TRUE(m.is_object()); EXPECT_EQ(m["vel"], nlohmann::json({1., 2., 3.})); EXPECT_EQ(m["acc"], nlohmann::json({0., -1., 0.})); @@ -126,8 +129,34 @@ TEST(REveTransMotion, TransJson) el->RefMainTrans().ClearMotion(); nlohmann::json j2; el->WriteTransJson(j2); - EXPECT_TRUE(j2.contains("mot")); - EXPECT_TRUE(j2["mot"].is_null()); + EXPECT_TRUE(j2.contains("motion")); + EXPECT_TRUE(j2["motion"].is_null()); delete el; } + +// The server time goes last, as a key of an object message or as a trailer +// entry of an array message, and is set again in place before each send. The +// size does not change and nothing else in the message does. +TEST(REveTransMotion, ServerTimeStampedInPlace) +{ + for (std::string msg : {std::string(R"({"a":1})"), std::string(R"([{"a":1},{"b":2}])")}) { + REX::REveUtil::AppendServerTime(msg); + auto before = nlohmann::json::parse(msg); + auto &tb = before.is_array() ? before.back() : before; + const double t0 = tb["server_time"].get(); + const auto size = msg.size(); + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + REX::REveUtil::StampServerTime(msg); + EXPECT_EQ(msg.size(), size); + auto after = nlohmann::json::parse(msg); + auto &ta = after.is_array() ? after.back() : after; + EXPECT_GE(ta["server_time"].get(), t0 + 20.); + + tb.erase("server_time"); + ta.erase("server_time"); + EXPECT_EQ(after, before); + } +} + diff --git a/ui5/eve7/lib/EveManager.js b/ui5/eve7/lib/EveManager.js index d75adcb26546b..ae98d40690f74 100644 --- a/ui5/eve7/lib/EveManager.js +++ b/ui5/eve7/lib/EveManager.js @@ -22,6 +22,7 @@ sap.ui.define([], function() { constructor() { this.map = {}; // use object, not array + this.server_time_offset = null; // performance.now() - server ms this.childs = []; this.last_json = null; this.scene_changes = null; @@ -144,9 +145,13 @@ sap.ui.define([], function() { } else if (resp[0] && resp[0].content == "REveScene::StreamElements") { + // The last entry is a trailer with the server time, see + // REveScene::AppendServerTime(); the rest are header and elements. + this.noteServerTime(resp.pop().server_time); this.ImportSceneJson(resp); } else if (resp.header && resp.header.content == "ElementsRepresentaionChanges") { + this.noteServerTime(resp.server_time); this.ImportSceneChangeJson(resp); } else if (resp.content == "BeginChanges") { @@ -166,6 +171,7 @@ sap.ui.define([], function() { } } else if (resp.content == "Motion") { + this.noteServerTime(resp.server_time); this.ImportMotion(resp); } else if (resp.content == "BrowseElement") { @@ -271,6 +277,40 @@ sap.ui.define([], function() { } } + //============================================================================== + // The server clock, for motion + //============================================================================== + + /** Feed one observation of the server clock: `t` is the server time, in + * ms, at which the message carrying it was sent. + * + * A sample is performance.now() - t: the true offset plus the one-way + * delay plus local processing delay. The additions are positive, so the + * smallest sample is the best estimate, as in NTP. A sample above the + * estimate pulls it up by 0.0005 of the difference, so one fast outlier + * does not pin it low and clock drift is followed. + * + * The residual error is a constant lag of about the one-way latency. It + * shifts every object equally, so objects do not jitter against each + * other. + */ + noteServerTime(t) { + if (t === undefined) return; + const s = performance.now() - t; + + if (this.server_time_offset === null || s < this.server_time_offset) + this.server_time_offset = s; + else + this.server_time_offset += (s - this.server_time_offset) * 0.0005; + } + + /** The server's time now, in ms; 0 before the first message. */ + serverNow() { + return (this.server_time_offset === null) ? 0 : performance.now() - this.server_time_offset; + } + + //============================================================================== + ImportSceneJson(arr) { this.last_json = null; @@ -457,6 +497,7 @@ sap.ui.define([], function() { delete obj.bbox_not_in_world; delete obj.sel_list; delete obj.matrix; + delete obj.motion; jQuery.extend(obj, em); this.ParseUpdateTriggersAndProcessPostStream(obj); } @@ -821,7 +862,7 @@ sap.ui.define([], function() { // The element mirrors what the server sent; a view that is built // again, see EveScene.redrawScene(), places its node from it. if (em.matrix) obj.matrix = em.matrix; else delete obj.matrix; - obj.mot = em.mot; + obj.motion = em.motion; let scene = this.GetElement(em.fSceneId); if (scene) diff --git a/ui5/eve7/lib/EveScene.js b/ui5/eve7/lib/EveScene.js index 142e1aae4dfda..0101776a2abc6 100644 --- a/ui5/eve7/lib/EveScene.js +++ b/ui5/eve7/lib/EveScene.js @@ -186,9 +186,23 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { } this.nodeAdd(parentNode, node); this.id2node_map.set(elem.fElementId, node); + this.applyMotion(elem, node); return node; } + /** Register the trajectory of `elem` on `node`, or drop it when `elem` + * does not move. The node must hold the matrix the server sent, which + * is the start of the trajectory. */ + applyMotion(elem, node) + { + let mo = this.glctrl.viewer.motion; + if (!mo) return; + if (elem.motion) + mo.update(elem.fElementId, node, elem.motion, this); + else + mo.remove(elem.fElementId); + } + /** Give `elem`, a leaf, a group node, so that it can hold children. */ promoteToGroup(elem) { @@ -353,11 +367,8 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { /** Rebuild the drawing of `el`. With `keepMatrix`, for a rebuild without * new state, the node keeps its matrix. */ replaceElement(el, keepMatrix) { - // Drop the trajectory: it refers to the object about to be replaced. - // The next transformation update registers it on the new object. - let mo = this.glctrl.viewer.motion; - if (mo) mo.remove(el.fElementId); - + // The trajectory stays with the node; a new leaf node registers it + // again, see addElementNode(). ++this.content_stamp; let node = this.id2node_map.get(el.fElementId); @@ -436,9 +447,9 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { ++this.trans_stamp; // After the matrix is applied: Motion takes the start position from it. - // Every transformation update carries "mot"; null clears the trajectory. - if (mo && msg.mot !== undefined) - mo.update(msg.fElementId, node, msg.mot, this); + // Every transformation update carries "motion"; null clears the trajectory. + if (mo && msg.motion !== undefined) + mo.update(msg.fElementId, node, msg.motion, this); } elementsRemoved(ids) @@ -515,6 +526,13 @@ sap.ui.define(['rootui5/eve7/lib/EveManager'], function(EveManager) { this.replaceElement(el, !(msg.changeBit & this.mgr.EChangeBits.kCBObjProps)); } } + + // A full update carries the whole transformation, motion included, + // and has just set the node to the matrix the server sent. + if (msg.changeBit & this.mgr.EChangeBits.kCBObjProps) { + let node = this.id2node_map.get(el.fElementId); + if (node) this.applyMotion(el, node); + } } //============================================================================== diff --git a/ui5/eve7/lib/Motion.js b/ui5/eve7/lib/Motion.js index 0b7b0141a43c9..6c0f49e12cecf 100644 --- a/ui5/eve7/lib/Motion.js +++ b/ui5/eve7/lib/Motion.js @@ -19,7 +19,6 @@ sap.ui.define([], function() { constructor(viewer) { this.viewer = viewer; this.objects = new Map(); // element id -> entry - this.offset = null; // performance.now() - server ms this.raf = 0; this.enabled = true; @@ -96,44 +95,18 @@ sap.ui.define([], function() { } } - //-------------------------------------------------------------------- - // The shared clock - //-------------------------------------------------------------------- - - /** Feed one observation of the server clock; t0 is in server ms. - * - * A sample is performance.now() - t0: the true offset plus the one-way - * delay plus local processing delay. The additions are positive, so the - * smallest sample is the best estimate, as in NTP. A sample above the - * estimate pulls it up by 0.0005 of the difference, so one fast outlier - * does not pin it low and clock drift is followed. - * - * The residual error is a constant lag of about the one-way latency. It - * shifts every object equally, so objects do not jitter against each - * other. - */ - noteServerTime(t0) { - const s = performance.now() - t0; - - if (this.offset === null || s < this.offset) - this.offset = s; - else - this.offset += (s - this.offset) * 0.0005; - } - + /** The server's time now, in ms, from EveManager's clock. */ serverNow() { - return (this.offset === null) ? 0 : performance.now() - this.offset; + return this.viewer.get_manager().serverNow(); } //-------------------------------------------------------------------- - /** Take the `mot` block of a transformation update. Null clears it. + /** Take the `motion` block of a transformation update. Null clears it. * `scene` is the EveScene of `obj3d`; moving the object raises its * trans_stamp. */ - update(id, obj3d, mot, scene) { - if (!mot) { this.remove(id); return; } - - this.noteServerTime(mot.t0); + update(id, obj3d, motion, scene) { + if (!motion) { this.remove(id); return; } const m = obj3d._matrix ? obj3d._matrix.elements : null; if (!m) return; @@ -146,12 +119,12 @@ sap.ui.define([], function() { // is kept as the three basis columns, scale and all. p0: [m[12], m[13], m[14]], r0: [m[0], m[1], m[2], m[4], m[5], m[6], m[8], m[9], m[10]], - t0: mot.t0, - vel: mot.vel, - acc: mot.acc, - axis: mot.axis || [0, 0, 1], - rate: mot.rate || 0, - max_dt: mot.max_dt + t0: motion.t0, + vel: motion.vel, + acc: motion.acc, + axis: motion.axis || [0, 0, 1], + rate: motion.rate || 0, + max_dt: motion.max_dt }); this.start(); From a47d133e9c2b7782bf296506828e3333002fc0f2 Mon Sep 17 00:00:00 2001 From: Matevz Tadel Date: Fri, 16 Oct 2026 22:38:32 -0700 Subject: [PATCH 6/7] [eve7] Add orbits.C; trans_update.C moves four boxes round a square - orbits.C: bodies orbiting in the frames of their parents, three levels of six by default, 258 bodies; spins set with REveTrans::SetMotion() and refreshed every 2 s. Shapes as in gl/glViewerLOD.C. - trans_update.C: a still box in the centre and four boxes going round a square about it, each moved by SetTransMatrix() every 200 ms. Assisted-by: Claude:Opus 5.5 --- tutorials/visualisation/eve7/orbits.C | 156 ++++++++++++++++++++ tutorials/visualisation/eve7/trans_update.C | 71 +++++++-- 2 files changed, 211 insertions(+), 16 deletions(-) create mode 100644 tutorials/visualisation/eve7/orbits.C diff --git a/tutorials/visualisation/eve7/orbits.C b/tutorials/visualisation/eve7/orbits.C new file mode 100644 index 0000000000000..7627f6e2edcf5 --- /dev/null +++ b/tutorials/visualisation/eve7/orbits.C @@ -0,0 +1,156 @@ +/// \file +/// \ingroup tutorial_eve_7 +/// Bodies orbiting bodies orbiting bodies, built as an element hierarchy. +/// +/// The shapes are the random spheres, tubes, tube segments and cut tubes of +/// gl/glViewerLOD.C. Each body sits at a distance from the origin of a pivot, +/// a plain REveElement that spins about its local z axis, so the body moves on +/// a circle in the pivot's xy plane. Each body spins about its own z axis as +/// well. The pivots of a body's children are the body's children, so the +/// children orbit in the body's frame and follow both its orbit and its spin. +/// +/// The spins are set with REveTrans::SetMotion() and extrapolated on the +/// client. A timer advances each matrix to the current angle and sends the +/// motion again every kRefreshSec, which also brings the motion to a client +/// that connects later. +/// +/// \macro_code +/// +/// \author Matevz Tadel + +#include +#include +#include +#include +#include +#include +#include + +#include "TGeoSphere.h" +#include "TGeoTube.h" +#include "TMath.h" +#include "TRandom.h" +#include "TTimer.h" + +#include +#include + +using namespace ROOT::Experimental; + +namespace { + +const Double_t kRefreshSec = 2.; + +const Color_t kColors[] = {kRed, kGreen + 1, kBlue, kOrange, kMagenta, kCyan + 1, + kYellow + 1, kAzure + 1, kViolet, kTeal, kPink + 1, kSpring}; + +/// An element that spins about its local z axis at fRate rad/s, starting from +/// fBase at server time fT0Ms. +struct Spinner { + REveElement *fEl; + REveTrans fBase; + Double_t fRate; + Double_t fT0Ms; +}; + +std::vector gSpinners; + +/// A random shape of the kinds glViewerLOD.C uses, about `size` across. +TGeoShape *RandomShape(Double_t size) +{ + TRandom &r = *gRandom; + const Double_t rMin = r.Rndm() * 0.5 * size; + const Double_t rMax = rMin + (0.2 + r.Rndm() * 0.5) * size; + const Double_t dz = (0.2 + r.Rndm()) * size; + const Double_t phi1 = r.Rndm() * 90.; + const Double_t phi2 = phi1 + 90. + r.Rndm() * 180.; + + switch (r.Integer(4)) { + case 0: return new TGeoSphere(0., rMax); + case 1: return new TGeoTube(rMin, rMax, dz); + case 2: return new TGeoTubeSeg(rMin, rMax, dz, phi1, phi2); + default: + return new TGeoCtub(rMin, rMax, dz, phi1, phi2, r.Rndm() * .5, r.Rndm() * .5, -1. + r.Rndm() * .5, r.Rndm() * .5, + r.Rndm() * .5, 1. - r.Rndm() * .5); + } +} + +/// Give `t` a random orientation, keeping its position. +void RandomOrientation(REveTrans &t) +{ + TRandom &r = *gRandom; + t.RotateLF(1, 2, r.Uniform(0, TMath::TwoPi())); + t.RotateLF(2, 3, r.Uniform(0, TMath::Pi())); + t.RotateLF(1, 2, r.Uniform(0, TMath::TwoPi())); +} + +void AddSpinner(REveElement *el, Double_t rate) +{ + gSpinners.push_back({el, el->RefMainTrans(), rate, REveUtil::ServerTimeMs()}); +} + +/// Under `parent`, add `n` pivots, each with a body at `radius`, and recurse +/// for `levels` - 1 more levels with smaller bodies and orbits. +void AddLevel(REveElement *parent, Int_t levels, Int_t n, Double_t radius, Double_t size) +{ + TRandom &r = *gRandom; + for (Int_t i = 0; i < n; ++i) { + auto pivot = new REveElement(Form("%s pivot %d", parent->GetCName(), i)); + RandomOrientation(pivot->RefMainTrans()); + parent->AddElement(pivot); + AddSpinner(pivot, r.Uniform(0.2, 0.6) * (r.Rndm() < 0.5 ? -1 : 1)); + + auto body = new REveGeoShape(Form("%s body %d", parent->GetCName(), i)); + body->SetShape(RandomShape(size)); + body->SetMainColor(kColors[r.Integer(std::size(kColors))]); + body->RefMainTrans().SetPos(radius * r.Uniform(0.7, 1.3), 0, 0); + RandomOrientation(body->RefMainTrans()); + pivot->AddElement(body); + AddSpinner(body, r.Uniform(0.5, 1.5) * (r.Rndm() < 0.5 ? -1 : 1)); + + if (levels > 1) + AddLevel(body, levels - 1, n, 0.35 * radius, 0.4 * size); + } +} + +/// Advances every spinner to the current angle and sends the spin again. +class OrbitRefresher : public TTimer { +public: + OrbitRefresher() : TTimer(Long_t(1000 * kRefreshSec), kTRUE) {} + + Bool_t Notify() override + { + REveManager::ChangeGuard ch; + const Double_t now = REveUtil::ServerTimeMs(); + for (auto &s : gSpinners) { + REveTrans &t = s.fEl->RefMainTrans(); + t = s.fBase; + t.RotateLF(1, 2, s.fRate * (now - s.fT0Ms) / 1000.); + t.SetMotion(REveVectorD(), REveVectorD(), REveVectorD(0, 0, 1), s.fRate, 2 * kRefreshSec); + s.fEl->StampTrans(); + } + Reset(); + return kTRUE; + } +}; + +} // namespace + +void orbits(Int_t levels = 3, Int_t n = 6, UInt_t seed = 4357) +{ + auto eveMng = REveManager::Create(); + eveMng->AllowMultipleRemoteConnections(false, false); + + gRandom->SetSeed(seed); + gSpinners.clear(); + + auto system = new REveElement("Orbits"); + eveMng->GetEventScene()->AddElement(system); + AddLevel(system, levels, n, 1000, 150); + + eveMng->Show(); + + auto timer = new OrbitRefresher; + timer->Notify(); + timer->TurnOn(); +} diff --git a/tutorials/visualisation/eve7/trans_update.C b/tutorials/visualisation/eve7/trans_update.C index e06e9cb534d4f..e6469deb45c7c 100644 --- a/tutorials/visualisation/eve7/trans_update.C +++ b/tutorials/visualisation/eve7/trans_update.C @@ -2,9 +2,10 @@ /// \ingroup tutorial_eve_7 /// Move an element from the server by changing only its transformation. /// -/// One box stands still. Every 1.5 s the other is moved by SetTransMatrix(), -/// which streams only the new matrix. The client moves the object it already -/// has and rebuilds no geometry. +/// One box stands still in the centre. Four boxes go round a square about it, +/// a step every 200 ms, each moved by SetTransMatrix(), which streams only the +/// new matrix. The client moves the objects it already has and rebuilds no +/// geometry. /// /// \macro_code /// @@ -18,24 +19,55 @@ #include #include +#include + using namespace ROOT::Experimental; +/// Position (`x`, `y`) at `s` along the perimeter of a square of half-side +/// `a`, s in [0, 4) going counter-clockwise from the corner (a, -a). +static void square_pos(Double_t s, Double_t a, Double_t &x, Double_t &y) +{ + const Double_t f = 2 * a * (s - TMath::Floor(s)) - a; // -a .. a along the side + switch (Int_t(TMath::Floor(s)) % 4) { + case 0: + x = a; + y = f; + break; + case 1: + x = -f; + y = a; + break; + case 2: + x = -a; + y = -f; + break; + default: + x = f; + y = -a; + break; + } +} + class REveTransMover : public TTimer { // clang-format off - REveElement *fEl{nullptr}; - int fStep{0}; + std::vector fEls; + int fStep{0}; // clang-format on public: - REveTransMover(REveElement *el, Long_t ms) : TTimer(ms, kTRUE), fEl(el) {} + REveTransMover(std::vector els, Long_t ms) : TTimer(ms, kTRUE), fEls(std::move(els)) {} Bool_t Notify() override { REveManager::ChangeGuard ch; - REveTrans t; - t.SetPos(30.0 * TMath::Sin(0.4 * fStep), 0.0, 0.0); - fEl->SetTransMatrix(t.Array()); // a transformation-only change + for (size_t i = 0; i < fEls.size(); ++i) { + Double_t x, y; + square_pos(0.02 * fStep + i, 40, x, y); + REveTrans t; + t.SetPos(0.0, y, x); // in the yz plane, which faces the default camera + fEls[i]->SetTransMatrix(t.Array()); // a transformation-only change + } ++fStep; Reset(); @@ -64,16 +96,23 @@ void trans_update() auto eveMng = REveManager::Create(); eveMng->AllowMultipleRemoteConnections(false, false); - auto still = make_box("Still", kCyan, 10, -20); + auto still = make_box("Still", kCyan, 10, 0); eveMng->GetEventScene()->AddElement(still); - auto mover = make_box("Mover", kMagenta, 10, 20); - // Give the box a transformation from the start, so the first stream already - // carries its matrix. - mover->InitMainTrans(); - eveMng->GetEventScene()->AddElement(mover); + const Color_t colors[] = {kMagenta, kOrange, kGreen + 1, kBlue}; + std::vector movers; + for (int i = 0; i < 4; ++i) { + auto mover = make_box(Form("Mover %d", i), colors[i], 6, 0); + // Give the box a transformation from the start, so the first stream + // already carries its matrix. + mover->InitMainTrans(); + eveMng->GetEventScene()->AddElement(mover); + movers.push_back(mover); + } eveMng->Show(); - (new REveTransMover(mover, 1500))->TurnOn(); + auto timer = new REveTransMover(movers, 200); + timer->Notify(); + timer->TurnOn(); } From 2dffca1985653548f01d69d5b589988f7bb25a3b Mon Sep 17 00:00:00 2001 From: Matevz Tadel Date: Fri, 16 Oct 2026 14:43:19 -0700 Subject: [PATCH 7/7] [eve7] Box sets: axis-aligned boxes by centre and half size, corner adders, clearer names The REveBoxSet API was inconsistent and hard to understand: the docs and the drawing disagreed on where an axis-aligned box is, and the names did not say which convention a call uses. This commit sanitizes the API, keeping the old enum kBT_FreeBox and the two box registration functions AddFreeBox() and AddBox(verts) as aliases. - Stored kBT_Instanced and kBT_InstancedScaled instances as centre and half size, which is what the client draws. - AddInstance(), AddInstanceScaled(): take centre and half size. - Added AddInstanceCorner(), AddInstanceScaledCorner(): take lower corner and full size, as TEveBoxSet::AddBox(). - Removed the two axis-aligned AddBox() overloads. - Applied the default size to kBT_Instanced only. SetDefWidth() & co. set the full size; added SetDefHalfWidth() & co. for the half size. Default is 1, a unit box; boxes that do not set a size draw half as large as before. - Renamed EShape_e to EInstanceShape_e (kIS_*), SetShape() to SetInstanceShape(). - Renamed kBT_FreeBox to kBT_Vertices, BFreeBox_t to Vertices_t, AddFreeBox() to AddVertices(). - Added the box-type check to AddCone() and AddEllipticCone(); adder errors name the function and the expected box type. - ComputeBBox() adds the box of each instance; it widened the box of the positions by the largest instance. - Aligned the digit structs; removed a stale TODO. - Added a gtest for the corner adders and the default-size setters. Assisted-by: Claude:Opus 5.5 --- graf3d/eve7/inc/ROOT/REveBoxSet.hxx | 112 ++++---- graf3d/eve7/src/REveBoxSet.cxx | 243 ++++++++++-------- graf3d/eve7/test/streaming.cxx | 58 ++++- tutorials/visualisation/eve7/boxset.C | 4 +- .../visualisation/eve7/collection_proxies.C | 4 +- 5 files changed, 267 insertions(+), 154 deletions(-) diff --git a/graf3d/eve7/inc/ROOT/REveBoxSet.hxx b/graf3d/eve7/inc/ROOT/REveBoxSet.hxx index dba9e9c01a944..38f32aeac492e 100644 --- a/graf3d/eve7/inc/ROOT/REveBoxSet.hxx +++ b/graf3d/eve7/inc/ROOT/REveBoxSet.hxx @@ -29,44 +29,53 @@ class REveBoxSet: public REveDigitSet public: enum EBoxType_e { - kBT_Undef, // unknown-ignored - kBT_FreeBox, // arbitrary box: specify 8*(x,y,z) box corners - kBT_Instanced, // axis-aligned digit w/ fixed dimensions: specify (x,y,z) - kBT_InstancedScaled, // axis-aligned digit: specify (x,y,z) and (w, h, d) - kBT_InstancedScaledRotated // generic Mat4 transformation + kBT_Undef, // unknown-ignored + kBT_Vertices, // arbitrary box: specify its 8 vertices, 8*(x,y,z) + kBT_FreeBox = kBT_Vertices, // the old name of kBT_Vertices + kBT_Instanced, // axis-aligned box of the default size, see SetDefWidth(): specify its centre + kBT_InstancedScaled, // axis-aligned box: specify its centre and half size + kBT_InstancedScaledRotated // any transformation of the instance shape: specify its matrix }; - enum EShape_e { - kBox, - kHex, - kCone, - kConeCapped + /// The shape every instance of a kBT_Instanced* box set draws. + enum EInstanceShape_e { + kIS_Box, + kIS_Hex, + kIS_Cone, + kIS_ConeCapped }; - struct BFreeBox_t : public DigitBase_t { Float_t fVertices[8][3]; }; - - struct Instanced_t : public DigitBase_t { Float_t fX, fY, fZ; }; // save only position == INSTANCED_T - - struct InstancedScaled_t : public Instanced_t { Float_t fW, fH, fD; }; // scaled box INSTANCED_SCALED_T - - struct InstancedScaledRotated_t : public DigitBase_t { Float_t fMat[16]; }; // INSTANCED_SCALEDROTATED - - // ++ TODO add rotated + struct Vertices_t : public DigitBase_t { + Float_t fVertices[8][3]; + }; // the 8 vertices + struct Instanced_t : public DigitBase_t { + Float_t fX, fY, fZ; + }; // centre + struct InstancedScaled_t : public Instanced_t { + Float_t fW, fH, fD; + }; // centre and half size + struct InstancedScaledRotated_t : public DigitBase_t { + Float_t fMat[16]; + }; // column-major matrix + + using BFreeBox_t = Vertices_t; // the old name of Vertices_t protected: - EBoxType_e fBoxType; // Type of rendered box. - EShape_e fShapeType{kBox}; + EBoxType_e fBoxType; // Type of rendered box. + EInstanceShape_e fInstanceShape{kIS_Box}; // Shape of the instances of the kBT_Instanced* types. + + Float_t fDefHalfWidth{0.5}; // Half size along x of a kBT_Instanced box. + Float_t fDefHalfHeight{0.5}; // Half size along y of a kBT_Instanced box. + Float_t fDefHalfDepth{0.5}; // Half size along z of a kBT_Instanced box. - Float_t fDefWidth {1}; // Breadth assigned to first coordinate (A). - Float_t fDefHeight {1}; // Breadth assigned to second coordinate (B). - Float_t fDefDepth {1}; // Breadth assigned to third coordinate (C). + Int_t fBoxSkip; // Number of boxes to skip for each drawn box during scene rotation. - Int_t fBoxSkip; // Number of boxes to skip for each drawn box during scene rotation. + Bool_t fDrawConeCap{false}; // Close the base of a cone. - Bool_t fDrawConeCap{false}; + int fTexX{0}, fTexY{0}; // Size of the instance-data texture. - int fTexX{0}, fTexY{0}; static Int_t SizeofAtom(EBoxType_e bt); + void InstanceHalfSize(const DigitBase_t &digit, Float_t half[3]) const; void WriteShapeData(REveDigitSet::DigitBase_t &digit); unsigned int GetColorFromDigit(REveDigitSet::DigitBase_t &digit); float GetColorFromDigitAsFloat(REveDigitSet::DigitBase_t &digit); @@ -78,39 +87,52 @@ public: void Reset(EBoxType_e boxType, Bool_t valIsCol, Int_t chunkSize); void Reset(); - void AddFreeBox(const Float_t* verts); - void AddInstanceScaled(Float_t a, Float_t b, Float_t c, Float_t w, Float_t h, Float_t d); - void AddInstance(Float_t a, Float_t b, Float_t c); + void AddVertices(const Float_t *verts); + void AddFreeBox(const Float_t *verts) { AddVertices(verts); } // the old name of AddVertices() + void AddBox(const Float_t *verts) { AddVertices(verts); } // the TEveBoxSet name of AddVertices() + void AddInstanceScaled(Float_t cx, Float_t cy, Float_t cz, Float_t hw, Float_t hh, Float_t hd); + void AddInstanceScaledCorner(Float_t x, Float_t y, Float_t z, Float_t w, Float_t h, Float_t d); + void AddInstance(Float_t cx, Float_t cy, Float_t cz); + void AddInstanceCorner(Float_t x, Float_t y, Float_t z); void AddInstanceMat4(const Float_t* mat4); - void AddBox(const Float_t* verts) { AddFreeBox(verts); } - void AddBox(Float_t a, Float_t b, Float_t c, Float_t w, Float_t h, Float_t d) { AddInstanceScaled(a, b, c, w, h, d); } - void AddBox(Float_t a, Float_t b, Float_t c) { AddInstance(a, b, c);} - void AddCone(const REveVector& pos, const REveVector& dir, Float_t r); void AddEllipticCone(const REveVector& pos, const REveVector& dir, Float_t r, Float_t r2, Float_t angle=0); void AddHex(const REveVector& pos, Float_t r, Float_t angle, Float_t depth); - void SetShape(EShape_e x){fShapeType = x;} + + EInstanceShape_e GetInstanceShape() const { return fInstanceShape; } + void SetInstanceShape(EInstanceShape_e s) { fInstanceShape = s; } void ComputeBBox() override; void Test(Int_t nboxes); - Float_t GetDefWidth() const { return fDefWidth; } - Float_t GetDefHeight() const { return fDefHeight; } - Float_t GetDefDepth() const { return fDefDepth; } - Bool_t GetDrawConeCap() const { return fDrawConeCap; } - - void SetDefWidth(Float_t v) { fDefWidth = v ; } - void SetDefHeight(Float_t v) { fDefHeight = v ; } - void SetDefDepth(Float_t v) { fDefDepth = v ; } - void SetDrawConeCap(Bool_t x) { fDrawConeCap=x; StampObjProps(); } + // The size of a kBT_Instanced box, full or half. + Float_t GetDefWidth() const { return 2 * fDefHalfWidth; } + Float_t GetDefHeight() const { return 2 * fDefHalfHeight; } + Float_t GetDefDepth() const { return 2 * fDefHalfDepth; } + Float_t GetDefHalfWidth() const { return fDefHalfWidth; } + Float_t GetDefHalfHeight() const { return fDefHalfHeight; } + Float_t GetDefHalfDepth() const { return fDefHalfDepth; } + + void SetDefWidth(Float_t v) { fDefHalfWidth = 0.5f * v; } + void SetDefHeight(Float_t v) { fDefHalfHeight = 0.5f * v; } + void SetDefDepth(Float_t v) { fDefHalfDepth = 0.5f * v; } + void SetDefHalfWidth(Float_t v) { fDefHalfWidth = v; } + void SetDefHalfHeight(Float_t v) { fDefHalfHeight = v; } + void SetDefHalfDepth(Float_t v) { fDefHalfDepth = v; } + + Bool_t GetDrawConeCap() const { return fDrawConeCap; } + void SetDrawConeCap(Bool_t x) + { + fDrawConeCap = x; + StampObjProps(); + } Int_t GetBoxSkip() const { return fBoxSkip; } void SetBoxSkip(Int_t bs) { fBoxSkip = bs; } - Int_t WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) override; void BuildRenderData() override; diff --git a/graf3d/eve7/src/REveBoxSet.cxx b/graf3d/eve7/src/REveBoxSet.cxx index 8062acc299d36..ef4403f3dbef8 100644 --- a/graf3d/eve7/src/REveBoxSet.cxx +++ b/graf3d/eve7/src/REveBoxSet.cxx @@ -33,12 +33,22 @@ a signal value and a TRef. A collection of 3D-markers. The way how they are defined depends on the fBoxType data-member. - - kBT_FreeBox arbitrary box: specify 8*(x,y,z) box corners - - kBT_AABox axis-aligned box: specify (x,y,z) and (w, h, d) - - kBT_AABoxFixedDim axis-aligned box w/ fixed dimensions: specify (x,y,z) - also set fDefWidth, fDefHeight and fDefDepth - - kBT_Cone cone defined with position, axis-vector and radius - - EllipticCone cone with elliptic base (specify another radius and angle in deg) + - kBT_Vertices arbitrary box: specify its 8 vertices, 8*(x,y,z) + - kBT_InstancedScaled axis-aligned box of its own size + - kBT_Instanced axis-aligned box of the default size, set by + SetDefWidth() & co., or SetDefHalfWidth() & co. + - kBT_InstancedScaledRotated any transformation of the instance shape, a box, + hexagonal prism or cone: AddInstanceMat4(), AddHex(), + AddCone(), AddEllipticCone() + +An axis-aligned box is stored as its centre and half size, and spans centre - +half size to centre + half size. AddInstance() and AddInstanceScaled() take the +centre and the half size. AddInstanceCorner() and AddInstanceScaledCorner() take +the lower corner and the full size, as TEveBoxSet's AddBox() does. The default +size of a kBT_Instanced box is set as the full size with SetDefWidth(), +SetDefHeight() and SetDefDepth(), or as the half size with SetDefHalfWidth(), +SetDefHalfHeight() and SetDefHalfDepth(). It defaults to 1, a unit box, as in +TEveBoxSet. Each primitive can be assigned: @@ -76,7 +86,7 @@ Int_t REveBoxSet::SizeofAtom(REveBoxSet::EBoxType_e bt) switch (bt) { case kBT_Undef: return 0; - case kBT_FreeBox: return sizeof(BFreeBox_t); + case kBT_Vertices: return sizeof(Vertices_t); case kBT_Instanced: return sizeof(Instanced_t); case kBT_InstancedScaled: return sizeof(InstancedScaled_t); case kBT_InstancedScaledRotated: return sizeof(InstancedScaledRotated_t); @@ -85,6 +95,24 @@ Int_t REveBoxSet::SizeofAtom(REveBoxSet::EBoxType_e bt) return 0; } +//////////////////////////////////////////////////////////////////////////////// +/// Half size of an axis-aligned instance, kBT_Instanced or kBT_InstancedScaled, +/// as the client draws it. It can be negative where the size is. + +void REveBoxSet::InstanceHalfSize(const DigitBase_t &digit, Float_t half[3]) const +{ + if (fBoxType == kBT_InstancedScaled) { + const InstancedScaled_t &b = (const InstancedScaled_t &)digit; + half[0] = b.fW; + half[1] = b.fH; + half[2] = b.fD; + } else { + half[0] = fDefHalfWidth; + half[1] = fDefHalfHeight; + half[2] = fDefHalfDepth; + } +} + //////////////////////////////////////////////////////////////////////////////// /// Reset the data containers to zero size. /// The arguments describe the basic parameters of data storage. @@ -110,50 +138,78 @@ void REveBoxSet::Reset() //////////////////////////////////////////////////////////////////////////////// /// Create a new box from a set of 8 vertices. -/// To be used for box-type kBT_FreeBox. +/// To be used for box-type kBT_Vertices. -void REveBoxSet::AddFreeBox(const Float_t* verts) +void REveBoxSet::AddVertices(const Float_t *verts) { - static const REveException eH("REveBoxSet::AddBox "); + static const REveException eH("REveBoxSet::AddVertices "); - if (fBoxType != kBT_FreeBox) - throw(eH + "expect free box-type."); + if (fBoxType != kBT_Vertices) + throw(eH + "expect box-type kBT_Vertices."); - BFreeBox_t* b = (BFreeBox_t*) NewDigit(); + Vertices_t *b = (Vertices_t *)NewDigit(); memcpy(b->fVertices, verts, sizeof(b->fVertices)); REveShape::CheckAndFixBoxOrientationFv(b->fVertices); } //////////////////////////////////////////////////////////////////////////////// -/// Create a new axis-aligned box from at a given position and with -/// specified dimensions. -/// To be used for box-type kBT_AABox. +/// Create a new axis-aligned box with centre (cx, cy, cz) and half size +/// (hw, hh, hd). +/// To be used for box-type kBT_InstancedScaled. -void REveBoxSet::AddInstanceScaled(Float_t a, Float_t b, Float_t c, Float_t w, Float_t h, Float_t d) +void REveBoxSet::AddInstanceScaled(Float_t cx, Float_t cy, Float_t cz, Float_t hw, Float_t hh, Float_t hd) { - static const REveException eH("REveBoxSet::AddBox "); + static const REveException eH("REveBoxSet::AddInstanceScaled "); if (fBoxType != kBT_InstancedScaled) - throw(eH + "expect axis-aligned box-type."); + throw(eH + "expect box-type kBT_InstancedScaled."); InstancedScaled_t* box = (InstancedScaled_t*) NewDigit(); - box->fX = a; box->fY = b; box->fZ = c; - box->fW = w; box->fH = h; box->fD = d; + box->fX = cx; + box->fY = cy; + box->fZ = cz; + box->fW = hw; + box->fH = hh; + box->fD = hd; +} + +//////////////////////////////////////////////////////////////////////////////// +/// Create a new axis-aligned box with lower corner (x, y, z) and size +/// (w, h, d). +/// To be used for box-type kBT_InstancedScaled. + +void REveBoxSet::AddInstanceScaledCorner(Float_t x, Float_t y, Float_t z, Float_t w, Float_t h, Float_t d) +{ + AddInstanceScaled(x + 0.5f * w, y + 0.5f * h, z + 0.5f * d, 0.5f * w, 0.5f * h, 0.5f * d); } //////////////////////////////////////////////////////////////////////////////// -/// Create a new axis-aligned box from at a given position. -/// To be used for box-type kBT_AABoxFixedDim. +/// Create a new axis-aligned box with centre (cx, cy, cz) and the default +/// size, see SetDefWidth() and SetDefHalfWidth(). +/// To be used for box-type kBT_Instanced. -void REveBoxSet::AddInstance(Float_t a, Float_t b, Float_t c) +void REveBoxSet::AddInstance(Float_t cx, Float_t cy, Float_t cz) { - static const REveException eH("REveBoxSet::AddBox "); + static const REveException eH("REveBoxSet::AddInstance "); if (fBoxType != kBT_Instanced) - throw(eH + "expect axis-aligned fixed-dimension box-type."); + throw(eH + "expect box-type kBT_Instanced."); Instanced_t* box = (Instanced_t*) NewDigit(); - box->fX = a; box->fY = b; box->fZ = c; + box->fX = cx; + box->fY = cy; + box->fZ = cz; +} + +//////////////////////////////////////////////////////////////////////////////// +/// Create a new axis-aligned box with lower corner (x, y, z) and the default +/// size, see SetDefWidth() and SetDefHalfWidth(). The box is stored by its +/// centre, computed with the current default size, so set that first. +/// To be used for box-type kBT_Instanced. + +void REveBoxSet::AddInstanceCorner(Float_t x, Float_t y, Float_t z) +{ + AddInstance(x + fDefHalfWidth, y + fDefHalfHeight, z + fDefHalfDepth); } //////////////////////////////////////////////////////////////////////////////// @@ -161,9 +217,9 @@ void REveBoxSet::AddInstance(Float_t a, Float_t b, Float_t c) /// void REveBoxSet::AddInstanceMat4(const Float_t* arr) { - static const REveException eH("REveBoxSet::AddMat4Box "); + static const REveException eH("REveBoxSet::AddInstanceMat4 "); if (fBoxType != kBT_InstancedScaledRotated) - throw(eH + "expect Mat4 box-type."); + throw(eH + "expect box-type kBT_InstancedScaledRotated."); InstancedScaledRotated_t* b = (InstancedScaledRotated_t*) NewDigit(); memcpy(b->fMat, arr, sizeof(b->fMat)); @@ -171,13 +227,15 @@ void REveBoxSet::AddInstanceMat4(const Float_t* arr) //////////////////////////////////////////////////////////////////////////////// /// Create a cone with apex at pos, axis dir and radius r. -/// To be used for box-type kBT_Cone. +/// To be used for box-type kBT_InstancedScaledRotated. void REveBoxSet::AddCone(const REveVector& pos, const REveVector& dir, Float_t r) { static const REveException eH("REveBoxSet::AddCone "); using namespace TMath; - fShapeType = kCone; + if (fBoxType != kBT_InstancedScaledRotated) + throw(eH + "expect box-type kBT_InstancedScaledRotated."); + fInstanceShape = kIS_Cone; REveTrans t; float h = dir.Mag(); @@ -204,7 +262,9 @@ void REveBoxSet::AddEllipticCone(const REveVector& pos, const REveVector& dir, { static const REveException eH("REveBoxSet::AddEllipticCone "); using namespace TMath; - fShapeType = kCone; + if (fBoxType != kBT_InstancedScaledRotated) + throw(eH + "expect box-type kBT_InstancedScaledRotated."); + fInstanceShape = kIS_Cone; REveTrans t; float h = dir.Mag(); float phi = ATan2(dir.fY, dir.fX); @@ -232,9 +292,9 @@ void REveBoxSet::AddHex(const REveVector& pos, Float_t r, Float_t angle, Float_t static const REveException eH("REveBoxSet::AddHex "); if (fBoxType != kBT_InstancedScaledRotated) - throw(eH + "expect hex box-type."); + throw(eH + "expect box-type kBT_InstancedScaledRotated."); - fShapeType = kHex; + fInstanceShape = kIS_Hex; InstancedScaledRotated_t* hex = (InstancedScaledRotated_t*) NewDigit(); REveTrans t; // AMT do we need to reuse ??? @@ -270,75 +330,50 @@ void REveBoxSet::ComputeBBox() BBoxInit(); - // The client draws an instance as pos + M * (def * v), with v in the shape - // template, which is within [-1, 1]^3, and M the instance's scale or matrix. - // The box of the positions is widened on each axis by the largest reach of - // any instance along that axis. - Float_t reach[3] = {0, 0, 0}; - const Float_t def[3] = {std::abs(fDefWidth), std::abs(fDefHeight), std::abs(fDefDepth)}; - + // An instance is drawn at pos + M * v, with v in [-1, 1]^3 and M its half + // sizes or its matrix. Each instance adds its own axis-aligned box. REveChunkManager::iterator bi(fPlex); - switch (fBoxType) - { + switch (fBoxType) { - case kBT_FreeBox: - { - while (bi.next()) { - BFreeBox_t& b = * (BFreeBox_t*) bi(); - for (Int_t i = 0; i < 8; ++i) - BBoxCheckPoint(b.fVertices[i]); - } - break; + case kBT_Vertices: { + while (bi.next()) { + Vertices_t &b = *(Vertices_t *)bi(); + for (Int_t i = 0; i < 8; ++i) + BBoxCheckPoint(b.fVertices[i]); } + break; + } - case kBT_InstancedScaled: - { - while (bi.next()) { - InstancedScaled_t& b = * (InstancedScaled_t*) bi(); - BBoxCheckPoint(b.fX, b.fY, b.fZ); - reach[0] = std::max(reach[0], def[0] * std::abs(b.fW)); - reach[1] = std::max(reach[1], def[1] * std::abs(b.fH)); - reach[2] = std::max(reach[2], def[2] * std::abs(b.fD)); - } - break; + case kBT_InstancedScaled: + case kBT_Instanced: { + Float_t half[3]; + while (bi.next()) { + Instanced_t &b = *(Instanced_t *)bi(); + InstanceHalfSize(b, half); + BBoxCheckBox(REveVector(b.fX, b.fY, b.fZ), REveVector(half[0], half[1], half[2])); } + break; + } - case kBT_Instanced: - { - while (bi.next()) { - Instanced_t& b = * (Instanced_t*) bi(); - BBoxCheckPoint(b.fX, b.fY, b.fZ); - } + case kBT_InstancedScaledRotated: { + while (bi.next()) { + InstancedScaledRotated_t &b = *(InstancedScaledRotated_t *)bi(); + float *a = b.fMat; + // Column-major: column j of M is a[4j], a[4j+1], a[4j+2]. M [-1, 1]^3 + // reaches |a[k]| + |a[4+k]| + |a[8+k]| along axis k. + REveVector h; for (Int_t k = 0; k < 3; ++k) - reach[k] = def[k]; - break; - } - - case kBT_InstancedScaledRotated: - { - while (bi.next()) { - InstancedScaledRotated_t& b = * (InstancedScaledRotated_t*) bi(); - float* a = b.fMat; - BBoxCheckPoint(a[12], a[13], a[14]); - // Column-major: column j of M is a[4j], a[4j+1], a[4j+2]. - for (Int_t k = 0; k < 3; ++k) - reach[k] = std::max(reach[k], - std::abs(a[k]) * def[0] + std::abs(a[4 + k]) * def[1] + std::abs(a[8 + k]) * def[2]); - } - break; + h[k] = std::abs(a[k]) + std::abs(a[4 + k]) + std::abs(a[8 + k]); + BBoxCheckBox(REveVector(a[12], a[13], a[14]), h); } + break; + } - default: - { - throw eH + "unsupported box-type."; - } + default: { + throw eH + "unsupported box-type."; + } } // end switch box-type - - for (Int_t k = 0; k < 3; ++k) { - fBBoxMin[k] -= reach[k]; - fBBoxMax[k] += reach[k]; - } } //////////////////////////////////////////////////////////////////////////////// @@ -347,13 +382,11 @@ void REveBoxSet::ComputeBBox() Int_t REveBoxSet::WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) { j["boxType"] = int(fBoxType); - j["shapeType"] = int(fShapeType); - if (fShapeType == kCone) - { + j["shapeType"] = int(fInstanceShape); + if (fInstanceShape == kIS_Cone) { j["coneCap"] = fDrawConeCap; } - j["instanced"] = Instanced(); if (Instanced()) { @@ -384,9 +417,13 @@ Int_t REveBoxSet::WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) j["texX"] = fTexX; j["texY"] = fTexY; j["instanceFlag"] = instanceFlag; - j["defWidth"] = fDefWidth; - j["defHeight"] = fDefHeight; - j["defDepth"] = fDefDepth; + // The client scales the shape template, which is within [-1, 1]^3, by + // these: the default half size for kBT_Instanced, 1 for the types that + // carry their own size or matrix. + const bool fixed = (fBoxType == kBT_Instanced); + j["defWidth"] = fixed ? fDefHalfWidth : 1.f; + j["defHeight"] = fixed ? fDefHalfHeight : 1.f; + j["defDepth"] = fixed ? fDefHalfDepth : 1.f; // printf("TEXTURE SIZE X=%d, Y=%d\n", fTexX, fTexY); } @@ -473,8 +510,8 @@ float REveBoxSet::GetColorFromDigitAsFloat(REveDigitSet::DigitBase_t &digit) void REveBoxSet::WriteShapeData(REveDigitSet::DigitBase_t &digit) { switch (fBoxType) { - case REveBoxSet::kBT_FreeBox: { - REveBoxSet::BFreeBox_t &b = (REveBoxSet::BFreeBox_t &)(digit); + case REveBoxSet::kBT_Vertices: { + REveBoxSet::Vertices_t &b = (REveBoxSet::Vertices_t &)(digit); // vertices for (int c = 0; c < 8; c++) { for (int j = 0; j < 3; j++) @@ -512,7 +549,7 @@ void REveBoxSet::WriteShapeData(REveDigitSet::DigitBase_t &digit) break; } - default: assert(false && "REveBoxSet::BuildRenderData only kBT_FreeBox type supported"); + default: assert(false && "REveBoxSet::BuildRenderData only kBT_Vertices type supported"); } } @@ -544,7 +581,7 @@ void REveBoxSet::Test(Int_t nboxes) bool REveBoxSet::Instanced() { - return gEve->IsRCore() && (fBoxType != kBT_FreeBox); + return gEve->IsRCore() && (fBoxType != kBT_Vertices); } diff --git a/graf3d/eve7/test/streaming.cxx b/graf3d/eve7/test/streaming.cxx index b91d5dfdc664b..64a78e0439962 100644 --- a/graf3d/eve7/test/streaming.cxx +++ b/graf3d/eve7/test/streaming.cxx @@ -324,15 +324,19 @@ TEST(REveStreaming, BoxSetBBoxHoldsInstances) auto fixed = new REX::REveBoxSet("fixed"); fixed->Reset(REX::REveBoxSet::kBT_Instanced, kFALSE, 4); - fixed->SetDefWidth(2); + fixed->SetDefHalfWidth(2); + fixed->SetDefHeight(2); + fixed->SetDefDepth(2); fixed->AddInstance(10, 10, 10); expectBox(fixed, {8, 9, 9}, {12, 11, 11}); auto scaled = new REX::REveBoxSet("scaled"); scaled->Reset(REX::REveBoxSet::kBT_InstancedScaled, kFALSE, 4); + // Each instance adds its own box: the small one at -10 does not take the + // half sizes of the large one. scaled->AddInstanceScaled(10, 10, 10, 1, 2, 3); scaled->AddInstanceScaled(-10, 0, 0, 1, 1, 1); - expectBox(scaled, {-11, -2, -3}, {11, 12, 13}); + expectBox(scaled, {-11, -1, -1}, {11, 12, 13}); // A cone with its apex at the origin, 60 long along y, radius 5. Its box is // that of the template cube [-1, 1]^3 under the cone's matrix. @@ -346,6 +350,56 @@ TEST(REveStreaming, BoxSetBBoxHoldsInstances) delete cone; } +// An axis-aligned instance added by its corner and full size is stored as its +// centre and half size, which the client draws. The set's box is the union of +// the instances' boxes. +TEST(REveStreaming, BoxSetCornerAdders) +{ + REX::REveManager::Create(); + auto expectBox = [](REX::REveElement *e, REX::REveVector lo, REX::REveVector hi) { + ASSERT_EQ(e->AssertBBox(), REX::REveElement::kBBoxSet); + for (int k = 0; k < 3; ++k) { + EXPECT_NEAR(e->GetBBoxMin()[k], lo[k], 1e-4) << e->GetCName() << " min " << k; + EXPECT_NEAR(e->GetBBoxMax()[k], hi[k], 1e-4) << e->GetCName() << " max " << k; + } + }; + + // SetDefWidth() & co. take the full size, SetDefHalfWidth() & co. the half. + auto fixed = new REX::REveBoxSet("fixed"); + fixed->Reset(REX::REveBoxSet::kBT_Instanced, kFALSE, 4); + EXPECT_FLOAT_EQ(fixed->GetDefWidth(), 1.f); + EXPECT_FLOAT_EQ(fixed->GetDefHalfWidth(), 0.5f); + fixed->SetDefWidth(4); + fixed->SetDefHalfHeight(1); + fixed->SetDefDepth(2); + EXPECT_FLOAT_EQ(fixed->GetDefHalfWidth(), 2.f); + EXPECT_FLOAT_EQ(fixed->GetDefHeight(), 2.f); + fixed->AddInstanceCorner(10, 10, 10); + expectBox(fixed, {10, 10, 10}, {14, 12, 12}); + + // Boxes from (10, 10, 10) to (11, 12, 13) and from (-10, 0, 0) to (-9, 1, 1). + auto scaled = new REX::REveBoxSet("scaled"); + scaled->Reset(REX::REveBoxSet::kBT_InstancedScaled, kFALSE, 4); + scaled->UseSingleColor(); + scaled->AddInstanceScaledCorner(10, 10, 10, 1, 2, 3); + scaled->AddInstanceScaledCorner(-10, 0, 0, 1, 1, 1); + expectBox(scaled, {-10, 0, 0}, {11, 12, 13}); + + // The first texel holds the centre and the second the half size. + nlohmann::json j; + const int bin = scaled->WriteCoreJson(j, 0); + std::vector buf(bin / sizeof(float)); + scaled->GetRenderData()->Write(reinterpret_cast(buf.data()), bin); + EXPECT_FLOAT_EQ(buf[0], 10.5f); + EXPECT_FLOAT_EQ(buf[1], 11.f); + EXPECT_FLOAT_EQ(buf[2], 11.5f); + EXPECT_FLOAT_EQ(buf[4], 0.5f); + EXPECT_FLOAT_EQ(buf[6], 1.5f); + + delete fixed; + delete scaled; +} + // An element emptied of its contents sends no render data, not the render data // of what it held before. TEST(REveStreaming, EmptiedElementSendsNoRenderData) diff --git a/tutorials/visualisation/eve7/boxset.C b/tutorials/visualisation/eve7/boxset.C index d762f2380cd5c..c5cdbe7a0b807 100644 --- a/tutorials/visualisation/eve7/boxset.C +++ b/tutorials/visualisation/eve7/boxset.C @@ -30,7 +30,7 @@ REveBoxSet *boxset_free(Int_t num = 100) auto q = new REveBoxSet("BoxSet"); q->SetPalette(pal); - q->Reset(REveBoxSet::kBT_FreeBox, kFALSE, 64); + q->Reset(REveBoxSet::kBT_Vertices, kFALSE, 64); #define RND_BOX(x) (Float_t) r.Uniform(-(x), (x)) @@ -49,7 +49,7 @@ REveBoxSet *boxset_free(Int_t num = 100) x - a + RND_BOX(d), y - a + RND_BOX(d), z + a + RND_BOX(d), x - a + RND_BOX(d), y + a + RND_BOX(d), z + a + RND_BOX(d), x + a + RND_BOX(d), y + a + RND_BOX(d), z + a + RND_BOX(d), x + a + RND_BOX(d), y - a + RND_BOX(d), z + a + RND_BOX(d)}; - q->AddFreeBox(verts); + q->AddVertices(verts); q->DigitValue(r.Uniform(0, 130)); } q->RefitPlex(); diff --git a/tutorials/visualisation/eve7/collection_proxies.C b/tutorials/visualisation/eve7/collection_proxies.C index 9f58a14163bc5..d01c4cc735970 100644 --- a/tutorials/visualisation/eve7/collection_proxies.C +++ b/tutorials/visualisation/eve7/collection_proxies.C @@ -394,7 +394,7 @@ private: boxset->SetAlwaysSecSelect(true); boxset->SetDetIdsAsSecondaryIndices(true); boxset->SetSelectionMaster(((REveDataCollection *)collection)->GetItemList()); - boxset->Reset(REveBoxSet::kBT_FreeBox, true, collection->GetNItems()); + boxset->Reset(REveBoxSet::kBT_Vertices, true, collection->GetNItems()); TRandom r(0); #define RND_BOX(x) (Float_t) r.Uniform(-(x), (x)) @@ -413,7 +413,7 @@ private: x - a + RND_BOX(d), y - a + RND_BOX(d), z + a + RND_BOX(d), x - a + RND_BOX(d), y + a + RND_BOX(d), z + a + RND_BOX(d), x + a + RND_BOX(d), y + a + RND_BOX(d), z + a + RND_BOX(d), x + a + RND_BOX(d), y - a + RND_BOX(d), z + a + RND_BOX(d)}; - boxset->AddBox(verts); + boxset->AddVertices(verts); boxset->DigitValue(item->GetVisible() ? 1 : 0); if (item->GetVisible())