diff --git a/src/Mod/Part/App/TopoShape.h b/src/Mod/Part/App/TopoShape.h index 573e4eb322..bf30436a82 100644 --- a/src/Mod/Part/App/TopoShape.h +++ b/src/Mod/Part/App/TopoShape.h @@ -638,6 +638,44 @@ public: void mapSubElement(const std::vector &shapes, const char *op=nullptr); bool hasPendingElementMap() const; + /** Helper class to return the generated and modified shape given an input shape + * + * Shape history information is extracted using OCCT APIs + * BRepBuilderAPI_MakeShape::Generated/Modified(). However, there is often + * some glitches in various derived class. So we use this class as an + * abstraction, and create various derived classes to deal with the glitches. + */ + struct PartExport Mapper { + /// Helper vector for temporary storage of both generated and modified shapes + mutable std::vector _res; + virtual ~Mapper() {} + /// Return a list of shape generated from the given input shape + virtual const std::vector &generated(const TopoDS_Shape &) const { + return _res; + } + /// Return a list of shape modified from the given input shape + virtual const std::vector &modified(const TopoDS_Shape &) const { + return _res; + } + }; + + /** Core function to generate mapped element names from shape history + * + * @param shape: the new shape + * @param mapper: for mapping input shapes to generated/modified shapes + * @param sources: list of source shapes. + * @param op: optional string to be encoded into topo naming for indicating + * the operation + * + * @return The original content of this TopoShape is discarded and replaced + * with the given 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 &makeShapeWithElementMap(const TopoDS_Shape &shape, + const Mapper &mapper, + const std::vector &sources, + const char *op=nullptr); /** Helper class to return the generated and modified shape given an input shape * diff --git a/src/Mod/Part/App/TopoShapeExpansion.cpp b/src/Mod/Part/App/TopoShapeExpansion.cpp index e7554ebaa8..be31389811 100644 --- a/src/Mod/Part/App/TopoShapeExpansion.cpp +++ b/src/Mod/Part/App/TopoShapeExpansion.cpp @@ -25,21 +25,23 @@ #include "PreCompiled.h" #ifndef _PreComp_ -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include +#include +#include #endif -#include -#include -#include #include "TopoShape.h" #include "TopoShapeCache.h" +#include "TopoShapeOpCode.h" #include "FaceMaker.h" @@ -338,6 +340,8 @@ void checkAndMatchHasher(TopoShape& topoShape1, const TopoShape& topoShape2) } } // namespace + +// TODO: Refactor mapSubElementTypeForShape to reduce complexity void TopoShape::mapSubElementTypeForShape(const TopoShape& other, TopAbs_ShapeEnum type, const char* op, @@ -387,7 +391,8 @@ void TopoShape::mapSubElementTypeForShape(const TopoShape& other, } std::ostringstream ss; char elementType {shapeName(type)[0]}; - if ( ! elementMap() ) { + if (!elementMap()) { + // NOLINTNEXTLINE FC_THROWM(NullShapeException, "No element map"); } elementMap()->encodeElementName(elementType, name, ss, &sids, Tag, op, other.Tag); @@ -509,6 +514,735 @@ void TopoShape::mapSubElement(const std::vector& shapes, const char* } } +struct ShapeInfo +{ + const TopoDS_Shape& shape; + TopoShapeCache::Ancestry& cache; + TopAbs_ShapeEnum type; + const char* shapetype; + + ShapeInfo(const TopoDS_Shape& shape, TopAbs_ShapeEnum type, TopoShapeCache::Ancestry& cache) + : shape(shape) + , cache(cache) + , type(type) + , shapetype(TopoShape::shapeName(type).c_str()) + {} + + int count() const + { + return cache.count(); + } + + TopoDS_Shape find(int index) + { + return cache.find(shape, index); + } + + int find(const TopoDS_Shape& subshape) + { + return cache.find(shape, subshape); + } +}; + +//////////////////////////////////////// +// makESHAPE -> makeShapeWithElementMap +/////////////////////////////////////// + +struct NameKey +{ + Data::MappedName name; + long tag = 0; + int shapetype = 0; + + NameKey() + = default; + explicit NameKey(const Data::MappedName& n) + : name(n) + {} + NameKey(int type, Data::MappedName n) + : name(std::move(n)) + { + // Order the shape type from vertex < edge < face < other. We'll rely + // on this for sorting when we name the geometry element. + switch (type) { + case TopAbs_VERTEX: + shapetype = 0; + break; + case TopAbs_EDGE: + shapetype = 1; + break; + case TopAbs_FACE: + shapetype = 2; + break; + default: + shapetype = 3; + } + } + bool operator<(const NameKey& other) const + { + if (shapetype < other.shapetype) { + return true; + } + if (shapetype > other.shapetype) { + return false; + } + if (tag < other.tag) { + return true; + } + if (tag > other.tag) { + return false; + } + return name < other.name; + } +}; + +struct NameInfo +{ + int index{}; + Data::ElementIDRefs sids; + const char* shapetype{}; +}; + + +const std::string& modPostfix() +{ + static std::string postfix(TopoShape::elementMapPrefix() + ":M"); + return postfix; +} + +const std::string& modgenPostfix() +{ + static std::string postfix(modPostfix() + "G"); + return postfix; +} + +const std::string& genPostfix() +{ + static std::string postfix(TopoShape::elementMapPrefix() + ":G"); + return postfix; +} + +const std::string& upperPostfix() +{ + static std::string postfix(TopoShape::elementMapPrefix() + ":U"); + return postfix; +} + +const std::string& lowerPostfix() +{ + static std::string postfix(TopoShape::elementMapPrefix() + ":L"); + return postfix; +} + +// TODO: Refactor makeShapeWithElementMap to reduce complexity +TopoShape& TopoShape::makeShapeWithElementMap(const TopoDS_Shape& shape, + const Mapper& mapper, + const std::vector& shapes, + const char* op) +{ + setShape(shape); + if (shape.IsNull()) { + FC_THROWM(NullShapeException, "Null shape"); + } + + if (shapes.empty()) { + return *this; + } + + size_t canMap = 0; + for (auto& incomingShape : shapes) { + if (canMapElement(incomingShape)) { + ++canMap; + } + } + if (canMap == 0U) { + return *this; + } + if (canMap != shapes.size() && FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + FC_WARN("Not all input shapes are mappable"); // NOLINT + } + + if (!op) { + op = Part::OpCodes::Maker; + } + std::string _op = op; + _op += '_'; + + initCache(); + ShapeInfo vinfo(_Shape, TopAbs_VERTEX, _cache->getAncestry(TopAbs_VERTEX)); + ShapeInfo einfo(_Shape, TopAbs_EDGE, _cache->getAncestry(TopAbs_EDGE)); + ShapeInfo finfo(_Shape, TopAbs_FACE, _cache->getAncestry(TopAbs_FACE)); + mapSubElement(shapes, op); + + std::array infos = {&vinfo, &einfo, &finfo}; + + std::array infoMap{}; + infoMap[TopAbs_VERTEX] = &vinfo; + infoMap[TopAbs_EDGE] = &einfo; + infoMap[TopAbs_WIRE] = &einfo; + infoMap[TopAbs_FACE] = &finfo; + infoMap[TopAbs_SHELL] = &finfo; + infoMap[TopAbs_SOLID] = &finfo; + infoMap[TopAbs_COMPOUND] = &finfo; + infoMap[TopAbs_COMPSOLID] = &finfo; + + std::ostringstream ss; + std::string postfix; + Data::MappedName newName; + + std::map> newNames; + + // First, collect names from other shapes that generates or modifies the + // new shape + for (auto& pinfo : infos) { + auto& info = *pinfo; + for (const auto & incomingShape : shapes) { + if (!canMapElement(incomingShape)) { + continue; + } + auto& otherMap = incomingShape._cache->getAncestry(info.type); + if (otherMap.count() == 0) { + continue; + } + + for (int i = 1; i <= otherMap.count(); i++) { + const auto& otherElement = otherMap.find(incomingShape._Shape, i); + // Find all new objects that are a modification of the old object + Data::ElementIDRefs sids; + NameKey key(info.type, + incomingShape.getMappedName(Data::IndexedName::fromConst(info.shapetype, i), + true, + &sids)); + + int newShapeCounter = 0; + for (auto& newShape : mapper.modified(otherElement)) { + ++newShapeCounter; + if (newShape.ShapeType() >= TopAbs_SHAPE) { + // NOLINTNEXTLINE + FC_ERR("unknown modified shape type " << newShape.ShapeType() << " from " + << info.shapetype << i); + continue; + } + auto& newInfo = *infoMap[newShape.ShapeType()]; + if (newInfo.type != newShape.ShapeType()) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + // TODO: it seems modified shape may report higher + // level shape type just like generated shape below. + // Maybe we shall do the same for name construction. + // NOLINTNEXTLINE + FC_WARN("modified shape type " << shapeName(newShape.ShapeType()) + << " mismatch with " << info.shapetype + << i); + } + continue; + } + int newShapeIndex = newInfo.find(newShape); + if (newShapeIndex == 0) { + // This warning occurs in makERevolve. It generates + // some shape from a vertex that never made into the + // final shape. There may be incomingShape cases there. + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + // NOLINTNEXTLINE + FC_WARN("Cannot find " << op << " modified " << newInfo.shapetype + << " from " << info.shapetype << i); + } + continue; + } + + Data::IndexedName element = Data::IndexedName::fromConst(newInfo.shapetype, newShapeIndex); + if (getMappedName(element)) { + continue; + } + + key.tag = incomingShape.Tag; + auto& name_info = newNames[element][key]; + name_info.sids = sids; + name_info.index = newShapeCounter; + name_info.shapetype = info.shapetype; + } + + int checkParallel = -1; + gp_Pln pln; + + // Find all new objects that were generated from an old object + // (e.g. a face generated from an edge) + newShapeCounter = 0; + for (auto& newShape : mapper.generated(otherElement)) { + if (newShape.ShapeType() >= TopAbs_SHAPE) { + // NOLINTNEXTLINE + FC_ERR("unknown generated shape type " << newShape.ShapeType() << " from " + << info.shapetype << i); + continue; + } + + int parallelFace = -1; + int coplanarFace = -1; + auto& newInfo = *infoMap[newShape.ShapeType()]; + std::vector newShapes; + int shapeOffset = 0; + if (newInfo.type == newShape.ShapeType()) { + newShapes.push_back(newShape); + } + else { + // It is possible for the maker to report generating a + // higher level shape, such as shell or solid. For + // example, when extruding, OCC will report the + // extruding face generating the entire solid. However, + // it will also report the edges of the extruding face + // generating the side faces. In this case, too much + // information is bad for us. We don't want the name of + // the side face (and its edges) to be coupled with + // incomingShape (unrelated) edges in the extruding face. + // + // shapeOffset below is used to make sure the higher + // level mapped names comes late after sorting. We'll + // ignore those names if there are more precise mapping + // available. + shapeOffset = 3; + + if (info.type == TopAbs_FACE && checkParallel < 0) { + if (!TopoShape(otherElement).findPlane(pln)) { + checkParallel = 0; + } + else { + checkParallel = 1; + } + } + for (TopExp_Explorer xp(newShape, newInfo.type); xp.More(); xp.Next()) { + newShapes.push_back(xp.Current()); + + if ((parallelFace < 0 || coplanarFace < 0) && checkParallel > 0) { + // Specialized checking for high level mapped + // face that are either coplanar or parallel + // with the source face, which are common in + // operations like extrusion. Once found, the + // first coplanar face will assign an index of + // INT_MIN+1, and the first parallel face + // INT_MIN. The purpose of these special + // indexing is to make the name more stable for + // those generated faces. + // + // For example, the top or bottom face of an + // extrusion will be named using the extruding + // face. With a fixed index, the name is no + // longer affected by adding/removing of holes + // inside the extruding face/sketch. + gp_Pln plnOther; + if (TopoShape(newShapes.back()).findPlane(plnOther)) { + if (pln.Axis().IsParallel(plnOther.Axis(), + Precision::Angular())) { + if (coplanarFace < 0) { + gp_Vec vec(pln.Axis().Location(), + plnOther.Axis().Location()); + Standard_Real D1 = + gp_Vec(pln.Axis().Direction()).Dot(vec); + if (D1 < 0) { + D1 = -D1; + } + Standard_Real D2 = + gp_Vec(plnOther.Axis().Direction()).Dot(vec); + if (D2 < 0) { + D2 = -D2; + } + if (D1 <= Precision::Confusion() + && D2 <= Precision::Confusion()) { + coplanarFace = (int)newShapes.size(); + continue; + } + } + if (parallelFace < 0) { + parallelFace = (int)newShapes.size(); + } + } + } + } + } + } + key.shapetype += shapeOffset; + for (auto& workingShape : newShapes) { + ++newShapeCounter; + int workingShapeIndex = newInfo.find(workingShape); + if (!workingShapeIndex) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + FC_WARN("Cannot find " << op << " generated " << newInfo.shapetype + << " from " << info.shapetype << i); + } + continue; + } + + Data::IndexedName element = + Data::IndexedName::fromConst(newInfo.shapetype, workingShapeIndex); + auto mapped = getMappedName(element); + if (mapped) { + continue; + } + + key.tag = incomingShape.Tag; + auto& name_info = newNames[element][key]; + name_info.sids = sids; + if (newShapeCounter == parallelFace) { + name_info.index = std::numeric_limits::min(); + } + else if (newShapeCounter == coplanarFace) { + name_info.index = std::numeric_limits::min() + 1; + } + else { + name_info.index = -newShapeCounter; + } + name_info.shapetype = info.shapetype; + } + key.shapetype -= shapeOffset; + } + } + } + } + + // We shall first exclude those names generated from high level mapping. If + // there are still any unnamed elements left after we go through the process + // below, we set delayed=true, and start using those excluded names. + bool delayed = false; + + while (true) { + + // Construct the names for modification/generation info collected in + // the previous step + for (auto itName = newNames.begin(), itNext = itName; itNext != newNames.end(); + itName = itNext) { + // We treat the first modified/generated source shape name specially. + // If case there are more than one source shape. We hash the first + // source name separately, and then obtain the second string id by + // hashing all the source names together. We then use the second + // string id as the postfix for our name. + // + // In this way, we can associate the same source that are modified by + // multiple other shapes. + + ++itNext; + + auto& element = itName->first; + auto& names = itName->second; + const auto& first_key = names.begin()->first; + auto& first_info = names.begin()->second; + + if (!delayed && first_key.shapetype >= 3 && first_info.index > INT_MIN + 1) { + // This name is mapped from high level (shell, solid, etc.) + // Delay till next round. + // + // index>INT_MAX+1 is for checking generated coplanar and + // parallel face mapping, which has special fixed index to make + // name stable. These names are not delayed. + continue; + } + if (!delayed && getMappedName(element)) { + newNames.erase(itName); + continue; + } + + int name_type = + first_info.index > 0 ? 1 : 2; // index>0 means modified, or else generated + Data::MappedName first_name = first_key.name; + + Data::ElementIDRefs sids(first_info.sids); + + postfix.clear(); + if (names.size() > 1) { + ss.str(""); + ss << '('; + bool first = true; + auto it = names.begin(); + int count = 0; + for (++it; it != names.end(); ++it) { + auto& other_key = it->first; + if (other_key.shapetype >= 3 && first_key.shapetype < 3) { + // shapetype>=3 means it's a high level mapping (e.g. a face + // generates a solid). We don't want that if there are more + // precise low level mapping available. See comments above + // for more details. + break; + } + if (first) { + first = false; + } + else { + ss << '|'; + } + auto& other_info = it->second; + std::ostringstream ss2; + if (other_info.index != 1) { + // 'K' marks the additional source shape of this + // generate (or modified) shape. + ss2 << elementMapPrefix() << 'K'; + if (other_info.index == INT_MIN) { + ss2 << '0'; + } + else if (other_info.index == INT_MIN + 1) { + ss2 << "00"; + } + else { + // The same source shape may generate or modify + // more than one shape. The index here marks the + // position it is reported by OCC. Including the + // index here is likely to degrade name stablilty, + // but is unfortunately a necessity to avoid + // duplicate names. + ss2 << other_info.index; + } + } + Data::MappedName other_name = other_key.name; + elementMap()->encodeElementName(other_info.shapetype[0], + other_name, + ss2, + &sids, + Tag, + 0, + other_key.tag); + ss << other_name; + if ((name_type == 1 && other_info.index < 0) + || (name_type == 2 && other_info.index > 0)) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + // NOLINTNEXTLINE + FC_WARN("element is both generated and modified"); + } + name_type = 0; + } + sids += other_info.sids; + // To avoid the name becoming to long, just put some limit here + if (++count == 4) { + break; + } + } + if (!first) { + ss << ')'; + if (Hasher) { + sids.push_back(Hasher->getID(ss.str().c_str())); + ss.str(""); + ss << sids.back().toString(); + } + postfix = ss.str(); + } + } + + ss.str(""); + if (name_type == 2) { + ss << genPostfix(); + } + else if (name_type == 1) { + ss << modPostfix(); + } + else { + ss << modgenPostfix(); + } + if (first_info.index == INT_MIN) { + ss << '0'; + } + else if (first_info.index == INT_MIN + 1) { + ss << "00"; + } + else if (abs(first_info.index) > 1) { + ss << abs(first_info.index); + } + ss << postfix; + elementMap() + ->encodeElementName(element[0], first_name, ss, &sids, Tag, op, first_key.tag); + elementMap()->setElementName(element, first_name, Tag, &sids); + + if (!delayed && first_key.shapetype < 3) { + newNames.erase(itName); + } + } + + // The reverse pass. Starting from the highest level element, i.e. + // Face, for any element that are named, assign names for its lower unnamed + // elements. For example, if Edge1 is named E1, and its vertexes are not + // named, then name them as E1;U1, E1;U2, etc. + // + // In order to make the name as stable as possible, we may assign multiple + // names (which must be sorted, because we may use the first one to name + // upper element in the final pass) to lower element if it appears in + // multiple higher elements, e.g. same edge in multiple faces. + + for (size_t infoIndex = infos.size() - 1; infoIndex != 0; --infoIndex) { + std::map> + names; + auto& info = *infos[infoIndex]; + auto& next = *infos[infoIndex - 1]; + int elementCounter = 1; + auto it = newNames.end(); + if (delayed) { + it = newNames.upper_bound(Data::IndexedName::fromConst(info.shapetype, 0)); + } + for (;; ++elementCounter) { + Data::IndexedName element; + if (!delayed) { + if (elementCounter > info.count()) { + break; + } + element = Data::IndexedName::fromConst(info.shapetype, elementCounter); + if (newNames.count(element) != 0U) { + continue; + } + } + else if (it == newNames.end() + || !boost::starts_with(it->first.getType(), info.shapetype)) { + break; + } + else { + element = it->first; + ++it; + elementCounter = element.getIndex(); + if (elementCounter == 0 || elementCounter > info.count()) { + continue; + } + } + Data::ElementIDRefs sids; + Data::MappedName mapped = getMappedName(element, false, &sids); + if (!mapped) { + continue; + } + + TopTools_IndexedMapOfShape submap; + TopExp::MapShapes(info.find(elementCounter), next.type, submap); + for (int submapIndex = 1, infoCounter = 1; submapIndex <= submap.Extent(); ++submapIndex) { + ss.str(""); + int elementIndex = next.find(submap(submapIndex)); + assert(elementIndex); + Data::IndexedName indexedName = Data::IndexedName::fromConst(next.shapetype, elementIndex); + if (getMappedName(indexedName)) { + continue; + } + auto& infoRef = names[indexedName][mapped]; + infoRef.index = infoCounter++; + infoRef.sids = sids; + } + } + // Assign the actual names + for (auto& actualName : names) { +#ifndef FC_ELEMENT_MAP_ALL + // Do we really want multiple names for an element in this case? + // If not, we just pick the name in the first sorting order here. + auto& name = *actualName.second.begin(); +#else + for (auto& name : actualName.second) +#endif + { + auto& infoRef = name.second; + auto& sids = infoRef.sids; + newName = name.first; + ss.str(""); + ss << upperPostfix(); + if (infoRef.index > 1) { + ss << infoRef.index; + } + elementMap()->encodeElementName(actualName.first[0], newName, ss, &sids, Tag, op); + elementMap()->setElementName(actualName.first, newName, Tag, &sids); + } + } + } + + // The forward pass. For any elements that are not named, try construct its + // name from the lower elements + bool hasUnnamed = false; + for (size_t ifo = 1; ifo < infos.size(); ++ifo) { + auto& info = *infos[ifo]; + auto& prev = *infos[ifo - 1]; + for (int i = 1; i <= info.count(); ++i) { + Data::IndexedName element = Data::IndexedName::fromConst(info.shapetype, i); + if (getMappedName(element)) { + continue; + } + + Data::ElementIDRefs sids; + std::map names; + TopExp_Explorer xp; + if (info.type == TopAbs_FACE) { + xp.Init(BRepTools::OuterWire(TopoDS::Face(info.find(i))), TopAbs_EDGE); + } + else { + xp.Init(info.find(i), prev.type); + } + for (; xp.More(); xp.Next()) { + int j = prev.find(xp.Current()); + assert(j); + Data::IndexedName prevElement = Data::IndexedName::fromConst(prev.shapetype, j); + if (!delayed && (newNames.count(prevElement) != 0U)) { + names.clear(); + break; + } + Data::ElementIDRefs sid; + Data::MappedName name = getMappedName(prevElement, false, &sid); + if (!name) { + // only assign name if all lower elements are named + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + // NOLINTNEXTLINE + FC_WARN("unnamed lower element " << prevElement); + } + names.clear(); + break; + } + auto res = names.emplace(name, prevElement); + if (res.second) { + sids += sid; + } + else if (prevElement != res.first->second) { + // The seam edge will appear twice, which is normal. We + // only warn if the mapped element names are different. + // NOLINTNEXTLINE + FC_WARN("lower element " << prevElement << " and " << res.first->second + << " has duplicated name " << name << " for " + << info.shapetype << i); + } + } + if (names.empty()) { + hasUnnamed = true; + continue; + } + auto it = names.begin(); + newName = it->first; + if (names.size() == 1) { + ss << lowerPostfix(); + } + else { + bool first = true; + ss.str(""); + if (!Hasher) { + ss << lowerPostfix(); + } + ss << '('; + int count = 0; + for (++it; it != names.end(); ++it) { + if (first) { + first = false; + } + else { + ss << '|'; + } + ss << it->first; + + // To avoid the name becoming to long, just put some limit here + if (++count == 4) { + break; + } + } + ss << ')'; + if (Hasher) { + sids.push_back(Hasher->getID(ss.str().c_str())); + ss.str(""); + ss << lowerPostfix() << sids.back().toString(); + } + } + elementMap()->encodeElementName(element[0], newName, ss, &sids, Tag, op); + elementMap()->setElementName(element, newName, Tag, &sids); + } + } + if (!hasUnnamed || delayed || newNames.empty()) { + break; + } + delayed = true; + } + return *this; +} + namespace { void addShapesToBuilder(const std::vector& shapes, @@ -634,7 +1368,8 @@ TopoShape& TopoShape::makeElementFace(const std::vector& shapes, /** * Encode and set an element name in the elementMap. If a hasher is defined, apply it to the name. * - * @param element The element name(type) that provides 1 one character suffix to the name IF . + * @param element The element name(type) that provides 1 one character suffix to the name IF + * . * @param names The subnames to build the name from. If empty, return the TopoShape MappedName. * @param marker The elementMap name or suffix to start the name with. If null, use the * elementMapPrefix.