Toponaming/Part: Cleanups, problem fixes, and tests

This commit is contained in:
bgbsww
2024-03-01 10:18:17 -05:00
parent 09a5ceca03
commit 5892a8339e
13 changed files with 278 additions and 31 deletions

View File

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

View File

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

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

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

@@ -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<App::DocumentObject>(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<PropertyShapeCache>(
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;

View File

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

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