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 46ec9d379e370..0000000000000 Binary files a/builtins/rendercore/RenderCore-2.2.tar.gz and /dev/null differ diff --git a/builtins/rendercore/RenderCore-2.3.tar.gz b/builtins/rendercore/RenderCore-2.3.tar.gz new file mode 100644 index 0000000000000..b8ae90cac7889 Binary files /dev/null and b/builtins/rendercore/RenderCore-2.3.tar.gz differ 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/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/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..7a90ff973dcb8 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;///"); AuntList_t &RefAunts() { return fAunts; } @@ -255,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); @@ -262,6 +298,13 @@ public: virtual void BuildRenderData(); // clang-format on + virtual void ComputeBBox(); + EBBoxState AssertBBox(); + void ResetBBox() { fBBoxState = kBBoxInvalid; } + EBBoxState GetBBoxState() const { return fBBoxState; } + const REveVector &GetBBoxMin() const { return fBBoxMin; } + const REveVector &GetBBoxMax() const { return fBBoxMax; } + void* GetUserData() const { return fUserData; } void SetUserData(void* ud) { fUserData = ud; } @@ -327,16 +370,16 @@ public: // Change-stamping and change bits //--------------------------------- - // kCBTransBBox 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. No bounding box is sent despite the name. + // 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. - kCBTransBBox = BIT(1), // Transformation changed; cheap update, no rebuild. - kCBObjProps = BIT(2), // Object changed, requires dropping its display-lists. + kCBCoreJson = BIT(0), // Core JSON changed (colour, name, ...); render data did not. + kCBTrans = BIT(1), // Transformation changed; cheap update, no rebuild. + 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. @@ -358,9 +401,14 @@ protected: static void AppendMirErrorString(std::string_view err_str); public: - void StampColorSelection() { AddStamp(kCBColorSelection); } - void StampTransBBox() { AddStamp(kCBTransBBox); } - void StampObjProps() { if ( ! (fChangeBits & kCBObjProps)) AddStamp(kCBObjProps); } + void StampCoreJson() { AddStamp(kCBCoreJson); } + void StampTrans() { AddStamp(kCBTrans); } + void StampObjProps() + { + fBBoxState = kBBoxInvalid; + if (!(fChangeBits & kCBObjProps)) + AddStamp(kCBObjProps); + } void StampVisibility() { AddStamp(kCBVisibility); } void StampElementAdded() { AddStamp(kCBElementAdded); } // void StampElementRemoved() { AddStamp(kCBElementRemoved); } diff --git a/graf3d/eve7/inc/ROOT/REveGeoTopNode.hxx b/graf3d/eve7/inc/ROOT/REveGeoTopNode.hxx index 084e7821620f5..2abdf24265e5f 100644 --- a/graf3d/eve7/inc/ROOT/REveGeoTopNode.hxx +++ b/graf3d/eve7/inc/ROOT/REveGeoTopNode.hxx @@ -168,6 +168,7 @@ public: void SetGeoData(REveGeoTopNodeData *d, bool rebuild = true); Int_t WriteCoreJson(nlohmann::json &j, Int_t rnr_offset) override; void BuildRenderData() override; + void ComputeBBox() override; void GetIndicesFromBrowserStack(const std::vector &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/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/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..059e1928aa996 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; @@ -78,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/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/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/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/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..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" @@ -155,7 +156,7 @@ REveBoxProjected::~REveBoxProjected() } //////////////////////////////////////////////////////////////////////////////// -/// Compute bounding-box, virtual from TAttBBox. +/// Fill the bounding box (virtual from REveElement). void REveBoxProjected::ComputeBBox() { @@ -169,7 +170,7 @@ void REveBoxProjected::ComputeBBox() void REveBoxProjected::SetDepthLocal(Float_t d) { - SetDepthCommon(d, this, fBBox); + SetDepthCommon(d, this); } //////////////////////////////////////////////////////////////////////////////// @@ -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/REveBoxSet.cxx b/graf3d/eve7/src/REveBoxSet.cxx index 938417089a716..ef4403f3dbef8 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 @@ -31,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: @@ -74,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); @@ -83,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. @@ -108,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); } //////////////////////////////////////////////////////////////////////////////// @@ -159,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)); @@ -169,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(); @@ -202,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); @@ -230,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 ??? @@ -243,7 +305,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,54 +330,48 @@ void REveBoxSet::ComputeBBox() BBoxInit(); + // 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) - { - - 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; - } + switch (fBoxType) { - case kBT_InstancedScaled: - { - 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); - } - 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_Instanced: - { - 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); - } - 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_InstancedScaledRotated: - { - while (bi.next()) { - InstancedScaledRotated_t& b = * (InstancedScaledRotated_t*) bi(); - float* a = b.fMat; - BBoxCheckPoint(a[12], a[13], a[14]); - } - break; + 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) + 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 } @@ -326,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()) { @@ -363,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); } @@ -411,12 +469,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(); } @@ -458,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++) @@ -497,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"); } } @@ -529,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/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/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 9304f3cde2656..937040b3619b4 100644 --- a/graf3d/eve7/src/REveElement.cxx +++ b/graf3d/eve7/src/REveElement.cxx @@ -25,10 +25,14 @@ #include "TClass.h" #include "TPRegexp.h" #include "TROOT.h" +#include "TBase64.h" #include "TColor.h" #include #include +#include +#include +#include #include @@ -287,12 +291,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(); } //////////////////////////////////////////////////////////////////////////////// @@ -405,7 +409,7 @@ void REveElement::CopyVizParams(const REveElement* el) fMainTransparency = el->fMainTransparency; if (fMainColorPtr == & fDefaultColor) fDefaultColor = el->GetMainColor(); - AddStamp(kCBColorSelection | kCBObjProps); + AddStamp(kCBCoreJson | kCBObjProps); } //////////////////////////////////////////////////////////////////////////////// @@ -728,7 +732,7 @@ void REveElement::SetMainColor(Color_t color) if (fMainColorPtr) { *fMainColorPtr = color; - StampColorSelection(); + StampCoreJson(); } PropagateMainColorToProjecteds(color, old_color); @@ -780,7 +784,7 @@ void REveElement::SetMainTransparency(Char_t t) if (t > 100) t = 100; fMainTransparency = t; - StampColorSelection(); + StampCoreJson(); PropagateMainTransparencyToProjecteds(t, old_t); } @@ -833,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(); } //////////////////////////////////////////////////////////////////////////////// @@ -862,7 +871,7 @@ void REveElement::DestroyMainTrans() void REveElement::SetTransMatrix(Double_t* carr) { RefMainTrans().SetFrom(carr); - StampTransBBox(); + StampTrans(); } //////////////////////////////////////////////////////////////////////////////// @@ -871,7 +880,26 @@ void REveElement::SetTransMatrix(Double_t* carr) void REveElement::SetTransMatrix(const TGeoMatrix& mat) { RefMainTrans().SetFrom(mat); - StampTransBBox(); + 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; } //////////////////////////////////////////////////////////////////////////////// @@ -1463,6 +1491,9 @@ void REveElement::RecheckImpliedSelections() void REveElement::AddStamp(UChar_t bits) { + if (bits & kCBObjProps) + fBBoxState = kBBoxInvalid; + if (fDestructing == kNone && fScene && fScene->IsAcceptingChanges()) { if (gDebug > 0) @@ -1478,44 +1509,126 @@ void REveElement::AddStamp(UChar_t bits) } //////////////////////////////////////////////////////////////////////////////// -/// Write transformation Matrix to render data +/// 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::BuildRenderData() +void REveElement::AssertBBoxExtents(Float_t epsilon) { - if (fMainTrans && fRenderData) { - fRenderData->SetMatrix(fMainTrans->Array()); + 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; + } } } //////////////////////////////////////////////////////////////////////////////// -/// Write the main transformation and its motion for a kCBTransBBox update. +/// 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; +} + +//////////////////////////////////////////////////////////////////////////////// +/// 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() +{ +} + +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(); +} + +//////////////////////////////////////////////////////////////////////////////// +/// 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 + +//////////////////////////////////////////////////////////////////////////////// +/// 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. 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(); - 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; } } @@ -1556,6 +1669,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) { @@ -1566,11 +1682,33 @@ 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; 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()); + } + } + } + + // With or without render data: the client sets it on the element's node. + // 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()); } } 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 cc420c9f47ad3..dc6ea746acfe9 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())); } } @@ -370,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() { @@ -387,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; } @@ -463,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); } } @@ -474,22 +466,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..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,20 +320,22 @@ 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); } } } //////////////////////////////////////////////////////////////////////////////// -/// Compute bounding-box, virtual from TAttBBox. +/// Fill the bounding box (virtual from REveElement). void REveJetConeProjected::ComputeBBox() { @@ -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); } //////////////////////////////////////////////////////////////////////////////// @@ -352,7 +368,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..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" @@ -352,7 +353,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; @@ -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/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/REvePointSet.cxx b/graf3d/eve7/src/REvePointSet.cxx index 90896e7c24feb..6788a27c84dff 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 @@ -577,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 ac850f41823ed..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" @@ -154,17 +155,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 +182,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; @@ -221,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 08bb60b63c916..2f60e970a3c3c 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; @@ -283,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 89519d562ea04..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,19 +224,13 @@ 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 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, 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->StampObjProps(); } //////////////////////////////////////////////////////////////////////////////// diff --git a/graf3d/eve7/src/REveProjectionManager.cxx b/graf3d/eve7/src/REveProjectionManager.cxx index 873bea72ac714..6453d2695d898 100644 --- a/graf3d/eve7/src/REveProjectionManager.cxx +++ b/graf3d/eve7/src/REveProjectionManager.cxx @@ -10,16 +10,22 @@ *************************************************************************/ #include -#include #include #include #include +#include +#include #include "TBuffer3D.h" #include "TBuffer3DTypes.h" #include "TClass.h" +#include +#include +#include +#include + #include /** \class REveProjectionManager @@ -34,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. @@ -82,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) { @@ -96,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() @@ -207,15 +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) { - TAttBBox* bbox = dynamic_cast(d); - if (bbox) - bbox->ComputeBBox(); - } + for (auto &d : fDependentEls) + d->ComputeBBox(); + + for (auto &n : fNieces) + n->AuntChanged(this); } //////////////////////////////////////////////////////////////////////////////// @@ -255,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); @@ -283,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); - StampTransBBox(); UpdateDependentElements(new_el); @@ -315,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); - StampTransBBox(); UpdateDependentElements(new_el); } @@ -343,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); @@ -356,7 +419,6 @@ Int_t REveProjectionManager::SubImportChildren(REveElement* el, REveElement* pro for (auto &nel: new_els) ProjectChildrenRecurse(nel); AssertBBoxExtents(0.1); - StampTransBBox(); UpdateDependentElements(proj_parent); } @@ -374,14 +436,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 +459,50 @@ void REveProjectionManager::ProjectChildren() for (auto &n : fNieces) ProjectChildrenRecurse(n); AssertBBoxExtents(0.1); - StampTransBBox(); UpdateDependentElements(this); } //////////////////////////////////////////////////////////////////////////////// -/// Virtual from TAttBBox; fill bounding-box information. +/// 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. /// -/// 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. +/// 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. -void REveProjectionManager::ComputeBBox() +const REveTrans *REveProjectionManager::GetModelTrans(REveElement *model, REveTrans &t) { - static const REveException eH("REveProjectionManager::ComputeBBox "); + 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. +/// ImportElements() and ProjectChildren() also widen it as they project. + +void REveProjectionManager::ComputeBBox() +{ if ( ! HasChildren() && ! HasNieces()) { BBoxZero(); @@ -423,4 +510,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/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/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..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) @@ -228,7 +230,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 +242,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; } @@ -315,13 +317,11 @@ void REveScene::StreamRepresentationChanges() jobj["fRnrChildren"] = el->GetRnrChildren(); } - if (bits & kCBColorSelection) - { - el->WriteCoreJson(jobj, -1); + if (bits & kCBCoreJson) { + el->WriteCoreJson(jobj, -1); } - if (bits & kCBTransBBox) - { + if (bits & kCBTrans) { el->WriteTransJson(jobj); } } @@ -349,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() ); @@ -358,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/REveStraightLineSet.cxx b/graf3d/eve7/src/REveStraightLineSet.cxx index 25fc573976168..f16f07ecb0e4e 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()) @@ -307,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/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..b61713f5d56a0 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) { @@ -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/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/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 4cc19cc42ff92..f676eaad862e2 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; @@ -125,6 +128,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) { @@ -158,7 +185,7 @@ TEST(REveChangeCycle, TransOnlyGoesToMotion) eve->BeginChange(); moved->RefMainTrans().SetPos(1, 2, 3); - moved->StampTransBBox(); + moved->StampTrans(); edited->StampObjProps(); nlohmann::json arr = nlohmann::json::array(); @@ -167,12 +194,16 @@ 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.); - EXPECT_TRUE(arr[0]["mot"].is_null()); + // 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]["motion"].is_null()); EXPECT_EQ(moved->GetChangeBits(), 0); EXPECT_NE(edited->GetChangeBits(), 0); @@ -183,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/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/graf3d/eve7/test/streaming.cxx b/graf3d/eve7/test/streaming.cxx index 9e100e4f5ce63..64a78e0439962 100644 --- a/graf3d/eve7/test/streaming.cxx +++ b/graf3d/eve7/test/streaming.cxx @@ -15,8 +15,17 @@ #include #include #include +#include +#include +#include +#include +#include #include +#include "TGeoTube.h" + +#include "TBase64.h" + #include #include @@ -62,16 +71,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; } @@ -129,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()); @@ -160,3 +175,292 @@ 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 as JSON, base64 of 16 doubles, 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_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; +} + +// 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->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, -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. + 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 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) +{ + 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. +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/graf3d/eve7/test/trans_motion.cxx b/graf3d/eve7/test/trans_motion.cxx index 34c58f8b7644d..2bd6227afd369 100644 --- a/graf3d/eve7/test/trans_motion.cxx +++ b/graf3d/eve7/test/trans_motion.cxx @@ -15,8 +15,15 @@ #include #include +#include "TBase64.h" + #include +#include +#include +#include +#include + namespace REX = ROOT::Experimental; TEST(REveTransMotion, SetMotion) @@ -95,16 +102,21 @@ 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); nlohmann::json j; el->WriteTransJson(j); - ASSERT_EQ(j["matrix"].size(), 16u); - EXPECT_DOUBLE_EQ(j["matrix"][12].get(), 4.); - auto &m = j["mot"]; + // 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["motion"]; ASSERT_TRUE(m.is_object()); EXPECT_EQ(m["vel"], nlohmann::json({1., 2., 3.})); EXPECT_EQ(m["acc"], nlohmann::json({0., -1., 0.})); @@ -117,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/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()) 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/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/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(); } diff --git a/ui5/eve7/controller/Summary.controller.js b/ui5/eve7/controller/Summary.controller.js index 2d4e7ca1ee8f3..fde83e80ce0fd 100644 --- a/ui5/eve7/controller/Summary.controller.js +++ b/ui5/eve7/controller/Summary.controller.js @@ -331,10 +331,10 @@ 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 + 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/EveElementsRCore.js b/ui5/eve7/lib/EveElementsRCore.js index dd1952d329ea3..1dce725b8fdb5 100644 --- a/ui5/eve7/lib/EveElementsRCore.js +++ b/ui5/eve7/lib/EveElementsRCore.js @@ -587,6 +587,17 @@ 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). Returns false for an + * element without one. */ + RcSetElementBBox(geo, el) { + let bb = el.bbox; + 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) { let col = RcCol(colIdx); sSize *= this.POINT_SIZE_FAC; @@ -784,9 +795,9 @@ 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]))); + // 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); @@ -878,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); @@ -1045,12 +1055,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 +1378,9 @@ 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]))); + // 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; } @@ -1711,9 +1716,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..ae98d40690f74 100644 --- a/ui5/eve7/lib/EveManager.js +++ b/ui5/eve7/lib/EveManager.js @@ -9,9 +9,20 @@ 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 + this.server_time_offset = null; // performance.now() - server ms this.childs = []; this.last_json = null; this.scene_changes = null; @@ -25,7 +36,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 = { "kCBCoreJson": 1, "kCBTrans": 2, "kCBObjProps": 4, "kCBVisibility": 8, "kCBAdded": 16 }; this.controllers = []; this.gl_controllers = []; @@ -134,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") { @@ -156,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") { @@ -233,7 +249,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; @@ -261,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; @@ -440,28 +490,26 @@ 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.bbox; + delete obj.bbox_not_in_world; delete obj.sel_list; + delete obj.matrix; + delete obj.motion; 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); } - if (em.changeBit & this.EChangeBits.kCBTransBBox) { - // 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. + if (em.changeBit & this.EChangeBits.kCBTrans) { + // 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; } } @@ -516,20 +564,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; } @@ -813,11 +859,10 @@ 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; - obj.mot = em.mot; + // 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.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 d608ce0b90ef3..0101776a2abc6 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,226 @@ 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) + { + 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 (prnt === undefined) { - prnt = this.mgr.GetElement(this.id); - res3d = []; + 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); + 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) + { + 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; + } - this.create3DObjects(elem.fRnrChildren && all_ancestor_children_visible, elem, res3d); + /** 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 +307,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 +315,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,69 +340,63 @@ 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) { + // The trajectory stays with the node; a new leaf node registers it + // again, see addElementNode(). + ++this.content_stamp; - // 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); + 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 - * kCBTransBBox change, outside the BeginChanges/EndChanges cycle. */ + * kCBTrans change, outside the BeginChanges/EndChanges cycle. */ sceneElementMotion(msg) { let el = this.mgr.GetElement(msg.fElementId); @@ -274,35 +413,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); + // Every transformation update carries "motion"; null clears the trajectory. + if (mo && msg.motion !== undefined) + mo.update(msg.fElementId, node, msg.motion, 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 +466,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,24 +491,48 @@ 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. - if (msg.changeBit & this.mgr.EChangeBits.kCBTransBBox) { + // Update the transformation in place. + if (msg.changeBit & this.mgr.EChangeBits.kCBTrans) { this.updateElementTrans(el, msg); } - // other change bits + // 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) { - if ((el.changeBit & this.mgr.EChangeBits.kCBObjProps) || (el.changeBit & this.mgr.EChangeBits.kCBColorSelection)) - { + 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); + else + rebuild = true; + } + if (rebuild) { this.need_bbox_update = true; - this.replaceElement(el); + 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/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..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; @@ -90,64 +89,42 @@ 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(); } } - //-------------------------------------------------------------------- - // 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. */ - update(id, obj3d, mot) { - if (!mot) { this.remove(id); return; } - - this.noteServerTime(mot.t0); + /** 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, motion, scene) { + if (!motion) { this.remove(id); return; } const m = obj3d._matrix ? obj3d._matrix.elements : null; if (!m) return; 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. 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(); @@ -157,6 +134,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 +230,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; }