diff --git a/src/App/GeoFeature.cpp b/src/App/GeoFeature.cpp index aeb67d8c70..1c96e9e299 100644 --- a/src/App/GeoFeature.cpp +++ b/src/App/GeoFeature.cpp @@ -25,6 +25,7 @@ #include +#include "ComplexGeoData.h" #include "GeoFeature.h" #include "GeoFeatureGroupExtension.h" #include "ElementNamingUtils.h" @@ -78,17 +79,52 @@ PyObject* GeoFeature::getPyObject() return Py::new_reference_to(PythonObject); } - -std::pair GeoFeature::getElementName( - const char *name, ElementNameType type) const +std::pair +GeoFeature::getElementName(const char *name, ElementNameType type) const { (void)type; std::pair ret; if(!name) return ret; + auto prop = getPropertyOfGeometry(); + if (!prop) { + return std::make_pair("", name); + } + + auto geo = prop->getComplexData(); + if (!geo) { + return std::make_pair("", name); + } + + return _getElementName(name, geo->getElementName(name)); +} + +std::pair +GeoFeature::_getElementName(const char* name, const Data::MappedElement& mapped) const +{ + std::pair ret; + if (mapped.index && mapped.name) { + std::ostringstream ss; + ss << Data::ComplexGeoData::elementMapPrefix() << mapped.name << '.' << mapped.index; + ret.first = ss.str(); + mapped.index.appendToStringBuffer(ret.second); + } + else if (mapped.name) { + // FC_TRACE("element mapped name " << name << " not found in " << getFullName()); + ret.first = name; + const char* dot = strrchr(name, '.'); + if (dot) { + // deliberately mangle the old style element name to signal a + // missing reference + ret.second = Data::MISSING_PREFIX; + ret.second += dot + 1; + } + } + else { + mapped.index.appendToStringBuffer(ret.second); + } - ret.second = name; return ret; } diff --git a/src/App/GeoFeature.h b/src/App/GeoFeature.h index 9f71ee58c7..e43ba869e1 100644 --- a/src/App/GeoFeature.h +++ b/src/App/GeoFeature.h @@ -26,7 +26,7 @@ #include "DocumentObject.h" #include "PropertyGeo.h" - +#include "MappedElement.h" namespace App { @@ -120,6 +120,10 @@ public: * @return Base::Placement The transformation from the global reference coordinate system */ Base::Placement globalPlacement() const; + +protected: + std::pair _getElementName(const char* name, + const Data::MappedElement& mapped) const; }; } //namespace App diff --git a/src/Mod/Part/App/PartFeature.cpp b/src/Mod/Part/App/PartFeature.cpp index c830b31ac3..2212f4695a 100644 --- a/src/Mod/Part/App/PartFeature.cpp +++ b/src/Mod/Part/App/PartFeature.cpp @@ -757,3 +757,235 @@ bool Part::checkIntersection(const TopoDS_Shape& first, const TopoDS_Shape& seco } } + +/** + * Override getElementName to support the Export type. Other calls are passed to the original + * method + * @param name The name to search for, or if non existent, name of current Feature is returned + * @param type An element type name. + * @return The element name located, of + */ +std::pair Feature::getElementName(const char* name, + ElementNameType type) const +{ + if (type != ElementNameType::Export) { + return App::GeoFeature::getElementName(name, type); + } + + // This function is overridden to provide higher level shape topo names that + // are generated on demand, e.g. Wire, Shell, Solid, etc. + + auto prop = Base::freecad_dynamic_cast(getPropertyOfGeometry()); + if (!prop) { + return App::GeoFeature::getElementName(name, type); + } + + TopoShape shape = prop->getShape(); + Data::MappedElement mapped = shape.getElementName(name); + auto res = shape.shapeTypeAndIndex(mapped.index); + static const int MinLowerTopoNames = 3; + static const int MaxLowerTopoNames = 10; + if (res.second && !mapped.name) { + // Here means valid index name, but no mapped name, check to see if + // we shall generate the high level topo name. + // + // The general idea of the algorithm is to find the minimum number of + // lower elements that can identify the given higher element, and + // combine their names to generate the name for the higher element. + // + // In theory, all it takes to find one lower element that only appear + // in the given higher element. To make the algorithm more robust + // against model changes, we shall take minimum MinLowerTopoNames lower + // elements. + // + // On the other hand, it may be possible to take too many elements for + // disambiguation. We shall limit to maximum MaxLowerTopoNames. If the + // chosen elements are not enough to disambiguate the higher element, + // we'll include an index for disambiguation. + + auto subshape = shape.getSubTopoShape(res.first, res.second, true); + TopAbs_ShapeEnum lower; + Data::IndexedName idxName; + if (!subshape.isNull()) { + switch (res.first) { + case TopAbs_WIRE: + lower = TopAbs_EDGE; + idxName = Data::IndexedName::fromConst("Edge", 1); + break; + case TopAbs_SHELL: + case TopAbs_SOLID: + case TopAbs_COMPOUND: + case TopAbs_COMPSOLID: + lower = TopAbs_FACE; + idxName = Data::IndexedName::fromConst("Face", 1); + break; + default: + lower = TopAbs_SHAPE; + } + if (lower != TopAbs_SHAPE) { + typedef std::pair> NameEntry; + std::vector indices; + std::vector names; + std::vector ancestors; + int count = 0; + for (auto& ss : subshape.getSubTopoShapes(lower)) { + auto name = ss.getMappedName(idxName); + if (!name) { + continue; + } + indices.emplace_back(name.size(), + shape.findAncestors(ss.getShape(), res.first)); + names.push_back(name); + if (indices.back().second.size() == 1 && ++count >= MinLowerTopoNames) { + break; + } + } + + if (names.size() >= MaxLowerTopoNames) { + std::stable_sort(indices.begin(), + indices.end(), + [](const NameEntry& a, const NameEntry& b) { + return a.second.size() < b.second.size(); + }); + std::vector sorted; + auto pos = 0; + sorted.reserve(names.size()); + for (auto& v : indices) { + size_t size = ancestors.size(); + if (size == 0) { + ancestors = v.second; + } + else if (size > 1) { + for (auto it = ancestors.begin(); it != ancestors.end();) { + if (std::find(v.second.begin(), v.second.end(), *it) + == v.second.end()) { + it = ancestors.erase(it); + if (ancestors.size() == 1) { + break; + } + } + else { + ++it; + } + } + } + auto itPos = sorted.end(); + if (size == 1 || size != ancestors.size()) { + itPos = sorted.begin() + pos; + ++pos; + } + sorted.insert(itPos, names[v.first]); + if (size == 1 && sorted.size() >= MinLowerTopoNames) { + break; + } + } + } + + names.resize(std::min((int)names.size(), MaxLowerTopoNames)); + if (names.size()) { + std::string op; + if (ancestors.size() > 1) { + // The current chosen elements are not enough to + // identify the higher element, generate an index for + // disambiguation. + auto it = std::find(ancestors.begin(), ancestors.end(), res.second); + if (it == ancestors.end()) { + assert(0 && "ancestor not found"); // this shouldn't happened + } + else { + op = Data::POSTFIX_TAG + std::to_string(it - ancestors.begin()); + } + } + + // Note: setting names to shape will change its underlying + // shared element name table. This actually violates the + // const'ness of this function. + // + // To be const correct, we should have made the element + // name table to be implicit sharing (i.e. copy on change). + // + // Not sure if there is any side effect of indirectly + // change the element map inside the Shape property without + // recording the change in undo stack. + // + mapped.name = shape.setElementComboName(mapped.index, + names, + mapped.index.getType(), + op.c_str()); + } + } + } + return App::GeoFeature::_getElementName(name, mapped); + } + + if (!res.second && mapped.name) { + const char* dot = strchr(name, '.'); + if (dot) { + ++dot; + // Here means valid mapped name, but cannot find the corresponding + // indexed name. This usually means the model has been changed. The + // original indexed name is usually appended to the mapped name + // separated by a dot. We use it as a clue to decode the combo name + // set above, and try to single out one sub shape that has all the + // lower elements encoded in the combo name. But since we don't + // always use all the lower elements for encoding, this can only be + // consider a heuristics. + if (Data::hasMissingElement(dot)) { + dot += strlen(Data::MISSING_PREFIX); + } + std::pair occindex = shape.shapeTypeAndIndex(dot); + if (occindex.second > 0) { + auto idxName = Data::IndexedName::fromConst(shape.shapeName(occindex.first).c_str(), + occindex.second); + std::string postfix; + auto names = + shape.decodeElementComboName(idxName, mapped.name, idxName.getType(), &postfix); + std::vector ancestors; + for (auto& name : names) { + auto index = shape.getIndexedName(name); + if (!index) { + ancestors.clear(); + break; + } + auto oidx = shape.shapeTypeAndIndex(index); + auto subshape = shape.getSubShape(oidx.first, oidx.second); + if (subshape.IsNull()) { + ancestors.clear(); + break; + } + auto current = shape.findAncestors(subshape, occindex.first); + if (ancestors.empty()) { + ancestors = std::move(current); + } + else { + for (auto it = ancestors.begin(); it != ancestors.end();) { + if (std::find(current.begin(), current.end(), *it) == current.end()) { + it = ancestors.erase(it); + } + else { + ++it; + } + } + if (ancestors.empty()) { // model changed beyond recognition, bail! + break; + } + } + } + if (ancestors.size() > 1 && boost::starts_with(postfix, Data::POSTFIX_INDEX)) { + std::istringstream iss(postfix.c_str() + strlen(Data::POSTFIX_INDEX)); + int idx; + if (iss >> idx && idx >= 0 && idx < (int)ancestors.size()) { + ancestors.resize(1, ancestors[idx]); + } + } + if (ancestors.size() == 1) { + idxName.setIndex(ancestors.front()); + mapped.index = idxName; + return App::GeoFeature::_getElementName(name, mapped); + } + } + } + } + + return App::GeoFeature::getElementName(name, type); +} diff --git a/src/Mod/Part/App/PartFeature.h b/src/Mod/Part/App/PartFeature.h index 543e03f96d..5e377e6e55 100644 --- a/src/Mod/Part/App/PartFeature.h +++ b/src/Mod/Part/App/PartFeature.h @@ -64,6 +64,9 @@ public: PyObject* getPyObject() override; + std::pair getElementName( + const char *name, ElementNameType type=Normal) const override; + TopLoc_Location getLocation() const; DocumentObject *getSubObject(const char *subname, PyObject **pyObj, diff --git a/src/Mod/Part/App/TopoShape.cpp b/src/Mod/Part/App/TopoShape.cpp index 4cbb2db9d6..b303f2ad50 100644 --- a/src/Mod/Part/App/TopoShape.cpp +++ b/src/Mod/Part/App/TopoShape.cpp @@ -477,6 +477,22 @@ std::pair TopoShape::shapeTypeAndIndex(const char *name) { return std::make_pair(type,idx); } +std::pair TopoShape::shapeTypeAndIndex(const Data::IndexedName& element) +{ + if (!element) { + return std::make_pair(TopAbs_SHAPE, 0); + } + static const std::string _subshape("SubShape"); + if (boost::equals(element.getType(), _subshape)) { + return std::make_pair(TopAbs_SHAPE, element.getIndex()); + } + TopAbs_ShapeEnum shapetype = shapeType(element.getType(), true); + if (shapetype == TopAbs_SHAPE) { + return std::make_pair(TopAbs_SHAPE, 0); + } + return std::make_pair(shapetype, element.getIndex()); +} + TopAbs_ShapeEnum TopoShape::shapeType(const char *type, bool silent) { if(type) { initShapeNameMap(); diff --git a/src/Mod/Part/App/TopoShape.h b/src/Mod/Part/App/TopoShape.h index b5840144f3..89c55a3940 100644 --- a/src/Mod/Part/App/TopoShape.h +++ b/src/Mod/Part/App/TopoShape.h @@ -1040,7 +1040,52 @@ public: const char* op = nullptr, double tol3d = 0.0, double tolBound = 0.0, - double tolAngluar = 0.0); + double tolAngular = 0.0); + + /* Make a shape with some subshapes replaced. + * + * @param source: the source shape + * @param s: replacement mapping the existing sub shape of source to new shapes + * + * @return The original content of this TopoShape is discarded and replaced + * with the new shape. The function returns the TopoShape itself as + * a self reference so that multiple operations can be carried out + * for the same shape in the same line of code. + */ + TopoShape& replaceElementShape(const TopoShape& source, + const std::vector>& s); + /* Make a new shape using this shape with some subshapes replaced by others + * + * @param s: replacement mapping the existing sub shape of source to new shapes + * + * @return Return the new shape. The TopoShape itself is not modified. + */ + TopoShape replaceElementShape(const std::vector>& s) const + { + return TopoShape(0, Hasher).replaceElementShape(*this, s); + } + + /* Make a shape with some subshapes removed + * + * @param source: the source shape + * @param s: the subshapes to be removed + * + * @return The original content of this TopoShape is discarded and replaced + * with the new shape. The function returns the TopoShape itself as + * a self reference so that multiple operations can be carried out + * for the same shape in the same line of code. + */ + TopoShape& removeElementShape(const TopoShape& source, const std::vector& s); + /* Make a new shape using this shape with some subshapes removed + * + * @param s: the subshapes to be removed + * + * @return Return the new shape. The TopoShape itself is not modified. + */ + TopoShape removeElementShape(const std::vector& s) const + { + return TopoShape(0, Hasher).removeElementShape(*this, s); + } /** Make shape using generalized fusion and return the modified sub shapes * @@ -1131,13 +1176,19 @@ public: static const std::string& shapeName(TopAbs_ShapeEnum type, bool silent = false); const std::string& shapeName(bool silent = false) const; static std::pair shapeTypeAndIndex(const char* name); + static std::pair shapeTypeAndIndex(const Data::IndexedName &name); - Data::MappedName setElementComboName(const Data::IndexedName & element, + Data::MappedName setElementComboName(const Data::IndexedName & element, const std::vector &names, const char *marker=nullptr, const char *op=nullptr, const Data::ElementIDRefs *sids=nullptr); + std::vector decodeElementComboName(const Data::IndexedName& element, + const Data::MappedName& name, + const char* marker = nullptr, + std::string* postfix = nullptr) const; + /** @name sub shape cached functions * * Mapped element names introduces some overhead when getting sub shapes diff --git a/src/Mod/Part/App/TopoShapeExpansion.cpp b/src/Mod/Part/App/TopoShapeExpansion.cpp index b6829b3260..3faf12ffe9 100644 --- a/src/Mod/Part/App/TopoShapeExpansion.cpp +++ b/src/Mod/Part/App/TopoShapeExpansion.cpp @@ -3059,7 +3059,7 @@ TopoShape& TopoShape::makeElementSolid(const TopoShape& shape, const char* op) } return *this; } - + TopoShape& TopoShape::makeElementMirror(const TopoShape& shape, const gp_Ax2& ax2, const char* op) { if (!op) { @@ -3108,6 +3108,48 @@ TopoShape& TopoShape::makeElementSlices(const TopoShape& shape, return makeElementCompound(wires, op, SingleShapeCompoundCreationPolicy::returnShape); } +TopoShape& TopoShape::replaceElementShape(const TopoShape& shape, + const std::vector>& s) +{ + if (shape.isNull()) { + FC_THROWM(NullShapeException, "Null shape"); + } + BRepTools_ReShape reshape; + std::vector shapes; + shapes.reserve(s.size() + 1); + for (auto& v : s) { + if (v.first.isNull() || v.second.isNull()) { + FC_THROWM(NullShapeException, "Null input shape"); + } + reshape.Replace(v.first.getShape(), v.second.getShape()); + shapes.push_back(v.second); + } + // TODO: This does not work when replacing a shape in a compound. Should we replace with + // something else? + // Note that remove works with a compound. + shapes.push_back(shape); + setShape(reshape.Apply(shape.getShape(), TopAbs_SHAPE)); + mapSubElement(shapes); + return *this; +} + +TopoShape& TopoShape::removeElementShape(const TopoShape& shape, const std::vector& s) +{ + if (shape.isNull()) { + FC_THROWM(NullShapeException, "Null shape"); + } + BRepTools_ReShape reshape; + for (auto& sh : s) { + if (sh.isNull()) { + FC_THROWM(NullShapeException, "Null input shape"); + } + reshape.Remove(sh.getShape()); + } + setShape(reshape.Apply(shape.getShape(), TopAbs_SHAPE)); + mapSubElement(shape); + return *this; +} + TopoShape& TopoShape::makeElementFillet(const TopoShape& shape, const std::vector& edges, double radius1, @@ -4054,6 +4096,97 @@ Data::MappedName TopoShape::setElementComboName(const Data::IndexedName& element return elementMap()->setElementName(element, newName, Tag, &sids); } +std::vector TopoShape::decodeElementComboName(const Data::IndexedName& element, + const Data::MappedName& name, + const char* marker, + std::string* postfix) const +{ + std::vector names; + if (!element) { + return names; + } + if (!marker) { + marker = ""; + } + int plen = (int)elementMapPrefix().size(); + int markerLen = strlen(marker); + int len; + int pos = name.findTagInElementName(nullptr, &len); + if (pos < 0) { + // It is possible to encode combo name without using a tag, e.g. + // Sketcher object creates wire using edges that are created by itself, + // so there will be no tag to encode. + // + // In this case, just search for the brackets + len = name.find("("); + if (len < 0) { + // No bracket is also possible, if there is only one name in the combo + pos = len = name.size(); + } + else { + pos = name.find(")"); + if (pos < 0) { + // non closing bracket? + return {}; + } + ++pos; + } + if (len <= (int)markerLen) { + return {}; + } + len -= markerLen + plen; + } + + if (name.find(elementMapPrefix(), len) != len || name.find(marker, len + plen) != len + plen) { + return {}; + } + + names.emplace_back(name, 0, len); + + std::string text; + len += plen + markerLen; + name.appendToBuffer(text, len, pos - len); + + if (this->Hasher) { + if (auto id = App::StringID::fromString(names.back().toRawBytes())) { + if (App::StringIDRef sid = this->Hasher->getID(id)) { + names.pop_back(); + names.emplace_back(sid); + } + else { + return names; + } + } + if (auto id = App::StringID::fromString(text.c_str())) { + if (App::StringIDRef sid = this->Hasher->getID(id)) { + text = sid.dataToText(); + } + else { + return names; + } + } + } + if (text.empty() || text[0] != '(') { + return names; + } + auto endPos = text.rfind(')'); + if (endPos == std::string::npos) { + return names; + } + + if (postfix) { + *postfix = text.substr(endPos + 1); + } + + text.resize(endPos); + std::istringstream iss(text.c_str() + 1); + std::string token; + while (std::getline(iss, token, '|')) { + names.emplace_back(token); + } + return names; +} + /** * Reorient the outer and inner wires of the TopoShape * diff --git a/tests/src/Mod/Part/App/CMakeLists.txt b/tests/src/Mod/Part/App/CMakeLists.txt index ee397c0f81..ceea6c5787 100644 --- a/tests/src/Mod/Part/App/CMakeLists.txt +++ b/tests/src/Mod/Part/App/CMakeLists.txt @@ -11,6 +11,7 @@ target_sources( ${CMAKE_CURRENT_SOURCE_DIR}/FeaturePartCut.cpp ${CMAKE_CURRENT_SOURCE_DIR}/FeaturePartFuse.cpp ${CMAKE_CURRENT_SOURCE_DIR}/FeatureRevolution.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/PartFeature.cpp ${CMAKE_CURRENT_SOURCE_DIR}/PartFeatures.cpp ${CMAKE_CURRENT_SOURCE_DIR}/PartTestHelpers.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TopoShape.cpp diff --git a/tests/src/Mod/Part/App/PartFeature.cpp b/tests/src/Mod/Part/App/PartFeature.cpp new file mode 100644 index 0000000000..f42d306245 --- /dev/null +++ b/tests/src/Mod/Part/App/PartFeature.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "gtest/gtest.h" + +#include "Mod/Part/App/FeaturePartCommon.h" +#include +#include +#include "PartTestHelpers.h" + +using namespace Part; +using namespace PartTestHelpers; + +class FeaturePartTest: public ::testing::Test, public PartTestHelperClass +{ +protected: + static void SetUpTestSuite() + { + tests::initApplication(); + } + + + void SetUp() override + { + createTestDoc(); + _common = dynamic_cast(_doc->addObject("Part::Common")); + } + + void TearDown() override + {} + + Common* _common = nullptr; // NOLINT Can't be private in a test framework +}; + +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]); + + // Act + _common->execute(); + const TopoShape& ts = _common->Shape.getShape(); + + auto namePair = _common->getElementName("test"); + auto namePairExport = _common->getElementName("test", App::GeoFeature::Export); + auto namePairSelf = _common->getElementName(nullptr); + // Assert + EXPECT_STREQ(namePair.first.c_str(), ""); + EXPECT_STREQ(namePair.second.c_str(), "test"); + EXPECT_STREQ(namePairExport.first.c_str(), ""); + EXPECT_STREQ(namePairExport.second.c_str(), "test"); + EXPECT_STREQ(namePairSelf.first.c_str(), ""); + EXPECT_STREQ(namePairSelf.second.c_str(), ""); +#ifndef FC_USE_TNP_FIX + EXPECT_EQ(ts.getElementMap().size(), 0); +#else + EXPECT_EQ(ts.getElementMap().size(), 26); // Value and code TBD +#endif + // TBD +} diff --git a/tests/src/Mod/Part/App/TopoShapeExpansion.cpp b/tests/src/Mod/Part/App/TopoShapeExpansion.cpp index e12642891a..6ffdd86d69 100644 --- a/tests/src/Mod/Part/App/TopoShapeExpansion.cpp +++ b/tests/src/Mod/Part/App/TopoShapeExpansion.cpp @@ -172,24 +172,24 @@ TEST_F(TopoShapeExpansionTest, makeElementCompoundTwoCubes) EXPECT_TRUE( allElementsMatch(topoShape, { - "Edge1;:H1,E;:H7,E", "Edge2;:H1,E;:H7,E", "Edge3;:H1,E;:H7,E", - "Edge4;:H1,E;:H7,E", "Edge1;:H2,E;:H7,E", "Edge2;:H2,E;:H7,E", - "Edge3;:H2,E;:H7,E", "Edge4;:H2,E;:H7,E", "Edge1;:H3,E;:H7,E", - "Edge2;:H3,E;:H7,E", "Edge3;:H3,E;:H7,E", "Edge4;:H3,E;:H7,E", - "Edge1;:H8,E;:He,E", "Edge2;:H8,E;:He,E", "Edge3;:H8,E;:He,E", - "Edge4;:H8,E;:He,E", "Edge1;:H9,E;:He,E", "Edge2;:H9,E;:He,E", - "Edge3;:H9,E;:He,E", "Edge4;:H9,E;:He,E", "Edge1;:Ha,E;:He,E", - "Edge2;:Ha,E;:He,E", "Edge3;:Ha,E;:He,E", "Edge4;:Ha,E;:He,E", - "Vertex1;:H8,V;:He,V", "Vertex2;:H8,V;:He,V", "Vertex3;:H8,V;:He,V", - "Vertex4;:H8,V;:He,V", "Vertex1;:H9,V;:He,V", "Vertex2;:H9,V;:He,V", - "Vertex3;:H9,V;:He,V", "Vertex4;:H9,V;:He,V", "Face1;:H1,F;:H7,F", - "Face1;:H2,F;:H7,F", "Face1;:H3,F;:H7,F", "Face1;:H4,F;:H7,F", - "Face1;:H5,F;:H7,F", "Face1;:H6,F;:H7,F", "Face1;:H8,F;:He,F", + "Vertex1;:H1,V;:H7,V", "Vertex2;:H1,V;:H7,V", "Vertex3;:H1,V;:H7,V", + "Vertex4;:H1,V;:H7,V", "Vertex1;:H2,V;:H7,V", "Vertex2;:H2,V;:H7,V", + "Vertex3;:H2,V;:H7,V", "Vertex4;:H2,V;:H7,V", "Face1;:H8,F;:He,F", "Face1;:H9,F;:He,F", "Face1;:Ha,F;:He,F", "Face1;:Hb,F;:He,F", - "Face1;:Hc,F;:He,F", "Face1;:Hd,F;:He,F", "Vertex1;:H1,V;:H7,V", - "Vertex2;:H1,V;:H7,V", "Vertex3;:H1,V;:H7,V", "Vertex4;:H1,V;:H7,V", - "Vertex1;:H2,V;:H7,V", "Vertex2;:H2,V;:H7,V", "Vertex3;:H2,V;:H7,V", - "Vertex4;:H2,V;:H7,V", + "Face1;:Hc,F;:He,F", "Face1;:Hd,F;:He,F", "Edge1;:H8,E;:He,E", + "Edge2;:H8,E;:He,E", "Edge3;:H8,E;:He,E", "Edge4;:H8,E;:He,E", + "Edge1;:H9,E;:He,E", "Edge2;:H9,E;:He,E", "Edge3;:H9,E;:He,E", + "Edge4;:H9,E;:He,E", "Edge1;:Ha,E;:He,E", "Edge2;:Ha,E;:He,E", + "Edge3;:Ha,E;:He,E", "Edge4;:Ha,E;:He,E", "Vertex1;:H8,V;:He,V", + "Vertex2;:H8,V;:He,V", "Vertex3;:H8,V;:He,V", "Vertex4;:H8,V;:He,V", + "Vertex1;:H9,V;:He,V", "Vertex2;:H9,V;:He,V", "Vertex3;:H9,V;:He,V", + "Vertex4;:H9,V;:He,V", "Edge1;:H1,E;:H7,E", "Edge2;:H1,E;:H7,E", + "Edge3;:H1,E;:H7,E", "Edge4;:H1,E;:H7,E", "Edge1;:H2,E;:H7,E", + "Edge2;:H2,E;:H7,E", "Edge3;:H2,E;:H7,E", "Edge4;:H2,E;:H7,E", + "Edge1;:H3,E;:H7,E", "Edge2;:H3,E;:H7,E", "Edge3;:H3,E;:H7,E", + "Edge4;:H3,E;:H7,E", "Face1;:H1,F;:H7,F", "Face1;:H2,F;:H7,F", + "Face1;:H3,F;:H7,F", "Face1;:H4,F;:H7,F", "Face1;:H5,F;:H7,F", + "Face1;:H6,F;:H7,F", })); } @@ -2282,4 +2282,68 @@ TEST_F(TopoShapeExpansionTest, makeElementBSplineFace) })); } +TEST_F(TopoShapeExpansionTest, replaceElementShape) +{ + // Arrange + auto [cube1, cube2] = CreateTwoTopoShapeCubes(); + // We can't use a compound in replaceElementShape, so we'll make a replacement wire and a shell + auto wire {BRepBuilderAPI_MakeWire( + BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 0.0, 0.0), gp_Pnt(1.0, 0.0, 0.0)), + BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 0.0, 0.0), gp_Pnt(1.0, 1.0, 0.0)), + BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 1.0, 0.0), gp_Pnt(0.0, 0.0, 0.0))) + .Wire()}; + auto shell = cube1.makeElementShell(); + auto wires = shell.getSubTopoShapes(TopAbs_WIRE); + // Act + TopoShape& result = shell.replaceElementShape(shell, {{wires[0], wire}}); + Base::BoundBox3d bb = result.getBoundBox(); + // Assert shape is correct + EXPECT_TRUE(PartTestHelpers::boxesMatch(bb, Base::BoundBox3d(0.0, 0.0, 0.0, 1.0, 1.0, 1.0))); + EXPECT_FLOAT_EQ(getArea(result.getShape()), 5); + EXPECT_EQ(result.countSubElements("Wire"), 6); + // Assert that we're creating a correct element map + EXPECT_TRUE(result.getMappedChildElements().empty()); + EXPECT_TRUE(elementsMatch( + result, + { + "Edge1", "Edge1;:H1,E", "Edge1;:H2,E", "Edge1;:H3,E", "Edge2", + "Edge2;:H1,E", "Edge2;:H2,E", "Edge2;:H3,E", "Edge3", "Edge3;:H1,E", + "Edge3;:H2,E", "Edge3;:H3,E", "Edge4;:H1,E", "Edge4;:H2,E", "Edge4;:H3,E", + "Face1;:H2,F", "Face1;:H3,F", "Face1;:H4,F", "Face1;:H5,F", "Face1;:H6,F", + "Vertex1", "Vertex1;:H1,V", "Vertex1;:H2,V", "Vertex2", "Vertex2;:H1,V", + "Vertex2;:H2,V", "Vertex3", "Vertex3;:H1,V", "Vertex3;:H2,V", "Vertex4;:H1,V", + "Vertex4;:H2,V", + })); +} + +TEST_F(TopoShapeExpansionTest, removeElementShape) +{ + // Arrange + auto [cube1, cube2] = CreateTwoTopoShapeCubes(); + auto faces = cube1.getSubTopoShapes(TopAbs_FACE); + // Act + TopoShape result = cube1.removeElementShape({faces[0]}); + Base::BoundBox3d bb = result.getBoundBox(); + // Assert shape is correct + EXPECT_TRUE(PartTestHelpers::boxesMatch(bb, Base::BoundBox3d(0.0, 0.0, 0.0, 1.0, 1.0, 1.0))); + EXPECT_FLOAT_EQ(getArea(result.getShape()), 5); + EXPECT_EQ(result.countSubShapes("Compound"), 1); + EXPECT_EQ(result.countSubShapes("Face"), 5); + // Assert that we're creating a correct element map + EXPECT_TRUE(result.getMappedChildElements().empty()); + EXPECT_TRUE( + elementsMatch(result, + { + "Edge1;:H1,E;:H7,E", "Edge1;:H2,E;:H7,E", "Edge1;:H3,E;:H7,E", + "Edge2;:H1,E;:H7,E", "Edge2;:H2,E;:H7,E", "Edge2;:H3,E;:H7,E", + "Edge3;:H1,E;:H7,E", "Edge3;:H2,E;:H7,E", "Edge3;:H3,E;:H7,E", + "Edge4;:H1,E;:H7,E", "Edge4;:H2,E;:H7,E", "Edge4;:H3,E;:H7,E", + "Face1;:H2,F;:H7,F", "Face1;:H3,F;:H7,F", "Face1;:H4,F;:H7,F", + "Face1;:H5,F;:H7,F", "Face1;:H6,F;:H7,F", "Vertex1;:H1,V;:H7,V", + "Vertex1;:H2,V;:H7,V", "Vertex2;:H1,V;:H7,V", "Vertex2;:H2,V;:H7,V", + "Vertex3;:H1,V;:H7,V", "Vertex3;:H2,V;:H7,V", "Vertex4;:H1,V;:H7,V", + "Vertex4;:H2,V;:H7,V", + })); +} + // NOLINTEND(readability-magic-numbers,cppcoreguidelines-avoid-magic-numbers)