From 09a5ceca03657143c65a5b3c29b69c1a887ef131 Mon Sep 17 00:00:00 2001 From: "Zheng, Lei" Date: Thu, 29 Feb 2024 20:58:51 -0500 Subject: [PATCH 1/2] Toposhape/Part: Transfer in PropoertyTopoShape and related --- src/App/ComplexGeoData.cpp | 24 ++ src/App/ComplexGeoData.h | 12 + src/App/PropertyGeo.cpp | 34 ++ src/App/PropertyGeo.h | 10 + src/Mod/Part/App/PropertyTopoShape.cpp | 433 ++++++++++++++++++++++++- src/Mod/Part/App/PropertyTopoShape.h | 73 ++++- 6 files changed, 575 insertions(+), 11 deletions(-) diff --git a/src/App/ComplexGeoData.cpp b/src/App/ComplexGeoData.cpp index 95df883b8f..717e173cb7 100644 --- a/src/App/ComplexGeoData.cpp +++ b/src/App/ComplexGeoData.cpp @@ -189,6 +189,17 @@ const std::string &ComplexGeoData::elementMapPrefix() { return prefix; } +std::string ComplexGeoData::getElementMapVersion() const { + return "4"; +} + +bool ComplexGeoData::checkElementMapVersion(const char * ver) const +{ + return !boost::equals(ver, "3") + && !boost::equals(ver, "4") + && !boost::starts_with(ver, "3."); +} + size_t ComplexGeoData::getElementMapSize(bool flush) const { if (flush) { @@ -653,5 +664,18 @@ void ComplexGeoData::beforeSave() const } } +void ComplexGeoData::hashChildMaps() +{ + flushElementMap(); + if (_elementMap) + _elementMap->hashChildMaps(Tag); +} + +bool ComplexGeoData::hasChildElementMap() const +{ + flushElementMap(); + return _elementMap && _elementMap->hasChildElementMap(); +} + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) diff --git a/src/App/ComplexGeoData.h b/src/App/ComplexGeoData.h index a7a4fa290c..6fb11ee915 100644 --- a/src/App/ComplexGeoData.h +++ b/src/App/ComplexGeoData.h @@ -242,6 +242,12 @@ public: std::vector > getElementMappedNames(const IndexedName & element, bool needUnmapped=false) const; + /// Hash the child element map postfixes to shorten element name from hierarchical maps + void hashChildMaps(); + + /// Check if there is child element map + bool hasChildElementMap() const; + /// Append the Tag (if and only if it is non zero) into the element map virtual void reTagElementMap(long tag, App::StringHasherRef hasher, @@ -289,6 +295,12 @@ public: /// Get the current element map size size_t getElementMapSize(bool flush=true) const; + /// Return the current element map version + virtual std::string getElementMapVersion() const; + + /// Return true to signal element map version change + virtual bool checkElementMapVersion(const char * ver) const; + /// Check if the given sub-name only contains an element name static bool isElementName(const char *subName) { return (subName != nullptr) && (*subName != 0) && findElementName(subName)==subName; diff --git a/src/App/PropertyGeo.cpp b/src/App/PropertyGeo.cpp index bddff7d9a0..6ce4186d76 100644 --- a/src/App/PropertyGeo.cpp +++ b/src/App/PropertyGeo.cpp @@ -1244,6 +1244,40 @@ PropertyComplexGeoData::PropertyComplexGeoData() = default; PropertyComplexGeoData::~PropertyComplexGeoData() = default; +std::string PropertyComplexGeoData::getElementMapVersion(bool) const { + auto data = getComplexData(); + if(!data) + return std::string(); + auto owner = Base::freecad_dynamic_cast(getContainer()); + std::ostringstream ss; + if(owner && owner->getDocument() + && owner->getDocument()->getStringHasher()==data->Hasher) + ss << "1."; + else + ss << "0."; + ss << data->getElementMapVersion(); + return ss.str(); +} + +bool PropertyComplexGeoData::checkElementMapVersion(const char * ver) const +{ + auto data = getComplexData(); + if(!data) + return false; + auto owner = Base::freecad_dynamic_cast(getContainer()); + std::ostringstream ss; + const char *prefix; + if(owner && owner->getDocument() + && owner->getDocument()->getStringHasher() == data->Hasher) + prefix = "1."; + else + prefix = "0."; + if (!boost::starts_with(ver, prefix)) + return true; + return data->checkElementMapVersion(ver+2); +} + + void PropertyComplexGeoData::afterRestore() { auto data = getComplexData(); diff --git a/src/App/PropertyGeo.h b/src/App/PropertyGeo.h index 861442ed57..d6c78f0a52 100644 --- a/src/App/PropertyGeo.h +++ b/src/App/PropertyGeo.h @@ -541,6 +541,16 @@ public: Base::BoundBox3d getBoundingBox() const override = 0; //@} + /** Return the element map version + * + * @param persisted: if true, return the restored element map version. Or + * else, return the current element map version + */ + virtual std::string getElementMapVersion(bool restored=false) const; + + /// Return true to signal element map version change + virtual bool checkElementMapVersion(const char * ver) const; + void afterRestore() override; }; diff --git a/src/Mod/Part/App/PropertyTopoShape.cpp b/src/Mod/Part/App/PropertyTopoShape.cpp index 4ecd86916e..c32889ab99 100644 --- a/src/Mod/Part/App/PropertyTopoShape.cpp +++ b/src/Mod/Part/App/PropertyTopoShape.cpp @@ -36,6 +36,7 @@ #endif // _PreComp_ #include +#include #include #include #include @@ -45,10 +46,14 @@ #include #include +#include "PartFeature.h" +#include "PartPyCXX.h" #include "PropertyTopoShape.h" #include "TopoShapePy.h" +FC_LOG_LEVEL_INIT("App", true, true) +namespace sp = std::placeholders; using namespace Part; TYPESYSTEM_SOURCE(Part::PropertyPartShape , App::PropertyComplexGeoData) @@ -61,14 +66,32 @@ void PropertyPartShape::setValue(const TopoShape& sh) { aboutToSetValue(); _Shape = sh; + auto obj = Base::freecad_dynamic_cast(getContainer()); + if(obj) { + auto tag = obj->getID(); + if(_Shape.Tag && tag!=_Shape.Tag) { + auto hasher = _Shape.Hasher?_Shape.Hasher:obj->getDocument()->getStringHasher(); + _Shape.reTagElementMap(tag,hasher); + } else + _Shape.Tag = obj->getID(); + if (!_Shape.Hasher && _Shape.hasChildElementMap()) { + _Shape.Hasher = obj->getDocument()->getStringHasher(); + _Shape.hashChildMaps(); + } + } hasSetValue(); + _Ver.clear(); } -void PropertyPartShape::setValue(const TopoDS_Shape& sh) +void PropertyPartShape::setValue(const TopoDS_Shape& sh, bool resetElementMap) { aboutToSetValue(); - _Shape.setShape(sh); + auto obj = dynamic_cast(getContainer()); + if(obj) + _Shape.Tag = obj->getID(); + _Shape.setShape(sh,resetElementMap); hasSetValue(); + _Ver.clear(); } const TopoDS_Shape& PropertyPartShape::getValue() const @@ -76,13 +99,23 @@ const TopoDS_Shape& PropertyPartShape::getValue() const return _Shape.getShape(); } -const TopoShape& PropertyPartShape::getShape() const +TopoShape PropertyPartShape::getShape() const { - return this->_Shape; + _Shape.initCache(-1); + auto res = _Shape; +// if (Feature::isElementMappingDisabled(getContainer())) + if ( false ) // TODO Implement + res.Tag = -1; + else if (!res.Tag) { + if (auto parent = Base::freecad_dynamic_cast(getContainer())) + res.Tag = parent->getID(); + } + return res; } const Data::ComplexGeoData* PropertyPartShape::getComplexData() const { + _Shape.initCache(-1); return &(this->_Shape); } @@ -140,8 +173,21 @@ PyObject *PropertyPartShape::getPyObject() void PropertyPartShape::setPyObject(PyObject *value) { if (PyObject_TypeCheck(value, &(TopoShapePy::Type))) { - TopoShapePy *pcObject = static_cast(value); - setValue(*pcObject->getTopoShapePtr()); + auto shape = *static_cast(value)->getTopoShapePtr(); + auto owner = dynamic_cast(getContainer()); + if(owner && owner->getDocument()) { + if(shape.Tag || shape.getElementMapSize()) { + // We can't trust the meaning of the input shape tag, so we + // remap anyway + TopoShape res(owner->getID(),owner->getDocument()->getStringHasher(),shape.getShape()); + res.mapSubElement(shape); + shape = res; + }else{ + shape.Tag = owner->getID(); + shape.Hasher->clear(); // reset(); + } + } + setValue(shape); } else { std::string error = std::string("type must be 'Shape', not "); @@ -153,6 +199,15 @@ void PropertyPartShape::setPyObject(PyObject *value) App::Property *PropertyPartShape::Copy() const { PropertyPartShape *prop = new PropertyPartShape(); + +// if (PartParams::getShapePropertyCopy()) { + if ( false ) { // TODO: PartParams + // makeElementCopy() consume too much memory for complex geometry. + prop->_Shape = this->_Shape.makeElementCopy(); + } else + prop->_Shape = this->_Shape; + prop->_Ver = this->_Ver; + prop->_Shape = this->_Shape; if (!_Shape.getShape().IsNull()) { BRepBuilderAPI_Copy copy(_Shape.getShape()); @@ -164,9 +219,11 @@ App::Property *PropertyPartShape::Copy() const void PropertyPartShape::Paste(const App::Property &from) { - aboutToSetValue(); - _Shape = dynamic_cast(from)._Shape; - hasSetValue(); + auto prop = Base::freecad_dynamic_cast(&from); + if(prop) { + setValue(prop->_Shape); + _Ver = prop->_Ver; + } } unsigned int PropertyPartShape::getMemSize () const @@ -188,6 +245,19 @@ void PropertyPartShape::getPaths(std::vector &paths) cons << App::ObjectIdentifier::Component::SimpleComponent(App::ObjectIdentifier::String("Volume"))); } +void PropertyPartShape::beforeSave() const +{ + _HasherIndex = 0; + _SaveHasher = false; + auto owner = Base::freecad_dynamic_cast(getContainer()); + if(owner && !_Shape.isNull() && _Shape.getElementMapSize()>0) { + auto ret = owner->getDocument()->addStringHasher(_Shape.Hasher); + _HasherIndex = ret.second; + _SaveHasher = ret.first; + _Shape.beforeSave(); + } +} + void PropertyPartShape::Save (Base::Writer &writer) const { if(!writer.isForceXML()) { @@ -205,6 +275,69 @@ void PropertyPartShape::Save (Base::Writer &writer) const } } +#ifdef NOT_YET_AND_MAYBE_NEVER +void PropertyPartShape::Save (Base::Writer &writer) const +{ + //See SaveDocFile(), RestoreDocFile() + writer.Stream() << writer.ind() << "(getContainer()); + if(owner && !_Shape.isNull() + && _Shape.getElementMapSize()>0 + && !_Shape.Hasher.isNull()) { + writer.Stream() << " HasherIndex=\"" << _HasherIndex << '"'; + if(_SaveHasher) + writer.Stream() << " SaveHasher=\"1\""; + } + std::string version; + // If exporting, do not export mapped element name, but still make a mark + if(owner) { + if(!owner->isExporting()) + version = _Ver.size()?_Ver:owner->getElementMapVersion(this); + }else + version = _Ver.size()?_Ver:_Shape.getElementMapVersion(); + writer.Stream() << " ElementMap=\"" << version << '"'; + + bool binary = writer.getMode("BinaryBrep"); + bool toXML = writer.getFileVersion()>1 && writer.isForceXML()>=(binary?3:2); + if(!toXML) { + writer.Stream() << " file=\"" + << writer.addFile(getFileName(binary?".bin":".brp"), this) + << "\"/>\n"; + } else if(binary) { + writer.Stream() << " binary=\"1\">\n"; + TopoShape shape; + shape.setShape(_Shape.getShape()); + shape.exportBinary(writer.beginCharStream(true)); + writer.endCharStream() << writer.ind() << "\n"; + } else { + writer.Stream() << " brep=\"1\">\n"; + _Shape.exportBrep(writer.beginCharStream(false)<<'\n'); + writer.endCharStream() << '\n' << writer.ind() << "\n"; + } + + if(_SaveHasher) { + if(!toXML) + _Shape.Hasher->setPersistenceFileName(getFileName(".Table").c_str()); + else + _Shape.Hasher->setPersistenceFileName(0); + _Shape.Hasher->Save(writer); + } + if(version.size()) { + if(!toXML) + _Shape.setPersistenceFileName(getFileName(".Map").c_str()); + else + _Shape.setPersistenceFileName(0); + _Shape.Save(writer); + } +} +#endif + +std::string PropertyPartShape::getElementMapVersion(bool restored) const { + if(restored) + return _Ver; + return PropertyComplexGeoData::getElementMapVersion(false); +} + void PropertyPartShape::Restore(Base::XMLReader &reader) { reader.readElement("Part"); @@ -215,6 +348,105 @@ void PropertyPartShape::Restore(Base::XMLReader &reader) reader.addFile(file.c_str(),this); } } +#ifdef NOT_YET_AND_MAYBE_NEVER +void PropertyPartShape::Restore(Base::XMLReader &reader) +{ + reader.readElement("Part"); + + auto owner = Base::freecad_dynamic_cast(getContainer()); + _Ver = "?"; + bool has_ver = reader.hasAttribute("ElementMap"); + if(has_ver) + _Ver = reader.getAttribute("ElementMap"); + + int hasher_idx = reader.getAttributeAsInteger("HasherIndex","-1"); + int save_hasher = reader.getAttributeAsInteger("SaveHasher",""); + + TopoDS_Shape sh; + + if(reader.hasAttribute("file")) { + std::string file = reader.getAttribute("file"); + if (!file.empty()) { + // initiate a file read + reader.addFile(file.c_str(),this); + } + } else if(reader.getAttributeAsInteger("binary","")) { + TopoShape shape; + shape.importBinary(reader.beginCharStream(true)); + sh = shape.getShape(); + } else if(reader.getAttributeAsInteger("brep","")) { + BRep_Builder builder; + BRepTools::Read(sh, reader.beginCharStream(false), builder); + } + + reader.readEndElement("Part"); + + if(owner && hasher_idx>=0) { + _Shape.Hasher = owner->getDocument()->getStringHasher(hasher_idx); + if(save_hasher) + _Shape.Hasher->Restore(reader); + } + + if(has_ver) { + // The file name here is not used for restore, but just a way to get + // more useful error message if something wrong when restoring + _Shape.setPersistenceFileName(getFileName().c_str()); + if(owner && owner->getDocument()->testStatus(App::Document::PartialDoc)) + _Shape.Restore(reader); + else if(_Ver == "?" || _Ver.empty()) { + // This indicate the shape is saved by legacy version without + // element map info. + if(owner) { + // This will ask user for recompute after import + owner->getDocument()->addRecomputeObject(owner); + } + }else{ + _Shape.Restore(reader); + if (owner ? owner->checkElementMapVersion(this, _Ver.c_str()) + : _Shape.checkElementMapVersion(_Ver.c_str())) { + auto ver = owner?owner->getElementMapVersion(this):_Shape.getElementMapVersion(); + if(!owner || !owner->getNameInDocument() || !_Shape.getElementMapSize()) { + _Ver = ver; + } else { + // version mismatch, signal for regenerating. + static const char *warnedDoc=0; + if(warnedDoc != owner->getDocument()->getName()) { + warnedDoc = owner->getDocument()->getName(); + FC_WARN("Recomputation required for document '" << warnedDoc + << "' on geo element version change in " << getFullName() + << ": " << _Ver << " -> " << ver); + } + owner->getDocument()->addRecomputeObject(owner); + } + } + } + } else if(owner && !owner->getDocument()->testStatus(App::Document::PartialDoc)) { + if(App::DocumentParams::getWarnRecomputeOnRestore()) { + FC_WARN("Pending recompute for generating element map: " << owner->getFullName()); + owner->getDocument()->addRecomputeObject(owner); + } + } + + if (!sh.IsNull() || !_Shape.isNull()) { + aboutToSetValue(); + _Shape.setShape(sh,false); + hasSetValue(); + } +} +#endif + +void PropertyPartShape::afterRestore() +{ + if (_Shape.isRestoreFailed()) { + // this cause GeoFeature::updateElementReference() to call + // PropertyLinkBase::updateElementReferences() with reverse = true, in + // order to try to regenerate the element map + _Ver = "?"; + } + else if (_Shape.getElementMapSize() == 0) + _Shape.Hasher->clear(); //reset(); + PropertyComplexGeoData::afterRestore(); +} // The following function is copied from OCCT BRepTools.cxx and modified // to disable saving of triangulation @@ -406,6 +638,88 @@ void PropertyPartShape::RestoreDocFile(Base::Reader &reader) // ------------------------------------------------------------------------- +ShapeHistory::ShapeHistory(BRepBuilderAPI_MakeShape& mkShape, TopAbs_ShapeEnum type, + const TopoDS_Shape& newS, const TopoDS_Shape& oldS) +{ + reset(mkShape,type,newS,oldS); +} + +void ShapeHistory::reset(BRepBuilderAPI_MakeShape& mkShape, TopAbs_ShapeEnum type, + const TopoDS_Shape& newS, const TopoDS_Shape& oldS) +{ + shapeMap.clear(); + this->type = type; + + TopTools_IndexedMapOfShape newM, oldM; + TopExp::MapShapes(newS, type, newM); // map containing all old objects of type "type" + TopExp::MapShapes(oldS, type, oldM); // map containing all new objects of type "type" + + // Look at all objects in the old shape and try to find the modified object in the new shape + for (int i=1; i<=oldM.Extent(); i++) { + bool found = false; + TopTools_ListIteratorOfListOfShape it; + // Find all new objects that are a modification of the old object (e.g. a face was resized) + for (it.Initialize(mkShape.Modified(oldM(i))); it.More(); it.Next()) { + found = true; + for (int j=1; j<=newM.Extent(); j++) { // one old object might create several new ones! + if (newM(j).IsPartner(it.Value())) { + shapeMap[i-1].push_back(j-1); // adjust indices to start at zero + break; + } + } + } + + // Find all new objects that were generated from an old object (e.g. a face generated from an edge) + for (it.Initialize(mkShape.Generated(oldM(i))); it.More(); it.Next()) { + found = true; + for (int j=1; j<=newM.Extent(); j++) { + if (newM(j).IsPartner(it.Value())) { + shapeMap[i-1].push_back(j-1); + break; + } + } + } + + if (!found) { + // Find all old objects that don't exist any more (e.g. a face was completely cut away) + if (mkShape.IsDeleted(oldM(i))) { + shapeMap[i-1] = std::vector(); + } + else { + // Mop up the rest (will this ever be reached?) + for (int j=1; j<=newM.Extent(); j++) { + if (newM(j).IsPartner(oldM(i))) { + shapeMap[i-1].push_back(j-1); + break; + } + } + } + } + } +} + +void ShapeHistory::join(const ShapeHistory& newH) +{ + ShapeHistory join; + + for (ShapeHistory::MapList::const_iterator it = shapeMap.begin(); it != shapeMap.end(); ++it) { + int old_shape_index = it->first; + if (it->second.empty()) + join.shapeMap[old_shape_index] = ShapeHistory::List(); + for (ShapeHistory::List::const_iterator jt = it->second.begin(); jt != it->second.end(); ++jt) { + ShapeHistory::MapList::const_iterator kt = newH.shapeMap.find(*jt); + if (kt != newH.shapeMap.end()) { + ShapeHistory::List& ary = join.shapeMap[old_shape_index]; + ary.insert(ary.end(), kt->second.begin(), kt->second.end()); + } + } + } + + shapeMap.swap(join.shapeMap); +} + +// ------------------------------------------------------------------------- + TYPESYSTEM_SOURCE(Part::PropertyShapeHistory , App::PropertyLists) PropertyShapeHistory::PropertyShapeHistory() = default; @@ -577,3 +891,104 @@ void PropertyFilletEdges::Paste(const Property &from) _lValueList = dynamic_cast(from)._lValueList; hasSetValue(); } + +// ------------------------------------------------------------------------- + +TYPESYSTEM_SOURCE(Part::PropertyShapeCache, App::Property); + +App::Property *PropertyShapeCache::Copy(void) const { + return new PropertyShapeCache(); +} + +void PropertyShapeCache::Paste(const App::Property &) { + cache.clear(); +} + +void PropertyShapeCache::Save (Base::Writer &) const +{ +} + +void PropertyShapeCache::Restore(Base::XMLReader &) +{ +} + +PyObject *PropertyShapeCache::getPyObject() { + Py::List res; + for(auto &v : cache) + res.append(Py::TupleN(Py::String(v.first),shape2pyshape(v.second))); + return Py::new_reference_to(res); +} + +void PropertyShapeCache::setPyObject(PyObject *value) { + if(!value) + return; + if(value == Py_None) { + cache.clear(); + return; + } + App::PropertyStringList prop; + prop.setPyObject(value); + for(const auto &sub : prop.getValues()) + cache.erase(sub); +} + +#define SHAPE_CACHE_NAME "_Part_ShapeCache" +PropertyShapeCache *PropertyShapeCache::get(const App::DocumentObject *obj, bool create) { + auto prop = Base::freecad_dynamic_cast( + obj->getDynamicPropertyByName(SHAPE_CACHE_NAME)); + if(prop && prop->getContainer()==obj) + return prop; + if(!create) + return 0; + + prop = static_cast( + const_cast(obj)->addDynamicProperty("Part::PropertyShapeCache", + SHAPE_CACHE_NAME,"Part","Shape cache", + App::Prop_NoPersist|App::Prop_Output|App::Prop_Hidden)); + if(!prop) + FC_ERR("Failed to add shape cache for " << obj->getFullName()); + else + prop->connChanged = const_cast(obj)->signalEarlyChanged.connect( + std::bind(&PropertyShapeCache::slotChanged,prop,sp::_1,sp::_2)); + return prop; +} + +bool PropertyShapeCache::getShape(const App::DocumentObject *obj, TopoShape &shape, const char *subname) { +// if (PartParams::getDisableShapeCache()) +// return false; //TODO PartParams + auto prop = get(obj,false); + if(!prop) + return false; + if(!subname) subname = ""; + auto it = prop->cache.find(subname); + if(it!=prop->cache.end()) { + shape = it->second; + return !shape.isNull(); + } + return false; +} + +void PropertyShapeCache::setShape( + const App::DocumentObject *obj, const TopoShape &shape, const char *subname) +{ +// if (PartParams::getDisableShapeCache()) +// return; // TODO: Part Params + auto prop = get(obj,true); + if(!prop) + return; + if(!subname) subname = ""; + prop->cache[subname] = shape; +} + +void PropertyShapeCache::slotChanged(const App::DocumentObject &, const App::Property &prop) { + auto propName = prop.getName(); + if(!propName) return; + if(strcmp(propName,"Group")==0 || + strcmp(propName,"Shape")==0 || + strstr(propName,"Touched")!=0) + { + FC_LOG("clear shape cache on changed " << prop.getFullName()); + cache.clear(); + } +} + diff --git a/src/Mod/Part/App/PropertyTopoShape.h b/src/Mod/Part/App/PropertyTopoShape.h index 54c495fdc4..32df7f4ff3 100644 --- a/src/Mod/Part/App/PropertyTopoShape.h +++ b/src/Mod/Part/App/PropertyTopoShape.h @@ -35,6 +35,7 @@ namespace Part { +class Feature; /** The part shape property class. * @author Werner Mayer */ @@ -51,10 +52,10 @@ public: /// set the part shape void setValue(const TopoShape&); /// set the part shape - void setValue(const TopoDS_Shape&); + void setValue(const TopoDS_Shape&, bool resetElementMap=true); /// get the part shape const TopoDS_Shape& getValue() const; - const TopoShape& getShape() const; + TopoShape getShape() const; const Data::ComplexGeoData* getComplexData() const override; //@} @@ -85,6 +86,8 @@ public: void Save (Base::Writer &writer) const override; void Restore(Base::XMLReader &reader) override; + virtual void beforeSave() const override; + void SaveDocFile (Base::Writer &writer) const override; void RestoreDocFile(Base::Reader &reader) override; @@ -96,6 +99,13 @@ public: /// Get valid paths for this property; used by auto completer void getPaths(std::vector & paths) const override; + virtual std::string getElementMapVersion(bool restored=false) const override; + void resetElementMapVersion() {_Ver.clear();} + + virtual void afterRestore() override; + + friend class Feature; + private: void saveToFile(Base::Writer &writer) const; void loadFromFile(Base::Reader &reader); @@ -103,6 +113,9 @@ private: private: TopoShape _Shape; + std::string _Ver; + mutable int _HasherIndex = 0; + mutable bool _SaveHasher = false; }; struct PartExport ShapeHistory { @@ -115,6 +128,20 @@ struct PartExport ShapeHistory { TopAbs_ShapeEnum type; MapList shapeMap; + ShapeHistory() {} + /** + * Build a history of changes + * MakeShape: The operation that created the changes, e.g. BRepAlgoAPI_Common + * type: The type of object we are interested in, e.g. TopAbs_FACE + * newS: The new shape that was created by the operation + * oldS: The original shape prior to the operation + */ + ShapeHistory(BRepBuilderAPI_MakeShape& mkShape, TopAbs_ShapeEnum type, + const TopoDS_Shape& newS, const TopoDS_Shape& oldS); + void reset(BRepBuilderAPI_MakeShape& mkShape, TopAbs_ShapeEnum type, + const TopoDS_Shape& newS, const TopoDS_Shape& oldS); + void join(const ShapeHistory &newH); + }; class PartExport PropertyShapeHistory : public App::PropertyLists @@ -168,6 +195,20 @@ private: struct PartExport FilletElement { int edgeid; double radius1, radius2; + + FilletElement(int id=0,double r1=1.0,double r2=1.0) + :edgeid(id),radius1(r1),radius2(r2) + {} + + bool operator<(const FilletElement &other) const { + return edgeid < other.edgeid; + } + + bool operator==(const FilletElement &other) const { + return edgeid == other.edgeid + && radius1 == other.radius1 + && radius2 == other.radius2; + } }; class PartExport PropertyFilletEdges : public App::PropertyLists @@ -215,6 +256,34 @@ private: std::vector _lValueList; }; + +class PartExport PropertyShapeCache: public App::Property { + TYPESYSTEM_HEADER_WITH_OVERRIDE(); +public: + virtual App::Property *Copy(void) const override; + + virtual void Paste(const App::Property &) override; + + virtual PyObject *getPyObject() override; + + virtual void setPyObject(PyObject *value) override; + + virtual void Save (Base::Writer &writer) const override; + + virtual void Restore(Base::XMLReader &reader) override; + + static PropertyShapeCache *get(const App::DocumentObject *obj, bool create); + static bool getShape(const App::DocumentObject *obj, TopoShape &shape, const char *subname=0); + static void setShape(const App::DocumentObject *obj, const TopoShape &shape, const char *subname=0); + +private: + void slotChanged(const App::DocumentObject &, const App::Property &prop); + +private: + std::unordered_map cache; + boost::signals2::scoped_connection connChanged; +}; + } //namespace Part From 5892a8339e18d33b15683daff4c01d70553c8b19 Mon Sep 17 00:00:00 2001 From: bgbsww Date: Fri, 1 Mar 2024 10:18:17 -0500 Subject: [PATCH 2/2] Toponaming/Part: Cleanups, problem fixes, and tests --- src/App/DocumentObject.cpp | 21 +++ src/App/DocumentObject.h | 4 + src/App/Property.cpp | 4 +- src/App/PropertyContainer.h | 5 + src/Mod/Part/App/AppPart.cpp | 1 + src/Mod/Part/App/Attacher.cpp | 10 +- src/Mod/Part/App/PropertyTopoShape.cpp | 67 +++++-- src/Mod/Part/App/PropertyTopoShape.h | 2 +- tests/src/Mod/Part/App/CMakeLists.txt | 1 + tests/src/Mod/Part/App/FeaturePartCommon.cpp | 2 - tests/src/Mod/Part/App/PartFeature.cpp | 2 - tests/src/Mod/Part/App/PartFeatures.cpp | 2 - tests/src/Mod/Part/App/PropertyTopoShape.cpp | 188 +++++++++++++++++++ 13 files changed, 278 insertions(+), 31 deletions(-) create mode 100644 tests/src/Mod/Part/App/PropertyTopoShape.cpp diff --git a/src/App/DocumentObject.cpp b/src/App/DocumentObject.cpp index 708b6ea3b9..8adcbc419b 100644 --- a/src/App/DocumentObject.cpp +++ b/src/App/DocumentObject.cpp @@ -723,6 +723,27 @@ void DocumentObject::onBeforeChange(const Property* prop) signalBeforeChange(*this,*prop); } +void DocumentObject::onEarlyChange(const Property *prop) +{ + if(GetApplication().isClosingAll()) + return; + + if(!GetApplication().isRestoring() && + !prop->testStatus(Property::PartialTrigger) && + getDocument() && + getDocument()->testStatus(Document::PartialDoc)) + { + static App::Document *warnedDoc; + if(warnedDoc != getDocument()) { + warnedDoc = getDocument(); + FC_WARN("Changes to partial loaded document will not be saved: " + << getFullName() << '.' << prop->getName()); + } + } + + signalEarlyChanged(*this, *prop); +} + /// get called by the container when a Property was changed void DocumentObject::onChanged(const Property* prop) { diff --git a/src/App/DocumentObject.h b/src/App/DocumentObject.h index b9b745ad54..f1271e1c1d 100644 --- a/src/App/DocumentObject.h +++ b/src/App/DocumentObject.h @@ -110,6 +110,8 @@ public: boost::signals2::signal signalBeforeChange; /// signal on changed property of this object boost::signals2::signal signalChanged; + /// signal on changed property of this object before document scoped signalChangedObject + boost::signals2::signal signalEarlyChanged; /// returns the type name of the ViewProvider virtual const char* getViewProviderName() const { @@ -607,6 +609,8 @@ protected: void onBeforeChange(const Property* prop) override; /// get called by the container when a property was changed void onChanged(const Property* prop) override; + /// get called by the container when a property was changed + void onEarlyChange(const Property* prop) override; /// get called after a document has been fully restored virtual void onDocumentRestored(); /// get called after an undo/redo transaction is finished diff --git a/src/App/Property.cpp b/src/App/Property.cpp index a8bb121723..fd02085d85 100644 --- a/src/App/Property.cpp +++ b/src/App/Property.cpp @@ -211,8 +211,10 @@ void Property::destroy(Property *p) { void Property::touch() { PropertyCleaner guard(this); - if (father) + if (father) { + father->onEarlyChange(this); father->onChanged(this); + } StatusBits.set(Touched); } diff --git a/src/App/PropertyContainer.h b/src/App/PropertyContainer.h index 17a2102efc..1bb7ecd7db 100644 --- a/src/App/PropertyContainer.h +++ b/src/App/PropertyContainer.h @@ -237,6 +237,11 @@ public: protected: + /** get called by the container when a property has changed + * + * This function is called before onChanged() + */ + virtual void onEarlyChange(const Property* /*prop*/){} /// get called by the container when a property has changed virtual void onChanged(const Property* /*prop*/){} /// get called before the value is changed diff --git a/src/Mod/Part/App/AppPart.cpp b/src/Mod/Part/App/AppPart.cpp index de78cddefe..4190208200 100644 --- a/src/Mod/Part/App/AppPart.cpp +++ b/src/Mod/Part/App/AppPart.cpp @@ -398,6 +398,7 @@ PyMOD_INIT_FUNC(Part) Part::PropertyGeometryList ::init(); Part::PropertyShapeHistory ::init(); Part::PropertyFilletEdges ::init(); + Part::PropertyShapeCache ::init(); Part::PropertyTopoShapeList ::init(); Part::FaceMaker ::init(); diff --git a/src/Mod/Part/App/Attacher.cpp b/src/Mod/Part/App/Attacher.cpp index cebff12d76..70e0e19df9 100644 --- a/src/Mod/Part/App/Attacher.cpp +++ b/src/Mod/Part/App/Attacher.cpp @@ -771,15 +771,15 @@ void AttachEngine::readLinks(const App::PropertyLinkSubList &references, } App::GeoFeature* geof = static_cast(objs[i]); geofs[i] = geof; - const Part::TopoShape* shape; + Part::TopoShape shape; if (geof->isDerivedFrom(Part::Feature::getClassTypeId())){ - shape = &(static_cast(geof)->Shape.getShape()); - if (shape->isNull()){ + shape = (static_cast(geof)->Shape.getShape()); + if (shape.isNull()){ throw AttachEngineException("AttachEngine3D: Part has null shape"); } if (sub[i].length()>0){ try{ - storage.push_back(shape->getSubShape(sub[i].c_str())); + storage.push_back(shape.getSubShape(sub[i].c_str())); } catch (Standard_Failure&){ throw AttachEngineException("AttachEngine3D: subshape not found"); } @@ -787,7 +787,7 @@ void AttachEngine::readLinks(const App::PropertyLinkSubList &references, throw AttachEngineException("AttachEngine3D: null subshape"); shapes[i] = &(storage[storage.size()-1]); } else { - shapes[i] = &(shape->getShape()); + shapes[i] = &(shape.getShape()); } } else if ( geof->isDerivedFrom(App::Plane::getClassTypeId()) ){ //obtain Z axis and origin of placement diff --git a/src/Mod/Part/App/PropertyTopoShape.cpp b/src/Mod/Part/App/PropertyTopoShape.cpp index c32889ab99..02d7d45426 100644 --- a/src/Mod/Part/App/PropertyTopoShape.cpp +++ b/src/Mod/Part/App/PropertyTopoShape.cpp @@ -103,8 +103,10 @@ TopoShape PropertyPartShape::getShape() const { _Shape.initCache(-1); auto res = _Shape; -// if (Feature::isElementMappingDisabled(getContainer())) - if ( false ) // TODO Implement + // March, 2024 Toponaming project: There was originally an unused feature to disable elementMapping + // that has not been kept: + // if (Feature::isElementMappingDisabled(getContainer())) + if ( false ) res.Tag = -1; else if (!res.Tag) { if (auto parent = Base::freecad_dynamic_cast(getContainer())) @@ -184,7 +186,9 @@ void PropertyPartShape::setPyObject(PyObject *value) shape = res; }else{ shape.Tag = owner->getID(); - shape.Hasher->clear(); // reset(); + if ( shape.Hasher ) { // TODO: This null guard added during TNP transition + shape.Hasher->clear(); + } } } setValue(shape); @@ -200,20 +204,15 @@ App::Property *PropertyPartShape::Copy() const { PropertyPartShape *prop = new PropertyPartShape(); + // March, 2024 Toponaming project: There was originally a feature to enable making an element + // copy ( new geometry and map ) that has not been kept: // if (PartParams::getShapePropertyCopy()) { - if ( false ) { // TODO: PartParams - // makeElementCopy() consume too much memory for complex geometry. - prop->_Shape = this->_Shape.makeElementCopy(); - } else - prop->_Shape = this->_Shape; - prop->_Ver = this->_Ver; - +// // makeElementCopy() consume too much memory for complex geometry. +// prop->_Shape = this->_Shape.makeElementCopy(); +// } else +// prop->_Shape = this->_Shape; prop->_Shape = this->_Shape; - if (!_Shape.getShape().IsNull()) { - BRepBuilderAPI_Copy copy(_Shape.getShape()); - prop->_Shape.setShape(copy.Shape()); - } - + prop->_Ver = this->_Ver; return prop; } @@ -348,6 +347,7 @@ void PropertyPartShape::Restore(Base::XMLReader &reader) reader.addFile(file.c_str(),this); } } + #ifdef NOT_YET_AND_MAYBE_NEVER void PropertyPartShape::Restore(Base::XMLReader &reader) { @@ -433,7 +433,6 @@ void PropertyPartShape::Restore(Base::XMLReader &reader) hasSetValue(); } } -#endif void PropertyPartShape::afterRestore() { @@ -447,6 +446,7 @@ void PropertyPartShape::afterRestore() _Shape.Hasher->clear(); //reset(); PropertyComplexGeoData::afterRestore(); } +#endif // The following function is copied from OCCT BRepTools.cxx and modified // to disable saving of triangulation @@ -912,6 +912,10 @@ void PropertyShapeCache::Restore(Base::XMLReader &) { } +/** + * Make a new python List with a tuple for each cache entry containing the key and the shape + * @return the python list + */ PyObject *PropertyShapeCache::getPyObject() { Py::List res; for(auto &v : cache) @@ -919,6 +923,10 @@ PyObject *PropertyShapeCache::getPyObject() { return Py::new_reference_to(res); } +/** + * Remove the cache entries for every element in the list + * @param value A python list of entry names + */ void PropertyShapeCache::setPyObject(PyObject *value) { if(!value) return; @@ -933,6 +941,12 @@ void PropertyShapeCache::setPyObject(PyObject *value) { } #define SHAPE_CACHE_NAME "_Part_ShapeCache" +/** + * Find or create the shape cache for a document object + * @param obj The document object + * @param create True if we should create the cache if it doesn't exist + * @return The shape cache, or null if we aren't creating and it doesn't exist + */ PropertyShapeCache *PropertyShapeCache::get(const App::DocumentObject *obj, bool create) { auto prop = Base::freecad_dynamic_cast( obj->getDynamicPropertyByName(SHAPE_CACHE_NAME)); @@ -953,9 +967,18 @@ PropertyShapeCache *PropertyShapeCache::get(const App::DocumentObject *obj, bool return prop; } +/** + * Look up and return a shape in the cache + * @param obj The document object to look in + * @param shape The found shape is returned here + * @param subname The key to look up + * @return True if the name was found + */ bool PropertyShapeCache::getShape(const App::DocumentObject *obj, TopoShape &shape, const char *subname) { +// March, 2024 Toponaming project: There was originally a feature to disable shape cache +// that has not been kept: // if (PartParams::getDisableShapeCache()) -// return false; //TODO PartParams +// return false; auto prop = get(obj,false); if(!prop) return false; @@ -968,11 +991,19 @@ bool PropertyShapeCache::getShape(const App::DocumentObject *obj, TopoShape &sha return false; } +/** + * Find or create the property shape cache in a document object and then add an entry + * @param obj The Object + * @param shape The shape to cache + * @param subname The key to point at that shape + */ void PropertyShapeCache::setShape( const App::DocumentObject *obj, const TopoShape &shape, const char *subname) { +// March, 2024 Toponaming project: There was originally a feature to disable shape cache +// that has not been kept: // if (PartParams::getDisableShapeCache()) -// return; // TODO: Part Params +// return; auto prop = get(obj,true); if(!prop) return; diff --git a/src/Mod/Part/App/PropertyTopoShape.h b/src/Mod/Part/App/PropertyTopoShape.h index 32df7f4ff3..fd9bb42570 100644 --- a/src/Mod/Part/App/PropertyTopoShape.h +++ b/src/Mod/Part/App/PropertyTopoShape.h @@ -102,7 +102,7 @@ public: virtual std::string getElementMapVersion(bool restored=false) const override; void resetElementMapVersion() {_Ver.clear();} - virtual void afterRestore() override; +// virtual void afterRestore() override; friend class Feature; diff --git a/tests/src/Mod/Part/App/CMakeLists.txt b/tests/src/Mod/Part/App/CMakeLists.txt index ceea6c5787..7b93f834d6 100644 --- a/tests/src/Mod/Part/App/CMakeLists.txt +++ b/tests/src/Mod/Part/App/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources( ${CMAKE_CURRENT_SOURCE_DIR}/PartFeature.cpp ${CMAKE_CURRENT_SOURCE_DIR}/PartFeatures.cpp ${CMAKE_CURRENT_SOURCE_DIR}/PartTestHelpers.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/PropertyTopoShape.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TopoShape.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TopoShapeCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TopoShapeExpansion.cpp diff --git a/tests/src/Mod/Part/App/FeaturePartCommon.cpp b/tests/src/Mod/Part/App/FeaturePartCommon.cpp index ab53c936d3..8f7b6bc4ec 100644 --- a/tests/src/Mod/Part/App/FeaturePartCommon.cpp +++ b/tests/src/Mod/Part/App/FeaturePartCommon.cpp @@ -209,8 +209,6 @@ TEST_F(FeaturePartCommonTest, testMapping) { // Arrange - _boxes[0]->Shape.getShape().Tag = 1L; - _boxes[1]->Shape.getShape().Tag = 2L; _common->Base.setValue(_boxes[0]); _common->Tool.setValue(_boxes[1]); // Act diff --git a/tests/src/Mod/Part/App/PartFeature.cpp b/tests/src/Mod/Part/App/PartFeature.cpp index f42d306245..6dc7465f07 100644 --- a/tests/src/Mod/Part/App/PartFeature.cpp +++ b/tests/src/Mod/Part/App/PartFeature.cpp @@ -34,8 +34,6 @@ protected: TEST_F(FeaturePartTest, testGetElementName) { // Arrange - _boxes[0]->Shape.getShape().Tag = 1L; - _boxes[1]->Shape.getShape().Tag = 2L; _common->Base.setValue(_boxes[0]); _common->Tool.setValue(_boxes[1]); diff --git a/tests/src/Mod/Part/App/PartFeatures.cpp b/tests/src/Mod/Part/App/PartFeatures.cpp index 2ec5c96bed..fab72f6378 100644 --- a/tests/src/Mod/Part/App/PartFeatures.cpp +++ b/tests/src/Mod/Part/App/PartFeatures.cpp @@ -38,14 +38,12 @@ TEST_F(PartFeaturesTest, testRuledSurface) _edge1->X2.setValue(2); _edge1->Y2.setValue(0); _edge1->Z2.setValue(0); - _edge1->Shape.getShape().Tag = 1L; // TODO: Can remove when TNP is on? _edge2->X1.setValue(0); _edge2->Y1.setValue(2); _edge2->Z1.setValue(0); _edge2->X2.setValue(2); _edge2->Y2.setValue(2); _edge2->Z2.setValue(0); - _edge2->Shape.getShape().Tag = 2L; // TODO: Can remove when TNP is on? auto _ruled = dynamic_cast(_doc->addObject("Part::RuledSurface")); _ruled->Curve1.setValue(_edge1); _ruled->Curve2.setValue(_edge2); diff --git a/tests/src/Mod/Part/App/PropertyTopoShape.cpp b/tests/src/Mod/Part/App/PropertyTopoShape.cpp new file mode 100644 index 0000000000..e1eb5851b0 --- /dev/null +++ b/tests/src/Mod/Part/App/PropertyTopoShape.cpp @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "gtest/gtest.h" + +#include +#include "Mod/Part/App/FeaturePartCommon.h" +#include "Mod/Part/App/PropertyTopoShape.h" +#include +#include "PartTestHelpers.h" +#include "Mod/Part/App/TopoShapeCompoundPy.h" + +using namespace Part; +using namespace PartTestHelpers; + +class PropertyTopoShapeTest: public ::testing::Test, public PartTestHelperClass +{ +protected: + static void SetUpTestSuite() + { + tests::initApplication(); + } + + void SetUp() override + { + createTestDoc(); + _common = dynamic_cast(_doc->addObject("Part::Common")); + _common->Base.setValue(_boxes[0]); + _common->Tool.setValue(_boxes[1]); + _common->execute(); // We should now have an elementMap with 26 entries. + } + + void TearDown() override + {} + + Common* _common = nullptr; // NOLINT Can't be private in a test framework +}; + +TEST_F(PropertyTopoShapeTest, testPropertyPartShapeTopoShape) +{ + // Arrange + auto partShape = PropertyPartShape(); + auto topoShapeIn = _common->Shape.getShape(); + auto topoDsShapeIn = _common->Shape.getShape().getShape(); + // Act + partShape.setValue(topoShapeIn); + auto topoShapeOut = partShape.getShape(); + auto topoDsShapeOut = partShape.getValue(); + // Assert + EXPECT_TRUE(topoShapeOut.isSame(topoShapeIn)); + EXPECT_TRUE(topoDsShapeOut.IsSame(topoDsShapeIn)); + EXPECT_EQ(getVolume(topoDsShapeOut), 3); +#ifdef FC_USE_TNP_FIX + EXPECT_EQ(topoShapeOut.getElementMapSize(), 26); +#else + EXPECT_EQ(topoShapeOut.getElementMapSize(), 0); +#endif +} + +TEST_F(PropertyTopoShapeTest, testPropertyPartShapeTopoDSShape) +{ + // Arrange + auto property = _boxes[0]->addDynamicProperty("Part::PropertyPartShape", "test"); + auto partShape = dynamic_cast(property); + auto topoShapeIn = _common->Shape.getShape(); + auto topoDsShapeIn = _common->Shape.getShape().getShape(); + // Act + // The second parameter of setValue, whether to resetElementMap is never called and irrelevant. + // It appears to exist only to pass to the lower level setShape call. + partShape->setValue(topoDsShapeIn); + auto topoShapeOut = partShape->getShape(); + auto topoDsShapeOut = partShape->getValue(); + // Assert + EXPECT_FALSE(topoShapeOut.isSame(topoShapeIn)); + EXPECT_TRUE(topoDsShapeOut.IsSame(topoDsShapeIn)); + EXPECT_EQ(getVolume(topoDsShapeOut), 3); + EXPECT_EQ(topoShapeOut.getElementMapSize(), 0); // We passed in a TopoDS_Shape so lost the map +} + +TEST_F(PropertyTopoShapeTest, testPropertyPartShapeGetPyObject) +{ + // Arrange + Py_Initialize(); + Base::PyGILStateLocker lock; + auto partShape = PropertyPartShape(); + auto topoDsShapeIn = _common->Shape.getShape().getShape(); + auto topoDsShapeIn2 = _boxes[3]->Shape.getShape().getShape(); + // Act + partShape.setValue(topoDsShapeIn); + auto pyObj = partShape.getPyObject(); + // Assert + EXPECT_TRUE(PyObject_TypeCheck(pyObj, &TopoShapeCompoundPy::Type)); // _common is a compound. + // We can't build a TopoShapeCompoundPy directly ( protected destructor ), so we'll flip + // the compound out and in to prove setPyObject works as expected. + // Act + partShape.setValue(topoDsShapeIn2); + auto pyObj2 = partShape.getPyObject(); + // Assert the shape is no longer a compound + EXPECT_FALSE( + PyObject_TypeCheck(pyObj2, &TopoShapeCompoundPy::Type)); // _boxes[3] is not a compound. + Py_XDECREF(pyObj2); + // Act + partShape.setPyObject(pyObj); + auto pyObj3 = partShape.getPyObject(); + // Assert the shape returns to a compound if we setPyObject + EXPECT_TRUE(PyObject_TypeCheck(pyObj3, &TopoShapeCompoundPy::Type)); // _common is a compound. + Py_XDECREF(pyObj3); + Py_XDECREF(pyObj); +} + +// Possible future PropertyPartShape tests: +// Copy, Paste, getMemSize, beforeSave, Save. Restore + + +TEST_F(PropertyTopoShapeTest, testShapeHistory) +{ + // Arrange + TopoDS_Shape baseShape = _boxes[0]->Shape.getShape().getShape(); + BRepFilletAPI_MakeFillet mkFillet(baseShape); + auto edges = _boxes[0]->Shape.getShape().getSubTopoShapes(TopAbs_EDGE); + mkFillet.Add(0.1, 0.1, TopoDS::Edge(edges[0].getShape())); + TopoDS_Shape newShape = mkFillet.Shape(); + // Act to test the constructor + auto hist = ShapeHistory(mkFillet, TopAbs_EDGE, newShape, baseShape); + // Assert + EXPECT_EQ(hist.type, TopAbs_EDGE); + EXPECT_EQ(hist.shapeMap.size(), 11); // We filleted away one of the cubes 12 Edges + // Act to test the join operation + hist.join(hist); + // Assert + EXPECT_EQ(hist.type, TopAbs_EDGE); + EXPECT_EQ(hist.shapeMap.size(), 9); // TODO: Is this correct? + // Act to test the reset operation + hist.reset(mkFillet, TopAbs_VERTEX, newShape, baseShape); + // Assert + EXPECT_EQ(hist.type, TopAbs_VERTEX); + EXPECT_EQ(hist.shapeMap.size(), 8); // Vertexes on a cube. +} + +TEST_F(PropertyTopoShapeTest, testPropertyShapeHistory) +{ + // N/A nothing to really test +} + +TEST_F(PropertyTopoShapeTest, testPropertyShapeCache) +{ + // Arrange + PropertyShapeCache propertyShapeCache; + TopoShape topoShapeIn {_boxes[0]->Shape.getShape()}; // Any TopoShape to test with + TopoShape topoShapeOut; + char* subName = "Face1"; // Cache key + // Act + auto gotShapeNotYet = propertyShapeCache.getShape(_boxes[0], topoShapeOut, subName); + propertyShapeCache.setShape(_boxes[0], topoShapeIn, subName); + auto gotShapeGood = propertyShapeCache.getShape(_boxes[0], topoShapeOut, subName); + // Assert + ASSERT_FALSE(gotShapeNotYet); + ASSERT_TRUE(gotShapeGood); + EXPECT_EQ(getVolume(topoShapeIn.getShape()), 6); + EXPECT_EQ(getVolume(topoShapeOut.getShape()), 6); + EXPECT_TRUE(topoShapeIn.isSame(topoShapeOut)); +} + +TEST_F(PropertyTopoShapeTest, testPropertyShapeCachePyObj) +{ + // Arrange + Py_Initialize(); + Base::PyGILStateLocker lock; + PropertyShapeCache catalyst; + auto propertyShapeCache = catalyst.get(_boxes[1], true); + auto faces = _boxes[1]->Shape.getShape().getSubTopoShapes(TopAbs_FACE); + propertyShapeCache->setShape(_boxes[1], faces[0], "Face1"); + propertyShapeCache->setShape(_boxes[1], faces[1], "Face2"); + propertyShapeCache->setShape(_boxes[1], faces[2], "Face3"); + propertyShapeCache->setShape(_boxes[1], faces[3], "Face4"); + auto eraseList = PyList_New(2); + PyList_SetItem(eraseList, 0, PyUnicode_FromString("Face2")); + PyList_SetItem(eraseList, 1, PyUnicode_FromString("Face3")); + // Act + auto pyObjOut = propertyShapeCache->getPyObject(); + propertyShapeCache->setPyObject(eraseList); // pyObjInRepr); + auto pyObjOutErased = propertyShapeCache->getPyObject(); + // The faces that come back from python are in a random order compared to what was passed in + // so testing them individually is difficult. We'll just make sure the counts are right. + EXPECT_EQ(PyList_Size(pyObjOut), 4); // All four faces we added to the cache + EXPECT_EQ(PyList_Size(pyObjOutErased), 2); // setPyObject removed Face2 and Face3 + Py_XDECREF(pyObjOut); + Py_XDECREF(pyObjOutErased); +}