From e788a410a58f1ba0f377d70e450d16052285d398 Mon Sep 17 00:00:00 2001 From: Nicolas RECHATIN Date: Wed, 21 Jul 2021 09:37:50 +0200 Subject: [PATCH] feature addVolume v1.0 --- src/Model/Model_Document.cpp | 9 +- src/Model/Model_Objects.h | 2 +- src/Model/Model_ResultVolume.cpp | 489 +++++++++++++++++- src/Model/Model_ResultVolume.h | 129 ++++- src/ModelAPI/CMakeLists.txt | 1 + src/ModelAPI/ModelAPI_ResultVolume.cpp | 157 ++++++ src/ModelAPI/ModelAPI_ResultVolume.h | 182 ++++++- src/ModelAPI/ModelAPI_Tools.cpp | 18 + src/ModelAPI/ModelAPI_Tools.h | 12 +- src/OperaPlugin/OperaPlugin_Volume.cpp | 36 +- src/PrimitivesPlugin/PrimitivesPlugin_Box.cpp | 27 +- 11 files changed, 989 insertions(+), 73 deletions(-) create mode 100644 src/ModelAPI/ModelAPI_ResultVolume.cpp diff --git a/src/Model/Model_Document.cpp b/src/Model/Model_Document.cpp index ab44f2686..8a1dc2f24 100644 --- a/src/Model/Model_Document.cpp +++ b/src/Model/Model_Document.cpp @@ -1571,16 +1571,17 @@ std::shared_ptr Model_Document::createConstruction( return myObjs->createConstruction(theFeatureData, theIndex); } -std::shared_ptr Model_Document::createBody( +std::shared_ptr Model_Document::createVolume( const std::shared_ptr& theFeatureData, const int theIndex) { - return myObjs->createBody(theFeatureData, theIndex); + return myObjs->createVolume(theFeatureData, theIndex); } -std::shared_ptr Model_Document::createVolume( + +std::shared_ptr Model_Document::createBody( const std::shared_ptr& theFeatureData, const int theIndex) { - return myObjs->createVolume(theFeatureData, theIndex); + return myObjs->createBody(theFeatureData, theIndex); } std::shared_ptr Model_Document::createPart( diff --git a/src/Model/Model_Objects.h b/src/Model/Model_Objects.h index d64456060..d4b6a98d7 100644 --- a/src/Model/Model_Objects.h +++ b/src/Model/Model_Objects.h @@ -125,7 +125,7 @@ class Model_Objects std::shared_ptr createBody( const std::shared_ptr& theFeatureData, const int theIndex = 0, const std::wstring& theNameShape = L""); - /// Creates a body result + /// Creates a volume result std::shared_ptr createVolume( const std::shared_ptr& theFeatureData, const int theIndex = 0, const std::wstring& theNameShape = L""); diff --git a/src/Model/Model_ResultVolume.cpp b/src/Model/Model_ResultVolume.cpp index 3145ebe9b..eda5d6984 100644 --- a/src/Model/Model_ResultVolume.cpp +++ b/src/Model/Model_ResultVolume.cpp @@ -39,19 +39,496 @@ #include // if this attribute exists, the shape is connected topology -Standard_GUID kIsConnectedTopology("e51392e0-3a4d-405d-8e36-bbfe19858ef5"); +Standard_GUID kIsConnectedTopologyVolume("e51392e0-3a4d-405d-8e36-bbfe19858ef5"); // if this attribute exists, the connected topology flag must be recomputed -Standard_GUID kUpdateConnectedTopology("01ef7a45-0bec-4266-b0b4-4aa570921818"); +Standard_GUID kUpdateConnectedTopologyVolume("01ef7a45-0bec-4266-b0b4-4aa570921818"); -Model_ResultVolume::Model_ResultVolume() : Model_ResultVolume() +Model_ResultVolume::Model_ResultVolume() : ModelAPI_ResultVolume() { myBuilder = new Model_BodyBuilder(this); - // myLastConcealed = false; - // updateSubs(shape()); // in case of open, etc. + myLastConcealed = false; + updateSubs(shape()); // in case of open, etc. } Model_ResultVolume::~Model_ResultVolume() { - // updateSubs(std::shared_ptr()); // erase sub-results + updateSubs(std::shared_ptr()); // erase sub-results delete myBuilder; } + +bool Model_ResultVolume::generated(const GeomShapePtr& theNewShape, + const std::string& theName, const bool theCheckIsInResult) +{ + bool aResult = false; + if (mySubs.size()) { // consists of subs + for (std::vector::const_iterator aSubIter = mySubs.cbegin(); + aSubIter != mySubs.cend(); + ++aSubIter) + { + const ResultVolumePtr& aSub = *aSubIter; + if (aSub->generated(theNewShape, theName, theCheckIsInResult)) + aResult = true; + } + } else { // do for this directly + if (myBuilder->generated(theNewShape, theName, theCheckIsInResult)) + aResult = true; + } + return aResult; +} + +void Model_ResultVolume::loadGeneratedShapes(const std::shared_ptr& theAlgo, + const GeomShapePtr& theOldShape, + const GeomAPI_Shape::ShapeType theShapeTypeToExplore, + const std::string& theName, + const bool theSaveOldIfNotInTree) +{ + if (mySubs.size()) { // consists of subs + for (std::vector::const_iterator aSubIter = mySubs.cbegin(); + aSubIter != mySubs.cend(); + ++aSubIter) + { + const ResultVolumePtr& aSub = *aSubIter; + aSub->loadGeneratedShapes( + theAlgo, theOldShape, theShapeTypeToExplore, theName, theSaveOldIfNotInTree); + } + } else { // do for this directly + myBuilder->loadGeneratedShapes( + theAlgo, theOldShape, theShapeTypeToExplore, theName, theSaveOldIfNotInTree); + } +} + +void Model_ResultVolume::loadModifiedShapes(const std::shared_ptr& theAlgo, + const GeomShapePtr& theOldShape, + const GeomAPI_Shape::ShapeType theShapeTypeToExplore, + const std::string& theName) +{ + if (mySubs.size()) { // consists of subs + // optimization of getting of new shapes for specific sub-result + if (!theAlgo->isNewShapesCollected(theOldShape, theShapeTypeToExplore)) + theAlgo->collectNewShapes(theOldShape, theShapeTypeToExplore); + std::vector::const_iterator aSubIter = mySubs.cbegin(); + for(; aSubIter != mySubs.cend(); aSubIter++) { + (*aSubIter)->loadModifiedShapes(theAlgo, theOldShape, theShapeTypeToExplore, theName); + } + } else { // do for this directly + myBuilder->loadModifiedShapes(theAlgo, theOldShape, theShapeTypeToExplore, theName); + } +} + +void Model_ResultVolume::loadFirstLevel(GeomShapePtr theShape, const std::string& theName) +{ + if (mySubs.size()) { // consists of subs + for (std::vector::const_iterator aSubIter = mySubs.cbegin(); + aSubIter != mySubs.cend(); + ++aSubIter) + { + const ResultVolumePtr& aSub = *aSubIter; + aSub->loadFirstLevel(theShape, theName); + } + } else { // do for this directly + myBuilder->loadFirstLevel(theShape, theName); + } +} + +int Model_ResultVolume::numberOfSubs(bool /*forTree*/) const +{ + return int(mySubs.size()); +} + +ResultVolumePtr Model_ResultVolume::subResult(const int theIndex, bool /*forTree*/) const +{ + if (theIndex >= int(mySubs.size())) + return ResultVolumePtr(); + return mySubs.at(theIndex); +} + +bool Model_ResultVolume::isSub(ObjectPtr theResult, int& theIndex) const +{ + std::map::const_iterator aFound = mySubsMap.find(theResult); + if (aFound != mySubsMap.end()) { + theIndex = aFound->second; + return true; + } + return false; +} + +void Model_ResultVolume::colorConfigInfo(std::string& theSection, std::string& theName, + std::string& theDefault) +{ + theSection = "Visualization"; + theName = "result_body_color"; + theDefault = DEFAULT_COLOR(); +} + +bool Model_ResultVolume::setDisabled(std::shared_ptr theThis, const bool theFlag) +{ + bool aChanged = ModelAPI_ResultVolume::setDisabled(theThis, theFlag); + if (aChanged) { // state is changed, so modifications are needed + updateSubs(shape(), false); // to set disabled/enabled + } + return aChanged; +} + +bool Model_ResultVolume::isConcealed() +{ + return myLastConcealed; +} + +void Model_ResultVolume::setIsConcealed(const bool theValue, const bool theForced) +{ + if (ModelAPI_ResultVolume::isConcealed() != theValue) { + ModelAPI_ResultVolume::setIsConcealed(theValue, theForced); + updateConcealment(); + } +} + +// recursively check all subs for concealment flag, returns true if everybody have "flag" state, +// in theAll returns results with "flag" state +static bool checkAllSubs(ResultVolumePtr theParent, bool theFlag, std::list& theAll) +{ + if (theParent->isConcealed() != theFlag) + theAll.push_back(theParent); + bool aResult = theParent->ModelAPI_ResultVolume::isConcealed() == theFlag; + for(int a = 0; a < theParent->numberOfSubs(); a++) { + bool aSubRes = checkAllSubs(theParent->subResult(a), theFlag, theAll); + if (theFlag) + aResult = aResult || aSubRes; // concealed: one makes concealed everyone + else + aResult = aResult && aSubRes; // not concealed: all must be not concealed + } + return aResult; +} + +void Model_ResultVolume::updateConcealment() +{ + if (myLastConcealed != ModelAPI_ResultVolume::isConcealed()) { + // check the whole tree of results: if one is concealed, everybody are concealed + ResultVolumePtr anOwner = std::dynamic_pointer_cast(data()->owner()); + if (!anOwner.get()) + return; // "this" is invalid + ResultVolumePtr aParent = ModelAPI_Tools::volumeOwner(anOwner); + while(aParent.get()) { + anOwner = aParent; + aParent = ModelAPI_Tools::volumeOwner(anOwner); + } + // iterate all results and collect all results whose state may be updated + std::list anUpdated; + bool aNewFlag = !myLastConcealed; + if (checkAllSubs(anOwner, aNewFlag, anUpdated)) { // state of everyone must be updated + std::list::iterator aRes = anUpdated.begin(); + for(; aRes != anUpdated.end(); aRes++) { + bool aLastConcealed = (*aRes)->isConcealed(); + if (aNewFlag != aLastConcealed) { + std::dynamic_pointer_cast(*aRes)->myLastConcealed = aNewFlag; + if (aNewFlag) { // become concealed, behaves like removed + ModelAPI_EventCreator::get()->sendDeleted(document(), groupName()); + } else { // become not-concealed, behaves like created + static Events_ID anEvent = Events_Loop::eventByName(EVENT_OBJECT_CREATED); + ModelAPI_EventCreator::get()->sendUpdated(*aRes, anEvent); + } + static Events_ID EVENT_DISP = // must be redisplayed in any case + Events_Loop::loop()->eventByName(EVENT_OBJECT_TO_REDISPLAY); + ModelAPI_EventCreator::get()->sendUpdated(*aRes, EVENT_DISP); + } + } + } + } +} + +void Model_ResultVolume::addShapeColor( const std::wstring& theName,std::vector& color) { + + if (myColorsShape.find(theName) == myColorsShape.end()) + myColorsShape[ theName ] = color; +} + +std::wstring Model_ResultVolume::addShapeName(std::shared_ptr theshape, + const std::wstring& theName ){ + + int indice = 1; + std::wstringstream aName; + aName << theName; + while(myNamesShape.find(aName.str()) != myNamesShape.end() ){ + aName.str(L""); + aName << theName << L"__" << indice; + indice++; + } + myNamesShape[ aName.str() ] = theshape; + + return aName.str(); +} + +std::wstring Model_ResultVolume::findShapeName(std::shared_ptr theShape){ + + TopoDS_Shape aShape = theShape->impl(); + for (std::map< std::wstring, std::shared_ptr >::iterator it = + myNamesShape.begin(); + it != myNamesShape.end(); + ++it) + { + TopoDS_Shape curSelectedShape = (*it).second->impl(); + if ((aShape.IsSame(curSelectedShape))) { + return (*it).first; + } + } + return L"material not found" ; +} + +void Model_ResultVolume::setShapeName( + std::map< std::wstring, std::shared_ptr>& theShapeName, + std::map< std::wstring, std::vector>& theColorsShape) +{ + myNamesShape = theShapeName; + myColorsShape = theColorsShape; +} + +void Model_ResultVolume::clearShapeNameAndColor(){ + myNamesShape.clear(); + myColorsShape.clear(); +} + +void Model_ResultVolume::updateSubs(const std::shared_ptr& theThisShape, + const bool theShapeChanged) +{ + static Events_Loop* aLoop = Events_Loop::loop(); + static Events_ID EVENT_DISP = aLoop->eventByName(EVENT_OBJECT_TO_REDISPLAY); + static Events_ID EVENT_UPD = aLoop->eventByName(EVENT_OBJECT_UPDATED); + static const ModelAPI_EventCreator* aECreator = ModelAPI_EventCreator::get(); + // erase flag that topology is connected: the shape is new + if (theShapeChanged && data().get()) { + TDF_Label aDataLab = std::dynamic_pointer_cast(data())->label(); + if (!aDataLab.IsNull()) { + TDataStd_UAttribute::Set(aDataLab, kUpdateConnectedTopologyVolume); + isConnectedTopology(); // to store this flag in transaction, #2630 + } + } + // iterate all sub-solids of compsolid to make sub-results synchronized with them + TopoDS_Shape aThisShape; + if (theThisShape.get()) aThisShape = theThisShape->impl(); + if (!aThisShape.IsNull() && (aThisShape.ShapeType() == TopAbs_COMPSOLID || + aThisShape.ShapeType() == TopAbs_COMPOUND)) { + bool aWasEmpty = mySubs.empty(); + Model_Objects* anObjects = std::dynamic_pointer_cast(document())->objects(); + unsigned int aSubIndex = 0; + TopoDS_Iterator aShapesIter(aThisShape); + for(; aShapesIter.More(); aShapesIter.Next(), aSubIndex++) { + std::shared_ptr aShape(new GeomAPI_Shape); + aShape->setImpl(new TopoDS_Shape(aShapesIter.Value())); + ResultVolumePtr aSub; + if (mySubs.size() <= aSubIndex) { // it is needed to create a new sub-result + std::wstring thenameshape = L""; + // find shape name read + for (std::map< std::wstring, std::shared_ptr >::iterator it = + myNamesShape.begin(); + it != myNamesShape.end(); + ++it) + { + TopoDS_Shape curSelectedShape = (*it).second->impl(); + if (!(aShapesIter.Value().IsSame(curSelectedShape))) continue; + thenameshape = (*it).first; + break; + } + aSub = anObjects->createVolume(this->data(), aSubIndex,thenameshape); + //finf color read + std::map< std::wstring, std::vector>::iterator itColor = + myColorsShape.find(thenameshape); + if (itColor != myColorsShape.end()){ + ModelAPI_Tools::setColor(aSub,(*itColor).second); + } + aSub->setShapeName(myNamesShape,myColorsShape); + mySubs.push_back(aSub); + mySubsMap[aSub] = int(mySubs.size() - 1); + if (isConcealed()) { // for issue #2579 note7 + aSub->ModelAPI_ResultVolume::setIsConcealed(true); + std::dynamic_pointer_cast(aSub)->updateConcealment(); + } + + } else { // just update shape of this result + aSub = mySubs[aSubIndex]; + } + GeomShapePtr anOldSubShape = aSub->shape(); + if (!aShape->isEqual(anOldSubShape)) { + if (myAlgo.get()) { + std::list anOldForSub; + computeOldForSub(aShape, myOlds, anOldForSub); + myIsGenerated ? aSub->storeGenerated(anOldForSub, aShape, myAlgo) : + aSub->storeModified(anOldForSub, aShape, myAlgo); + } else { + aSub->store(aShape, false); + } + aECreator->sendUpdated(aSub, EVENT_DISP); + aECreator->sendUpdated(aSub, EVENT_UPD); + } + aSub->setDisabled(aSub, isDisabled()); + } + // erase left, unused results + while(mySubs.size() > aSubIndex) { + ResultVolumePtr anErased = *(mySubs.rbegin()); + if (anErased->ModelAPI_ResultVolume::isConcealed()) { + anErased->ModelAPI_ResultVolume::setIsConcealed(false); + std::dynamic_pointer_cast(anErased)->updateConcealment(); + } + anErased->setDisabled(anErased, true); + mySubsMap.erase(anErased); + mySubs.pop_back(); + } + if (aWasEmpty) { // erase all subs + // redisplay this because result with and without subs are displayed differently + aECreator->sendUpdated(data()->owner(), EVENT_DISP); + } + cleanCash(); + } else if (!mySubs.empty()) { // erase all subs + while(!mySubs.empty()) { + ResultVolumePtr anErased = *(mySubs.rbegin()); + if (anErased->ModelAPI_ResultVolume::isConcealed()) { + anErased->ModelAPI_ResultVolume::setIsConcealed(false); + std::dynamic_pointer_cast(anErased)->updateConcealment(); + } + anErased->setDisabled(anErased, true); // even if it is invalid (to erase subs on abort/undo) + mySubs.pop_back(); + } + mySubsMap.clear(); + // redisplay this because result with and without subs are displayed differently + aECreator->sendUpdated(data()->owner(), EVENT_DISP); + } +} + +void Model_ResultVolume::updateSubs( + const GeomShapePtr& theThisShape, const std::list& theOlds, + const std::shared_ptr theMakeShape, const bool isGenerated) +{ + myAlgo = theMakeShape; + myOlds = theOlds; + myIsGenerated = isGenerated; + // to avoid changing of "isDisabled" flag in the "updateSubs" cycle + isDisabled(); + + updateSubs(theThisShape, true); + myAlgo.reset(); + myOlds.clear(); + myHistoryCash.Clear(); +} + +void Model_ResultVolume::setTextureFile(const std::string & theTextureFile) +{ + ModelAPI_Result::setTextureFile(theTextureFile); + for( auto sub : mySubs){ + sub->setTextureFile(theTextureFile); + } + for(auto map : mySubsMap){ + map.first->setTextureFile(theTextureFile); + } +} + +bool Model_ResultVolume::isConnectedTopology() +{ + TDF_Label aDataLab = std::dynamic_pointer_cast(data())->label(); + if (!aDataLab.IsNull()) { + if (aDataLab.IsAttribute(kUpdateConnectedTopologyVolume)) { // recompute state + aDataLab.ForgetAttribute(kUpdateConnectedTopologyVolume); + GeomShapePtr aShape = shape(); + if (aShape.get() && aShape->isConnectedTopology()) { + TDataStd_UAttribute::Set(aDataLab, kIsConnectedTopologyVolume); + } else { + aDataLab.ForgetAttribute(kIsConnectedTopologyVolume); + } + } + return aDataLab.IsAttribute(kIsConnectedTopologyVolume); + } + return false; // invalid case +} + +void Model_ResultVolume::cleanCash() +{ + myBuilder->cleanCash(); + for (std::vector::const_iterator aSubIter = mySubs.cbegin(); + aSubIter != mySubs.cend(); ++aSubIter) + { + const ResultVolumePtr& aSub = *aSubIter; + aSub->cleanCash(); + } +} + +// adds to the theSubSubs map all sub-shapes of theSub if it is compound of compsolid +static void collectSubs( + const GeomShapePtr theSub, TopTools_MapOfShape& theSubSubs, const bool theOneLevelMore) +{ + if (theSub->isNull()) + return; + if (theSubSubs.Add(theSub->impl())) { + bool aIsComp = theSub->isCompound() || theSub->isCompSolid(); + if (aIsComp) { + for(GeomAPI_ShapeIterator anIter(theSub); anIter.more(); anIter.next()) + collectSubs(anIter.current(), theSubSubs, theOneLevelMore); + } else if (theOneLevelMore) { + GeomAPI_Shape::ShapeType aSubType = GeomAPI_Shape::ShapeType(int(theSub->shapeType()) + 1); + if (aSubType == GeomAPI_Shape::SHAPE) + return; + if (aSubType == GeomAPI_Shape::SHELL) + aSubType = GeomAPI_Shape::FACE; + if (aSubType == GeomAPI_Shape::WIRE) + aSubType = GeomAPI_Shape::EDGE; + + for(GeomAPI_ShapeExplorer anExp(theSub, aSubType); anExp.more(); anExp.next()) { + collectSubs(anExp.current(), theSubSubs, false); + } + } + } +} + +void Model_ResultVolume::computeOldForSub(const GeomShapePtr& theSub, + const std::list& theAllOlds, std::list& theOldForSub) +{ + // the old can be also used for sub-shape of theSub; collect all subs of compound or compsolid + TopTools_MapOfShape aSubSubs; + collectSubs(theSub, aSubSubs, false); + + std::list::const_iterator aRootOlds = theAllOlds.cbegin(); + for (; aRootOlds != theAllOlds.cend(); aRootOlds++) { + // use sub-shapes of olds too if they are compounds or compsolids + TopTools_MapOfShape anOldSubs; + // iterate one level more (for intersection of solids this is face) + collectSubs(*aRootOlds, anOldSubs, true); + for (TopTools_MapOfShape::Iterator anOldIter(anOldSubs); anOldIter.More(); anOldIter.Next()) { + TopoDS_Shape anOldShape = anOldIter.Value(); + if (anOldShape.ShapeType() == TopAbs_COMPOUND || anOldShape.ShapeType() == TopAbs_SHELL || + anOldShape.ShapeType() == TopAbs_WIRE || anOldShape.ShapeType() == TopAbs_COMPSOLID) + continue; // container old-shapes are not supported by the history, may cause crash + GeomShapePtr anOldSub(new GeomAPI_Shape); + anOldSub->setImpl(new TopoDS_Shape(anOldShape)); + + ListOfShape aNews; + if (myHistoryCash.IsBound(anOldShape)) { + const TopTools_ListOfShape& aList = myHistoryCash.Find(anOldShape); + for(TopTools_ListIteratorOfListOfShape anIter(aList); anIter.More(); anIter.Next()) { + GeomShapePtr aShape(new GeomAPI_Shape); + aShape->setImpl(new TopoDS_Shape(anIter.Value())); + aNews.push_back(aShape); + } + } else { + myIsGenerated ? myAlgo->generated(anOldSub, aNews) : myAlgo->modified(anOldSub, aNews); + // MakeShape may return alone old shape if there is no history information for this input + if (aNews.size() == 1 && aNews.front()->isEqual(anOldSub)) + aNews.clear(); + // store result in the history + TopTools_ListOfShape aList; + for (ListOfShape::iterator aNewIter = aNews.begin(); aNewIter != aNews.end(); aNewIter++) { + aList.Append((*aNewIter)->impl()); + } + myHistoryCash.Bind(anOldShape, aList); + } + + for (ListOfShape::iterator aNewIter = aNews.begin(); aNewIter != aNews.end(); aNewIter++) { + if (aSubSubs.Contains((*aNewIter)->impl())) { + // check list already contains this sub + std::list::iterator aResIter = theOldForSub.begin(); + for(; aResIter != theOldForSub.end(); aResIter++) + if ((*aResIter)->isSame(anOldSub)) + break; + if (aResIter == theOldForSub.end()) + theOldForSub.push_back(anOldSub); // found old used for new theSubShape creation + break; + } + } + } + } +} diff --git a/src/Model/Model_ResultVolume.h b/src/Model/Model_ResultVolume.h index 63e5684bb..598e52db3 100644 --- a/src/Model/Model_ResultVolume.h +++ b/src/Model/Model_ResultVolume.h @@ -38,14 +38,131 @@ */ class Model_ResultVolume : public ModelAPI_ResultVolume { + /// Sub-bodies if this is compsolid or compound: zero-based index to subs + std::vector mySubs; + /// Also keep map of result to index in mySubs to facilitate speed of access from OB + std::map mySubsMap; + /// Keeps the last state of the concealment flag in order to update it when needed. + bool myLastConcealed; + /// History information for update subs + std::shared_ptr myAlgo; + /// All old shapes used for the root result construction + std::list myOlds; + /// Information about the kind of the history information: modified or generated + bool myIsGenerated; + /// Map from old shape to list of new shapes, cash for computeOldForSub method + TopTools_DataMapOfShapeListOfShape myHistoryCash; - public: - /// Removes the stored builders - MODEL_EXPORT virtual ~Model_ResultVolume(); +public: + + /// Removes the stored builders + MODEL_EXPORT virtual ~Model_ResultVolume(); + + /// Records the subshape newShape which was generated during a topological construction. + /// As an example, consider the case of a face generated in construction of a box. + MODEL_EXPORT virtual bool generated(const GeomShapePtr& theNewShape, + const std::string& theName, const bool theCheckIsInResult = true) override; + + /// load generated shapes + MODEL_EXPORT + virtual void loadGeneratedShapes(const std::shared_ptr& theAlgo, + const GeomShapePtr& theOldShape, + const GeomAPI_Shape::ShapeType theShapeTypeToExplore, + const std::string& theName = "", + const bool theSaveOldIfNotInTree = false) override; + + /// load modified shapes for sub-objects + MODEL_EXPORT + virtual void loadModifiedShapes(const std::shared_ptr& theAlgo, + const GeomShapePtr& theOldShape, + const GeomAPI_Shape::ShapeType theShapeTypeToExplore, + const std::string& theName = "") override; + + /// load shapes of the first level (to be used during shape import) + MODEL_EXPORT virtual void loadFirstLevel(GeomShapePtr theShape, const std::string& theName); + + /// Returns the number of sub-elements + MODEL_EXPORT virtual int numberOfSubs(bool forTree = false) const; + + /// Returns the sub-result by zero-base index + MODEL_EXPORT virtual ResultVolumePtr subResult(const int theIndex, + bool forTree = false) const; + + /// Returns true if feature or result belong to this composite feature as subs + /// Returns theIndex - zero based index of sub if found + MODEL_EXPORT virtual bool isSub(ObjectPtr theObject, int& theIndex) const; + + /// Returns the parameters of color definition in the resources configuration manager + MODEL_EXPORT virtual void colorConfigInfo(std::string& theSection, std::string& theName, + std::string& theDefault); + + /// Disables the result body: keeps the resulting shape as selection, but erases the underlaying + /// naming data structure if theFlag if false. Or restores every thing on theFlag is true. + MODEL_EXPORT virtual bool setDisabled(std::shared_ptr theThis, + const bool theFlag); + + /// The compsolid is concealed if at least one of the sub is concealed + MODEL_EXPORT virtual bool isConcealed(); + + /// Sets all subs as concealed in the data tree (referenced by other objects) + MODEL_EXPORT virtual void setIsConcealed(const bool theValue, const bool theForced = false); + + /// Returns true is the topology is connected. + MODEL_EXPORT virtual bool isConnectedTopology(); + + /// Cleans cash related to the already stored elements + MODEL_EXPORT virtual void cleanCash() override; + + /// sets the texture file + MODEL_EXPORT virtual void setTextureFile(const std::string & theTextureFile) override; + + +protected: + /// Makes a body on the given feature + Model_ResultVolume(); + + /// Updates the sub-bodies if shape of this object is composite-solid + void updateSubs(const std::shared_ptr& theThisShape, + const bool theShapeChanged = true); + + /// Updates the sub-bodies in accordance to the algorithm history information + void updateSubs( + const GeomShapePtr& theThisShape, const std::list& theOlds, + const std::shared_ptr theMakeShape, const bool isGenerated); + + /// Checks the state of children and parents to send events of creation/erase when needed + void updateConcealment(); + + /// Adds to theOldForSub only old shapes that where used for theSub creation + void computeOldForSub(const GeomShapePtr& theSub, + const std::list& theAllOlds, std::list& theOldForSub); + + friend class Model_Objects; + + /// Add shape Name for read shape in step file + std::wstring addShapeName(std::shared_ptr,const std::wstring& theName) override; + + /// Add color for shape Name read shape in step file + void addShapeColor( const std::wstring& theName,std::vector& color) override; + + /// Set the map of name and color read shape in step file + void setShapeName(std::map< std::wstring, + std::shared_ptr>& theShapeName, + std::map< std::wstring, + std::vector>& theColorsShape) override; + + /// find the name of shapp read in step file + std::wstring findShapeName(std::shared_ptr theShape) override; + + /// Clear the map of name and color read shape in step file + void clearShapeNameAndColor() override; + + /// map with the name read in step file and shape + std::map< std::wstring, std::shared_ptr > myNamesShape; + + /// map with the name contruct and color read + std::map< std::wstring, std::vector> myColorsShape; - protected: - /// Makes a body on the given feature - Model_ResultVolume(); }; #endif diff --git a/src/ModelAPI/CMakeLists.txt b/src/ModelAPI/CMakeLists.txt index 5a8668f2a..b9977abdf 100644 --- a/src/ModelAPI/CMakeLists.txt +++ b/src/ModelAPI/CMakeLists.txt @@ -108,6 +108,7 @@ SET(PROJECT_SOURCES ModelAPI_Plugin.cpp ModelAPI_Result.cpp ModelAPI_ResultBody.cpp + ModelAPI_ResultVolume.cpp ModelAPI_ResultConstruction.cpp ModelAPI_ResultField.cpp ModelAPI_ResultGroup.cpp diff --git a/src/ModelAPI/ModelAPI_ResultVolume.cpp b/src/ModelAPI/ModelAPI_ResultVolume.cpp new file mode 100644 index 000000000..39537a7d1 --- /dev/null +++ b/src/ModelAPI/ModelAPI_ResultVolume.cpp @@ -0,0 +1,157 @@ +// Copyright (C) 2014-2021 CEA/DEN, EDF R&D +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#include "ModelAPI_ResultVolume.h" + +#include +#include +#include + +ModelAPI_ResultVolume::ModelAPI_ResultVolume() + : myBuilder(0) +{ + myConnect = ConnectionNotComputed; +} + +ModelAPI_ResultVolume::~ModelAPI_ResultVolume() +{ +} + +std::string ModelAPI_ResultVolume::groupName() +{ + return group(); +} + +void ModelAPI_ResultVolume::store(const GeomShapePtr& theShape, + const bool theIsStoreSameShapes) +{ + myBuilder->store(theShape, theIsStoreSameShapes); + myConnect = ConnectionNotComputed; + + static Events_Loop* aLoop = Events_Loop::loop(); + static Events_ID aRedispEvent = aLoop->eventByName(EVENT_OBJECT_TO_REDISPLAY); + static const ModelAPI_EventCreator* aECreator = ModelAPI_EventCreator::get(); + aECreator->sendUpdated(data()->owner(), aRedispEvent); + + updateSubs(theShape); +} + +void ModelAPI_ResultVolume::storeGenerated(const GeomShapePtr& theFromShape, + const GeomShapePtr& theToShape) +{ + myBuilder->storeGenerated(theFromShape, theToShape); + myConnect = ConnectionNotComputed; + + static Events_Loop* aLoop = Events_Loop::loop(); + static Events_ID aRedispEvent = aLoop->eventByName(EVENT_OBJECT_TO_REDISPLAY); + static const ModelAPI_EventCreator* aECreator = ModelAPI_EventCreator::get(); + aECreator->sendUpdated(data()->owner(), aRedispEvent); + + updateSubs(theToShape); +} + +void ModelAPI_ResultVolume::storeGenerated( + const std::list& theFromShapes, const GeomShapePtr& theToShape, + const std::shared_ptr theMakeShape) +{ + myBuilder->storeGenerated(theFromShapes, theToShape, theMakeShape); + myConnect = ConnectionNotComputed; + + static Events_Loop* aLoop = Events_Loop::loop(); + static Events_ID aRedispEvent = aLoop->eventByName(EVENT_OBJECT_TO_REDISPLAY); + static const ModelAPI_EventCreator* aECreator = ModelAPI_EventCreator::get(); + aECreator->sendUpdated(data()->owner(), aRedispEvent); + + updateSubs(theToShape, theFromShapes, theMakeShape, true); +} + +void ModelAPI_ResultVolume::storeModified(const GeomShapePtr& theOldShape, + const GeomShapePtr& theNewShape, + const bool theIsCleanStored) +{ + myBuilder->storeModified(theOldShape, theNewShape, theIsCleanStored); + myConnect = ConnectionNotComputed; + + static Events_Loop* aLoop = Events_Loop::loop(); + static Events_ID aRedispEvent = aLoop->eventByName(EVENT_OBJECT_TO_REDISPLAY); + static const ModelAPI_EventCreator* aECreator = ModelAPI_EventCreator::get(); + aECreator->sendUpdated(data()->owner(), aRedispEvent); + + updateSubs(theNewShape); +} + +void ModelAPI_ResultVolume::storeModified( + const std::list& theOldShapes, const GeomShapePtr& theNewShape, + const std::shared_ptr theMakeShape) +{ + myBuilder->storeModified(theOldShapes, theNewShape, theMakeShape); + myConnect = ConnectionNotComputed; + + static Events_Loop* aLoop = Events_Loop::loop(); + static Events_ID aRedispEvent = aLoop->eventByName(EVENT_OBJECT_TO_REDISPLAY); + static const ModelAPI_EventCreator* aECreator = ModelAPI_EventCreator::get(); + aECreator->sendUpdated(data()->owner(), aRedispEvent); + + updateSubs(theNewShape, theOldShapes, theMakeShape, false); +} + +GeomShapePtr ModelAPI_ResultVolume::shape() +{ + return myBuilder->shape(); +} + +void ModelAPI_ResultVolume::generated(const GeomShapePtr& theOldShape, + const GeomShapePtr& theNewShape, + const std::string& theName) +{ + myBuilder->generated(theOldShape, theNewShape, theName); +} + +void ModelAPI_ResultVolume::modified(const GeomShapePtr& theOldShape, + const GeomShapePtr& theNewShape, + const std::string& theName) +{ + myBuilder->modified(theOldShape, theNewShape, theName); +} + + +void ModelAPI_ResultVolume::loadDeletedShapes(const GeomMakeShapePtr& theAlgo, + const GeomShapePtr& theOldShape, + const GeomAPI_Shape::ShapeType theShapeTypeToExplore, + const GeomShapePtr& theShapesToExclude) +{ + myBuilder->loadDeletedShapes(theAlgo, theOldShape, theShapeTypeToExplore, theShapesToExclude); +} + +// LCOV_EXCL_START +bool ModelAPI_ResultVolume::isConnectedTopology() +{ + if (myConnect == ConnectionNotComputed) { + myConnect = shape()->isConnectedTopology() ? IsConnected : IsNotConnected; + } + return myConnect == IsConnected; +} + +void ModelAPI_ResultVolume::setDisplayed(const bool theDisplay) +{ + ModelAPI_Result::setDisplayed(theDisplay); + for (int i = 0; i < numberOfSubs(); i++) + subResult(i)->setDisplayed(theDisplay); +} +// LCOV_EXCL_STOP diff --git a/src/ModelAPI/ModelAPI_ResultVolume.h b/src/ModelAPI/ModelAPI_ResultVolume.h index ad15c197f..3bf86b1a6 100644 --- a/src/ModelAPI/ModelAPI_ResultVolume.h +++ b/src/ModelAPI/ModelAPI_ResultVolume.h @@ -20,7 +20,12 @@ #ifndef ModelAPI_ResultVolume_H_ #define ModelAPI_ResultVolume_H_ -#include "ModelAPI_ResultBody.h" +#include "ModelAPI_Result.h" +#include +#include +#include +#include +#include class ModelAPI_BodyBuilder; class GeomAlgoAPI_MakeShape; @@ -34,18 +39,175 @@ class GeomAlgoAPI_MakeShape; * of result must be optimized. * Also provides a container of sub-body result in case it is compound or compsolid. */ -class ModelAPI_ResultVolume : public ModelAPI_ResultBody +class ModelAPI_ResultVolume : public ModelAPI_Result { - public: +public: + /// Internal enumeration for storage the information of connected topology flag + enum ConnectedTopologyFlag { + ConnectionNotComputed, ///< not yet computed + IsConnected, ///< the topology is connected + IsNotConnected ///< the topology is connected + }; - inline static std::string group() - { - static std::string MY_GROUP = "Volumes"; - return MY_GROUP; - } +protected: + /// Keeps (not persistently) the connected topology flag + ConnectedTopologyFlag myConnect; - protected: - ModelAPI_BodyBuilder* myBuilder; ///< provides the body processing in naming shape + ModelAPI_BodyBuilder* myBuilder; ///< provides the body processing in naming shape + +public: + MODELAPI_EXPORT virtual ~ModelAPI_ResultVolume(); + + /// Returns the group identifier of this result + MODELAPI_EXPORT virtual std::string groupName(); + + /// Returns the group identifier of this result + inline static std::string group() + { + static std::string MY_GROUP = "Volumes"; + return MY_GROUP; + } + + /// default color for a result body + inline static const std::string& DEFAULT_COLOR() + { + static const std::string RESULT_BODY_COLOR("200,200,230"); + return RESULT_BODY_COLOR; + } + + /// default deflection for a result body + inline static const std::string DEFAULT_DEFLECTION() + { + return "0.0001"; + } + + /// Returns the number of sub-elements + MODELAPI_EXPORT virtual int numberOfSubs(bool forTree = false) const = 0; + + /// Returns the sub-result by zero-base index + MODELAPI_EXPORT virtual std::shared_ptr subResult( + const int theIndex, bool forTree = false) const = 0; + + /// Returns true if theResult belong to this composite result as sub. + /// Returns theIndex - zero based index of sub if found + MODELAPI_EXPORT virtual bool isSub(ObjectPtr theResult, int& theIndex) const = 0; + + /// \brief Stores the shape (called by the execution method). + /// param[in] theShape shape to store. + /// param[in] theIsStoreSameShapes if false stores reference to the same shape + /// if it is already in document. + MODELAPI_EXPORT virtual void store(const GeomShapePtr& theShape, + const bool theIsStoreSameShapes = true); + + /// Stores the generated shape (called by the execution method). + MODELAPI_EXPORT virtual void storeGenerated(const GeomShapePtr& theFromShape, + const GeomShapePtr& theToShape); + + /// Stores the root modified shapes (called by the execution method). + MODELAPI_EXPORT virtual void storeGenerated( + const std::list& theFromShapes, const GeomShapePtr& theToShape, + const std::shared_ptr theMakeShape); + + /// Stores the modified shape (called by the execution method). + MODELAPI_EXPORT virtual void storeModified(const GeomShapePtr& theOldShape, + const GeomShapePtr& theNewShape, + const bool theIsCleanStored = true); + + /// Stores the root modified shapes (called by the execution method). + MODELAPI_EXPORT virtual void storeModified( + const std::list& theOldShapes, const GeomShapePtr& theNewShape, + const std::shared_ptr theMakeShape); + + /// Returns the shape-result produced by this feature + MODELAPI_EXPORT virtual GeomShapePtr shape(); + + /// Records the subshape newShape which was generated during a topological construction. + /// As an example, consider the case of a face generated in construction of a box. + /// Returns true if it is stored correctly (the final shape contains this new sub-shape) + MODELAPI_EXPORT virtual bool generated(const GeomShapePtr& theNewShape, + const std::string& theName, const bool theCheckIsInResult = true) = 0; + + /// Records the shape newShape which was generated from the shape oldShape during a topological + /// construction. As an example, consider the case of a face generated from an edge in + /// construction of a prism. + MODELAPI_EXPORT virtual void generated(const GeomShapePtr& theOldShape, + const GeomShapePtr& theNewShape, + const std::string& theName = ""); + + /// Records the shape newShape which is a modification of the shape oldShape. + /// As an example, consider the case of a face split or merged in a Boolean operation. + MODELAPI_EXPORT virtual void modified(const GeomShapePtr& theOldShape, + const GeomShapePtr& theNewShape, + const std::string& theName = ""); + + /// load deleted shapes + MODELAPI_EXPORT + virtual void loadDeletedShapes(const std::shared_ptr& theAlgo, + const GeomShapePtr& theOldShape, + const GeomAPI_Shape::ShapeType theShapeTypeToExplore, + const GeomShapePtr& theShapesToExclude = GeomShapePtr()); + + /// load and orient modified shapes + MODELAPI_EXPORT + virtual void loadModifiedShapes(const std::shared_ptr& theAlgo, + const GeomShapePtr& theOldShape, + const GeomAPI_Shape::ShapeType theShapeTypeToExplore, + const std::string& theName = "") = 0; + + /// load and orient generated shapes + MODELAPI_EXPORT + virtual void loadGeneratedShapes(const std::shared_ptr& theAlgo, + const GeomShapePtr& theOldShape, + const GeomAPI_Shape::ShapeType theShapeTypeToExplore, + const std::string& theName = "", + const bool theSaveOldIfNotInTree = false) = 0; + + /// load shapes of the first level (to be used during shape import) + MODELAPI_EXPORT virtual void loadFirstLevel(GeomShapePtr theShape, + const std::string& theName) = 0; + + /// Returns true is the topology is connected. + MODELAPI_EXPORT virtual bool isConnectedTopology() = 0; + + /// Set displayed flag to the result and all sub results + /// \param theDisplay a boolean value + MODELAPI_EXPORT virtual void setDisplayed(const bool theDisplay); + + /// Updates the sub-bodies if shape of this object is compsolid or compound + MODELAPI_EXPORT virtual void updateSubs(const GeomShapePtr& theThisShape, + const bool theShapeChanged = true) = 0; + + /// Updates the sub-bodies in accordance to the algorithm history information + MODELAPI_EXPORT virtual void updateSubs( + const GeomShapePtr& theThisShape, const std::list& theOlds, + const std::shared_ptr theMakeShape, const bool isGenerated) = 0; + + /// Cleans cash related to the already stored elements + MODELAPI_EXPORT virtual void cleanCash() = 0; + + /// Add shape Name for read shape in step file + MODELAPI_EXPORT virtual std::wstring addShapeName + (std::shared_ptr,const std::wstring& theName) = 0; + + /// Add color for shape Name read shape in step file + MODELAPI_EXPORT virtual void addShapeColor + (const std::wstring& theName,std::vector& theColor) = 0; + + /// Set the map of name and color read shape in step file + MODELAPI_EXPORT virtual void setShapeName + (std::map< std::wstring, std::shared_ptr > &theShapeName, + std::map< std::wstring, std::vector> & theColorsShape) = 0; + + /// Clear the map of name and color read shape in step file + MODELAPI_EXPORT virtual void clearShapeNameAndColor() = 0; + + /// find the name of shapp read in step file + MODELAPI_EXPORT virtual std::wstring findShapeName(std::shared_ptr theShape) = 0; + + +protected: + /// Default constructor accessible only from Model_Objects + MODELAPI_EXPORT ModelAPI_ResultVolume(); }; diff --git a/src/ModelAPI/ModelAPI_Tools.cpp b/src/ModelAPI/ModelAPI_Tools.cpp index 9b9a83531..b9d547f3a 100644 --- a/src/ModelAPI/ModelAPI_Tools.cpp +++ b/src/ModelAPI/ModelAPI_Tools.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -304,6 +305,23 @@ int bodyIndex(const ResultPtr& theSub) return anIndex; // not found } +ResultVolumePtr volumeOwner(const ResultPtr& theSub, const bool theRoot) +{ + if (theSub.get()) { + ObjectPtr aParent = theSub->document()->parent(theSub); + if (aParent.get()) { + if (theRoot) { // try to find parent of parent + ResultPtr aResultParent = std::dynamic_pointer_cast(aParent); + ResultVolumePtr aGrandParent = volumeOwner(aResultParent, true); + if (aGrandParent.get()) + aParent = aGrandParent; + } + return std::dynamic_pointer_cast(aParent); + } + } + return ResultVolumePtr(); // not found +} + bool hasSubResults(const ResultPtr& theResult) { ResultBodyPtr aCompSolid = std::dynamic_pointer_cast(theResult); diff --git a/src/ModelAPI/ModelAPI_Tools.h b/src/ModelAPI/ModelAPI_Tools.h index e740cd1c9..a74dc6568 100644 --- a/src/ModelAPI/ModelAPI_Tools.h +++ b/src/ModelAPI/ModelAPI_Tools.h @@ -28,6 +28,7 @@ class ModelAPI_Feature; class ModelAPI_Result; class ModelAPI_ResultParameter; class ModelAPI_ResultBody; +class ModelAPI_ResultVolume; class GeomAPI_Shape; class GeomAPI_ShapeHierarchy; @@ -50,7 +51,7 @@ MODELAPI_EXPORT std::shared_ptr shape( MODELAPI_EXPORT std::string getFeatureError(const std::shared_ptr& theFeature); /*! - * Searches for variable with name \param theName in \param theDocument. + * Searches for variable with name \param theName in \param theDocument. * If found, set it value in the \param outValue and returns true. * theSearcher must be located later in the history than the found variable. */ @@ -114,6 +115,15 @@ MODELAPI_EXPORT std::shared_ptr */ MODELAPI_EXPORT int bodyIndex(const std::shared_ptr& theSub); +/*! + * Returns the result - parent of this result. + * \param theSub the sub-element of composit result + * \param theRoot if it is true, returns the root father + * \returns null if it is not sub-element of composite + */ +MODELAPI_EXPORT std::shared_ptr + volumeOwner(const std::shared_ptr& theSub, const bool theRoot = false); + /*! * Returns true if the result contains a not empty list of sub results. * It processes result compsolid. diff --git a/src/OperaPlugin/OperaPlugin_Volume.cpp b/src/OperaPlugin/OperaPlugin_Volume.cpp index 5ef8b4bbb..ac2c3bfe2 100644 --- a/src/OperaPlugin/OperaPlugin_Volume.cpp +++ b/src/OperaPlugin/OperaPlugin_Volume.cpp @@ -19,20 +19,16 @@ #include -#include -#include -#include -#include -#include +#include #include +#include +#include +#include +#include +#include +#include -#include - - -#include -#include - - +#include #include #include @@ -44,26 +40,24 @@ OperaPlugin_Volume::OperaPlugin_Volume() // Nothing to do during instantiation //================================================================================================= void OperaPlugin_Volume::initAttributes() { + //Get Medium data()->addAttribute(OperaPlugin_Volume::MEDIUM(), ModelAPI_AttributeString::typeId()); - - // AttributeSelectionListPtr aList = - // std::dynamic_pointer_cast(data()->addAttribute( - // OperaPlugin_Volume::LIST_ID(), ModelAPI_AttributeSelectionList::typeId())); - + //Get Objects AttributeSelectionListPtr aList = std::dynamic_pointer_cast( - data()->addAttribute(OperaPlugin_Volume::LIST_ID(), ModelAPI_AttributeSelectionList::typeId())); + data()->addAttribute(LIST_ID(), ModelAPI_AttributeSelectionList::typeId())); aList->setWholeResultAllowed(true); // allow to select the whole result - } -#include //================================================================================================= void OperaPlugin_Volume::execute() { // Getting objects. std::cout << "Before" << std::endl; - std::cout << "EXECUTE" << std::endl; + if (results().empty() || firstResult()->isDisabled()) { // just create result if not exists + ResultPtr aGroup = document()->createVolume(data()); + setResult(aGroup); + } std::cout << "After" << std::endl; } diff --git a/src/PrimitivesPlugin/PrimitivesPlugin_Box.cpp b/src/PrimitivesPlugin/PrimitivesPlugin_Box.cpp index ccddc413d..a13d2ff98 100644 --- a/src/PrimitivesPlugin/PrimitivesPlugin_Box.cpp +++ b/src/PrimitivesPlugin/PrimitivesPlugin_Box.cpp @@ -164,27 +164,6 @@ void PrimitivesPlugin_Box::createBoxByTwoPoints() setResult(aResultBox, aResultIndex); } -//================================================================================================= -static void toto(std::shared_ptr theBoxAlgo, - std::shared_ptr theResultBox) -{ - // Load the result - theResultBox->store(theBoxAlgo->shape()); - - // Prepare the naming - theBoxAlgo->prepareNamingFaces(); - - // Insert to faces - std::map< std::string, std::shared_ptr > listOfFaces = - theBoxAlgo->getCreatedFaces(); - for (std::map< std::string, std::shared_ptr >::iterator it = listOfFaces.begin(); - it != listOfFaces.end(); - ++it) - { - theResultBox->generated((*it).second, (*it).first); - } -} - //================================================================================================= void PrimitivesPlugin_Box::createBoxByOnePointAndDims() { @@ -224,9 +203,9 @@ void PrimitivesPlugin_Box::createBoxByOnePointAndDims() } int aResultIndex = 0; - ResultVolumePtr aResultBox = document()->createVolume(data(), aResultIndex); - toto(aBoxAlgo, aResultBox); - setVolume(aResultBox, aResultIndex); + ResultBodyPtr aResultBox = document()->createBody(data(), aResultIndex); + loadNamingDS(aBoxAlgo, aResultBox); + setResult(aResultBox, aResultIndex); } //================================================================================================= -- 2.39.2