Merge pull request #12683 from bgbsww/bgbsww-toponamingPropertyTopoShape

Toponaming/Part: property topo shape
This commit is contained in:
Chris Hennes
2024-03-04 15:05:16 -06:00
committed by GitHub
17 changed files with 839 additions and 28 deletions

View File

@@ -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)

View File

@@ -242,6 +242,12 @@ public:
std::vector<std::pair<MappedName, ElementIDRefs> >
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,
@@ -285,6 +291,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;

View File

@@ -748,6 +748,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)
{

View File

@@ -111,6 +111,8 @@ public:
boost::signals2::signal<void (const App::DocumentObject&, const App::Property&)> signalBeforeChange;
/// signal on changed property of this object
boost::signals2::signal<void (const App::DocumentObject&, const App::Property&)> signalChanged;
/// signal on changed property of this object before document scoped signalChangedObject
boost::signals2::signal<void (const App::DocumentObject&, const App::Property&)> signalEarlyChanged;
/// returns the type name of the ViewProvider
virtual const char* getViewProviderName() const {
@@ -614,6 +616,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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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<DocumentObject>(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<DocumentObject>(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();

View File

@@ -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;
};

View File

@@ -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();

View File

@@ -771,15 +771,15 @@ void AttachEngine::readLinks(const App::PropertyLinkSubList &references,
}
App::GeoFeature* geof = static_cast<App::GeoFeature*>(objs[i]);
geofs[i] = geof;
const Part::TopoShape* shape;
Part::TopoShape shape;
if (geof->isDerivedFrom(Part::Feature::getClassTypeId())){
shape = &(static_cast<Part::Feature*>(geof)->Shape.getShape());
if (shape->isNull()){
shape = (static_cast<Part::Feature*>(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

View File

@@ -36,6 +36,7 @@
#endif // _PreComp_
#include <App/Application.h>
#include <App/Document.h>
#include <App/DocumentObject.h>
#include <App/ObjectIdentifier.h>
#include <Base/Console.h>
@@ -45,10 +46,14 @@
#include <Base/Stream.h>
#include <Base/Writer.h>
#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<App::DocumentObject>(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<App::DocumentObject*>(getContainer());
if(obj)
_Shape.Tag = obj->getID();
_Shape.setShape(sh,resetElementMap);
hasSetValue();
_Ver.clear();
}
const TopoDS_Shape& PropertyPartShape::getValue() const
@@ -76,13 +99,25 @@ 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;
// 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<App::DocumentObject>(getContainer()))
res.Tag = parent->getID();
}
return res;
}
const Data::ComplexGeoData* PropertyPartShape::getComplexData() const
{
_Shape.initCache(-1);
return &(this->_Shape);
}
@@ -140,8 +175,23 @@ PyObject *PropertyPartShape::getPyObject()
void PropertyPartShape::setPyObject(PyObject *value)
{
if (PyObject_TypeCheck(value, &(TopoShapePy::Type))) {
TopoShapePy *pcObject = static_cast<TopoShapePy*>(value);
setValue(*pcObject->getTopoShapePtr());
auto shape = *static_cast<TopoShapePy*>(value)->getTopoShapePtr();
auto owner = dynamic_cast<App::DocumentObject*>(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();
if ( shape.Hasher ) { // TODO: This null guard added during TNP transition
shape.Hasher->clear();
}
}
}
setValue(shape);
}
else {
std::string error = std::string("type must be 'Shape', not ");
@@ -153,20 +203,26 @@ void PropertyPartShape::setPyObject(PyObject *value)
App::Property *PropertyPartShape::Copy() const
{
PropertyPartShape *prop = new PropertyPartShape();
prop->_Shape = this->_Shape;
if (!_Shape.getShape().IsNull()) {
BRepBuilderAPI_Copy copy(_Shape.getShape());
prop->_Shape.setShape(copy.Shape());
}
// 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()) {
// // makeElementCopy() consume too much memory for complex geometry.
// prop->_Shape = this->_Shape.makeElementCopy();
// } else
// prop->_Shape = this->_Shape;
prop->_Shape = this->_Shape;
prop->_Ver = this->_Ver;
return prop;
}
void PropertyPartShape::Paste(const App::Property &from)
{
aboutToSetValue();
_Shape = dynamic_cast<const PropertyPartShape&>(from)._Shape;
hasSetValue();
auto prop = Base::freecad_dynamic_cast<const PropertyPartShape>(&from);
if(prop) {
setValue(prop->_Shape);
_Ver = prop->_Ver;
}
}
unsigned int PropertyPartShape::getMemSize () const
@@ -188,6 +244,19 @@ void PropertyPartShape::getPaths(std::vector<App::ObjectIdentifier> &paths) cons
<< App::ObjectIdentifier::Component::SimpleComponent(App::ObjectIdentifier::String("Volume")));
}
void PropertyPartShape::beforeSave() const
{
_HasherIndex = 0;
_SaveHasher = false;
auto owner = Base::freecad_dynamic_cast<App::DocumentObject>(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 +274,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() << "<Part";
auto owner = dynamic_cast<App::DocumentObject*>(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() << "</Part>\n";
} else {
writer.Stream() << " brep=\"1\">\n";
_Shape.exportBrep(writer.beginCharStream(false)<<'\n');
writer.endCharStream() << '\n' << writer.ind() << "</Part>\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");
@@ -216,6 +348,106 @@ void PropertyPartShape::Restore(Base::XMLReader &reader)
}
}
#ifdef NOT_YET_AND_MAYBE_NEVER
void PropertyPartShape::Restore(Base::XMLReader &reader)
{
reader.readElement("Part");
auto owner = Base::freecad_dynamic_cast<App::DocumentObject>(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();
}
}
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();
}
#endif
// 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<int>();
}
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,135 @@ void PropertyFilletEdges::Paste(const Property &from)
_lValueList = dynamic_cast<const PropertyFilletEdges&>(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 &)
{
}
/**
* 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)
res.append(Py::TupleN(Py::String(v.first),shape2pyshape(v.second)));
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;
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"
/**
* 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<PropertyShapeCache>(
obj->getDynamicPropertyByName(SHAPE_CACHE_NAME));
if(prop && prop->getContainer()==obj)
return prop;
if(!create)
return 0;
prop = static_cast<PropertyShapeCache*>(
const_cast<App::DocumentObject*>(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<App::DocumentObject*>(obj)->signalEarlyChanged.connect(
std::bind(&PropertyShapeCache::slotChanged,prop,sp::_1,sp::_2));
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;
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;
}
/**
* 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;
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();
}
}

View File

@@ -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<App::ObjectIdentifier> & 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<FilletElement> _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<std::string, TopoShape> cache;
boost::signals2::scoped_connection connChanged;
};
} //namespace Part

View File

@@ -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

View File

@@ -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

View File

@@ -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]);

View File

@@ -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<RuledSurface*>(_doc->addObject("Part::RuledSurface"));
_ruled->Curve1.setValue(_edge1);
_ruled->Curve2.setValue(_edge2);

View File

@@ -0,0 +1,188 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "gtest/gtest.h"
#include <BRepFilletAPI_MakeFillet.hxx>
#include "Mod/Part/App/FeaturePartCommon.h"
#include "Mod/Part/App/PropertyTopoShape.h"
#include <src/App/InitApplication.h>
#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<Common*>(_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<PropertyPartShape*>(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);
}